Files
metabuilder/packages/dashboard/seed/scripts/db/operations.lua
2025-12-30 22:01:42 +00:00

376 lines
10 KiB
Lua

-- 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