Files
metabuilder/packages/css_designer/seed/scripts/db/operations.lua
2025-12-30 22:03:58 +00:00

359 lines
9.8 KiB
Lua

-- css_designer/seed/scripts/db/operations.lua
-- DBAL operations for Theme and Style entities
-- @module css_designer.db.operations
local M = {}
local json = require('json')
---------------------------------------------------------------------------
-- THEME OPERATIONS
---------------------------------------------------------------------------
---@class ThemeCreateParams
---@field tenantId string
---@field name string
---@field baseTheme string|nil ('light', 'dark', or parent theme ID)
---@field variables table CSS variable definitions
---@field metadata table|nil
---Create a new theme
---@param dbal table DBAL client instance
---@param params ThemeCreateParams
---@return table Created theme
function M.createTheme(dbal, params)
return dbal:create('Theme', {
tenantId = params.tenantId,
name = params.name,
baseTheme = params.baseTheme or 'light',
variables = json.encode(params.variables),
metadata = params.metadata and json.encode(params.metadata) or nil,
isActive = true,
version = 1,
createdAt = os.time() * 1000,
updatedAt = os.time() * 1000,
})
end
---Get theme by ID
---@param dbal table
---@param themeId string
---@return table|nil Theme with decoded variables
function M.getTheme(dbal, themeId)
local theme = dbal:read('Theme', themeId)
if theme then
theme.variables = json.decode(theme.variables or '{}')
if theme.metadata then
theme.metadata = json.decode(theme.metadata)
end
end
return theme
end
---Get theme by name
---@param dbal table
---@param tenantId string
---@param name string
---@return table|nil Theme
function M.getThemeByName(dbal, tenantId, name)
local theme = dbal:findFirst('Theme', {
where = { tenantId = tenantId, name = name },
})
if theme then
theme.variables = json.decode(theme.variables or '{}')
end
return theme
end
---List themes for tenant
---@param dbal table
---@param tenantId string
---@param activeOnly boolean|nil
---@return table[] Themes
function M.listThemes(dbal, tenantId, activeOnly)
local where = { tenantId = tenantId }
if activeOnly then
where.isActive = true
end
local result = dbal:list('Theme', {
where = where,
orderBy = { name = 'asc' },
take = 50,
})
return result.items or {}
end
---Update theme
---@param dbal table
---@param themeId string
---@param updates table
---@return table Updated theme
function M.updateTheme(dbal, themeId, updates)
if updates.variables and type(updates.variables) == 'table' then
updates.variables = json.encode(updates.variables)
-- Increment version on variable changes
local theme = M.getTheme(dbal, themeId)
if theme then
updates.version = (theme.version or 1) + 1
end
end
if updates.metadata and type(updates.metadata) == 'table' then
updates.metadata = json.encode(updates.metadata)
end
updates.updatedAt = os.time() * 1000
return dbal:update('Theme', themeId, updates)
end
---Set theme variable
---@param dbal table
---@param themeId string
---@param varName string CSS variable name
---@param value string Value
function M.setVariable(dbal, themeId, varName, value)
local theme = M.getTheme(dbal, themeId)
if not theme then
error('Theme not found: ' .. themeId)
end
local vars = theme.variables or {}
vars[varName] = value
return M.updateTheme(dbal, themeId, { variables = vars })
end
---Clone theme
---@param dbal table
---@param themeId string
---@param newName string
---@return table Cloned theme
function M.cloneTheme(dbal, themeId, newName)
local original = M.getTheme(dbal, themeId)
if not original then
error('Theme not found: ' .. themeId)
end
return M.createTheme(dbal, {
tenantId = original.tenantId,
name = newName,
baseTheme = original.baseTheme,
variables = original.variables,
metadata = original.metadata,
})
end
---Delete theme
---@param dbal table
---@param themeId string
function M.deleteTheme(dbal, themeId)
return dbal:delete('Theme', themeId)
end
---------------------------------------------------------------------------
-- STYLE COMPONENT OPERATIONS
---------------------------------------------------------------------------
---@class StyleComponentParams
---@field tenantId string
---@field themeId string|nil
---@field name string
---@field selector string CSS selector
---@field styles table CSS properties
---@field variants table|nil Style variants
---Create style component
---@param dbal table
---@param params StyleComponentParams
---@return table Created component
function M.createComponent(dbal, params)
return dbal:create('StyleComponent', {
tenantId = params.tenantId,
themeId = params.themeId,
name = params.name,
selector = params.selector,
styles = json.encode(params.styles),
variants = params.variants and json.encode(params.variants) or nil,
createdAt = os.time() * 1000,
updatedAt = os.time() * 1000,
})
end
---Get component by ID
---@param dbal table
---@param componentId string
---@return table|nil Component
function M.getComponent(dbal, componentId)
local comp = dbal:read('StyleComponent', componentId)
if comp then
comp.styles = json.decode(comp.styles or '{}')
if comp.variants then
comp.variants = json.decode(comp.variants)
end
end
return comp
end
---List components for theme
---@param dbal table
---@param themeId string|nil
---@param tenantId string
---@return table[] Components
function M.listComponents(dbal, tenantId, themeId)
local where = { tenantId = tenantId }
if themeId then
where.themeId = themeId
end
local result = dbal:list('StyleComponent', {
where = where,
orderBy = { name = 'asc' },
take = 200,
})
return result.items or {}
end
---Update component styles
---@param dbal table
---@param componentId string
---@param styles table CSS properties
function M.updateStyles(dbal, componentId, styles)
return dbal:update('StyleComponent', componentId, {
styles = json.encode(styles),
updatedAt = os.time() * 1000,
})
end
---Delete component
---@param dbal table
---@param componentId string
function M.deleteComponent(dbal, componentId)
return dbal:delete('StyleComponent', componentId)
end
---------------------------------------------------------------------------
-- CSS GENERATION
---------------------------------------------------------------------------
---Generate CSS variables from theme
---@param dbal table
---@param themeId string
---@return string CSS custom properties
function M.generateVariablesCSS(dbal, themeId)
local theme = M.getTheme(dbal, themeId)
if not theme then
return ''
end
local css = ':root {\n'
for name, value in pairs(theme.variables or {}) do
css = css .. ' --' .. name .. ': ' .. value .. ';\n'
end
css = css .. '}\n'
return css
end
---Generate CSS for all components
---@param dbal table
---@param tenantId string
---@param themeId string|nil
---@return string Generated CSS
function M.generateComponentsCSS(dbal, tenantId, themeId)
local components = M.listComponents(dbal, tenantId, themeId)
local css = ''
for _, comp in ipairs(components) do
local styles = json.decode(comp.styles or '{}')
css = css .. comp.selector .. ' {\n'
for prop, value in pairs(styles) do
css = css .. ' ' .. prop .. ': ' .. value .. ';\n'
end
css = css .. '}\n\n'
-- Add variants if present
local variants = comp.variants and json.decode(comp.variants) or nil
if variants then
for variant, varStyles in pairs(variants) do
css = css .. comp.selector .. ':' .. variant .. ' {\n'
for prop, value in pairs(varStyles) do
css = css .. ' ' .. prop .. ': ' .. value .. ';\n'
end
css = css .. '}\n\n'
end
end
end
return css
end
---Generate complete CSS bundle
---@param dbal table
---@param tenantId string
---@param themeId string
---@return string Complete CSS
function M.generateFullCSS(dbal, tenantId, themeId)
local css = '/* Generated Theme CSS */\n\n'
css = css .. M.generateVariablesCSS(dbal, themeId)
css = css .. '\n/* Component Styles */\n\n'
css = css .. M.generateComponentsCSS(dbal, tenantId, themeId)
return css
end
---------------------------------------------------------------------------
-- PRESET MANAGEMENT
---------------------------------------------------------------------------
---Save theme as preset
---@param dbal table
---@param themeId string
---@param presetName string
---@param description string|nil
function M.saveAsPreset(dbal, themeId, presetName, description)
local theme = M.getTheme(dbal, themeId)
if not theme then
error('Theme not found: ' .. themeId)
end
return dbal:create('ThemePreset', {
tenantId = theme.tenantId,
name = presetName,
description = description,
variables = json.encode(theme.variables),
baseTheme = theme.baseTheme,
createdAt = os.time() * 1000,
})
end
---List available presets
---@param dbal table
---@param tenantId string
---@return table[] Presets
function M.listPresets(dbal, tenantId)
local result = dbal:list('ThemePreset', {
where = { tenantId = tenantId },
orderBy = { name = 'asc' },
take = 50,
})
return result.items or {}
end
---Apply preset to theme
---@param dbal table
---@param themeId string
---@param presetId string
function M.applyPreset(dbal, themeId, presetId)
local preset = dbal:read('ThemePreset', presetId)
if not preset then
error('Preset not found: ' .. presetId)
end
local variables = json.decode(preset.variables or '{}')
return M.updateTheme(dbal, themeId, {
variables = variables,
baseTheme = preset.baseTheme,
})
end
return M