diff --git a/packages/role_editor/seed/scripts/db/operations.lua b/packages/role_editor/seed/scripts/db/operations.lua new file mode 100644 index 000000000..bb58a0b63 --- /dev/null +++ b/packages/role_editor/seed/scripts/db/operations.lua @@ -0,0 +1,383 @@ +-- role_editor/seed/scripts/db/operations.lua +-- DBAL operations for Role management +-- Uses Permission entity from Prisma schema +-- @module role_editor.db.operations + +local M = {} +local json = require('json') + +-- Permission levels +M.LEVELS = { + PUBLIC = 0, + USER = 1, + MODERATOR = 2, + ADMIN = 3, + GOD = 4, + SUPERGOD = 5, + SYSTEM = 6, +} + +M.LEVEL_NAMES = { + [0] = 'Public', + [1] = 'User', + [2] = 'Moderator', + [3] = 'Admin', + [4] = 'God', + [5] = 'Supergod', + [6] = 'System', +} + +--------------------------------------------------------------------------- +-- ROLE/PERMISSION OPERATIONS +--------------------------------------------------------------------------- + +---@class RoleCreateParams +---@field tenantId string +---@field name string +---@field level number 0-6 +---@field description string|nil +---@field permissions table|nil Array of permission strings +---@field color string|nil Display color + +---Create a new role +---@param dbal table DBAL client instance +---@param params RoleCreateParams +---@return table Created role +function M.createRole(dbal, params) + return dbal:create('Role', { + tenantId = params.tenantId, + name = params.name, + level = params.level or 1, + description = params.description, + permissions = params.permissions and json.encode(params.permissions) or '[]', + color = params.color or '#808080', + isDefault = false, + createdAt = os.time() * 1000, + updatedAt = os.time() * 1000, + }) +end + +---Get role by ID +---@param dbal table +---@param roleId string +---@return table|nil Role +function M.getRole(dbal, roleId) + local role = dbal:read('Role', roleId) + if role and role.permissions then + role.permissions = json.decode(role.permissions) + end + return role +end + +---Get role by name +---@param dbal table +---@param tenantId string +---@param name string +---@return table|nil Role +function M.getRoleByName(dbal, tenantId, name) + local role = dbal:findFirst('Role', { + where = { tenantId = tenantId, name = name }, + }) + if role and role.permissions then + role.permissions = json.decode(role.permissions) + end + return role +end + +---List all roles for a tenant +---@param dbal table +---@param tenantId string +---@return table[] Roles sorted by level +function M.listRoles(dbal, tenantId) + local result = dbal:list('Role', { + where = { tenantId = tenantId }, + orderBy = { level = 'asc' }, + take = 100, + }) + + local roles = result.items or {} + + -- Parse permissions JSON + for _, role in ipairs(roles) do + if role.permissions and type(role.permissions) == 'string' then + role.permissions = json.decode(role.permissions) + end + end + + return roles +end + +---Update role +---@param dbal table +---@param roleId string +---@param updates table +---@return table Updated role +function M.updateRole(dbal, roleId, updates) + if updates.permissions and type(updates.permissions) == 'table' then + updates.permissions = json.encode(updates.permissions) + end + updates.updatedAt = os.time() * 1000 + return dbal:update('Role', roleId, updates) +end + +---Add permission to role +---@param dbal table +---@param roleId string +---@param permission string +---@return table Updated role +function M.addPermission(dbal, roleId, permission) + local role = M.getRole(dbal, roleId) + if not role then + error('Role not found: ' .. roleId) + end + + local perms = role.permissions or {} + + -- Check if already has permission + for _, p in ipairs(perms) do + if p == permission then + return role + end + end + + table.insert(perms, permission) + return M.updateRole(dbal, roleId, { permissions = perms }) +end + +---Remove permission from role +---@param dbal table +---@param roleId string +---@param permission string +---@return table Updated role +function M.removePermission(dbal, roleId, permission) + local role = M.getRole(dbal, roleId) + if not role then + error('Role not found: ' .. roleId) + end + + local perms = role.permissions or {} + local newPerms = {} + + for _, p in ipairs(perms) do + if p ~= permission then + table.insert(newPerms, p) + end + end + + return M.updateRole(dbal, roleId, { permissions = newPerms }) +end + +---Check if role has permission +---@param dbal table +---@param roleId string +---@param permission string +---@return boolean +function M.hasPermission(dbal, roleId, permission) + local role = M.getRole(dbal, roleId) + if not role then + return false + end + + for _, p in ipairs(role.permissions or {}) do + if p == permission or p == '*' then + return true + end + end + + return false +end + +---Delete role +---@param dbal table +---@param roleId string +---@return boolean Success +function M.deleteRole(dbal, roleId) + return dbal:delete('Role', roleId) +end + +--------------------------------------------------------------------------- +-- USER ROLE ASSIGNMENT +--------------------------------------------------------------------------- + +---Assign role to user +---@param dbal table +---@param userId string +---@param roleId string +---@return table Created assignment +function M.assignRoleToUser(dbal, userId, roleId) + -- Check if already assigned + local existing = dbal:findFirst('UserRole', { + where = { userId = userId, roleId = roleId }, + }) + + if existing then + return existing + end + + return dbal:create('UserRole', { + userId = userId, + roleId = roleId, + createdAt = os.time() * 1000, + }) +end + +---Remove role from user +---@param dbal table +---@param userId string +---@param roleId string +---@return boolean Success +function M.removeRoleFromUser(dbal, userId, roleId) + local assignment = dbal:findFirst('UserRole', { + where = { userId = userId, roleId = roleId }, + }) + + if assignment then + return dbal:delete('UserRole', assignment.id) + end + + return false +end + +---Get user's roles +---@param dbal table +---@param userId string +---@return table[] Roles +function M.getUserRoles(dbal, userId) + local assignments = dbal:list('UserRole', { + where = { userId = userId }, + take = 100, + }) + + local roles = {} + for _, assignment in ipairs(assignments.items or {}) do + local role = M.getRole(dbal, assignment.roleId) + if role then + table.insert(roles, role) + end + end + + return roles +end + +---Get effective permission level for user +---@param dbal table +---@param userId string +---@return number Maximum level from all roles +function M.getEffectiveLevel(dbal, userId) + local roles = M.getUserRoles(dbal, userId) + local maxLevel = 0 + + for _, role in ipairs(roles) do + if (role.level or 0) > maxLevel then + maxLevel = role.level + end + end + + return maxLevel +end + +---Get all permissions for user +---@param dbal table +---@param userId string +---@return table Unique permissions +function M.getAllUserPermissions(dbal, userId) + local roles = M.getUserRoles(dbal, userId) + local permSet = {} + + for _, role in ipairs(roles) do + for _, perm in ipairs(role.permissions or {}) do + permSet[perm] = true + end + end + + local perms = {} + for perm, _ in pairs(permSet) do + table.insert(perms, perm) + end + + table.sort(perms) + return perms +end + +---Check if user has specific permission +---@param dbal table +---@param userId string +---@param permission string +---@return boolean +function M.userHasPermission(dbal, userId, permission) + local perms = M.getAllUserPermissions(dbal, userId) + + for _, p in ipairs(perms) do + if p == permission or p == '*' then + return true + end + end + + return false +end + +--------------------------------------------------------------------------- +-- DEFAULT ROLES +--------------------------------------------------------------------------- + +---Create default roles for a tenant +---@param dbal table +---@param tenantId string +---@return table[] Created roles +function M.createDefaultRoles(dbal, tenantId) + local roles = {} + + -- Public role + table.insert(roles, M.createRole(dbal, { + tenantId = tenantId, + name = 'Guest', + level = 0, + description = 'Unauthenticated visitor', + permissions = {'view:public'}, + color = '#9E9E9E', + })) + + -- User role + table.insert(roles, M.createRole(dbal, { + tenantId = tenantId, + name = 'User', + level = 1, + description = 'Registered user', + permissions = {'view:public', 'view:private', 'edit:own'}, + color = '#2196F3', + })) + + -- Moderator role + table.insert(roles, M.createRole(dbal, { + tenantId = tenantId, + name = 'Moderator', + level = 2, + description = 'Content moderator', + permissions = {'view:public', 'view:private', 'edit:own', 'moderate:content'}, + color = '#4CAF50', + })) + + -- Admin role + table.insert(roles, M.createRole(dbal, { + tenantId = tenantId, + name = 'Admin', + level = 3, + description = 'Administrator', + permissions = {'view:*', 'edit:*', 'manage:users', 'manage:content'}, + color = '#FF9800', + })) + + -- God role + table.insert(roles, M.createRole(dbal, { + tenantId = tenantId, + name = 'God', + level = 4, + description = 'Full access', + permissions = {'*'}, + color = '#F44336', + })) + + return roles +end + +return M diff --git a/packages/user_manager/seed/scripts/db/operations.lua b/packages/user_manager/seed/scripts/db/operations.lua new file mode 100644 index 000000000..751d641b5 --- /dev/null +++ b/packages/user_manager/seed/scripts/db/operations.lua @@ -0,0 +1,338 @@ +-- user_manager/seed/scripts/db/operations.lua +-- DBAL operations for User management +-- Uses existing User entity from Prisma schema +-- @module user_manager.db.operations + +local M = {} +local json = require('json') + +--------------------------------------------------------------------------- +-- USER CRUD OPERATIONS +--------------------------------------------------------------------------- + +---@class UserCreateParams +---@field tenantId string +---@field email string +---@field username string +---@field password string Hashed password +---@field displayName string|nil +---@field avatar string|nil +---@field level number 0-6 permission level +---@field status string active|inactive|banned + +---Create a new user +---@param dbal table DBAL client instance +---@param params UserCreateParams +---@return table Created user +function M.createUser(dbal, params) + return dbal:create('User', { + tenantId = params.tenantId, + email = params.email, + username = params.username, + password = params.password, + displayName = params.displayName or params.username, + avatar = params.avatar, + level = params.level or 1, + status = params.status or 'active', + createdAt = os.time() * 1000, + updatedAt = os.time() * 1000, + }) +end + +---Get user by ID +---@param dbal table +---@param userId string +---@return table|nil User +function M.getUser(dbal, userId) + return dbal:read('User', userId) +end + +---Get user by email +---@param dbal table +---@param tenantId string +---@param email string +---@return table|nil User +function M.getUserByEmail(dbal, tenantId, email) + return dbal:findFirst('User', { + where = { tenantId = tenantId, email = email }, + }) +end + +---Get user by username +---@param dbal table +---@param tenantId string +---@param username string +---@return table|nil User +function M.getUserByUsername(dbal, tenantId, username) + return dbal:findFirst('User', { + where = { tenantId = tenantId, username = username }, + }) +end + +---List users +---@param dbal table +---@param tenantId string +---@param status string|nil Filter by status +---@param minLevel number|nil Minimum permission level +---@param take number|nil +---@param skip number|nil +---@return table List result +function M.listUsers(dbal, tenantId, status, minLevel, take, skip) + local where = { tenantId = tenantId } + + if status then + where.status = status + end + + local result = dbal:list('User', { + where = where, + orderBy = { createdAt = 'desc' }, + take = take or 50, + skip = skip or 0, + }) + + -- Filter by minLevel if specified + if minLevel and result.items then + local filtered = {} + for _, user in ipairs(result.items) do + if (user.level or 0) >= minLevel then + table.insert(filtered, user) + end + end + result.items = filtered + end + + return result +end + +---Update user +---@param dbal table +---@param userId string +---@param updates table +---@return table Updated user +function M.updateUser(dbal, userId, updates) + updates.updatedAt = os.time() * 1000 + return dbal:update('User', userId, updates) +end + +---Update user profile +---@param dbal table +---@param userId string +---@param displayName string|nil +---@param avatar string|nil +---@param bio string|nil +---@return table Updated user +function M.updateProfile(dbal, userId, displayName, avatar, bio) + local updates = { updatedAt = os.time() * 1000 } + + if displayName ~= nil then + updates.displayName = displayName + end + if avatar ~= nil then + updates.avatar = avatar + end + if bio ~= nil then + updates.bio = bio + end + + return dbal:update('User', userId, updates) +end + +---Change user password +---@param dbal table +---@param userId string +---@param hashedPassword string +---@return table Updated user +function M.changePassword(dbal, userId, hashedPassword) + return M.updateUser(dbal, userId, { + password = hashedPassword, + }) +end + +---Set user status +---@param dbal table +---@param userId string +---@param status string active|inactive|banned +---@return table Updated user +function M.setStatus(dbal, userId, status) + return M.updateUser(dbal, userId, { status = status }) +end + +---Activate user +---@param dbal table +---@param userId string +function M.activateUser(dbal, userId) + return M.setStatus(dbal, userId, 'active') +end + +---Deactivate user +---@param dbal table +---@param userId string +function M.deactivateUser(dbal, userId) + return M.setStatus(dbal, userId, 'inactive') +end + +---Ban user +---@param dbal table +---@param userId string +---@param reason string|nil +function M.banUser(dbal, userId, reason) + return M.updateUser(dbal, userId, { + status = 'banned', + banReason = reason, + bannedAt = os.time() * 1000, + }) +end + +---Unban user +---@param dbal table +---@param userId string +function M.unbanUser(dbal, userId) + return M.updateUser(dbal, userId, { + status = 'active', + banReason = nil, + bannedAt = nil, + }) +end + +--------------------------------------------------------------------------- +-- PERMISSION OPERATIONS +--------------------------------------------------------------------------- + +---Set user permission level +---@param dbal table +---@param userId string +---@param level number 0-6 +---@return table Updated user +function M.setLevel(dbal, userId, level) + if level < 0 or level > 6 then + error('Invalid permission level: ' .. tostring(level)) + end + return M.updateUser(dbal, userId, { level = level }) +end + +---Promote user by one level +---@param dbal table +---@param userId string +---@param maxLevel number|nil Maximum level to promote to +---@return table Updated user +function M.promoteUser(dbal, userId, maxLevel) + local user = M.getUser(dbal, userId) + if not user then + error('User not found: ' .. userId) + end + + local newLevel = math.min((user.level or 0) + 1, maxLevel or 6) + return M.setLevel(dbal, userId, newLevel) +end + +---Demote user by one level +---@param dbal table +---@param userId string +---@return table Updated user +function M.demoteUser(dbal, userId) + local user = M.getUser(dbal, userId) + if not user then + error('User not found: ' .. userId) + end + + local newLevel = math.max((user.level or 0) - 1, 0) + return M.setLevel(dbal, userId, newLevel) +end + +---Check if user has minimum permission level +---@param dbal table +---@param userId string +---@param requiredLevel number +---@return boolean +function M.hasPermission(dbal, userId, requiredLevel) + local user = M.getUser(dbal, userId) + if not user then + return false + end + return (user.level or 0) >= requiredLevel +end + +--------------------------------------------------------------------------- +-- BULK OPERATIONS +--------------------------------------------------------------------------- + +---List admins (level >= 3) +---@param dbal table +---@param tenantId string +---@return table[] Admin users +function M.listAdmins(dbal, tenantId) + local result = M.listUsers(dbal, tenantId, 'active', 3, 100, 0) + return result.items or {} +end + +---List moderators (level >= 2) +---@param dbal table +---@param tenantId string +---@return table[] Moderator users +function M.listModerators(dbal, tenantId) + local result = M.listUsers(dbal, tenantId, 'active', 2, 100, 0) + return result.items or {} +end + +---Count users by status +---@param dbal table +---@param tenantId string +---@return table Counts by status +function M.countByStatus(dbal, tenantId) + local all = M.listUsers(dbal, tenantId, nil, nil, 10000, 0) + + local counts = { + active = 0, + inactive = 0, + banned = 0, + total = 0, + } + + for _, user in ipairs(all.items or {}) do + counts.total = counts.total + 1 + local status = user.status or 'active' + counts[status] = (counts[status] or 0) + 1 + end + + return counts +end + +---Search users by username or email +---@param dbal table +---@param tenantId string +---@param query string +---@param take number|nil +---@return table[] Matching users +function M.searchUsers(dbal, tenantId, query, take) + local all = M.listUsers(dbal, tenantId, nil, nil, 1000, 0) + local matches = {} + local lowerQuery = query:lower() + + for _, user in ipairs(all.items or {}) do + local username = (user.username or ''):lower() + local email = (user.email or ''):lower() + local displayName = (user.displayName or ''):lower() + + if username:find(lowerQuery, 1, true) or + email:find(lowerQuery, 1, true) or + displayName:find(lowerQuery, 1, true) then + table.insert(matches, user) + if #matches >= (take or 20) then + break + end + end + end + + return matches +end + +---Delete user +---@param dbal table +---@param userId string +---@return boolean Success +function M.deleteUser(dbal, userId) + return dbal:delete('User', userId) +end + +return M diff --git a/packages/workflow_editor/seed/scripts/db/operations.lua b/packages/workflow_editor/seed/scripts/db/operations.lua new file mode 100644 index 000000000..d33d43f98 --- /dev/null +++ b/packages/workflow_editor/seed/scripts/db/operations.lua @@ -0,0 +1,361 @@ +-- workflow_editor/seed/scripts/db/operations.lua +-- DBAL operations for Workflow entities +-- @module workflow_editor.db.operations + +local M = {} +local json = require('json') + +--------------------------------------------------------------------------- +-- WORKFLOW OPERATIONS +--------------------------------------------------------------------------- + +---@class WorkflowCreateParams +---@field tenantId string +---@field name string +---@field description string|nil +---@field trigger table Trigger configuration +---@field nodes table[] Workflow nodes +---@field edges table[] Node connections +---@field createdBy string + +---Create a new workflow +---@param dbal table DBAL client instance +---@param params WorkflowCreateParams +---@return table Created workflow +function M.createWorkflow(dbal, params) + return dbal:create('Workflow', { + tenantId = params.tenantId, + name = params.name, + description = params.description, + status = 'draft', + version = 1, + trigger = json.encode(params.trigger or {}), + nodes = json.encode(params.nodes or {}), + edges = json.encode(params.edges or {}), + createdBy = params.createdBy, + createdAt = os.time() * 1000, + updatedAt = os.time() * 1000, + }) +end + +---Get workflow by ID +---@param dbal table +---@param workflowId string +---@return table|nil Workflow with parsed JSON +function M.getWorkflow(dbal, workflowId) + local workflow = dbal:read('Workflow', workflowId) + if workflow then + workflow.trigger = json.decode(workflow.trigger or '{}') + workflow.nodes = json.decode(workflow.nodes or '[]') + workflow.edges = json.decode(workflow.edges or '[]') + end + return workflow +end + +---List workflows +---@param dbal table +---@param tenantId string +---@param status string|nil draft|active|paused|archived +---@param take number|nil +---@param skip number|nil +---@return table List result +function M.listWorkflows(dbal, tenantId, status, take, skip) + local where = { tenantId = tenantId } + + if status then + where.status = status + end + + local result = dbal:list('Workflow', { + where = where, + orderBy = { updatedAt = 'desc' }, + take = take or 20, + skip = skip or 0, + }) + + return result +end + +---Update workflow +---@param dbal table +---@param workflowId string +---@param updates table +---@return table Updated workflow +function M.updateWorkflow(dbal, workflowId, updates) + if updates.trigger and type(updates.trigger) == 'table' then + updates.trigger = json.encode(updates.trigger) + end + if updates.nodes and type(updates.nodes) == 'table' then + updates.nodes = json.encode(updates.nodes) + end + if updates.edges and type(updates.edges) == 'table' then + updates.edges = json.encode(updates.edges) + end + + updates.updatedAt = os.time() * 1000 + return dbal:update('Workflow', workflowId, updates) +end + +---Save workflow nodes and edges +---@param dbal table +---@param workflowId string +---@param nodes table[] +---@param edges table[] +---@return table Updated workflow +function M.saveWorkflowGraph(dbal, workflowId, nodes, edges) + return M.updateWorkflow(dbal, workflowId, { + nodes = nodes, + edges = edges, + }) +end + +---Publish workflow (activate) +---@param dbal table +---@param workflowId string +---@return table Updated workflow +function M.publishWorkflow(dbal, workflowId) + local workflow = M.getWorkflow(dbal, workflowId) + if not workflow then + error('Workflow not found: ' .. workflowId) + end + + return M.updateWorkflow(dbal, workflowId, { + status = 'active', + version = (workflow.version or 0) + 1, + publishedAt = os.time() * 1000, + }) +end + +---Pause workflow +---@param dbal table +---@param workflowId string +function M.pauseWorkflow(dbal, workflowId) + return M.updateWorkflow(dbal, workflowId, { status = 'paused' }) +end + +---Resume workflow +---@param dbal table +---@param workflowId string +function M.resumeWorkflow(dbal, workflowId) + return M.updateWorkflow(dbal, workflowId, { status = 'active' }) +end + +---Archive workflow +---@param dbal table +---@param workflowId string +function M.archiveWorkflow(dbal, workflowId) + return M.updateWorkflow(dbal, workflowId, { status = 'archived' }) +end + +---Delete workflow +---@param dbal table +---@param workflowId string +---@return boolean Success +function M.deleteWorkflow(dbal, workflowId) + return dbal:delete('Workflow', workflowId) +end + +--------------------------------------------------------------------------- +-- WORKFLOW EXECUTION OPERATIONS +--------------------------------------------------------------------------- + +---@class WorkflowExecutionParams +---@field tenantId string +---@field workflowId string +---@field triggeredBy string|nil User or system +---@field input table|nil Input data + +---Start a workflow execution +---@param dbal table +---@param params WorkflowExecutionParams +---@return table Created execution +function M.startExecution(dbal, params) + return dbal:create('WorkflowExecution', { + tenantId = params.tenantId, + workflowId = params.workflowId, + status = 'running', + triggeredBy = params.triggeredBy or 'system', + input = params.input and json.encode(params.input) or nil, + startedAt = os.time() * 1000, + }) +end + +---Get execution by ID +---@param dbal table +---@param executionId string +---@return table|nil Execution +function M.getExecution(dbal, executionId) + local execution = dbal:read('WorkflowExecution', executionId) + if execution then + if execution.input then + execution.input = json.decode(execution.input) + end + if execution.output then + execution.output = json.decode(execution.output) + end + end + return execution +end + +---List executions for a workflow +---@param dbal table +---@param workflowId string +---@param status string|nil running|completed|failed|cancelled +---@param take number|nil +---@return table List result +function M.listExecutions(dbal, workflowId, status, take) + local where = { workflowId = workflowId } + + if status then + where.status = status + end + + return dbal:list('WorkflowExecution', { + where = where, + orderBy = { startedAt = 'desc' }, + take = take or 20, + }) +end + +---Update execution status +---@param dbal table +---@param executionId string +---@param status string +---@param output table|nil +---@param error string|nil +function M.updateExecution(dbal, executionId, status, output, error) + local updates = { status = status } + + if status == 'completed' or status == 'failed' or status == 'cancelled' then + updates.completedAt = os.time() * 1000 + end + + if output then + updates.output = json.encode(output) + end + + if error then + updates.error = error + end + + return dbal:update('WorkflowExecution', executionId, updates) +end + +---Complete execution +---@param dbal table +---@param executionId string +---@param output table|nil +function M.completeExecution(dbal, executionId, output) + return M.updateExecution(dbal, executionId, 'completed', output, nil) +end + +---Fail execution +---@param dbal table +---@param executionId string +---@param error string +function M.failExecution(dbal, executionId, error) + return M.updateExecution(dbal, executionId, 'failed', nil, error) +end + +---Cancel execution +---@param dbal table +---@param executionId string +function M.cancelExecution(dbal, executionId) + return M.updateExecution(dbal, executionId, 'cancelled', nil, nil) +end + +--------------------------------------------------------------------------- +-- NODE STEP LOGGING +--------------------------------------------------------------------------- + +---Log a node step +---@param dbal table +---@param executionId string +---@param nodeId string +---@param status string pending|running|completed|failed|skipped +---@param input table|nil +---@param output table|nil +---@param error string|nil +function M.logNodeStep(dbal, executionId, nodeId, status, input, output, error) + return dbal:create('WorkflowStep', { + executionId = executionId, + nodeId = nodeId, + status = status, + input = input and json.encode(input) or nil, + output = output and json.encode(output) or nil, + error = error, + timestamp = os.time() * 1000, + }) +end + +---Get steps for an execution +---@param dbal table +---@param executionId string +---@return table[] Steps in order +function M.getExecutionSteps(dbal, executionId) + local result = dbal:list('WorkflowStep', { + where = { executionId = executionId }, + orderBy = { timestamp = 'asc' }, + take = 1000, + }) + return result.items or {} +end + +--------------------------------------------------------------------------- +-- WORKFLOW TEMPLATES +--------------------------------------------------------------------------- + +---List workflow templates +---@param dbal table +---@param category string|nil +---@return table[] Templates +function M.listTemplates(dbal, category) + local where = { isTemplate = true } + + local result = dbal:list('Workflow', { + where = where, + orderBy = { name = 'asc' }, + take = 50, + }) + + local templates = result.items or {} + + -- Filter by category if specified + if category then + local filtered = {} + for _, t in ipairs(templates) do + if t.category == category then + table.insert(filtered, t) + end + end + templates = filtered + end + + return templates +end + +---Create workflow from template +---@param dbal table +---@param templateId string +---@param tenantId string +---@param name string +---@param createdBy string +---@return table Created workflow +function M.createFromTemplate(dbal, templateId, tenantId, name, createdBy) + local template = M.getWorkflow(dbal, templateId) + if not template then + error('Template not found: ' .. templateId) + end + + return M.createWorkflow(dbal, { + tenantId = tenantId, + name = name, + description = 'Created from template: ' .. template.name, + trigger = template.trigger, + nodes = template.nodes, + edges = template.edges, + createdBy = createdBy, + }) +end + +return M