diff --git a/packages/arcade_lobby/seed/scripts/db/operations.lua b/packages/arcade_lobby/seed/scripts/db/operations.lua new file mode 100644 index 000000000..193d9e654 --- /dev/null +++ b/packages/arcade_lobby/seed/scripts/db/operations.lua @@ -0,0 +1,394 @@ +-- arcade_lobby/seed/scripts/db/operations.lua +-- DBAL operations for Game and Leaderboard entities +-- @module arcade_lobby.db.operations + +local M = {} +local json = require('json') + +--------------------------------------------------------------------------- +-- GAME OPERATIONS +--------------------------------------------------------------------------- + +---@class GameCreateParams +---@field tenantId string +---@field name string +---@field slug string +---@field description string|nil +---@field thumbnail string|nil +---@field category string +---@field minPlayers number +---@field maxPlayers number +---@field config table|nil Game configuration + +---Create a new game entry +---@param dbal table DBAL client instance +---@param params GameCreateParams +---@return table Created game +function M.createGame(dbal, params) + return dbal:create('Game', { + tenantId = params.tenantId, + name = params.name, + slug = params.slug or M._slugify(params.name), + description = params.description, + thumbnail = params.thumbnail, + category = params.category, + minPlayers = params.minPlayers or 1, + maxPlayers = params.maxPlayers or 1, + config = params.config and json.encode(params.config) or '{}', + isActive = true, + playCount = 0, + createdAt = os.time() * 1000, + updatedAt = os.time() * 1000, + }) +end + +---Get game by ID +---@param dbal table +---@param gameId string +---@return table|nil Game +function M.getGame(dbal, gameId) + local game = dbal:read('Game', gameId) + if game and game.config then + game.config = json.decode(game.config) + end + return game +end + +---Get game by slug +---@param dbal table +---@param tenantId string +---@param slug string +---@return table|nil Game +function M.getGameBySlug(dbal, tenantId, slug) + local game = dbal:findFirst('Game', { + where = { tenantId = tenantId, slug = slug }, + }) + if game and game.config then + game.config = json.decode(game.config) + end + return game +end + +---List all games +---@param dbal table +---@param tenantId string +---@param category string|nil Filter by category +---@param take number|nil +---@return table[] Games +function M.listGames(dbal, tenantId, category, take) + local where = { tenantId = tenantId, isActive = true } + + local result = dbal:list('Game', { + where = where, + orderBy = { playCount = 'desc' }, + take = take or 50, + }) + + local games = result.items or {} + + -- Filter by category if specified + if category then + local filtered = {} + for _, game in ipairs(games) do + if game.category == category then + table.insert(filtered, game) + end + end + games = filtered + end + + return games +end + +---Increment play count +---@param dbal table +---@param gameId string +function M.incrementPlayCount(dbal, gameId) + local game = M.getGame(dbal, gameId) + if game then + dbal:update('Game', gameId, { + playCount = (game.playCount or 0) + 1, + }) + end +end + +---Update game +---@param dbal table +---@param gameId string +---@param updates table +---@return table Updated game +function M.updateGame(dbal, gameId, updates) + if updates.config and type(updates.config) == 'table' then + updates.config = json.encode(updates.config) + end + updates.updatedAt = os.time() * 1000 + return dbal:update('Game', gameId, updates) +end + +---Deactivate game +---@param dbal table +---@param gameId string +function M.deactivateGame(dbal, gameId) + return M.updateGame(dbal, gameId, { isActive = false }) +end + +--------------------------------------------------------------------------- +-- LEADERBOARD OPERATIONS +--------------------------------------------------------------------------- + +---@class LeaderboardEntryParams +---@field tenantId string +---@field gameId string +---@field userId string +---@field username string +---@field score number +---@field metadata table|nil Additional game-specific data + +---Submit a score +---@param dbal table +---@param params LeaderboardEntryParams +---@return table Created entry +function M.submitScore(dbal, params) + return dbal:create('LeaderboardEntry', { + tenantId = params.tenantId, + gameId = params.gameId, + userId = params.userId, + username = params.username, + score = params.score, + metadata = params.metadata and json.encode(params.metadata) or nil, + submittedAt = os.time() * 1000, + }) +end + +---Get high scores for a game +---@param dbal table +---@param gameId string +---@param take number|nil +---@return table[] Sorted entries +function M.getHighScores(dbal, gameId, take) + local result = dbal:list('LeaderboardEntry', { + where = { gameId = gameId }, + orderBy = { score = 'desc' }, + take = take or 10, + }) + return result.items or {} +end + +---Get user's best score for a game +---@param dbal table +---@param gameId string +---@param userId string +---@return table|nil Best entry +function M.getUserBestScore(dbal, gameId, userId) + local result = dbal:list('LeaderboardEntry', { + where = { gameId = gameId, userId = userId }, + orderBy = { score = 'desc' }, + take = 1, + }) + + if result.items and #result.items > 0 then + return result.items[1] + end + return nil +end + +---Get user's rank on leaderboard +---@param dbal table +---@param gameId string +---@param userId string +---@return number|nil Rank (1-based) or nil if no score +function M.getUserRank(dbal, gameId, userId) + local userBest = M.getUserBestScore(dbal, gameId, userId) + if not userBest then + return nil + end + + local allScores = M.getHighScores(dbal, gameId, 10000) + + for rank, entry in ipairs(allScores) do + if entry.userId == userId then + return rank + end + end + + return nil +end + +---Get leaderboard with user's position highlighted +---@param dbal table +---@param gameId string +---@param userId string +---@param around number|nil How many entries to show around user +---@return table Leaderboard data +function M.getLeaderboardWithUser(dbal, gameId, userId, around) + around = around or 3 + local topScores = M.getHighScores(dbal, gameId, 10) + local userRank = M.getUserRank(dbal, gameId, userId) + + return { + top = topScores, + userRank = userRank, + userBest = M.getUserBestScore(dbal, gameId, userId), + } +end + +--------------------------------------------------------------------------- +-- GAME SESSION OPERATIONS +--------------------------------------------------------------------------- + +---@class GameSessionParams +---@field tenantId string +---@field gameId string +---@field hostId string +---@field maxPlayers number + +---Create a game session (multiplayer lobby) +---@param dbal table +---@param params GameSessionParams +---@return table Created session +function M.createSession(dbal, params) + return dbal:create('GameSession', { + tenantId = params.tenantId, + gameId = params.gameId, + hostId = params.hostId, + status = 'waiting', + maxPlayers = params.maxPlayers, + currentPlayers = 1, + players = json.encode({ params.hostId }), + createdAt = os.time() * 1000, + }) +end + +---Get session by ID +---@param dbal table +---@param sessionId string +---@return table|nil Session +function M.getSession(dbal, sessionId) + local session = dbal:read('GameSession', sessionId) + if session and session.players then + session.players = json.decode(session.players) + end + return session +end + +---List active sessions for a game +---@param dbal table +---@param gameId string +---@return table[] Sessions +function M.listActiveSessions(dbal, gameId) + local result = dbal:list('GameSession', { + where = { gameId = gameId, status = 'waiting' }, + orderBy = { createdAt = 'desc' }, + take = 20, + }) + + local sessions = result.items or {} + for _, session in ipairs(sessions) do + if session.players then + session.players = json.decode(session.players) + end + end + + return sessions +end + +---Join a session +---@param dbal table +---@param sessionId string +---@param userId string +---@return table|nil Updated session or nil if full +function M.joinSession(dbal, sessionId, userId) + local session = M.getSession(dbal, sessionId) + if not session then + error('Session not found: ' .. sessionId) + end + + if session.currentPlayers >= session.maxPlayers then + return nil -- Session full + end + + local players = session.players or {} + + -- Check if already in session + for _, pid in ipairs(players) do + if pid == userId then + return session + end + end + + table.insert(players, userId) + + return dbal:update('GameSession', sessionId, { + players = json.encode(players), + currentPlayers = #players, + }) +end + +---Leave a session +---@param dbal table +---@param sessionId string +---@param userId string +function M.leaveSession(dbal, sessionId, userId) + local session = M.getSession(dbal, sessionId) + if not session then + return + end + + local players = session.players or {} + local newPlayers = {} + + for _, pid in ipairs(players) do + if pid ~= userId then + table.insert(newPlayers, pid) + end + end + + if #newPlayers == 0 then + -- No players left, delete session + dbal:delete('GameSession', sessionId) + else + dbal:update('GameSession', sessionId, { + players = json.encode(newPlayers), + currentPlayers = #newPlayers, + hostId = newPlayers[1], -- Transfer host if needed + }) + end +end + +---Start a session +---@param dbal table +---@param sessionId string +function M.startSession(dbal, sessionId) + return dbal:update('GameSession', sessionId, { + status = 'playing', + startedAt = os.time() * 1000, + }) +end + +---End a session +---@param dbal table +---@param sessionId string +function M.endSession(dbal, sessionId) + return dbal:update('GameSession', sessionId, { + status = 'finished', + endedAt = os.time() * 1000, + }) +end + +--------------------------------------------------------------------------- +-- UTILITY FUNCTIONS +--------------------------------------------------------------------------- + +---Generate URL-safe slug +---@param text string +---@return string +function M._slugify(text) + local slug = text:lower() + slug = slug:gsub('[^%w%s-]', '') + slug = slug:gsub('%s+', '-') + slug = slug:gsub('-+', '-') + slug = slug:gsub('^-', ''):gsub('-$', '') + return slug:sub(1, 50) +end + +return M diff --git a/packages/dashboard/seed/scripts/db/operations.lua b/packages/dashboard/seed/scripts/db/operations.lua new file mode 100644 index 000000000..29a0340ff --- /dev/null +++ b/packages/dashboard/seed/scripts/db/operations.lua @@ -0,0 +1,375 @@ +-- dashboard/seed/scripts/db/operations.lua +-- DBAL operations for Dashboard widgets and layouts +-- @module dashboard.db.operations + +local M = {} +local json = require('json') + +--------------------------------------------------------------------------- +-- DASHBOARD LAYOUT OPERATIONS +--------------------------------------------------------------------------- + +---@class DashboardLayoutParams +---@field tenantId string +---@field userId string|nil User-specific or tenant-wide +---@field name string Layout name +---@field widgets table[] Widget configurations +---@field settings table|nil Layout settings + +---Save dashboard layout +---@param dbal table DBAL client instance +---@param params DashboardLayoutParams +---@return table Created/updated layout +function M.saveLayout(dbal, params) + local where = { + tenantId = params.tenantId, + name = params.name, + } + + if params.userId then + where.userId = params.userId + end + + local existing = dbal:findFirst('DashboardLayout', { where = where }) + + if existing then + return dbal:update('DashboardLayout', existing.id, { + widgets = json.encode(params.widgets), + settings = params.settings and json.encode(params.settings) or nil, + updatedAt = os.time() * 1000, + }) + end + + return dbal:create('DashboardLayout', { + tenantId = params.tenantId, + userId = params.userId, + name = params.name, + widgets = json.encode(params.widgets), + settings = params.settings and json.encode(params.settings) or '{}', + isDefault = false, + createdAt = os.time() * 1000, + updatedAt = os.time() * 1000, + }) +end + +---Get layout by name +---@param dbal table +---@param tenantId string +---@param name string +---@param userId string|nil +---@return table|nil Layout +function M.getLayout(dbal, tenantId, name, userId) + local where = { tenantId = tenantId, name = name } + + if userId then + where.userId = userId + end + + local layout = dbal:findFirst('DashboardLayout', { where = where }) + + if layout then + layout.widgets = json.decode(layout.widgets or '[]') + layout.settings = json.decode(layout.settings or '{}') + end + + return layout +end + +---Get user's default layout +---@param dbal table +---@param tenantId string +---@param userId string +---@return table|nil Layout +function M.getDefaultLayout(dbal, tenantId, userId) + -- First try user's default + local userDefault = dbal:findFirst('DashboardLayout', { + where = { tenantId = tenantId, userId = userId, isDefault = true }, + }) + + if userDefault then + userDefault.widgets = json.decode(userDefault.widgets or '[]') + userDefault.settings = json.decode(userDefault.settings or '{}') + return userDefault + end + + -- Fall back to tenant default + local tenantDefault = dbal:findFirst('DashboardLayout', { + where = { tenantId = tenantId, isDefault = true }, + }) + + if tenantDefault then + tenantDefault.widgets = json.decode(tenantDefault.widgets or '[]') + tenantDefault.settings = json.decode(tenantDefault.settings or '{}') + return tenantDefault + end + + return nil +end + +---List all layouts +---@param dbal table +---@param tenantId string +---@param userId string|nil +---@return table[] Layouts +function M.listLayouts(dbal, tenantId, userId) + local where = { tenantId = tenantId } + + if userId then + where.userId = userId + end + + local result = dbal:list('DashboardLayout', { + where = where, + orderBy = { name = 'asc' }, + take = 50, + }) + + return result.items or {} +end + +---Set layout as default +---@param dbal table +---@param tenantId string +---@param layoutId string +---@param userId string|nil +function M.setDefault(dbal, tenantId, layoutId, userId) + -- Clear existing defaults + local layouts = M.listLayouts(dbal, tenantId, userId) + for _, layout in ipairs(layouts) do + if layout.isDefault then + dbal:update('DashboardLayout', layout.id, { isDefault = false }) + end + end + + return dbal:update('DashboardLayout', layoutId, { + isDefault = true, + updatedAt = os.time() * 1000, + }) +end + +---Delete layout +---@param dbal table +---@param layoutId string +---@return boolean Success +function M.deleteLayout(dbal, layoutId) + return dbal:delete('DashboardLayout', layoutId) +end + +--------------------------------------------------------------------------- +-- WIDGET OPERATIONS +--------------------------------------------------------------------------- + +---@class WidgetConfig +---@field id string Unique widget ID +---@field type string Widget type (stats, chart, list, etc.) +---@field title string Widget title +---@field x number Grid x position +---@field y number Grid y position +---@field w number Grid width +---@field h number Grid height +---@field config table Widget-specific config + +---Add widget to layout +---@param dbal table +---@param layoutId string +---@param widget WidgetConfig +---@return table Updated layout +function M.addWidget(dbal, layoutId, widget) + local layout = dbal:read('DashboardLayout', layoutId) + if not layout then + error('Layout not found: ' .. layoutId) + end + + local widgets = json.decode(layout.widgets or '[]') + + -- Generate ID if not provided + if not widget.id then + widget.id = 'widget_' .. os.time() .. '_' .. math.random(1000, 9999) + end + + table.insert(widgets, widget) + + return dbal:update('DashboardLayout', layoutId, { + widgets = json.encode(widgets), + updatedAt = os.time() * 1000, + }) +end + +---Update widget in layout +---@param dbal table +---@param layoutId string +---@param widgetId string +---@param updates table +---@return table Updated layout +function M.updateWidget(dbal, layoutId, widgetId, updates) + local layout = dbal:read('DashboardLayout', layoutId) + if not layout then + error('Layout not found: ' .. layoutId) + end + + local widgets = json.decode(layout.widgets or '[]') + + for i, widget in ipairs(widgets) do + if widget.id == widgetId then + for key, value in pairs(updates) do + widgets[i][key] = value + end + break + end + end + + return dbal:update('DashboardLayout', layoutId, { + widgets = json.encode(widgets), + updatedAt = os.time() * 1000, + }) +end + +---Remove widget from layout +---@param dbal table +---@param layoutId string +---@param widgetId string +---@return table Updated layout +function M.removeWidget(dbal, layoutId, widgetId) + local layout = dbal:read('DashboardLayout', layoutId) + if not layout then + error('Layout not found: ' .. layoutId) + end + + local widgets = json.decode(layout.widgets or '[]') + local newWidgets = {} + + for _, widget in ipairs(widgets) do + if widget.id ~= widgetId then + table.insert(newWidgets, widget) + end + end + + return dbal:update('DashboardLayout', layoutId, { + widgets = json.encode(newWidgets), + updatedAt = os.time() * 1000, + }) +end + +---Update widget positions (after drag) +---@param dbal table +---@param layoutId string +---@param positions table[] Array of {id, x, y, w, h} +---@return table Updated layout +function M.updatePositions(dbal, layoutId, positions) + local layout = dbal:read('DashboardLayout', layoutId) + if not layout then + error('Layout not found: ' .. layoutId) + end + + local widgets = json.decode(layout.widgets or '[]') + + -- Create lookup map + local posMap = {} + for _, pos in ipairs(positions) do + posMap[pos.id] = pos + end + + -- Update positions + for i, widget in ipairs(widgets) do + local pos = posMap[widget.id] + if pos then + widgets[i].x = pos.x + widgets[i].y = pos.y + widgets[i].w = pos.w or widgets[i].w + widgets[i].h = pos.h or widgets[i].h + end + end + + return dbal:update('DashboardLayout', layoutId, { + widgets = json.encode(widgets), + updatedAt = os.time() * 1000, + }) +end + +--------------------------------------------------------------------------- +-- WIDGET DATA CACHE +--------------------------------------------------------------------------- + +---Cache widget data +---@param dbal table +---@param tenantId string +---@param widgetType string +---@param cacheKey string +---@param data table +---@param ttlSeconds number|nil +function M.cacheWidgetData(dbal, tenantId, widgetType, cacheKey, data, ttlSeconds) + local key = widgetType .. ':' .. cacheKey + local expiresAt = ttlSeconds and ((os.time() + ttlSeconds) * 1000) or nil + + local existing = dbal:findFirst('WidgetCache', { + where = { tenantId = tenantId, cacheKey = key }, + }) + + if existing then + return dbal:update('WidgetCache', existing.id, { + data = json.encode(data), + expiresAt = expiresAt, + updatedAt = os.time() * 1000, + }) + end + + return dbal:create('WidgetCache', { + tenantId = tenantId, + cacheKey = key, + widgetType = widgetType, + data = json.encode(data), + expiresAt = expiresAt, + createdAt = os.time() * 1000, + updatedAt = os.time() * 1000, + }) +end + +---Get cached widget data +---@param dbal table +---@param tenantId string +---@param widgetType string +---@param cacheKey string +---@return table|nil Cached data or nil if expired/not found +function M.getCachedData(dbal, tenantId, widgetType, cacheKey) + local key = widgetType .. ':' .. cacheKey + + local cached = dbal:findFirst('WidgetCache', { + where = { tenantId = tenantId, cacheKey = key }, + }) + + if cached then + -- Check expiry + if cached.expiresAt and cached.expiresAt < (os.time() * 1000) then + dbal:delete('WidgetCache', cached.id) + return nil + end + + return json.decode(cached.data) + end + + return nil +end + +---Clear expired cache entries +---@param dbal table +---@param tenantId string +---@return number Number of entries cleared +function M.clearExpiredCache(dbal, tenantId) + local now = os.time() * 1000 + local result = dbal:list('WidgetCache', { + where = { tenantId = tenantId }, + take = 10000, + }) + + local count = 0 + for _, entry in ipairs(result.items or {}) do + if entry.expiresAt and entry.expiresAt < now then + dbal:delete('WidgetCache', entry.id) + count = count + 1 + end + end + + return count +end + +return M diff --git a/packages/data_table/seed/scripts/db/operations.lua b/packages/data_table/seed/scripts/db/operations.lua new file mode 100644 index 000000000..5479a1ce3 --- /dev/null +++ b/packages/data_table/seed/scripts/db/operations.lua @@ -0,0 +1,346 @@ +-- data_table/seed/scripts/db/operations.lua +-- DBAL operations for generic data table management +-- Provides reusable CRUD helpers for any entity +-- @module data_table.db.operations + +local M = {} +local json = require('json') + +--------------------------------------------------------------------------- +-- GENERIC CRUD OPERATIONS +--------------------------------------------------------------------------- + +---Create a record in any entity +---@param dbal table DBAL client instance +---@param entity string Entity name +---@param data table Record data +---@return table Created record +function M.create(dbal, entity, data) + -- Auto-add timestamps if not present + if not data.createdAt then + data.createdAt = os.time() * 1000 + end + return dbal:create(entity, data) +end + +---Read a record by ID +---@param dbal table +---@param entity string Entity name +---@param id string Record ID +---@return table|nil Record +function M.read(dbal, entity, id) + return dbal:read(entity, id) +end + +---Update a record +---@param dbal table +---@param entity string Entity name +---@param id string Record ID +---@param data table Fields to update +---@return table Updated record +function M.update(dbal, entity, id, data) + -- Auto-update timestamp if not present + if not data.updatedAt then + data.updatedAt = os.time() * 1000 + end + return dbal:update(entity, id, data) +end + +---Delete a record +---@param dbal table +---@param entity string Entity name +---@param id string Record ID +---@return boolean Success +function M.delete(dbal, entity, id) + return dbal:delete(entity, id) +end + +---List records with filtering and pagination +---@param dbal table +---@param entity string Entity name +---@param options table List options +---@return table List result with items and total +function M.list(dbal, entity, options) + return dbal:list(entity, { + where = options.where or {}, + orderBy = options.orderBy or { createdAt = 'desc' }, + take = options.take or 50, + skip = options.skip or 0, + }) +end + +---Find first matching record +---@param dbal table +---@param entity string Entity name +---@param where table Where conditions +---@return table|nil First matching record +function M.findFirst(dbal, entity, where) + return dbal:findFirst(entity, { where = where }) +end + +--------------------------------------------------------------------------- +-- ADVANCED QUERY HELPERS +--------------------------------------------------------------------------- + +---Search records by text field +---@param dbal table +---@param entity string Entity name +---@param field string Field to search +---@param query string Search query +---@param tenantId string|nil Tenant filter +---@param take number|nil Max results +---@return table[] Matching records +function M.search(dbal, entity, field, query, tenantId, take) + local where = {} + if tenantId then + where.tenantId = tenantId + end + + local result = dbal:list(entity, { + where = where, + take = 1000, + }) + + local matches = {} + local lowerQuery = query:lower() + + for _, record in ipairs(result.items or {}) do + local fieldValue = record[field] + if fieldValue and type(fieldValue) == 'string' then + if fieldValue:lower():find(lowerQuery, 1, true) then + table.insert(matches, record) + if #matches >= (take or 50) then + break + end + end + end + end + + return matches +end + +---Count records matching conditions +---@param dbal table +---@param entity string Entity name +---@param where table|nil Where conditions +---@return number Count +function M.count(dbal, entity, where) + local result = dbal:list(entity, { + where = where or {}, + take = 100000, + }) + return result.total or #(result.items or {}) +end + +---Check if record exists +---@param dbal table +---@param entity string Entity name +---@param where table Where conditions +---@return boolean Exists +function M.exists(dbal, entity, where) + local record = M.findFirst(dbal, entity, where) + return record ~= nil +end + +---Get distinct values for a field +---@param dbal table +---@param entity string Entity name +---@param field string Field name +---@param where table|nil Where conditions +---@return table[] Distinct values +function M.distinct(dbal, entity, field, where) + local result = dbal:list(entity, { + where = where or {}, + take = 10000, + }) + + local seen = {} + local values = {} + + for _, record in ipairs(result.items or {}) do + local value = record[field] + if value ~= nil and not seen[tostring(value)] then + seen[tostring(value)] = true + table.insert(values, value) + end + end + + return values +end + +--------------------------------------------------------------------------- +-- BULK OPERATIONS +--------------------------------------------------------------------------- + +---Create multiple records +---@param dbal table +---@param entity string Entity name +---@param records table[] Records to create +---@return table[] Created records +function M.createMany(dbal, entity, records) + local created = {} + for _, data in ipairs(records) do + local record = M.create(dbal, entity, data) + table.insert(created, record) + end + return created +end + +---Update multiple records by IDs +---@param dbal table +---@param entity string Entity name +---@param ids table[] Record IDs +---@param data table Fields to update +---@return number Count of updated records +function M.updateMany(dbal, entity, ids, data) + local count = 0 + for _, id in ipairs(ids) do + M.update(dbal, entity, id, data) + count = count + 1 + end + return count +end + +---Delete multiple records by IDs +---@param dbal table +---@param entity string Entity name +---@param ids table[] Record IDs +---@return number Count of deleted records +function M.deleteMany(dbal, entity, ids) + local count = 0 + for _, id in ipairs(ids) do + if M.delete(dbal, entity, id) then + count = count + 1 + end + end + return count +end + +---Delete records matching conditions +---@param dbal table +---@param entity string Entity name +---@param where table Where conditions +---@return number Count of deleted records +function M.deleteWhere(dbal, entity, where) + local result = dbal:list(entity, { + where = where, + take = 10000, + }) + + local count = 0 + for _, record in ipairs(result.items or {}) do + if M.delete(dbal, entity, record.id) then + count = count + 1 + end + end + + return count +end + +--------------------------------------------------------------------------- +-- TABLE CONFIGURATION +--------------------------------------------------------------------------- + +---@class TableConfig +---@field entity string Entity name +---@field columns table[] Column definitions +---@field defaultSort table|nil Default sort order +---@field pageSize number|nil Default page size +---@field filters table[]|nil Available filters + +---Save table configuration +---@param dbal table +---@param tenantId string +---@param tableKey string Unique key for this table config +---@param config TableConfig +---@return table Saved config +function M.saveTableConfig(dbal, tenantId, tableKey, config) + local existing = dbal:findFirst('TableConfig', { + where = { tenantId = tenantId, tableKey = tableKey }, + }) + + if existing then + return dbal:update('TableConfig', existing.id, { + config = json.encode(config), + updatedAt = os.time() * 1000, + }) + end + + return dbal:create('TableConfig', { + tenantId = tenantId, + tableKey = tableKey, + config = json.encode(config), + createdAt = os.time() * 1000, + updatedAt = os.time() * 1000, + }) +end + +---Get table configuration +---@param dbal table +---@param tenantId string +---@param tableKey string +---@return TableConfig|nil +function M.getTableConfig(dbal, tenantId, tableKey) + local record = dbal:findFirst('TableConfig', { + where = { tenantId = tenantId, tableKey = tableKey }, + }) + + if record and record.config then + return json.decode(record.config) + end + + return nil +end + +--------------------------------------------------------------------------- +-- USER PREFERENCES +--------------------------------------------------------------------------- + +---Save user column preferences +---@param dbal table +---@param userId string +---@param tableKey string +---@param columns table[] Visible columns in order +---@param columnWidths table|nil Column widths +function M.saveUserPreferences(dbal, userId, tableKey, columns, columnWidths) + local existing = dbal:findFirst('UserTablePreference', { + where = { userId = userId, tableKey = tableKey }, + }) + + local data = { + columns = json.encode(columns), + columnWidths = columnWidths and json.encode(columnWidths) or nil, + updatedAt = os.time() * 1000, + } + + if existing then + return dbal:update('UserTablePreference', existing.id, data) + end + + data.userId = userId + data.tableKey = tableKey + data.createdAt = os.time() * 1000 + return dbal:create('UserTablePreference', data) +end + +---Get user column preferences +---@param dbal table +---@param userId string +---@param tableKey string +---@return table|nil Preferences +function M.getUserPreferences(dbal, userId, tableKey) + local record = dbal:findFirst('UserTablePreference', { + where = { userId = userId, tableKey = tableKey }, + }) + + if record then + return { + columns = record.columns and json.decode(record.columns) or nil, + columnWidths = record.columnWidths and json.decode(record.columnWidths) or nil, + } + end + + return nil +end + +return M