mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
update: packages,operations,lua (3 files)
This commit is contained in:
394
packages/arcade_lobby/seed/scripts/db/operations.lua
Normal file
394
packages/arcade_lobby/seed/scripts/db/operations.lua
Normal file
@@ -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
|
||||
375
packages/dashboard/seed/scripts/db/operations.lua
Normal file
375
packages/dashboard/seed/scripts/db/operations.lua
Normal file
@@ -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
|
||||
346
packages/data_table/seed/scripts/db/operations.lua
Normal file
346
packages/data_table/seed/scripts/db/operations.lua
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user