From dc1109bf13ba7e272a71225973bf189faadd55e2 Mon Sep 17 00:00:00 2001 From: Richard Ward Date: Tue, 30 Dec 2025 22:03:58 +0000 Subject: [PATCH] code: packages,operations,lua (4 files) --- .../seed/scripts/db/operations.lua | 358 +++++++++++++++ .../seed/scripts/db/operations.lua | 368 +++++++++++++++ .../seed/scripts/db/operations.lua | 426 ++++++++++++++++++ storybook/src/mocks/packages/index.ts | 1 + 4 files changed, 1153 insertions(+) create mode 100644 packages/css_designer/seed/scripts/db/operations.lua create mode 100644 packages/form_builder/seed/scripts/db/operations.lua create mode 100644 packages/schema_editor/seed/scripts/db/operations.lua diff --git a/packages/css_designer/seed/scripts/db/operations.lua b/packages/css_designer/seed/scripts/db/operations.lua new file mode 100644 index 000000000..3030aecdd --- /dev/null +++ b/packages/css_designer/seed/scripts/db/operations.lua @@ -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 diff --git a/packages/form_builder/seed/scripts/db/operations.lua b/packages/form_builder/seed/scripts/db/operations.lua new file mode 100644 index 000000000..0f3e46231 --- /dev/null +++ b/packages/form_builder/seed/scripts/db/operations.lua @@ -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 diff --git a/packages/schema_editor/seed/scripts/db/operations.lua b/packages/schema_editor/seed/scripts/db/operations.lua new file mode 100644 index 000000000..6695fac57 --- /dev/null +++ b/packages/schema_editor/seed/scripts/db/operations.lua @@ -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 diff --git a/storybook/src/mocks/packages/index.ts b/storybook/src/mocks/packages/index.ts index 316fba9f3..b7e2c1599 100644 --- a/storybook/src/mocks/packages/index.ts +++ b/storybook/src/mocks/packages/index.ts @@ -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',