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

427 lines
12 KiB
Lua

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