code: packages,operations,lua (4 files)

This commit is contained in:
Richard Ward
2025-12-30 22:03:58 +00:00
parent 2814e11896
commit dc1109bf13
4 changed files with 1153 additions and 0 deletions

View File

@@ -0,0 +1,358 @@
-- 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

View File

@@ -0,0 +1,368 @@
-- form_builder/seed/scripts/db/operations.lua
-- DBAL operations for Form and Submission entities
-- @module form_builder.db.operations
local M = {}
local json = require('json')
---------------------------------------------------------------------------
-- FORM OPERATIONS
---------------------------------------------------------------------------
---@class FormCreateParams
---@field tenantId string
---@field name string
---@field description string|nil
---@field fields table[] Form field definitions
---@field settings table|nil Form settings
---@field validationRules table|nil Validation rules
---Create a new form
---@param dbal table DBAL client instance
---@param params FormCreateParams
---@return table Created form
function M.createForm(dbal, params)
return dbal:create('Form', {
tenantId = params.tenantId,
name = params.name,
description = params.description,
fields = json.encode(params.fields),
settings = params.settings and json.encode(params.settings) or '{}',
validationRules = params.validationRules and json.encode(params.validationRules) or nil,
isActive = true,
version = 1,
submissionCount = 0,
createdAt = os.time() * 1000,
updatedAt = os.time() * 1000,
})
end
---Get form by ID
---@param dbal table
---@param formId string
---@return table|nil Form with decoded fields
function M.getForm(dbal, formId)
local form = dbal:read('Form', formId)
if form then
form.fields = json.decode(form.fields or '[]')
form.settings = json.decode(form.settings or '{}')
if form.validationRules then
form.validationRules = json.decode(form.validationRules)
end
end
return form
end
---List forms for tenant
---@param dbal table
---@param tenantId string
---@param activeOnly boolean|nil
---@return table[] Forms
function M.listForms(dbal, tenantId, activeOnly)
local where = { tenantId = tenantId }
if activeOnly then
where.isActive = true
end
local result = dbal:list('Form', {
where = where,
orderBy = { createdAt = 'desc' },
take = 100,
})
return result.items or {}
end
---Update form definition
---@param dbal table
---@param formId string
---@param updates table
---@return table Updated form
function M.updateForm(dbal, formId, updates)
-- Encode complex fields
if updates.fields and type(updates.fields) == 'table' then
updates.fields = json.encode(updates.fields)
end
if updates.settings and type(updates.settings) == 'table' then
updates.settings = json.encode(updates.settings)
end
if updates.validationRules and type(updates.validationRules) == 'table' then
updates.validationRules = json.encode(updates.validationRules)
end
-- Increment version on field changes
if updates.fields then
local form = M.getForm(dbal, formId)
if form then
updates.version = (form.version or 1) + 1
end
end
updates.updatedAt = os.time() * 1000
return dbal:update('Form', formId, updates)
end
---Clone form
---@param dbal table
---@param formId string
---@param newName string
---@return table Cloned form
function M.cloneForm(dbal, formId, newName)
local original = M.getForm(dbal, formId)
if not original then
error('Form not found: ' .. formId)
end
return M.createForm(dbal, {
tenantId = original.tenantId,
name = newName,
description = original.description,
fields = original.fields, -- Already decoded
settings = original.settings, -- Already decoded
validationRules = original.validationRules,
})
end
---Delete form
---@param dbal table
---@param formId string
---@return boolean
function M.deleteForm(dbal, formId)
return dbal:delete('Form', formId)
end
---------------------------------------------------------------------------
-- FORM FIELD OPERATIONS
---------------------------------------------------------------------------
---@class FormField
---@field id string Unique field ID
---@field type string Field type (text, select, checkbox, etc.)
---@field name string Field name/key
---@field label string Display label
---@field required boolean
---@field options table|nil For select/radio fields
---@field validation table|nil Validation config
---@field order number Display order
---Add field to form
---@param dbal table
---@param formId string
---@param field FormField
---@return table Updated form
function M.addField(dbal, formId, field)
local form = M.getForm(dbal, formId)
if not form then
error('Form not found: ' .. formId)
end
local fields = form.fields or {}
-- Generate ID if not provided
if not field.id then
field.id = 'field_' .. os.time() .. '_' .. math.random(1000, 9999)
end
-- Set order to end if not specified
if not field.order then
field.order = #fields + 1
end
table.insert(fields, field)
return M.updateForm(dbal, formId, { fields = fields })
end
---Update field in form
---@param dbal table
---@param formId string
---@param fieldId string
---@param updates table
---@return table Updated form
function M.updateField(dbal, formId, fieldId, updates)
local form = M.getForm(dbal, formId)
if not form then
error('Form not found: ' .. formId)
end
local fields = form.fields or {}
for i, field in ipairs(fields) do
if field.id == fieldId then
for key, value in pairs(updates) do
fields[i][key] = value
end
break
end
end
return M.updateForm(dbal, formId, { fields = fields })
end
---Remove field from form
---@param dbal table
---@param formId string
---@param fieldId string
---@return table Updated form
function M.removeField(dbal, formId, fieldId)
local form = M.getForm(dbal, formId)
if not form then
error('Form not found: ' .. formId)
end
local fields = form.fields or {}
local newFields = {}
for _, field in ipairs(fields) do
if field.id ~= fieldId then
table.insert(newFields, field)
end
end
return M.updateForm(dbal, formId, { fields = newFields })
end
---Reorder fields
---@param dbal table
---@param formId string
---@param fieldIds table[] Ordered array of field IDs
---@return table Updated form
function M.reorderFields(dbal, formId, fieldIds)
local form = M.getForm(dbal, formId)
if not form then
error('Form not found: ' .. formId)
end
local fieldMap = {}
for _, field in ipairs(form.fields or {}) do
fieldMap[field.id] = field
end
local orderedFields = {}
for order, fieldId in ipairs(fieldIds) do
local field = fieldMap[fieldId]
if field then
field.order = order
table.insert(orderedFields, field)
end
end
return M.updateForm(dbal, formId, { fields = orderedFields })
end
---------------------------------------------------------------------------
-- SUBMISSION OPERATIONS
---------------------------------------------------------------------------
---@class SubmissionParams
---@field tenantId string
---@field formId string
---@field userId string|nil Submitter user ID
---@field data table Form data
---@field metadata table|nil Additional metadata
---Submit form response
---@param dbal table
---@param params SubmissionParams
---@return table Created submission
function M.submitForm(dbal, params)
-- Increment form submission count
local form = dbal:read('Form', params.formId)
if form then
dbal:update('Form', params.formId, {
submissionCount = (form.submissionCount or 0) + 1,
})
end
return dbal:create('FormSubmission', {
tenantId = params.tenantId,
formId = params.formId,
userId = params.userId,
data = json.encode(params.data),
metadata = params.metadata and json.encode(params.metadata) or nil,
status = 'submitted',
submittedAt = os.time() * 1000,
})
end
---Get submission by ID
---@param dbal table
---@param submissionId string
---@return table|nil Submission
function M.getSubmission(dbal, submissionId)
local sub = dbal:read('FormSubmission', submissionId)
if sub then
sub.data = json.decode(sub.data or '{}')
if sub.metadata then
sub.metadata = json.decode(sub.metadata)
end
end
return sub
end
---List submissions for a form
---@param dbal table
---@param formId string
---@param options table|nil Filtering options
---@return table List result
function M.listSubmissions(dbal, formId, options)
options = options or {}
local result = dbal:list('FormSubmission', {
where = { formId = formId },
orderBy = { submittedAt = 'desc' },
take = options.take or 50,
skip = options.skip or 0,
})
return {
items = result.items or {},
total = result.total or 0,
}
end
---Update submission status
---@param dbal table
---@param submissionId string
---@param status string (submitted, reviewed, approved, rejected)
---@param reviewNotes string|nil
function M.updateSubmissionStatus(dbal, submissionId, status, reviewNotes)
return dbal:update('FormSubmission', submissionId, {
status = status,
reviewNotes = reviewNotes,
reviewedAt = os.time() * 1000,
})
end
---Delete submission
---@param dbal table
---@param submissionId string
function M.deleteSubmission(dbal, submissionId)
return dbal:delete('FormSubmission', submissionId)
end
---Export submissions as table
---@param dbal table
---@param formId string
---@return table[] Array of submission data objects
function M.exportSubmissions(dbal, formId)
local result = dbal:list('FormSubmission', {
where = { formId = formId },
orderBy = { submittedAt = 'asc' },
take = 10000,
})
local exports = {}
for _, sub in ipairs(result.items or {}) do
local data = json.decode(sub.data or '{}')
data._submissionId = sub.id
data._submittedAt = sub.submittedAt
data._userId = sub.userId
data._status = sub.status
table.insert(exports, data)
end
return exports
end
return M

View File

@@ -0,0 +1,426 @@
-- schema_editor/seed/scripts/db/operations.lua
-- DBAL operations for Schema Registry entities
-- @module schema_editor.db.operations
local M = {}
local json = require('json')
---------------------------------------------------------------------------
-- ENTITY SCHEMA OPERATIONS
---------------------------------------------------------------------------
---@class EntitySchemaParams
---@field tenantId string
---@field name string Entity name
---@field fields table[] Field definitions
---@field indexes table[]|nil Index definitions
---@field relations table[]|nil Relation definitions
---@field acl table|nil Access control rules
---Create or update entity schema
---@param dbal table DBAL client instance
---@param params EntitySchemaParams
---@return table Created/updated schema
function M.saveEntitySchema(dbal, params)
local existing = dbal:findFirst('EntitySchema', {
where = { tenantId = params.tenantId, name = params.name },
})
local data = {
fields = json.encode(params.fields),
indexes = params.indexes and json.encode(params.indexes) or nil,
relations = params.relations and json.encode(params.relations) or nil,
acl = params.acl and json.encode(params.acl) or nil,
updatedAt = os.time() * 1000,
}
if existing then
data.version = (existing.version or 1) + 1
return dbal:update('EntitySchema', existing.id, data)
end
data.tenantId = params.tenantId
data.name = params.name
data.version = 1
data.createdAt = os.time() * 1000
return dbal:create('EntitySchema', data)
end
---Get entity schema by name
---@param dbal table
---@param tenantId string
---@param name string
---@return table|nil Schema with decoded fields
function M.getEntitySchema(dbal, tenantId, name)
local schema = dbal:findFirst('EntitySchema', {
where = { tenantId = tenantId, name = name },
})
if schema then
schema.fields = json.decode(schema.fields or '[]')
schema.indexes = schema.indexes and json.decode(schema.indexes) or {}
schema.relations = schema.relations and json.decode(schema.relations) or {}
schema.acl = schema.acl and json.decode(schema.acl) or nil
end
return schema
end
---List all entity schemas
---@param dbal table
---@param tenantId string
---@return table[] Schemas
function M.listEntitySchemas(dbal, tenantId)
local result = dbal:list('EntitySchema', {
where = { tenantId = tenantId },
orderBy = { name = 'asc' },
take = 200,
})
return result.items or {}
end
---Delete entity schema
---@param dbal table
---@param tenantId string
---@param name string
function M.deleteEntitySchema(dbal, tenantId, name)
local schema = M.getEntitySchema(dbal, tenantId, name)
if schema then
return dbal:delete('EntitySchema', schema.id)
end
return false
end
---------------------------------------------------------------------------
-- FIELD OPERATIONS
---------------------------------------------------------------------------
---@class FieldDefinition
---@field name string
---@field type string (String, Int, Float, Boolean, DateTime, Json, etc.)
---@field required boolean|nil
---@field unique boolean|nil
---@field default any|nil
---@field validation table|nil
---Add field to entity schema
---@param dbal table
---@param tenantId string
---@param entityName string
---@param field FieldDefinition
---@return table Updated schema
function M.addField(dbal, tenantId, entityName, field)
local schema = M.getEntitySchema(dbal, tenantId, entityName)
if not schema then
error('Entity schema not found: ' .. entityName)
end
local fields = schema.fields or {}
-- Check for duplicate field name
for _, f in ipairs(fields) do
if f.name == field.name then
error('Field already exists: ' .. field.name)
end
end
table.insert(fields, field)
return M.saveEntitySchema(dbal, {
tenantId = tenantId,
name = entityName,
fields = fields,
indexes = schema.indexes,
relations = schema.relations,
acl = schema.acl,
})
end
---Update field definition
---@param dbal table
---@param tenantId string
---@param entityName string
---@param fieldName string
---@param updates table
---@return table Updated schema
function M.updateField(dbal, tenantId, entityName, fieldName, updates)
local schema = M.getEntitySchema(dbal, tenantId, entityName)
if not schema then
error('Entity schema not found: ' .. entityName)
end
local fields = schema.fields or {}
local found = false
for i, field in ipairs(fields) do
if field.name == fieldName then
for key, value in pairs(updates) do
fields[i][key] = value
end
found = true
break
end
end
if not found then
error('Field not found: ' .. fieldName)
end
return M.saveEntitySchema(dbal, {
tenantId = tenantId,
name = entityName,
fields = fields,
indexes = schema.indexes,
relations = schema.relations,
acl = schema.acl,
})
end
---Remove field from entity
---@param dbal table
---@param tenantId string
---@param entityName string
---@param fieldName string
---@return table Updated schema
function M.removeField(dbal, tenantId, entityName, fieldName)
local schema = M.getEntitySchema(dbal, tenantId, entityName)
if not schema then
error('Entity schema not found: ' .. entityName)
end
local fields = schema.fields or {}
local newFields = {}
for _, field in ipairs(fields) do
if field.name ~= fieldName then
table.insert(newFields, field)
end
end
return M.saveEntitySchema(dbal, {
tenantId = tenantId,
name = entityName,
fields = newFields,
indexes = schema.indexes,
relations = schema.relations,
acl = schema.acl,
})
end
---------------------------------------------------------------------------
-- INDEX OPERATIONS
---------------------------------------------------------------------------
---@class IndexDefinition
---@field name string
---@field fields table[] Field names
---@field unique boolean|nil
---Add index to entity
---@param dbal table
---@param tenantId string
---@param entityName string
---@param index IndexDefinition
---@return table Updated schema
function M.addIndex(dbal, tenantId, entityName, index)
local schema = M.getEntitySchema(dbal, tenantId, entityName)
if not schema then
error('Entity schema not found: ' .. entityName)
end
local indexes = schema.indexes or {}
table.insert(indexes, index)
return M.saveEntitySchema(dbal, {
tenantId = tenantId,
name = entityName,
fields = schema.fields,
indexes = indexes,
relations = schema.relations,
acl = schema.acl,
})
end
---Remove index from entity
---@param dbal table
---@param tenantId string
---@param entityName string
---@param indexName string
---@return table Updated schema
function M.removeIndex(dbal, tenantId, entityName, indexName)
local schema = M.getEntitySchema(dbal, tenantId, entityName)
if not schema then
error('Entity schema not found: ' .. entityName)
end
local indexes = schema.indexes or {}
local newIndexes = {}
for _, idx in ipairs(indexes) do
if idx.name ~= indexName then
table.insert(newIndexes, idx)
end
end
return M.saveEntitySchema(dbal, {
tenantId = tenantId,
name = entityName,
fields = schema.fields,
indexes = newIndexes,
relations = schema.relations,
acl = schema.acl,
})
end
---------------------------------------------------------------------------
-- RELATION OPERATIONS
---------------------------------------------------------------------------
---@class RelationDefinition
---@field name string
---@field type string (hasOne, hasMany, belongsTo, manyToMany)
---@field target string Target entity name
---@field foreignKey string|nil
---@field through string|nil For manyToMany
---Add relation to entity
---@param dbal table
---@param tenantId string
---@param entityName string
---@param relation RelationDefinition
---@return table Updated schema
function M.addRelation(dbal, tenantId, entityName, relation)
local schema = M.getEntitySchema(dbal, tenantId, entityName)
if not schema then
error('Entity schema not found: ' .. entityName)
end
local relations = schema.relations or {}
table.insert(relations, relation)
return M.saveEntitySchema(dbal, {
tenantId = tenantId,
name = entityName,
fields = schema.fields,
indexes = schema.indexes,
relations = relations,
acl = schema.acl,
})
end
---Remove relation from entity
---@param dbal table
---@param tenantId string
---@param entityName string
---@param relationName string
---@return table Updated schema
function M.removeRelation(dbal, tenantId, entityName, relationName)
local schema = M.getEntitySchema(dbal, tenantId, entityName)
if not schema then
error('Entity schema not found: ' .. entityName)
end
local relations = schema.relations or {}
local newRelations = {}
for _, rel in ipairs(relations) do
if rel.name ~= relationName then
table.insert(newRelations, rel)
end
end
return M.saveEntitySchema(dbal, {
tenantId = tenantId,
name = entityName,
fields = schema.fields,
indexes = schema.indexes,
relations = newRelations,
acl = schema.acl,
})
end
---------------------------------------------------------------------------
-- SCHEMA VALIDATION
---------------------------------------------------------------------------
---Validate entity schema
---@param dbal table
---@param tenantId string
---@param entityName string
---@return table Validation result
function M.validateSchema(dbal, tenantId, entityName)
local schema = M.getEntitySchema(dbal, tenantId, entityName)
if not schema then
return { valid = false, errors = { 'Entity schema not found' } }
end
local errors = {}
-- Validate fields
for _, field in ipairs(schema.fields or {}) do
if not field.name or field.name == '' then
table.insert(errors, 'Field must have a name')
end
if not field.type or field.type == '' then
table.insert(errors, 'Field must have a type: ' .. (field.name or 'unknown'))
end
end
-- Validate indexes reference valid fields
local fieldNames = {}
for _, field in ipairs(schema.fields or {}) do
fieldNames[field.name] = true
end
for _, idx in ipairs(schema.indexes or {}) do
for _, fieldName in ipairs(idx.fields or {}) do
if not fieldNames[fieldName] then
table.insert(errors, 'Index references non-existent field: ' .. fieldName)
end
end
end
return {
valid = #errors == 0,
errors = errors,
}
end
---------------------------------------------------------------------------
-- SCHEMA VERSION HISTORY
---------------------------------------------------------------------------
---Get schema version history
---@param dbal table
---@param tenantId string
---@param entityName string
---@return table[] Version history
function M.getVersionHistory(dbal, tenantId, entityName)
local result = dbal:list('SchemaVersion', {
where = { tenantId = tenantId, entityName = entityName },
orderBy = { version = 'desc' },
take = 50,
})
return result.items or {}
end
---Save schema version snapshot
---@param dbal table
---@param tenantId string
---@param entityName string
---@param schema table Current schema
---@param changeNote string|nil
function M.saveVersion(dbal, tenantId, entityName, schema, changeNote)
return dbal:create('SchemaVersion', {
tenantId = tenantId,
entityName = entityName,
version = schema.version or 1,
snapshot = json.encode(schema),
changeNote = changeNote,
createdAt = os.time() * 1000,
})
end
return M

View File

@@ -16,6 +16,7 @@ import { autoRegisterPackages } from '../auto-loader'
// Packages known to have good components.json files
const AUTO_LOAD_PACKAGES = [
'arcade_lobby',
'audit_log',
'dashboard',
'data_table',
'form_builder',