@gzl10/nexus-backend 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-error-CKbYJQ9V.d.ts +136 -0
- package/dist/cli.js +2769 -9275
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +116 -142
- package/dist/index.js +1126 -359
- package/dist/index.js.map +1 -1
- package/dist/main.js +1136 -359
- package/dist/main.js.map +1 -1
- package/dist/migration-helpers/index.d.ts +63 -0
- package/dist/migration-helpers/index.js +12116 -0
- package/dist/migration-helpers/index.js.map +1 -0
- package/dist/testing/index.d.ts +81 -0
- package/dist/testing/index.js +1675 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +26 -23
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/db/migration-engine.ts","../../src/core/openapi/schema-builder.ts","../../src/core/openapi/path-builder.ts","../../src/core/plugin-ops.ts","../../src/engine/table-prefix.ts","../../src/engine/module-store.ts","../../src/core/utils/id.ts","../../src/engine/definition-extractors.ts","../../src/engine/registry.ts","../../src/config/paths.ts","../../src/engine/module-queries.ts","../../src/core/logger/proxy.ts","../../src/core/logger/config.ts","../../src/core/logger/service.ts","../../src/core/logger/debounce.ts","../../src/core/logger/index.ts","../../src/modules/logger/logger.entity.ts","../../src/modules/logger/index.ts","../../src/modules/masters/definitions.ts","../../src/modules/masters/registry.ts","../../src/modules/masters/data/currencies.json","../../src/modules/masters/data/languages.json","../../src/modules/masters/data/timezones.json","../../src/modules/masters/data/social-networks.json","../../src/modules/masters/data/genders.json","../../src/modules/masters/data/marital-statuses.json","../../src/modules/masters/data/education-levels.json","../../src/modules/masters/data/industries.json","../../src/modules/masters/data/company-types.json","../../src/modules/masters/data/units.json","../../src/modules/masters/data/countries.json","../../src/modules/masters/data/product-categories.json","../../src/modules/masters/data/phone-prefixes.json","../../src/modules/masters/data/document-types.json","../../src/modules/masters/constants.ts","../../src/modules/masters/actions/install-type.ts","../../src/modules/masters/actions/uninstall-type.ts","../../src/modules/masters/index.ts","../../src/modules/system/system.helpers.ts","../../src/modules/system/system.controller.ts","../../src/modules/system/system.routes.ts","../../src/modules/system/system.entity.ts","../../src/modules/system/migration-history.entity.ts","../../src/modules/system/env-config.entity.ts","../../src/modules/system/env-config.registry.ts","../../src/modules/system/actions/factory-reset.action.ts","../../src/modules/system/actions/restart-server.action.ts","../../src/modules/system/index.ts","../../src/modules/ui-settings/ui-branding.entity.ts","../../src/modules/ui-settings/ui-theme.entity.ts","../../src/modules/ui-settings/ui-effects.entity.ts","../../src/modules/ui-settings/ui-accessibility.entity.ts","../../src/modules/ui-settings/index.ts","../../src/modules/storage/drivers/filesystem.driver.ts","../../src/modules/storage/drivers/s3.driver.ts","../../src/modules/storage/storage.config.ts","../../src/modules/storage/storage.service.ts","../../src/modules/storage/storage.entity.ts","../../src/modules/storage/actions/helpers.ts","../../src/modules/storage/actions/upload.action.ts","../../src/modules/storage/actions/upload-image.action.ts","../../src/modules/storage/actions/index.ts","../../src/modules/storage/storage.routes.ts","../../src/modules/storage/index.ts","../../src/modules/users/users.entity.ts","../../src/modules/users/users.routes.ts","../../src/modules/users/users.service.ts","../../src/modules/users/index.ts","../../src/modules/auth/auth.entity.ts","../../src/modules/auth/auth.routes.ts","../../src/modules/auth/auth.config.ts","../../src/modules/auth/auth.middleware.ts","../../src/modules/auth/auth.pat.entity.ts","../../src/modules/auth/actions/providers.action.ts","../../src/modules/auth/actions/helpers.ts","../../src/modules/auth/actions/login.action.ts","../../src/modules/auth/actions/register.action.ts","../../src/modules/auth/actions/forgot-password.action.ts","../../src/modules/auth/actions/reset-password.action.ts","../../src/modules/auth/actions/refresh.action.ts","../../src/modules/auth/actions/me.action.ts","../../src/modules/auth/actions/logout.action.ts","../../src/modules/auth/actions/logout-all.action.ts","../../src/modules/auth/actions/sessions.action.ts","../../src/modules/auth/actions/revoke-session.action.ts","../../src/modules/auth/actions/stop-impersonate.action.ts","../../src/modules/auth/actions/create-token.action.ts","../../src/modules/auth/actions/list-tokens.action.ts","../../src/modules/auth/actions/revoke-token.action.ts","../../src/modules/auth/actions/impersonate.action.ts","../../src/modules/auth/actions/index.ts","../../src/modules/auth/jwt.utils.ts","../../src/modules/auth/otp-manager.ts","../../src/modules/auth/auth.service.ts","../../src/modules/auth/auth.types.ts","../../src/modules/auth/index.ts","../../src/modules/mail/mail.config.ts","../../src/modules/mail/mail.service.ts","../../src/modules/mail/mail.entity.ts","../../src/modules/mail/mail.routes.ts","../../src/modules/mail/index.ts","../../src/modules/observability/observability.service.ts","../../src/modules/observability/observability.config.ts","../../src/modules/observability/observability.entity.ts","../../src/modules/observability/diagnostics.action.ts","../../src/modules/observability/index.ts","../../src/modules/plugins/plugins.helpers.ts","../../src/modules/plugins/actions/install-plugin.action.ts","../../src/modules/plugins/actions/uninstall-plugin.action.ts","../../src/modules/plugins/actions/toggle-plugin.action.ts","../../src/modules/plugins/plugins.entity.ts","../../src/modules/plugins/plugins.routes.ts","../../src/modules/plugins/index.ts","../../src/modules/audit/audit.entity.ts","../../src/modules/audit/audit.service.ts","../../src/modules/audit/index.ts","../../src/modules/index.ts","../../src/engine/loader.ts","../../src/engine/subject-extractor.ts","../../src/db/sql-utils.ts","../../src/db/sqlite-compat.ts","../../src/runtime/types.ts","../../src/runtime/helpers/compose-hooks.ts","../../src/runtime/helpers/sensitive-fields.ts","../../src/runtime/helpers/casl-filter.ts","../../src/runtime/helpers/seed-loader.ts","../../src/runtime/helpers/index.ts","../../src/db/filter-helpers.ts","../../src/runtime/services/base.service.ts","../../src/runtime/services/collection.service.ts","../../src/runtime/services/single.service.ts","../../src/runtime/services/view.service.ts","../../src/runtime/services/computed.service.ts","../../src/runtime/services/external.service.ts","../../src/runtime/validation/schema-builder.ts","../../src/runtime/validation/index.ts","../../src/runtime/controllers/entity.controller.ts","../../src/core/sse/batch-reporter.ts","../../src/core/sse/index.ts","../../src/runtime/routes/entity.routes.ts","../../src/runtime/entity-factory.ts","../../src/runtime/routes/action.routes.ts","../../src/runtime/index.ts","../../src/db/seed-runner.ts","../../src/core/utils/sequence.ts","../../src/db/ensure-system-tables.ts","../../src/db/migration-sources.ts","../../src/config/env.ts","../../src/config/database.ts","../../src/db/query-interceptor.ts","../../src/db/connection.ts","../../src/db/migration-lock.ts","../../src/db/migration-runner.ts","../../src/db/schema-reader.ts","../../src/db/migration-helpers.ts","../../src/db/migration-generator.ts","../../src/core/errors/error-codes.ts","../../src/core/errors/app-error.ts","../../src/db/knex-adapter.ts","../../src/db/memory-adapter.ts","../../src/db/redis-adapter.ts","../../src/db/schema-adapter.ts","../../src/db/memory-schema-adapter.ts","../../src/db/memory-knex.ts","../../src/db/query-helpers.ts","../../src/db/memory-table-builder.ts","../../src/db/index.ts","../../src/config/load-config.ts","../../src/engine/events-api.ts","../../src/core/cache/lru-cache.ts","../../src/core/cache/managed-cache.ts","../../src/core/cache/redis-managed-cache.ts","../../src/core/cache/scoped-cache-manager.ts","../../src/core/cache/cache-manager.ts","../../src/engine/context.ts","../../src/engine/index.ts","../../src/core/openapi/generator.ts","../../src/core/openapi/index.ts","../../src/core/abilities/ability.factory.ts","../../src/core/jwt/index.ts","../../src/core/module-routes.ts","../../src/core/middleware/error.middleware.ts","../../src/core/middleware/request-id.middleware.ts","../../src/core/middleware/nexus-client.middleware.ts","../../src/core/spa-handler.ts","../../src/core/utils/cors.ts","../../src/core/app.ts","../../src/core/events-hub/emitter.ts","../../src/core/events-hub/socket.ts","../../src/core/events-hub/realtime-debouncer.ts","../../src/core/events-hub/event-bridge.ts","../../src/core/utils/port-check.ts","../../src/core/tunnel.ts","../../src/instrumentation.ts","../../src/core/server.ts","../../src/core/events-hub/room-utils.ts","../../src/core/middleware/ability.middleware.ts","../../src/core/middleware/rate-limit.middleware.ts","../../src/core/middleware/validate.middleware.ts","../../src/core/middleware/timeout.middleware.ts","../../src/core/crypto/hash.ts","../../src/core/crypto/symmetric.ts","../../src/core/crypto/index.ts","../../src/core/utils/safe-json.ts","../../src/core/cache/index.ts","../../src/core/index.ts","../../src/db/schema-helpers.ts","../../src/migration-helpers/index.ts"],"sourcesContent":["/**\n * Migration engine for tests\n *\n * This module provides utilities to run migrations from entity definitions,\n * primarily used in integration tests to set up the database schema.\n *\n * For production, use the file-based migration system (migration-runner.ts).\n */\nimport type { Knex } from 'knex'\nimport type { ModuleManifest, ModuleContext, EntityDefinition, FieldDefinition, FieldDbConfig, EntityIndex } from '@gzl10/nexus-sdk'\n\n/**\n * Entity types that have their own database table.\n */\nconst PERSISTENT_TYPES = new Set([\n 'collection',\n 'tree',\n 'dag',\n 'event',\n 'reference',\n 'config',\n 'temp',\n])\n\n/**\n * Create all tables from all modules in FK-dependency order.\n * Collects all entity definitions, sorts by FK references, and creates inline.\n * This ensures cross-module FKs work correctly (including SQLite).\n */\nexport async function runAllGeneratedMigrations(\n ctx: ModuleContext,\n modules: ModuleManifest[]\n): Promise<void> {\n if (process.env['NODE_ENV'] !== 'test') {\n throw new Error('runAllGeneratedMigrations() is only allowed in test mode.')\n }\n\n const db = ctx.db?.knex ?? (ctx as unknown as { db: Knex }).db\n\n // Collect all persistent entities with table names\n const entities: Array<{ tableName: string; entityDef: EntityDefinition & { table?: string } }> = []\n\n for (const mod of modules) {\n if (!mod.definitions) continue\n for (const def of mod.definitions) {\n const entityType = def.type ?? 'collection'\n if (!PERSISTENT_TYPES.has(entityType)) continue\n const entityDef = def as EntityDefinition & { table?: string }\n if (!entityDef.table) continue\n entities.push({ tableName: entityDef.table, entityDef })\n }\n }\n\n // Sort entities: tables without FK deps first, then tables that reference already-created tables\n const sorted = topologicalSortEntities(entities)\n\n for (const { tableName, entityDef } of sorted) {\n // Skip if table already exists\n const exists = await db.schema.hasTable(tableName)\n if (exists) continue\n\n // Create table with FKs inline\n await db.schema.createTable(tableName, (table) => {\n if (entityDef.fields) {\n for (const [fieldName, field] of Object.entries(entityDef.fields)) {\n if (!field.db || field.db.virtual) continue\n addColumn(table, fieldName, field, db)\n }\n }\n\n const hasTimestamps = (entityDef as EntityDefinition & { timestamps?: boolean }).timestamps\n if (hasTimestamps) {\n table.timestamp('created_at').nullable().defaultTo(db.fn.now())\n table.timestamp('updated_at').nullable().defaultTo(db.fn.now())\n }\n\n const hasAudit = (entityDef as EntityDefinition & { audit?: boolean }).audit\n if (hasAudit) {\n table.string('created_by', 26).nullable()\n table.string('updated_by', 26).nullable()\n }\n\n // Auto-field: soft delete\n const hasSoftDelete = (entityDef as EntityDefinition & { softDelete?: boolean }).softDelete\n if (hasSoftDelete) {\n table.timestamp('deleted_at').nullable()\n }\n\n // Auto-field: tree parent reference\n if (entityDef.type === 'tree') {\n table.string('parent_id', 26).nullable().references(`${tableName}.id`).onDelete('SET NULL')\n }\n })\n\n // Create entity-level indexes\n const entityIndexes = (entityDef as EntityDefinition & { indexes?: EntityIndex[] }).indexes\n if (entityIndexes?.length) {\n await db.schema.alterTable(tableName, (table) => {\n for (const idx of entityIndexes) {\n if (idx.unique) {\n table.unique(idx.columns)\n } else {\n table.index(idx.columns)\n }\n }\n })\n }\n }\n}\n\n/**\n * Topological sort of entities by FK references.\n * Entities that reference other tables come after the referenced tables.\n */\nfunction topologicalSortEntities(\n entities: Array<{ tableName: string; entityDef: EntityDefinition & { table?: string } }>\n): Array<{ tableName: string; entityDef: EntityDefinition & { table?: string } }> {\n const tableSet = new Set(entities.map(e => e.tableName))\n\n // Build dependency graph: tableName → set of tables it references\n const deps = new Map<string, Set<string>>()\n for (const { tableName, entityDef } of entities) {\n const refs = new Set<string>()\n if (entityDef.fields) {\n for (const field of Object.values(entityDef.fields)) {\n const relation = (field as FieldDefinition & { relation?: { table: string } }).relation\n if (relation && tableSet.has(relation.table) && relation.table !== tableName) {\n refs.add(relation.table)\n }\n }\n }\n deps.set(tableName, refs)\n }\n\n // Kahn's algorithm\n const sorted: Array<{ tableName: string; entityDef: EntityDefinition & { table?: string } }> = []\n const entityMap = new Map(entities.map(e => [e.tableName, e]))\n const inDegree = new Map<string, number>()\n\n for (const [table, tableDeps] of deps) {\n inDegree.set(table, tableDeps.size)\n }\n\n const queue: string[] = []\n for (const [table, degree] of inDegree) {\n if (degree === 0) queue.push(table)\n }\n\n while (queue.length > 0) {\n const table = queue.shift()!\n const entity = entityMap.get(table)\n if (entity) sorted.push(entity)\n\n // Reduce in-degree for dependents\n for (const [other, otherDeps] of deps) {\n if (otherDeps.has(table)) {\n otherDeps.delete(table)\n const newDegree = (inDegree.get(other) ?? 1) - 1\n inDegree.set(other, newDegree)\n if (newDegree === 0) queue.push(other)\n }\n }\n }\n\n // Add any remaining (circular deps) at the end\n for (const entity of entities) {\n if (!sorted.some(s => s.tableName === entity.tableName)) {\n sorted.push(entity)\n }\n }\n\n return sorted\n}\n\n/**\n * @deprecated Use runAllGeneratedMigrations instead.\n * Kept for backward compatibility with tests that use it directly.\n */\nexport async function runGeneratedMigration(\n ctx: ModuleContext,\n module: ModuleManifest\n): Promise<void> {\n await runAllGeneratedMigrations(ctx, [module])\n}\n\n/**\n * Add a column to a table based on field definition.\n * FKs are applied inline (works for all DBs including SQLite).\n */\nfunction addColumn(\n table: Knex.CreateTableBuilder,\n fieldName: string,\n field: FieldDefinition,\n db: Knex\n): void {\n const columnName = fieldName\n const dbConfig = field.db as FieldDbConfig\n\n let column: Knex.ColumnBuilder\n\n switch (dbConfig.type) {\n case 'string':\n column = table.string(columnName, dbConfig.size || 255)\n break\n case 'text':\n column = table.text(columnName)\n break\n case 'integer':\n column = table.integer(columnName)\n break\n case 'decimal': {\n const precision = dbConfig.precision\n if (Array.isArray(precision)) {\n column = table.decimal(columnName, precision[0], precision[1])\n } else {\n column = table.decimal(columnName, precision || 10, 2)\n }\n break\n }\n case 'boolean':\n column = table.boolean(columnName)\n break\n case 'date':\n column = table.date(columnName)\n break\n case 'datetime':\n column = table.timestamp(columnName)\n break\n case 'json':\n column = table.json(columnName)\n break\n case 'uuid':\n column = table.uuid(columnName)\n break\n case 'array':\n column = table.json(columnName)\n break\n default:\n column = table.string(columnName, 255)\n }\n\n // Handle nullable\n if (dbConfig.nullable === false) {\n column.notNullable()\n } else {\n column.nullable()\n }\n\n // Handle primary key\n if (dbConfig.primary || dbConfig.idType || columnName === 'id') {\n column.primary()\n }\n\n // Handle unique\n if (dbConfig.unique) {\n column.unique()\n }\n\n // Handle default value\n if (dbConfig.default !== undefined) {\n // For JSON/array columns, Knex MySQL needs an object/array (not a string)\n // to generate the correct DEFAULT ('...') expression\n const isJsonColumn = dbConfig.type === 'json' || dbConfig.type === 'array'\n if (isJsonColumn && typeof dbConfig.default === 'string') {\n try {\n column.defaultTo(JSON.parse(dbConfig.default))\n } catch {\n column.defaultTo(dbConfig.default)\n }\n } else {\n column.defaultTo(dbConfig.default)\n }\n } else if (dbConfig.defaultFn === 'now') {\n column.defaultTo(db.fn.now())\n }\n\n // Handle foreign key via relation config (inline)\n const relation = (field as FieldDefinition & { relation?: { table: string; column?: string; onDelete?: string; onUpdate?: string } }).relation\n if (relation) {\n const fkRef = column.references(relation.column || 'id').inTable(relation.table)\n if (relation.onDelete) {\n fkRef.onDelete(relation.onDelete)\n }\n if (relation.onUpdate) {\n fkRef.onUpdate(relation.onUpdate)\n }\n }\n}\n","import type { SchemaObject } from 'openapi3-ts/oas31'\nimport type { CollectionEntityDefinition, FieldDefinition, ActionDefinition } from '@gzl10/nexus-sdk'\nimport { resolveLocalized } from '@gzl10/nexus-sdk'\n\n/**\n * Maps EntityDefinition db.type to an OpenAPI type\n */\nfunction mapDbType(dbType: string): SchemaObject {\n switch (dbType) {\n case 'string':\n case 'text':\n return { type: 'string' }\n case 'integer':\n return { type: 'integer' }\n case 'float':\n case 'decimal':\n return { type: 'number' }\n case 'boolean':\n return { type: 'boolean' }\n case 'json':\n return { type: 'object' }\n case 'date':\n return { type: 'string', format: 'date' }\n case 'datetime':\n case 'timestamp':\n return { type: 'string', format: 'date-time' }\n default:\n return { type: 'string' }\n }\n}\n\n/**\n * Maps input type to OpenAPI type (for actions without db config)\n */\nfunction mapInputType(inputType: string): SchemaObject {\n switch (inputType) {\n case 'number':\n case 'decimal':\n case 'rate':\n return { type: 'number' }\n case 'checkbox':\n case 'switch':\n return { type: 'boolean' }\n case 'select':\n case 'radio':\n return { type: 'string' }\n case 'multiselect':\n case 'tags':\n case 'transfer':\n return { type: 'array', items: { type: 'string' } }\n case 'json':\n return { type: 'object' }\n case 'file':\n case 'image':\n return { type: 'string', description: 'File ID' }\n case 'multifile':\n case 'multiimage':\n return { type: 'array', items: { type: 'string' }, description: 'File IDs' }\n case 'date':\n return { type: 'string', format: 'date' }\n case 'datetime':\n return { type: 'string', format: 'date-time' }\n case 'email':\n return { type: 'string', format: 'email' }\n case 'url':\n return { type: 'string', format: 'uri' }\n case 'uuid':\n return { type: 'string', format: 'uuid' }\n case 'password':\n return { type: 'string', format: 'password' }\n default:\n return { type: 'string' }\n }\n}\n\n/**\n * Converts a FieldDefinition to an OpenAPI SchemaObject\n */\nfunction fieldToSchema(field: FieldDefinition): SchemaObject {\n // Use db.type if available, otherwise infer from input type\n const dbType = field.db?.type\n const schema: SchemaObject = dbType\n ? { ...mapDbType(dbType) }\n : { ...mapInputType(field.input ?? 'text') }\n\n // Add description from label\n if (field.label) {\n schema.description = resolveLocalized(field.label, 'en')\n }\n\n // Constraints from validation\n if (field.validation) {\n const v = field.validation\n if (v.min !== undefined) {\n if (schema.type === 'string') schema.minLength = v.min\n else if (schema.type === 'integer' || schema.type === 'number') schema.minimum = v.min\n }\n if (v.max !== undefined) {\n if (schema.type === 'string') schema.maxLength = v.max\n else if (schema.type === 'integer' || schema.type === 'number') schema.maximum = v.max\n }\n if (v.enum) schema.enum = v.enum\n if (v.format === 'email') schema.format = 'email'\n if (v.pattern) schema.pattern = v.pattern\n }\n\n // Enum from static options\n if (field.options?.static && !schema.enum) {\n schema.enum = field.options.static.map(opt => opt.value)\n }\n\n return schema\n}\n\n/**\n * Generates OpenAPI Schema from CollectionEntityDefinition\n */\nexport function entityToSchema(entity: CollectionEntityDefinition): SchemaObject {\n const properties: Record<string, SchemaObject> = {}\n const required: string[] = []\n\n for (const [name, field] of Object.entries(entity.fields)) {\n // Skip password and sensitive fields in output\n if (entity.casl?.sensitiveFields?.includes(name)) continue\n\n properties[name] = fieldToSchema(field)\n\n if (field.required && field.db && !field.db.nullable) {\n required.push(name)\n }\n }\n\n // Timestamps if enabled\n if (entity.timestamps) {\n properties['created_at'] = { type: 'string', format: 'date-time' }\n properties['updated_at'] = { type: 'string', format: 'date-time' }\n }\n\n return {\n type: 'object',\n properties,\n required: required.length > 0 ? required : undefined\n }\n}\n\n/**\n * Generates schema for create input (no id, no timestamps)\n */\nexport function entityToCreateSchema(entity: CollectionEntityDefinition): SchemaObject {\n const properties: Record<string, SchemaObject> = {}\n const required: string[] = []\n\n for (const [name, field] of Object.entries(entity.fields)) {\n // Skip id and auto-generated fields\n if (name === 'id') continue\n if (field.db?.defaultFn) continue // Auto-generated values\n\n properties[name] = fieldToSchema(field)\n\n if (field.required) {\n required.push(name)\n }\n }\n\n return {\n type: 'object',\n properties,\n required: required.length > 0 ? required : undefined\n }\n}\n\n/**\n * Generates schema for update input (all optional)\n */\nexport function entityToUpdateSchema(entity: CollectionEntityDefinition): SchemaObject {\n const properties: Record<string, SchemaObject> = {}\n\n for (const [name, field] of Object.entries(entity.fields)) {\n // Skip id\n if (name === 'id') continue\n\n properties[name] = fieldToSchema(field)\n }\n\n return {\n type: 'object',\n properties\n // No required - everything is optional in update\n }\n}\n\n/**\n * Generates schema from action fields (for action input)\n */\nexport function actionToInputSchema(action: ActionDefinition): SchemaObject {\n if (!action.input) {\n return { type: 'object' }\n }\n\n const properties: Record<string, SchemaObject> = {}\n const required: string[] = []\n\n for (const [name, field] of Object.entries(action.input)) {\n properties[name] = fieldToSchema(field as FieldDefinition)\n\n if ((field as FieldDefinition).required) {\n required.push(name)\n }\n }\n\n return {\n type: 'object',\n properties,\n required: required.length > 0 ? required : undefined\n }\n}\n\n/**\n * Generates schema from a generic fields object\n */\nexport function fieldsToSchema(\n fields: Record<string, unknown>,\n options?: { allOptional?: boolean }\n): SchemaObject {\n const properties: Record<string, SchemaObject> = {}\n const required: string[] = []\n\n for (const [name, field] of Object.entries(fields)) {\n const f = field as FieldDefinition\n properties[name] = fieldToSchema(f)\n\n if (!options?.allOptional && f.required) {\n required.push(name)\n }\n }\n\n return {\n type: 'object',\n properties,\n required: required.length > 0 ? required : undefined\n }\n}\n","import type { PathItemObject, OperationObject, ResponsesObject, ParameterObject, SchemaObjectType } from 'openapi3-ts/oas31'\nimport type {\n CollectionEntityDefinition,\n SingleEntityDefinition,\n // ConfigEntityDefinition absorbed into SingleEntityDefinition\n EventEntityDefinition,\n TreeEntityDefinition,\n DagEntityDefinition,\n ViewEntityDefinition,\n ComputedEntityDefinition,\n ActionDefinition,\n CustomRouteDefinition\n} from '@gzl10/nexus-sdk'\nimport { resolveLocalized } from '@gzl10/nexus-sdk'\n\n// ============================================================================\n// Shared Parameter Refs\n// ============================================================================\n\n/** Shared pagination parameter refs — registered in generator.ts components.parameters */\nconst listParameters = [\n { $ref: '#/components/parameters/PageParam' },\n { $ref: '#/components/parameters/LimitParam' },\n { $ref: '#/components/parameters/SortParam' },\n { $ref: '#/components/parameters/OrderParam' },\n { $ref: '#/components/parameters/SearchParam' },\n { $ref: '#/components/parameters/FiltersParam' }\n]\n\n// ============================================================================\n// Response Helpers\n// ============================================================================\n\nconst paginatedResponse = (schemaRef: string): ResponsesObject => ({\n '200': {\n description: 'Paginated list',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n items: { type: 'array', items: { $ref: schemaRef } },\n total: { type: 'integer' },\n page: { type: 'integer' },\n limit: { type: 'integer' },\n totalPages: { type: 'integer' },\n hasNext: { type: 'boolean' }\n }\n }\n }\n }\n }\n})\n\nconst itemResponse = (schemaRef: string, description: string): ResponsesObject => ({\n '200': {\n description,\n content: { 'application/json': { schema: { $ref: schemaRef } } }\n },\n '404': {\n description: 'Not found',\n content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } }\n }\n})\n\nconst actionResponse = (description: string): ResponsesObject => ({\n '200': {\n description,\n content: {\n 'application/json': {\n schema: { type: 'object', additionalProperties: true }\n }\n }\n },\n '400': {\n description: 'Validation error',\n content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } }\n }\n})\n\nconst errorResponses: ResponsesObject = {\n '400': {\n description: 'Validation error',\n content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } }\n },\n '401': {\n description: 'Unauthorized',\n content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } }\n },\n '403': {\n description: 'Forbidden',\n content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } }\n },\n '404': {\n description: 'Not found',\n content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } }\n }\n}\n\n/** Count endpoint response */\nconst countResponse: ResponsesObject = {\n '200': {\n description: 'Count result',\n content: { 'application/json': { schema: { type: 'object', properties: { count: { type: 'integer' } } } } }\n }\n}\n\n// ============================================================================\n// Collection Entity (full CRUD)\n// ============================================================================\n\n/**\n * Generates CRUD paths for a CollectionEntityDefinition\n */\nexport function collectionToPaths(\n entity: CollectionEntityDefinition,\n basePath: string,\n schemaName: string,\n tag: string\n): Record<string, PathItemObject> {\n const schemaRef = `#/components/schemas/${schemaName}`\n const createSchemaRef = `#/components/schemas/${schemaName}Create`\n const updateSchemaRef = `#/components/schemas/${schemaName}Update`\n const label = resolveLocalized(entity.label, 'en')\n\n const paths: Record<string, PathItemObject> = {}\n\n // GET /count\n paths[`${basePath}/count`] = {\n get: {\n tags: [tag],\n summary: `Count ${label}`,\n operationId: `count${schemaName}`,\n parameters: [{ $ref: '#/components/parameters/FiltersParam' }],\n responses: countResponse\n } as OperationObject\n }\n\n // GET /items (list) y POST /items (create)\n paths[basePath] = {\n get: {\n tags: [tag],\n summary: `List ${label}`,\n operationId: `list${schemaName}`,\n parameters: listParameters,\n responses: paginatedResponse(schemaRef)\n } as OperationObject,\n post: {\n tags: [tag],\n summary: `Create ${label}`,\n operationId: `create${schemaName}`,\n requestBody: {\n required: true,\n content: { 'application/json': { schema: { $ref: createSchemaRef } } }\n },\n responses: {\n '201': {\n description: 'Created',\n content: { 'application/json': { schema: { $ref: schemaRef } } }\n },\n ...errorResponses\n }\n } as OperationObject\n }\n\n // GET /items/:id, PUT /items/:id, DELETE /items/:id\n paths[`${basePath}/{id}`] = {\n get: {\n tags: [tag],\n summary: `Get ${label} by ID`,\n operationId: `get${schemaName}ById`,\n parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],\n responses: itemResponse(schemaRef, `${label} found`)\n } as OperationObject,\n put: {\n tags: [tag],\n summary: `Update ${label}`,\n operationId: `update${schemaName}`,\n parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],\n requestBody: {\n required: true,\n content: { 'application/json': { schema: { $ref: updateSchemaRef } } }\n },\n responses: itemResponse(schemaRef, `${label} updated`)\n } as OperationObject,\n delete: {\n tags: [tag],\n summary: `Delete ${label}`,\n operationId: `delete${schemaName}`,\n parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],\n responses: { '204': { description: 'Deleted' }, ...errorResponses }\n } as OperationObject\n }\n\n return paths\n}\n\n// Backward compatibility alias\nexport const entityToPaths = collectionToPaths\n\n// ============================================================================\n// Single Entity (GET/PUT only, stored in single_records)\n// ============================================================================\n\nexport function singleToPaths(\n entity: SingleEntityDefinition,\n basePath: string,\n schemaName: string,\n tag: string\n): Record<string, PathItemObject> {\n const schemaRef = `#/components/schemas/${schemaName}`\n const updateSchemaRef = `#/components/schemas/${schemaName}Update`\n const label = resolveLocalized(entity.label, 'en')\n\n return {\n [basePath]: {\n get: {\n tags: [tag],\n summary: `Get ${label}`,\n operationId: `get${schemaName}`,\n responses: {\n '200': {\n description: label,\n content: { 'application/json': { schema: { $ref: schemaRef } } }\n }\n }\n } as OperationObject,\n put: {\n tags: [tag],\n summary: `Update ${label}`,\n operationId: `update${schemaName}`,\n requestBody: {\n required: true,\n content: { 'application/json': { schema: { $ref: updateSchemaRef } } }\n },\n responses: {\n '200': {\n description: `${label} updated`,\n content: { 'application/json': { schema: { $ref: schemaRef } } }\n },\n ...errorResponses\n }\n } as OperationObject\n }\n }\n}\n\n// ============================================================================\n// Config Entity (GET/PUT with scope parameter)\n// ============================================================================\n\nexport function configToPaths(\n entity: SingleEntityDefinition,\n basePath: string,\n schemaName: string,\n tag: string\n): Record<string, PathItemObject> {\n const schemaRef = `#/components/schemas/${schemaName}`\n const updateSchemaRef = `#/components/schemas/${schemaName}Update`\n const label = resolveLocalized(entity.label, 'en')\n const scopeField = entity.scopeField ?? 'scope'\n\n return {\n [basePath]: {\n get: {\n tags: [tag],\n summary: `List ${label} configs`,\n operationId: `list${schemaName}`,\n responses: paginatedResponse(schemaRef)\n } as OperationObject\n },\n [`${basePath}/{${scopeField}}`]: {\n get: {\n tags: [tag],\n summary: `Get ${label} by ${scopeField}`,\n operationId: `get${schemaName}ByScope`,\n parameters: [{ name: scopeField, in: 'path', required: true, schema: { type: 'string' } }],\n responses: itemResponse(schemaRef, `${label} config`)\n } as OperationObject,\n put: {\n tags: [tag],\n summary: `Update ${label} config`,\n operationId: `update${schemaName}`,\n parameters: [{ name: scopeField, in: 'path', required: true, schema: { type: 'string' } }],\n requestBody: {\n required: true,\n content: { 'application/json': { schema: { $ref: updateSchemaRef } } }\n },\n responses: itemResponse(schemaRef, `${label} updated`)\n } as OperationObject\n }\n }\n}\n\n// ============================================================================\n// Event Entity (GET list only, read-only append-only log)\n// ============================================================================\n\nexport function eventToPaths(\n entity: EventEntityDefinition,\n basePath: string,\n schemaName: string,\n tag: string\n): Record<string, PathItemObject> {\n const schemaRef = `#/components/schemas/${schemaName}`\n const label = resolveLocalized(entity.label, 'en')\n\n return {\n [basePath]: {\n get: {\n tags: [tag],\n summary: `List ${label}`,\n operationId: `list${schemaName}`,\n parameters: [\n { name: 'page', in: 'query', schema: { type: 'integer', default: 1 } },\n { name: 'limit', in: 'query', schema: { type: 'integer', default: 20 } },\n { name: 'sort', in: 'query', schema: { type: 'string' } },\n { name: 'order', in: 'query', schema: { type: 'string', enum: ['asc', 'desc'] } }\n ],\n responses: paginatedResponse(schemaRef)\n } as OperationObject\n },\n [`${basePath}/{id}`]: {\n get: {\n tags: [tag],\n summary: `Get ${label} by ID`,\n operationId: `get${schemaName}ById`,\n parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],\n responses: itemResponse(schemaRef, `${label} found`)\n } as OperationObject\n }\n }\n}\n\n// ============================================================================\n// Tree Entity (CRUD + hierarchy operations)\n// ============================================================================\n\nexport function treeToPaths(\n entity: TreeEntityDefinition,\n basePath: string,\n schemaName: string,\n tag: string\n): Record<string, PathItemObject> {\n const schemaRef = `#/components/schemas/${schemaName}`\n const label = resolveLocalized(entity.label, 'en')\n\n // Start with collection CRUD (includes /count)\n const paths: Record<string, PathItemObject> = {\n ...collectionToPaths(entity as unknown as CollectionEntityDefinition, basePath, schemaName, tag)\n }\n\n // GET /tree - full tree structure\n paths[`${basePath}/tree`] = {\n get: {\n tags: [tag],\n summary: `Get ${label} tree`,\n operationId: `get${schemaName}Tree`,\n parameters: [\n { name: 'rootId', in: 'query', schema: { type: 'string' }, description: 'Subtree root ID' },\n { name: 'maxDepth', in: 'query', schema: { type: 'integer', minimum: 1 }, description: 'Maximum depth' }\n ],\n responses: {\n '200': {\n description: `${label} tree`,\n content: { 'application/json': { schema: { type: 'array', items: { $ref: schemaRef } } } }\n }\n }\n } as OperationObject\n }\n\n // GET /roots - root nodes\n paths[`${basePath}/roots`] = {\n get: {\n tags: [tag],\n summary: `Get ${label} roots`,\n operationId: `get${schemaName}Roots`,\n responses: {\n '200': {\n description: `Root ${label}`,\n content: { 'application/json': { schema: { type: 'array', items: { $ref: schemaRef } } } }\n }\n }\n } as OperationObject\n }\n\n // POST /:id/move — OpenAPI 3.1: type array for nullable\n paths[`${basePath}/{id}/move`] = {\n post: {\n tags: [tag],\n summary: `Move ${label}`,\n operationId: `move${schemaName}`,\n parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n parentId: { type: ['string', 'null'], description: 'New parent ID (null for root)' }\n }\n }\n }\n }\n },\n responses: itemResponse(schemaRef, `${label} moved`)\n } as OperationObject\n }\n\n // GET /:id/ancestors\n paths[`${basePath}/{id}/ancestors`] = {\n get: {\n tags: [tag],\n summary: `Get ${label} ancestors`,\n operationId: `get${schemaName}Ancestors`,\n parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],\n responses: {\n '200': {\n description: `${label} ancestors`,\n content: { 'application/json': { schema: { type: 'array', items: { $ref: schemaRef } } } }\n }\n }\n } as OperationObject\n }\n\n // GET /:id/descendants\n paths[`${basePath}/{id}/descendants`] = {\n get: {\n tags: [tag],\n summary: `Get ${label} descendants`,\n operationId: `get${schemaName}Descendants`,\n parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],\n responses: {\n '200': {\n description: `${label} descendants`,\n content: { 'application/json': { schema: { type: 'array', items: { $ref: schemaRef } } } }\n }\n }\n } as OperationObject\n }\n\n // GET /:id/children\n paths[`${basePath}/{id}/children`] = {\n get: {\n tags: [tag],\n summary: `Get ${label} children`,\n operationId: `get${schemaName}Children`,\n parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],\n responses: {\n '200': {\n description: `${label} children`,\n content: { 'application/json': { schema: { type: 'array', items: { $ref: schemaRef } } } }\n }\n }\n } as OperationObject\n }\n\n return paths\n}\n\n// ============================================================================\n// DAG Entity (Tree + multi-parent operations)\n// ============================================================================\n\nexport function dagToPaths(\n entity: DagEntityDefinition,\n basePath: string,\n schemaName: string,\n tag: string\n): Record<string, PathItemObject> {\n const schemaRef = `#/components/schemas/${schemaName}`\n const label = resolveLocalized(entity.label, 'en')\n\n // Start with all tree paths (CRUD + hierarchy)\n const paths: Record<string, PathItemObject> = {\n ...treeToPaths(entity as unknown as TreeEntityDefinition, basePath, schemaName, tag)\n }\n\n // GET + PUT + POST /:id/parents\n paths[`${basePath}/{id}/parents`] = {\n get: {\n tags: [tag],\n summary: `Get ${label} parents`,\n operationId: `get${schemaName}Parents`,\n parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],\n responses: {\n '200': {\n description: `${label} parents`,\n content: { 'application/json': { schema: { type: 'array', items: { $ref: schemaRef } } } }\n }\n }\n } as OperationObject,\n put: {\n tags: [tag],\n summary: `Set ${label} parents`,\n operationId: `set${schemaName}Parents`,\n parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n required: ['parentIds'],\n properties: { parentIds: { type: 'array', items: { type: 'string' } } }\n }\n }\n }\n },\n responses: { '204': { description: 'Parents updated' } }\n } as OperationObject,\n post: {\n tags: [tag],\n summary: `Add ${label} parent`,\n operationId: `add${schemaName}Parent`,\n parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n required: ['parentId'],\n properties: { parentId: { type: 'string' } }\n }\n }\n }\n },\n responses: { '204': { description: 'Parent added' } }\n } as OperationObject\n }\n\n // DELETE /:id/parents/:parentId\n paths[`${basePath}/{id}/parents/{parentId}`] = {\n delete: {\n tags: [tag],\n summary: `Remove ${label} parent`,\n operationId: `remove${schemaName}Parent`,\n parameters: [\n { name: 'id', in: 'path', required: true, schema: { type: 'string' } },\n { name: 'parentId', in: 'path', required: true, schema: { type: 'string' } }\n ],\n responses: { '204': { description: 'Parent removed' }, ...errorResponses }\n } as OperationObject\n }\n\n return paths\n}\n\n// ============================================================================\n// View Entity (read-only: list, get by ID, count)\n// ============================================================================\n\nexport function viewToPaths(\n entity: ViewEntityDefinition,\n basePath: string,\n schemaName: string,\n tag: string\n): Record<string, PathItemObject> {\n const schemaRef = `#/components/schemas/${schemaName}`\n const label = resolveLocalized(entity.label, 'en')\n\n return {\n [`${basePath}/count`]: {\n get: {\n tags: [tag],\n summary: `Count ${label}`,\n operationId: `count${schemaName}`,\n parameters: [{ $ref: '#/components/parameters/FiltersParam' }],\n responses: countResponse\n } as OperationObject\n },\n [basePath]: {\n get: {\n tags: [tag],\n summary: `List ${label}`,\n operationId: `list${schemaName}`,\n parameters: listParameters,\n responses: paginatedResponse(schemaRef)\n } as OperationObject\n },\n [`${basePath}/{id}`]: {\n get: {\n tags: [tag],\n summary: `Get ${label} by ID`,\n operationId: `get${schemaName}ById`,\n parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],\n responses: itemResponse(schemaRef, `${label} found`)\n } as OperationObject\n }\n }\n}\n\n// ============================================================================\n// Computed Entity (read-only + recompute)\n// ============================================================================\n\nexport function computedToPaths(\n entity: ComputedEntityDefinition,\n basePath: string,\n schemaName: string,\n tag: string\n): Record<string, PathItemObject> {\n const schemaRef = `#/components/schemas/${schemaName}`\n const label = resolveLocalized(entity.label, 'en')\n\n return {\n [`${basePath}/count`]: {\n get: {\n tags: [tag],\n summary: `Count ${label}`,\n operationId: `count${schemaName}`,\n parameters: [{ $ref: '#/components/parameters/FiltersParam' }],\n responses: countResponse\n } as OperationObject\n },\n [`${basePath}/recompute`]: {\n post: {\n tags: [tag],\n summary: `Recompute ${label}`,\n operationId: `recompute${schemaName}`,\n responses: {\n '200': {\n description: 'Recomputation result',\n content: { 'application/json': { schema: { type: 'object', properties: { recomputed: { type: 'integer' } } } } }\n }\n }\n } as OperationObject\n },\n [basePath]: {\n get: {\n tags: [tag],\n summary: `List ${label}`,\n operationId: `list${schemaName}`,\n parameters: listParameters,\n responses: paginatedResponse(schemaRef)\n } as OperationObject\n },\n [`${basePath}/{id}`]: {\n get: {\n tags: [tag],\n summary: `Get ${label} by ID`,\n operationId: `get${schemaName}ById`,\n parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],\n responses: itemResponse(schemaRef, `${label} found`)\n } as OperationObject\n }\n }\n}\n\n// ============================================================================\n// Actions (entity-level, row-level, module-level)\n// ============================================================================\n\nexport function actionToPaths(\n action: ActionDefinition,\n basePath: string,\n schemaName: string,\n tag: string,\n scope: 'module' | 'entity' | 'row' = 'row'\n): Record<string, PathItemObject> {\n const label = resolveLocalized(action.label, 'en')\n const method = (action.method ?? 'POST').toLowerCase() as 'post' | 'get' | 'put' | 'delete' | 'patch'\n const hasInput = !!action.input && Object.keys(action.input).length > 0\n const inputSchemaRef = `#/components/schemas/${schemaName}`\n\n // Build path based on scope\n let actionPath: string\n let pathParams: Array<{ name: string; in: 'path'; required: true; schema: { type: 'string' } }> = []\n\n switch (scope) {\n case 'module':\n // /module/action\n actionPath = `${basePath}/${action.key}`\n break\n case 'entity':\n // /module/entity/action\n actionPath = `${basePath}/${action.key}`\n break\n case 'row':\n default:\n // /module/entity/:id/action\n actionPath = `${basePath}/{id}/${action.key}`\n pathParams = [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }]\n break\n }\n\n // Build unique operationId based on scope:\n // - module scope: actionKeyAction (e.g., loginAction)\n // - entity/row scope: entityPrefixActionKey (e.g., UsersApprove)\n let operationId: string\n if (scope === 'module') {\n operationId = `${action.key}Action`\n } else {\n // For entity/row scope, schemaName contains entity name (e.g., \"UsersApproveInput\")\n const entityPrefix = schemaName.replace(/Input$/, '')\n operationId = `${entityPrefix}${capitalize(action.key)}`\n }\n\n const operation: OperationObject = {\n tags: [tag],\n summary: label,\n operationId,\n parameters: pathParams.length > 0 ? pathParams : undefined,\n responses: actionResponse(`${label} result`)\n }\n\n // Add request body for methods that support it\n if (hasInput && ['post', 'put', 'patch'].includes(method)) {\n operation.requestBody = {\n required: true,\n content: { 'application/json': { schema: { $ref: inputSchemaRef } } }\n }\n }\n\n return {\n [actionPath]: { [method]: operation }\n }\n}\n\n// ============================================================================\n// Custom Routes (module-level, manually defined)\n// ============================================================================\n\n/**\n * Generates OpenAPI path from a CustomRouteDefinition\n */\nexport function customRouteToPaths(\n route: CustomRouteDefinition,\n modulePrefix: string,\n tag: string,\n inputSchemaName?: string\n): Record<string, PathItemObject> {\n const summary = resolveLocalized(route.summary, 'en')\n const description = route.description ? resolveLocalized(route.description, 'en') : undefined\n const method = route.method.toLowerCase() as 'get' | 'post' | 'put' | 'delete' | 'patch'\n\n // Build full path - convert :param to {param} for OpenAPI\n const openApiPath = route.path.replace(/:(\\w+)/g, '{$1}')\n const fullPath = `${modulePrefix}${openApiPath}`\n\n // Build parameters array\n const parameters: ParameterObject[] = []\n\n // Helper to map param type to OpenAPI type\n const mapParamType = (type?: string): SchemaObjectType => {\n switch (type) {\n case 'integer':\n return 'integer'\n case 'number':\n return 'number'\n case 'boolean':\n return 'boolean'\n default:\n return 'string'\n }\n }\n\n // Path parameters (from :param in path or explicit params)\n const pathParamMatches = route.path.match(/:(\\w+)/g) || []\n for (const match of pathParamMatches) {\n const paramName = match.slice(1) // Remove leading :\n const paramDef = route.params?.[paramName]\n parameters.push({\n name: paramName,\n in: 'path',\n required: true,\n schema: {\n type: mapParamType(paramDef?.type),\n enum: paramDef?.enum\n },\n description: paramDef?.description ? resolveLocalized(paramDef.description, 'en') : undefined\n })\n }\n\n // Query parameters\n if (route.query) {\n for (const [name, paramDef] of Object.entries(route.query)) {\n parameters.push({\n name,\n in: 'query',\n required: paramDef.required,\n schema: {\n type: mapParamType(paramDef.type),\n default: paramDef.default,\n enum: paramDef.enum\n },\n description: paramDef.description ? resolveLocalized(paramDef.description, 'en') : undefined\n })\n }\n }\n\n // Build operation\n const operation: OperationObject = {\n tags: route.tags ?? [tag],\n summary,\n description,\n operationId: route.operationId ?? `${method}${fullPath.replace(/[^a-zA-Z0-9]/g, '_')}`,\n parameters: parameters.length > 0 ? parameters : undefined,\n responses: buildCustomRouteResponse(route)\n }\n\n // Add security if auth is required (default: true)\n if (route.auth !== false) {\n operation.security = [{ bearerAuth: [] }]\n }\n\n // Add request body for methods that support it\n const hasInput = route.input && Object.keys(route.input).length > 0\n if (hasInput && ['post', 'put', 'patch'].includes(method) && inputSchemaName) {\n operation.requestBody = {\n required: true,\n content: { 'application/json': { schema: { $ref: `#/components/schemas/${inputSchemaName}` } } }\n }\n }\n\n return {\n [fullPath]: { [method]: operation }\n }\n}\n\n/**\n * Builds response object for custom route\n */\nfunction buildCustomRouteResponse(route: CustomRouteDefinition): ResponsesObject {\n const output = route.output\n\n if (!output) {\n return {\n '200': {\n description: 'Success',\n content: { 'application/json': { schema: { type: 'object' } } }\n }\n }\n }\n\n const description = output.description ? resolveLocalized(output.description, 'en') : 'Success'\n\n // Build schema based on output type\n let schema: Record<string, unknown>\n\n if (output.type === 'array' && output.items) {\n if (output.items.$ref) {\n schema = { type: 'array', items: { $ref: output.items.$ref } }\n } else {\n schema = { type: 'array', items: { type: output.items.type ?? 'object' } }\n }\n } else if (output.type === 'object') {\n schema = { type: 'object', additionalProperties: true }\n } else {\n schema = { type: output.type }\n }\n\n return {\n '200': {\n description,\n content: { 'application/json': { schema } }\n },\n '400': {\n description: 'Validation error',\n content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorResponse' } } }\n }\n }\n}\n\nfunction capitalize(str: string): string {\n return str.charAt(0).toUpperCase() + str.slice(1).replace(/_([a-z])/g, (_, c) => c.toUpperCase())\n}\n","/**\n * Plugin operations — shared logic for CLI and UI actions.\n *\n * File-based: reads/writes nexus.plugins.json. No DB dependency.\n */\n\nimport { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'\nimport { join, dirname } from 'node:path'\nimport { exec } from 'node:child_process'\nimport { promisify } from 'node:util'\nimport type { PluginManifest } from '@gzl10/nexus-sdk'\n\nconst execAsync = promisify(exec)\n\nconst PLUGIN_PREFIX = '@gzl10/nexus-plugin-'\n\n// ── Name helpers ──────────────────────────────────────────────────────────────\n\n/** Normalize short name (\"charts\") to full npm name (\"@gzl10/nexus-plugin-charts\") */\nexport function normalizePluginName(name: string): string {\n if (name.startsWith('@gzl10/nexus-plugin-')) return name\n if (name.startsWith('@gzl10/nexus-')) return name.replace('@gzl10/nexus-', PLUGIN_PREFIX)\n if (name.startsWith('nexus-plugin-')) return `@gzl10/${name}`\n if (name.startsWith('nexus-')) return `${PLUGIN_PREFIX}${name.replace('nexus-', '')}`\n return `${PLUGIN_PREFIX}${name}`\n}\n\n/** Extract short name from full npm name */\nexport function shortPluginName(name: string): string {\n return name.replace(PLUGIN_PREFIX, '')\n}\n\n/**\n * Resolve plugin identifier to full npm name.\n * Accepts: 3-char code (e.g. 'cht'), short suffix ('charts'), or full name ('@gzl10/nexus-plugin-charts').\n * Falls back to normalizePluginName() for non-code identifiers or unknown codes.\n */\nexport function resolvePluginName(identifier: string, discovered: PluginManifest[]): string {\n if (/^[a-z]{3}$/.test(identifier)) {\n const found = discovered.find(p => p.code === identifier)\n if (found) return found.name\n }\n return normalizePluginName(identifier)\n}\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport interface PluginEntry {\n enabled: boolean\n}\n\nexport type PluginsFile = Record<string, PluginEntry>\n\n// ── File helpers ──────────────────────────────────────────────────────────────\n\n/** Get path to nexus.plugins.json */\nexport function getPluginsFilePath(projectPath?: string): string {\n const base = projectPath || process.cwd()\n return join(base, 'data', 'nexus.plugins.json')\n}\n\n/** Read nexus.plugins.json (returns {} if missing) */\nexport function readPluginsFile(filePath?: string): PluginsFile {\n const path = filePath || getPluginsFilePath()\n try {\n return JSON.parse(readFileSync(path, 'utf-8'))\n } catch {\n return {}\n }\n}\n\n/** Write nexus.plugins.json */\nexport function writePluginsFile(data: PluginsFile, filePath?: string): void {\n const path = filePath || getPluginsFilePath()\n const dir = dirname(path)\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true })\n }\n writeFileSync(path, JSON.stringify(data, null, 2) + '\\n', 'utf-8')\n}\n\n// ── Queries ───────────────────────────────────────────────────────────────────\n\n/** Get plugin state from nexus.plugins.json */\nexport function getPluginState(name: string, projectPath?: string): PluginEntry | undefined {\n const plugins = readPluginsFile(getPluginsFilePath(projectPath))\n return plugins[name]\n}\n\n/** Get all plugin states */\nexport function getAllPluginStates(projectPath?: string): PluginsFile {\n return readPluginsFile(getPluginsFilePath(projectPath))\n}\n\n// ── Operations ────────────────────────────────────────────────────────────────\n\n/** Check if a package exists in the pnpm workspace */\nasync function isWorkspacePackage(name: string): Promise<boolean> {\n try {\n const { stdout } = await execAsync('pnpm list -r --json --depth -1')\n const packages = JSON.parse(stdout) as Array<{ name?: string }>\n return packages.some(p => p.name === name)\n } catch {\n return false\n }\n}\n\n/** Install plugin via pnpm and register in nexus.plugins.json */\nexport async function installPlugin(name: string, opts?: {\n version?: string\n projectPath?: string\n}): Promise<void> {\n // Guard: prevent installing plugins inside the framework package itself\n const projectPath = opts?.projectPath ?? process.cwd()\n const pkgPath = join(projectPath, 'package.json')\n if (existsSync(pkgPath)) {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { name?: string }\n if (pkg.name === '@gzl10/nexus-backend') {\n throw new Error('Cannot install plugins inside @gzl10/nexus-backend. Run this command from a Nexus project directory.')\n }\n }\n\n const isWorkspace = await isWorkspacePackage(name)\n\n let pkg: string\n if (opts?.version) {\n pkg = `${name}@${opts.version}`\n } else if (isWorkspace) {\n pkg = `${name}@workspace:*`\n } else {\n pkg = name\n }\n\n const flag = isWorkspace ? '-D ' : ''\n await execAsync(`pnpm add ${flag}${pkg}`)\n\n const filePath = getPluginsFilePath(opts?.projectPath)\n const plugins = readPluginsFile(filePath)\n plugins[name] = { enabled: true }\n writePluginsFile(plugins, filePath)\n}\n\n/** Check if a package is installed (listed in package.json dependencies or devDependencies) */\nexport function isPackageInstalled(name: string, projectPath?: string): boolean {\n const base = projectPath || process.cwd()\n const pkgPath = join(base, 'package.json')\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n }\n return !!(pkg.dependencies?.[name] || pkg.devDependencies?.[name])\n } catch {\n return false\n }\n}\n\n/** Uninstall plugin via pnpm and remove from nexus.plugins.json */\nexport async function uninstallPlugin(name: string, projectPath?: string): Promise<void> {\n const filePath = getPluginsFilePath(projectPath)\n const plugins = readPluginsFile(filePath)\n const isRegistered = name in plugins\n const isInstalled = isPackageInstalled(name, projectPath)\n\n if (!isRegistered && !isInstalled) {\n throw new Error(`Plugin ${shortPluginName(name)} is not installed`)\n }\n\n if (isInstalled) {\n await execAsync(`pnpm remove ${name}`)\n }\n\n if (isRegistered) {\n delete plugins[name]\n writePluginsFile(plugins, filePath)\n }\n}\n\n/** Enable a plugin */\nexport function enablePlugin(name: string, projectPath?: string): void {\n const filePath = getPluginsFilePath(projectPath)\n const plugins = readPluginsFile(filePath)\n\n if (!(name in plugins)) {\n throw new Error(`Plugin ${name} is not installed`)\n }\n\n plugins[name] = { enabled: true }\n writePluginsFile(plugins, filePath)\n}\n\n/** Disable a plugin */\nexport function disablePlugin(name: string, projectPath?: string): void {\n const filePath = getPluginsFilePath(projectPath)\n const plugins = readPluginsFile(filePath)\n\n if (!(name in plugins)) {\n throw new Error(`Plugin ${name} is not installed`)\n }\n\n plugins[name] = { enabled: false }\n writePluginsFile(plugins, filePath)\n}\n","/**\n * Automatic table prefix for plugin entities.\n *\n * Mutates entity definitions in-place, prepending `{code}_` to table names\n * and resolving intra-plugin relation.table references.\n */\n\nimport type { EntityDefinition, ModuleManifest } from '@gzl10/nexus-sdk'\n\n/**\n * Prefix all table names in plugin modules with `{code}_`.\n * Also resolves intra-plugin `relation.table` references and DAG `parentsTable`.\n *\n * Mutates modules in-place.\n *\n * @param code - Plugin code (3 lowercase chars)\n * @param modules - Plugin's module manifests\n * @returns Set of original (unprefixed) table names declared by the plugin\n */\nexport function prefixPluginTables(code: string, modules: ModuleManifest[]): Set<string> {\n const prefix = `${code}_`\n\n // 1. Collect all table names declared by this plugin (before prefixing)\n const pluginTables = new Set<string>()\n for (const mod of modules) {\n for (const def of mod.definitions ?? []) {\n const table = getTable(def)\n if (table) pluginTables.add(table)\n }\n }\n\n // 2. Prefix table names, parentsTable, and resolve intra-plugin relation references\n for (const mod of modules) {\n for (const def of mod.definitions ?? []) {\n const table = getTable(def)\n if (!table) continue\n\n // Prefix table (skip if already prefixed)\n if (!table.startsWith(prefix)) {\n setTable(def, `${prefix}${table}`)\n }\n\n // Prefix explicit parentsTable for DAG entities (always prefix, no guard)\n const defRecord = def as Record<string, unknown>\n const parentsTable = defRecord['parentsTable'] as string | undefined\n if (parentsTable) {\n defRecord['parentsTable'] = `${prefix}${parentsTable}`\n }\n\n // Resolve relation.table references\n const fields = (def as { fields?: Record<string, unknown> }).fields\n if (!fields) continue\n\n for (const field of Object.values(fields)) {\n const relation = (field as { relation?: { table: string } })?.relation\n if (!relation?.table) continue\n\n // Only prefix if it's an intra-plugin reference\n if (pluginTables.has(relation.table)) {\n relation.table = `${prefix}${relation.table}`\n }\n }\n }\n }\n\n return pluginTables\n}\n\nfunction getTable(def: EntityDefinition): string | undefined {\n return ('table' in def && typeof def.table === 'string') ? def.table : undefined\n}\n\nfunction setTable(def: EntityDefinition, value: string): void {\n if ('table' in def) {\n (def as { table: string }).table = value\n }\n}\n","import type { ModuleManifest, PluginManifest } from '@gzl10/nexus-sdk'\n\n/**\n * Module source type\n */\nexport type ModuleSource = 'core' | 'plugin' | 'standalone'\n\n/**\n * Registered module with source metadata\n */\nexport interface RegisteredModule extends ModuleManifest {\n /** Module source */\n _source: ModuleSource\n /** Plugin name (only if _source === 'plugin') */\n _pluginName?: string\n /** If true, module routes won't be mounted (services still available via init) */\n _disableEndpoints?: boolean\n /** Table name prefix for plugin modules (e.g. 'sch_') */\n _tablePrefix?: string\n /** Original (unprefixed) table names declared by this plugin */\n _pluginTables?: Set<string>\n}\n\n/**\n * In-memory engine state.\n * IMPORTANT: This file DOES NOT import modules to avoid circular dependencies.\n */\nexport const moduleStore = {\n /** Registered modules with source metadata */\n modules: [] as RegisteredModule[],\n\n /** Registered plugins (by name) */\n plugins: new Map<string, PluginManifest>(),\n\n /** Registered plugins (by 3-char code) */\n pluginsByCode: new Map<string, PluginManifest>(),\n\n /** Registered tables (for uniqueness validation) */\n tables: new Set<string>(),\n\n /** Registered subjects (for uniqueness validation) */\n subjects: new Set<string>(['all']),\n\n /** Map of table names to their CASL subjects */\n tableToSubject: new Map<string, string>()\n}\n\n/**\n * Resets the store to its initial state.\n * Used by stop() to allow clean restarts.\n */\nexport function resetStore(): void {\n moduleStore.modules.length = 0\n moduleStore.plugins.clear()\n moduleStore.pluginsByCode.clear()\n moduleStore.tables.clear()\n moduleStore.subjects.clear()\n moduleStore.subjects.add('all')\n moduleStore.tableToSubject.clear()\n}\n","import { ulid } from 'ulidx'\nimport { nanoid } from 'nanoid'\nimport { createId as cuid2 } from '@paralleldrive/cuid2'\nimport { randomUUID } from 'node:crypto'\n\n/**\n * Supported ID generation types.\n * Must match IdType from @gzl10/nexus-sdk\n */\nexport type IdType = 'ulid' | 'uuid' | 'nanoid' | 'cuid2' | 'auto' | 'custom' | 'pattern'\n\n/**\n * Generates a unique ULID identifier.\n *\n * ULID properties:\n * - Time-sortable: lexicographically sortable by creation time\n * - Secure: 80 bits of cryptographically random entropy\n * - URL-safe: uppercase letters and numbers only (Crockford Base32)\n * - 26 characters fixed length\n *\n * @returns A unique ULID string (e.g., \"01ARZ3NDEKTSV4RRFFQ69G5FAV\")\n */\nexport function generateId(): string {\n return ulid()\n}\n\n/**\n * Generates a unique identifier based on the specified type.\n *\n * @param type - ID generation strategy\n * @returns Generated ID string, or undefined for 'auto'/'custom' types\n *\n * @example\n * ```typescript\n * generateIdByType('ulid') // \"01ARZ3NDEKTSV4RRFFQ69G5FAV\"\n * generateIdByType('uuid') // \"550e8400-e29b-41d4-a716-446655440000\"\n * generateIdByType('nanoid') // \"V1StGXR8_Z5jdHi6B-myT\"\n * generateIdByType('cuid2') // \"clh3am1u70000qj0f9fgk2e1x\"\n * generateIdByType('auto') // undefined (DB handles it)\n * generateIdByType('custom') // undefined (user provides it)\n * ```\n */\nexport function generateIdByType(type: IdType = 'ulid'): string | undefined {\n switch (type) {\n case 'ulid':\n return ulid()\n case 'uuid':\n return randomUUID()\n case 'nanoid':\n return nanoid()\n case 'cuid2':\n return cuid2()\n case 'auto':\n // Auto-increment: DB handles ID generation\n return undefined\n case 'custom':\n // Custom: user must provide the ID\n return undefined\n case 'pattern':\n // Pattern: handled by generatePatternId() in sequence.ts\n return undefined\n default:\n // Fallback to ULID for unknown types\n return ulid()\n }\n}\n","/**\n * Entity definition extractors.\n *\n * Shared utilities for extracting metadata from EntityDefinitions and ModuleManifests.\n */\n\nimport type { EntityDefinition, ModuleManifest, PluginManifest } from '@gzl10/nexus-sdk'\n\n/** Generic interface for items with dependencies */\ninterface WithDependencies {\n name: string\n dependencies?: string[]\n optionalDependencies?: string[]\n}\n\n/** Entity types that have a database table */\nconst TYPES_WITH_TABLE = new Set(['collection', 'reference', 'event', 'config', 'temp', 'view', undefined])\n\n/**\n * Extracts table name and CASL subject from an entity definition.\n *\n * Tables are only present for persistent entity types:\n * collection, reference, event, config, temp, view.\n *\n * Subject is optional and must be explicitly defined in casl.subject.\n * Computed, action, external, and virtual entities don't have tables.\n */\nexport function getTableAndSubject(def: EntityDefinition): { table?: string; subject?: string } {\n const caslSubject = (def as { casl?: { subject?: string } }).casl?.subject\n\n if (!TYPES_WITH_TABLE.has(def.type)) {\n // computed, action, external, virtual no tienen tabla\n return { subject: caslSubject }\n }\n\n const table = (def as { table: string }).table\n return { table, subject: caslSubject }\n}\n\n/**\n * Generic topological sort using Kahn's algorithm.\n * Items with no dependencies come first, then items that depend on them, etc.\n *\n * @param items - Items to sort\n * @param entityType - Name for error messages ('module' or 'plugin')\n * @throws Error if circular dependencies are detected\n */\nfunction topologicalSortGeneric<T extends WithDependencies>(\n items: T[],\n entityType: string,\n keyFn: (item: T) => string = (item) => item.name\n): T[] {\n const itemMap = new Map(items.map(i => [keyFn(i), i]))\n const inDegree = new Map<string, number>()\n const adjList = new Map<string, string[]>()\n\n // Initialize\n for (const item of items) {\n const key = keyFn(item)\n inDegree.set(key, 0)\n adjList.set(key, [])\n }\n\n // Build graph: for each dependency, add edge dep -> item\n for (const item of items) {\n const key = keyFn(item)\n for (const dep of [...(item.dependencies ?? []), ...(item.optionalDependencies ?? [])]) {\n if (itemMap.has(dep)) {\n adjList.get(dep)!.push(key)\n inDegree.set(key, (inDegree.get(key) ?? 0) + 1)\n }\n }\n }\n\n // Start with items that have no dependencies\n const queue = items\n .filter(i => inDegree.get(keyFn(i)) === 0)\n .map(i => keyFn(i))\n\n const sorted: T[] = []\n\n while (queue.length > 0) {\n const key = queue.shift()!\n sorted.push(itemMap.get(key)!)\n\n for (const dependent of adjList.get(key) ?? []) {\n const newDegree = (inDegree.get(dependent) ?? 1) - 1\n inDegree.set(dependent, newDegree)\n if (newDegree === 0) {\n queue.push(dependent)\n }\n }\n }\n\n if (sorted.length !== items.length) {\n const remaining = items.filter(i => !sorted.includes(i)).map(i => keyFn(i))\n throw new Error(`Circular dependency detected in ${entityType}s: ${remaining.join(', ')}`)\n }\n\n return sorted\n}\n\n/**\n * Sorts modules by their dependencies using Kahn's algorithm (topological sort).\n * Modules with no dependencies come first, then modules that depend on them, etc.\n *\n * @throws Error if circular dependencies are detected\n */\nexport function topologicalSort(modules: ModuleManifest[]): ModuleManifest[] {\n return topologicalSortGeneric(modules, 'module')\n}\n\n/**\n * Sorts plugins by their dependencies using Kahn's algorithm (topological sort).\n * Plugins with no dependencies come first, then plugins that depend on them, etc.\n *\n * @throws Error if circular dependencies are detected\n */\nexport function topologicalSortPlugins(plugins: PluginManifest[]): PluginManifest[] {\n return topologicalSortGeneric(plugins, 'plugin')\n}\n\n/**\n * Validates that all hard dependencies exist in the module list.\n * Optional dependencies are not validated.\n *\n * @throws Error if any hard dependency is missing\n */\nexport function validateModuleDependencies(modules: ModuleManifest[]): void {\n const names = new Set(modules.map(m => m.name))\n const errors: string[] = []\n\n for (const mod of modules) {\n for (const dep of mod.dependencies ?? []) {\n if (!names.has(dep)) {\n errors.push(`Module \"${mod.name}\" requires \"${dep}\" but it is not registered`)\n }\n }\n }\n\n if (errors.length > 0) {\n throw new Error(`Missing dependencies:\\n - ${errors.join('\\n - ')}`)\n }\n}\n\n/**\n * Validates that all hard dependencies exist in the plugin list.\n *\n * @throws Error if any hard dependency is missing\n */\nexport function validatePluginDependencies(plugins: PluginManifest[]): void {\n const names = new Set(plugins.map(p => p.name))\n const errors: string[] = []\n\n for (const plugin of plugins) {\n for (const dep of plugin.dependencies ?? []) {\n if (!names.has(dep)) {\n errors.push(`Plugin \"${plugin.name}\" requires \"${dep}\" but it is not registered`)\n }\n }\n }\n\n if (errors.length > 0) {\n throw new Error(`Missing plugin dependencies:\\n - ${errors.join('\\n - ')}`)\n }\n}\n","import type { ModuleManifest, PluginManifest, TreeEntityDefinition, DagEntityDefinition } from '@gzl10/nexus-sdk'\nimport { getPluginState } from '../core/plugin-ops.js'\nimport { prefixPluginTables } from './table-prefix.js'\n\n/** Options for registerPlugin */\nexport interface RegisterPluginOptions {\n /** Disable endpoint mounting (services still available via init) */\n disableEndpoints?: boolean\n}\nimport { moduleStore, type ModuleSource, type RegisteredModule } from './module-store.js'\nimport { generateId } from '../core/utils/id.js'\nimport { getTableAndSubject } from './definition-extractors.js'\n\n/**\n * Validates uniqueness of tables and subjects when registering a module\n */\nfunction validateUniqueness(mod: ModuleManifest): void {\n const errors: string[] = []\n\n // Validar label requerido\n if (!mod.label) {\n throw new Error(`Módulo '${mod.name}': label es requerido`)\n }\n\n for (const def of mod.definitions ?? []) {\n const { table, subject } = getTableAndSubject(def)\n\n if (table && moduleStore.tables.has(table)) {\n errors.push(`tabla '${table}' ya está registrada`)\n }\n if (subject && moduleStore.subjects.has(subject)) {\n errors.push(`subject '${subject}' ya está registrado`)\n }\n\n // Validate tree entity seed has at least one root node (only for inline seeds)\n if (def.type === 'tree') {\n const treeDef = def as TreeEntityDefinition\n if (!treeDef.seed) {\n errors.push(`tree entity '${table}' requires seed data`)\n } else if (Array.isArray(treeDef.seed)) {\n // Only validate inline arrays, not URL-based seeds\n if (treeDef.seed.length === 0) {\n errors.push(`tree entity '${table}' requires at least one seed record`)\n } else {\n const hasRoot = treeDef.seed.some((s: Record<string, unknown>) => s['parent_id'] === null || s['parent_id'] === undefined)\n if (!hasRoot) {\n errors.push(`tree entity '${table}' seed must include at least one root node (parent_id: null)`)\n }\n }\n }\n // SeedConfig (URL-based) seeds are validated at runtime during seeding\n }\n\n // Validate DAG entity seed has at least one record (only for inline seeds)\n if (def.type === 'dag') {\n const dagDef = def as DagEntityDefinition\n if (!dagDef.seed) {\n errors.push(`dag entity '${table}' requires seed data`)\n } else if (Array.isArray(dagDef.seed) && dagDef.seed.length === 0) {\n errors.push(`dag entity '${table}' requires at least one seed record`)\n }\n // Note: DAG roots are nodes without entries in the parents table,\n // which is managed separately from the seed data\n }\n }\n\n if (errors.length > 0) {\n throw new Error(`Módulo '${mod.name}' tiene conflictos:\\n - ${errors.join('\\n - ')}`)\n }\n}\n\n/**\n * Registers tables and subjects for a module\n */\nfunction registerTablesAndSubjects(mod: ModuleManifest): void {\n for (const def of mod.definitions ?? []) {\n const { table, subject } = getTableAndSubject(def)\n if (table) moduleStore.tables.add(table)\n if (subject) moduleStore.subjects.add(subject)\n if (table && subject) moduleStore.tableToSubject.set(table, subject)\n }\n}\n\n/**\n * Options for registering a module\n */\ninterface RegisterModuleOptions {\n /** Module source ('core', 'plugin', 'standalone') */\n source?: ModuleSource\n /** Plugin name (only if source === 'plugin') */\n pluginName?: string\n /** If true, module routes won't be mounted (services still available via init) */\n disableEndpoints?: boolean\n /** Table name prefix for plugin modules */\n tablePrefix?: string\n /** Original (unprefixed) table names declared by this plugin */\n pluginTables?: Set<string>\n}\n\n/**\n * Registers a module manifest in the engine store.\n *\n * @remarks\n * Registration is separate from initialization to support dependency ordering.\n * All modules must be registered before the server starts so the engine can\n * resolve dependencies and determine initialization order via topological sort.\n *\n * The engine validates uniqueness of table names and CASL subjects to prevent\n * conflicts between modules. Tree entities require at least one root node in seed.\n *\n * @param mod - Module manifest with name, definitions, and optional init/destroy\n * @param options - Source (core/plugin/standalone) and plugin name if applicable\n *\n * @example\n * ```typescript\n * registerModule(postsModule)\n * registerModule(commentsModule, { source: 'plugin', pluginName: 'cms' })\n * ```\n */\nexport function registerModule(\n mod: ModuleManifest,\n options: RegisterModuleOptions = {}\n): void {\n const { source = 'core', pluginName, disableEndpoints, tablePrefix, pluginTables } = options\n\n validateUniqueness(mod)\n registerTablesAndSubjects(mod)\n\n // Generate unique ID and stamp module name for each entity definition\n for (const def of mod.definitions ?? []) {\n (def as unknown as { _id: string })._id = generateId()\n ;(def as unknown as { _moduleName: string })._moduleName = mod.name\n }\n\n // Generate unique ID for each page definition\n for (const page of mod.pages ?? []) {\n (page as unknown as { _id: string })._id = generateId()\n }\n\n const registered: RegisteredModule = {\n ...mod,\n _source: source,\n _pluginName: pluginName,\n _disableEndpoints: disableEndpoints,\n _tablePrefix: tablePrefix,\n _pluginTables: pluginTables\n }\n moduleStore.modules.push(registered)\n}\n\n/**\n * Check if a plugin should be loaded based on nexus.plugins.json state.\n *\n * @remarks\n * Reads the file-based plugin registry directly (no DB, no module imports).\n *\n * @param pluginName - Name of the plugin to check\n * @param projectPath - Optional project root path\n * @returns true if plugin should be loaded, false if disabled\n */\nexport function shouldLoadPlugin(pluginName: string, projectPath?: string): boolean {\n const state = getPluginState(pluginName, projectPath)\n // Only load if explicitly enabled in nexus.plugins.json\n if (!state) return false\n return state.enabled\n}\n\n/**\n * Registers all modules from a plugin manifest.\n *\n * @remarks\n * Plugins bundle multiple related modules under a single namespace. This enables:\n * - Shared branding (label, icon) inherited by modules without their own\n * - Unified category assignment for UI grouping\n * - Plugin-level lookup via getPlugin() for configuration access\n *\n * Use `disableEndpoints` when you need plugin services available via ctx.services\n * but don't want HTTP routes mounted (e.g., internal-only modules).\n *\n * @param plugin - Plugin manifest with modules array and shared metadata\n * @param options - Optional settings like disableEndpoints\n *\n * @example\n * ```typescript\n * registerPlugin(cmsPlugin)\n * registerPlugin(analyticsPlugin, { disableEndpoints: true })\n * ```\n */\nexport function registerPlugin(plugin: PluginManifest, options?: RegisterPluginOptions): void {\n // Validate code format: exactly 3 lowercase letters\n if (!/^[a-z]{3}$/.test(plugin.code)) {\n throw new Error(\n `Plugin '${plugin.name}': code must be exactly 3 lowercase letters, got '${plugin.code}'`\n )\n }\n // Validate code uniqueness\n const existingByCode = moduleStore.pluginsByCode.get(plugin.code)\n if (existingByCode) {\n throw new Error(\n `Plugin code '${plugin.code}' already registered by '${existingByCode.name}'`\n )\n }\n\n // Register in both indexes\n moduleStore.plugins.set(plugin.name, plugin)\n moduleStore.pluginsByCode.set(plugin.code, plugin)\n\n // Auto-prefix table names with plugin code\n const pluginTables = prefixPluginTables(plugin.code, plugin.modules)\n const tablePrefix = `${plugin.code}_`\n\n for (const mod of plugin.modules) {\n // Inyectar propiedades del plugin al módulo (solo si no están definidas)\n const moduleWithPluginProps = {\n ...mod,\n label: mod.label ?? plugin.label,\n icon: mod.icon ?? plugin.icon,\n category: plugin.category,\n }\n registerModule(moduleWithPluginProps, {\n source: 'plugin',\n pluginName: plugin.name,\n disableEndpoints: options?.disableEndpoints,\n tablePrefix,\n pluginTables\n })\n }\n}\n","import { dirname, join } from 'path'\nimport { fileURLToPath } from 'url'\nimport { existsSync, readFileSync } from 'fs'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n/**\n * Searches upward for the nexus-backend package.json\n */\nfunction findLibRoot(startDir: string): string {\n let dir = startDir\n while (dir !== '/') {\n const pkgPath = join(dir, 'package.json')\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))\n if (pkg.name === '@gzl10/nexus-backend') {\n return dir\n }\n } catch {\n // Continuar buscando\n }\n }\n dir = dirname(dir)\n }\n // Fallback: subir 2 niveles desde shared/\n return join(startDir, '../..')\n}\n\n// Cache del resultado\nlet _libPath: string | null = null\n\n/**\n * Root path of the nexus-backend library.\n * - Dev: /project/nexus-backend/\n * - Lib: /user-project/node_modules/@gzl10/nexus-backend/\n */\nexport function getLibPath(): string {\n if (!_libPath) {\n _libPath = findLibRoot(__dirname)\n }\n return _libPath\n}\n\n/**\n * Root path of the user project (process.cwd).\n * - Dev: matches getLibPath()\n * - Lib: /user-project/\n */\nexport function getProjectPath(): string {\n return process.cwd();\n}\n\n/**\n * Searches for .env by walking up from cwd (max 5 levels).\n * Useful for monorepos where .env is at the root.\n */\nexport function findEnvFile(): string | null {\n let dir = process.cwd()\n for (let i = 0; i < 5; i++) {\n const envPath = join(dir, '.env')\n if (existsSync(envPath)) return envPath\n const parent = dirname(dir)\n if (parent === dir) break\n dir = parent\n }\n return null\n}\n\n/**\n * Core migrations directory (shipped with nexus-backend).\n * Located at {libPath}/migrations.\n */\nexport function getCoreMigrationsDir(): string {\n return join(getLibPath(), 'migrations')\n}\n\n/**\n * Project migrations directory (user's own modules).\n * Override with MIGRATIONS_DIR env var.\n */\nexport function getProjectMigrationsDir(): string {\n if (process.env['MIGRATIONS_DIR']) {\n return process.env['MIGRATIONS_DIR']\n }\n return join(getProjectPath(), 'migrations')\n}\n\n/**\n * Get the migrations directory path (backwards compat alias for getProjectMigrationsDir).\n */\nexport function getMigrationsDir(): string {\n return getProjectMigrationsDir()\n}\n","import { join } from 'node:path'\nimport { readFileSync } from 'node:fs'\nimport type { ModuleManifest, PluginManifest } from '@gzl10/nexus-sdk'\nimport { moduleStore, type RegisteredModule } from './module-store.js'\nimport { getLibPath, getProjectPath } from '../config/paths.js'\n\n/**\n * Application manifest containing modules and metadata\n */\nexport interface AppManifest {\n /** Package name */\n name: string\n /** Package version */\n version: string\n /** Registered modules */\n modules: ModuleManifest[]\n /** Registered plugins (only for user manifest) */\n plugins?: PluginManifest[]\n}\n\ninterface PackageJson {\n name: string\n version: string\n}\n\nfunction readPackageJson(dir: string): PackageJson {\n const content = readFileSync(join(dir, 'package.json'), 'utf-8')\n const pkg = JSON.parse(content)\n return { name: pkg.name, version: pkg.version }\n}\n\n/**\n * Strip internal metadata from RegisteredModule\n */\nfunction toModuleManifest(mod: RegisteredModule): ModuleManifest {\n const { _source, _pluginName, ...manifest } = mod\n return manifest as ModuleManifest\n}\n\n/**\n * Gets all registered modules\n */\nexport function getModules(): ModuleManifest[] {\n return moduleStore.modules.map(toModuleManifest)\n}\n\n/**\n * Sorts modules by dependencies (topological sort).\n * Ensures a module is processed after its dependencies.\n */\nexport function getOrderedModules(): ModuleManifest[] {\n return getOrderedModulesInternal().map(toModuleManifest)\n}\n\n/**\n * Internal version that preserves RegisteredModule metadata (_source, _disableEndpoints, etc.)\n * Used by module-routes.ts to check _disableEndpoints\n */\nexport function getOrderedModulesInternal(): RegisteredModule[] {\n const sorted: RegisteredModule[] = []\n const visited = new Set<string>()\n const moduleMap = new Map(moduleStore.modules.map(m => [m.name, m]))\n\n function visit(mod: RegisteredModule) {\n if (visited.has(mod.name)) return\n visited.add(mod.name)\n\n for (const dep of [...(mod.dependencies ?? []), ...(mod.optionalDependencies ?? [])]) {\n const depMod = moduleMap.get(dep)\n if (depMod) visit(depMod)\n }\n sorted.push(mod)\n }\n\n moduleStore.modules.forEach(visit)\n return sorted\n}\n\n/**\n * Gets a module by name\n */\nexport function getModule(name: string): ModuleManifest | undefined {\n const mod = moduleStore.modules.find(m => m.name === name)\n return mod ? toModuleManifest(mod) : undefined\n}\n\n/**\n * Gets a plugin by name\n */\nexport function getPlugin(name: string): PluginManifest | undefined {\n return moduleStore.plugins.get(name)\n}\n\n/**\n * Gets all registered plugins\n */\nexport function getPlugins(): PluginManifest[] {\n return [...moduleStore.plugins.values()]\n}\n\n/**\n * Gets all subjects registered in modules.\n * Includes 'all', which is always available.\n */\nexport function getRegisteredSubjects(): string[] {\n return [...moduleStore.subjects]\n}\n\n/**\n * Validates that a subject exists in a registered module\n */\nexport function isValidSubject(subject: string): boolean {\n return moduleStore.subjects.has(subject)\n}\n\n/**\n * Gets the CASL subject for a registered table name\n */\nexport function getSubjectForTable(table: string): string | undefined {\n return moduleStore.tableToSubject.get(table)\n}\n\n/**\n * Get core modules (registered by nexus-backend)\n */\nexport function getCoreModules(): ModuleManifest[] {\n return moduleStore.modules\n .filter(m => m._source === 'core')\n .map(toModuleManifest)\n}\n\n/**\n * Get user modules (registered via plugins or standalone)\n */\nexport function getUserModules(): ModuleManifest[] {\n return moduleStore.modules\n .filter(m => m._source !== 'core')\n .map(toModuleManifest)\n}\n\n/**\n * Get the core manifest (nexus-backend library)\n * Reads name/version from getLibPath()/package.json\n */\nexport function getCoreManifest(): AppManifest {\n const pkg = readPackageJson(getLibPath())\n return {\n name: pkg.name,\n version: pkg.version,\n modules: getCoreModules()\n }\n}\n\n/**\n * Get the user manifest (user's project)\n * Reads name/version from getProjectPath()/package.json\n * Returns null if getLibPath() === getProjectPath() (development mode)\n */\nexport function getUserManifest(): AppManifest | null {\n const libPath = getLibPath()\n const projectPath = getProjectPath()\n\n // In development mode (same path), there's no user manifest\n if (libPath === projectPath) {\n return null\n }\n\n const pkg = readPackageJson(projectPath)\n return {\n name: pkg.name,\n version: pkg.version,\n modules: getUserModules(),\n plugins: getPlugins()\n }\n}\n\n/**\n * Check if running as a library (user project exists)\n */\nexport function hasUserApp(): boolean {\n return getLibPath() !== getProjectPath()\n}\n\n/**\n * Checks if a module is registered by name\n */\nexport function hasModule(name: string): boolean {\n return moduleStore.modules.some(m => m.name === name)\n}\n\n/**\n * Checks if a plugin is registered by name\n */\nexport function hasPlugin(name: string): boolean {\n return moduleStore.plugins.has(name)\n}\n\n/**\n * Gets a plugin by its 3-char code\n */\nexport function getPluginByCode(code: string): PluginManifest | undefined {\n return moduleStore.pluginsByCode.get(code)\n}\n\n/**\n * Checks if a plugin is registered by its 3-char code\n */\nexport function hasPluginByCode(code: string): boolean {\n return moduleStore.pluginsByCode.has(code)\n}\n","import pino from 'pino'\nimport { createRequire } from 'module'\n\n/**\n * Global logger with bootstrap fallback.\n *\n * Initially creates a basic pino logger.\n * When the logger module initializes, it replaces the instance.\n */\n\nconst isDev = process.env['NODE_ENV'] !== 'production'\n\n/**\n * Detects whether pino-pretty is available.\n * In production or when it is not installed, returns false.\n */\nexport function hasPinoPretty(): boolean {\n if (!isDev) return false\n try {\n const require = createRequire(import.meta.url)\n require.resolve('pino-pretty')\n return true\n } catch {\n return false\n }\n}\n\n// Logger inicial (fallback durante bootstrap, antes de que el módulo se inicialice)\nlet loggerInstance: pino.Logger = pino({\n level: process.env['LOG_LEVEL'] || 'info',\n redact: {\n paths: [\n 'password', 'token', 'secret', 'authorization',\n 'req.headers.authorization', 'req.headers.cookie'\n ],\n censor: '[REDACTED]'\n },\n transport: hasPinoPretty()\n ? { target: 'pino-pretty', options: { colorize: true, sync: true } }\n : undefined\n})\n\n/**\n * Replaces the logger instance.\n * Called from the logger module init().\n */\nexport function setLoggerInstance(instance: pino.Logger): void {\n loggerInstance = instance\n}\n\n/**\n * Proxy that delegates to the current logger.\n * Allows hot-swapping when the module initializes.\n */\nexport const logger = new Proxy({} as pino.Logger, {\n get(_, prop) {\n const target = loggerInstance as unknown as Record<string | symbol, unknown>\n const value = target[prop]\n // Bind methods para preservar `this`\n return typeof value === 'function' ? value.bind(loggerInstance) : value\n }\n})\n\nexport const createChildLogger = (context: string) => logger.child({ context })\n","import { z } from 'zod'\nimport type { LoggerConfig } from './types.js'\n\n/**\n * Environment variable schema for logger.\n * Completely independent from global configuration.\n */\nconst loggerEnvSchema = z.object({\n LOG_LEVEL: z.enum(['silent', 'fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),\n LOG_FORMAT: z.enum(['json', 'pretty']).default('pretty'),\n NODE_ENV: z.string().default('development'),\n // Sentry opcional - solo se activa si DSN está definido\n SENTRY_DSN: z.string().url().optional(),\n SENTRY_SAMPLE_RATE: z.coerce.number().min(0).max(1).default(1.0)\n})\n\nexport const loggerEnv = loggerEnvSchema.parse(process.env)\n\n/**\n * Gets logger configuration from environment variables\n */\nexport function getLoggerConfig(): LoggerConfig {\n return {\n level: loggerEnv.LOG_LEVEL,\n format: loggerEnv.LOG_FORMAT,\n // Sentry solo si DSN está definido\n sentry: loggerEnv.SENTRY_DSN\n ? {\n dsn: loggerEnv.SENTRY_DSN,\n environment: loggerEnv.NODE_ENV,\n sampleRate: loggerEnv.SENTRY_SAMPLE_RATE\n }\n : undefined\n }\n}\n","import pino from 'pino'\nimport * as Sentry from '@sentry/node'\nimport { createRequire } from 'node:module'\nimport type { LoggerConfig, LoggerService } from './types.js'\n\nconst isDev = process.env['NODE_ENV'] !== 'production'\n\n/**\n * Detects whether pino-pretty is available.\n * In production or when it is not installed, returns false.\n */\nfunction hasPinoPretty(): boolean {\n if (!isDev) return false\n try {\n const require = createRequire(import.meta.url)\n require.resolve('pino-pretty')\n return true\n } catch {\n return false\n }\n}\n\nlet loggerInstance: LoggerService | null = null\nlet pinoInstance: pino.Logger | null = null\nlet sentryEnabled = false\n\n/**\n * GDPR-compliant redaction paths for sensitive data.\n * These fields will be replaced with [REDACTED] in logs.\n */\nconst REDACT_PATHS = [\n // Auth credentials\n 'password',\n 'currentPassword',\n 'newPassword',\n 'token',\n 'accessToken',\n 'refreshToken',\n 'jwt',\n 'apiKey',\n 'secret',\n 'authorization',\n // Nested in request body\n 'req.body.password',\n 'req.body.currentPassword',\n 'req.body.newPassword',\n 'req.body.token',\n 'req.headers.authorization',\n // Cookies (may contain session tokens)\n 'req.headers.cookie',\n // Error context\n 'err.config.headers.authorization',\n 'error.config.headers.authorization'\n]\n\n\n/**\n * Initializes the logger service with Pino and optional Sentry\n */\nexport function initLoggerService(config: LoggerConfig): LoggerService {\n // Si ya está inicializado, retornar instancia existente (idempotente)\n if (loggerInstance) {\n return loggerInstance\n }\n\n // Inicializar Pino with GDPR-compliant redaction\n const usePretty = config.format === 'pretty' && hasPinoPretty()\n const pinoLogger = pino({\n level: config.level,\n redact: {\n paths: REDACT_PATHS,\n censor: '[REDACTED]'\n },\n transport: usePretty\n ? { target: 'pino-pretty', options: { colorize: true, sync: true } }\n : undefined\n })\n\n // Inicializar Sentry solo si DSN está configurado\n sentryEnabled = !!config.sentry\n if (sentryEnabled && config.sentry) {\n Sentry.init({\n dsn: config.sentry.dsn,\n environment: config.sentry.environment,\n sampleRate: config.sentry.sampleRate\n })\n pinoLogger.info({ sentry: config.sentry.dsn }, 'Sentry initialized')\n }\n\n // Guardar instancia de pino para shared/logger.ts\n pinoInstance = pinoLogger\n\n loggerInstance = {\n fatal: pinoLogger.fatal.bind(pinoLogger),\n error: pinoLogger.error.bind(pinoLogger),\n warn: pinoLogger.warn.bind(pinoLogger),\n info: pinoLogger.info.bind(pinoLogger),\n debug: pinoLogger.debug.bind(pinoLogger),\n trace: pinoLogger.trace.bind(pinoLogger),\n child: pinoLogger.child.bind(pinoLogger),\n\n // Sentry helpers (no-op si no está habilitado)\n captureException: (error: Error, context?: Record<string, unknown>) => {\n if (sentryEnabled) {\n Sentry.captureException(error, { extra: context })\n }\n },\n captureMessage: (msg: string, level: 'info' | 'warning' | 'error' = 'info') => {\n if (sentryEnabled) {\n Sentry.captureMessage(msg, level)\n }\n },\n setUser: (user: { id: string; email?: string }) => {\n if (sentryEnabled) {\n Sentry.setUser(user)\n }\n },\n isSentryEnabled: () => sentryEnabled\n }\n\n return loggerInstance\n}\n\n/**\n * Gets the LoggerService instance (singleton)\n */\nexport function getLoggerService(): LoggerService {\n if (!loggerInstance) {\n throw new Error('Logger not initialized. Call initLoggerService() first.')\n }\n return loggerInstance\n}\n\n/**\n * Resets the logger service (tests only)\n */\nexport function resetLoggerService(): void {\n loggerInstance = null\n pinoInstance = null\n sentryEnabled = false\n}\n\n/**\n * Gets the pino.Logger instance (for ctx.logger)\n */\nexport function getPinoLogger(): pino.Logger {\n if (!pinoInstance) {\n throw new Error('Logger not initialized. Call initLoggerService() first.')\n }\n return pinoInstance\n}\n\nexport function captureExceptionSafe(error: Error, context?: Record<string, unknown>): void {\n if (loggerInstance?.isSentryEnabled()) {\n loggerInstance.captureException(error, context)\n }\n}\n","type LogLevel = 'debug' | 'info' | 'warn' | 'error'\ntype LogEmitter = (level: LogLevel, message: string, data?: Record<string, unknown>) => void\n\ninterface DebouncerOptions {\n /** Debounce window in ms (default: 10000) */\n windowMs?: number\n}\n\ninterface PendingEntry {\n count: number\n timer: ReturnType<typeof setTimeout>\n data?: Record<string, unknown>\n}\n\n/**\n * Suppresses repeated log messages within a time window.\n * First occurrence is emitted immediately. Subsequent identical messages\n * are counted and a summary is emitted when the window expires.\n */\nexport class LogDebouncer {\n private pending = new Map<string, PendingEntry>()\n private windowMs: number\n\n constructor(\n private emit: LogEmitter,\n options: DebouncerOptions = {}\n ) {\n this.windowMs = options.windowMs ?? 10_000\n }\n\n log(level: LogLevel, message: string, data?: Record<string, unknown>): void {\n const key = `${level}:${message}`\n const existing = this.pending.get(key)\n\n if (existing) {\n existing.count++\n return\n }\n\n // Emit immediately\n this.emit(level, message, data)\n\n // Start counting window\n const entry: PendingEntry = {\n count: 0,\n data,\n timer: setTimeout(() => {\n this.pending.delete(key)\n if (entry.count > 0) {\n this.emit(level, `${message} (repeated ${entry.count} more times)`, data)\n }\n }, this.windowMs)\n }\n this.pending.set(key, entry)\n }\n\n dispose(): void {\n for (const entry of this.pending.values()) {\n clearTimeout(entry.timer)\n }\n this.pending.clear()\n }\n}\n","// Logger bootstrap — lives in core/ because it must initialize before modules load\nexport { logger, createChildLogger, setLoggerInstance, hasPinoPretty } from './proxy.js'\nexport { getLoggerConfig, loggerEnv } from './config.js'\nexport { initLoggerService, getLoggerService, resetLoggerService, getPinoLogger, captureExceptionSafe } from './service.js'\nexport { LogDebouncer } from './debounce.js'\nexport type { LoggerConfig, LoggerService } from './types.js'\n","import type { ComputedEntityDefinition, EventEntityDefinition, ActionDefinition, AuthRequest, ModuleContext } from '@gzl10/nexus-sdk'\nimport { useIdField, useSelectField, useTextField, useTextareaField, useSwitchField, useNumberField, useDatetimeField } from '@gzl10/nexus-sdk/fields'\nimport { getLoggerConfig } from '../../core/logger/config.js'\nimport { getLoggerService } from '../../core/logger/service.js'\n\n/**\n * Computed Entity: config\n * Shows the current logger configuration (read-only)\n */\nexport const loggerConfigEntity: ComputedEntityDefinition = {\n type: 'computed',\n label: { en: 'Logger Config', es: 'Configuración del registro' },\n routePrefix: '/config',\n\n fields: {\n level: useSelectField({\n label: { en: 'Log Level', es: 'Nivel de registro' },\n hint: { en: 'Env: LOG_LEVEL (default: info)', es: 'Env: LOG_LEVEL (default: info)' },\n options: [\n { value: 'silent', label: { en: 'Silent (no logs)', es: 'Silencio (sin logs)' } },\n { value: 'fatal', label: { en: 'Fatal', es: 'Fatal' } },\n { value: 'error', label: { en: 'Error', es: 'Error' } },\n { value: 'warn', label: { en: 'Warn', es: 'Advertencia' } },\n { value: 'info', label: { en: 'Info', es: 'Información' } },\n { value: 'debug', label: { en: 'Debug', es: 'Depuración' } },\n { value: 'trace', label: { en: 'Trace', es: 'Rastreo' } }\n ],\n nullable: false\n }),\n format: useSelectField({\n label: { en: 'Format', es: 'Formato' },\n hint: { en: 'Env: LOG_FORMAT (default: pretty)', es: 'Env: LOG_FORMAT (default: pretty)' },\n options: [\n { value: 'pretty', label: { en: 'Pretty (development)', es: 'Bonito (desarrollo)' } },\n { value: 'json', label: { en: 'JSON (production)', es: 'JSON (producción)' } }\n ],\n nullable: false\n }),\n sentry_enabled: useSwitchField({\n label: { en: 'Sentry Enabled', es: 'Sentry habilitado' },\n hint: { en: 'Env: SENTRY_DSN (optional)', es: 'Env: SENTRY_DSN (opcional)' }\n }),\n sentry_environment: useTextField({\n label: { en: 'Sentry Environment', es: 'Entorno de Sentry' },\n hint: { en: 'Env: SENTRY_ENVIRONMENT (default: development)', es: 'Env: SENTRY_ENVIRONMENT (default: development)' },\n size: 50,\n nullable: true\n }),\n sentry_sample_rate: useNumberField({\n label: { en: 'Sentry Sample Rate', es: 'Tasa de muestreo de Sentry' },\n hint: { en: 'Env: SENTRY_SAMPLE_RATE (default: 1.0)', es: 'Env: SENTRY_SAMPLE_RATE (default: 1.0)' },\n nullable: true\n })\n },\n\n compute: async () => {\n const config = getLoggerConfig()\n return [{\n level: config.level,\n format: config.format,\n sentry_enabled: !!config.sentry,\n sentry_environment: config.sentry?.environment ?? null,\n sentry_sample_rate: config.sentry?.sampleRate ?? null\n }]\n },\n\n cache: { ttl: 0 },\n\n casl: {\n subject: 'LoggerConfig',\n permissions: {\n SUPPORT: { actions: ['read'] },\n AUDITOR: { actions: ['read'] }\n }\n }\n}\n\n/**\n * Action: health\n * GET /logger/health - Logging service status\n *\n * - No auth: basic status only (for external health checks)\n * - ADMIN auth: full configuration info\n */\nexport const healthAction: ActionDefinition = {\n key: 'health',\n label: { en: 'Health Check', es: 'Verificación de salud' },\n icon: 'mdi:heart-pulse',\n scope: 'module',\n method: 'GET',\n skipAuth: true,\n output: {},\n handler: async (_ctx, _input, req) => {\n const config = getLoggerConfig()\n const logger = getLoggerService()\n\n // Info básica para todos (healthchecks)\n const basicInfo = {\n status: 'ok',\n level: config.level\n }\n\n // Info completa solo para admins autenticados\n const authReq = req as AuthRequest\n if (authReq.user && authReq.ability?.can('manage', 'all')) {\n return {\n ...basicInfo,\n format: config.format,\n sentry: {\n enabled: logger.isSentryEnabled(),\n environment: config.sentry?.environment ?? null\n }\n }\n }\n\n return basicInfo\n }\n}\n\n// ============================================================================\n// Client Errors (event entity + ingestion action)\n// ============================================================================\n\n/**\n * Event Entity: client_errors\n * Append-only log of errors reported by the frontend (UI/client).\n * Visible in Nexus admin panel for ADMIN/SUPPORT roles.\n */\nexport const clientErrorsEntity: EventEntityDefinition = {\n type: 'event',\n immutable: true,\n table: 'logger__client_errors',\n label: { en: 'Client Errors', es: 'Errores del cliente' },\n labelPlural: { en: 'Client Errors', es: 'Errores del cliente' },\n labelField: 'message',\n routePrefix: '/client-errors',\n retention: { days: 90 },\n\n fields: {\n id: useIdField(),\n level: {\n ...useSelectField({\n label: { en: 'Level', es: 'Nivel' },\n required: true,\n options: [\n { value: 'error', label: { en: 'Error', es: 'Error' } },\n { value: 'fatal', label: { en: 'Fatal', es: 'Fatal' } }\n ],\n meta: { sortable: true, searchable: true }\n }),\n validation: { enum: ['error', 'fatal'] }\n },\n tag: useTextField({\n label: { en: 'Tag', es: 'Etiqueta' },\n size: 100,\n nullable: true,\n meta: { searchable: true }\n }),\n message: useTextareaField({\n label: { en: 'Message', es: 'Mensaje' },\n required: true,\n meta: { searchable: true }\n }),\n url: useTextField({\n label: { en: 'URL', es: 'URL' },\n size: 2048,\n nullable: true\n }),\n user_agent: useTextareaField({\n label: { en: 'User Agent', es: 'Agente de usuario' },\n nullable: true,\n meta: { exportable: false }\n }),\n ip_address: useTextField({\n label: { en: 'IP Address', es: 'Dirección IP' },\n size: 45,\n nullable: true,\n meta: { searchable: true }\n }),\n created_at: useDatetimeField({\n label: { en: 'Date', es: 'Fecha' },\n disabled: true,\n nullable: false,\n meta: { sortable: true }\n })\n },\n\n defaultSort: { field: 'created_at', order: 'desc' },\n\n casl: {\n subject: 'ClientError',\n permissions: {\n ADMIN: { actions: ['read'] },\n SUPPORT: { actions: ['read'] },\n AUDITOR: { actions: ['read'] }\n }\n }\n}\n\n/**\n * Action: report-client-error\n * POST /logger/client-errors/report - Receives error reports from the frontend.\n *\n * - skipAuth: true (frontend may not have a valid token at time of error)\n * - Rate limited: 10 requests/min per IP\n * - Always responds 204 (no info leak)\n */\nexport const reportClientErrorAction: ActionDefinition = {\n key: 'report-client-error',\n label: { en: 'Report Client Error', es: 'Reportar error del cliente' },\n icon: 'mdi:alert-circle-outline',\n scope: 'module',\n method: 'POST',\n skipAuth: true,\n output: {},\n middleware: (ctx: ModuleContext) => ctx.core.middleware.rateLimit({ windowMs: 60_000, max: 10, message: 'Too many error reports' }),\n\n handler: async (ctx, input, req) => {\n const body = input as Record<string, unknown>\n const ip = req?.ip || req?.socket?.remoteAddress || 'unknown'\n\n // Validate required fields\n const message = typeof body['message'] === 'string' ? (body['message'] as string).slice(0, 5000) : null\n if (!message) {\n return { status: 204 }\n }\n\n const level = body['level'] === 'fatal' ? 'fatal' : 'error'\n const tag = typeof body['tag'] === 'string' ? (body['tag'] as string).slice(0, 100) : null\n const url = typeof body['url'] === 'string' ? (body['url'] as string).slice(0, 2048) : null\n const userAgent = typeof body['userAgent'] === 'string' ? (body['userAgent'] as string).slice(0, 1000) : null\n\n // Insert into event entity via runtime service\n try {\n const db = ctx.db.getKnex()\n await db('logger__client_errors').insert({\n id: crypto.randomUUID(),\n level,\n tag,\n message,\n url,\n user_agent: userAgent,\n ip_address: ip,\n created_at: new Date()\n })\n } catch {\n // Silently fail — don't let client error reporting break anything\n }\n\n // Also log via pino so Sentry captures it if enabled\n const pinoLogger = getLoggerService()\n pinoLogger.error({ source: 'client', tag, url, ip }, `[Client] ${message}`)\n\n return { status: 204 }\n }\n}\n","/**\n * @module logger\n * @description Pino-based structured logging with pluggable reporters\n */\n\nimport type { ModuleManifest, ModuleContext, EventEntityDefinition } from '@gzl10/nexus-sdk'\nimport { getLoggerConfig, initLoggerService, getPinoLogger, setLoggerInstance } from '../../core/logger/index.js'\nimport { loggerConfigEntity, healthAction, clientErrorsEntity, reportClientErrorAction } from './logger.entity.js'\n\n// Re-export types desde core\nexport type { LoggerConfig, LoggerService } from '../../core/logger/types.js'\n\nexport const loggerModule: ModuleManifest = {\n name: 'logger',\n label: { en: 'Logger', es: 'Registro' },\n icon: 'mdi:text-box-outline',\n description: { en: 'Centralized logging with Pino and optional Sentry integration', es: 'Registro centralizado con Pino e integración opcional con Sentry' },\n type: 'core',\n category: 'analytics',\n dependencies: [], // Sin dependencias - debe cargar primero\n definitions: [\n loggerConfigEntity,\n clientErrorsEntity as EventEntityDefinition & { type: 'event' }\n ],\n\n actions: [\n healthAction,\n reportClientErrorAction\n ],\n init: (ctx: ModuleContext) => {\n const config = getLoggerConfig()\n const service = initLoggerService(config)\n // Registrar servicio\n ctx.services.register('logger', service)\n // Actualizar el logger global de shared/logger.ts\n setLoggerInstance(getPinoLogger())\n service.debug('Logger module initialized')\n },\n routePrefix: '/logger'\n}\n","/**\n * Master Entity Definition\n *\n * Single table for all reference data, discriminated by `type`.\n * Type-specific fields stored in `metadata` JSON column.\n */\n\nimport type { CollectionEntityDefinition, RolePermission } from '@gzl10/nexus-sdk'\nimport {\n useTextField, useLocalizedField, useJsonField,\n isActiveField, orderField\n} from '@gzl10/nexus-sdk/fields'\n\nconst masterCaslPermissions: Record<string, RolePermission> = {\n '*': { actions: ['read'] },\n ADMIN: { actions: ['manage'] }\n}\n\nexport const mastersEntity: CollectionEntityDefinition = {\n table: \"masters\",\n seedable: true,\n routePrefix: \"/\",\n label: { en: \"Master\", es: \"Maestro\" },\n labelPlural: { en: \"Masters\", es: \"Maestros\" },\n labelField: \"label\",\n timestamps: true,\n availableDisplayModes: ['board'],\n groupBy: \"type\",\n groupableFields: ['type'],\n //columnDragFields: ['is_active'],\n fields: {\n id: useTextField({\n label: { en: \"ID\", es: \"ID\" },\n required: true,\n size: 100,\n hidden: true,\n meta: { sortable: true },\n }),\n type: useTextField({\n label: { en: \"Type\", es: \"Tipo\" },\n required: true,\n size: 50,\n index: true,\n meta: { sortable: true, searchable: true },\n }),\n code: useTextField({\n label: { en: \"Code\", es: \"Código\" },\n required: true,\n size: 50,\n meta: { sortable: true, searchable: true },\n }),\n label: useLocalizedField({ label: { en: \"Name\", es: \"Nombre\" } }),\n order: { ...orderField, meta: { ...orderField.meta, showInDisplay: false } },\n is_active: isActiveField,\n metadata: useJsonField({\n label: { en: \"Metadata\", es: \"Metadatos\" },\n hint: {\n en: \"Type-specific fields (symbol, flag, etc.)\",\n es: \"Campos específicos del tipo\",\n },\n meta: { showInDisplay: false },\n }),\n },\n hooks: () => ({\n beforeCreate: async (data: Record<string, unknown>) => {\n if (data['type'] && data['code'] && !data['id']) {\n data['id'] = `${data['type']}:${data['code']}`\n }\n return data\n },\n beforeUpdate: async (_id: string, data: Partial<Record<string, unknown>>) => {\n if ('type' in data || 'code' in data) {\n throw new Error('Cannot update type or code of a master record. Delete and recreate instead.')\n }\n return data\n }\n }),\n defaultSort: { field: \"order\", order: \"asc\" },\n indexes: [{ columns: [\"type\", \"code\"], unique: true }],\n casl: { subject: \"Master\", permissions: masterCaslPermissions },\n};\n","/**\n * Master Registry\n *\n * Collects master registrations during module init phase.\n * Plugins and modules call register() to add their reference data.\n * Seed runs after all modules have registered their masters.\n */\n\nimport type { ModuleContext } from '@gzl10/nexus-sdk'\n\nexport interface MasterEntry {\n code: string\n label: string | { en?: string; es?: string; [key: string]: string | undefined }\n order?: number\n is_active?: boolean\n metadata?: Record<string, unknown>\n}\n\nexport interface MasterRegistration {\n type: string\n entries: MasterEntry[]\n seed: 'if-empty' | 'always'\n}\n\n/**\n * Creates a registry for collecting and seeding master data.\n *\n * @example\n * ```typescript\n * // In a plugin's init():\n * const registry = ctx.services.get('masters')\n * registry.register('blood-types', [\n * { code: 'A+', label: { en: 'A positive', es: 'A positivo' } },\n * { code: 'O-', label: { en: 'O negative', es: 'O negativo' } },\n * ])\n * ```\n */\nexport function createMasterRegistry() {\n const registrations: MasterRegistration[] = []\n\n return {\n /**\n * Register master entries for a type. Called by plugins/modules in init().\n */\n register(type: string, entries: MasterEntry[], options?: { seed?: 'if-empty' | 'always' }) {\n registrations.push({\n type,\n entries,\n seed: options?.seed ?? 'if-empty'\n })\n },\n\n /**\n * Seed all registered masters into the DB. Called once after all modules init.\n */\n async seed(ctx: ModuleContext) {\n for (const reg of registrations) {\n const { type, entries, seed } = reg\n\n if (seed === 'if-empty') {\n const existing = await ctx.db.knex('masters').where({ type }).first()\n if (existing) continue\n }\n\n const rows = entries.map((entry, i) => ({\n id: `${type}:${entry.code}`,\n type,\n code: entry.code,\n label: JSON.stringify(typeof entry.label === 'string' ? { en: entry.label } : entry.label),\n order: entry.order ?? i,\n is_active: entry.is_active ?? true,\n metadata: entry.metadata ? JSON.stringify(entry.metadata) : null\n }))\n\n if (seed === 'always') {\n for (const row of rows) {\n await ctx.db.knex('masters')\n .insert(row)\n .onConflict(['type', 'code'])\n .merge()\n }\n } else {\n await ctx.db.knex('masters')\n .insert(rows)\n .onConflict(['type', 'code'])\n .ignore()\n }\n }\n },\n\n /** Get all registrations (for testing/debugging) */\n getRegistrations() {\n return [...registrations]\n }\n }\n}\n\nexport type MasterRegistry = ReturnType<typeof createMasterRegistry>\n","[\n {\n \"code\": \"EUR\",\n \"label\": {\n \"en\": \"Euro\",\n \"es\": \"Euro\"\n },\n \"order\": 1,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"€\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"USD\",\n \"label\": {\n \"en\": \"US Dollar\",\n \"es\": \"Dólar estadounidense\"\n },\n \"order\": 2,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"$\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"GBP\",\n \"label\": {\n \"en\": \"British Pound\",\n \"es\": \"Libra esterlina\"\n },\n \"order\": 3,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"£\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"JPY\",\n \"label\": {\n \"en\": \"Japanese Yen\",\n \"es\": \"Yen japonés\"\n },\n \"order\": 4,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"¥\",\n \"decimals\": 0\n }\n },\n {\n \"code\": \"CNY\",\n \"label\": {\n \"en\": \"Chinese Yuan\",\n \"es\": \"Yuan chino\"\n },\n \"order\": 5,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"¥\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"CHF\",\n \"label\": {\n \"en\": \"Swiss Franc\",\n \"es\": \"Franco suizo\"\n },\n \"order\": 6,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"CHF\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"CAD\",\n \"label\": {\n \"en\": \"Canadian Dollar\",\n \"es\": \"Dólar canadiense\"\n },\n \"order\": 7,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"C$\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"AUD\",\n \"label\": {\n \"en\": \"Australian Dollar\",\n \"es\": \"Dólar australiano\"\n },\n \"order\": 8,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"A$\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"MXN\",\n \"label\": {\n \"en\": \"Mexican Peso\",\n \"es\": \"Peso mexicano\"\n },\n \"order\": 9,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"$\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"ARS\",\n \"label\": {\n \"en\": \"Argentine Peso\",\n \"es\": \"Peso argentino\"\n },\n \"order\": 10,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"$\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"BRL\",\n \"label\": {\n \"en\": \"Brazilian Real\",\n \"es\": \"Real brasileño\"\n },\n \"order\": 11,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"R$\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"COP\",\n \"label\": {\n \"en\": \"Colombian Peso\",\n \"es\": \"Peso colombiano\"\n },\n \"order\": 12,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"$\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"CLP\",\n \"label\": {\n \"en\": \"Chilean Peso\",\n \"es\": \"Peso chileno\"\n },\n \"order\": 13,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"$\",\n \"decimals\": 0\n }\n },\n {\n \"code\": \"PEN\",\n \"label\": {\n \"en\": \"Peruvian Sol\",\n \"es\": \"Sol peruano\"\n },\n \"order\": 14,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"S/\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"UYU\",\n \"label\": {\n \"en\": \"Uruguayan Peso\",\n \"es\": \"Peso uruguayo\"\n },\n \"order\": 15,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"$U\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"BOB\",\n \"label\": {\n \"en\": \"Bolivian Boliviano\",\n \"es\": \"Boliviano\"\n },\n \"order\": 16,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"Bs\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"PYG\",\n \"label\": {\n \"en\": \"Paraguayan Guarani\",\n \"es\": \"Guaraní paraguayo\"\n },\n \"order\": 17,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"₲\",\n \"decimals\": 0\n }\n },\n {\n \"code\": \"VES\",\n \"label\": {\n \"en\": \"Venezuelan Bolivar\",\n \"es\": \"Bolívar venezolano\"\n },\n \"order\": 18,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"Bs.S\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"DOP\",\n \"label\": {\n \"en\": \"Dominican Peso\",\n \"es\": \"Peso dominicano\"\n },\n \"order\": 19,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"RD$\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"CRC\",\n \"label\": {\n \"en\": \"Costa Rican Colon\",\n \"es\": \"Colón costarricense\"\n },\n \"order\": 20,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"₡\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"GTQ\",\n \"label\": {\n \"en\": \"Guatemalan Quetzal\",\n \"es\": \"Quetzal guatemalteco\"\n },\n \"order\": 21,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"Q\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"HNL\",\n \"label\": {\n \"en\": \"Honduran Lempira\",\n \"es\": \"Lempira hondureño\"\n },\n \"order\": 22,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"L\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"NIO\",\n \"label\": {\n \"en\": \"Nicaraguan Cordoba\",\n \"es\": \"Córdoba nicaragüense\"\n },\n \"order\": 23,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"C$\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"PAB\",\n \"label\": {\n \"en\": \"Panamanian Balboa\",\n \"es\": \"Balboa panameño\"\n },\n \"order\": 24,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"B/.\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"INR\",\n \"label\": {\n \"en\": \"Indian Rupee\",\n \"es\": \"Rupia india\"\n },\n \"order\": 25,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"₹\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"KRW\",\n \"label\": {\n \"en\": \"South Korean Won\",\n \"es\": \"Won surcoreano\"\n },\n \"order\": 26,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"₩\",\n \"decimals\": 0\n }\n },\n {\n \"code\": \"SEK\",\n \"label\": {\n \"en\": \"Swedish Krona\",\n \"es\": \"Corona sueca\"\n },\n \"order\": 27,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"kr\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"NOK\",\n \"label\": {\n \"en\": \"Norwegian Krone\",\n \"es\": \"Corona noruega\"\n },\n \"order\": 28,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"kr\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"DKK\",\n \"label\": {\n \"en\": \"Danish Krone\",\n \"es\": \"Corona danesa\"\n },\n \"order\": 29,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"kr\",\n \"decimals\": 2\n }\n },\n {\n \"code\": \"PLN\",\n \"label\": {\n \"en\": \"Polish Zloty\",\n \"es\": \"Zloty polaco\"\n },\n \"order\": 30,\n \"is_active\": false,\n \"metadata\": {\n \"symbol\": \"zł\",\n \"decimals\": 2\n }\n }\n]\n","[\n {\n \"code\": \"es\",\n \"label\": {\n \"en\": \"Spanish\",\n \"es\": \"Español\"\n },\n \"order\": 1,\n \"is_active\": true,\n \"metadata\": {\n \"native_name\": \"Español\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"en\",\n \"label\": {\n \"en\": \"English\",\n \"es\": \"Inglés\"\n },\n \"order\": 2,\n \"is_active\": true,\n \"metadata\": {\n \"native_name\": \"English\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"fr\",\n \"label\": {\n \"en\": \"French\",\n \"es\": \"Francés\"\n },\n \"order\": 3,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Français\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"de\",\n \"label\": {\n \"en\": \"German\",\n \"es\": \"Alemán\"\n },\n \"order\": 4,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Deutsch\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"it\",\n \"label\": {\n \"en\": \"Italian\",\n \"es\": \"Italiano\"\n },\n \"order\": 5,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Italiano\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"pt\",\n \"label\": {\n \"en\": \"Portuguese\",\n \"es\": \"Portugués\"\n },\n \"order\": 6,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Português\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"nl\",\n \"label\": {\n \"en\": \"Dutch\",\n \"es\": \"Neerlandés\"\n },\n \"order\": 7,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Nederlands\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"pl\",\n \"label\": {\n \"en\": \"Polish\",\n \"es\": \"Polaco\"\n },\n \"order\": 8,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Polski\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"ru\",\n \"label\": {\n \"en\": \"Russian\",\n \"es\": \"Ruso\"\n },\n \"order\": 9,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Русский\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"uk\",\n \"label\": {\n \"en\": \"Ukrainian\",\n \"es\": \"Ucraniano\"\n },\n \"order\": 10,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Українська\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"zh\",\n \"label\": {\n \"en\": \"Chinese\",\n \"es\": \"Chino\"\n },\n \"order\": 11,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"中文\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"ja\",\n \"label\": {\n \"en\": \"Japanese\",\n \"es\": \"Japonés\"\n },\n \"order\": 12,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"日本語\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"ko\",\n \"label\": {\n \"en\": \"Korean\",\n \"es\": \"Coreano\"\n },\n \"order\": 13,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"한국어\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"ar\",\n \"label\": {\n \"en\": \"Arabic\",\n \"es\": \"Árabe\"\n },\n \"order\": 14,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"العربية\",\n \"direction\": \"rtl\"\n }\n },\n {\n \"code\": \"he\",\n \"label\": {\n \"en\": \"Hebrew\",\n \"es\": \"Hebreo\"\n },\n \"order\": 15,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"עברית\",\n \"direction\": \"rtl\"\n }\n },\n {\n \"code\": \"hi\",\n \"label\": {\n \"en\": \"Hindi\",\n \"es\": \"Hindi\"\n },\n \"order\": 16,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"हिन्दी\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"bn\",\n \"label\": {\n \"en\": \"Bengali\",\n \"es\": \"Bengalí\"\n },\n \"order\": 17,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"বাংলা\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"tr\",\n \"label\": {\n \"en\": \"Turkish\",\n \"es\": \"Turco\"\n },\n \"order\": 18,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Türkçe\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"vi\",\n \"label\": {\n \"en\": \"Vietnamese\",\n \"es\": \"Vietnamita\"\n },\n \"order\": 19,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Tiếng Việt\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"th\",\n \"label\": {\n \"en\": \"Thai\",\n \"es\": \"Tailandés\"\n },\n \"order\": 20,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"ไทย\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"id\",\n \"label\": {\n \"en\": \"Indonesian\",\n \"es\": \"Indonesio\"\n },\n \"order\": 21,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Bahasa Indonesia\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"ms\",\n \"label\": {\n \"en\": \"Malay\",\n \"es\": \"Malayo\"\n },\n \"order\": 22,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Bahasa Melayu\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"tl\",\n \"label\": {\n \"en\": \"Filipino\",\n \"es\": \"Filipino\"\n },\n \"order\": 23,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Filipino\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"sv\",\n \"label\": {\n \"en\": \"Swedish\",\n \"es\": \"Sueco\"\n },\n \"order\": 24,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Svenska\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"no\",\n \"label\": {\n \"en\": \"Norwegian\",\n \"es\": \"Noruego\"\n },\n \"order\": 25,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Norsk\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"da\",\n \"label\": {\n \"en\": \"Danish\",\n \"es\": \"Danés\"\n },\n \"order\": 26,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Dansk\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"fi\",\n \"label\": {\n \"en\": \"Finnish\",\n \"es\": \"Finés\"\n },\n \"order\": 27,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Suomi\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"el\",\n \"label\": {\n \"en\": \"Greek\",\n \"es\": \"Griego\"\n },\n \"order\": 28,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Ελληνικά\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"cs\",\n \"label\": {\n \"en\": \"Czech\",\n \"es\": \"Checo\"\n },\n \"order\": 29,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Čeština\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"sk\",\n \"label\": {\n \"en\": \"Slovak\",\n \"es\": \"Eslovaco\"\n },\n \"order\": 30,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Slovenčina\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"hu\",\n \"label\": {\n \"en\": \"Hungarian\",\n \"es\": \"Húngaro\"\n },\n \"order\": 31,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Magyar\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"ro\",\n \"label\": {\n \"en\": \"Romanian\",\n \"es\": \"Rumano\"\n },\n \"order\": 32,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Română\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"bg\",\n \"label\": {\n \"en\": \"Bulgarian\",\n \"es\": \"Búlgaro\"\n },\n \"order\": 33,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Български\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"hr\",\n \"label\": {\n \"en\": \"Croatian\",\n \"es\": \"Croata\"\n },\n \"order\": 34,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Hrvatski\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"sl\",\n \"label\": {\n \"en\": \"Slovenian\",\n \"es\": \"Esloveno\"\n },\n \"order\": 35,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Slovenščina\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"sr\",\n \"label\": {\n \"en\": \"Serbian\",\n \"es\": \"Serbio\"\n },\n \"order\": 36,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Српски\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"lt\",\n \"label\": {\n \"en\": \"Lithuanian\",\n \"es\": \"Lituano\"\n },\n \"order\": 37,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Lietuvių\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"lv\",\n \"label\": {\n \"en\": \"Latvian\",\n \"es\": \"Letón\"\n },\n \"order\": 38,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Latviešu\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"et\",\n \"label\": {\n \"en\": \"Estonian\",\n \"es\": \"Estonio\"\n },\n \"order\": 39,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Eesti\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"ca\",\n \"label\": {\n \"en\": \"Catalan\",\n \"es\": \"Catalán\"\n },\n \"order\": 40,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Català\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"gl\",\n \"label\": {\n \"en\": \"Galician\",\n \"es\": \"Gallego\"\n },\n \"order\": 41,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Galego\",\n \"direction\": \"ltr\"\n }\n },\n {\n \"code\": \"eu\",\n \"label\": {\n \"en\": \"Basque\",\n \"es\": \"Euskera\"\n },\n \"order\": 42,\n \"is_active\": false,\n \"metadata\": {\n \"native_name\": \"Euskara\",\n \"direction\": \"ltr\"\n }\n }\n]\n","[\n {\n \"code\": \"UTC\",\n \"label\": {\n \"en\": \"(UTC+00:00) UTC\",\n \"es\": \"(UTC+00:00) UTC\"\n },\n \"order\": 1,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+00:00\"\n }\n },\n {\n \"code\": \"Europe/Madrid\",\n \"label\": {\n \"en\": \"(UTC+01:00) Madrid\",\n \"es\": \"(UTC+01:00) Madrid\"\n },\n \"order\": 2,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+01:00\"\n }\n },\n {\n \"code\": \"Europe/London\",\n \"label\": {\n \"en\": \"(UTC+00:00) London\",\n \"es\": \"(UTC+00:00) Londres\"\n },\n \"order\": 3,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+00:00\"\n }\n },\n {\n \"code\": \"Europe/Paris\",\n \"label\": {\n \"en\": \"(UTC+01:00) Paris\",\n \"es\": \"(UTC+01:00) París\"\n },\n \"order\": 4,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+01:00\"\n }\n },\n {\n \"code\": \"Europe/Berlin\",\n \"label\": {\n \"en\": \"(UTC+01:00) Berlin\",\n \"es\": \"(UTC+01:00) Berlín\"\n },\n \"order\": 5,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+01:00\"\n }\n },\n {\n \"code\": \"Europe/Rome\",\n \"label\": {\n \"en\": \"(UTC+01:00) Rome\",\n \"es\": \"(UTC+01:00) Roma\"\n },\n \"order\": 6,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+01:00\"\n }\n },\n {\n \"code\": \"Europe/Lisbon\",\n \"label\": {\n \"en\": \"(UTC+00:00) Lisbon\",\n \"es\": \"(UTC+00:00) Lisboa\"\n },\n \"order\": 7,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+00:00\"\n }\n },\n {\n \"code\": \"Europe/Amsterdam\",\n \"label\": {\n \"en\": \"(UTC+01:00) Amsterdam\",\n \"es\": \"(UTC+01:00) Ámsterdam\"\n },\n \"order\": 8,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+01:00\"\n }\n },\n {\n \"code\": \"Europe/Brussels\",\n \"label\": {\n \"en\": \"(UTC+01:00) Brussels\",\n \"es\": \"(UTC+01:00) Bruselas\"\n },\n \"order\": 9,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+01:00\"\n }\n },\n {\n \"code\": \"Europe/Zurich\",\n \"label\": {\n \"en\": \"(UTC+01:00) Zurich\",\n \"es\": \"(UTC+01:00) Zúrich\"\n },\n \"order\": 10,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+01:00\"\n }\n },\n {\n \"code\": \"Europe/Vienna\",\n \"label\": {\n \"en\": \"(UTC+01:00) Vienna\",\n \"es\": \"(UTC+01:00) Viena\"\n },\n \"order\": 11,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+01:00\"\n }\n },\n {\n \"code\": \"Europe/Warsaw\",\n \"label\": {\n \"en\": \"(UTC+01:00) Warsaw\",\n \"es\": \"(UTC+01:00) Varsovia\"\n },\n \"order\": 12,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+01:00\"\n }\n },\n {\n \"code\": \"Europe/Prague\",\n \"label\": {\n \"en\": \"(UTC+01:00) Prague\",\n \"es\": \"(UTC+01:00) Praga\"\n },\n \"order\": 13,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+01:00\"\n }\n },\n {\n \"code\": \"Europe/Stockholm\",\n \"label\": {\n \"en\": \"(UTC+01:00) Stockholm\",\n \"es\": \"(UTC+01:00) Estocolmo\"\n },\n \"order\": 14,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+01:00\"\n }\n },\n {\n \"code\": \"Europe/Oslo\",\n \"label\": {\n \"en\": \"(UTC+01:00) Oslo\",\n \"es\": \"(UTC+01:00) Oslo\"\n },\n \"order\": 15,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+01:00\"\n }\n },\n {\n \"code\": \"Europe/Copenhagen\",\n \"label\": {\n \"en\": \"(UTC+01:00) Copenhagen\",\n \"es\": \"(UTC+01:00) Copenhague\"\n },\n \"order\": 16,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+01:00\"\n }\n },\n {\n \"code\": \"Europe/Helsinki\",\n \"label\": {\n \"en\": \"(UTC+02:00) Helsinki\",\n \"es\": \"(UTC+02:00) Helsinki\"\n },\n \"order\": 17,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+02:00\"\n }\n },\n {\n \"code\": \"Europe/Athens\",\n \"label\": {\n \"en\": \"(UTC+02:00) Athens\",\n \"es\": \"(UTC+02:00) Atenas\"\n },\n \"order\": 18,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+02:00\"\n }\n },\n {\n \"code\": \"Europe/Moscow\",\n \"label\": {\n \"en\": \"(UTC+03:00) Moscow\",\n \"es\": \"(UTC+03:00) Moscú\"\n },\n \"order\": 19,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+03:00\"\n }\n },\n {\n \"code\": \"America/New_York\",\n \"label\": {\n \"en\": \"(UTC-05:00) New York\",\n \"es\": \"(UTC-05:00) Nueva York\"\n },\n \"order\": 20,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"-05:00\"\n }\n },\n {\n \"code\": \"America/Chicago\",\n \"label\": {\n \"en\": \"(UTC-06:00) Chicago\",\n \"es\": \"(UTC-06:00) Chicago\"\n },\n \"order\": 21,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"-06:00\"\n }\n },\n {\n \"code\": \"America/Denver\",\n \"label\": {\n \"en\": \"(UTC-07:00) Denver\",\n \"es\": \"(UTC-07:00) Denver\"\n },\n \"order\": 22,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"-07:00\"\n }\n },\n {\n \"code\": \"America/Los_Angeles\",\n \"label\": {\n \"en\": \"(UTC-08:00) Los Angeles\",\n \"es\": \"(UTC-08:00) Los Ángeles\"\n },\n \"order\": 23,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"-08:00\"\n }\n },\n {\n \"code\": \"America/Toronto\",\n \"label\": {\n \"en\": \"(UTC-05:00) Toronto\",\n \"es\": \"(UTC-05:00) Toronto\"\n },\n \"order\": 24,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"-05:00\"\n }\n },\n {\n \"code\": \"America/Mexico_City\",\n \"label\": {\n \"en\": \"(UTC-06:00) Mexico City\",\n \"es\": \"(UTC-06:00) Ciudad de México\"\n },\n \"order\": 25,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"-06:00\"\n }\n },\n {\n \"code\": \"America/Buenos_Aires\",\n \"label\": {\n \"en\": \"(UTC-03:00) Buenos Aires\",\n \"es\": \"(UTC-03:00) Buenos Aires\"\n },\n \"order\": 26,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"-03:00\"\n }\n },\n {\n \"code\": \"America/Sao_Paulo\",\n \"label\": {\n \"en\": \"(UTC-03:00) São Paulo\",\n \"es\": \"(UTC-03:00) São Paulo\"\n },\n \"order\": 27,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"-03:00\"\n }\n },\n {\n \"code\": \"America/Bogota\",\n \"label\": {\n \"en\": \"(UTC-05:00) Bogotá\",\n \"es\": \"(UTC-05:00) Bogotá\"\n },\n \"order\": 28,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"-05:00\"\n }\n },\n {\n \"code\": \"America/Lima\",\n \"label\": {\n \"en\": \"(UTC-05:00) Lima\",\n \"es\": \"(UTC-05:00) Lima\"\n },\n \"order\": 29,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"-05:00\"\n }\n },\n {\n \"code\": \"America/Santiago\",\n \"label\": {\n \"en\": \"(UTC-04:00) Santiago\",\n \"es\": \"(UTC-04:00) Santiago\"\n },\n \"order\": 30,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"-04:00\"\n }\n },\n {\n \"code\": \"America/Caracas\",\n \"label\": {\n \"en\": \"(UTC-04:00) Caracas\",\n \"es\": \"(UTC-04:00) Caracas\"\n },\n \"order\": 31,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"-04:00\"\n }\n },\n {\n \"code\": \"Asia/Tokyo\",\n \"label\": {\n \"en\": \"(UTC+09:00) Tokyo\",\n \"es\": \"(UTC+09:00) Tokio\"\n },\n \"order\": 32,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+09:00\"\n }\n },\n {\n \"code\": \"Asia/Shanghai\",\n \"label\": {\n \"en\": \"(UTC+08:00) Shanghai\",\n \"es\": \"(UTC+08:00) Shanghái\"\n },\n \"order\": 33,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+08:00\"\n }\n },\n {\n \"code\": \"Asia/Hong_Kong\",\n \"label\": {\n \"en\": \"(UTC+08:00) Hong Kong\",\n \"es\": \"(UTC+08:00) Hong Kong\"\n },\n \"order\": 34,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+08:00\"\n }\n },\n {\n \"code\": \"Asia/Singapore\",\n \"label\": {\n \"en\": \"(UTC+08:00) Singapore\",\n \"es\": \"(UTC+08:00) Singapur\"\n },\n \"order\": 35,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+08:00\"\n }\n },\n {\n \"code\": \"Asia/Seoul\",\n \"label\": {\n \"en\": \"(UTC+09:00) Seoul\",\n \"es\": \"(UTC+09:00) Seúl\"\n },\n \"order\": 36,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+09:00\"\n }\n },\n {\n \"code\": \"Asia/Dubai\",\n \"label\": {\n \"en\": \"(UTC+04:00) Dubai\",\n \"es\": \"(UTC+04:00) Dubái\"\n },\n \"order\": 37,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+04:00\"\n }\n },\n {\n \"code\": \"Asia/Kolkata\",\n \"label\": {\n \"en\": \"(UTC+05:30) Kolkata\",\n \"es\": \"(UTC+05:30) Calcuta\"\n },\n \"order\": 38,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+05:30\"\n }\n },\n {\n \"code\": \"Australia/Sydney\",\n \"label\": {\n \"en\": \"(UTC+11:00) Sydney\",\n \"es\": \"(UTC+11:00) Sídney\"\n },\n \"order\": 39,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+11:00\"\n }\n },\n {\n \"code\": \"Pacific/Auckland\",\n \"label\": {\n \"en\": \"(UTC+13:00) Auckland\",\n \"es\": \"(UTC+13:00) Auckland\"\n },\n \"order\": 40,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+13:00\"\n }\n },\n {\n \"code\": \"Atlantic/Canary\",\n \"label\": {\n \"en\": \"(UTC+00:00) Canary Islands\",\n \"es\": \"(UTC+00:00) Islas Canarias\"\n },\n \"order\": 41,\n \"is_active\": true,\n \"metadata\": {\n \"offset\": \"+00:00\"\n }\n }\n]\n","[\n {\n \"code\": \"linkedin\",\n \"label\": {\n \"en\": \"LinkedIn\"\n },\n \"order\": 1,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:linkedin\",\n \"base_url\": \"https://linkedin.com/in/\",\n \"placeholder\": \"username\"\n }\n },\n {\n \"code\": \"twitter\",\n \"label\": {\n \"en\": \"X (Twitter)\"\n },\n \"order\": 2,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:twitter\",\n \"base_url\": \"https://x.com/\",\n \"placeholder\": \"@username\"\n }\n },\n {\n \"code\": \"github\",\n \"label\": {\n \"en\": \"GitHub\"\n },\n \"order\": 3,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:github\",\n \"base_url\": \"https://github.com/\",\n \"placeholder\": \"username\"\n }\n },\n {\n \"code\": \"instagram\",\n \"label\": {\n \"en\": \"Instagram\"\n },\n \"order\": 4,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:instagram\",\n \"base_url\": \"https://instagram.com/\",\n \"placeholder\": \"@username\"\n }\n },\n {\n \"code\": \"facebook\",\n \"label\": {\n \"en\": \"Facebook\"\n },\n \"order\": 5,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:facebook\",\n \"base_url\": \"https://facebook.com/\",\n \"placeholder\": \"username\"\n }\n },\n {\n \"code\": \"youtube\",\n \"label\": {\n \"en\": \"YouTube\"\n },\n \"order\": 6,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:youtube\",\n \"base_url\": \"https://youtube.com/@\",\n \"placeholder\": \"@channel\"\n }\n },\n {\n \"code\": \"tiktok\",\n \"label\": {\n \"en\": \"TikTok\"\n },\n \"order\": 7,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:music-note\",\n \"base_url\": \"https://tiktok.com/@\",\n \"placeholder\": \"@username\"\n }\n },\n {\n \"code\": \"whatsapp\",\n \"label\": {\n \"en\": \"WhatsApp\"\n },\n \"order\": 8,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:whatsapp\",\n \"base_url\": \"https://wa.me/\",\n \"placeholder\": \"+34600000000\"\n }\n },\n {\n \"code\": \"telegram\",\n \"label\": {\n \"en\": \"Telegram\"\n },\n \"order\": 9,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:telegram\",\n \"base_url\": \"https://t.me/\",\n \"placeholder\": \"@username\"\n }\n },\n {\n \"code\": \"discord\",\n \"label\": {\n \"en\": \"Discord\"\n },\n \"order\": 10,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:discord\",\n \"base_url\": null,\n \"placeholder\": \"username#0000\"\n }\n },\n {\n \"code\": \"slack\",\n \"label\": {\n \"en\": \"Slack\"\n },\n \"order\": 11,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:slack\",\n \"base_url\": null,\n \"placeholder\": \"workspace\"\n }\n },\n {\n \"code\": \"pinterest\",\n \"label\": {\n \"en\": \"Pinterest\"\n },\n \"order\": 12,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:pinterest\",\n \"base_url\": \"https://pinterest.com/\",\n \"placeholder\": \"username\"\n }\n },\n {\n \"code\": \"reddit\",\n \"label\": {\n \"en\": \"Reddit\"\n },\n \"order\": 13,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:reddit\",\n \"base_url\": \"https://reddit.com/user/\",\n \"placeholder\": \"u/username\"\n }\n },\n {\n \"code\": \"twitch\",\n \"label\": {\n \"en\": \"Twitch\"\n },\n \"order\": 14,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:twitch\",\n \"base_url\": \"https://twitch.tv/\",\n \"placeholder\": \"username\"\n }\n },\n {\n \"code\": \"bereal\",\n \"label\": {\n \"en\": \"BeReal\"\n },\n \"order\": 15,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:camera\",\n \"base_url\": null,\n \"placeholder\": \"username\"\n }\n }\n]\n","[\n {\n \"code\": \"MALE\",\n \"label\": {\n \"en\": \"Male\",\n \"es\": \"Masculino\"\n },\n \"order\": 1,\n \"is_active\": true\n },\n {\n \"code\": \"FEMALE\",\n \"label\": {\n \"en\": \"Female\",\n \"es\": \"Femenino\"\n },\n \"order\": 2,\n \"is_active\": true\n },\n {\n \"code\": \"OTHER\",\n \"label\": {\n \"en\": \"Other\",\n \"es\": \"Otro\"\n },\n \"order\": 3,\n \"is_active\": true\n },\n {\n \"code\": \"PREFER_NOT_TO_SAY\",\n \"label\": {\n \"en\": \"Prefer not to say\",\n \"es\": \"Prefiero no decir\"\n },\n \"order\": 4,\n \"is_active\": true\n }\n]\n","[\n {\n \"code\": \"SINGLE\",\n \"label\": {\n \"en\": \"Single\",\n \"es\": \"Soltero/a\"\n },\n \"order\": 1,\n \"is_active\": true\n },\n {\n \"code\": \"MARRIED\",\n \"label\": {\n \"en\": \"Married\",\n \"es\": \"Casado/a\"\n },\n \"order\": 2,\n \"is_active\": true\n },\n {\n \"code\": \"DOMESTIC_PARTNERSHIP\",\n \"label\": {\n \"en\": \"Domestic Partnership\",\n \"es\": \"Pareja de hecho\"\n },\n \"order\": 3,\n \"is_active\": true\n },\n {\n \"code\": \"DIVORCED\",\n \"label\": {\n \"en\": \"Divorced\",\n \"es\": \"Divorciado/a\"\n },\n \"order\": 4,\n \"is_active\": true\n },\n {\n \"code\": \"SEPARATED\",\n \"label\": {\n \"en\": \"Separated\",\n \"es\": \"Separado/a\"\n },\n \"order\": 5,\n \"is_active\": true\n },\n {\n \"code\": \"WIDOWED\",\n \"label\": {\n \"en\": \"Widowed\",\n \"es\": \"Viudo/a\"\n },\n \"order\": 6,\n \"is_active\": true\n }\n]\n","[\n {\n \"code\": \"NONE\",\n \"label\": {\n \"en\": \"No formal education\",\n \"es\": \"Sin estudios\"\n },\n \"order\": 0,\n \"is_active\": true\n },\n {\n \"code\": \"PRIMARY\",\n \"label\": {\n \"en\": \"Primary Education\",\n \"es\": \"Educacion Primaria\"\n },\n \"order\": 1,\n \"is_active\": true\n },\n {\n \"code\": \"SECONDARY\",\n \"label\": {\n \"en\": \"Secondary Education (ESO)\",\n \"es\": \"Educacion Secundaria (ESO)\"\n },\n \"order\": 2,\n \"is_active\": true\n },\n {\n \"code\": \"HIGH_SCHOOL\",\n \"label\": {\n \"en\": \"High School (Bachillerato)\",\n \"es\": \"Bachillerato\"\n },\n \"order\": 3,\n \"is_active\": true\n },\n {\n \"code\": \"VOCATIONAL\",\n \"label\": {\n \"en\": \"Vocational Training (FP)\",\n \"es\": \"Formacion Profesional (FP)\"\n },\n \"order\": 4,\n \"is_active\": true\n },\n {\n \"code\": \"BACHELOR\",\n \"label\": {\n \"en\": \"Bachelor's Degree\",\n \"es\": \"Grado Universitario\"\n },\n \"order\": 5,\n \"is_active\": true\n },\n {\n \"code\": \"MASTER\",\n \"label\": {\n \"en\": \"Master's Degree\",\n \"es\": \"Master\"\n },\n \"order\": 6,\n \"is_active\": true\n },\n {\n \"code\": \"DOCTORATE\",\n \"label\": {\n \"en\": \"Doctorate (PhD)\",\n \"es\": \"Doctorado\"\n },\n \"order\": 7,\n \"is_active\": true\n }\n]\n","[\n {\n \"code\": \"TECH\",\n \"label\": {\n \"en\": \"Technology & Software\",\n \"es\": \"Tecnologia y Software\"\n },\n \"order\": 1,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:laptop\"\n }\n },\n {\n \"code\": \"FINANCE\",\n \"label\": {\n \"en\": \"Finance & Banking\",\n \"es\": \"Finanzas y Banca\"\n },\n \"order\": 2,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:bank\"\n }\n },\n {\n \"code\": \"HEALTHCARE\",\n \"label\": {\n \"en\": \"Healthcare & Pharma\",\n \"es\": \"Salud y Farmacia\"\n },\n \"order\": 3,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:hospital\"\n }\n },\n {\n \"code\": \"EDUCATION\",\n \"label\": {\n \"en\": \"Education\",\n \"es\": \"Educacion\"\n },\n \"order\": 4,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:school\"\n }\n },\n {\n \"code\": \"RETAIL\",\n \"label\": {\n \"en\": \"Retail & Commerce\",\n \"es\": \"Comercio y Retail\"\n },\n \"order\": 5,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:store\"\n }\n },\n {\n \"code\": \"MANUFACTURING\",\n \"label\": {\n \"en\": \"Manufacturing\",\n \"es\": \"Manufactura\"\n },\n \"order\": 6,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:factory\"\n }\n },\n {\n \"code\": \"CONSTRUCTION\",\n \"label\": {\n \"en\": \"Construction\",\n \"es\": \"Construccion\"\n },\n \"order\": 7,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:hammer\"\n }\n },\n {\n \"code\": \"TRANSPORT\",\n \"label\": {\n \"en\": \"Transport & Logistics\",\n \"es\": \"Transporte y Logistica\"\n },\n \"order\": 8,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:truck\"\n }\n },\n {\n \"code\": \"HOSPITALITY\",\n \"label\": {\n \"en\": \"Hospitality & Tourism\",\n \"es\": \"Hosteleria y Turismo\"\n },\n \"order\": 9,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:bed\"\n }\n },\n {\n \"code\": \"REAL_ESTATE\",\n \"label\": {\n \"en\": \"Real Estate\",\n \"es\": \"Inmobiliaria\"\n },\n \"order\": 10,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:home-city\"\n }\n },\n {\n \"code\": \"ENERGY\",\n \"label\": {\n \"en\": \"Energy & Utilities\",\n \"es\": \"Energia y Utilities\"\n },\n \"order\": 11,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:lightning-bolt\"\n }\n },\n {\n \"code\": \"AGRICULTURE\",\n \"label\": {\n \"en\": \"Agriculture\",\n \"es\": \"Agricultura\"\n },\n \"order\": 12,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:sprout\"\n }\n },\n {\n \"code\": \"MEDIA\",\n \"label\": {\n \"en\": \"Media & Entertainment\",\n \"es\": \"Medios y Entretenimiento\"\n },\n \"order\": 13,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:television\"\n }\n },\n {\n \"code\": \"TELECOM\",\n \"label\": {\n \"en\": \"Telecommunications\",\n \"es\": \"Telecomunicaciones\"\n },\n \"order\": 14,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:antenna\"\n }\n },\n {\n \"code\": \"CONSULTING\",\n \"label\": {\n \"en\": \"Consulting\",\n \"es\": \"Consultoria\"\n },\n \"order\": 15,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:briefcase\"\n }\n },\n {\n \"code\": \"LEGAL\",\n \"label\": {\n \"en\": \"Legal Services\",\n \"es\": \"Servicios Legales\"\n },\n \"order\": 16,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:gavel\"\n }\n },\n {\n \"code\": \"INSURANCE\",\n \"label\": {\n \"en\": \"Insurance\",\n \"es\": \"Seguros\"\n },\n \"order\": 17,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:shield\"\n }\n },\n {\n \"code\": \"AUTOMOTIVE\",\n \"label\": {\n \"en\": \"Automotive\",\n \"es\": \"Automocion\"\n },\n \"order\": 18,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:car\"\n }\n },\n {\n \"code\": \"FOOD\",\n \"label\": {\n \"en\": \"Food & Beverage\",\n \"es\": \"Alimentacion y Bebidas\"\n },\n \"order\": 19,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:food\"\n }\n },\n {\n \"code\": \"OTHER\",\n \"label\": {\n \"en\": \"Other\",\n \"es\": \"Otros\"\n },\n \"order\": 99,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:dots-horizontal\"\n }\n }\n]\n","[\n {\n \"code\": \"AUTONOMO\",\n \"label\": {\n \"en\": \"Self-employed\",\n \"es\": \"Autonomo\"\n },\n \"order\": 1,\n \"is_active\": true,\n \"metadata\": {\n \"description\": {\n \"en\": \"Individual self-employed worker\",\n \"es\": \"Trabajador por cuenta propia\"\n }\n }\n },\n {\n \"code\": \"SL\",\n \"label\": {\n \"en\": \"Limited Company (SL)\",\n \"es\": \"Sociedad Limitada (SL)\"\n },\n \"order\": 2,\n \"is_active\": true,\n \"metadata\": {\n \"description\": {\n \"en\": \"Limited liability company\",\n \"es\": \"Sociedad de responsabilidad limitada\"\n }\n }\n },\n {\n \"code\": \"SLU\",\n \"label\": {\n \"en\": \"Single-member Ltd (SLU)\",\n \"es\": \"Sociedad Limitada Unipersonal (SLU)\"\n },\n \"order\": 3,\n \"is_active\": true,\n \"metadata\": {\n \"description\": {\n \"en\": \"Single-member limited company\",\n \"es\": \"SL con un unico socio\"\n }\n }\n },\n {\n \"code\": \"SA\",\n \"label\": {\n \"en\": \"Corporation (SA)\",\n \"es\": \"Sociedad Anonima (SA)\"\n },\n \"order\": 4,\n \"is_active\": true,\n \"metadata\": {\n \"description\": {\n \"en\": \"Public limited company\",\n \"es\": \"Sociedad por acciones\"\n }\n }\n },\n {\n \"code\": \"COOP\",\n \"label\": {\n \"en\": \"Cooperative\",\n \"es\": \"Cooperativa\"\n },\n \"order\": 5,\n \"is_active\": true,\n \"metadata\": {\n \"description\": {\n \"en\": \"Worker-owned cooperative\",\n \"es\": \"Sociedad cooperativa\"\n }\n }\n },\n {\n \"code\": \"CB\",\n \"label\": {\n \"en\": \"Community of Goods\",\n \"es\": \"Comunidad de Bienes (CB)\"\n },\n \"order\": 6,\n \"is_active\": true,\n \"metadata\": {\n \"description\": {\n \"en\": \"Partnership without legal entity\",\n \"es\": \"Agrupacion sin personalidad juridica\"\n }\n }\n },\n {\n \"code\": \"SC\",\n \"label\": {\n \"en\": \"Civil Partnership\",\n \"es\": \"Sociedad Civil (SC)\"\n },\n \"order\": 7,\n \"is_active\": true,\n \"metadata\": {\n \"description\": {\n \"en\": \"Civil partnership\",\n \"es\": \"Sociedad civil privada\"\n }\n }\n },\n {\n \"code\": \"ASSOC\",\n \"label\": {\n \"en\": \"Association\",\n \"es\": \"Asociacion\"\n },\n \"order\": 8,\n \"is_active\": true,\n \"metadata\": {\n \"description\": {\n \"en\": \"Non-profit association\",\n \"es\": \"Asociacion sin animo de lucro\"\n }\n }\n },\n {\n \"code\": \"FOUND\",\n \"label\": {\n \"en\": \"Foundation\",\n \"es\": \"Fundacion\"\n },\n \"order\": 9,\n \"is_active\": true,\n \"metadata\": {\n \"description\": {\n \"en\": \"Non-profit foundation\",\n \"es\": \"Fundacion sin animo de lucro\"\n }\n }\n },\n {\n \"code\": \"OTHER\",\n \"label\": {\n \"en\": \"Other\",\n \"es\": \"Otros\"\n },\n \"order\": 99,\n \"is_active\": true,\n \"metadata\": {\n \"description\": {\n \"en\": \"Other legal form\",\n \"es\": \"Otra forma juridica\"\n }\n }\n }\n]\n","[\n {\n \"code\": \"KG\",\n \"label\": {\n \"en\": \"Kilogram\",\n \"es\": \"Kilogramo\"\n },\n \"order\": 1,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"kg\",\n \"category\": \"weight\"\n }\n },\n {\n \"code\": \"G\",\n \"label\": {\n \"en\": \"Gram\",\n \"es\": \"Gramo\"\n },\n \"order\": 2,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"g\",\n \"category\": \"weight\"\n }\n },\n {\n \"code\": \"MG\",\n \"label\": {\n \"en\": \"Milligram\",\n \"es\": \"Miligramo\"\n },\n \"order\": 3,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"mg\",\n \"category\": \"weight\"\n }\n },\n {\n \"code\": \"T\",\n \"label\": {\n \"en\": \"Ton\",\n \"es\": \"Tonelada\"\n },\n \"order\": 4,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"t\",\n \"category\": \"weight\"\n }\n },\n {\n \"code\": \"L\",\n \"label\": {\n \"en\": \"Liter\",\n \"es\": \"Litro\"\n },\n \"order\": 10,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"l\",\n \"category\": \"volume\"\n }\n },\n {\n \"code\": \"ML\",\n \"label\": {\n \"en\": \"Milliliter\",\n \"es\": \"Mililitro\"\n },\n \"order\": 11,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"ml\",\n \"category\": \"volume\"\n }\n },\n {\n \"code\": \"M3\",\n \"label\": {\n \"en\": \"Cubic Meter\",\n \"es\": \"Metro cubico\"\n },\n \"order\": 12,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"m3\",\n \"category\": \"volume\"\n }\n },\n {\n \"code\": \"M\",\n \"label\": {\n \"en\": \"Meter\",\n \"es\": \"Metro\"\n },\n \"order\": 20,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"m\",\n \"category\": \"length\"\n }\n },\n {\n \"code\": \"CM\",\n \"label\": {\n \"en\": \"Centimeter\",\n \"es\": \"Centimetro\"\n },\n \"order\": 21,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"cm\",\n \"category\": \"length\"\n }\n },\n {\n \"code\": \"MM\",\n \"label\": {\n \"en\": \"Millimeter\",\n \"es\": \"Milimetro\"\n },\n \"order\": 22,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"mm\",\n \"category\": \"length\"\n }\n },\n {\n \"code\": \"KM\",\n \"label\": {\n \"en\": \"Kilometer\",\n \"es\": \"Kilometro\"\n },\n \"order\": 23,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"km\",\n \"category\": \"length\"\n }\n },\n {\n \"code\": \"M2\",\n \"label\": {\n \"en\": \"Square Meter\",\n \"es\": \"Metro cuadrado\"\n },\n \"order\": 30,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"m2\",\n \"category\": \"area\"\n }\n },\n {\n \"code\": \"HA\",\n \"label\": {\n \"en\": \"Hectare\",\n \"es\": \"Hectarea\"\n },\n \"order\": 31,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"ha\",\n \"category\": \"area\"\n }\n },\n {\n \"code\": \"UNIT\",\n \"label\": {\n \"en\": \"Unit\",\n \"es\": \"Unidad\"\n },\n \"order\": 40,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"ud\",\n \"category\": \"count\"\n }\n },\n {\n \"code\": \"DOZEN\",\n \"label\": {\n \"en\": \"Dozen\",\n \"es\": \"Docena\"\n },\n \"order\": 41,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"dz\",\n \"category\": \"count\"\n }\n },\n {\n \"code\": \"PAIR\",\n \"label\": {\n \"en\": \"Pair\",\n \"es\": \"Par\"\n },\n \"order\": 42,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"par\",\n \"category\": \"count\"\n }\n },\n {\n \"code\": \"BOX\",\n \"label\": {\n \"en\": \"Box\",\n \"es\": \"Caja\"\n },\n \"order\": 43,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"caja\",\n \"category\": \"count\"\n }\n },\n {\n \"code\": \"PALLET\",\n \"label\": {\n \"en\": \"Pallet\",\n \"es\": \"Pale\"\n },\n \"order\": 44,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"palet\",\n \"category\": \"count\"\n }\n },\n {\n \"code\": \"HOUR\",\n \"label\": {\n \"en\": \"Hour\",\n \"es\": \"Hora\"\n },\n \"order\": 50,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"h\",\n \"category\": \"time\"\n }\n },\n {\n \"code\": \"DAY\",\n \"label\": {\n \"en\": \"Day\",\n \"es\": \"Dia\"\n },\n \"order\": 51,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"d\",\n \"category\": \"time\"\n }\n },\n {\n \"code\": \"MONTH\",\n \"label\": {\n \"en\": \"Month\",\n \"es\": \"Mes\"\n },\n \"order\": 52,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"mes\",\n \"category\": \"time\"\n }\n },\n {\n \"code\": \"YEAR\",\n \"label\": {\n \"en\": \"Year\",\n \"es\": \"Ano\"\n },\n \"order\": 53,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"ano\",\n \"category\": \"time\"\n }\n },\n {\n \"code\": \"PERCENT\",\n \"label\": {\n \"en\": \"Percentage\",\n \"es\": \"Porcentaje\"\n },\n \"order\": 90,\n \"is_active\": true,\n \"metadata\": {\n \"symbol\": \"%\",\n \"category\": \"other\"\n }\n }\n]\n","[\n {\n \"code\": \"AD\",\n \"label\": {\n \"en\": \"Andorra\",\n \"es\": \"Andorra\"\n },\n \"order\": 1,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"AND\",\n \"numeric\": \"020\",\n \"phone_prefix\": \"+376\",\n \"flag\": \"🇦🇩\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"AE\",\n \"label\": {\n \"en\": \"United Arab Emirates\",\n \"es\": \"Emiratos Árabes Unidos\"\n },\n \"order\": 2,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"ARE\",\n \"numeric\": \"784\",\n \"phone_prefix\": \"+971\",\n \"flag\": \"🇦🇪\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"AF\",\n \"label\": {\n \"en\": \"Afghanistan\",\n \"es\": \"Afganistán\"\n },\n \"order\": 3,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"AFG\",\n \"numeric\": \"004\",\n \"phone_prefix\": \"+93\",\n \"flag\": \"🇦🇫\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"AG\",\n \"label\": {\n \"en\": \"Antigua and Barbuda\",\n \"es\": \"Antigua y Barbuda\"\n },\n \"order\": 4,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"ATG\",\n \"numeric\": \"028\",\n \"phone_prefix\": \"+1-268\",\n \"flag\": \"🇦🇬\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"AL\",\n \"label\": {\n \"en\": \"Albania\",\n \"es\": \"Albania\"\n },\n \"order\": 5,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"ALB\",\n \"numeric\": \"008\",\n \"phone_prefix\": \"+355\",\n \"flag\": \"🇦🇱\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"AM\",\n \"label\": {\n \"en\": \"Armenia\",\n \"es\": \"Armenia\"\n },\n \"order\": 6,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"ARM\",\n \"numeric\": \"051\",\n \"phone_prefix\": \"+374\",\n \"flag\": \"🇦🇲\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"AO\",\n \"label\": {\n \"en\": \"Angola\",\n \"es\": \"Angola\"\n },\n \"order\": 7,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"AGO\",\n \"numeric\": \"024\",\n \"phone_prefix\": \"+244\",\n \"flag\": \"🇦🇴\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"AR\",\n \"label\": {\n \"en\": \"Argentina\",\n \"es\": \"Argentina\"\n },\n \"order\": 8,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"ARG\",\n \"numeric\": \"032\",\n \"phone_prefix\": \"+54\",\n \"flag\": \"🇦🇷\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"AT\",\n \"label\": {\n \"en\": \"Austria\",\n \"es\": \"Austria\"\n },\n \"order\": 9,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"AUT\",\n \"numeric\": \"040\",\n \"phone_prefix\": \"+43\",\n \"flag\": \"🇦🇹\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"AU\",\n \"label\": {\n \"en\": \"Australia\",\n \"es\": \"Australia\"\n },\n \"order\": 10,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"AUS\",\n \"numeric\": \"036\",\n \"phone_prefix\": \"+61\",\n \"flag\": \"🇦🇺\",\n \"region\": \"oceania\"\n }\n },\n {\n \"code\": \"AZ\",\n \"label\": {\n \"en\": \"Azerbaijan\",\n \"es\": \"Azerbaiyán\"\n },\n \"order\": 11,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"AZE\",\n \"numeric\": \"031\",\n \"phone_prefix\": \"+994\",\n \"flag\": \"🇦🇿\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"BA\",\n \"label\": {\n \"en\": \"Bosnia and Herzegovina\",\n \"es\": \"Bosnia y Herzegovina\"\n },\n \"order\": 12,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BIH\",\n \"numeric\": \"070\",\n \"phone_prefix\": \"+387\",\n \"flag\": \"🇧🇦\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"BB\",\n \"label\": {\n \"en\": \"Barbados\",\n \"es\": \"Barbados\"\n },\n \"order\": 13,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BRB\",\n \"numeric\": \"052\",\n \"phone_prefix\": \"+1-246\",\n \"flag\": \"🇧🇧\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"BD\",\n \"label\": {\n \"en\": \"Bangladesh\",\n \"es\": \"Bangladés\"\n },\n \"order\": 14,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BGD\",\n \"numeric\": \"050\",\n \"phone_prefix\": \"+880\",\n \"flag\": \"🇧🇩\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"BE\",\n \"label\": {\n \"en\": \"Belgium\",\n \"es\": \"Bélgica\"\n },\n \"order\": 15,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BEL\",\n \"numeric\": \"056\",\n \"phone_prefix\": \"+32\",\n \"flag\": \"🇧🇪\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"BF\",\n \"label\": {\n \"en\": \"Burkina Faso\",\n \"es\": \"Burkina Faso\"\n },\n \"order\": 16,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BFA\",\n \"numeric\": \"854\",\n \"phone_prefix\": \"+226\",\n \"flag\": \"🇧🇫\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"BG\",\n \"label\": {\n \"en\": \"Bulgaria\",\n \"es\": \"Bulgaria\"\n },\n \"order\": 17,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BGR\",\n \"numeric\": \"100\",\n \"phone_prefix\": \"+359\",\n \"flag\": \"🇧🇬\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"BH\",\n \"label\": {\n \"en\": \"Bahrain\",\n \"es\": \"Baréin\"\n },\n \"order\": 18,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BHR\",\n \"numeric\": \"048\",\n \"phone_prefix\": \"+973\",\n \"flag\": \"🇧🇭\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"BI\",\n \"label\": {\n \"en\": \"Burundi\",\n \"es\": \"Burundi\"\n },\n \"order\": 19,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BDI\",\n \"numeric\": \"108\",\n \"phone_prefix\": \"+257\",\n \"flag\": \"🇧🇮\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"BJ\",\n \"label\": {\n \"en\": \"Benin\",\n \"es\": \"Benín\"\n },\n \"order\": 20,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BEN\",\n \"numeric\": \"204\",\n \"phone_prefix\": \"+229\",\n \"flag\": \"🇧🇯\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"BN\",\n \"label\": {\n \"en\": \"Brunei\",\n \"es\": \"Brunéi\"\n },\n \"order\": 21,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BRN\",\n \"numeric\": \"096\",\n \"phone_prefix\": \"+673\",\n \"flag\": \"🇧🇳\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"BO\",\n \"label\": {\n \"en\": \"Bolivia\",\n \"es\": \"Bolivia\"\n },\n \"order\": 22,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BOL\",\n \"numeric\": \"068\",\n \"phone_prefix\": \"+591\",\n \"flag\": \"🇧🇴\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"BR\",\n \"label\": {\n \"en\": \"Brazil\",\n \"es\": \"Brasil\"\n },\n \"order\": 23,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BRA\",\n \"numeric\": \"076\",\n \"phone_prefix\": \"+55\",\n \"flag\": \"🇧🇷\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"BS\",\n \"label\": {\n \"en\": \"Bahamas\",\n \"es\": \"Bahamas\"\n },\n \"order\": 24,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BHS\",\n \"numeric\": \"044\",\n \"phone_prefix\": \"+1-242\",\n \"flag\": \"🇧🇸\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"BT\",\n \"label\": {\n \"en\": \"Bhutan\",\n \"es\": \"Bután\"\n },\n \"order\": 25,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BTN\",\n \"numeric\": \"064\",\n \"phone_prefix\": \"+975\",\n \"flag\": \"🇧🇹\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"BW\",\n \"label\": {\n \"en\": \"Botswana\",\n \"es\": \"Botsuana\"\n },\n \"order\": 26,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BWA\",\n \"numeric\": \"072\",\n \"phone_prefix\": \"+267\",\n \"flag\": \"🇧🇼\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"BY\",\n \"label\": {\n \"en\": \"Belarus\",\n \"es\": \"Bielorrusia\"\n },\n \"order\": 27,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BLR\",\n \"numeric\": \"112\",\n \"phone_prefix\": \"+375\",\n \"flag\": \"🇧🇾\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"BZ\",\n \"label\": {\n \"en\": \"Belize\",\n \"es\": \"Belice\"\n },\n \"order\": 28,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"BLZ\",\n \"numeric\": \"084\",\n \"phone_prefix\": \"+501\",\n \"flag\": \"🇧🇿\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"CA\",\n \"label\": {\n \"en\": \"Canada\",\n \"es\": \"Canadá\"\n },\n \"order\": 29,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"CAN\",\n \"numeric\": \"124\",\n \"phone_prefix\": \"+1\",\n \"flag\": \"🇨🇦\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"CD\",\n \"label\": {\n \"en\": \"Democratic Republic of the Congo\",\n \"es\": \"República Democrática del Congo\"\n },\n \"order\": 30,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"COD\",\n \"numeric\": \"180\",\n \"phone_prefix\": \"+243\",\n \"flag\": \"🇨🇩\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"CF\",\n \"label\": {\n \"en\": \"Central African Republic\",\n \"es\": \"República Centroafricana\"\n },\n \"order\": 31,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"CAF\",\n \"numeric\": \"140\",\n \"phone_prefix\": \"+236\",\n \"flag\": \"🇨🇫\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"CG\",\n \"label\": {\n \"en\": \"Republic of the Congo\",\n \"es\": \"República del Congo\"\n },\n \"order\": 32,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"COG\",\n \"numeric\": \"178\",\n \"phone_prefix\": \"+242\",\n \"flag\": \"🇨🇬\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"CH\",\n \"label\": {\n \"en\": \"Switzerland\",\n \"es\": \"Suiza\"\n },\n \"order\": 33,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"CHE\",\n \"numeric\": \"756\",\n \"phone_prefix\": \"+41\",\n \"flag\": \"🇨🇭\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"CI\",\n \"label\": {\n \"en\": \"Ivory Coast\",\n \"es\": \"Costa de Marfil\"\n },\n \"order\": 34,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"CIV\",\n \"numeric\": \"384\",\n \"phone_prefix\": \"+225\",\n \"flag\": \"🇨🇮\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"CL\",\n \"label\": {\n \"en\": \"Chile\",\n \"es\": \"Chile\"\n },\n \"order\": 35,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"CHL\",\n \"numeric\": \"152\",\n \"phone_prefix\": \"+56\",\n \"flag\": \"🇨🇱\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"CM\",\n \"label\": {\n \"en\": \"Cameroon\",\n \"es\": \"Camerún\"\n },\n \"order\": 36,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"CMR\",\n \"numeric\": \"120\",\n \"phone_prefix\": \"+237\",\n \"flag\": \"🇨🇲\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"CN\",\n \"label\": {\n \"en\": \"China\",\n \"es\": \"China\"\n },\n \"order\": 37,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"CHN\",\n \"numeric\": \"156\",\n \"phone_prefix\": \"+86\",\n \"flag\": \"🇨🇳\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"CO\",\n \"label\": {\n \"en\": \"Colombia\",\n \"es\": \"Colombia\"\n },\n \"order\": 38,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"COL\",\n \"numeric\": \"170\",\n \"phone_prefix\": \"+57\",\n \"flag\": \"🇨🇴\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"CR\",\n \"label\": {\n \"en\": \"Costa Rica\",\n \"es\": \"Costa Rica\"\n },\n \"order\": 39,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"CRI\",\n \"numeric\": \"188\",\n \"phone_prefix\": \"+506\",\n \"flag\": \"🇨🇷\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"CU\",\n \"label\": {\n \"en\": \"Cuba\",\n \"es\": \"Cuba\"\n },\n \"order\": 40,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"CUB\",\n \"numeric\": \"192\",\n \"phone_prefix\": \"+53\",\n \"flag\": \"🇨🇺\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"CV\",\n \"label\": {\n \"en\": \"Cape Verde\",\n \"es\": \"Cabo Verde\"\n },\n \"order\": 41,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"CPV\",\n \"numeric\": \"132\",\n \"phone_prefix\": \"+238\",\n \"flag\": \"🇨🇻\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"CY\",\n \"label\": {\n \"en\": \"Cyprus\",\n \"es\": \"Chipre\"\n },\n \"order\": 42,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"CYP\",\n \"numeric\": \"196\",\n \"phone_prefix\": \"+357\",\n \"flag\": \"🇨🇾\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"CZ\",\n \"label\": {\n \"en\": \"Czech Republic\",\n \"es\": \"República Checa\"\n },\n \"order\": 43,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"CZE\",\n \"numeric\": \"203\",\n \"phone_prefix\": \"+420\",\n \"flag\": \"🇨🇿\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"DE\",\n \"label\": {\n \"en\": \"Germany\",\n \"es\": \"Alemania\"\n },\n \"order\": 44,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"DEU\",\n \"numeric\": \"276\",\n \"phone_prefix\": \"+49\",\n \"flag\": \"🇩🇪\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"DJ\",\n \"label\": {\n \"en\": \"Djibouti\",\n \"es\": \"Yibuti\"\n },\n \"order\": 45,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"DJI\",\n \"numeric\": \"262\",\n \"phone_prefix\": \"+253\",\n \"flag\": \"🇩🇯\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"DK\",\n \"label\": {\n \"en\": \"Denmark\",\n \"es\": \"Dinamarca\"\n },\n \"order\": 46,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"DNK\",\n \"numeric\": \"208\",\n \"phone_prefix\": \"+45\",\n \"flag\": \"🇩🇰\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"DM\",\n \"label\": {\n \"en\": \"Dominica\",\n \"es\": \"Dominica\"\n },\n \"order\": 47,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"DMA\",\n \"numeric\": \"212\",\n \"phone_prefix\": \"+1-767\",\n \"flag\": \"🇩🇲\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"DO\",\n \"label\": {\n \"en\": \"Dominican Republic\",\n \"es\": \"República Dominicana\"\n },\n \"order\": 48,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"DOM\",\n \"numeric\": \"214\",\n \"phone_prefix\": \"+1-809\",\n \"flag\": \"🇩🇴\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"DZ\",\n \"label\": {\n \"en\": \"Algeria\",\n \"es\": \"Argelia\"\n },\n \"order\": 49,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"DZA\",\n \"numeric\": \"012\",\n \"phone_prefix\": \"+213\",\n \"flag\": \"🇩🇿\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"EC\",\n \"label\": {\n \"en\": \"Ecuador\",\n \"es\": \"Ecuador\"\n },\n \"order\": 50,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"ECU\",\n \"numeric\": \"218\",\n \"phone_prefix\": \"+593\",\n \"flag\": \"🇪🇨\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"EE\",\n \"label\": {\n \"en\": \"Estonia\",\n \"es\": \"Estonia\"\n },\n \"order\": 51,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"EST\",\n \"numeric\": \"233\",\n \"phone_prefix\": \"+372\",\n \"flag\": \"🇪🇪\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"EG\",\n \"label\": {\n \"en\": \"Egypt\",\n \"es\": \"Egipto\"\n },\n \"order\": 52,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"EGY\",\n \"numeric\": \"818\",\n \"phone_prefix\": \"+20\",\n \"flag\": \"🇪🇬\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"ER\",\n \"label\": {\n \"en\": \"Eritrea\",\n \"es\": \"Eritrea\"\n },\n \"order\": 53,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"ERI\",\n \"numeric\": \"232\",\n \"phone_prefix\": \"+291\",\n \"flag\": \"🇪🇷\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"ES\",\n \"label\": {\n \"en\": \"Spain\",\n \"es\": \"España\"\n },\n \"order\": 54,\n \"is_active\": true,\n \"metadata\": {\n \"alpha3\": \"ESP\",\n \"numeric\": \"724\",\n \"phone_prefix\": \"+34\",\n \"flag\": \"🇪🇸\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"ET\",\n \"label\": {\n \"en\": \"Ethiopia\",\n \"es\": \"Etiopía\"\n },\n \"order\": 55,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"ETH\",\n \"numeric\": \"231\",\n \"phone_prefix\": \"+251\",\n \"flag\": \"🇪🇹\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"FI\",\n \"label\": {\n \"en\": \"Finland\",\n \"es\": \"Finlandia\"\n },\n \"order\": 56,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"FIN\",\n \"numeric\": \"246\",\n \"phone_prefix\": \"+358\",\n \"flag\": \"🇫🇮\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"FJ\",\n \"label\": {\n \"en\": \"Fiji\",\n \"es\": \"Fiyi\"\n },\n \"order\": 57,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"FJI\",\n \"numeric\": \"242\",\n \"phone_prefix\": \"+679\",\n \"flag\": \"🇫🇯\",\n \"region\": \"oceania\"\n }\n },\n {\n \"code\": \"FR\",\n \"label\": {\n \"en\": \"France\",\n \"es\": \"Francia\"\n },\n \"order\": 58,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"FRA\",\n \"numeric\": \"250\",\n \"phone_prefix\": \"+33\",\n \"flag\": \"🇫🇷\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"GA\",\n \"label\": {\n \"en\": \"Gabon\",\n \"es\": \"Gabón\"\n },\n \"order\": 59,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"GAB\",\n \"numeric\": \"266\",\n \"phone_prefix\": \"+241\",\n \"flag\": \"🇬🇦\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"GB\",\n \"label\": {\n \"en\": \"United Kingdom\",\n \"es\": \"Reino Unido\"\n },\n \"order\": 60,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"GBR\",\n \"numeric\": \"826\",\n \"phone_prefix\": \"+44\",\n \"flag\": \"🇬🇧\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"GD\",\n \"label\": {\n \"en\": \"Grenada\",\n \"es\": \"Granada\"\n },\n \"order\": 61,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"GRD\",\n \"numeric\": \"308\",\n \"phone_prefix\": \"+1-473\",\n \"flag\": \"🇬🇩\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"GE\",\n \"label\": {\n \"en\": \"Georgia\",\n \"es\": \"Georgia\"\n },\n \"order\": 62,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"GEO\",\n \"numeric\": \"268\",\n \"phone_prefix\": \"+995\",\n \"flag\": \"🇬🇪\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"GH\",\n \"label\": {\n \"en\": \"Ghana\",\n \"es\": \"Ghana\"\n },\n \"order\": 63,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"GHA\",\n \"numeric\": \"288\",\n \"phone_prefix\": \"+233\",\n \"flag\": \"🇬🇭\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"GM\",\n \"label\": {\n \"en\": \"Gambia\",\n \"es\": \"Gambia\"\n },\n \"order\": 64,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"GMB\",\n \"numeric\": \"270\",\n \"phone_prefix\": \"+220\",\n \"flag\": \"🇬🇲\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"GN\",\n \"label\": {\n \"en\": \"Guinea\",\n \"es\": \"Guinea\"\n },\n \"order\": 65,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"GIN\",\n \"numeric\": \"324\",\n \"phone_prefix\": \"+224\",\n \"flag\": \"🇬🇳\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"GQ\",\n \"label\": {\n \"en\": \"Equatorial Guinea\",\n \"es\": \"Guinea Ecuatorial\"\n },\n \"order\": 66,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"GNQ\",\n \"numeric\": \"226\",\n \"phone_prefix\": \"+240\",\n \"flag\": \"🇬🇶\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"GR\",\n \"label\": {\n \"en\": \"Greece\",\n \"es\": \"Grecia\"\n },\n \"order\": 67,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"GRC\",\n \"numeric\": \"300\",\n \"phone_prefix\": \"+30\",\n \"flag\": \"🇬🇷\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"GT\",\n \"label\": {\n \"en\": \"Guatemala\",\n \"es\": \"Guatemala\"\n },\n \"order\": 68,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"GTM\",\n \"numeric\": \"320\",\n \"phone_prefix\": \"+502\",\n \"flag\": \"🇬🇹\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"GW\",\n \"label\": {\n \"en\": \"Guinea-Bissau\",\n \"es\": \"Guinea-Bisáu\"\n },\n \"order\": 69,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"GNB\",\n \"numeric\": \"624\",\n \"phone_prefix\": \"+245\",\n \"flag\": \"🇬🇼\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"GY\",\n \"label\": {\n \"en\": \"Guyana\",\n \"es\": \"Guyana\"\n },\n \"order\": 70,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"GUY\",\n \"numeric\": \"328\",\n \"phone_prefix\": \"+592\",\n \"flag\": \"🇬🇾\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"HN\",\n \"label\": {\n \"en\": \"Honduras\",\n \"es\": \"Honduras\"\n },\n \"order\": 71,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"HND\",\n \"numeric\": \"340\",\n \"phone_prefix\": \"+504\",\n \"flag\": \"🇭🇳\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"HR\",\n \"label\": {\n \"en\": \"Croatia\",\n \"es\": \"Croacia\"\n },\n \"order\": 72,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"HRV\",\n \"numeric\": \"191\",\n \"phone_prefix\": \"+385\",\n \"flag\": \"🇭🇷\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"HT\",\n \"label\": {\n \"en\": \"Haiti\",\n \"es\": \"Haití\"\n },\n \"order\": 73,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"HTI\",\n \"numeric\": \"332\",\n \"phone_prefix\": \"+509\",\n \"flag\": \"🇭🇹\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"HU\",\n \"label\": {\n \"en\": \"Hungary\",\n \"es\": \"Hungría\"\n },\n \"order\": 74,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"HUN\",\n \"numeric\": \"348\",\n \"phone_prefix\": \"+36\",\n \"flag\": \"🇭🇺\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"ID\",\n \"label\": {\n \"en\": \"Indonesia\",\n \"es\": \"Indonesia\"\n },\n \"order\": 75,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"IDN\",\n \"numeric\": \"360\",\n \"phone_prefix\": \"+62\",\n \"flag\": \"🇮🇩\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"IE\",\n \"label\": {\n \"en\": \"Ireland\",\n \"es\": \"Irlanda\"\n },\n \"order\": 76,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"IRL\",\n \"numeric\": \"372\",\n \"phone_prefix\": \"+353\",\n \"flag\": \"🇮🇪\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"IL\",\n \"label\": {\n \"en\": \"Israel\",\n \"es\": \"Israel\"\n },\n \"order\": 77,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"ISR\",\n \"numeric\": \"376\",\n \"phone_prefix\": \"+972\",\n \"flag\": \"🇮🇱\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"IN\",\n \"label\": {\n \"en\": \"India\",\n \"es\": \"India\"\n },\n \"order\": 78,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"IND\",\n \"numeric\": \"356\",\n \"phone_prefix\": \"+91\",\n \"flag\": \"🇮🇳\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"IQ\",\n \"label\": {\n \"en\": \"Iraq\",\n \"es\": \"Irak\"\n },\n \"order\": 79,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"IRQ\",\n \"numeric\": \"368\",\n \"phone_prefix\": \"+964\",\n \"flag\": \"🇮🇶\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"IR\",\n \"label\": {\n \"en\": \"Iran\",\n \"es\": \"Irán\"\n },\n \"order\": 80,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"IRN\",\n \"numeric\": \"364\",\n \"phone_prefix\": \"+98\",\n \"flag\": \"🇮🇷\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"IS\",\n \"label\": {\n \"en\": \"Iceland\",\n \"es\": \"Islandia\"\n },\n \"order\": 81,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"ISL\",\n \"numeric\": \"352\",\n \"phone_prefix\": \"+354\",\n \"flag\": \"🇮🇸\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"IT\",\n \"label\": {\n \"en\": \"Italy\",\n \"es\": \"Italia\"\n },\n \"order\": 82,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"ITA\",\n \"numeric\": \"380\",\n \"phone_prefix\": \"+39\",\n \"flag\": \"🇮🇹\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"JM\",\n \"label\": {\n \"en\": \"Jamaica\",\n \"es\": \"Jamaica\"\n },\n \"order\": 83,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"JAM\",\n \"numeric\": \"388\",\n \"phone_prefix\": \"+1-876\",\n \"flag\": \"🇯🇲\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"JO\",\n \"label\": {\n \"en\": \"Jordan\",\n \"es\": \"Jordania\"\n },\n \"order\": 84,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"JOR\",\n \"numeric\": \"400\",\n \"phone_prefix\": \"+962\",\n \"flag\": \"🇯🇴\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"JP\",\n \"label\": {\n \"en\": \"Japan\",\n \"es\": \"Japón\"\n },\n \"order\": 85,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"JPN\",\n \"numeric\": \"392\",\n \"phone_prefix\": \"+81\",\n \"flag\": \"🇯🇵\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"KE\",\n \"label\": {\n \"en\": \"Kenya\",\n \"es\": \"Kenia\"\n },\n \"order\": 86,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"KEN\",\n \"numeric\": \"404\",\n \"phone_prefix\": \"+254\",\n \"flag\": \"🇰🇪\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"KG\",\n \"label\": {\n \"en\": \"Kyrgyzstan\",\n \"es\": \"Kirguistán\"\n },\n \"order\": 87,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"KGZ\",\n \"numeric\": \"417\",\n \"phone_prefix\": \"+996\",\n \"flag\": \"🇰🇬\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"KH\",\n \"label\": {\n \"en\": \"Cambodia\",\n \"es\": \"Camboya\"\n },\n \"order\": 88,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"KHM\",\n \"numeric\": \"116\",\n \"phone_prefix\": \"+855\",\n \"flag\": \"🇰🇭\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"KI\",\n \"label\": {\n \"en\": \"Kiribati\",\n \"es\": \"Kiribati\"\n },\n \"order\": 89,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"KIR\",\n \"numeric\": \"296\",\n \"phone_prefix\": \"+686\",\n \"flag\": \"🇰🇮\",\n \"region\": \"oceania\"\n }\n },\n {\n \"code\": \"KM\",\n \"label\": {\n \"en\": \"Comoros\",\n \"es\": \"Comoras\"\n },\n \"order\": 90,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"COM\",\n \"numeric\": \"174\",\n \"phone_prefix\": \"+269\",\n \"flag\": \"🇰🇲\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"KN\",\n \"label\": {\n \"en\": \"Saint Kitts and Nevis\",\n \"es\": \"San Cristóbal y Nieves\"\n },\n \"order\": 91,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"KNA\",\n \"numeric\": \"659\",\n \"phone_prefix\": \"+1-869\",\n \"flag\": \"🇰🇳\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"KP\",\n \"label\": {\n \"en\": \"North Korea\",\n \"es\": \"Corea del Norte\"\n },\n \"order\": 92,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"PRK\",\n \"numeric\": \"408\",\n \"phone_prefix\": \"+850\",\n \"flag\": \"🇰🇵\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"KR\",\n \"label\": {\n \"en\": \"South Korea\",\n \"es\": \"Corea del Sur\"\n },\n \"order\": 93,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"KOR\",\n \"numeric\": \"410\",\n \"phone_prefix\": \"+82\",\n \"flag\": \"🇰🇷\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"KW\",\n \"label\": {\n \"en\": \"Kuwait\",\n \"es\": \"Kuwait\"\n },\n \"order\": 94,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"KWT\",\n \"numeric\": \"414\",\n \"phone_prefix\": \"+965\",\n \"flag\": \"🇰🇼\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"KZ\",\n \"label\": {\n \"en\": \"Kazakhstan\",\n \"es\": \"Kazajistán\"\n },\n \"order\": 95,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"KAZ\",\n \"numeric\": \"398\",\n \"phone_prefix\": \"+7\",\n \"flag\": \"🇰🇿\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"LA\",\n \"label\": {\n \"en\": \"Laos\",\n \"es\": \"Laos\"\n },\n \"order\": 96,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"LAO\",\n \"numeric\": \"418\",\n \"phone_prefix\": \"+856\",\n \"flag\": \"🇱🇦\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"LB\",\n \"label\": {\n \"en\": \"Lebanon\",\n \"es\": \"Líbano\"\n },\n \"order\": 97,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"LBN\",\n \"numeric\": \"422\",\n \"phone_prefix\": \"+961\",\n \"flag\": \"🇱🇧\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"LC\",\n \"label\": {\n \"en\": \"Saint Lucia\",\n \"es\": \"Santa Lucía\"\n },\n \"order\": 98,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"LCA\",\n \"numeric\": \"662\",\n \"phone_prefix\": \"+1-758\",\n \"flag\": \"🇱🇨\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"LI\",\n \"label\": {\n \"en\": \"Liechtenstein\",\n \"es\": \"Liechtenstein\"\n },\n \"order\": 99,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"LIE\",\n \"numeric\": \"438\",\n \"phone_prefix\": \"+423\",\n \"flag\": \"🇱🇮\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"LK\",\n \"label\": {\n \"en\": \"Sri Lanka\",\n \"es\": \"Sri Lanka\"\n },\n \"order\": 100,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"LKA\",\n \"numeric\": \"144\",\n \"phone_prefix\": \"+94\",\n \"flag\": \"🇱🇰\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"LR\",\n \"label\": {\n \"en\": \"Liberia\",\n \"es\": \"Liberia\"\n },\n \"order\": 101,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"LBR\",\n \"numeric\": \"430\",\n \"phone_prefix\": \"+231\",\n \"flag\": \"🇱🇷\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"LS\",\n \"label\": {\n \"en\": \"Lesotho\",\n \"es\": \"Lesoto\"\n },\n \"order\": 102,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"LSO\",\n \"numeric\": \"426\",\n \"phone_prefix\": \"+266\",\n \"flag\": \"🇱🇸\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"LT\",\n \"label\": {\n \"en\": \"Lithuania\",\n \"es\": \"Lituania\"\n },\n \"order\": 103,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"LTU\",\n \"numeric\": \"440\",\n \"phone_prefix\": \"+370\",\n \"flag\": \"🇱🇹\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"LU\",\n \"label\": {\n \"en\": \"Luxembourg\",\n \"es\": \"Luxemburgo\"\n },\n \"order\": 104,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"LUX\",\n \"numeric\": \"442\",\n \"phone_prefix\": \"+352\",\n \"flag\": \"🇱🇺\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"LV\",\n \"label\": {\n \"en\": \"Latvia\",\n \"es\": \"Letonia\"\n },\n \"order\": 105,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"LVA\",\n \"numeric\": \"428\",\n \"phone_prefix\": \"+371\",\n \"flag\": \"🇱🇻\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"LY\",\n \"label\": {\n \"en\": \"Libya\",\n \"es\": \"Libia\"\n },\n \"order\": 106,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"LBY\",\n \"numeric\": \"434\",\n \"phone_prefix\": \"+218\",\n \"flag\": \"🇱🇾\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"MA\",\n \"label\": {\n \"en\": \"Morocco\",\n \"es\": \"Marruecos\"\n },\n \"order\": 107,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MAR\",\n \"numeric\": \"504\",\n \"phone_prefix\": \"+212\",\n \"flag\": \"🇲🇦\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"MC\",\n \"label\": {\n \"en\": \"Monaco\",\n \"es\": \"Mónaco\"\n },\n \"order\": 108,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MCO\",\n \"numeric\": \"492\",\n \"phone_prefix\": \"+377\",\n \"flag\": \"🇲🇨\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"MD\",\n \"label\": {\n \"en\": \"Moldova\",\n \"es\": \"Moldavia\"\n },\n \"order\": 109,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MDA\",\n \"numeric\": \"498\",\n \"phone_prefix\": \"+373\",\n \"flag\": \"🇲🇩\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"ME\",\n \"label\": {\n \"en\": \"Montenegro\",\n \"es\": \"Montenegro\"\n },\n \"order\": 110,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MNE\",\n \"numeric\": \"499\",\n \"phone_prefix\": \"+382\",\n \"flag\": \"🇲🇪\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"MG\",\n \"label\": {\n \"en\": \"Madagascar\",\n \"es\": \"Madagascar\"\n },\n \"order\": 111,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MDG\",\n \"numeric\": \"450\",\n \"phone_prefix\": \"+261\",\n \"flag\": \"🇲🇬\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"MH\",\n \"label\": {\n \"en\": \"Marshall Islands\",\n \"es\": \"Islas Marshall\"\n },\n \"order\": 112,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MHL\",\n \"numeric\": \"584\",\n \"phone_prefix\": \"+692\",\n \"flag\": \"🇲🇭\",\n \"region\": \"oceania\"\n }\n },\n {\n \"code\": \"MK\",\n \"label\": {\n \"en\": \"North Macedonia\",\n \"es\": \"Macedonia del Norte\"\n },\n \"order\": 113,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MKD\",\n \"numeric\": \"807\",\n \"phone_prefix\": \"+389\",\n \"flag\": \"🇲🇰\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"ML\",\n \"label\": {\n \"en\": \"Mali\",\n \"es\": \"Malí\"\n },\n \"order\": 114,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MLI\",\n \"numeric\": \"466\",\n \"phone_prefix\": \"+223\",\n \"flag\": \"🇲🇱\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"MM\",\n \"label\": {\n \"en\": \"Myanmar\",\n \"es\": \"Myanmar\"\n },\n \"order\": 115,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MMR\",\n \"numeric\": \"104\",\n \"phone_prefix\": \"+95\",\n \"flag\": \"🇲🇲\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"MN\",\n \"label\": {\n \"en\": \"Mongolia\",\n \"es\": \"Mongolia\"\n },\n \"order\": 116,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MNG\",\n \"numeric\": \"496\",\n \"phone_prefix\": \"+976\",\n \"flag\": \"🇲🇳\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"MR\",\n \"label\": {\n \"en\": \"Mauritania\",\n \"es\": \"Mauritania\"\n },\n \"order\": 117,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MRT\",\n \"numeric\": \"478\",\n \"phone_prefix\": \"+222\",\n \"flag\": \"🇲🇷\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"MT\",\n \"label\": {\n \"en\": \"Malta\",\n \"es\": \"Malta\"\n },\n \"order\": 118,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MLT\",\n \"numeric\": \"470\",\n \"phone_prefix\": \"+356\",\n \"flag\": \"🇲🇹\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"MU\",\n \"label\": {\n \"en\": \"Mauritius\",\n \"es\": \"Mauricio\"\n },\n \"order\": 119,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MUS\",\n \"numeric\": \"480\",\n \"phone_prefix\": \"+230\",\n \"flag\": \"🇲🇺\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"MV\",\n \"label\": {\n \"en\": \"Maldives\",\n \"es\": \"Maldivas\"\n },\n \"order\": 120,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MDV\",\n \"numeric\": \"462\",\n \"phone_prefix\": \"+960\",\n \"flag\": \"🇲🇻\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"MW\",\n \"label\": {\n \"en\": \"Malawi\",\n \"es\": \"Malaui\"\n },\n \"order\": 121,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MWI\",\n \"numeric\": \"454\",\n \"phone_prefix\": \"+265\",\n \"flag\": \"🇲🇼\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"MX\",\n \"label\": {\n \"en\": \"Mexico\",\n \"es\": \"México\"\n },\n \"order\": 122,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MEX\",\n \"numeric\": \"484\",\n \"phone_prefix\": \"+52\",\n \"flag\": \"🇲🇽\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"MY\",\n \"label\": {\n \"en\": \"Malaysia\",\n \"es\": \"Malasia\"\n },\n \"order\": 123,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MYS\",\n \"numeric\": \"458\",\n \"phone_prefix\": \"+60\",\n \"flag\": \"🇲🇾\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"MZ\",\n \"label\": {\n \"en\": \"Mozambique\",\n \"es\": \"Mozambique\"\n },\n \"order\": 124,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"MOZ\",\n \"numeric\": \"508\",\n \"phone_prefix\": \"+258\",\n \"flag\": \"🇲🇿\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"NA\",\n \"label\": {\n \"en\": \"Namibia\",\n \"es\": \"Namibia\"\n },\n \"order\": 125,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"NAM\",\n \"numeric\": \"516\",\n \"phone_prefix\": \"+264\",\n \"flag\": \"🇳🇦\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"NE\",\n \"label\": {\n \"en\": \"Niger\",\n \"es\": \"Níger\"\n },\n \"order\": 126,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"NER\",\n \"numeric\": \"562\",\n \"phone_prefix\": \"+227\",\n \"flag\": \"🇳🇪\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"NG\",\n \"label\": {\n \"en\": \"Nigeria\",\n \"es\": \"Nigeria\"\n },\n \"order\": 127,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"NGA\",\n \"numeric\": \"566\",\n \"phone_prefix\": \"+234\",\n \"flag\": \"🇳🇬\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"NI\",\n \"label\": {\n \"en\": \"Nicaragua\",\n \"es\": \"Nicaragua\"\n },\n \"order\": 128,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"NIC\",\n \"numeric\": \"558\",\n \"phone_prefix\": \"+505\",\n \"flag\": \"🇳🇮\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"NL\",\n \"label\": {\n \"en\": \"Netherlands\",\n \"es\": \"Países Bajos\"\n },\n \"order\": 129,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"NLD\",\n \"numeric\": \"528\",\n \"phone_prefix\": \"+31\",\n \"flag\": \"🇳🇱\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"NO\",\n \"label\": {\n \"en\": \"Norway\",\n \"es\": \"Noruega\"\n },\n \"order\": 130,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"NOR\",\n \"numeric\": \"578\",\n \"phone_prefix\": \"+47\",\n \"flag\": \"🇳🇴\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"NP\",\n \"label\": {\n \"en\": \"Nepal\",\n \"es\": \"Nepal\"\n },\n \"order\": 131,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"NPL\",\n \"numeric\": \"524\",\n \"phone_prefix\": \"+977\",\n \"flag\": \"🇳🇵\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"NR\",\n \"label\": {\n \"en\": \"Nauru\",\n \"es\": \"Nauru\"\n },\n \"order\": 132,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"NRU\",\n \"numeric\": \"520\",\n \"phone_prefix\": \"+674\",\n \"flag\": \"🇳🇷\",\n \"region\": \"oceania\"\n }\n },\n {\n \"code\": \"NZ\",\n \"label\": {\n \"en\": \"New Zealand\",\n \"es\": \"Nueva Zelanda\"\n },\n \"order\": 133,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"NZL\",\n \"numeric\": \"554\",\n \"phone_prefix\": \"+64\",\n \"flag\": \"🇳🇿\",\n \"region\": \"oceania\"\n }\n },\n {\n \"code\": \"OM\",\n \"label\": {\n \"en\": \"Oman\",\n \"es\": \"Omán\"\n },\n \"order\": 134,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"OMN\",\n \"numeric\": \"512\",\n \"phone_prefix\": \"+968\",\n \"flag\": \"🇴🇲\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"PA\",\n \"label\": {\n \"en\": \"Panama\",\n \"es\": \"Panamá\"\n },\n \"order\": 135,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"PAN\",\n \"numeric\": \"591\",\n \"phone_prefix\": \"+507\",\n \"flag\": \"🇵🇦\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"PE\",\n \"label\": {\n \"en\": \"Peru\",\n \"es\": \"Perú\"\n },\n \"order\": 136,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"PER\",\n \"numeric\": \"604\",\n \"phone_prefix\": \"+51\",\n \"flag\": \"🇵🇪\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"PG\",\n \"label\": {\n \"en\": \"Papua New Guinea\",\n \"es\": \"Papúa Nueva Guinea\"\n },\n \"order\": 137,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"PNG\",\n \"numeric\": \"598\",\n \"phone_prefix\": \"+675\",\n \"flag\": \"🇵🇬\",\n \"region\": \"oceania\"\n }\n },\n {\n \"code\": \"PH\",\n \"label\": {\n \"en\": \"Philippines\",\n \"es\": \"Filipinas\"\n },\n \"order\": 138,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"PHL\",\n \"numeric\": \"608\",\n \"phone_prefix\": \"+63\",\n \"flag\": \"🇵🇭\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"PK\",\n \"label\": {\n \"en\": \"Pakistan\",\n \"es\": \"Pakistán\"\n },\n \"order\": 139,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"PAK\",\n \"numeric\": \"586\",\n \"phone_prefix\": \"+92\",\n \"flag\": \"🇵🇰\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"PL\",\n \"label\": {\n \"en\": \"Poland\",\n \"es\": \"Polonia\"\n },\n \"order\": 140,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"POL\",\n \"numeric\": \"616\",\n \"phone_prefix\": \"+48\",\n \"flag\": \"🇵🇱\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"PT\",\n \"label\": {\n \"en\": \"Portugal\",\n \"es\": \"Portugal\"\n },\n \"order\": 141,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"PRT\",\n \"numeric\": \"620\",\n \"phone_prefix\": \"+351\",\n \"flag\": \"🇵🇹\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"PW\",\n \"label\": {\n \"en\": \"Palau\",\n \"es\": \"Palaos\"\n },\n \"order\": 142,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"PLW\",\n \"numeric\": \"585\",\n \"phone_prefix\": \"+680\",\n \"flag\": \"🇵🇼\",\n \"region\": \"oceania\"\n }\n },\n {\n \"code\": \"PY\",\n \"label\": {\n \"en\": \"Paraguay\",\n \"es\": \"Paraguay\"\n },\n \"order\": 143,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"PRY\",\n \"numeric\": \"600\",\n \"phone_prefix\": \"+595\",\n \"flag\": \"🇵🇾\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"QA\",\n \"label\": {\n \"en\": \"Qatar\",\n \"es\": \"Catar\"\n },\n \"order\": 144,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"QAT\",\n \"numeric\": \"634\",\n \"phone_prefix\": \"+974\",\n \"flag\": \"🇶🇦\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"RO\",\n \"label\": {\n \"en\": \"Romania\",\n \"es\": \"Rumanía\"\n },\n \"order\": 145,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"ROU\",\n \"numeric\": \"642\",\n \"phone_prefix\": \"+40\",\n \"flag\": \"🇷🇴\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"RS\",\n \"label\": {\n \"en\": \"Serbia\",\n \"es\": \"Serbia\"\n },\n \"order\": 146,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SRB\",\n \"numeric\": \"688\",\n \"phone_prefix\": \"+381\",\n \"flag\": \"🇷🇸\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"RU\",\n \"label\": {\n \"en\": \"Russia\",\n \"es\": \"Rusia\"\n },\n \"order\": 147,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"RUS\",\n \"numeric\": \"643\",\n \"phone_prefix\": \"+7\",\n \"flag\": \"🇷🇺\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"RW\",\n \"label\": {\n \"en\": \"Rwanda\",\n \"es\": \"Ruanda\"\n },\n \"order\": 148,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"RWA\",\n \"numeric\": \"646\",\n \"phone_prefix\": \"+250\",\n \"flag\": \"🇷🇼\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"SA\",\n \"label\": {\n \"en\": \"Saudi Arabia\",\n \"es\": \"Arabia Saudita\"\n },\n \"order\": 149,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SAU\",\n \"numeric\": \"682\",\n \"phone_prefix\": \"+966\",\n \"flag\": \"🇸🇦\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"SB\",\n \"label\": {\n \"en\": \"Solomon Islands\",\n \"es\": \"Islas Salomón\"\n },\n \"order\": 150,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SLB\",\n \"numeric\": \"090\",\n \"phone_prefix\": \"+677\",\n \"flag\": \"🇸🇧\",\n \"region\": \"oceania\"\n }\n },\n {\n \"code\": \"SC\",\n \"label\": {\n \"en\": \"Seychelles\",\n \"es\": \"Seychelles\"\n },\n \"order\": 151,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SYC\",\n \"numeric\": \"690\",\n \"phone_prefix\": \"+248\",\n \"flag\": \"🇸🇨\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"SD\",\n \"label\": {\n \"en\": \"Sudan\",\n \"es\": \"Sudán\"\n },\n \"order\": 152,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SDN\",\n \"numeric\": \"729\",\n \"phone_prefix\": \"+249\",\n \"flag\": \"🇸🇩\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"SE\",\n \"label\": {\n \"en\": \"Sweden\",\n \"es\": \"Suecia\"\n },\n \"order\": 153,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SWE\",\n \"numeric\": \"752\",\n \"phone_prefix\": \"+46\",\n \"flag\": \"🇸🇪\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"SG\",\n \"label\": {\n \"en\": \"Singapore\",\n \"es\": \"Singapur\"\n },\n \"order\": 154,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SGP\",\n \"numeric\": \"702\",\n \"phone_prefix\": \"+65\",\n \"flag\": \"🇸🇬\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"SI\",\n \"label\": {\n \"en\": \"Slovenia\",\n \"es\": \"Eslovenia\"\n },\n \"order\": 155,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SVN\",\n \"numeric\": \"705\",\n \"phone_prefix\": \"+386\",\n \"flag\": \"🇸🇮\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"SK\",\n \"label\": {\n \"en\": \"Slovakia\",\n \"es\": \"Eslovaquia\"\n },\n \"order\": 156,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SVK\",\n \"numeric\": \"703\",\n \"phone_prefix\": \"+421\",\n \"flag\": \"🇸🇰\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"SL\",\n \"label\": {\n \"en\": \"Sierra Leone\",\n \"es\": \"Sierra Leona\"\n },\n \"order\": 157,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SLE\",\n \"numeric\": \"694\",\n \"phone_prefix\": \"+232\",\n \"flag\": \"🇸🇱\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"SM\",\n \"label\": {\n \"en\": \"San Marino\",\n \"es\": \"San Marino\"\n },\n \"order\": 158,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SMR\",\n \"numeric\": \"674\",\n \"phone_prefix\": \"+378\",\n \"flag\": \"🇸🇲\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"SN\",\n \"label\": {\n \"en\": \"Senegal\",\n \"es\": \"Senegal\"\n },\n \"order\": 159,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SEN\",\n \"numeric\": \"686\",\n \"phone_prefix\": \"+221\",\n \"flag\": \"🇸🇳\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"SO\",\n \"label\": {\n \"en\": \"Somalia\",\n \"es\": \"Somalia\"\n },\n \"order\": 160,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SOM\",\n \"numeric\": \"706\",\n \"phone_prefix\": \"+252\",\n \"flag\": \"🇸🇴\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"SR\",\n \"label\": {\n \"en\": \"Suriname\",\n \"es\": \"Surinam\"\n },\n \"order\": 161,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SUR\",\n \"numeric\": \"740\",\n \"phone_prefix\": \"+597\",\n \"flag\": \"🇸🇷\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"SS\",\n \"label\": {\n \"en\": \"South Sudan\",\n \"es\": \"Sudán del Sur\"\n },\n \"order\": 162,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SSD\",\n \"numeric\": \"728\",\n \"phone_prefix\": \"+211\",\n \"flag\": \"🇸🇸\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"ST\",\n \"label\": {\n \"en\": \"Sao Tome and Principe\",\n \"es\": \"Santo Tomé y Príncipe\"\n },\n \"order\": 163,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"STP\",\n \"numeric\": \"678\",\n \"phone_prefix\": \"+239\",\n \"flag\": \"🇸🇹\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"SV\",\n \"label\": {\n \"en\": \"El Salvador\",\n \"es\": \"El Salvador\"\n },\n \"order\": 164,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SLV\",\n \"numeric\": \"222\",\n \"phone_prefix\": \"+503\",\n \"flag\": \"🇸🇻\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"SY\",\n \"label\": {\n \"en\": \"Syria\",\n \"es\": \"Siria\"\n },\n \"order\": 165,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SYR\",\n \"numeric\": \"760\",\n \"phone_prefix\": \"+963\",\n \"flag\": \"🇸🇾\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"SZ\",\n \"label\": {\n \"en\": \"Eswatini\",\n \"es\": \"Esuatini\"\n },\n \"order\": 166,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"SWZ\",\n \"numeric\": \"748\",\n \"phone_prefix\": \"+268\",\n \"flag\": \"🇸🇿\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"TD\",\n \"label\": {\n \"en\": \"Chad\",\n \"es\": \"Chad\"\n },\n \"order\": 167,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"TCD\",\n \"numeric\": \"148\",\n \"phone_prefix\": \"+235\",\n \"flag\": \"🇹🇩\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"TG\",\n \"label\": {\n \"en\": \"Togo\",\n \"es\": \"Togo\"\n },\n \"order\": 168,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"TGO\",\n \"numeric\": \"768\",\n \"phone_prefix\": \"+228\",\n \"flag\": \"🇹🇬\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"TH\",\n \"label\": {\n \"en\": \"Thailand\",\n \"es\": \"Tailandia\"\n },\n \"order\": 169,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"THA\",\n \"numeric\": \"764\",\n \"phone_prefix\": \"+66\",\n \"flag\": \"🇹🇭\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"TJ\",\n \"label\": {\n \"en\": \"Tajikistan\",\n \"es\": \"Tayikistán\"\n },\n \"order\": 170,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"TJK\",\n \"numeric\": \"762\",\n \"phone_prefix\": \"+992\",\n \"flag\": \"🇹🇯\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"TL\",\n \"label\": {\n \"en\": \"Timor-Leste\",\n \"es\": \"Timor Oriental\"\n },\n \"order\": 171,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"TLS\",\n \"numeric\": \"626\",\n \"phone_prefix\": \"+670\",\n \"flag\": \"🇹🇱\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"TM\",\n \"label\": {\n \"en\": \"Turkmenistan\",\n \"es\": \"Turkmenistán\"\n },\n \"order\": 172,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"TKM\",\n \"numeric\": \"795\",\n \"phone_prefix\": \"+993\",\n \"flag\": \"🇹🇲\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"TN\",\n \"label\": {\n \"en\": \"Tunisia\",\n \"es\": \"Túnez\"\n },\n \"order\": 173,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"TUN\",\n \"numeric\": \"788\",\n \"phone_prefix\": \"+216\",\n \"flag\": \"🇹🇳\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"TO\",\n \"label\": {\n \"en\": \"Tonga\",\n \"es\": \"Tonga\"\n },\n \"order\": 174,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"TON\",\n \"numeric\": \"776\",\n \"phone_prefix\": \"+676\",\n \"flag\": \"🇹🇴\",\n \"region\": \"oceania\"\n }\n },\n {\n \"code\": \"TR\",\n \"label\": {\n \"en\": \"Turkey\",\n \"es\": \"Turquía\"\n },\n \"order\": 175,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"TUR\",\n \"numeric\": \"792\",\n \"phone_prefix\": \"+90\",\n \"flag\": \"🇹🇷\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"TT\",\n \"label\": {\n \"en\": \"Trinidad and Tobago\",\n \"es\": \"Trinidad y Tobago\"\n },\n \"order\": 176,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"TTO\",\n \"numeric\": \"780\",\n \"phone_prefix\": \"+1-868\",\n \"flag\": \"🇹🇹\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"TV\",\n \"label\": {\n \"en\": \"Tuvalu\",\n \"es\": \"Tuvalu\"\n },\n \"order\": 177,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"TUV\",\n \"numeric\": \"798\",\n \"phone_prefix\": \"+688\",\n \"flag\": \"🇹🇻\",\n \"region\": \"oceania\"\n }\n },\n {\n \"code\": \"TZ\",\n \"label\": {\n \"en\": \"Tanzania\",\n \"es\": \"Tanzania\"\n },\n \"order\": 178,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"TZA\",\n \"numeric\": \"834\",\n \"phone_prefix\": \"+255\",\n \"flag\": \"🇹🇿\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"UA\",\n \"label\": {\n \"en\": \"Ukraine\",\n \"es\": \"Ucrania\"\n },\n \"order\": 179,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"UKR\",\n \"numeric\": \"804\",\n \"phone_prefix\": \"+380\",\n \"flag\": \"🇺🇦\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"UG\",\n \"label\": {\n \"en\": \"Uganda\",\n \"es\": \"Uganda\"\n },\n \"order\": 180,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"UGA\",\n \"numeric\": \"800\",\n \"phone_prefix\": \"+256\",\n \"flag\": \"🇺🇬\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"US\",\n \"label\": {\n \"en\": \"United States\",\n \"es\": \"Estados Unidos\"\n },\n \"order\": 181,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"USA\",\n \"numeric\": \"840\",\n \"phone_prefix\": \"+1\",\n \"flag\": \"🇺🇸\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"UY\",\n \"label\": {\n \"en\": \"Uruguay\",\n \"es\": \"Uruguay\"\n },\n \"order\": 182,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"URY\",\n \"numeric\": \"858\",\n \"phone_prefix\": \"+598\",\n \"flag\": \"🇺🇾\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"UZ\",\n \"label\": {\n \"en\": \"Uzbekistan\",\n \"es\": \"Uzbekistán\"\n },\n \"order\": 183,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"UZB\",\n \"numeric\": \"860\",\n \"phone_prefix\": \"+998\",\n \"flag\": \"🇺🇿\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"VA\",\n \"label\": {\n \"en\": \"Vatican City\",\n \"es\": \"Ciudad del Vaticano\"\n },\n \"order\": 184,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"VAT\",\n \"numeric\": \"336\",\n \"phone_prefix\": \"+379\",\n \"flag\": \"🇻🇦\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"VC\",\n \"label\": {\n \"en\": \"Saint Vincent and the Grenadines\",\n \"es\": \"San Vicente y las Granadinas\"\n },\n \"order\": 185,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"VCT\",\n \"numeric\": \"670\",\n \"phone_prefix\": \"+1-784\",\n \"flag\": \"🇻🇨\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"VE\",\n \"label\": {\n \"en\": \"Venezuela\",\n \"es\": \"Venezuela\"\n },\n \"order\": 186,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"VEN\",\n \"numeric\": \"862\",\n \"phone_prefix\": \"+58\",\n \"flag\": \"🇻🇪\",\n \"region\": \"americas\"\n }\n },\n {\n \"code\": \"VN\",\n \"label\": {\n \"en\": \"Vietnam\",\n \"es\": \"Vietnam\"\n },\n \"order\": 187,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"VNM\",\n \"numeric\": \"704\",\n \"phone_prefix\": \"+84\",\n \"flag\": \"🇻🇳\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"VU\",\n \"label\": {\n \"en\": \"Vanuatu\",\n \"es\": \"Vanuatu\"\n },\n \"order\": 188,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"VUT\",\n \"numeric\": \"548\",\n \"phone_prefix\": \"+678\",\n \"flag\": \"🇻🇺\",\n \"region\": \"oceania\"\n }\n },\n {\n \"code\": \"WS\",\n \"label\": {\n \"en\": \"Samoa\",\n \"es\": \"Samoa\"\n },\n \"order\": 189,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"WSM\",\n \"numeric\": \"882\",\n \"phone_prefix\": \"+685\",\n \"flag\": \"🇼🇸\",\n \"region\": \"oceania\"\n }\n },\n {\n \"code\": \"XK\",\n \"label\": {\n \"en\": \"Kosovo\",\n \"es\": \"Kosovo\"\n },\n \"order\": 190,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"XKX\",\n \"numeric\": \"383\",\n \"phone_prefix\": \"+383\",\n \"flag\": \"🇽🇰\",\n \"region\": \"europe\"\n }\n },\n {\n \"code\": \"YE\",\n \"label\": {\n \"en\": \"Yemen\",\n \"es\": \"Yemen\"\n },\n \"order\": 191,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"YEM\",\n \"numeric\": \"887\",\n \"phone_prefix\": \"+967\",\n \"flag\": \"🇾🇪\",\n \"region\": \"asia\"\n }\n },\n {\n \"code\": \"ZA\",\n \"label\": {\n \"en\": \"South Africa\",\n \"es\": \"Sudáfrica\"\n },\n \"order\": 192,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"ZAF\",\n \"numeric\": \"710\",\n \"phone_prefix\": \"+27\",\n \"flag\": \"🇿🇦\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"ZM\",\n \"label\": {\n \"en\": \"Zambia\",\n \"es\": \"Zambia\"\n },\n \"order\": 193,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"ZMB\",\n \"numeric\": \"894\",\n \"phone_prefix\": \"+260\",\n \"flag\": \"🇿🇲\",\n \"region\": \"africa\"\n }\n },\n {\n \"code\": \"ZW\",\n \"label\": {\n \"en\": \"Zimbabwe\",\n \"es\": \"Zimbabue\"\n },\n \"order\": 194,\n \"is_active\": false,\n \"metadata\": {\n \"alpha3\": \"ZWE\",\n \"numeric\": \"716\",\n \"phone_prefix\": \"+263\",\n \"flag\": \"🇿🇼\",\n \"region\": \"africa\"\n }\n }\n]\n","[\n {\n \"code\": \"ELECTRONICS\",\n \"label\": {\n \"en\": \"Electronics\",\n \"es\": \"Electronica\"\n },\n \"order\": 1,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:laptop\"\n }\n },\n {\n \"code\": \"CLOTHING\",\n \"label\": {\n \"en\": \"Clothing & Fashion\",\n \"es\": \"Ropa y Moda\"\n },\n \"order\": 2,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:tshirt-crew\"\n }\n },\n {\n \"code\": \"HOME\",\n \"label\": {\n \"en\": \"Home & Garden\",\n \"es\": \"Hogar y Jardin\"\n },\n \"order\": 3,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:home\"\n }\n },\n {\n \"code\": \"FOOD\",\n \"label\": {\n \"en\": \"Food & Beverages\",\n \"es\": \"Alimentacion y Bebidas\"\n },\n \"order\": 4,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:food\"\n }\n },\n {\n \"code\": \"HEALTH\",\n \"label\": {\n \"en\": \"Health & Beauty\",\n \"es\": \"Salud y Belleza\"\n },\n \"order\": 5,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:heart-pulse\"\n }\n },\n {\n \"code\": \"SPORTS\",\n \"label\": {\n \"en\": \"Sports & Outdoors\",\n \"es\": \"Deportes y Aire libre\"\n },\n \"order\": 6,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:soccer\"\n }\n },\n {\n \"code\": \"AUTOMOTIVE\",\n \"label\": {\n \"en\": \"Automotive\",\n \"es\": \"Automocion\"\n },\n \"order\": 7,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:car\"\n }\n },\n {\n \"code\": \"TOYS\",\n \"label\": {\n \"en\": \"Toys & Games\",\n \"es\": \"Juguetes y Juegos\"\n },\n \"order\": 8,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:gamepad-variant\"\n }\n },\n {\n \"code\": \"BOOKS\",\n \"label\": {\n \"en\": \"Books & Media\",\n \"es\": \"Libros y Medios\"\n },\n \"order\": 9,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:book\"\n }\n },\n {\n \"code\": \"OFFICE\",\n \"label\": {\n \"en\": \"Office & Supplies\",\n \"es\": \"Oficina y Papeleria\"\n },\n \"order\": 10,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:briefcase\"\n }\n },\n {\n \"code\": \"PETS\",\n \"label\": {\n \"en\": \"Pets\",\n \"es\": \"Mascotas\"\n },\n \"order\": 11,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:dog\"\n }\n },\n {\n \"code\": \"BABY\",\n \"label\": {\n \"en\": \"Baby & Kids\",\n \"es\": \"Bebes y Ninos\"\n },\n \"order\": 12,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:baby-carriage\"\n }\n },\n {\n \"code\": \"TOOLS\",\n \"label\": {\n \"en\": \"Tools & Hardware\",\n \"es\": \"Herramientas y Ferreteria\"\n },\n \"order\": 13,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:hammer\"\n }\n },\n {\n \"code\": \"SERVICES\",\n \"label\": {\n \"en\": \"Services\",\n \"es\": \"Servicios\"\n },\n \"order\": 14,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:hand-heart\"\n }\n },\n {\n \"code\": \"OTHER\",\n \"label\": {\n \"en\": \"Other\",\n \"es\": \"Otros\"\n },\n \"order\": 99,\n \"is_active\": true,\n \"metadata\": {\n \"icon\": \"mdi:dots-horizontal\"\n }\n }\n]\n","[\n {\n \"code\": \"+34\",\n \"label\": {\n \"en\": \"+34 (Spain)\"\n },\n \"order\": 1,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XXX XXX XXX\"\n }\n },\n {\n \"code\": \"+33\",\n \"label\": {\n \"en\": \"+33 (France)\"\n },\n \"order\": 2,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX X XX XX XX XX\"\n }\n },\n {\n \"code\": \"+49\",\n \"label\": {\n \"en\": \"+49 (Germany)\"\n },\n \"order\": 3,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XXX XXXXXXX\"\n }\n },\n {\n \"code\": \"+39\",\n \"label\": {\n \"en\": \"+39 (Italy)\"\n },\n \"order\": 4,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XXX XXX XXXX\"\n }\n },\n {\n \"code\": \"+351\",\n \"label\": {\n \"en\": \"+351 (Portugal)\"\n },\n \"order\": 5,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XXX XXX XXX XXX\"\n }\n },\n {\n \"code\": \"+44\",\n \"label\": {\n \"en\": \"+44 (UK)\"\n },\n \"order\": 6,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XXXX XXXXXX\"\n }\n },\n {\n \"code\": \"+353\",\n \"label\": {\n \"en\": \"+353 (Ireland)\"\n },\n \"order\": 7,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XXX XX XXX XXXX\"\n }\n },\n {\n \"code\": \"+31\",\n \"label\": {\n \"en\": \"+31 (Netherlands)\"\n },\n \"order\": 8,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX X XXXX XXXX\"\n }\n },\n {\n \"code\": \"+32\",\n \"label\": {\n \"en\": \"+32 (Belgium)\"\n },\n \"order\": 9,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX X XXX XX XX\"\n }\n },\n {\n \"code\": \"+41\",\n \"label\": {\n \"en\": \"+41 (Switzerland)\"\n },\n \"order\": 10,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XX XXX XX XX\"\n }\n },\n {\n \"code\": \"+43\",\n \"label\": {\n \"en\": \"+43 (Austria)\"\n },\n \"order\": 11,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX X XXXX XXXX\"\n }\n },\n {\n \"code\": \"+46\",\n \"label\": {\n \"en\": \"+46 (Sweden)\"\n },\n \"order\": 12,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XX XXX XX XX\"\n }\n },\n {\n \"code\": \"+47\",\n \"label\": {\n \"en\": \"+47 (Norway)\"\n },\n \"order\": 13,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XXX XX XXX\"\n }\n },\n {\n \"code\": \"+45\",\n \"label\": {\n \"en\": \"+45 (Denmark)\"\n },\n \"order\": 14,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XX XX XX XX\"\n }\n },\n {\n \"code\": \"+358\",\n \"label\": {\n \"en\": \"+358 (Finland)\"\n },\n \"order\": 15,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XXX XX XXX XXXX\"\n }\n },\n {\n \"code\": \"+48\",\n \"label\": {\n \"en\": \"+48 (Poland)\"\n },\n \"order\": 16,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XXX XXX XXX\"\n }\n },\n {\n \"code\": \"+1\",\n \"label\": {\n \"en\": \"+1 (USA/Canada)\"\n },\n \"order\": 17,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+X (XXX) XXX-XXXX\"\n }\n },\n {\n \"code\": \"+52\",\n \"label\": {\n \"en\": \"+52 (Mexico)\"\n },\n \"order\": 18,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XX XXXX XXXX\"\n }\n },\n {\n \"code\": \"+54\",\n \"label\": {\n \"en\": \"+54 (Argentina)\"\n },\n \"order\": 19,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XX XXXX XXXX\"\n }\n },\n {\n \"code\": \"+55\",\n \"label\": {\n \"en\": \"+55 (Brazil)\"\n },\n \"order\": 20,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XX XXXXX XXXX\"\n }\n },\n {\n \"code\": \"+57\",\n \"label\": {\n \"en\": \"+57 (Colombia)\"\n },\n \"order\": 21,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XXX XXX XXXX\"\n }\n },\n {\n \"code\": \"+56\",\n \"label\": {\n \"en\": \"+56 (Chile)\"\n },\n \"order\": 22,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX X XXXX XXXX\"\n }\n },\n {\n \"code\": \"+51\",\n \"label\": {\n \"en\": \"+51 (Peru)\"\n },\n \"order\": 23,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XXX XXX XXX\"\n }\n },\n {\n \"code\": \"+86\",\n \"label\": {\n \"en\": \"+86 (China)\"\n },\n \"order\": 24,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XXX XXXX XXXX\"\n }\n },\n {\n \"code\": \"+81\",\n \"label\": {\n \"en\": \"+81 (Japan)\"\n },\n \"order\": 25,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XX XXXX XXXX\"\n }\n },\n {\n \"code\": \"+82\",\n \"label\": {\n \"en\": \"+82 (South Korea)\"\n },\n \"order\": 26,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XX XXXX XXXX\"\n }\n },\n {\n \"code\": \"+91\",\n \"label\": {\n \"en\": \"+91 (India)\"\n },\n \"order\": 27,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XXXXX XXXXX\"\n }\n },\n {\n \"code\": \"+65\",\n \"label\": {\n \"en\": \"+65 (Singapore)\"\n },\n \"order\": 28,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XXXX XXXX\"\n }\n },\n {\n \"code\": \"+971\",\n \"label\": {\n \"en\": \"+971 (UAE)\"\n },\n \"order\": 29,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XXX XX XXX XXXX\"\n }\n },\n {\n \"code\": \"+966\",\n \"label\": {\n \"en\": \"+966 (Saudi Arabia)\"\n },\n \"order\": 30,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XXX XX XXX XXXX\"\n }\n },\n {\n \"code\": \"+972\",\n \"label\": {\n \"en\": \"+972 (Israel)\"\n },\n \"order\": 31,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XXX XX XXX XXXX\"\n }\n },\n {\n \"code\": \"+90\",\n \"label\": {\n \"en\": \"+90 (Turkey)\"\n },\n \"order\": 32,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XXX XXX XXXX\"\n }\n },\n {\n \"code\": \"+61\",\n \"label\": {\n \"en\": \"+61 (Australia)\"\n },\n \"order\": 33,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX X XXXX XXXX\"\n }\n },\n {\n \"code\": \"+64\",\n \"label\": {\n \"en\": \"+64 (New Zealand)\"\n },\n \"order\": 34,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XX XXX XXXX\"\n }\n },\n {\n \"code\": \"+27\",\n \"label\": {\n \"en\": \"+27 (South Africa)\"\n },\n \"order\": 35,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XX XXX XXXX\"\n }\n },\n {\n \"code\": \"+20\",\n \"label\": {\n \"en\": \"+20 (Egypt)\"\n },\n \"order\": 36,\n \"is_active\": true,\n \"metadata\": {\n \"format\": \"+XX XX XXX XXXX\"\n }\n }\n]\n","[\n {\n \"code\": \"DNI\",\n \"label\": {\n \"en\": \"National ID\",\n \"es\": \"DNI\"\n },\n \"order\": 1,\n \"is_active\": true,\n \"metadata\": {\n \"pattern\": \"^[0-9]{8}[A-Z]$\"\n }\n },\n {\n \"code\": \"NIE\",\n \"label\": {\n \"en\": \"Foreigner ID\",\n \"es\": \"NIE\"\n },\n \"order\": 2,\n \"is_active\": true,\n \"metadata\": {\n \"pattern\": \"^[XYZ][0-9]{7}[A-Z]$\"\n }\n },\n {\n \"code\": \"PASSPORT\",\n \"label\": {\n \"en\": \"Passport\",\n \"es\": \"Pasaporte\"\n },\n \"order\": 3,\n \"is_active\": true\n },\n {\n \"code\": \"DRIVERS_LICENSE\",\n \"label\": {\n \"en\": \"Driver's License\",\n \"es\": \"Permiso de conducir\"\n },\n \"order\": 4,\n \"is_active\": true\n },\n {\n \"code\": \"CIF\",\n \"label\": {\n \"en\": \"Company Tax ID\",\n \"es\": \"CIF\"\n },\n \"order\": 5,\n \"is_active\": true,\n \"metadata\": {\n \"pattern\": \"^[A-Z][0-9]{8}$\"\n }\n },\n {\n \"code\": \"VAT\",\n \"label\": {\n \"en\": \"VAT Number\",\n \"es\": \"NIF-IVA\"\n },\n \"order\": 6,\n \"is_active\": true\n },\n {\n \"code\": \"SSN\",\n \"label\": {\n \"en\": \"Social Security Number\",\n \"es\": \"Número de Seguridad Social\"\n },\n \"order\": 7,\n \"is_active\": true\n },\n {\n \"code\": \"TIN\",\n \"label\": {\n \"en\": \"Tax Identification Number\",\n \"es\": \"Número de Identificación Fiscal\"\n },\n \"order\": 8,\n \"is_active\": true\n }\n]\n","/**\n * Masters module constants — separated to avoid circular imports with actions.\n */\nimport type { MasterEntry } from './registry.js'\n\n/** Minimal master types seeded on fresh install */\nexport const DEFAULT_MASTER_TYPES = ['languages', 'timezones'] as const\n\n/** Predefined master data loaders (lazy-loaded on seed) */\nexport const PREDEFINED_MASTERS: Record<string, () => Promise<MasterEntry[]>> = {\n currencies: () => import('./data/currencies.json', { with: { type: 'json' } }).then(m => m.default),\n languages: () => import('./data/languages.json', { with: { type: 'json' } }).then(m => m.default),\n timezones: () => import('./data/timezones.json', { with: { type: 'json' } }).then(m => m.default),\n 'social-networks': () => import('./data/social-networks.json', { with: { type: 'json' } }).then(m => m.default),\n genders: () => import('./data/genders.json', { with: { type: 'json' } }).then(m => m.default),\n 'marital-statuses': () => import('./data/marital-statuses.json', { with: { type: 'json' } }).then(m => m.default),\n 'education-levels': () => import('./data/education-levels.json', { with: { type: 'json' } }).then(m => m.default),\n industries: () => import('./data/industries.json', { with: { type: 'json' } }).then(m => m.default),\n 'company-types': () => import('./data/company-types.json', { with: { type: 'json' } }).then(m => m.default),\n units: () => import('./data/units.json', { with: { type: 'json' } }).then(m => m.default),\n countries: () => import('./data/countries.json', { with: { type: 'json' } }).then(m => m.default),\n 'product-categories': () => import('./data/product-categories.json', { with: { type: 'json' } }).then(m => m.default),\n 'phone-prefixes': () => import('./data/phone-prefixes.json', { with: { type: 'json' } }).then(m => m.default),\n 'document-types': () => import('./data/document-types.json', { with: { type: 'json' } }).then(m => m.default),\n}\n","import type { ActionDefinition } from '@gzl10/nexus-sdk'\nimport { useSelectField } from '@gzl10/nexus-sdk/fields'\nimport { PREDEFINED_MASTERS, DEFAULT_MASTER_TYPES } from '../constants.js'\n\n/**\n * Install a predefined master type — loads data from the framework catalog.\n * Only available in development (handler throws ForbiddenError in production).\n */\nexport const installTypeAction: ActionDefinition = {\n key: 'install-type',\n scope: 'module',\n label: { en: 'Install Master Type', es: 'Instalar Tipo de Maestro' },\n icon: 'mdi:database-plus',\n variant: 'primary',\n output: {},\n input: {\n type: useSelectField({\n label: { en: 'Master Type', es: 'Tipo de Maestro' },\n required: true,\n options: Object.keys(PREDEFINED_MASTERS)\n .filter(t => !DEFAULT_MASTER_TYPES.includes(t as typeof DEFAULT_MASTER_TYPES[number]))\n .map(t => ({\n value: t,\n label: t.replace(/[-_]/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase())\n })),\n }),\n },\n\n handler: async (ctx, input) => {\n if (process.env['NODE_ENV'] === 'production') {\n throw new ctx.core.errors.ForbiddenError('Master type management is only available in development')\n }\n\n const { type } = input as { type: string }\n const loader = PREDEFINED_MASTERS[type]\n if (!loader) {\n throw new ctx.core.errors.AppError(`Unknown predefined master type: ${type}`, 400)\n }\n\n // Check if already installed\n const existing = await ctx.db.knex('masters').where({ type }).first()\n if (existing) {\n throw new ctx.core.errors.ConflictError(`Master type \"${type}\" is already installed`)\n }\n\n const entries = await loader()\n const rows = entries.map((entry, i) => ({\n id: `${type}:${entry.code}`,\n type,\n code: entry.code,\n label: JSON.stringify(typeof entry.label === 'string' ? { en: entry.label } : entry.label),\n order: entry.order ?? i,\n is_active: entry.is_active ?? true,\n metadata: entry.metadata ? JSON.stringify(entry.metadata) : null,\n }))\n\n await ctx.db.knex('masters')\n .insert(rows)\n .onConflict(['type', 'code'])\n .ignore()\n\n return { installed: type, count: rows.length }\n },\n}\n","import type { ActionDefinition } from '@gzl10/nexus-sdk'\nimport { useTextField } from '@gzl10/nexus-sdk/fields'\nimport { DEFAULT_MASTER_TYPES } from '../constants.js'\n\n/**\n * Uninstall a master type — removes all records of that type.\n * Cannot uninstall default types (languages, timezones).\n * Only available in development (handler throws ForbiddenError in production).\n */\nexport const uninstallTypeAction: ActionDefinition = {\n key: 'uninstall-type',\n scope: 'module',\n label: { en: 'Uninstall Master Type', es: 'Desinstalar Tipo de Maestro' },\n icon: 'mdi:database-minus',\n variant: 'danger',\n output: {},\n\n confirm: {\n type: 'simple',\n title: { en: 'Uninstall Master Type', es: 'Desinstalar Tipo de Maestro' },\n message: {\n en: 'This will delete ALL records of the selected type. This action cannot be undone.',\n es: 'Esto eliminará TODOS los registros del tipo seleccionado. Esta acción no se puede deshacer.'\n },\n },\n\n input: {\n type: useTextField({\n label: { en: 'Type slug to uninstall', es: 'Slug del tipo a desinstalar' },\n required: true,\n hint: {\n en: 'Enter the master type slug (e.g., \"currencies\", \"countries\")',\n es: 'Introduce el slug del tipo (ej: \"currencies\", \"countries\")'\n },\n }),\n },\n\n handler: async (ctx, input) => {\n if (process.env['NODE_ENV'] === 'production') {\n throw new ctx.core.errors.ForbiddenError('Master type management is only available in development')\n }\n\n const { type } = input as { type: string }\n\n // Prevent uninstalling default types\n if (DEFAULT_MASTER_TYPES.includes(type as typeof DEFAULT_MASTER_TYPES[number])) {\n throw new ctx.core.errors.AppError(`Cannot uninstall default master type \"${type}\" (required by core)`, 400)\n }\n\n // Check if type exists\n const existing = await ctx.db.knex('masters').where({ type }).first()\n if (!existing) {\n throw new ctx.core.errors.NotFoundError(`Master type \"${type}\" is not installed`)\n }\n\n const deleted = await ctx.db.knex('masters').where({ type }).del()\n return { uninstalled: type, count: deleted }\n },\n}\n","/**\n * Masters Module — Core reference data catalogs.\n *\n * Single table `masters` with `type` discriminator.\n * Plugins register their own master types via the MasterRegistry service.\n * Predefined masters (countries, currencies, etc.) are seeded based on config.\n */\n\nimport type { ModuleManifest } from '@gzl10/nexus-sdk'\nimport { mastersEntity } from './definitions.js'\nimport { createMasterRegistry } from './registry.js'\nimport { DEFAULT_MASTER_TYPES, PREDEFINED_MASTERS } from './constants.js'\nimport { installTypeAction } from './actions/install-type.js'\nimport { uninstallTypeAction } from './actions/uninstall-type.js'\n\nexport { mastersEntity } from './definitions.js'\nexport { createMasterRegistry } from './registry.js'\nexport { DEFAULT_MASTER_TYPES, PREDEFINED_MASTERS } from './constants.js'\nexport type { MasterEntry, MasterRegistry } from './registry.js'\n\nexport const mastersModule: ModuleManifest = {\n name: 'masters',\n type: 'core',\n label: { en: 'Masters', es: 'Maestros' },\n icon: 'mdi:database-outline',\n description: { en: 'Reference data catalogs', es: 'Catálogos de datos de referencia' },\n category: 'settings',\n routePrefix: '/masters',\n definitions: [mastersEntity],\n actions: [installTypeAction, uninstallTypeAction],\n\n init: (ctx) => {\n if (!ctx.services.has('masters')) {\n ctx.services.register('masters', createMasterRegistry())\n }\n },\n\n seed: async (ctx) => {\n // Seed runs before init (migrations phase), ensure registry exists\n if (!ctx.services.has('masters')) {\n ctx.services.register('masters', createMasterRegistry())\n }\n const registry = ctx.services.get('masters') as ReturnType<typeof createMasterRegistry>\n\n // Only seed defaults if no data exists (fresh install or empty DB).\n // User data from data/seeds/masters.json is already imported by startup.\n const existing = await ctx.db.knex('masters').first()\n if (!existing) {\n // Fresh install: only seed minimal defaults (languages + timezones)\n for (const type of DEFAULT_MASTER_TYPES) {\n const loader = PREDEFINED_MASTERS[type]\n if (!loader) continue\n const entries = await loader()\n registry.register(type, entries, { seed: 'if-empty' })\n }\n }\n\n // Seed plugin-registered masters (always)\n await registry.seed(ctx)\n }\n}\n","/**\n * System Module Helpers\n *\n * Shared transformation functions for converting internal types to DTOs.\n * Used by both compute() functions and the controller.\n */\n\nimport { join } from 'node:path'\nimport { existsSync } from 'node:fs'\nimport type {\n Request,\n ModuleContext,\n ModuleManifest,\n PluginManifest,\n InputType,\n FieldDefinitionDTO,\n FieldCondition,\n ActionDefinitionDTO,\n ActionDefinition,\n ModuleDTO,\n PluginDTO,\n EntityDefinitionDTO,\n LocalizedString\n} from '@gzl10/nexus-sdk'\n\n/**\n * Check if a module has a seed file\n */\nexport function moduleHasSeedWithPath(mod: ModuleManifest, libPath: string): boolean {\n if (mod.seed) return true\n const seedPath = join(libPath, 'dist', 'modules', mod.name, `${mod.name}.seed.js`)\n return existsSync(seedPath)\n}\n\n/**\n * Checks if value is a FieldCondition object (serializable condition)\n * Uses same operators as FilterOperators: $eq, $ne, $in, $nin, $isnull\n */\nfunction isFieldCondition(value: unknown): value is FieldCondition {\n if (typeof value !== 'object' || value === null) return false\n const obj = value as Record<string, unknown>\n return typeof obj['field'] === 'string' && (\n '$eq' in obj ||\n '$ne' in obj ||\n '$in' in obj ||\n '$nin' in obj ||\n '$isnull' in obj\n )\n}\n\n/**\n * Evaluates a ConditionalBoolean to a serializable value.\n * - FieldCondition objects are passed through (serializable)\n * - Functions are evaluated with empty data to get default value\n * - Booleans are passed through\n */\nexport function evalConditionalBoolean(value: unknown): boolean | FieldCondition | undefined {\n if (value === undefined) return undefined\n // Pass through FieldCondition objects for frontend evaluation\n if (isFieldCondition(value)) return value\n // Evaluate functions with empty data\n if (typeof value === 'function') return value({}) as boolean\n return value as boolean\n}\n\n/**\n * Converts FieldDefinition to a serializable DTO (no functions)\n * Filters out sensitive database schema information for security\n */\nexport function toFieldDTO(name: string, field: Record<string, unknown>): FieldDefinitionDTO {\n const relation = field['relation'] as Record<string, unknown> | undefined\n const validation = field['validation'] as Record<string, unknown> | undefined\n const options = field['options'] as Record<string, unknown> | undefined\n const storage = field['storage'] as Record<string, unknown> | undefined\n const meta = field['meta'] as Record<string, unknown> | undefined\n const inputProps = field['inputProps'] as Record<string, unknown> | undefined\n const displayProps = field['displayProps'] as Record<string, unknown> | undefined\n\n return {\n name,\n label: field['label'] as string,\n input: field['input'] as InputType,\n placeholder: field['placeholder'] as string | undefined,\n hint: field['hint'] as LocalizedString | undefined,\n hidden: evalConditionalBoolean(field['hidden']),\n disabled: evalConditionalBoolean(field['disabled']),\n required: evalConditionalBoolean(field['required']),\n // db: FILTERED - sensitive database schema information\n defaultValue: field['defaultValue'] ?? (field['db'] as Record<string, unknown> | undefined)?.['default'],\n relation: relation ? {\n labelField: relation['labelField'] as string | undefined\n } : undefined,\n validation: validation ? {\n min: validation['min'] as number | undefined,\n max: validation['max'] as number | undefined,\n pattern: validation['pattern'] as string | undefined,\n format: validation['format'] as string | undefined,\n enum: validation['enum'] as string[] | undefined\n } : undefined,\n options: options\n ? Array.isArray(options)\n // Direct array format from useSelectField: [{ value, label }]\n ? { static: options as unknown as Array<{ value: string; label: string }> }\n // Object format: { endpoint, valueField, labelField, static }\n : {\n endpoint: options['endpoint'] as string | undefined,\n valueField: options['valueField'] as string | undefined,\n labelField: options['labelField'] as string | undefined,\n static: options['static'] as Array<{ value: string; label: string }> | undefined\n }\n : undefined,\n storage: storage ? {\n accept: storage['accept'] as string | undefined,\n maxSize: storage['maxSize'] as number | undefined,\n maxFiles: storage['maxFiles'] as number | undefined,\n // folder: FILTERED - sensitive server path information\n thumbnails: storage['thumbnails'] as Array<{ width: number; height: number }> | undefined,\n dedupe: storage['dedupe'] as boolean | undefined,\n isPublic: storage['isPublic'] as boolean | undefined\n } : undefined,\n meta: meta ? {\n sortable: meta['sortable'] as boolean | undefined,\n searchable: meta['searchable'] as boolean | undefined,\n exportable: meta['exportable'] as boolean | undefined,\n showInDisplay: meta['showInDisplay'] as boolean | undefined,\n showInForm: meta['showInForm'] as boolean | 'create' | 'edit' | undefined\n } : undefined,\n inputProps,\n displayProps\n }\n}\n\n/**\n * Converts ActionDefinition to a serializable DTO (no functions).\n * When req/ctx are provided, evaluates disabled() with request context.\n * Without req/ctx, disabled is left undefined (client assumes enabled).\n */\nexport function toActionDTO(action: ActionDefinition, req?: Request, ctx?: ModuleContext): ActionDefinitionDTO {\n const inputFields = action.input ?? {}\n const input: Record<string, FieldDefinitionDTO> = {}\n for (const [name, field] of Object.entries(inputFields)) {\n input[name] = toFieldDTO(name, field as unknown as Record<string, unknown>)\n }\n\n const outputFields = action.output ?? {}\n const output: Record<string, FieldDefinitionDTO> = {}\n for (const [name, field] of Object.entries(outputFields)) {\n output[name] = toFieldDTO(name, field as unknown as Record<string, unknown>)\n }\n\n // Evaluate disabled: function needs req/ctx, FieldCondition passed through as-is\n let isDisabled: boolean | FieldCondition | undefined\n if (typeof action.disabled === 'function') {\n isDisabled = req && ctx ? action.disabled(ctx, req) : undefined\n } else {\n isDisabled = action.disabled\n }\n\n return {\n key: action.key,\n label: action.label,\n icon: action.icon,\n scope: action.scope,\n group: action.group,\n disabled: isDisabled || undefined,\n disabledReason: isDisabled ? action.disabledReason : undefined,\n variant: action.variant,\n method: action.method,\n input: Object.keys(input).length > 0 ? input : undefined,\n order: action.order,\n batch: action.batch ? true : undefined,\n timeout: action.timeout,\n output: Object.keys(output).length > 0 ? output : (action.output ? {} : undefined),\n outputPage: action.outputPage,\n confirm: action.confirm,\n postAction: action.postAction\n }\n}\n\n/**\n * Infer route prefix for entity DTO.\n * Mirrors inferEntityRoutePath() from entity-factory.ts to ensure frontend\n * knows the actual mounted route, not just the explicit routePrefix.\n */\nfunction inferRoutePrefix(def: Record<string, unknown>): string {\n // Explicit routePrefix\n if (def['routePrefix'] !== undefined && def['routePrefix'] !== null) {\n const rp = def['routePrefix'] as string\n return rp.startsWith('/') ? rp : `/${rp}`\n }\n\n // Infer from table: remote_hosts -> /hosts\n if (def['table']) {\n const table = def['table'] as string\n const parts = table.split('_')\n const name = parts.length > 1 ? parts.slice(1).join('_') : table\n return `/${name}`\n }\n\n // Infer from key (single entities): remote_settings -> /remote_settings\n if (def['key']) {\n return `/${def['key']}`\n }\n\n // Fallback from label\n const label = def['label']\n if (typeof label === 'string') return `/${label.toLowerCase().replace(/\\s+/g, '-')}`\n if (label && typeof label === 'object') {\n const en = (label as Record<string, string>)['en'] ?? ''\n return `/${en.toLowerCase().replace(/\\s+/g, '-')}`\n }\n return '/'\n}\n\n/**\n * Converts an entity definition to a serializable DTO\n */\nexport function toEntityDefinitionDTO(def: Record<string, unknown>, _engine: ModuleContext['engine'], moduleName?: string): EntityDefinitionDTO {\n const defFields = (def['fields'] ?? {}) as Record<string, unknown>\n const fields: Record<string, FieldDefinitionDTO> = {}\n for (const [name, field] of Object.entries(defFields)) {\n fields[name] = toFieldDTO(name, field as unknown as Record<string, unknown>)\n }\n\n // Pre-calculate field metadata for frontend\n const fieldEntries = Object.entries(defFields)\n const searchableFields = fieldEntries.filter(([, f]) => {\n const field = f as unknown as Record<string, unknown>\n const meta = field['meta'] as Record<string, unknown> | undefined\n return meta?.['searchable'] === true && !field['hidden']\n })\n const sortableFields = fieldEntries.filter(([, f]) => {\n const field = f as unknown as Record<string, unknown>\n const meta = field['meta'] as Record<string, unknown> | undefined\n return meta?.['sortable'] === true && !field['hidden']\n })\n\n // Groupable fields: explicit definition takes precedence over auto-detection\n const explicitGroupable = def['groupableFields'] as string[] | undefined\n const groupableInputTypes = ['select', 'switch', 'checkbox', 'radio', 'tags']\n const autoGroupableFields = fieldEntries.filter(([, f]) => {\n const field = f as unknown as Record<string, unknown>\n const inputType = field['input'] as string | undefined\n return groupableInputTypes.includes(inputType ?? '') && !field['hidden']\n })\n const resolvedGroupableFields = explicitGroupable\n ?? (autoGroupableFields.length > 0 ? autoGroupableFields.map(([name]) => name) : undefined)\n\n const entityType = (def['type'] as string) ?? 'collection'\n const explicitDisplayMode = def['displayMode'] as 'table' | 'list' | 'masonry' | 'tree' | undefined\n const explicitAvailableModes = def['availableDisplayModes'] as string[] | undefined\n const defaultDisplayMode = explicitDisplayMode\n ?? (explicitAvailableModes?.length === 1 ? explicitAvailableModes[0] as typeof explicitDisplayMode : undefined)\n ?? (['tree', 'dag'].includes(entityType) ? 'tree' : 'table')\n const entityIdent = (def['table'] as string | undefined) ?? (def['key'] as string | undefined)\n\n return {\n id: (def as unknown as { _id: string })._id,\n key: def['key'] as string | undefined,\n type: entityType as EntityDefinitionDTO['type'],\n label: def['label'] as EntityDefinitionDTO['label'],\n labelField: def['labelField'] as string | undefined,\n icon: def['icon'] as string | undefined,\n routePrefix: inferRoutePrefix(def),\n order: def['order'] as number | undefined,\n fields,\n fieldsCount: Object.keys(fields).length,\n hasTimestamps: !!def['timestamps'],\n hasAudit: !!def['audit'],\n caslSubject: (def['casl'] as Record<string, unknown> | undefined)?.['subject'] as string | undefined,\n isSingle: entityType === 'computed' && !!def['isSingle'],\n displayMode: explicitDisplayMode,\n defaultDisplayMode,\n availableDisplayModes: (() => {\n const explicit = def['availableDisplayModes'] as string[] | undefined\n if (explicit?.length) return explicit as EntityDefinitionDTO['availableDisplayModes']\n const modes: string[] = ['table', 'list', 'masonry']\n if (['tree', 'dag'].includes(entityType)) modes.push('tree')\n if ((resolvedGroupableFields?.length ?? 0) > 0) modes.push('board')\n if (def['calendarFrom']) modes.push('calendar')\n return modes as EntityDefinitionDTO['availableDisplayModes']\n })(),\n hasSearchableFields: searchableFields.length > 0,\n hasSortableFields: sortableFields.length > 0,\n sortableFieldOptions: sortableFields.map(([name, f]) => ({\n value: name,\n label: (f as unknown as Record<string, unknown>)['label'] as string\n })),\n groupableFields: resolvedGroupableFields,\n columnDragFields: def['columnDragFields'] as string[] | undefined,\n groupBy: def['groupBy'] as string | undefined,\n subgroupBy: def['subgroupBy'] as string | undefined,\n calendarFrom: def['calendarFrom'] as string | undefined,\n calendarTo: def['calendarTo'] as string | undefined,\n defaultSort: def['defaultSort'] as { field: string; order: 'asc' | 'desc' } | undefined,\n allowCreate: def['allowCreate'] !== undefined ? !!def['allowCreate'] : ['collection', 'external', 'tree', 'dag'].includes(entityType),\n allowEdit: def['allowEdit'] !== undefined ? !!def['allowEdit'] : ['collection', 'external', 'tree', 'dag', 'single', 'config'].includes(entityType),\n allowDelete: def['allowDelete'] !== undefined ? !!def['allowDelete'] : ['collection', 'external', 'tree', 'dag'].includes(entityType),\n allowDragDrop: ['tree', 'dag'].includes(entityType) ? (def['allowDragDrop'] as boolean | undefined) : undefined,\n hidden: def['hidden'] as boolean | undefined,\n actions: Array.isArray(def['actions']) && def['actions'].length > 0\n ? (def['actions'] as ActionDefinition[]).map(a => toActionDTO(a))\n : undefined,\n realtime: def['realtime'] as EntityDefinitionDTO['realtime'],\n socketKey: moduleName && entityIdent ? `${moduleName}.${entityIdent}` : undefined,\n liveOn: def['liveOn'] as string[] | undefined,\n refreshInterval: def['refreshInterval'] as number | undefined\n }\n}\n\n/**\n * Converts ModuleManifest to a serializable ModuleDTO\n */\nexport function toModuleDTO(mod: ModuleManifest, ctx: ModuleContext): ModuleDTO {\n const libPath = ctx.core.getLibPath()\n\n return {\n name: mod.name,\n label: mod.label!,\n icon: mod.icon,\n description: mod.description,\n type: mod.type ?? 'core',\n category: mod.category,\n dependencies: mod.dependencies ?? [],\n routePrefix: mod.routePrefix ?? `/${mod.name}`,\n subjects: ctx.engine.getModuleSubjects(mod),\n definitions: (mod.definitions ?? [])\n .filter(def => ('expose' in def ? (def as { expose?: boolean }).expose : true) !== false)\n .map(def =>\n toEntityDefinitionDTO(def as unknown as Record<string, unknown>, ctx.engine, mod.name)\n ),\n actions: (mod.actions ?? []).length > 0\n ? (mod.actions ?? []).filter(a => !a.hidden).map(a => toActionDTO(a))\n : undefined,\n hasRoutes: !!mod.routes || (mod.definitions?.length ?? 0) > 0 || (mod.actions?.length ?? 0) > 0,\n hasMigrate: !!mod.migrate,\n hasSeed: moduleHasSeedWithPath(mod, libPath),\n hasInit: !!mod.init\n }\n}\n\n/**\n * Converts PluginManifest to a serializable PluginDTO\n */\nexport function toPluginDTO(plugin: PluginManifest, ctx: ModuleContext): PluginDTO {\n const state = ctx.core.plugins.getState(plugin.name)\n return {\n name: plugin.name,\n code: plugin.code,\n label: plugin.label,\n icon: plugin.icon,\n category: plugin.category,\n version: plugin.version,\n description: plugin.description,\n installed: true,\n enabled: state?.enabled ?? null,\n modules: plugin.modules.map(mod => toModuleDTO(mod, ctx)),\n envVars: plugin.envVars,\n peerDependencies: plugin.peerDependencies,\n llms: plugin.llms,\n setup: plugin.setup ? {\n steps: plugin.setup.steps,\n docsUrl: plugin.setup.docsUrl,\n prerequisites: plugin.setup.prerequisites,\n caveats: plugin.setup.caveats\n } : undefined\n }\n}\n\n","import type {\n Request,\n Response,\n ModuleContext,\n ModuleManifest,\n PluginManifest,\n ActionDefinition,\n PageDefinition,\n PageDefinitionDTO,\n ModuleDTO,\n PluginDTO,\n ManifestDTO,\n CombinedManifestDTO,\n CapabilitiesDTO\n} from '@gzl10/nexus-sdk'\nimport { toEntityDefinitionDTO, toActionDTO, moduleHasSeedWithPath } from './system.helpers.js'\n\n/**\n * Converts PageDefinition to a serializable DTO (no functions)\n */\nfunction toPageDTO(page: PageDefinition): PageDefinitionDTO {\n return {\n id: page.id,\n label: page.label,\n icon: page.icon,\n type: page.type,\n caslSubject: page.caslSubject,\n order: page.order,\n hidden: page.hidden,\n standalone: page.standalone,\n dataSource: page.dataSource,\n widgets: page.widgets,\n component: page.component,\n contentEndpoint: page.contentEndpoint,\n meta: page.meta,\n layout: page.layout\n }\n}\n\nexport function createSystemController(ctx: ModuleContext) {\n const { core, engine } = ctx\n const errors = core.errors\n\n function toModuleDTO(mod: ModuleManifest, libPath: string, req?: Request): ModuleDTO {\n return {\n name: mod.name,\n label: mod.label!,\n icon: mod.icon,\n description: mod.description,\n type: mod.type ?? 'core',\n category: mod.category,\n dependencies: mod.dependencies ?? [],\n routePrefix: mod.routePrefix ?? `/${mod.name}`,\n subjects: engine.getModuleSubjects(mod),\n definitions: (mod.definitions ?? []).map((def) => {\n // Use shared DTO builder (single source of truth for entity serialization)\n const baseDTO = toEntityDefinitionDTO(def as unknown as Record<string, unknown>, engine, mod.name)\n\n // Override actions with request-aware version (evaluates disabled() with req/ctx)\n const defActions = 'actions' in def && Array.isArray((def as { actions?: unknown }).actions)\n ? ((def as { actions: ActionDefinition[] }).actions).filter(a => !a.hidden)\n : []\n\n return {\n ...baseDTO,\n actions: defActions.length > 0\n ? defActions.map(action => toActionDTO(action, req, ctx))\n : undefined\n }\n }),\n actions: (mod.actions ?? []).length > 0\n ? (mod.actions ?? []).filter(a => !a.hidden).map(action => toActionDTO(action, req, ctx))\n : undefined,\n pages: (mod.pages ?? []).length > 0\n ? (mod.pages ?? []).map(toPageDTO)\n : undefined,\n hasRoutes: !!mod.routes || (mod.definitions?.length ?? 0) > 0 || (mod.pages?.length ?? 0) > 0 || (mod.actions?.length ?? 0) > 0,\n hasMigrate: !!mod.migrate,\n hasSeed: moduleHasSeedWithPath(mod, libPath),\n hasInit: !!mod.init\n }\n }\n\n function toPluginDTO(plugin: PluginManifest, libPath: string, req?: Request): PluginDTO {\n const state = ctx.core.plugins.getState(plugin.name)\n return {\n name: plugin.name,\n code: plugin.code,\n label: plugin.label,\n icon: plugin.icon,\n category: plugin.category,\n version: plugin.version,\n description: plugin.description,\n installed: true,\n enabled: state?.enabled ?? null,\n modules: plugin.modules.map(mod => toModuleDTO(mod, libPath, req)),\n envVars: plugin.envVars,\n peerDependencies: plugin.peerDependencies,\n // Serialize setup without postInstall function\n setup: plugin.setup ? {\n steps: plugin.setup.steps,\n docsUrl: plugin.setup.docsUrl,\n prerequisites: plugin.setup.prerequisites,\n caveats: plugin.setup.caveats\n } : undefined\n }\n }\n\n return {\n /**\n * GET /system/modules\n * Lists registered modules. Supports filtering by source, sorting, and filtering.\n * @query source - 'core' | 'user' | 'all' (default: 'all')\n * @query sort - field to sort by\n * @query order - 'asc' | 'desc' (default: 'asc')\n * @query filters - JSON string of filters\n */\n listModules(req: Request, res: Response) {\n const libPath = ctx.core.getLibPath()\n const source = (req.query?.['source'] as string) || 'all'\n const sort = req.query?.['sort'] as string | undefined\n const order = (req.query?.['order'] as 'asc' | 'desc') || 'asc'\n\n // Parse filters\n let filters: Record<string, unknown> | undefined\n if (req.query?.['filters']) {\n try {\n filters = JSON.parse(req.query['filters'] as string)\n } catch {\n // Invalid JSON, ignore filters\n }\n }\n\n let modules: ModuleManifest[]\n if (source === 'core') {\n modules = engine.getCoreModules()\n } else if (source === 'user') {\n modules = engine.getUserModules()\n } else {\n modules = engine.getModules()\n }\n\n let items = modules.map(mod => toModuleDTO(mod, libPath, req))\n\n // Apply filters\n if (filters && Object.keys(filters).length > 0) {\n items = items.filter(item => {\n const record = item as unknown as Record<string, unknown>\n for (const [key, value] of Object.entries(filters)) {\n if (value === undefined || value === null || value === '') continue\n const itemValue = record[key]\n if (Array.isArray(value) && value.length > 0) {\n if (!value.includes(itemValue)) return false\n } else if (itemValue !== value) {\n return false\n }\n }\n return true\n })\n }\n\n // Apply sorting\n if (sort) {\n items = items.sort((a, b) => {\n const aRecord = a as unknown as Record<string, unknown>\n const bRecord = b as unknown as Record<string, unknown>\n const aVal = aRecord[sort]\n const bVal = bRecord[sort]\n if (String(aVal ?? '') < String(bVal ?? '')) return order === 'asc' ? -1 : 1\n if (String(aVal ?? '') > String(bVal ?? '')) return order === 'asc' ? 1 : -1\n return 0\n })\n }\n\n res.json({\n items,\n total: items.length,\n page: 1,\n limit: items.length,\n totalPages: 1,\n hasNext: false\n })\n },\n\n /**\n * GET /system/modules/:name\n * Gets a module by name\n */\n getModule(req: Request<{ name: string }>, res: Response) {\n const { name } = req.params\n const mod = engine.getModules().find((m) => m.name === name)\n\n if (!mod) {\n throw new errors.NotFoundError('Módulo no encontrado')\n }\n\n res.json(toModuleDTO(mod, ctx.core.getLibPath(), req))\n },\n\n /**\n * GET /system/plugins\n * Lists all registered plugins\n */\n listPlugins(_req: Request, res: Response) {\n const libPath = ctx.core.getLibPath()\n const plugins = engine.getPlugins().map(p => toPluginDTO(p, libPath))\n res.json({\n items: plugins,\n total: plugins.length,\n page: 1,\n limit: plugins.length,\n totalPages: 1,\n hasNext: false\n })\n },\n\n /**\n * GET /system/plugin?name=xxx\n * Returns a plugin by name.\n * No authentication required.\n */\n getPlugin(req: Request, res: Response) {\n const name = req.query['name'] as string | undefined\n\n if (!name) {\n throw new errors.ValidationError('name query parameter is required')\n }\n\n const plugin = engine.getPlugins().find(p => p.name === name)\n\n if (!plugin) {\n throw new errors.NotFoundError('Plugin')\n }\n\n res.json(toPluginDTO(plugin, ctx.core.getLibPath(), req))\n },\n\n /**\n * GET /system/manifest\n * Returns combined manifest (core + user)\n */\n getManifest(req: Request, res: Response) {\n const coreManifest = engine.getCoreManifest()\n const userManifest = engine.getUserManifest()\n\n const result: CombinedManifestDTO = {\n core: toManifestDTO(coreManifest, req),\n user: userManifest ? toManifestDTO(userManifest, req) : null,\n hasUserApp: engine.hasUserApp(),\n tenantId: ctx.tenantId\n }\n\n res.json(result)\n },\n\n /**\n * GET /system/manifest/core\n * Returns core manifest (nexus-backend modules)\n */\n getCoreManifest(req: Request, res: Response) {\n const manifest = engine.getCoreManifest()\n res.json(toManifestDTO(manifest, req))\n },\n\n /**\n * GET /system/manifest/user\n * Returns user manifest (plugins + standalone modules)\n * Returns null if running in development mode\n */\n getUserManifest(req: Request, res: Response) {\n const manifest = engine.getUserManifest()\n if (!manifest) {\n res.json(null)\n return\n }\n res.json(toManifestDTO(manifest, req))\n },\n\n /**\n * GET /system/capabilities\n * Public endpoint — no authentication required.\n * Returns backend version and registered plugin codes.\n */\n getCapabilities(_req: Request, res: Response) {\n res.set('Cache-Control', 'public, max-age=300')\n const manifest = engine.getCoreManifest()\n const plugins = engine.getPlugins()\n const body: CapabilitiesDTO = {\n version: manifest.version,\n plugins: plugins.map(p => p.code),\n locales: ctx.locales\n }\n res.json(body)\n }\n }\n\n function toManifestDTO(manifest: { name: string; version: string; modules: ModuleManifest[]; plugins?: PluginManifest[] }, req?: Request): ManifestDTO {\n const libPath = ctx.core.getLibPath()\n return {\n name: manifest.name,\n version: manifest.version,\n modules: manifest.modules.map(mod => toModuleDTO(mod, libPath, req)),\n plugins: manifest.plugins?.map(p => toPluginDTO(p, libPath, req))\n }\n }\n}\n","import type { ModuleContext } from '@gzl10/nexus-sdk'\nimport { createSystemController } from './system.controller.js'\n\n/**\n * System Routes\n *\n * Rutas manuales para endpoints especiales.\n * Las listas de módulos/plugins ahora se manejan con computed entities.\n */\nexport function createSystemRoutes(ctx: ModuleContext) {\n const router = ctx.core.createRouter()\n const controller = createSystemController(ctx)\n const auth = ctx.core.middleware['auth']\n\n if (!auth) {\n throw new Error('Auth middleware is required for system routes')\n }\n\n // Rutas públicas - no requieren autenticación\n router.get('/capabilities', controller.getCapabilities)\n // Usa query ?name= porque los nombres de paquetes npm pueden contener /\n router.get('/plugin', controller.getPlugin)\n\n // Lista de módulos con filtro por source (tiene prioridad sobre computed entity)\n router.get('/modules', auth, controller.listModules)\n\n // Lookup por nombre\n router.get('/modules/:name', auth, controller.getModule)\n\n // AppManifest endpoints - requieren autenticación\n router.get('/manifest', auth, controller.getManifest)\n router.get('/manifest/core', auth, controller.getCoreManifest)\n router.get('/manifest/user', auth, controller.getUserManifest)\n\n return router\n}\n","import * as os from 'node:os'\nimport type { ComputedEntityDefinition, ModuleContext, ModuleDTO } from '@gzl10/nexus-sdk'\nimport { useIconField, useTextField, useSelectField, useNumberField, useCheckboxField, useTagsField, useNameField, useDescriptionField } from '@gzl10/nexus-sdk/fields'\nimport type { OsComputedDTO } from './system.types.js'\nimport { toModuleDTO } from './system.helpers.js'\n\n/**\n * EntityDefinition for system Modules (computed).\n * Data computed at runtime from the engine.\n */\nexport const moduleEntity: ComputedEntityDefinition = {\n type: 'computed',\n label: { en: 'Modules', es: 'Módulos' },\n icon: 'mdi:package-variant-closed',\n labelField: 'name',\n routePrefix: '/modules',\n displayMode: 'masonry',\n defaultSort: { field: 'name', order: 'asc' },\n\n fields: {\n name: useNameField({\n size: 50\n }),\n label: useTextField({\n label: { en: 'Label', es: 'Etiqueta' },\n size: 100,\n nullable: false,\n meta: { sortable: true }\n }),\n icon: useIconField({ label: { en: 'Icon', es: 'Icono' }, size: 50 }),\n type: {\n ...useSelectField({\n label: { en: 'Type', es: 'Tipo' },\n options: [\n { value: 'core', label: { en: 'Core', es: 'Core' } },\n { value: 'plugin', label: { en: 'Plugin', es: 'Plugin' } },\n { value: 'auth-plugin', label: { en: 'Auth Plugin', es: 'Plugin de autenticación' } },\n { value: 'custom', label: { en: 'Custom', es: 'Personalizado' } }\n ],\n nullable: false,\n meta: { sortable: true }\n }),\n validation: { enum: ['core', 'plugin', 'auth-plugin', 'custom'] }\n },\n description: useDescriptionField({\n mode: 'text'\n }),\n routePrefix: useTextField({\n label: { en: 'Route', es: 'Ruta' },\n size: 50,\n nullable: true\n }),\n dependencies: useTagsField({\n label: { en: 'Dependencies', es: 'Dependencias' },\n nullable: true\n }),\n subjects: useTagsField({\n label: { en: 'Subjects', es: 'Sujetos' },\n nullable: true\n }),\n definitionsCount: useNumberField({\n label: { en: 'Entities', es: 'Entidades' },\n nullable: false\n }),\n hasRoutes: useCheckboxField({\n label: { en: 'Routes', es: 'Rutas' }\n }),\n hasMigrate: useCheckboxField({\n label: { en: 'Migration', es: 'Migración' }\n }),\n hasInit: useCheckboxField({\n label: { en: 'Init', es: 'Inicialización' }\n })\n },\n\n // Requiere autenticación - datos de sistema son sensibles\n casl: {\n subject: 'Module',\n permissions: {\n MANAGER: { actions: ['read'] },\n EDITOR: { actions: ['read'] },\n USER: { actions: ['read'] },\n VIEWER: { actions: ['read'] },\n SUPPORT: { actions: ['read'] },\n AUDITOR: { actions: ['read'] }\n }\n },\n\n // Función que calcula los módulos desde el engine (devuelve ModuleDTO completo)\n compute: async (ctx: ModuleContext): Promise<ModuleDTO[]> => {\n return ctx.engine.getModules().map(mod => toModuleDTO(mod, ctx))\n },\n\n cache: {\n ttl: 60 // Cache de 60 segundos (módulos no cambian en runtime)\n }\n}\n\n/**\n * EntityDefinition for Operating System info (computed).\n * Data computed at runtime from node:os.\n */\nexport const osEntity: ComputedEntityDefinition = {\n type: 'computed',\n label: { en: 'OS Info', es: 'Información del SO' },\n icon: 'mdi:server',\n labelField: 'hostname',\n routePrefix: '/os',\n isSingle: true,\n order: 1,\n refreshInterval: 5000,\n\n fields: {\n hostname: useTextField({\n label: { en: 'Hostname', es: 'Nombre de servidor' },\n size: 100,\n nullable: false,\n inputProps: { order: 1 }\n }),\n platform: {\n ...useSelectField({\n label: { en: 'Platform', es: 'Plataforma' },\n options: [\n { value: 'darwin', label: { en: 'macOS', es: 'macOS' } },\n { value: 'linux', label: { en: 'Linux', es: 'Linux' } },\n { value: 'win32', label: { en: 'Windows', es: 'Windows' } },\n { value: 'freebsd', label: { en: 'FreeBSD', es: 'FreeBSD' } }\n ],\n nullable: false,\n meta: { sortable: true }\n }),\n inputProps: { order: 2 }\n },\n arch: {\n ...useSelectField({\n label: { en: 'Architecture', es: 'Arquitectura' },\n options: [\n { value: 'x64', label: { en: 'x64', es: 'x64' } },\n { value: 'arm64', label: { en: 'ARM64', es: 'ARM64' } },\n { value: 'arm', label: { en: 'ARM', es: 'ARM' } },\n { value: 'ia32', label: { en: 'x86', es: 'x86' } }\n ],\n nullable: false\n }),\n inputProps: { order: 3 }\n },\n type: {\n ...useTextField({\n label: { en: 'Type', es: 'Tipo' },\n size: 30,\n nullable: false\n }),\n inputProps: { order: 4 }\n },\n release: {\n ...useTextField({\n label: { en: 'Release', es: 'Versión' },\n size: 50,\n nullable: false\n }),\n inputProps: { order: 5 }\n },\n uptime: {\n ...useNumberField({\n label: { en: 'Uptime', es: 'Tiempo activo' },\n nullable: false,\n meta: { sortable: true }\n }),\n inputProps: { order: 6, format: 'duration' }\n },\n nexusUptime: {\n ...useNumberField({\n label: { en: 'Nexus Uptime', es: 'Tiempo activo de Nexus' },\n nullable: false\n }),\n inputProps: { order: 7, format: 'duration' }\n },\n cpuModel: {\n ...useTextField({\n label: { en: 'CPU Model', es: 'Modelo de CPU' },\n size: 100,\n nullable: false\n }),\n inputProps: { order: 8, span: 2 }\n },\n cpuCount: {\n ...useNumberField({\n label: { en: 'CPU Cores', es: 'Núcleos de CPU' },\n nullable: false\n }),\n inputProps: { order: 9 }\n },\n totalMemory: {\n ...useNumberField({\n label: { en: 'Total Memory', es: 'Memoria total' },\n nullable: false\n }),\n inputProps: { order: 10, format: 'bytes' }\n },\n freeMemory: {\n ...useNumberField({\n label: { en: 'Free Memory', es: 'Memoria libre' },\n nullable: false\n }),\n inputProps: { order: 11, format: 'bytes' }\n },\n loadAverage: {\n label: { en: 'Load Average (1m / 5m / 15m)', es: 'Promedio de carga (1m / 5m / 15m)' },\n input: 'text',\n db: { type: 'json', nullable: false },\n inputProps: { order: 12, format: 'loadAverage', span: 2 }\n }\n },\n\n // Info técnica del sistema - para soporte y auditoría\n casl: {\n subject: 'OsInfo',\n permissions: {\n SUPPORT: { actions: ['read'] },\n AUDITOR: { actions: ['read'] }\n }\n },\n\n compute: async (_ctx: ModuleContext): Promise<OsComputedDTO[]> => {\n const cpus = os.cpus()\n return [{\n hostname: os.hostname(),\n platform: os.platform(),\n arch: os.arch(),\n release: os.release(),\n type: os.type(),\n uptime: os.uptime(),\n nexusUptime: Math.floor(process.uptime()),\n totalMemory: os.totalmem(),\n freeMemory: os.freemem(),\n cpuCount: cpus.length,\n cpuModel: cpus[0]?.model ?? 'Unknown',\n loadAverage: os.loadavg()\n }]\n },\n\n // No cache — refreshInterval handles polling frequency from the frontend\n}\n","import type { EventEntityDefinition } from '@gzl10/nexus-sdk'\nimport { useIdField, useTextField, useNumberField, useSelectField, useDatetimeField, useTextareaField } from '@gzl10/nexus-sdk/fields'\n\n/**\n * Migration History Entity\n *\n * Provides read-only access to migration execution history.\n * Maps to _nexus_migrations table created by the migration system.\n *\n * Only accessible by ADMIN role.\n */\nexport const migrationHistoryEntity: EventEntityDefinition = {\n type: 'event',\n immutable: true,\n label: { en: 'Migration History', es: 'Historial de Migraciones' },\n icon: 'mdi:database-sync',\n labelField: 'name',\n routePrefix: '/migration-history',\n table: '_nexus_migrations',\n timestamps: true,\n calendarFrom: 'executed_at',\n\n fields: {\n id: useIdField(),\n name: useTextField({ label: { en: 'Migration Name', es: 'Nombre de la Migración' }, required: true, size: 255, unique: true, meta: { sortable: true, searchable: true } }),\n batch: useNumberField({ label: { en: 'Batch', es: 'Lote' }, required: true, meta: { sortable: true } }),\n status: useSelectField({\n label: { en: 'Status', es: 'Estado' },\n options: [\n { value: 'running', label: { en: 'Running', es: 'Ejecutando' } },\n { value: 'completed', label: { en: 'Completed', es: 'Completada' } },\n { value: 'failed', label: { en: 'Failed', es: 'Fallida' } },\n { value: 'rolled_back', label: { en: 'Rolled Back', es: 'Revertida' } }\n ],\n required: true,\n size: 20,\n meta: { sortable: true }\n }),\n executed_at: useDatetimeField({ label: { en: 'Executed At', es: 'Ejecutada el' }, meta: { sortable: true } }),\n rolled_back_at: useDatetimeField({ label: { en: 'Rolled Back At', es: 'Revertida el' }, meta: { sortable: true } }),\n execution_time_ms: useNumberField({ label: { en: 'Duration (ms)', es: 'Duración (ms)' }, meta: { sortable: true } }),\n error: useTextareaField({ label: { en: 'Error Message', es: 'Mensaje de Error' } })\n },\n\n casl: {\n subject: 'MigrationHistory',\n permissions: {\n ADMIN: { actions: ['read'] },\n SUPPORT: { actions: ['read'] }\n }\n },\n\n // Keep migration history for 1 year\n retention: { days: 365 },\n\n defaultSort: { field: 'executed_at', order: 'desc' }\n}\n","/**\n * Computed entity for environment variable configuration.\n *\n * Reads from EnvVarRegistry, crosses with process.env,\n * masks sensitive values, and exposes as read-only table.\n * Only ADMIN and OWNER can access.\n */\n\nimport type { ComputedEntityDefinition, ModuleContext } from '@gzl10/nexus-sdk'\nimport { useTextField, useSelectField, useCheckboxField } from '@gzl10/nexus-sdk/fields'\nimport type { EnvVarRegistry } from './env-config.registry.js'\n\n/**\n * Masks a value if it is marked as sensitive.\n * - URLs: masks the password component (user:*** @ host)\n * - Plain strings: replaced entirely with ***\n */\nfunction maskValue(value: string, sensitive: boolean): string {\n if (!sensitive) return value\n try {\n const url = new URL(value)\n if (url.password) {\n url.password = '***'\n return url.toString()\n }\n } catch { /* not a URL, mask entirely */ }\n return '***'\n}\n\nexport const envConfigEntity: ComputedEntityDefinition = {\n type: 'computed',\n label: { en: 'Environment', es: 'Entorno' },\n icon: 'mdi:cog-outline',\n labelField: 'name',\n routePrefix: '/env-config',\n defaultSort: { field: 'category', order: 'asc' },\n\n fields: {\n name: useTextField({\n label: { en: 'Variable', es: 'Variable' },\n size: 100,\n nullable: false,\n meta: { sortable: true, searchable: true }\n }),\n category: {\n ...useSelectField({\n label: { en: 'Category', es: 'Categoría' },\n options: [\n { value: 'server', label: { en: 'Server', es: 'Servidor' } },\n { value: 'auth', label: { en: 'Authentication', es: 'Autenticación' } },\n { value: 'mail', label: { en: 'Mail', es: 'Correo' } },\n { value: 'logger', label: { en: 'Logger', es: 'Logger' } },\n { value: 'observability', label: { en: 'Observability', es: 'Observabilidad' } },\n { value: 'storage', label: { en: 'Storage', es: 'Almacenamiento' } },\n { value: 'advanced', label: { en: 'Advanced', es: 'Avanzado' } }\n ],\n nullable: false,\n meta: { sortable: true }\n })\n },\n source: useTextField({\n label: { en: 'Source', es: 'Origen' },\n size: 50,\n nullable: false,\n meta: { sortable: true }\n }),\n value: useTextField({\n label: { en: 'Value', es: 'Valor' },\n size: 255,\n nullable: true\n }),\n default: useTextField({\n label: { en: 'Default', es: 'Por defecto' },\n size: 100,\n nullable: true\n }),\n configured: useCheckboxField({\n label: { en: 'Set', es: 'Configurada' },\n meta: { sortable: true }\n }),\n required: useCheckboxField({\n label: { en: 'Required', es: 'Requerida' },\n meta: { sortable: true }\n }),\n sensitive: useCheckboxField({\n label: { en: 'Sensitive', es: 'Sensible' }\n }),\n description: {\n label: { en: 'Description', es: 'Descripción' },\n input: 'text',\n db: { type: 'json', nullable: true }\n }\n },\n\n casl: {\n subject: 'EnvConfig',\n permissions: {\n ADMIN: { actions: ['read'] }\n // OWNER gets manage:all automatically\n }\n },\n\n compute: async (ctx: ModuleContext) => {\n const registry = ctx.services.get<EnvVarRegistry>('envVarRegistry')\n if (!registry) return []\n\n return registry.getAll().map(entry => {\n const raw = process.env[entry.name]\n const configured = raw !== undefined\n return {\n name: entry.name,\n category: entry.category,\n source: entry.source,\n value: configured ? maskValue(raw, entry.sensitive ?? false) : null,\n default: entry.default ?? null,\n configured,\n required: entry.required,\n sensitive: entry.sensitive ?? false,\n description: entry.description\n }\n })\n },\n\n cache: {\n ttl: 300 // 5 minutes — env vars don't change at runtime\n }\n}\n","/**\n * EnvVarRegistry — Dynamic registry of environment variables (core + plugins).\n *\n * Core vars are registered at system module init().\n * Plugin vars are injected automatically from PluginManifest.envVars.\n */\n\nimport type { PluginEnvVar } from '@gzl10/nexus-sdk'\n\nexport interface EnvVarEntry extends PluginEnvVar {\n /** Grouping category: server, auth, mail, logger, observability, storage, or plugin name */\n category: string\n /** Source identifier: 'core' or plugin package name */\n source: string\n}\n\nexport class EnvVarRegistry {\n private entries = new Map<string, EnvVarEntry>()\n\n /** Register one or more env var descriptors under a category */\n register(category: string, source: string, vars: PluginEnvVar[]): void {\n for (const v of vars) {\n this.entries.set(v.name, { ...v, category, source })\n }\n }\n\n /** Get all registered vars sorted by category then name */\n getAll(): EnvVarEntry[] {\n return [...this.entries.values()].sort((a, b) =>\n a.category.localeCompare(b.category) || a.name.localeCompare(b.name)\n )\n }\n\n get size(): number {\n return this.entries.size\n }\n}\n\n// Singleton\nlet instance: EnvVarRegistry | null = null\n\nexport function getEnvVarRegistry(): EnvVarRegistry {\n if (!instance) instance = new EnvVarRegistry()\n return instance\n}\n\n/** @internal - Only used in tests */\nfunction _resetEnvVarRegistry(): void {\n instance = null\n}\n\n/**\n * Registers all ~53 core environment variables used by Nexus backend.\n * Sourced from Zod schemas in config/ and modules/.\n */\nexport function registerCoreVars(registry: EnvVarRegistry): void {\n // ── Server (config/env.ts) ──────────────────────────────────────────\n registry.register('server', 'core', [\n { name: 'NODE_ENV', description: { en: 'Runtime environment. Controls debug output, error verbosity, and optimization behavior', es: 'Entorno de ejecución. Controla salida de debug, verbosidad de errores y optimizaciones' },\n required: false, default: 'development', sensitive: false },\n { name: 'PORT', description: { en: 'HTTP server listening port. Change if 3000 conflicts with another service', es: 'Puerto de escucha del servidor HTTP. Cambiar si 3000 está ocupado por otro servicio' },\n required: false, default: '3000', sensitive: false },\n { name: 'CORS_ORIGIN', description: { en: 'Allowed CORS origins (comma-separated). Use specific origins in production, never * with credentials', es: 'Orígenes CORS permitidos (separados por coma). Usar orígenes específicos en producción, nunca * con credenciales' },\n required: false, default: '*', sensitive: false },\n { name: 'BACKEND_URL', description: { en: 'Public URL of the backend. Used for generating links in emails, storage URLs, and OAuth callbacks', es: 'URL pública del backend. Usado para generar enlaces en emails, URLs de almacenamiento y callbacks OAuth' },\n required: false, default: 'http://localhost:3000', sensitive: false },\n { name: 'DATABASE_URL', description: { en: 'Database connection string. Supports SQLite (file:./dev.db), PostgreSQL, and MySQL', es: 'Cadena de conexión a base de datos. Soporta SQLite (file:./dev.db), PostgreSQL y MySQL' },\n required: false, default: 'file:./dev.db', sensitive: true,\n example: 'postgresql://user:pass@localhost:5432/nexus' },\n { name: 'REDIS_URL', description: { en: 'Redis connection URL. Enables distributed cache, rate limiting, and session storage. Required for multi-instance deployments', es: 'URL de conexión Redis. Habilita caché distribuida, rate limiting y almacenamiento de sesiones. Requerido para despliegues multi-instancia' },\n required: false, sensitive: true,\n example: 'redis://localhost:6379' },\n { name: 'REDIS_PREFIX', description: { en: 'Prefix for all Redis keys. Useful when sharing a Redis instance across multiple Nexus environments', es: 'Prefijo para todas las claves Redis. Útil al compartir una instancia Redis entre varios entornos Nexus' },\n required: false, default: 'nexus:', sensitive: false },\n { name: 'ADMIN_EMAIL', description: { en: 'Email for the default admin account created on first startup (only if no users exist)', es: 'Email de la cuenta admin por defecto creada en el primer inicio (solo si no existen usuarios)' },\n required: false, default: 'admin@nexus.local', sensitive: false },\n { name: 'ADMIN_PASSWORD', description: { en: 'Password for the default admin account. Change immediately after first login in production', es: 'Contraseña de la cuenta admin por defecto. Cambiar inmediatamente tras el primer login en producción' },\n required: false, default: 'admin', sensitive: true },\n { name: 'COOKIE_DOMAIN', description: { en: 'Domain scope for authentication cookies. Set to .example.com for SSO across subdomains', es: 'Dominio de las cookies de autenticación. Usar .example.com para SSO entre subdominios' },\n required: false, sensitive: false,\n example: '.example.com' },\n { name: 'TZ', description: { en: 'Server timezone in IANA format. Affects date formatting and scheduled tasks. UTC recommended for consistency', es: 'Zona horaria del servidor en formato IANA. Afecta formato de fechas y tareas programadas. UTC recomendado para consistencia' },\n required: false, default: 'UTC', sensitive: false,\n example: 'America/New_York' },\n { name: 'TRUST_PROXY', description: { en: 'Enable trust for proxy headers (X-Forwarded-For, etc.). Required behind Nginx, Traefik, or Caddy for correct client IP detection', es: 'Habilitar confianza en headers de proxy (X-Forwarded-For, etc.). Necesario tras Nginx, Traefik o Caddy para detección correcta de IP del cliente' },\n required: false, default: 'false', sensitive: false },\n { name: 'NEXUS_UI_ENABLED', description: { en: 'Serve the built-in Vue UI from the backend. Disable if deploying frontend separately (e.g., CDN or Nginx)', es: 'Servir la UI Vue integrada desde el backend. Deshabilitar si el frontend se despliega por separado (ej. CDN o Nginx)' },\n required: false, default: 'true', sensitive: false },\n { name: 'NEXUS_UI_BASE', description: { en: 'Base URL path where the UI is mounted. Change if serving Nexus under a sub-path (e.g., /admin)', es: 'Ruta base donde se monta la UI. Cambiar si Nexus se sirve bajo un sub-path (ej. /admin)' },\n required: false, default: '/', sensitive: false },\n { name: 'NEXUS_UI_PATH', description: { en: 'Filesystem path to the UI dist directory. Auto-detected from node_modules if not set', es: 'Ruta al directorio dist de la UI. Auto-detectado desde node_modules si no se establece' },\n required: false, sensitive: false },\n { name: 'NEXUS_CHECK_DRIFT', description: { en: 'Compare database schema against expected state on startup. Logs warnings if tables/columns are out of sync', es: 'Comparar esquema de BD con el estado esperado al iniciar. Registra advertencias si tablas/columnas están desincronizadas' },\n required: false, default: 'false', sensitive: false },\n { name: 'NEXUS_FAIL_ON_DRIFT', description: { en: 'Exit with error if schema drift is detected. Recommended in CI/CD pipelines to catch unapplied migrations', es: 'Salir con error si se detecta drift de esquema. Recomendado en pipelines CI/CD para detectar migraciones no aplicadas' },\n required: false, default: 'false', sensitive: false },\n ])\n\n // ── Auth (modules/auth/auth.config.ts) ──────────────────────────────\n registry.register('auth', 'core', [\n { name: 'AUTH_SECRET', description: { en: 'JWT signing secret. Must be at least 32 characters. Use a cryptographically random string in production', es: 'Clave de firma JWT. Mínimo 32 caracteres. Usar una cadena criptográficamente aleatoria en producción' },\n required: true, sensitive: true },\n { name: 'AUTH_ACCESS_EXPIRES', description: { en: 'Access token lifetime using ms-compatible format (e.g., 15m, 1h). Shorter = more secure, longer = fewer refreshes', es: 'Duración del token de acceso en formato ms (ej. 15m, 1h). Más corto = más seguro, más largo = menos refrescos' },\n required: false, default: '15m', sensitive: false,\n example: '30m' },\n { name: 'AUTH_REFRESH_EXPIRES', description: { en: 'Refresh token lifetime using ms-compatible format (e.g., 7d, 30d). Controls how long sessions last without re-login', es: 'Duración del token de refresco en formato ms (ej. 7d, 30d). Controla cuánto duran las sesiones sin re-login' },\n required: false, default: '7d', sensitive: false,\n example: '30d' },\n { name: 'AUTH_RATE_LIMIT_MAX', description: { en: 'Maximum failed login attempts per IP within the rate limit window before blocking', es: 'Intentos máximos de login fallidos por IP dentro de la ventana de rate limit antes de bloquear' },\n required: false, default: '5', sensitive: false },\n { name: 'AUTH_RATE_LIMIT_WINDOW', description: { en: 'Duration of the rate limit window in seconds. After this period, the failed attempt counter resets', es: 'Duración de la ventana de rate limit en segundos. Tras este periodo, el contador de intentos fallidos se reinicia' },\n required: false, default: '900', sensitive: false },\n { name: 'AUTH_COOKIE_DOMAIN', description: { en: 'Cookie domain specific to auth. Overrides COOKIE_DOMAIN when set. Useful for auth-specific subdomain scoping', es: 'Dominio de cookies específico para auth. Sobreescribe COOKIE_DOMAIN. Útil para scoping de subdominios específico de auth' },\n required: false, sensitive: false,\n example: '.auth.example.com' },\n { name: 'AUTH_CHALLENGE_THRESHOLD', description: { en: 'Number of failed login attempts before requiring OTP verification as an additional security step', es: 'Número de intentos fallidos de login antes de requerir verificación OTP como paso adicional de seguridad' },\n required: false, default: '2', sensitive: false },\n { name: 'AUTH_SKIP_REGISTER_OTP', description: { en: 'Skip email OTP verification during user registration. Only for development — never enable in production', es: 'Omitir verificación OTP por email durante registro de usuario. Solo para desarrollo — nunca habilitar en producción' },\n required: false, default: 'false', sensitive: false },\n ])\n\n // ── Mail (modules/mail/mail.config.ts) ──────────────────────────────\n registry.register('mail', 'core', [\n { name: 'SMTP_HOST', description: { en: 'SMTP server hostname. Default points to MailHog/Mailpit for development', es: 'Hostname del servidor SMTP. Por defecto apunta a MailHog/Mailpit para desarrollo' },\n required: false, default: 'localhost', sensitive: false },\n { name: 'SMTP_PORT', description: { en: 'SMTP server port. Common ports: 25 (plain), 465 (SSL), 587 (STARTTLS), 1025 (MailHog)', es: 'Puerto del servidor SMTP. Puertos comunes: 25 (plano), 465 (SSL), 587 (STARTTLS), 1025 (MailHog)' },\n required: false, default: '1025', sensitive: false },\n { name: 'SMTP_SECURE', description: { en: 'Use TLS/SSL for the SMTP connection. Enable for port 465, leave false for STARTTLS on port 587', es: 'Usar TLS/SSL para la conexión SMTP. Habilitar para puerto 465, dejar false para STARTTLS en puerto 587' },\n required: false, default: 'false', sensitive: false },\n { name: 'SMTP_USER', description: { en: 'SMTP authentication username. Required by most production mail providers (SendGrid, AWS SES, etc.)', es: 'Usuario de autenticación SMTP. Requerido por la mayoría de proveedores de correo en producción (SendGrid, AWS SES, etc.)' },\n required: false, sensitive: false },\n { name: 'SMTP_PASS', description: { en: 'SMTP authentication password or API key. Use app-specific passwords when available', es: 'Contraseña o API key de autenticación SMTP. Usar contraseñas específicas de aplicación cuando estén disponibles' },\n required: false, sensitive: true },\n { name: 'SMTP_FROM', description: { en: 'Default sender address for all outgoing emails (OTP, notifications, password reset)', es: 'Dirección de remitente por defecto para todos los emails salientes (OTP, notificaciones, reset de contraseña)' },\n required: false, default: 'noreply@nexus.local', sensitive: false,\n example: 'noreply@mycompany.com' },\n ])\n\n // ── Logger (core/logger/config.ts) ──────────────────────────────────\n registry.register('logger', 'core', [\n { name: 'LOG_LEVEL', description: { en: 'Pino log level. Options: trace, debug, info, warn, error, fatal, silent. Use debug for development troubleshooting', es: 'Nivel de log de Pino. Opciones: trace, debug, info, warn, error, fatal, silent. Usar debug para troubleshooting en desarrollo' },\n required: false, default: 'info', sensitive: false,\n example: 'debug' },\n { name: 'LOG_FORMAT', description: { en: 'Log output format. Use pretty for human-readable dev output, json for production log aggregators (ELK, Loki)', es: 'Formato de salida de logs. Usar pretty para desarrollo legible, json para agregadores de producción (ELK, Loki)' },\n required: false, default: 'pretty', sensitive: false },\n { name: 'SENTRY_DSN', description: { en: 'Sentry Data Source Name. When set, errors and unhandled exceptions are reported to Sentry automatically', es: 'DSN de Sentry. Cuando se establece, los errores y excepciones no manejadas se reportan a Sentry automáticamente' },\n required: false, sensitive: true,\n example: 'https://key@sentry.io/123' },\n { name: 'SENTRY_SAMPLE_RATE', description: { en: 'Percentage of events sent to Sentry (0.0-1.0). Lower values reduce costs in high-traffic production environments. Environment is inferred from NODE_ENV automatically', es: 'Porcentaje de eventos enviados a Sentry (0.0-1.0). Valores bajos reducen costos en entornos de producción con alto tráfico. El entorno se infiere de NODE_ENV automáticamente' },\n required: false, default: '1.0', sensitive: false },\n ])\n\n // ── Observability (modules/observability/observability.config.ts) ───\n registry.register('observability', 'core', [\n { name: 'OTEL_ENABLED', description: { en: 'Enable OpenTelemetry instrumentation for HTTP, Express, Knex, and Pino. Exposes metrics and traces', es: 'Habilitar instrumentación OpenTelemetry para HTTP, Express, Knex y Pino. Expone métricas y trazas' },\n required: false, default: 'false', sensitive: false },\n { name: 'OTEL_SERVICE_NAME', description: { en: 'Service name used in traces and metrics. Appears in Grafana, Jaeger, and Prometheus as the service identifier', es: 'Nombre del servicio en trazas y métricas. Aparece en Grafana, Jaeger y Prometheus como identificador del servicio' },\n required: false, default: 'nexus', sensitive: false },\n { name: 'OTEL_PROMETHEUS_PORT', description: { en: 'Dedicated port for the Prometheus /metrics endpoint. Runs on a separate HTTP server for K8s scraping', es: 'Puerto dedicado para el endpoint Prometheus /metrics. Corre en un servidor HTTP separado para scraping de K8s' },\n required: false, default: '9464', sensitive: false },\n { name: 'OTEL_EXPORTER_OTLP_ENDPOINT', description: { en: 'OTLP HTTP endpoint for exporting traces to Jaeger, Tempo, Grafana Cloud, or any OTLP-compatible collector', es: 'Endpoint HTTP OTLP para exportar trazas a Jaeger, Tempo, Grafana Cloud o cualquier colector compatible con OTLP' },\n required: false, sensitive: false,\n example: 'http://localhost:4318' },\n { name: 'OTEL_TRACE_SAMPLE_RATE', description: { en: 'Percentage of requests traced (0.0-1.0). Use 1.0 for dev, lower in production to reduce overhead and storage', es: 'Porcentaje de requests trazados (0.0-1.0). Usar 1.0 en dev, reducir en producción para menos overhead y almacenamiento' },\n required: false, default: '1.0', sensitive: false },\n ])\n\n // ── Storage (modules/storage/) ──────────────────────────────────────\n registry.register('storage', 'core', [\n { name: 'STORAGE_DRIVER', description: { en: 'File storage backend. Use filesystem for local development, s3 for production with any S3-compatible provider', es: 'Backend de almacenamiento de archivos. Usar filesystem para desarrollo local, s3 para producción con cualquier proveedor compatible con S3' },\n required: false, default: 'filesystem', sensitive: false },\n { name: 'STORAGE_PATH', description: { en: 'Local directory for uploaded files (filesystem driver only). Relative to the backend working directory', es: 'Directorio local para archivos subidos (solo driver filesystem). Relativo al directorio de trabajo del backend' },\n required: false, default: './storage', sensitive: false },\n { name: 'STORAGE_URL', description: { en: 'Public base URL for accessing stored files. Auto-generated from BACKEND_URL if not set', es: 'URL base pública para acceder a archivos almacenados. Auto-generada desde BACKEND_URL si no se establece' },\n required: false, sensitive: false },\n { name: 'STORAGE_MAX_SIZE', description: { en: 'Maximum allowed file upload size in bytes. Default 10MB (10485760). Increase for video or large document uploads', es: 'Tamaño máximo de subida de archivos en bytes. Por defecto 10MB (10485760). Aumentar para videos o documentos grandes' },\n required: false, default: '10485760', sensitive: false },\n { name: 'STORAGE_ALLOWED_TYPES', description: { en: 'Whitelist of allowed MIME types (comma-separated). Supports wildcards like image/*. Empty = allow all', es: 'Lista blanca de tipos MIME permitidos (separados por coma). Soporta comodines como image/*. Vacío = permitir todos' },\n required: false, sensitive: false,\n example: 'image/*,application/pdf,video/*' },\n { name: 'S3_BUCKET', description: { en: 'S3 bucket name for file storage. Must exist before starting the server. Required when STORAGE_DRIVER=s3', es: 'Nombre del bucket S3 para almacenamiento. Debe existir antes de iniciar el servidor. Requerido cuando STORAGE_DRIVER=s3' },\n required: false, sensitive: false },\n { name: 'S3_REGION', description: { en: 'AWS region or S3-compatible region identifier. Required for AWS S3, optional for MinIO', es: 'Región AWS o identificador de región compatible con S3. Requerido para AWS S3, opcional para MinIO' },\n required: false, sensitive: false,\n example: 'us-east-1' },\n { name: 'S3_ACCESS_KEY', description: { en: 'S3 access key ID for authentication. Use IAM credentials with minimal required permissions', es: 'ID de clave de acceso S3. Usar credenciales IAM con los permisos mínimos necesarios' },\n required: false, sensitive: true },\n { name: 'S3_SECRET_KEY', description: { en: 'S3 secret access key for authentication. Store securely, never commit to version control', es: 'Clave secreta de acceso S3. Almacenar de forma segura, nunca incluir en control de versiones' },\n required: false, sensitive: true },\n { name: 'S3_ENDPOINT', description: { en: 'Custom endpoint for S3-compatible services like MinIO, DigitalOcean Spaces, or Backblaze B2', es: 'Endpoint personalizado para servicios compatibles con S3 como MinIO, DigitalOcean Spaces o Backblaze B2' },\n required: false, sensitive: false,\n example: 'https://minio.example.com:9000' },\n ])\n\n // ── Misc (scattered across codebase) ────────────────────────────────\n registry.register('advanced', 'core', [\n { name: 'MODULES_DIR', description: { en: 'Path to a directory with custom module definitions loaded at startup. Enables extending Nexus without plugins', es: 'Ruta a un directorio con definiciones de módulos personalizados cargados al inicio. Permite extender Nexus sin plugins' },\n required: false, sensitive: false },\n { name: 'MIGRATIONS_DIR', description: { en: 'Override the default migrations directory. Used for project-specific migration files separate from core', es: 'Sobreescribir el directorio de migraciones por defecto. Usado para migraciones específicas del proyecto separadas del core' },\n required: false, sensitive: false },\n { name: 'NEXUS_ALLOW_PLUGIN_INSTALL', description: { en: 'Allow installing/uninstalling plugins at runtime via the admin UI. Disabled by default for security', es: 'Permitir instalar/desinstalar plugins en tiempo de ejecución desde la UI admin. Deshabilitado por defecto por seguridad' },\n required: false, default: 'false', sensitive: false },\n { name: 'NEXUS_CASL_DEBUG', description: { en: 'Log every CASL permission check (allow/deny) with subject, action, and conditions. Very verbose — only for debugging authorization issues', es: 'Loguear cada verificación de permisos CASL (allow/deny) con sujeto, acción y condiciones. Muy verboso — solo para depurar problemas de autorización' },\n required: false, default: 'false', sensitive: false },\n ])\n}\n","import type { ActionDefinition, AuthRequest } from '@gzl10/nexus-sdk'\n\n/** Tables that must never be truncated */\nconst SYSTEM_TABLES = new Set([\n '_nexus_migrations',\n '_nexus_migration_lock',\n '_nexus_sequences',\n 'sqlite_sequence', // SQLite internal\n 'knex_migrations', // Knex default (unused but safe)\n 'knex_migrations_lock'\n])\n\n/**\n * Standalone action - Factory Reset\n *\n * Truncates all data tables (preserving schema) and re-seeds\n * all modules with their default data.\n *\n * Protected by: CASL (ADMIN only) + double confirmation (type \"RESET\" + confirm dialog)\n */\nexport const factoryResetAction: ActionDefinition = {\n key: 'factory-reset',\n scope: 'module',\n group: { en: 'System', es: 'Sistema' },\n label: { en: 'Factory Reset', es: 'Reinicio de Fábrica' },\n icon: 'mdi:database-refresh',\n variant: 'danger',\n output: {},\n\n confirm: {\n type: 'double',\n title: { en: 'Factory Reset', es: 'Reinicio de Fábrica' },\n message: {\n en: 'This will DELETE ALL DATA and restore default values. This action cannot be undone.',\n es: 'Esto ELIMINARÁ TODOS LOS DATOS y restaurará los valores por defecto. Esta acción no se puede deshacer.'\n },\n verifyText: 'RESET',\n verifyLabel: {\n en: 'Type RESET to confirm',\n es: 'Escribe RESET para confirmar'\n },\n confirmText: { en: 'Reset Everything', es: 'Reiniciar Todo' },\n severity: 'error'\n },\n\n handler: async (ctx, _input, req) => {\n const authReq = req as AuthRequest\n const userId = authReq.user?.id ?? 'unknown'\n const knex = ctx.db.knex\n\n ctx.core.logger.warn({ userId }, 'Factory reset initiated')\n\n // Notify audit before reset (tables will be dropped); setImmediate lets async listeners flush\n ctx.events.notify('audit.log', {\n source: 'core:system',\n action: 'factory_reset',\n actorId: authReq.user?.id,\n ip: req?.ip,\n userAgent: req?.headers['user-agent']\n })\n await new Promise<void>(resolve => setImmediate(resolve))\n\n // 1. Get all user tables (exclude system infrastructure)\n const allTables = await getAllTables(knex)\n const dataTables = allTables.filter(t => !SYSTEM_TABLES.has(t))\n\n if (dataTables.length === 0) {\n return { success: true, message: 'No data tables found', tablesCleared: 0, modulesSeeded: 0 }\n }\n\n // 2. Truncate all data tables (disable FK checks for clean truncation)\n const client = knex.client.config.client as string\n const isSQLite = client === 'better-sqlite3' || client === 'sqlite3'\n const isPostgres = client === 'pg' || client === 'postgresql'\n\n if (isSQLite) {\n await knex.raw('PRAGMA foreign_keys = OFF')\n } else if (isPostgres) {\n // PostgreSQL: TRUNCATE ... CASCADE in a single statement\n const tableList = dataTables.map(t => `\"${t}\"`).join(', ')\n await knex.raw(`TRUNCATE TABLE ${tableList} CASCADE`)\n } else {\n // MySQL / others\n await knex.raw('SET FOREIGN_KEY_CHECKS = 0')\n }\n\n if (!isPostgres) {\n // SQLite and MySQL: delete table by table\n for (const table of dataTables) {\n try {\n await knex(table).del()\n } catch (err) {\n ctx.core.logger.warn({ table, err }, 'Failed to clear table, skipping')\n }\n }\n }\n\n // Reset sequences for SQLite\n if (isSQLite) {\n await knex.raw('PRAGMA foreign_keys = ON')\n } else if (!isPostgres) {\n await knex.raw('SET FOREIGN_KEY_CHECKS = 1')\n }\n\n // Also reset nexus sequences counter\n try {\n await knex('_nexus_sequences').del()\n } catch {\n // Table might not exist\n }\n\n ctx.core.logger.info({ tables: dataTables.length }, 'All data tables cleared')\n\n // 3. Re-seed all modules in dependency order\n const modules = ctx.engine.getModules()\n let modulesSeeded = 0\n\n for (const mod of modules) {\n try {\n const seeded = await ctx.db.seedModule(mod)\n if (seeded) modulesSeeded++\n } catch (err) {\n ctx.core.logger.error({ module: mod.name, err }, 'Seed failed during factory reset')\n }\n }\n\n ctx.core.logger.warn({ userId, tablesCleared: dataTables.length, modulesSeeded }, 'Factory reset completed')\n\n return {\n success: true,\n message: `Factory reset complete. ${dataTables.length} tables cleared, ${modulesSeeded} modules re-seeded.`,\n tablesCleared: dataTables.length,\n modulesSeeded\n }\n },\n\n casl: {\n subject: 'SystemManagement',\n permissions: {\n ADMIN: { actions: ['execute'] }\n }\n }\n}\n\n/**\n * Get all table names from the database\n */\nasync function getAllTables(knex: import('knex').Knex): Promise<string[]> {\n const client = knex.client.config.client as string\n\n if (client === 'better-sqlite3' || client === 'sqlite3') {\n const rows = await knex.raw(\"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'\")\n return (rows as Array<{ name: string }>).map(r => r.name)\n }\n\n if (client === 'pg' || client === 'postgresql') {\n const result = await knex.raw(\n \"SELECT tablename FROM pg_tables WHERE schemaname = 'public'\"\n )\n return result.rows.map((r: { tablename: string }) => r.tablename)\n }\n\n // MySQL\n const result = await knex.raw('SHOW TABLES')\n const key = Object.keys(result[0]?.[0] ?? {})[0] as string\n if (!key) return []\n return (result[0] as Record<string, string>[]).map(r => r[key]).filter(Boolean) as string[]\n}\n","import type { ActionDefinition, AuthRequest } from '@gzl10/nexus-sdk'\n\n/**\n * Standalone action - Restart Server\n *\n * Gracefully restarts the Nexus server process.\n * Protected by: CASL (ADMIN only) + simple confirmation\n */\nexport const restartServerAction: ActionDefinition = {\n key: 'restart-server',\n scope: 'module',\n group: { en: 'System', es: 'Sistema' },\n label: { en: 'Restart Server', es: 'Reiniciar Servidor' },\n icon: 'mdi:restart',\n variant: 'danger',\n\n confirm: {\n type: 'simple',\n title: { en: 'Restart Server', es: 'Reiniciar Servidor' },\n message: {\n en: 'The server will restart. All active connections will be dropped momentarily.',\n es: 'El servidor se reiniciará. Todas las conexiones activas se interrumpirán momentáneamente.'\n },\n confirmText: { en: 'Restart', es: 'Reiniciar' },\n severity: 'warning'\n },\n\n handler: async (ctx, _input, req) => {\n const authReq = req as AuthRequest\n const userId = authReq.user?.id ?? 'unknown'\n ctx.core.logger.warn({ userId }, 'Server restart initiated')\n\n ctx.events.notify('audit.log', {\n source: 'core:system',\n action: 'server_restart',\n actorId: authReq.user?.id,\n ip: req?.ip,\n userAgent: req?.headers['user-agent']\n })\n\n // Schedule exit after response is sent\n setTimeout(() => process.exit(0), 500)\n\n return { success: true, message: 'Server is restarting...' }\n },\n\n casl: {\n subject: 'SystemManagement',\n permissions: {\n ADMIN: { actions: ['execute'] }\n }\n }\n}\n","/**\n * @module system\n * @description System info, health check, OpenAPI spec\n */\n\nimport type { ModuleManifest } from '@gzl10/nexus-sdk'\nimport { createSystemRoutes } from './system.routes.js'\nimport { moduleEntity, osEntity } from './system.entity.js'\nimport { migrationHistoryEntity } from './migration-history.entity.js'\nimport { envConfigEntity } from './env-config.entity.js'\nimport { getEnvVarRegistry, registerCoreVars } from './env-config.registry.js'\nimport { factoryResetAction } from './actions/factory-reset.action.js'\nimport { restartServerAction } from './actions/restart-server.action.js'\n\n// Re-exports\nexport { createSystemController } from './system.controller.js'\nexport { moduleEntity, osEntity } from './system.entity.js'\nexport { migrationHistoryEntity } from './migration-history.entity.js'\nexport { envConfigEntity } from './env-config.entity.js'\nexport { getEnvVarRegistry } from './env-config.registry.js'\nexport type { EnvVarEntry } from './env-config.registry.js'\nexport type {\n OsComputedDTO\n} from './system.types.js'\n\nexport const systemModule: ModuleManifest = {\n name: 'system',\n label: { en: 'System', es: 'Sistema' },\n icon: 'mdi:cog-outline',\n description: { en: 'System configuration, module registry, and platform metadata', es: 'Configuración del sistema, registro de módulos y metadatos de plataforma' },\n type: 'core',\n category: 'settings',\n dependencies: ['logger'],\n definitions: [\n moduleEntity, osEntity, envConfigEntity,\n migrationHistoryEntity\n ],\n\n actions: [\n restartServerAction,\n factoryResetAction\n ],\n\n init: (ctx) => {\n const registry = getEnvVarRegistry()\n registerCoreVars(registry)\n\n // Inject envVars from registered plugins\n for (const plugin of ctx.engine.getPlugins()) {\n if (plugin.envVars?.length) {\n registry.register(plugin.name, plugin.code, plugin.envVars)\n }\n }\n\n ctx.services.register('envVarRegistry', registry)\n },\n routes: createSystemRoutes,\n routePrefix: '/system'\n}\n","import type { SingleEntityDefinition } from '@gzl10/nexus-sdk'\nimport { useTextField, useImageField } from '@gzl10/nexus-sdk/fields'\n\n/**\n * UI Branding - Corporate identity settings\n *\n * Stored in single_records table as JSON with key 'ui_branding'.\n * Public read access (needed before login for branding).\n * Only ADMIN/OWNER can update.\n */\nexport const uiBrandingEntity: SingleEntityDefinition = {\n type: 'single',\n realtime: 'sync',\n key: 'ui_branding',\n label: { en: 'Branding', es: 'Identidad Corporativa' },\n icon: 'mdi:palette-swatch-outline',\n public: true,\n routePrefix: '/ui-branding',\n\n defaults: {\n appName: 'Nexus',\n logo: null,\n logoDark: null,\n favicon: null\n },\n\n fields: {\n appName: useTextField({\n label: { en: 'App Name', es: 'Nombre de la App' },\n hint: { en: 'Displayed in the header, browser tab and emails', es: 'Se muestra en el header, pestaña del navegador y emails' },\n size: 100,\n required: true\n }),\n\n logo: useImageField({\n label: { en: 'Logo (Light Theme)', es: 'Logo (Tema Claro)' },\n hint: { en: 'Recommended: SVG or PNG with transparent background, max 200px height', es: 'Recomendado: SVG o PNG con fondo transparente, máx 200px de alto' },\n folder: 'branding',\n isPublic: true,\n dedupe: true\n }),\n\n logoDark: useImageField({\n label: { en: 'Logo (Dark Theme)', es: 'Logo (Tema Oscuro)' },\n hint: { en: 'Optional: Use a lighter version for dark backgrounds', es: 'Opcional: Usa una versión más clara para fondos oscuros' },\n folder: 'branding',\n isPublic: true,\n dedupe: true\n }),\n\n favicon: useImageField({\n label: { en: 'Favicon', es: 'Favicon' },\n hint: { en: 'Browser tab icon. Recommended: 32x32 or 64x64 PNG/ICO', es: 'Icono de pestaña del navegador. Recomendado: 32x32 o 64x64 PNG/ICO' },\n accept: 'image/x-icon,image/png,image/svg+xml',\n maxSize: '256KB',\n folder: 'branding',\n isPublic: true,\n dedupe: true\n })\n },\n\n casl: {\n subject: 'UiBranding',\n permissions: {\n ADMIN: { actions: ['read', 'update'] }\n }\n }\n}\n","import type { SingleEntityDefinition } from '@gzl10/nexus-sdk'\nimport { useSelectField, useColorField } from '@gzl10/nexus-sdk/fields'\n\n/**\n * UI Theme - Theme and color settings\n *\n * Stored in single_records table as JSON with key 'ui_theme'.\n * Public read access (needed before login for theming).\n * Only ADMIN/OWNER can update.\n */\nexport const uiThemeEntity: SingleEntityDefinition = {\n type: 'single',\n realtime: 'sync',\n key: 'ui_theme',\n label: { en: 'Theme & Colors', es: 'Tema y Colores' },\n icon: 'mdi:palette',\n public: true,\n routePrefix: '/ui-theme',\n\n defaults: {\n // Typography\n font: 'space-grotesk',\n // Theme & Colors\n theme: 'system',\n primaryColor: '#3B82F6',\n dopamineTheme: 'none',\n // Layout\n loginLayout: 'centered'\n },\n\n fields: {\n // === Typography ===\n font: useSelectField({\n label: { en: 'Font', es: 'Fuente' },\n hint: { en: 'Primary font for headings and UI elements', es: 'Fuente principal para títulos y elementos de interfaz' },\n options: [\n { value: 'space-grotesk', label: 'Space Grotesk' },\n { value: 'inter', label: 'Inter' },\n { value: 'plus-jakarta-sans', label: 'Plus Jakarta Sans' },\n { value: 'outfit', label: 'Outfit' },\n { value: 'rubik', label: 'Rubik' },\n { value: 'system', label: { en: 'System Default', es: 'Sistema' } }\n ]\n }),\n\n // === Theme & Colors ===\n theme: useSelectField({\n label: { en: 'Theme', es: 'Tema' },\n hint: { en: 'System follows your device preferences', es: 'Sistema sigue las preferencias de tu dispositivo' },\n options: [\n { value: 'light', label: { en: 'Light', es: 'Claro' } },\n { value: 'dark', label: { en: 'Dark', es: 'Oscuro' } },\n { value: 'system', label: { en: 'System', es: 'Sistema' } }\n ]\n }),\n\n dopamineTheme: useSelectField({\n label: { en: 'Dopamine Theme', es: 'Tema Dopamina' },\n hint: { en: 'Vibrant color presets optimized for light and dark modes (2025/2026 trends)', es: 'Presets de colores vibrantes optimizados para modo claro y oscuro (tendencias 2025/2026)' },\n options: [\n { value: 'none', label: { en: 'None (Custom Color)', es: 'Ninguno (Color personalizado)' } },\n { value: 'electric', label: { en: 'Electric (Cobalt Blue)', es: 'Eléctrico (Azul Cobalto)' } },\n { value: 'sunset', label: { en: 'Sunset (Coral Red)', es: 'Atardecer (Rojo Coral)' } },\n { value: 'ocean', label: { en: 'Ocean (Teal)', es: 'Océano (Verde Azulado)' } },\n { value: 'forest', label: { en: 'Forest (Mint)', es: 'Bosque (Menta)' } },\n { value: 'lavender', label: { en: 'Lavender (Violet)', es: 'Lavanda (Violeta)' } },\n { value: 'cherry', label: { en: 'Cherry (Fuchsia)', es: 'Cereza (Fucsia)' } },\n { value: 'amber', label: { en: 'Amber (Golden)', es: 'Ámbar (Dorado)' } },\n { value: 'tangerine', label: { en: 'Tangerine (Orange)', es: 'Mandarina (Naranja)' } },\n { value: 'slate', label: { en: 'Slate (Cool Gray)', es: 'Pizarra (Gris Frío)' } },\n { value: 'bronze', label: { en: 'Bronze (Earth)', es: 'Bronce (Tierra)' } }\n ]\n }),\n\n // === Login Layout ===\n loginLayout: useSelectField({\n label: { en: 'Login Layout', es: 'Diseño de Login' },\n hint: { en: 'Visual layout for authentication pages', es: 'Diseño visual para páginas de autenticación' },\n options: [\n { value: 'centered', label: { en: 'Centered', es: 'Centrado' } },\n { value: 'split', label: { en: 'Split', es: 'Dividido' } },\n { value: 'cover', label: { en: 'Cover', es: 'Portada' } },\n { value: 'floating', label: { en: 'Floating', es: 'Flotante' } },\n { value: 'minimal', label: { en: 'Minimal', es: 'Minimalista' } }\n ]\n }),\n\n primaryColor: {\n ...useColorField({\n label: { en: 'Primary Color', es: 'Color Principal' },\n hint: { en: 'Custom accent color for buttons, links and highlights', es: 'Color de acento personalizado para botones, enlaces y resaltados' }\n }),\n // Hide when a dopamine theme is active (show only if 'none')\n hidden: { field: 'dopamineTheme', $ne: 'none' }\n }\n },\n\n casl: {\n subject: 'UiTheme',\n permissions: {\n ADMIN: { actions: ['read', 'update'] }\n }\n }\n}\n","import type { SingleEntityDefinition } from '@gzl10/nexus-sdk'\nimport { useSelectField, useSwitchField } from '@gzl10/nexus-sdk/fields'\n\n/**\n * UI Effects - Visual effects settings\n *\n * Stored in single_records table as JSON with key 'ui_effects'.\n * Public read access (needed before login for visual effects).\n * Only ADMIN/OWNER can update.\n */\nexport const uiEffectsEntity: SingleEntityDefinition = {\n type: 'single',\n realtime: 'sync',\n key: 'ui_effects',\n label: { en: 'Visual Effects', es: 'Efectos Visuales' },\n icon: 'mdi:shimmer',\n public: true,\n routePrefix: '/ui-effects',\n\n defaults: {\n glassIntensity: 'medium',\n borderStyle: 'rounded',\n enableAnimations: true,\n enableOrganicShapes: false\n },\n\n fields: {\n glassIntensity: useSelectField({\n label: { en: 'Glass Intensity', es: 'Intensidad Glass' },\n hint: { en: 'Glassmorphism blur effect on cards and modals', es: 'Efecto de desenfoque glassmorphism en cards y modales' },\n options: [\n { value: 'none', label: { en: 'None', es: 'Ninguno' } },\n { value: 'low', label: { en: 'Low', es: 'Baja' } },\n { value: 'medium', label: { en: 'Medium', es: 'Media' } },\n { value: 'high', label: { en: 'High', es: 'Alta' } }\n ]\n }),\n\n borderStyle: useSelectField({\n label: { en: 'Border Style', es: 'Estilo de Bordes' },\n hint: { en: 'Corner radius for buttons, cards and inputs', es: 'Radio de esquinas para botones, cards e inputs' },\n options: [\n { value: 'sharp', label: { en: 'Sharp', es: 'Cuadrados' } },\n { value: 'rounded', label: { en: 'Rounded', es: 'Redondeados' } },\n { value: 'organic', label: { en: 'Organic', es: 'Orgánicos' } }\n ]\n }),\n\n enableAnimations: useSwitchField({\n label: { en: 'Enable Animations', es: 'Habilitar Animaciones' },\n hint: { en: 'Micro-interactions and transitions (hover, focus, page changes)', es: 'Micro-interacciones y transiciones (hover, focus, cambios de página)' },\n defaultValue: true,\n meta: { sortable: true }\n }),\n\n enableOrganicShapes: useSwitchField({\n label: { en: 'Enable Organic Shapes', es: 'Habilitar Formas Orgánicas' },\n hint: { en: 'Asymmetric border-radius for a more natural, playful look', es: 'Border-radius asimétrico para un aspecto más natural y divertido' },\n defaultValue: false,\n meta: { sortable: true },\n hidden: { field: 'borderStyle', $ne: 'organic' }\n })\n },\n\n casl: {\n subject: 'UiEffects',\n permissions: {\n ADMIN: { actions: ['read', 'update'] }\n }\n }\n}\n","import type { SingleEntityDefinition } from '@gzl10/nexus-sdk'\nimport { useSelectField, useSwitchField } from '@gzl10/nexus-sdk/fields'\n\n/**\n * UI Accessibility - Accessibility settings\n *\n * Stored in single_records table as JSON with key 'ui_accessibility'.\n * Public read access (needed before login for accessibility).\n * Only ADMIN/OWNER can update.\n */\nexport const uiAccessibilityEntity: SingleEntityDefinition = {\n type: 'single',\n realtime: 'sync',\n key: 'ui_accessibility',\n label: { en: 'Accessibility', es: 'Accesibilidad' },\n icon: 'mdi:human',\n public: true,\n routePrefix: '/ui-accessibility',\n\n defaults: {\n typographyScale: 'default',\n reducedMotion: false,\n highContrast: false\n },\n\n fields: {\n typographyScale: useSelectField({\n label: { en: 'Typography Scale', es: 'Escala Tipográfica' },\n hint: { en: 'Adjusts font sizes for better readability (WCAG 1.4.4)', es: 'Ajusta tamaños de fuente para mejor legibilidad (WCAG 1.4.4)' },\n options: [\n { value: 'compact', label: { en: 'Compact (smaller)', es: 'Compacta (más pequeña)' } },\n { value: 'default', label: { en: 'Default', es: 'Por defecto' } },\n { value: 'relaxed', label: { en: 'Relaxed (larger)', es: 'Relajada (más grande)' } }\n ]\n }),\n\n reducedMotion: useSwitchField({\n label: { en: 'Reduced Motion', es: 'Movimiento Reducido' },\n defaultValue: false,\n meta: { sortable: true },\n hint: {\n en: 'Minimizes animations for users sensitive to motion (WCAG 2.1)',\n es: 'Minimiza animaciones para usuarios sensibles al movimiento (WCAG 2.1)'\n }\n }),\n\n highContrast: useSwitchField({\n label: { en: 'High Contrast', es: 'Alto Contraste' },\n defaultValue: false,\n meta: { sortable: true },\n hint: {\n en: 'Increases text contrast for better readability (WCAG AAA)',\n es: 'Aumenta el contraste del texto para mejor legibilidad (WCAG AAA)'\n }\n })\n },\n\n casl: {\n subject: 'UiAccessibility',\n permissions: {\n ADMIN: { actions: ['read', 'update'] }\n }\n }\n}\n","/**\n * @module ui-settings\n * @description User-scoped UI preferences (theme, sidebar, locale)\n */\n\nimport type { ModuleManifest } from '@gzl10/nexus-sdk'\nimport { uiBrandingEntity } from './ui-branding.entity.js'\nimport { uiThemeEntity } from './ui-theme.entity.js'\nimport { uiEffectsEntity } from './ui-effects.entity.js'\nimport { uiAccessibilityEntity } from './ui-accessibility.entity.js'\n\n// Re-exports\nexport { uiBrandingEntity } from './ui-branding.entity.js'\nexport { uiThemeEntity } from './ui-theme.entity.js'\nexport { uiEffectsEntity } from './ui-effects.entity.js'\nexport { uiAccessibilityEntity } from './ui-accessibility.entity.js'\n\n/**\n * UI Settings Module\n *\n * Manages user interface configuration: branding, theme, visual effects, and accessibility.\n * All entities are 'single' type (stored in single_records table as JSON).\n * Public read access for theming before login, ADMIN/OWNER write access.\n */\nexport const uiSettingsModule: ModuleManifest = {\n name: 'ui-settings',\n label: { en: 'UI Settings', es: 'Configuración de UI' },\n icon: 'mdi:palette-outline',\n description: {\n en: 'User interface configuration: branding, themes, visual effects, and accessibility',\n es: 'Configuración de interfaz de usuario: marca, temas, efectos visuales y accesibilidad'\n },\n type: 'core',\n category: 'settings',\n dependencies: ['logger'],\n definitions: [\n uiBrandingEntity,\n uiThemeEntity,\n uiEffectsEntity,\n uiAccessibilityEntity\n ],\n routePrefix: '/ui-settings'\n}\n","/**\n * Filesystem Storage Driver\n *\n * Almacena archivos en el sistema de archivos local.\n */\n\nimport { createReadStream, createWriteStream, existsSync, unlinkSync, mkdirSync } from 'node:fs'\nimport { readFile, readdir, stat, copyFile, rename, mkdir } from 'node:fs/promises'\nimport { join, dirname, extname, basename } from 'node:path'\nimport { createHash } from 'node:crypto'\nimport type { StorageDriver, StorageFile, StorageFileInfo, PutOptions } from './driver.interface.js'\nimport type { StorageConfig } from '../storage.config.js'\n\nexport class FilesystemDriver implements StorageDriver {\n private basePath: string\n private baseUrl: string | undefined\n private generateId: () => string\n\n constructor(config: StorageConfig) {\n this.basePath = config.basePath\n this.baseUrl = config.baseUrl\n this.generateId = config.generateId\n\n // Crear directorio base si no existe\n if (!existsSync(this.basePath)) {\n mkdirSync(this.basePath, { recursive: true })\n }\n }\n\n async put(buffer: Buffer, filename: string, options?: PutOptions): Promise<StorageFile> {\n const id = this.generateId()\n const ext = extname(filename) || ''\n const diskFilename = `${id}${ext}`\n const folder = options?.folder || ''\n const relativePath = folder ? `${folder}/${diskFilename}` : diskFilename\n const fullPath = join(this.basePath, relativePath)\n\n // Crear subdirectorio si es necesario\n const dir = dirname(fullPath)\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true })\n }\n\n // Calcular hash SHA256\n const hash = createHash('sha256').update(buffer).digest('hex')\n\n // Escribir archivo\n await new Promise<void>((resolve, reject) => {\n const stream = createWriteStream(fullPath)\n stream.on('finish', resolve)\n stream.on('error', reject)\n stream.write(buffer)\n stream.end()\n })\n\n return {\n id,\n filename,\n diskFilename,\n mimetype: options?.mimetype || 'application/octet-stream',\n size: buffer.length,\n folder: folder || undefined,\n path: relativePath,\n url: this.getUrl(relativePath) || undefined,\n hash\n }\n }\n\n async get(path: string): Promise<NodeJS.ReadableStream> {\n const fullPath = join(this.basePath, path)\n if (!existsSync(fullPath)) {\n throw new Error(`File not found: ${path}`)\n }\n return createReadStream(fullPath)\n }\n\n async getBuffer(path: string): Promise<Buffer> {\n const fullPath = join(this.basePath, path)\n return readFile(fullPath)\n }\n\n async delete(path: string): Promise<void> {\n const fullPath = join(this.basePath, path)\n if (existsSync(fullPath)) {\n unlinkSync(fullPath)\n }\n }\n\n async exists(path: string): Promise<boolean> {\n const fullPath = join(this.basePath, path)\n return existsSync(fullPath)\n }\n\n getUrl(path: string): string | null {\n if (!this.baseUrl) return null\n return `${this.baseUrl}/files/${path}`\n }\n\n // ============================================================================\n // EXTENDED METHODS\n // ============================================================================\n\n async list(folder?: string): Promise<StorageFileInfo[]> {\n const targetPath = folder ? join(this.basePath, folder) : this.basePath\n\n if (!existsSync(targetPath)) {\n return []\n }\n\n const entries = await readdir(targetPath, { withFileTypes: true })\n const results: StorageFileInfo[] = []\n\n for (const entry of entries) {\n const entryPath = join(targetPath, entry.name)\n const relativePath = folder ? `${folder}/${entry.name}` : entry.name\n const stats = await stat(entryPath)\n\n results.push({\n path: relativePath,\n filename: entry.name,\n size: stats.size,\n lastModified: stats.mtime,\n isDirectory: entry.isDirectory()\n })\n }\n\n return results\n }\n\n async copy(src: string, dst: string): Promise<StorageFile> {\n const srcPath = join(this.basePath, src)\n const dstPath = join(this.basePath, dst)\n\n // Crear directorio destino si no existe\n const dstDir = dirname(dstPath)\n if (!existsSync(dstDir)) {\n await mkdir(dstDir, { recursive: true })\n }\n\n // Copiar archivo\n await copyFile(srcPath, dstPath)\n\n // Leer metadata del archivo copiado\n const buffer = await readFile(dstPath)\n const stats = await stat(dstPath)\n const hash = createHash('sha256').update(buffer).digest('hex')\n\n const id = this.generateId()\n const filename = basename(dst)\n const folder = dirname(dst) === '.' ? undefined : dirname(dst)\n\n return {\n id,\n filename,\n diskFilename: filename,\n mimetype: 'application/octet-stream',\n size: stats.size,\n folder,\n path: dst,\n url: this.getUrl(dst) || undefined,\n hash\n }\n }\n\n async move(src: string, dst: string): Promise<StorageFile> {\n const srcPath = join(this.basePath, src)\n const dstPath = join(this.basePath, dst)\n\n // Crear directorio destino si no existe\n const dstDir = dirname(dstPath)\n if (!existsSync(dstDir)) {\n await mkdir(dstDir, { recursive: true })\n }\n\n // Mover archivo (atómico en mismo filesystem)\n await rename(srcPath, dstPath)\n\n // Leer metadata del archivo movido\n const buffer = await readFile(dstPath)\n const stats = await stat(dstPath)\n const hash = createHash('sha256').update(buffer).digest('hex')\n\n const id = this.generateId()\n const filename = basename(dst)\n const folder = dirname(dst) === '.' ? undefined : dirname(dst)\n\n return {\n id,\n filename,\n diskFilename: filename,\n mimetype: 'application/octet-stream',\n size: stats.size,\n folder,\n path: dst,\n url: this.getUrl(dst) || undefined,\n hash\n }\n }\n\n async getMetadata(path: string): Promise<StorageFileInfo> {\n const fullPath = join(this.basePath, path)\n const stats = await stat(fullPath)\n\n return {\n path,\n filename: basename(path),\n size: stats.size,\n lastModified: stats.mtime,\n isDirectory: stats.isDirectory()\n }\n }\n\n async getSignedUrl(_path: string, _expiresInSeconds: number): Promise<string> {\n // Filesystem no soporta URLs firmadas\n throw new Error('Signed URLs are not supported by the filesystem driver')\n }\n\n async createFolder(path: string): Promise<void> {\n const fullPath = join(this.basePath, path)\n if (!existsSync(fullPath)) {\n await mkdir(fullPath, { recursive: true })\n }\n }\n}\n","/**\n * S3 Storage Driver\n *\n * Almacena archivos en AWS S3 o compatible (MinIO, DigitalOcean Spaces, etc.)\n */\n\nimport { Readable } from 'node:stream'\nimport { createHash } from 'node:crypto'\nimport { extname, basename, dirname } from 'node:path'\nimport {\n S3Client,\n PutObjectCommand,\n GetObjectCommand,\n DeleteObjectCommand,\n HeadObjectCommand,\n ListObjectsV2Command,\n CopyObjectCommand\n} from '@aws-sdk/client-s3'\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner'\nimport type { StorageDriver, StorageFile, StorageFileInfo, PutOptions } from './driver.interface.js'\nimport type { StorageConfig } from '../storage.config.js'\n\nexport class S3Driver implements StorageDriver {\n private client: S3Client\n private bucket: string\n private baseUrl: string | undefined\n private generateId: () => string\n\n constructor(config: StorageConfig) {\n if (!config.s3) {\n throw new Error('S3 configuration is required for S3Driver')\n }\n\n const { bucket, region, accessKeyId, secretAccessKey, endpoint } = config.s3\n\n this.bucket = bucket\n this.baseUrl = config.baseUrl\n this.generateId = config.generateId\n\n this.client = new S3Client({\n region,\n credentials: {\n accessKeyId,\n secretAccessKey\n },\n ...(endpoint && {\n endpoint,\n forcePathStyle: true // Necesario para MinIO\n })\n })\n }\n\n async put(buffer: Buffer, filename: string, options?: PutOptions): Promise<StorageFile> {\n const id = this.generateId()\n const ext = extname(filename) || ''\n const diskFilename = `${id}${ext}`\n const folder = options?.folder || ''\n const key = folder ? `${folder}/${diskFilename}` : diskFilename\n\n // Calcular hash SHA256\n const hash = createHash('sha256').update(buffer).digest('hex')\n\n // Subir a S3\n await this.client.send(new PutObjectCommand({\n Bucket: this.bucket,\n Key: key,\n Body: buffer,\n ContentType: options?.mimetype || 'application/octet-stream',\n ContentLength: buffer.length,\n Metadata: {\n 'original-filename': filename,\n 'sha256': hash\n }\n }))\n\n return {\n id,\n filename,\n diskFilename,\n mimetype: options?.mimetype || 'application/octet-stream',\n size: buffer.length,\n folder: folder || undefined,\n path: key,\n url: this.getUrl(key) || undefined,\n hash\n }\n }\n\n async get(path: string): Promise<NodeJS.ReadableStream> {\n const response = await this.client.send(new GetObjectCommand({\n Bucket: this.bucket,\n Key: path\n }))\n\n if (!response.Body) {\n throw new Error(`File not found: ${path}`)\n }\n\n // El Body de S3 es un Readable stream\n return response.Body as Readable\n }\n\n async getBuffer(path: string): Promise<Buffer> {\n const response = await this.client.send(new GetObjectCommand({\n Bucket: this.bucket,\n Key: path\n }))\n\n if (!response.Body) {\n throw new Error(`File not found: ${path}`)\n }\n\n // Convertir stream a buffer\n const stream = response.Body as Readable\n const chunks: Buffer[] = []\n\n for await (const chunk of stream) {\n chunks.push(Buffer.from(chunk))\n }\n\n return Buffer.concat(chunks)\n }\n\n async delete(path: string): Promise<void> {\n await this.client.send(new DeleteObjectCommand({\n Bucket: this.bucket,\n Key: path\n }))\n }\n\n async exists(path: string): Promise<boolean> {\n try {\n await this.client.send(new HeadObjectCommand({\n Bucket: this.bucket,\n Key: path\n }))\n return true\n } catch {\n return false\n }\n }\n\n getUrl(path: string): string | null {\n if (this.baseUrl) {\n return `${this.baseUrl}/files/${path}`\n }\n // URL pública de S3 (si el bucket tiene permisos públicos)\n return `https://${this.bucket}.s3.amazonaws.com/${path}`\n }\n\n // ============================================================================\n // EXTENDED METHODS\n // ============================================================================\n\n async list(folder?: string): Promise<StorageFileInfo[]> {\n const prefix = folder ? (folder.endsWith('/') ? folder : `${folder}/`) : ''\n\n const response = await this.client.send(new ListObjectsV2Command({\n Bucket: this.bucket,\n Prefix: prefix,\n Delimiter: '/'\n }))\n\n const results: StorageFileInfo[] = []\n\n // Archivos\n if (response.Contents) {\n for (const obj of response.Contents) {\n if (!obj.Key || obj.Key === prefix) continue\n\n results.push({\n path: obj.Key,\n filename: basename(obj.Key),\n size: obj.Size || 0,\n lastModified: obj.LastModified,\n isDirectory: false\n })\n }\n }\n\n // Carpetas (prefijos comunes)\n if (response.CommonPrefixes) {\n for (const prefix of response.CommonPrefixes) {\n if (!prefix.Prefix) continue\n\n const folderPath = prefix.Prefix.endsWith('/')\n ? prefix.Prefix.slice(0, -1)\n : prefix.Prefix\n\n results.push({\n path: folderPath,\n filename: basename(folderPath),\n size: 0,\n isDirectory: true\n })\n }\n }\n\n return results\n }\n\n async copy(src: string, dst: string): Promise<StorageFile> {\n // S3 CopyObject hace la copia server-side (eficiente)\n await this.client.send(new CopyObjectCommand({\n Bucket: this.bucket,\n CopySource: `${this.bucket}/${src}`,\n Key: dst\n }))\n\n // Obtener metadata del archivo copiado\n const headResponse = await this.client.send(new HeadObjectCommand({\n Bucket: this.bucket,\n Key: dst\n }))\n\n const id = this.generateId()\n const filename = basename(dst)\n const folder = dirname(dst) === '.' ? undefined : dirname(dst)\n\n return {\n id,\n filename,\n diskFilename: filename,\n mimetype: headResponse.ContentType || 'application/octet-stream',\n size: headResponse.ContentLength || 0,\n folder,\n path: dst,\n url: this.getUrl(dst) || undefined,\n hash: headResponse.Metadata?.['sha256']\n }\n }\n\n async move(src: string, dst: string): Promise<StorageFile> {\n // S3 no tiene move nativo: copy + delete\n const result = await this.copy(src, dst)\n await this.delete(src)\n return result\n }\n\n async getMetadata(path: string): Promise<StorageFileInfo> {\n const response = await this.client.send(new HeadObjectCommand({\n Bucket: this.bucket,\n Key: path\n }))\n\n return {\n path,\n filename: basename(path),\n size: response.ContentLength || 0,\n mimetype: response.ContentType,\n lastModified: response.LastModified,\n isDirectory: false\n }\n }\n\n async getSignedUrl(path: string, expiresInSeconds: number): Promise<string> {\n const command = new GetObjectCommand({\n Bucket: this.bucket,\n Key: path\n })\n\n return getSignedUrl(this.client, command, { expiresIn: expiresInSeconds })\n }\n\n async createFolder(path: string): Promise<void> {\n // En S3 las carpetas son prefijos, pero podemos crear un marker object\n const folderKey = path.endsWith('/') ? path : `${path}/`\n\n await this.client.send(new PutObjectCommand({\n Bucket: this.bucket,\n Key: folderKey,\n Body: '',\n ContentLength: 0\n }))\n }\n}\n","/**\n * Storage Module Configuration\n *\n * Configuration for the file storage system.\n * Supports local filesystem and S3 (AWS/MinIO).\n * Reads configuration from DB (storage_config) with env var fallback.\n */\n\nimport { join, isAbsolute } from 'node:path'\nimport { existsSync, mkdirSync } from 'node:fs'\nimport type { Knex } from 'knex'\nimport type { FilesystemMetadata, S3Metadata } from './storage.types.js'\n\nconst SINGLE_RECORDS_TABLE = 'single_records'\nconst CONFIG_KEY_PREFIX = 'storage_config:'\n\nexport interface StorageConfig {\n /** Configuration scope (unique identifier) */\n scope: string\n\n /** Storage driver */\n driver: 'filesystem' | 's3'\n\n /** Base path for filesystem (default: ./storage) */\n basePath: string\n\n /** Base public URL for generating file URLs */\n baseUrl?: string\n\n /** Maximum file size in bytes (default: 10MB) */\n maxFileSize: number\n\n /** Globally allowed MIME types (optional) */\n allowedMimeTypes?: string[]\n\n /** ID generator (injected from ctx.helpers) */\n generateId: () => string\n\n /** S3 configuration */\n s3?: {\n bucket: string\n region: string\n accessKeyId: string\n secretAccessKey: string\n /** Custom endpoint for MinIO/other providers */\n endpoint?: string\n }\n}\n\nconst DEFAULT_MAX_SIZE = 10 * 1024 * 1024 // 10MB\n\n/** Default scope for filesystem */\nexport const DEFAULT_FILESYSTEM_SCOPE = 'default_filesystem'\n\n/** Default scope for S3 */\nexport const DEFAULT_S3_SCOPE = 'default_s3'\n\n/** Default scope by driver */\nfunction getDefaultScope(driver: 'filesystem' | 's3'): string {\n return driver === 's3' ? DEFAULT_S3_SCOPE : DEFAULT_FILESYSTEM_SCOPE\n}\n\n/** Singleton config for compatibility with existing code */\nlet defaultConfig: StorageConfig | null = null\n\n/** Project path to resolve relative paths */\nlet projectPath: string = process.cwd()\n\n/** GenerateId function */\nlet generateIdFn: (() => string) | null = null\n\n/**\n * Gets the default storage configuration (singleton)\n */\nexport function getStorageConfig(): StorageConfig {\n if (!defaultConfig) {\n throw new Error('StorageConfig not initialized. Call initStorageConfig() first.')\n }\n return defaultConfig\n}\n\n/**\n * Resolves a storage path:\n * - Absolute paths: used directly\n * - Relative paths (./storage): resolved to projectPath/data/{path}\n */\nfunction resolveStoragePath(path: string, projPath: string): string {\n if (isAbsolute(path)) {\n return path\n }\n // Rutas relativas van dentro de data/\n const cleanPath = path.startsWith('./') ? path.slice(2) : path\n return join(projPath, 'data', cleanPath)\n}\n\n/**\n * DB record for storage_config\n */\ninterface StorageConfigRow {\n scope: string\n driver: 'filesystem' | 's3'\n base_url: string | null\n max_file_size: number\n allowed_mime_types: string | null\n metadata: string | null\n}\n\n/**\n * Gets storage configuration by scope from the DB.\n * Falls back to environment variables if not found in the DB.\n */\nexport async function getConfigByScope(\n db: Knex,\n scope: string\n): Promise<StorageConfig> {\n if (!generateIdFn) {\n throw new Error('StorageConfig not initialized. Call initStorageConfig() first.')\n }\n\n // Read from single_records (config absorbed into single)\n const kvRow = await db(SINGLE_RECORDS_TABLE)\n .where({ key: `${CONFIG_KEY_PREFIX}${scope}` })\n .first<{ value: string }>()\n\n if (kvRow) {\n try {\n const parsed = JSON.parse(kvRow.value) as StorageConfigRow\n return buildConfigFromRow({ ...parsed, scope })\n } catch { /* fall through to env vars */ }\n }\n\n // Fallback a env vars (usa config por defecto según driver esperado)\n const isS3Scope = scope.includes('s3')\n return buildConfigFromEnv(isS3Scope ? 's3' : 'filesystem')\n}\n\n/**\n * Builds StorageConfig from a DB record\n */\nfunction buildConfigFromRow(row: StorageConfigRow): StorageConfig {\n const metadata = row.metadata ? JSON.parse(row.metadata) : {}\n\n const config: StorageConfig = {\n scope: row.scope,\n driver: row.driver,\n basePath: '',\n baseUrl: row.base_url || undefined,\n maxFileSize: row.max_file_size,\n allowedMimeTypes: row.allowed_mime_types?.split(',').map(t => t.trim()),\n generateId: generateIdFn!\n }\n\n if (row.driver === 'filesystem') {\n const fsMeta = metadata as FilesystemMetadata\n config.basePath = resolveStoragePath(fsMeta.basePath || './storage', projectPath)\n\n // Crear directorio si no existe\n if (!existsSync(config.basePath)) {\n mkdirSync(config.basePath, { recursive: true })\n }\n } else if (row.driver === 's3') {\n const s3Meta = metadata as S3Metadata\n config.basePath = '' // No aplica para S3\n config.s3 = {\n bucket: s3Meta.bucket,\n region: s3Meta.region,\n accessKeyId: s3Meta.accessKeyId,\n secretAccessKey: s3Meta.secretAccessKey,\n endpoint: s3Meta.endpoint\n }\n }\n\n return config\n}\n\n/**\n * Builds StorageConfig from environment variables\n */\nfunction buildConfigFromEnv(driver: 'filesystem' | 's3'): StorageConfig {\n // Derive baseUrl: STORAGE_URL > BACKEND_URL/api/v1/storage > undefined\n const storageUrl = process.env['STORAGE_URL']\n const backendUrl = process.env['BACKEND_URL']\n const baseUrl = storageUrl || (backendUrl ? `${backendUrl}/api/v1/storage` : undefined)\n\n const config: StorageConfig = {\n scope: getDefaultScope(driver),\n driver,\n basePath: '',\n baseUrl,\n maxFileSize: parseInt(process.env['STORAGE_MAX_SIZE'] || '') || DEFAULT_MAX_SIZE,\n allowedMimeTypes: process.env['STORAGE_ALLOWED_TYPES']?.split(',').map(t => t.trim()),\n generateId: generateIdFn!\n }\n\n if (driver === 'filesystem') {\n const rawPath = process.env['STORAGE_PATH'] || './storage'\n config.basePath = resolveStoragePath(rawPath, projectPath)\n\n if (!existsSync(config.basePath)) {\n mkdirSync(config.basePath, { recursive: true })\n }\n } else if (driver === 's3') {\n const bucket = process.env['S3_BUCKET']\n const region = process.env['S3_REGION']\n const accessKeyId = process.env['S3_ACCESS_KEY']\n const secretAccessKey = process.env['S3_SECRET_KEY']\n\n if (!bucket || !region || !accessKeyId || !secretAccessKey) {\n throw new Error('S3 configuration incomplete. Required: S3_BUCKET, S3_REGION, S3_ACCESS_KEY, S3_SECRET_KEY')\n }\n\n config.s3 = {\n bucket,\n region,\n accessKeyId,\n secretAccessKey,\n endpoint: process.env['S3_ENDPOINT']\n }\n }\n\n return config\n}\n\nexport interface InitStorageConfigOptions {\n projectPath?: string\n generateId: () => string\n}\n\n/**\n * Initializes storage configuration (default singleton).\n * Uses 'default_filesystem' scope from DB if it exists, otherwise env vars.\n */\nexport function initStorageConfig(options: InitStorageConfigOptions): StorageConfig {\n projectPath = options.projectPath ?? process.cwd()\n generateIdFn = options.generateId\n\n // Config por defecto desde env vars (sync, para init rápido)\n const driver = (process.env['STORAGE_DRIVER'] as 'filesystem' | 's3') || 'filesystem'\n defaultConfig = buildConfigFromEnv(driver)\n\n return defaultConfig\n}\n","/**\n * Storage Service\n *\n * File management service with thumbnail support.\n * Supports multiple storage configurations (scopes) from the DB.\n */\n\nimport type { Logger } from 'pino'\nimport type { Knex } from 'knex'\nimport { basename } from 'node:path'\nimport { createHash } from 'node:crypto'\nimport sharp from 'sharp'\nimport type { StorageDriver, StorageFile } from './drivers/driver.interface.js'\nimport { FilesystemDriver } from './drivers/filesystem.driver.js'\nimport { S3Driver } from './drivers/s3.driver.js'\nimport { initStorageConfig, getConfigByScope, type StorageConfig } from './storage.config.js'\nimport type { UploadedFile, UploadOptions, StorageFileRecord } from './storage.types.js'\nimport type { LoggerReporter, ModuleContext } from '@gzl10/nexus-sdk'\n\nconst TABLE = 'storage_files'\n\n/**\n * Sanitizes a filename to prevent path traversal and other attacks.\n * - Removes path traversal sequences (../, ..\\)\n * - Removes null bytes\n * - Strips dangerous characters\n * - Falls back to 'file' if result is empty\n */\nfunction sanitizeFilename(filename: string): string {\n // Get only the base name (removes directory parts)\n let safe = basename(filename)\n\n // Remove null bytes (can bypass extension checks)\n // eslint-disable-next-line no-control-regex\n safe = safe.replace(/\\x00/g, '')\n\n // Remove path traversal attempts that might remain\n safe = safe.replace(/\\.\\./g, '')\n\n // Remove potentially dangerous characters for filesystems\n safe = safe.replace(/[<>:\"|?*]/g, '')\n\n // Trim whitespace and dots from start/end\n safe = safe.replace(/^[\\s.]+|[\\s.]+$/g, '')\n\n // If empty after sanitization, use default name\n if (!safe || safe.length === 0) {\n safe = 'file'\n }\n\n return safe\n}\n\nlet storageServiceInstance: StorageService | null = null\n\n/**\n * Gets the storage service instance\n */\nexport function getStorageService(): StorageService {\n if (!storageServiceInstance) {\n throw new Error('StorageService not initialized. Call initStorageService() first.')\n }\n return storageServiceInstance\n}\n\nexport interface InitStorageServiceOptions {\n db: Knex\n logger: Logger\n generateId: () => string\n errors: ModuleContext['core']['errors']\n nowTimestamp: (db: Knex) => string\n loggerService?: LoggerReporter\n projectPath?: string\n}\n\n/**\n * Initializes the storage service\n */\nexport function initStorageService(options: InitStorageServiceOptions): StorageService {\n const { db, logger, generateId, errors, nowTimestamp, loggerService, projectPath } = options\n const config = initStorageConfig({ projectPath, generateId })\n storageServiceInstance = new StorageService(db, config, logger, errors, nowTimestamp, loggerService)\n return storageServiceInstance\n}\n\nexport class StorageService {\n private driver: StorageDriver\n private config: StorageConfig\n private db: Knex\n private logger: Logger\n private errors: ModuleContext['core']['errors']\n private nowTimestamp: (db: Knex) => string\n private loggerService?: LoggerReporter\n /** Driver cache by scope */\n private driverCache: Map<string, StorageDriver> = new Map()\n\n constructor(db: Knex, config: StorageConfig, logger: Logger, errors: ModuleContext['core']['errors'], nowTimestamp: (db: Knex) => string, loggerService?: LoggerReporter) {\n this.db = db\n this.config = config\n this.logger = logger.child({ service: 'storage' })\n this.errors = errors\n this.nowTimestamp = nowTimestamp\n this.loggerService = loggerService\n\n // Inicializar driver por defecto según configuración\n this.driver = this.createDriver(config)\n this.logger.debug({ driver: config.driver }, 'Storage driver initialized')\n }\n\n /**\n * Creates a driver based on configuration\n */\n private createDriver(config: StorageConfig): StorageDriver {\n switch (config.driver) {\n case 's3':\n return new S3Driver(config)\n case 'filesystem':\n default:\n return new FilesystemDriver(config)\n }\n }\n\n /**\n * Gets a driver by scope (with cache)\n */\n private async getDriverByScope(scope: string): Promise<{ driver: StorageDriver; config: StorageConfig }> {\n // Revisar cache primero\n const cached = this.driverCache.get(scope)\n if (cached) {\n // Obtener config para validaciones\n const config = await getConfigByScope(this.db, scope)\n return { driver: cached, config }\n }\n\n // Obtener config desde BD\n const config = await getConfigByScope(this.db, scope)\n const driver = this.createDriver(config)\n\n // Guardar en cache\n this.driverCache.set(scope, driver)\n this.logger.debug({ scope, driver: config.driver }, 'Driver created for scope')\n\n return { driver, config }\n }\n\n /**\n * Uploads a file\n */\n async upload(file: UploadedFile, options?: UploadOptions): Promise<StorageFileRecord> {\n // Obtener driver y config según scope (o usar defaults)\n let driver = this.driver\n let config = this.config\n\n if (options?.scope) {\n const scoped = await this.getDriverByScope(options.scope)\n driver = scoped.driver\n config = scoped.config\n }\n\n // Validar tamaño\n if (file.size > config.maxFileSize) {\n throw new this.errors.ValidationError(`File size ${file.size} exceeds limit ${config.maxFileSize}`)\n }\n\n // Validar tipo MIME\n if (config.allowedMimeTypes?.length) {\n const allowed = config.allowedMimeTypes.some(type => {\n if (type.endsWith('/*')) {\n return file.mimetype.startsWith(type.slice(0, -1))\n }\n return type === file.mimetype\n })\n if (!allowed) {\n throw new this.errors.ValidationError(`File type ${file.mimetype} not allowed`)\n }\n }\n\n // Sanitize filename to prevent path traversal and other attacks\n const safeFilename = sanitizeFilename(file.originalname)\n\n // Deduplication: check if file with same hash already exists\n if (options?.dedupe) {\n const hash = createHash('sha256').update(file.buffer).digest('hex')\n const existing = await this.db(TABLE)\n .select('*')\n .where({ hash })\n .first()\n\n if (existing) {\n this.logger.info({ id: existing.id, hash }, 'File deduplicated - using existing record')\n return this.mapRecord(existing)\n }\n }\n\n // Guardar archivo principal\n const storageFile = await driver.put(file.buffer, safeFilename, {\n folder: options?.folder,\n mimetype: file.mimetype\n })\n\n // Generar thumbnails si es imagen y se solicitan\n let thumbnailPath: string | undefined\n if (options?.thumbnails?.length && this.isImage(file.mimetype)) {\n thumbnailPath = await this.generateThumbnails(\n file.buffer,\n storageFile,\n options.thumbnails,\n options.folder,\n driver\n )\n }\n\n // Guardar metadatos en BD\n const now = this.nowTimestamp(this.db)\n\n // Always store relative URL (host-independent)\n const fileUrl = `/api/v1/storage/files/${storageFile.id}`\n\n const record: StorageFileRecord = {\n ...storageFile,\n url: fileUrl,\n created_at: now,\n updated_at: now,\n created_by: options?.userId || null,\n updated_by: options?.userId || null\n }\n\n // isPublic defaults to false unless explicitly set\n const isPublic = options?.isPublic ?? false\n\n await this.db(TABLE).insert({\n id: record.id,\n filename: record.filename,\n disk_filename: record.diskFilename,\n mimetype: record.mimetype,\n size: record.size,\n folder: record.folder || null,\n scope: config.scope,\n path: record.path,\n url: record.url || null,\n thumbnail_path: thumbnailPath || null,\n hash: record.hash || null,\n is_public: isPublic,\n metadata: null,\n created_at: now,\n updated_at: now,\n created_by: record.created_by,\n updated_by: record.updated_by\n })\n\n this.logger.info({ id: record.id, filename: record.filename, size: record.size }, 'File uploaded')\n\n // Calculate thumbnail_url from thumbnail_path (derived data, not stored)\n const thumbnailUrl = thumbnailPath ? `/api/v1/storage/files/thumbnail/${record.id}` : undefined\n\n return {\n ...record,\n url: record.url,\n thumbnail_path: thumbnailPath,\n thumbnail_url: thumbnailUrl\n } as StorageFileRecord & { thumbnail_path?: string; thumbnail_url?: string }\n }\n\n /**\n * Generates thumbnails for an image\n */\n private async generateThumbnails(\n buffer: Buffer,\n originalFile: StorageFile,\n sizes: Array<{ width: number; height: number }>,\n folder: string | undefined,\n driver: StorageDriver\n ): Promise<string | undefined> {\n // Generar el primer thumbnail como principal\n const firstSize = sizes[0]\n if (!firstSize) return undefined\n\n try {\n const thumbnailBuffer = await sharp(buffer)\n .resize(firstSize.width, firstSize.height, {\n fit: 'cover',\n position: 'center'\n })\n .jpeg({ quality: 80 })\n .toBuffer()\n\n const thumbnailFolder = folder\n ? `${folder}/thumbnails/${firstSize.width}x${firstSize.height}`\n : `thumbnails/${firstSize.width}x${firstSize.height}`\n\n const thumbnail = await driver.put(\n thumbnailBuffer,\n originalFile.filename.replace(/\\.[^.]+$/, '.jpg'),\n { folder: thumbnailFolder, mimetype: 'image/jpeg' }\n )\n\n this.logger.debug(\n { originalId: originalFile.id, size: `${firstSize.width}x${firstSize.height}` },\n 'Thumbnail generated'\n )\n\n // Generar thumbnails adicionales en background (no bloqueante)\n if (sizes.length > 1) {\n this.generateAdditionalThumbnails(buffer, originalFile, sizes.slice(1), folder, driver)\n .catch(err => {\n const error = err instanceof Error ? err : new Error('Error generating additional thumbnails')\n this.logger.error({ err }, 'Error generating additional thumbnails')\n this.loggerService?.captureException(error, { service: 'storage', action: 'thumbnail', originalId: originalFile.id })\n })\n }\n\n // Return the thumbnail path (physical path in storage)\n return thumbnail.path\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Error generating thumbnail')\n this.logger.error({ err, originalId: originalFile.id }, 'Error generating thumbnail')\n this.loggerService?.captureException(error, { service: 'storage', action: 'thumbnail', originalId: originalFile.id })\n return undefined\n }\n }\n\n /**\n * Generates additional thumbnails in the background\n */\n private async generateAdditionalThumbnails(\n buffer: Buffer,\n originalFile: StorageFile,\n sizes: Array<{ width: number; height: number }>,\n folder: string | undefined,\n driver: StorageDriver\n ): Promise<void> {\n for (const size of sizes) {\n try {\n const thumbnailBuffer = await sharp(buffer)\n .resize(size.width, size.height, {\n fit: 'cover',\n position: 'center'\n })\n .jpeg({ quality: 80 })\n .toBuffer()\n\n const thumbnailFolder = folder\n ? `${folder}/thumbnails/${size.width}x${size.height}`\n : `thumbnails/${size.width}x${size.height}`\n\n await driver.put(\n thumbnailBuffer,\n originalFile.filename.replace(/\\.[^.]+$/, '.jpg'),\n { folder: thumbnailFolder, mimetype: 'image/jpeg' }\n )\n\n this.logger.debug(\n { originalId: originalFile.id, size: `${size.width}x${size.height}` },\n 'Additional thumbnail generated'\n )\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Error generating additional thumbnail')\n this.logger.error({ err, size }, 'Error generating additional thumbnail')\n this.loggerService?.captureException(error, { service: 'storage', action: 'thumbnail', size })\n }\n }\n }\n\n /**\n * Checks whether a mimetype is an image\n */\n private isImage(mimetype: string): boolean {\n return mimetype.startsWith('image/')\n }\n\n /**\n * Gets a file by ID\n */\n async getById(id: string): Promise<StorageFileRecord | null> {\n const record = await this.db(TABLE).where('id', id).first()\n if (!record) return null\n\n return this.mapRecord(record)\n }\n\n /**\n * Gets multiple files by IDs\n */\n async getByIds(ids: string[]): Promise<StorageFileRecord[]> {\n if (ids.length === 0) return []\n\n const records = await this.db(TABLE).whereIn('id', ids)\n return records.map((r: unknown) => this.mapRecord(r))\n }\n\n /**\n * Gets the appropriate driver for a scope.\n * If the scope matches the default, uses this.driver.\n */\n private async getDriverForScope(scope: string | undefined): Promise<StorageDriver> {\n // Si no hay scope o es el scope por defecto, usar driver default\n if (!scope || scope === this.config.scope) {\n return this.driver\n }\n return (await this.getDriverByScope(scope)).driver\n }\n\n /**\n * Gets a file stream\n */\n async getStream(id: string): Promise<NodeJS.ReadableStream> {\n const record = await this.getById(id)\n if (!record) {\n throw new this.errors.NotFoundError(`File not found: ${id}`)\n }\n\n const driver = await this.getDriverForScope(record.scope)\n return driver.get(record.path)\n }\n\n /**\n * Gets a file buffer\n */\n async getBuffer(id: string): Promise<Buffer> {\n const record = await this.getById(id)\n if (!record) {\n throw new this.errors.NotFoundError(`File not found: ${id}`)\n }\n\n const driver = await this.getDriverForScope(record.scope)\n return driver.getBuffer(record.path)\n }\n\n /**\n * Gets a file buffer by path (for thumbnails and other non-DB files)\n */\n async getBufferByPath(path: string): Promise<Buffer> {\n return this.driver.getBuffer(path)\n }\n\n /**\n * Deletes a file record.\n * If other records reference the same physical file (by hash),\n * only removes the DB record but keeps the physical file.\n */\n async delete(id: string): Promise<void> {\n const record = await this.getById(id)\n if (!record) return\n\n // Delete the DB record first\n await this.db(TABLE).where('id', id).delete()\n\n // Check if other records share the same hash (deduplicated file)\n if (record.hash) {\n const otherRefs = await this.db(TABLE)\n .where('hash', record.hash)\n .count('* as count')\n .first<{ count: number | string }>()\n\n if (Number(otherRefs?.count) > 0) {\n this.logger.info(\n { id, filename: record.filename, hash: record.hash, otherRefs: otherRefs?.count },\n 'File record removed (physical file preserved - other references exist)'\n )\n return\n }\n }\n\n // No other references, delete physical file\n const driver = await this.getDriverForScope(record.scope)\n await driver.delete(record.path)\n\n this.logger.info({ id, filename: record.filename }, 'File deleted')\n }\n\n /**\n * Lists files with pagination\n */\n async findAll(params: {\n page: number\n limit: number\n folder?: string\n mimetype?: string\n }): Promise<{\n items: StorageFileRecord[]\n total: number\n page: number\n limit: number\n totalPages: number\n hasNext: boolean\n }> {\n const { page, limit, folder, mimetype } = params\n const offset = (page - 1) * limit\n\n let query = this.db(TABLE)\n\n if (folder) {\n query = query.where('folder', folder)\n }\n if (mimetype) {\n query = query.where('mimetype', 'like', `${mimetype}%`)\n }\n\n const [countResult, items] = await Promise.all([\n query.clone().count('* as count').first<{ count: string | number }>(),\n query.clone().orderBy('created_at', 'desc').limit(limit).offset(offset)\n ])\n\n const total = Number(countResult?.count ?? 0)\n const totalPages = Math.ceil(total / limit)\n\n return {\n items: items.map((r: unknown) => this.mapRecord(r)),\n total,\n page,\n limit,\n totalPages,\n hasNext: page < totalPages\n }\n }\n\n // === GDPR / Cross-module isolation methods ===\n\n /**\n * Delete all files created by a user (GDPR Art. 17)\n * Deletes DB records and physical files (respecting dedup)\n */\n async deleteUserFiles(userId: string): Promise<number> {\n const files = await this.db(TABLE)\n .select('id')\n .where({ created_by: userId })\n\n let count = 0\n for (const file of files) {\n await this.delete(file.id)\n count++\n }\n return count\n }\n\n /**\n * Get user file metadata for GDPR export (Art. 15)\n */\n async getUserFiles(userId: string): Promise<Array<{ id: string; filename: string; mimetype: string; size: number; created_at: string }>> {\n return this.db(TABLE)\n .select('id', 'filename', 'mimetype', 'size', 'created_at')\n .where({ created_by: userId })\n .orderBy('created_at', 'desc')\n }\n\n /**\n * Maps a DB record to StorageFileRecord\n */\n private mapRecord(record: unknown): StorageFileRecord & { thumbnail_path?: string; thumbnail_url?: string } {\n const r = record as Record<string, unknown>\n const id = r['id'] as string\n const thumbnailPath = r['thumbnail_path'] as string | undefined\n\n return {\n id,\n filename: r['filename'] as string,\n diskFilename: r['disk_filename'] as string,\n mimetype: r['mimetype'] as string,\n size: r['size'] as number,\n folder: r['folder'] as string | undefined,\n scope: r['scope'] as string | undefined,\n path: r['path'] as string,\n url: r['url'] as string | undefined,\n hash: r['hash'] as string | undefined,\n created_at: r['created_at'] as string,\n updated_at: r['updated_at'] as string,\n created_by: r['created_by'] as string | null,\n updated_by: r['updated_by'] as string | null,\n thumbnail_path: thumbnailPath,\n // Calculate thumbnail_url from thumbnail_path (derived, not stored in DB)\n thumbnail_url: thumbnailPath ? `/api/v1/storage/files/thumbnail/${id}` : undefined\n }\n }\n}\n","import type { CollectionEntityDefinition, SingleEntityDefinition, ModuleContext, Request, Response, AuthRequest } from '@gzl10/nexus-sdk'\nimport { useIdField, useTextField, useSelectField, useUrlField, useNumberField, useCheckboxField, useJsonField, useMetadataField, usePublicField } from '@gzl10/nexus-sdk/fields'\nimport { getStorageService } from './storage.service.js'\n\n// ============================================================================\n// CONFIG ENTITY\n// ============================================================================\n\nconst DEFAULT_MAX_SIZE = 10 * 1024 * 1024 // 10MB\n\n/**\n * Config Entity: storage_config\n *\n * Common fields + JSON metadata for driver specialization.\n */\nexport const storageConfigEntity: SingleEntityDefinition = {\n type: 'config',\n key: 'storage_config',\n label: { en: 'Storage Config', es: 'Configuración de almacenamiento' },\n routePrefix: '/config',\n scopeField: 'scope',\n timestamps: true,\n\n // Defaults desde variables de entorno\n get defaults() {\n return {\n driver: process.env['STORAGE_DRIVER'] || 'filesystem',\n base_url: process.env['STORAGE_URL'] || '',\n max_file_size: parseInt(process.env['STORAGE_MAX_SIZE'] || '') || DEFAULT_MAX_SIZE,\n allowed_mime_types: process.env['STORAGE_ALLOWED_TYPES'] || '',\n metadata: {}\n }\n },\n\n fields: {\n id: useIdField(),\n scope: useTextField({\n label: { en: 'Scope', es: 'Ámbito' },\n disabled: true,\n size: 100,\n nullable: false,\n unique: true,\n hint: { en: 'Unique identifier (e.g. default_filesystem, default_s3)', es: 'Identificador único (ej: default_filesystem, default_s3)' }\n }),\n driver: {\n ...useSelectField({\n label: { en: 'Driver', es: 'Controlador' },\n required: true,\n hint: { en: 'Storage backend', es: 'Backend de almacenamiento' },\n options: [\n { value: 'filesystem', label: { en: 'Filesystem (local)', es: 'Sistema de archivos (local)' } },\n { value: 's3', label: { en: 'S3 (AWS/MinIO)', es: 'S3 (AWS/MinIO)' } }\n ]\n }),\n validation: { enum: ['filesystem', 's3'] }\n },\n base_url: useUrlField({\n label: { en: 'Base URL', es: 'URL base' },\n hint: { en: 'Public URL to generate file links', es: 'URL pública para generar enlaces a archivos' },\n size: 500,\n nullable: true\n }),\n max_file_size: useNumberField({\n label: { en: 'Max File Size (bytes)', es: 'Tamaño máximo de archivo (bytes)' },\n hint: { en: 'Maximum size in bytes (default: 10MB = 10485760)', es: 'Tamaño máximo en bytes (default: 10MB = 10485760)' },\n nullable: false,\n displayProps: { format: \"bytes\" }\n }),\n allowed_mime_types: useTextField({\n label: { en: 'Allowed MIME Types', es: 'Tipos MIME permitidos' },\n hint: { en: 'Allowed types separated by comma (e.g. image/*,application/pdf)', es: 'Tipos permitidos separados por coma (ej: image/*,application/pdf)' },\n size: 1000,\n nullable: true\n }),\n metadata: useJsonField({\n label: { en: 'Driver Config', es: 'Configuración del controlador' },\n hint: { en: 'Driver-specific config (filesystem: basePath, s3: bucket/region/keys)', es: 'Configuración específica del driver (filesystem: basePath, s3: bucket/region/keys)' },\n nullable: true\n }),\n is_default: useCheckboxField({\n label: { en: 'Default', es: 'Predeterminado' },\n hint: { en: 'Whether this is the default storage configuration', es: 'Si esta es la configuración de almacenamiento predeterminada' }\n })\n },\n\n casl: {\n subject: 'StorageConfig',\n permissions: {\n ADMIN: { actions: ['read', 'update'] }\n }\n }\n}\n\n// ============================================================================\n// COLLECTION ENTITY\n// ============================================================================\n\n/**\n * EntityDefinition for Storage Files\n *\n * Stores metadata for files uploaded to the system.\n * Physical files are stored in the configured driver (filesystem/S3).\n */\nexport const storageFilesEntity: CollectionEntityDefinition = {\n type: \"collection\",\n realtime: 'sync',\n table: \"storage_files\",\n label: { en: \"Storage Files\", es: \"Archivos almacenados\" },\n labelPlural: { en: \"Storage Files\", es: \"Archivos almacenados\" },\n labelField: \"filename\",\n timestamps: true,\n audit: true,\n routePrefix: \"/files\",\n allowCreate: false, // Files are created via upload action only\n allowEdit: false,\n\n actions: [\n {\n key: \"download\",\n label: { en: \"Download File\", es: \"Descargar archivo\" },\n icon: \"mdi:download\",\n method: \"GET\",\n select: [\"id\", \"filename\", \"mimetype\", \"size\", \"is_public\"],\n // Allow public access, verify auth manually for private files\n skipAuth: true,\n handler: async (\n ctx: ModuleContext,\n input: unknown,\n req?: Request,\n res?: Response\n ) => {\n if (!res) {\n throw new ctx.core.errors.AppError(\"Response required for download\", 500);\n }\n\n const { _record: file } = input as {\n _record: {\n id: string;\n filename: string;\n mimetype: string;\n size: number;\n is_public: boolean;\n };\n };\n\n // If file is not public, require authentication\n if (!file.is_public) {\n const authReq = req as AuthRequest | undefined;\n if (!authReq?.user) {\n throw new ctx.core.errors.UnauthorizedError(\"AUTH_REQUIRED\");\n }\n // Check CASL permission\n if (!authReq.ability?.can(\"read\", \"StorageFile\")) {\n throw new ctx.core.errors.ForbiddenError(\"PERMISSION_DENIED\");\n }\n }\n\n const storageService = getStorageService();\n const stream = await storageService.getStream(file.id);\n\n res.setHeader(\"Content-Type\", file.mimetype);\n res.setHeader(\n \"Content-Disposition\",\n `attachment; filename=\"${encodeURIComponent(file.filename)}\"`\n );\n res.setHeader(\"Content-Length\", file.size.toString());\n if (file.is_public) {\n res.setHeader(\"Cache-Control\", \"public, max-age=86400\");\n }\n\n stream.pipe(res);\n },\n // No CASL here - handled manually in handler\n },\n {\n key: \"inline\",\n label: { en: \"View File\", es: \"Ver archivo\" },\n icon: \"mdi:eye\",\n method: \"GET\",\n select: [\"id\", \"filename\", \"mimetype\", \"size\", \"is_public\"],\n // Allow public access, verify auth manually for private files\n skipAuth: true,\n handler: async (\n ctx: ModuleContext,\n input: unknown,\n req?: Request,\n res?: Response\n ) => {\n if (!res) {\n throw new ctx.core.errors.AppError(\n \"Response required for inline view\",\n 500\n );\n }\n\n const { _record: file } = input as {\n _record: {\n id: string;\n filename: string;\n mimetype: string;\n size: number;\n is_public: boolean;\n };\n };\n\n // If file is not public, require authentication\n if (!file.is_public) {\n const authReq = req as AuthRequest | undefined;\n if (!authReq?.user) {\n throw new ctx.core.errors.UnauthorizedError(\"AUTH_REQUIRED\");\n }\n // Check CASL permission\n if (!authReq.ability?.can(\"read\", \"StorageFile\")) {\n throw new ctx.core.errors.ForbiddenError(\"PERMISSION_DENIED\");\n }\n }\n\n const storageService = getStorageService();\n const stream = await storageService.getStream(file.id);\n\n res.setHeader(\"Content-Type\", file.mimetype);\n res.setHeader(\n \"Content-Disposition\",\n `inline; filename=\"${encodeURIComponent(file.filename)}\"`\n );\n res.setHeader(\"Content-Length\", file.size.toString());\n if (file.is_public) {\n res.setHeader(\"Cache-Control\", \"public, max-age=86400\");\n }\n\n stream.pipe(res);\n },\n // No CASL here - handled manually in handler\n },\n {\n key: \"thumbnail\",\n label: { en: \"View Thumbnail\", es: \"Ver miniatura\" },\n icon: \"mdi:image\",\n method: \"GET\",\n select: [\"id\", \"filename\", \"thumbnail_path\", \"is_public\"],\n // Allow public access, verify auth manually for private files\n skipAuth: true,\n handler: async (\n ctx: ModuleContext,\n input: unknown,\n req?: Request,\n res?: Response\n ) => {\n if (!res) {\n throw new ctx.core.errors.AppError(\n \"Response required for thumbnail view\",\n 500\n );\n }\n\n const { _record: file } = input as {\n _record: { id: string; filename: string; thumbnail_path?: string; is_public: boolean };\n };\n\n // If file is not public, require authentication\n if (!file.is_public) {\n const authReq = req as AuthRequest | undefined;\n if (!authReq?.user) {\n throw new ctx.core.errors.UnauthorizedError(\"AUTH_REQUIRED\");\n }\n if (!authReq.ability?.can(\"read\", \"StorageFile\")) {\n throw new ctx.core.errors.ForbiddenError(\"PERMISSION_DENIED\");\n }\n }\n\n // If no thumbnail exists, return 404\n if (!file.thumbnail_path) {\n throw new ctx.core.errors.NotFoundError(\"Thumbnail\");\n }\n\n const storageService = getStorageService();\n const thumbnailFilename = file.filename.replace(/\\.[^.]+$/, \".jpg\");\n\n try {\n const buffer = await storageService.getBufferByPath(\n file.thumbnail_path\n );\n\n res.setHeader(\"Content-Type\", \"image/jpeg\");\n res.setHeader(\n \"Content-Disposition\",\n `inline; filename=\"${encodeURIComponent(thumbnailFilename)}\"`\n );\n res.setHeader(\"Content-Length\", buffer.length.toString());\n res.setHeader(\"Cache-Control\", \"public, max-age=31536000\"); // Cache 1 year\n\n res.send(buffer);\n } catch {\n throw new ctx.core.errors.NotFoundError(\"Thumbnail file\");\n }\n },\n // No CASL here - handled manually in handler\n },\n ],\n\n fields: {\n id: useIdField(),\n filename: useTextField({\n label: { en: \"Original Filename\", es: \"Nombre original\" },\n required: true,\n size: 255,\n nullable: false,\n meta: { sortable: true, searchable: true }\n }),\n disk_filename: useTextField({\n label: { en: \"Disk Filename\", es: \"Nombre en disco\" },\n required: true,\n hidden: true,\n size: 255,\n nullable: false\n }),\n mimetype: useTextField({\n label: { en: \"MIME Type\", es: \"Tipo MIME\" },\n required: true,\n size: 100,\n nullable: false,\n index: true,\n meta: { sortable: true, searchable: true }\n }),\n size: useNumberField({\n label: { en: \"Size\", es: \"Tamaño\" },\n required: true,\n nullable: false,\n validation: { min: 0 },\n meta: { sortable: true },\n displayProps: { format: \"bytes\" }\n }),\n folder: useTextField({\n label: { en: \"Folder\", es: \"Carpeta\" },\n size: 100,\n nullable: true,\n index: true,\n meta: { searchable: true }\n }),\n scope: useTextField({\n label: { en: \"Storage Scope\", es: \"Ámbito de almacenamiento\" },\n hidden: true,\n size: 50,\n nullable: true,\n index: true,\n hint: {\n en: \"Storage configuration scope used for this file\",\n es: \"Ámbito de configuración de almacenamiento usado para este archivo\"\n }\n }),\n path: useTextField({\n label: { en: \"Full Path\", es: \"Ruta completa\" },\n required: true,\n hidden: true,\n size: 500,\n nullable: false\n }),\n url: useUrlField({\n label: { en: \"Public URL\", es: \"URL pública\" },\n size: 500,\n nullable: true,\n meta: { exportable: true, showInDisplay: false }\n }),\n thumbnail_path: useTextField({\n label: { en: \"Thumbnail Path\", es: \"Ruta de miniatura\" },\n size: 500,\n nullable: true,\n meta: { showInDisplay: false }\n }),\n hash: useTextField({\n label: { en: \"SHA256 Hash\", es: \"Hash SHA256\" },\n hidden: true,\n size: 64,\n nullable: true,\n index: true\n }),\n is_public: usePublicField({\n hint: { en: 'Allow unauthenticated access to this file', es: 'Permitir acceso sin autenticación a este archivo' }\n }),\n metadata: useMetadataField()\n },\n\n indexes: [{ columns: [\"folder\", \"filename\"] }],\n\n casl: {\n subject: \"StorageFile\",\n permissions: {\n MANAGER: { actions: [\"read\", \"delete\"] },\n EDITOR: { actions: [\"create\", \"read\", \"delete\"], conditions: { created_by: \"${user.id}\" } },\n CONTRIBUTOR: { actions: [\"create\", \"read\"], conditions: { created_by: \"${user.id}\" } },\n USER: { actions: [\"create\", \"read\"], conditions: { created_by: \"${user.id}\" } },\n VIEWER: { actions: [\"read\"] }\n },\n },\n};\n\n// NOTE: Module-scope actions (upload, upload-image) are in ./actions/\n// NOTE: Row-scope actions (download, inline, thumbnail) are in storageFilesEntity.actions[]\n","/**\n * Storage Actions - Shared helpers\n */\n\nimport type { ModuleContext, AuthRequest } from '@gzl10/nexus-sdk'\nimport type { RequestHandler } from 'express'\nimport multer from 'multer'\nimport { getStorageConfig } from '../storage.config.js'\n\n/**\n * Creates multer middleware with standard config\n */\nexport function createUploadMiddleware(ctx: ModuleContext, options?: { imageOnly?: boolean }): RequestHandler[] {\n const config = getStorageConfig()\n const rateLimit = ctx.core.middleware.rateLimit({\n windowMs: 60 * 1000,\n max: 20,\n message: 'Too many uploads, try again in 1 minute'\n })\n\n const upload = multer({\n storage: multer.memoryStorage(),\n limits: { fileSize: config.maxFileSize },\n fileFilter: (_req, file, cb) => {\n // Reject filenames with null bytes (can bypass security checks)\n if (file.originalname.includes('\\x00')) {\n cb(new Error('Invalid filename: contains null byte'))\n return\n }\n // Only accept images if imageOnly is true\n if (options?.imageOnly && !file.mimetype.startsWith('image/')) {\n cb(new Error('Only image files are allowed'))\n return\n }\n cb(null, true)\n }\n })\n\n return [rateLimit, upload.single('file')]\n}\n\n/**\n * Extracts upload options from request body\n */\nexport function extractUploadOptions(ctx: ModuleContext, req: AuthRequest) {\n const folder = req.body?.folder as string | undefined\n const scope = req.body?.scope as string | undefined\n const thumbnailsRaw = req.body?.thumbnails as string | undefined\n const thumbnails: Array<{ width: number; height: number }> | undefined = thumbnailsRaw\n ? ctx.core.safeJsonParse<Array<{ width: number; height: number }> | undefined>(\n thumbnailsRaw,\n undefined,\n { context: 'upload.thumbnails' }\n )\n : undefined\n const dedupe = req.body?.dedupe === 'true' || req.body?.dedupe === true\n const isPublic = req.body?.isPublic === 'true' || req.body?.isPublic === true\n\n return { folder, scope, thumbnails, dedupe, isPublic, userId: req.user?.id }\n}\n","/**\n * POST /storage/upload - Upload a file\n *\n * Accepts multipart/form-data with:\n * - file: The file to upload (required)\n * - folder: Optional folder path\n * - scope: Storage config scope\n * - thumbnails: JSON array of thumbnail sizes\n * - dedupe: Enable deduplication by hash\n * - isPublic: Mark file as publicly accessible\n */\n\nimport type { ActionDefinition, AuthRequest } from '@gzl10/nexus-sdk'\nimport { getStorageService } from '../storage.service.js'\nimport type { UploadedFile } from '../storage.types.js'\nimport { createUploadMiddleware, extractUploadOptions } from './helpers.js'\n\nexport const uploadAction: ActionDefinition = {\n key: 'upload',\n scope: 'module',\n group: { en: 'File Management', es: 'Gestión de archivos' },\n label: { en: 'Upload File', es: 'Cargar archivo' },\n icon: 'mdi:upload',\n successStatus: 201,\n output: {},\n\n input: {\n file: {\n label: { en: 'File', es: 'Archivo' },\n input: 'file',\n required: true\n },\n folder: {\n label: { en: 'Folder', es: 'Carpeta' },\n input: 'text',\n hint: { en: 'Optional folder path', es: 'Ruta de carpeta opcional' }\n },\n scope: {\n label: { en: 'Storage Scope', es: 'Ámbito de almacenamiento' },\n input: 'select',\n hint: { en: 'Storage config scope (default: uses default config)', es: 'Ámbito de configuración de almacenamiento (default: usa la configuración por defecto)' },\n options: { endpoint: '/storage/config', valueField: 'scope', labelField: 'scope' }\n },\n thumbnails: {\n label: { en: 'Thumbnails', es: 'Miniaturas' },\n input: 'json',\n hint: { en: 'Thumbnail sizes to generate (JSON array)', es: 'Tamaños de miniatura a generar (matriz JSON)' }\n },\n isPublic: {\n label: { en: 'Public', es: 'Público' },\n input: 'checkbox',\n hint: { en: 'Allow unauthenticated access to this file', es: 'Permitir acceso sin autenticación' }\n }\n },\n\n middleware: (ctx) => createUploadMiddleware(ctx),\n\n handler: async (ctx, _input, req, res) => {\n if (!req || !res) {\n throw new ctx.core.errors.AppError('Request and response required for upload', 500)\n }\n\n const authReq = req as AuthRequest\n const file = authReq.file as UploadedFile | undefined\n\n if (!file) {\n res.status(400).json({ error: 'No file provided' })\n return\n }\n\n const options = extractUploadOptions(ctx, authReq)\n const storageService = getStorageService()\n\n const result = await storageService.upload(\n {\n fieldname: file.fieldname,\n originalname: file.originalname,\n mimetype: file.mimetype,\n buffer: file.buffer,\n size: file.size\n },\n options\n )\n\n return result\n },\n\n casl: {\n subject: 'StorageFile',\n permissions: {\n MANAGER: { actions: ['execute'] },\n EDITOR: { actions: ['execute'] },\n CONTRIBUTOR: { actions: ['execute'] },\n USER: { actions: ['execute'] }\n }\n }\n}\n","/**\n * POST /storage/upload-image - Upload an image with automatic thumbnail generation\n *\n * Accepts multipart/form-data with:\n * - file: The image file to upload (required, image/* only)\n * - folder: Optional folder path\n * - scope: Storage config scope\n * - dedupe: Enable deduplication by hash\n * - isPublic: Mark file as publicly accessible\n *\n * Automatically generates a 256x256 thumbnail.\n */\n\nimport type { ActionDefinition, AuthRequest } from '@gzl10/nexus-sdk'\nimport { getStorageService } from '../storage.service.js'\nimport type { UploadedFile } from '../storage.types.js'\nimport { createUploadMiddleware, extractUploadOptions } from './helpers.js'\n\nexport const uploadImageAction: ActionDefinition = {\n key: 'upload-image',\n scope: 'module',\n group: { en: 'File Management', es: 'Gestión de archivos' },\n label: { en: 'Upload Image', es: 'Cargar imagen' },\n icon: 'mdi:image-plus',\n successStatus: 201,\n output: {},\n\n input: {\n file: {\n label: { en: 'Image', es: 'Imagen' },\n input: 'image',\n required: true\n },\n folder: {\n label: { en: 'Folder', es: 'Carpeta' },\n input: 'text',\n hint: { en: 'Optional folder path', es: 'Ruta de carpeta opcional' }\n },\n scope: {\n label: { en: 'Storage Scope', es: 'Ámbito de almacenamiento' },\n input: 'select',\n hint: { en: 'Storage config scope (default: uses default config)', es: 'Ámbito de configuración de almacenamiento (default: usa la configuración por defecto)' },\n options: { endpoint: '/storage/config', valueField: 'scope', labelField: 'scope' }\n },\n isPublic: {\n label: { en: 'Public', es: 'Público' },\n input: 'checkbox',\n hint: { en: 'Allow unauthenticated access to this file', es: 'Permitir acceso sin autenticación' }\n }\n },\n\n middleware: (ctx) => createUploadMiddleware(ctx, { imageOnly: true }),\n\n handler: async (ctx, _input, req, res) => {\n if (!req || !res) {\n throw new ctx.core.errors.AppError('Request and response required for upload', 500)\n }\n\n const authReq = req as AuthRequest\n const file = authReq.file as UploadedFile | undefined\n\n if (!file) {\n res.status(400).json({ error: 'No image provided' })\n return\n }\n\n const { folder, scope, dedupe, isPublic, userId } = extractUploadOptions(ctx, authReq)\n\n // Default thumbnail sizes for images\n const thumbnails = [{ width: 256, height: 256 }]\n\n const storageService = getStorageService()\n const result = await storageService.upload(\n {\n fieldname: file.fieldname,\n originalname: file.originalname,\n mimetype: file.mimetype,\n buffer: file.buffer,\n size: file.size\n },\n { folder, scope, userId, thumbnails, dedupe, isPublic }\n )\n\n return result\n },\n\n casl: {\n subject: 'StorageFile',\n permissions: {\n MANAGER: { actions: ['execute'] },\n EDITOR: { actions: ['execute'] },\n CONTRIBUTOR: { actions: ['execute'] },\n USER: { actions: ['execute'] }\n }\n }\n}\n","/**\n * Storage Actions - File upload endpoints\n *\n * Module-scope actions:\n * - upload: General file upload with optional thumbnails\n * - upload-image: Image upload with automatic 256x256 thumbnail\n */\n\nimport type { ActionDefinition } from '@gzl10/nexus-sdk'\n\n// Individual exports\nexport { uploadAction } from './upload.action.js'\nexport { uploadImageAction } from './upload-image.action.js'\n\n// Import for array\nimport { uploadAction } from './upload.action.js'\nimport { uploadImageAction } from './upload-image.action.js'\n\n/** All storage actions typed for module actions array */\nexport const storageActions: ActionDefinition[] = [\n uploadAction,\n uploadImageAction\n]\n","/**\n * Storage Routes\n *\n * Rutas adicionales para storage (upload multiple).\n * Las rutas principales (upload, download, inline) se manejan via EntityActions.\n */\n\nimport type { ModuleContext, Request, Response, AuthRequest } from '@gzl10/nexus-sdk'\nimport multer from 'multer'\nimport { getStorageConfig } from './storage.config.js'\nimport { getStorageService } from './storage.service.js'\nimport type { UploadedFile } from './storage.types.js'\nimport { storageFilesEntity, storageConfigEntity } from './storage.entity.js'\n\nexport function createStorageRoutes(ctx: ModuleContext) {\n const router = ctx.core.createRouter()\n const { auth } = ctx.core.middleware\n const config = getStorageConfig()\n const { ValidationError, NotFoundError } = ctx.core.errors as any\n\n // Configurar multer para almacenamiento en memoria\n const upload = multer({\n storage: multer.memoryStorage(),\n limits: { fileSize: config.maxFileSize }\n })\n\n // ============================================================================\n // UPLOAD MULTIPLE (no hay action para esto aún)\n // ============================================================================\n\n const uploadMultiple = async (req: Request, res: Response): Promise<void> => {\n const authReq = req as AuthRequest\n const files = (req as { files?: Express.Multer.File[] }).files\n\n if (!files?.length) throw new ValidationError('VALIDATION_ERROR')\n\n const folder = req.body?.['folder'] as string | undefined\n const storageService = getStorageService()\n const results = []\n\n for (const f of files) {\n const file: UploadedFile = {\n fieldname: f.fieldname,\n originalname: f.originalname,\n mimetype: f.mimetype,\n buffer: f.buffer,\n size: f.size\n }\n const result = await storageService.upload(file, {\n folder,\n userId: authReq.user?.id\n })\n results.push(result)\n }\n\n res.status(201).json(results)\n }\n\n // Rate limit para prevenir abuso de uploads\n const uploadRateLimit = ctx.core.middleware.rateLimit({ windowMs: 60 * 1000, max: 20, message: 'Too many uploads, try again in 1 minute' })\n\n if (auth) {\n router.post('/upload/multiple', uploadRateLimit, auth, upload.array('files', 10), uploadMultiple)\n } else {\n router.post('/upload/multiple', uploadRateLimit, upload.array('files', 10), uploadMultiple)\n }\n\n // ============================================================================\n // STATS (for dashboard widgets)\n // ============================================================================\n\n router.get('/stats', auth ? [auth] : [], async (_req: Request, res: Response) => {\n const knex = ctx.db.knex\n const stats = await knex('storage_files')\n .select(\n knex.raw('COUNT(*) as total_files'),\n knex.raw('COALESCE(SUM(size), 0) as total_size'),\n knex.raw('COUNT(DISTINCT folder) as total_folders')\n )\n .first()\n\n res.json({\n total_files: Number(stats?.total_files ?? 0),\n total_size: Number(stats?.total_size ?? 0),\n total_size_mb: Math.round(Number(stats?.total_size ?? 0) / 1024 / 1024 * 100) / 100,\n total_folders: Number(stats?.total_folders ?? 0)\n })\n })\n\n router.get('/stats/by-type', auth ? [auth] : [], async (_req: Request, res: Response) => {\n const knex = ctx.db.knex\n const rows = await knex('storage_files')\n .select('mime_type')\n .count('* as count')\n .sum('size as total_size')\n .groupBy('mime_type')\n .orderBy('count', 'desc')\n .limit(20)\n\n const items = (rows as { mime_type: string; count: string | number; total_size: string | number }[]).map(r => ({\n x: r.mime_type || 'unknown',\n y: Number(r.count)\n }))\n\n res.json(items)\n })\n\n // ============================================================================\n // CRUD para storage_files (runtime)\n // Las EntityActions 'download' e 'inline' se montan automáticamente\n // ============================================================================\n\n const filesService = ctx.runtime.createEntityService(storageFilesEntity)\n const filesController = ctx.runtime.createEntityController(filesService, storageFilesEntity)\n\n // Deshabilitar create y update via API REST estándar\n // Los archivos solo se crean via upload action\n delete filesController.create\n delete filesController.update\n\n // Sobrescribir delete para eliminar también el archivo físico\n filesController.delete = async (req: Request, res: Response): Promise<void> => {\n const id = String(req.params['id'] ?? '')\n if (!id) throw new ValidationError('VALIDATION_ERROR')\n\n const storageService = getStorageService()\n const file = await storageService.getById(id)\n if (!file) throw new NotFoundError('RESOURCE_NOT_FOUND')\n\n await storageService.delete(id)\n res.status(204).send()\n }\n\n const filesRouter = ctx.runtime.createEntityRouter(filesController, storageFilesEntity)\n router.use(storageFilesEntity.routePrefix ?? '/files', filesRouter)\n\n // ============================================================================\n // CONFIG (runtime)\n // ============================================================================\n\n const configService = ctx.runtime.createEntityService(storageConfigEntity)\n const configController = ctx.runtime.createEntityController(configService, storageConfigEntity)\n const configRouter = ctx.runtime.createEntityRouter(configController, storageConfigEntity)\n router.use(storageConfigEntity.routePrefix ?? '/config', configRouter)\n\n return router\n}\n","/**\n * @module storage\n * @description File storage and management with filesystem and S3 drivers\n */\n\nimport type { ModuleManifest, ModuleContext, LoggerReporter } from '@gzl10/nexus-sdk'\nimport { storageConfigEntity, storageFilesEntity } from './storage.entity.js'\nimport { storageActions } from './actions/index.js'\nimport { initStorageService } from './storage.service.js'\nimport { createStorageRoutes } from './storage.routes.js'\n\n// Re-exports\nexport { StorageService, getStorageService } from './storage.service.js'\nexport { getStorageConfig, type StorageConfig } from './storage.config.js'\nexport type { StorageDriver, StorageFile, PutOptions } from './drivers/driver.interface.js'\nexport { FilesystemDriver } from './drivers/filesystem.driver.js'\nexport { S3Driver } from './drivers/s3.driver.js'\nexport { storageConfigEntity, storageFilesEntity } from './storage.entity.js'\nexport { storageActions, uploadAction, uploadImageAction } from './actions/index.js'\n\n// Types from storage.types.ts\nexport type {\n FilesystemMetadata,\n S3Metadata,\n UploadedFile,\n UploadOptions,\n StorageFileRecord\n} from './storage.types.js'\n\nexport const storageModule: ModuleManifest = {\n name: 'storage',\n label: { en: 'Storage', es: 'Almacenamiento' },\n icon: 'mdi:cloud-upload-outline',\n description: { en: 'File storage and management', es: 'Almacenamiento y gestión de archivos' },\n type: 'core',\n category: 'assets',\n dependencies: ['logger'],\n\n definitions: [storageConfigEntity, storageFilesEntity],\n\n actions: storageActions,\n\n pages: [\n {\n id: 'storage-dashboard',\n label: { en: 'Storage Overview', es: 'Resumen de almacenamiento' },\n icon: 'mdi:chart-pie',\n type: 'dashboard',\n order: 0,\n widgets: [\n { id: 'stats', type: 'stat', endpoint: '/storage/stats', layout: { span: 6 } },\n { id: 'by-type', type: 'chart', label: { en: 'By file type', es: 'Por tipo de archivo' }, endpoint: '/storage/stats/by-type', chart: 'donut', layout: { span: 6 } }\n ]\n }\n ],\n\n routePrefix: '/storage',\n\n init: (ctx: ModuleContext) => {\n // Inicializar y registrar el servicio de storage\n const loggerService = ctx.services.getOptional<LoggerReporter>('logger')\n const storageService = initStorageService({\n db: ctx.db.knex,\n logger: ctx.core.logger,\n generateId: ctx.core.generateId,\n errors: ctx.core.errors,\n nowTimestamp: ctx.db.nowTimestamp,\n loggerService,\n projectPath: ctx.core.getProjectPath()\n })\n ctx.services.register('storage', storageService)\n ctx.core.logger.debug('Storage service registered')\n },\n\n routes: (ctx: ModuleContext) => createStorageRoutes(ctx),\n\n // Import dinámico para evitar ciclos\n seed: async (ctx) => {\n const { seed } = await import('./storage.seed.js')\n await seed(ctx)\n }\n}\n","import type { CollectionEntityDefinition, ModuleContext } from '@gzl10/nexus-sdk'\nimport { useIdField, useSelectField, useEmailField, usePasswordField, useTextField, useDatetimeField, useCheckboxField, useImageField, useNameField, useMetadataField, useDescriptionField } from '@gzl10/nexus-sdk/fields'\nimport { z } from 'zod'\n\n/**\n * EntityDefinition for Users\n *\n * Single source of truth for:\n * - Knex migrations (generateMigration)\n * - Zod schemas (generateZodSchema)\n * - TypeScript types (generateModel)\n * - CASL permissions (generateCaslPermissions)\n */\nexport const userEntity: CollectionEntityDefinition = {\n type: \"collection\",\n realtime: 'sync',\n table: \"users\",\n label: { en: \"User\", es: \"Usuario\" },\n labelPlural: { en: \"Users\", es: \"Usuarios\" },\n labelField: \"name\",\n timestamps: true,\n audit: true,\n order: 1,\n routePrefix: \"/\", // Monta en raíz del módulo: /users/*\n\n fields: {\n id: useIdField(),\n email: useEmailField({\n label: { en: \"Email\", es: \"Correo electrónico\" },\n placeholder: { en: \"user@example.com\", es: \"usuario@ejemplo.com\" },\n nullable: true,\n unique: true,\n meta: { sortable: true, searchable: true },\n }),\n password: usePasswordField({\n label: { en: \"Password\", es: \"Contraseña\" },\n placeholder: \"••••••••\",\n nullable: true,\n validation: { min: 6 },\n meta: { exportable: false, showInDisplay: false, showInForm: \"create\" },\n }),\n name: useNameField({\n size: 255,\n placeholder: { en: 'Full name', es: 'Nombre completo' },\n validation: { min: 1 }\n }),\n metadata: useMetadataField(),\n avatar: {\n ...useImageField({\n label: { en: \"Avatar\", es: \"Avatar\" },\n nullable: true,\n meta: { exportable: false },\n }),\n displayProps: { order: 1, width: 80 },\n relation: { table: \"storage_files\", column: \"id\", onDelete: \"SET NULL\" },\n storage: {\n accept: \"image/*\",\n maxSize: 2 * 1024 * 1024,\n folder: \"avatars\",\n thumbnails: [{ width: 128, height: 128 }],\n dedupe: true,\n },\n },\n\n // GDPR/Privacy consent fields\n consent_date: useDatetimeField({\n label: { en: \"Consent Date\", es: \"Fecha de consentimiento\" },\n hidden: true,\n nullable: true,\n meta: { exportable: true, showInForm: false, showInDisplay: false },\n }),\n consent_version: useTextField({\n label: { en: \"Consent Version\", es: \"Versión de consentimiento\" },\n hidden: true,\n size: 20,\n nullable: true,\n meta: { exportable: true, showInForm: false, showInDisplay: false },\n }),\n marketing_opt_in: useCheckboxField({\n label: { en: \"Marketing Opt-in\", es: \"Aceptar marketing\" },\n meta: { exportable: true, showInForm: false, showInDisplay: false },\n }),\n locale: useSelectField({\n label: { en: \"Language\", es: \"Idioma\" },\n options: [\n { value: \"es\", label: { en: \"Spanish\", es: \"Español\" } },\n { value: \"en\", label: { en: \"English\", es: \"Inglés\" } },\n ],\n nullable: true,\n meta: { sortable: true },\n defaultValue: 'en'\n }),\n timezone: useSelectField({\n label: { en: \"Timezone\", es: \"Zona horaria\" },\n master: \"timezones\",\n meta: { sortable: true },\n defaultValue: \"timezones:Europe/Madrid\",\n }),\n type: useSelectField({\n label: { en: \"Type\", es: \"Tipo\" },\n defaultValue: \"human\",\n options: [\n {\n value: \"human\",\n label: { en: \"Human\", es: \"Humano\" },\n icon: \"mdi:account\",\n },\n { value: \"bot\", label: { en: \"Bot\", es: \"Bot\" }, icon: \"mdi:robot\" },\n {\n value: \"service\",\n label: { en: \"Service\", es: \"Servicio\" },\n icon: \"mdi:cog\",\n },\n ],\n meta: { sortable: true, searchable: false },\n }),\n // Campo virtual para gestionar roles (no va a DB, se procesa en service)\n role_ids: {\n label: { en: \"Roles\", es: \"Roles\" },\n input: \"transfer\",\n db: { type: \"array\", virtual: true },\n options: {\n endpoint: \"/users/roles\",\n valueField: \"id\",\n labelField: \"name\",\n },\n meta: { showInDisplay: true, showInForm: true },\n },\n },\n\n // Entity Actions\n actions: [\n {\n key: \"change-password\",\n label: { en: \"Change Password\", es: \"Cambiar contraseña\" },\n icon: \"mdi:lock-reset\",\n method: \"POST\",\n select: [\"id\", \"password\"],\n confirm: {\n type: \"simple\",\n message: {\n en: \"You are about to change the password for this user.\",\n es: \"Vas a cambiar la contraseña de este usuario.\",\n },\n severity: \"warning\",\n },\n input: {\n newPassword: {\n input: \"password\",\n required: true,\n label: { en: \"New Password\", es: \"Nueva contraseña\" },\n hint: { en: \"Minimum 6 characters\", es: \"Mínimo 6 caracteres\" },\n },\n currentPassword: {\n input: \"password\",\n label: { en: \"Current Password\", es: \"Contraseña actual\" },\n hint: {\n en: \"Required only when changing your own password\",\n es: \"Requerido solo al cambiar tu propia contraseña\",\n },\n },\n },\n inputSchema: z.object({\n currentPassword: z\n .string()\n .optional()\n .transform((v) => v || undefined),\n newPassword: z\n .string()\n .min(6, \"Password must be at least 6 characters\"),\n }),\n middleware: (ctx: ModuleContext) =>\n ctx.core.middleware.rateLimit({\n windowMs: 15 * 60 * 1000,\n max: 5,\n message: 'Too many attempts, try again in 15 minutes',\n }),\n handler: async (ctx: ModuleContext, input: unknown) => {\n const {\n _record: user,\n _authUserId,\n currentPassword,\n newPassword,\n } = input as {\n _record: { id: string; password: string };\n _authUserId?: string;\n currentPassword?: string;\n newPassword: string;\n };\n const { verifyPassword, hashPassword } = ctx.core.crypto;\n\n // Si el usuario cambia SU PROPIA contraseña, debe proporcionar currentPassword\n const isOwnPassword = _authUserId === user.id;\n if (isOwnPassword) {\n if (!currentPassword) {\n throw new ctx.core.errors.ValidationError(\n \"Current password is required when changing your own password\",\n );\n }\n const isValid = await verifyPassword(currentPassword, user.password);\n if (!isValid) {\n throw new ctx.core.errors.UnauthorizedError(\n \"Current password is incorrect\",\n );\n }\n }\n // Si ADMIN/OWNER cambia la de otro usuario, no se verifica currentPassword\n // (CASL ya verifica que tenga permisos de update sobre User)\n\n // Hash y actualizar\n const hashedPassword = await hashPassword(newPassword);\n await ctx.db\n .knex(\"users\")\n .where(\"id\", user.id)\n .update({\n password: hashedPassword,\n updated_at: ctx.db.nowTimestamp(ctx.db.knex),\n });\n\n return { updated: true };\n },\n casl: { action: \"change-password\" },\n },\n ],\n\n // Autorización CASL ('*' = todos los roles no superuser pueden leer/actualizar su propio perfil)\n casl: {\n subject: \"User\",\n sensitiveFields: [\"password\"],\n permissions: {\n \"*\": [\n { actions: [\"read\"], conditions: { id: \"${user.id}\" } },\n {\n actions: [\"update\"],\n conditions: { id: \"${user.id}\" },\n fields: [\"name\", \"avatar\", \"locale\", \"timezone\"],\n },\n { actions: [\"change-password\"], conditions: { id: \"${user.id}\" } },\n ],\n },\n },\n};\n\n/**\n * EntityDefinition for Roles\n */\nexport const roleEntity: CollectionEntityDefinition = {\n type: 'collection',\n realtime: 'sync',\n table: \"roles\",\n label: { en: \"Role\", es: \"Rol\" },\n labelPlural: { en: \"Roles\", es: \"Roles\" },\n labelField: \"name\",\n timestamps: true,\n order: 10,\n routePrefix: \"/roles\", // Monta en /users/roles/*\n\n fields: {\n id: useIdField(),\n name: useNameField({\n size: 50,\n disabled: true,\n placeholder: \"ROLE_NAME\",\n unique: true,\n validation: { min: 2, max: 50, pattern: \"^[A-Z_]+$\" }\n }),\n description: useDescriptionField(),\n is_system: useCheckboxField({\n label: { en: \"System\", es: \"Sistema\" },\n disabled: true,\n meta: { sortable: true }\n })\n },\n\n casl: {\n subject: \"Role\",\n permissions: {\n '*': [{ actions: ['read'] }]\n },\n },\n};\n\n/**\n * EntityDefinition for User Roles (pivote many-to-many)\n */\nexport const userRoleEntity: CollectionEntityDefinition = {\n type: 'collection',\n realtime: 'sync',\n table: 'user_roles',\n label: { en: 'User Role', es: 'Rol de Usuario' },\n labelPlural: { en: 'User Roles', es: 'Roles de Usuario' },\n labelField: 'role_id',\n timestamps: true,\n order: 15,\n hidden: true, // Pivot table - managed via Users UI\n expose: false,\n\n fields: {\n id: useIdField(),\n user_id: useSelectField({\n label: { en: 'User', es: 'Usuario' },\n required: true,\n table: 'users',\n column: 'id',\n onDelete: 'CASCADE',\n endpoint: '/users',\n valueField: 'id',\n labelField: 'name',\n meta: { searchable: true }\n }),\n role_id: useSelectField({\n label: { en: 'Role', es: 'Rol' },\n required: true,\n table: 'roles',\n column: 'id',\n onDelete: 'CASCADE',\n endpoint: '/users/roles',\n valueField: 'id',\n labelField: 'name',\n meta: { searchable: true }\n })\n },\n\n indexes: [\n { columns: ['user_id', 'role_id'], unique: true }\n ],\n\n casl: {\n subject: 'UserRole',\n permissions: {\n // Solo ADMIN/OWNER pueden gestionar asignaciones (via manage all global)\n }\n }\n}\n","/**\n * Users Routes - Uses runtime with custom services\n *\n * Estructura:\n * - /users/* → CRUD Users (runtime auto-generado)\n * - /users/roles/* → CRUD Roles (runtime auto-generado)\n * - /users/change-password/:id → Change user password (entity action)\n *\n * Note: Permissions are now static in entity definitions (casl.permissions).\n */\n\nimport type { Request, Response, ModuleContext, Router, AuthRequest } from '@gzl10/nexus-sdk'\nimport { z } from 'zod'\nimport { userEntity, roleEntity } from './users.entity.js'\nimport type { UsersService } from './users.types.js'\n\n// GDPR Consent schema\nexport const consentSchema = z.object({\n version: z.string().min(1).max(20),\n marketingOptIn: z.boolean().default(false)\n})\n\n// ============================================================================\n// Routes Factory\n// ============================================================================\n\nexport function createUsersRoutes(ctx: ModuleContext): Router {\n const router = ctx.core.createRouter()\n const { auth, validate } = ctx.core.middleware\n const { ForbiddenError: _CASLForbiddenError } = ctx.core.abilities\n\n // Obtener servicio registrado en init() (type-safe desde ctx)\n const users = ctx.services.get<UsersService>('users')\n const usersService = users\n const rolesService = users.roles\n\n // ============================================================================\n // USERS ROUTES (runtime auto-generado con CASL)\n // ============================================================================\n\n const usersController = ctx.runtime.createEntityController(usersService, userEntity)\n\n // Interceptar delete para añadir validación \"no puedes eliminarte a ti mismo\"\n const originalUsersDelete = usersController.delete\n if (originalUsersDelete) {\n usersController.delete = async (req: Request, res: Response) => {\n const authReq = req as AuthRequest\n const id = String(req.params['id'] ?? '')\n\n if (authReq.user?.id === id) {\n throw new ctx.core.errors.ForbiddenError('No puedes eliminarte a ti mismo')\n }\n\n return originalUsersDelete(req, res)\n }\n }\n\n // ============================================================================\n // ROLES ROUTES (montar ANTES de users para evitar que /:id capture /roles)\n // ============================================================================\n\n const rolesController = ctx.runtime.createEntityController(rolesService, roleEntity)\n const rolesRouter = ctx.runtime.createEntityRouter(rolesController, roleEntity)\n const rolesPrefix = roleEntity.routePrefix ?? '/roles'\n\n // Montar rutas CRUD de roles\n // Note: Permissions are now static in entity definitions (casl.permissions)\n router.use(rolesPrefix, rolesRouter)\n\n // ============================================================================\n // GDPR / PRIVACY ROUTES (montar ANTES de CRUD para evitar que /:id capture /me)\n // ============================================================================\n\n // --- GET /me (Get current user) ---\n router.get('/me',\n auth!,\n async (req: Request, res: Response) => {\n const authReq = req as AuthRequest\n const userId = authReq.user?.id\n if (!userId) {\n throw new ctx.core.errors.UnauthorizedError('No authenticated')\n }\n\n const user = await usersService.findById(userId)\n res.json(user)\n }\n )\n\n // --- GET /me/export (GDPR Art. 15 - Right of Access) ---\n router.get('/me/export',\n auth!,\n async (req: Request, res: Response) => {\n const authReq = req as AuthRequest\n const userId = authReq.user?.id\n if (!userId) {\n throw new ctx.core.errors.UnauthorizedError('No authenticated')\n }\n\n const data = await usersService.exportUserData(userId)\n\n // Set headers for file download\n res.setHeader('Content-Type', 'application/json')\n res.setHeader('Content-Disposition', `attachment; filename=\"user-data-export-${new Date().toISOString().split('T')[0]}.json\"`)\n res.json(data)\n }\n )\n\n // --- DELETE /me (GDPR Art. 17 - Right to Erasure) ---\n router.delete('/me',\n auth!,\n async (req: Request, res: Response) => {\n const authReq = req as AuthRequest\n const userId = authReq.user?.id\n if (!userId) {\n throw new ctx.core.errors.UnauthorizedError('No authenticated')\n }\n\n const result = await usersService.deleteAccount(userId)\n\n // Clear auth cookie\n res.clearCookie('refreshToken', { path: '/api/v1' })\n res.json(result)\n }\n )\n\n // --- POST /me/consent (Record GDPR consent) ---\n router.post('/me/consent',\n auth!,\n validate({ body: consentSchema }),\n async (req: Request, res: Response) => {\n const authReq = req as AuthRequest\n const userId = authReq.user?.id\n if (!userId) {\n throw new ctx.core.errors.UnauthorizedError('No authenticated')\n }\n\n const { version, marketingOptIn } = req.body as { version: string; marketingOptIn: boolean }\n const result = await usersService.recordConsent(userId, version, marketingOptIn, {\n ipAddress: req.ip,\n userAgent: req.headers['user-agent']\n })\n res.json(result)\n }\n )\n\n // ============================================================================\n // USER ROLE MANAGEMENT (dedicated endpoints, montar ANTES de /:id entity router)\n // ============================================================================\n\n // GET /users/:userId/roles — get roles for a user\n router.get('/:userId/roles',\n auth!,\n async (req: Request, res: Response) => {\n const roles = await usersService.getUserRoles(String(req.params['userId'] ?? ''))\n res.json(roles)\n }\n )\n\n // POST /users/:userId/roles — add single role\n router.post('/:userId/roles',\n auth!,\n validate({ body: z.object({ roleId: z.string().min(1) }) }),\n async (req: Request, res: Response) => {\n const { roleId } = req.body as { roleId: string }\n const userId = String(req.params['userId'] ?? '')\n await usersService.assignRole(userId, roleId)\n ctx.events.notify('audit.log', {\n source: 'core:users',\n action: 'role_assigned',\n actorId: (req as AuthRequest).user?.id,\n resourceType: 'user',\n resourceId: userId,\n ip: req.ip,\n userAgent: req.headers['user-agent'],\n metadata: { roleId }\n })\n const roles = await usersService.getUserRoles(userId)\n res.json(roles)\n }\n )\n\n // PUT /users/:userId/roles — set all roles (batch replace)\n router.put('/:userId/roles',\n auth!,\n validate({ body: z.object({ roleIds: z.array(z.string()) }) }),\n async (req: Request, res: Response) => {\n const { roleIds } = req.body as { roleIds: string[] }\n const userId = String(req.params['userId'] ?? '')\n await usersService.setRoles(userId, roleIds)\n ctx.events.notify('audit.log', {\n source: 'core:users',\n action: 'roles_replaced',\n actorId: (req as AuthRequest).user?.id,\n resourceType: 'user',\n resourceId: userId,\n ip: req.ip,\n userAgent: req.headers['user-agent'],\n metadata: { roleIds }\n })\n const roles = await usersService.getUserRoles(userId)\n res.json(roles)\n }\n )\n\n // DELETE /users/:userId/roles/:roleId — remove single role\n router.delete('/:userId/roles/:roleId',\n auth!,\n async (req: Request, res: Response) => {\n const userId = String(req.params['userId'] ?? '')\n const roleId = String(req.params['roleId'] ?? '')\n await usersService.removeRole(userId, roleId)\n ctx.events.notify('audit.log', {\n source: 'core:users',\n action: 'role_removed',\n actorId: (req as AuthRequest).user?.id,\n resourceType: 'user',\n resourceId: userId,\n ip: req.ip,\n userAgent: req.headers['user-agent'],\n metadata: { roleId }\n })\n res.json({ success: true })\n }\n )\n\n // ============================================================================\n // USERS ROUTES (montar DESPUÉS de roles para que /roles no sea capturado por /:id)\n // ============================================================================\n\n const usersRouter = ctx.runtime.createEntityRouter(usersController, userEntity)\n router.use(userEntity.routePrefix ?? '/', usersRouter)\n\n return router\n}\n","/**\n * Users Service - Composition with createEntityService + hooks\n *\n * Uses ctx.createEntityService instead of inheritance to:\n * - Hash passwords in beforeCreate/beforeUpdate\n * - Validate email uniqueness\n * - Protect system roles\n * - Join user-role data in findAll/findById\n */\n\nimport type { ModuleContext, PaginatedResult, CollectionEntityService, EntityQuery } from '@gzl10/nexus-sdk'\nimport type {\n User,\n Role,\n UserWithoutPassword,\n UserWithRoles,\n RoleWithCounts\n} from './users.types.js'\nimport { userEntity, roleEntity, userRoleEntity } from './users.entity.js'\n\n// ============================================================================\n// HELPERS\n// ============================================================================\n\n/** User-role row from pivot table JOIN */\ninterface UserRoleRow {\n role_id: string\n role_name: string\n role_description: string | null\n role_is_system: boolean\n role_created_at: Date\n role_updated_at: Date\n}\n\n// ============================================================================\n// FACTORY FUNCTION\n// ============================================================================\n\n/**\n * Factory function to create the users service.\n * Uses composition with createEntityService + hooks.\n */\nexport function createUsersService(ctx: ModuleContext) {\n const { errors, crypto: { hashPassword }, socket, generateId } = ctx.core\n const db = ctx.db.knex\n const { nowTimestamp } = ctx.db\n const { isUserConnected, getUserSocketCount } = socket\n const USERS_TABLE = userEntity.table\n const ROLES_TABLE = roleEntity.table\n const USER_ROLES_TABLE = userRoleEntity.table\n\n // ============================================================================\n // USERS SERVICE (composición)\n // ============================================================================\n\n const baseUsersService = ctx.runtime.createEntityService<User>(userEntity, {\n hooks: {\n beforeCreate: async (data) => {\n const processed = { ...data }\n const userType = (processed.type as string) ?? 'human'\n\n // Validaciones condicionales según tipo de usuario\n if (userType === 'human') {\n // Humans requieren email (password es opcional para OIDC users)\n if (!processed.email) {\n throw new errors.ValidationError('Email is required for human users')\n }\n }\n\n // 1. Hash password (solo si existe)\n if (processed.password) {\n processed.password = await hashPassword(processed.password)\n }\n\n // 2. Validar email único (solo si existe)\n if (processed.email) {\n const existing = await db(USERS_TABLE).where({ email: processed.email }).first()\n if (existing) {\n throw new errors.ConflictError('El email ya está en uso')\n }\n }\n\n return processed\n },\n\n beforeUpdate: async (id, data) => {\n const processed = { ...data }\n\n // 1. Hash password si cambia\n if (processed.password) {\n processed.password = await hashPassword(processed.password)\n }\n\n // 2. Validar email único (excluyendo actual)\n if (processed.email) {\n const existing = await db(USERS_TABLE)\n .where({ email: processed.email })\n .whereNot({ id })\n .first()\n if (existing) {\n throw new errors.ConflictError('El email ya está en uso')\n }\n }\n\n return processed\n }\n }\n }) as CollectionEntityService<User>\n\n // Helpers para mapear rows\n function excludePassword(user: User): UserWithoutPassword {\n const { password: _, ...rest } = user\n return rest\n }\n\n /** Map roles from pivot table rows to Role array */\n function mapRolesFromRows(rows: UserRoleRow[]): Role[] {\n return rows.map(row => ({\n id: row.role_id,\n name: row.role_name,\n description: row.role_description,\n is_system: row.role_is_system,\n created_at: row.role_created_at,\n updated_at: row.role_updated_at\n }))\n }\n\n /** Get roles for a user via pivot table */\n async function getRolesForUser(userId: string): Promise<Role[]> {\n const rows = await db(USER_ROLES_TABLE)\n .select(\n `${USER_ROLES_TABLE}.role_id`,\n `${ROLES_TABLE}.name as role_name`,\n `${ROLES_TABLE}.description as role_description`,\n `${ROLES_TABLE}.is_system as role_is_system`,\n `${ROLES_TABLE}.created_at as role_created_at`,\n `${ROLES_TABLE}.updated_at as role_updated_at`\n )\n .join(ROLES_TABLE, `${USER_ROLES_TABLE}.role_id`, `${ROLES_TABLE}.id`)\n .where(`${USER_ROLES_TABLE}.user_id`, userId) as UserRoleRow[]\n\n return mapRolesFromRows(rows)\n }\n\n /** Map user row + roles to UserWithRoles */\n function mapUserWithRoles(user: User, roles: Role[]): UserWithRoles {\n const { password, ...userWithoutPassword } = user\n const userId = user.id\n\n return {\n ...userWithoutPassword,\n has_password: password !== null && password !== undefined,\n roles,\n role_ids: roles.map(r => r.id), // Virtual field for multiselect input\n presence: {\n isOnline: isUserConnected(userId),\n socketCount: getUserSocketCount(userId)\n }\n }\n }\n\n // Métodos de usuarios con lógica personalizada\n const usersService = {\n /**\n * findAll with roles (multi-role)\n */\n async findAll(query?: EntityQuery): Promise<PaginatedResult<UserWithRoles>> {\n const pagination = ctx.db.getPagination(query)\n\n // Base query for users\n let qb = db(USERS_TABLE).select(`${USERS_TABLE}.*`)\n let countQb = db(USERS_TABLE).count('* as count')\n\n // Filter by role_id (users who have this role)\n const roleIdFilter = (query as Record<string, unknown>)?.['role_id'] ?? query?.filters?.['role_id']\n if (roleIdFilter) {\n const userIdsWithRole = db(USER_ROLES_TABLE)\n .select('user_id')\n .where('role_id', roleIdFilter)\n qb = qb.whereIn(`${USERS_TABLE}.id`, userIdsWithRole)\n countQb = countQb.whereIn('id', userIdsWithRole)\n }\n\n // Apply search filter (uses meta.searchable from entity definition)\n if (query?.search) {\n ctx.db.applySearchFilter(qb, userEntity, query.search)\n ctx.db.applySearchFilter(countQb, userEntity, query.search)\n }\n\n const countResult = await countQb.first<{ count: string | number }>()\n const total = Number(countResult?.count ?? 0)\n\n if (query?.sort) {\n qb = qb.orderBy(`${USERS_TABLE}.${query.sort}`, query.order ?? 'asc')\n } else {\n qb = qb.orderBy(`${USERS_TABLE}.created_at`, 'desc')\n }\n qb = qb.limit(pagination.limit).offset(pagination.offset)\n\n const users = await qb as User[]\n\n // Get all roles for these users in a single query\n const userIds = users.map(u => u.id)\n const rolesMap = new Map<string, Role[]>()\n\n if (userIds.length > 0) {\n const userRolesRows = await db(USER_ROLES_TABLE)\n .select(\n `${USER_ROLES_TABLE}.user_id`,\n `${USER_ROLES_TABLE}.role_id`,\n `${ROLES_TABLE}.name as role_name`,\n `${ROLES_TABLE}.description as role_description`,\n `${ROLES_TABLE}.is_system as role_is_system`,\n `${ROLES_TABLE}.created_at as role_created_at`,\n `${ROLES_TABLE}.updated_at as role_updated_at`\n )\n .join(ROLES_TABLE, `${USER_ROLES_TABLE}.role_id`, `${ROLES_TABLE}.id`)\n .whereIn(`${USER_ROLES_TABLE}.user_id`, userIds) as (UserRoleRow & { user_id: string })[]\n\n // Group roles by user_id\n for (const row of userRolesRows) {\n const userRoles = rolesMap.get(row.user_id) ?? []\n userRoles.push({\n id: row.role_id,\n name: row.role_name,\n description: row.role_description,\n is_system: row.role_is_system,\n created_at: row.role_created_at,\n updated_at: row.role_updated_at\n })\n rolesMap.set(row.user_id, userRoles)\n }\n }\n\n // Map users with their roles\n const items = users.map(user => mapUserWithRoles(user, rolesMap.get(user.id) ?? []))\n\n return ctx.db.buildPaginatedResult(items, total, pagination)\n },\n\n /**\n * findById with roles and presence (used by controller for GET /users/:id)\n */\n async findById(id: string): Promise<UserWithRoles> {\n const user = await baseUsersService.findById(id)\n if (!user) throw new errors.NotFoundError('Usuario')\n\n const roles = await getRolesForUser(id)\n return mapUserWithRoles(user, roles)\n },\n\n /**\n * @deprecated Use findById instead (now includes roles)\n */\n async findByIdWithRoles(id: string): Promise<UserWithRoles | null> {\n const user = await baseUsersService.findById(id)\n if (!user) return null\n\n const roles = await getRolesForUser(id)\n return mapUserWithRoles(user, roles)\n },\n\n /**\n * findByIdWithPassword - for authentication\n */\n async findByIdWithPassword(id: string): Promise<User | null> {\n return baseUsersService.findById(id)\n },\n\n /**\n * create with userId for audit (returns without password)\n * Supports role_ids virtual field for multi-role assignment\n */\n async create(data: Record<string, unknown>, userId?: string): Promise<UserWithoutPassword> {\n // Extract role_ids (virtual field, not in DB)\n const { role_ids, ...userData } = data\n const roleIds = Array.isArray(role_ids) ? role_ids as string[] : []\n\n const user = await baseUsersService.create({ ...userData, created_by: userId ?? null })\n\n // Assign roles if provided\n if (roleIds.length > 0) {\n await this.setRoles(user.id, roleIds)\n }\n\n return excludePassword(user)\n },\n\n /**\n * update with userId for audit (returns without password)\n * Supports role_ids virtual field for multi-role assignment\n */\n async update(id: string, data: Record<string, unknown>, userId?: string): Promise<UserWithoutPassword> {\n // Extract role_ids (virtual field, not in DB)\n const { role_ids, ...userData } = data\n\n const user = await baseUsersService.update(id, { ...userData, updated_by: userId ?? null })\n\n // Update roles if provided (undefined = no change, array = replace)\n if (role_ids !== undefined) {\n const roleIds = Array.isArray(role_ids) ? role_ids as string[] : []\n await this.setRoles(id, roleIds)\n }\n\n return excludePassword(user)\n },\n\n /**\n * delete\n */\n async delete(id: string) {\n return baseUsersService.delete(id)\n },\n\n // ========================================================================\n // Multi-Role Methods\n // ========================================================================\n\n /**\n * Get role IDs for a user\n */\n async getRoleIds(userId: string): Promise<string[]> {\n const rows = await db(USER_ROLES_TABLE)\n .select('role_id')\n .where({ user_id: userId }) as { role_id: string }[]\n return rows.map(r => r.role_id)\n },\n\n /**\n * Get role names for a user (useful for ability building)\n */\n async getRoleNames(userId: string): Promise<string[]> {\n const rows = await db(USER_ROLES_TABLE)\n .select(`${ROLES_TABLE}.name`)\n .join(ROLES_TABLE, `${USER_ROLES_TABLE}.role_id`, `${ROLES_TABLE}.id`)\n .where(`${USER_ROLES_TABLE}.user_id`, userId) as { name: string }[]\n return rows.map(r => r.name)\n },\n\n /**\n * Get full roles for a user\n */\n async getUserRoles(userId: string): Promise<Role[]> {\n return getRolesForUser(userId)\n },\n\n /**\n * Assign a role to a user\n */\n async assignRole(userId: string, roleId: string): Promise<void> {\n // Verify user exists\n const user = await db(USERS_TABLE).where({ id: userId }).first()\n if (!user) throw new errors.NotFoundError('Usuario')\n\n // Verify role exists\n const role = await db(ROLES_TABLE).where({ id: roleId }).first()\n if (!role) throw new errors.NotFoundError('Rol')\n\n // Check if already assigned\n const existing = await db(USER_ROLES_TABLE)\n .where({ user_id: userId, role_id: roleId })\n .first()\n if (existing) return // Already assigned, no-op\n\n await db(USER_ROLES_TABLE).insert({\n id: generateId(),\n user_id: userId,\n role_id: roleId\n })\n },\n\n /**\n * Remove a role from a user\n */\n async removeRole(userId: string, roleId: string): Promise<void> {\n const deleted = await db(USER_ROLES_TABLE)\n .where({ user_id: userId, role_id: roleId })\n .delete()\n\n if (deleted === 0) {\n throw new errors.NotFoundError('Asignación de rol')\n }\n },\n\n /**\n * Set user roles (replaces all existing roles)\n */\n async setRoles(userId: string, roleIds: string[]): Promise<void> {\n // Verify user exists\n const user = await db(USERS_TABLE).where({ id: userId }).first()\n if (!user) throw new errors.NotFoundError('Usuario')\n\n // Verify all roles exist\n if (roleIds.length > 0) {\n const existingRoles = await db(ROLES_TABLE)\n .whereIn('id', roleIds)\n .select('id') as { id: string }[]\n if (existingRoles.length !== roleIds.length) {\n throw new errors.NotFoundError('Uno o más roles no existen')\n }\n }\n\n await db.transaction(async (trx) => {\n // Remove all existing roles\n await trx(USER_ROLES_TABLE).where({ user_id: userId }).delete()\n\n // Insert new roles\n if (roleIds.length > 0) {\n const inserts = roleIds.map(roleId => ({\n id: generateId(),\n user_id: userId,\n role_id: roleId\n }))\n await trx(USER_ROLES_TABLE).insert(inserts)\n }\n })\n },\n\n // ========================================================================\n // GDPR / Privacy Methods\n // ========================================================================\n\n /**\n * Export all user data (GDPR Art. 15 - Right of Access)\n * Returns JSON with all personal data for the user\n */\n async exportUserData(userId: string): Promise<Record<string, unknown>> {\n // 1. User profile (without password)\n const user = await db(USERS_TABLE)\n .where({ id: userId })\n .first() as User | undefined\n\n if (!user) {\n throw new errors.NotFoundError('Usuario')\n }\n\n const { password: _, ...userProfile } = user\n\n // 2. Roles info (multi-role)\n const roles = await getRolesForUser(userId)\n\n // 3. Auth data (sessions + audit) via auth service (optional - avoids circular dep)\n type AuthServiceLike = { getUserAuthData: (userId: string) => Promise<{ sessions: unknown[]; auditLog: unknown[] }> }\n const authService = ctx.services.getOptional<AuthServiceLike>('auth')\n const { sessions, auditLog } = authService\n ? await authService.getUserAuthData(userId)\n : { sessions: [], auditLog: [] }\n\n // 4. Uploaded files via storage service (optional - may not be loaded)\n type StorageServiceLike = { getUserFiles: (userId: string) => Promise<unknown[]> }\n const storageService = ctx.services.getOptional<StorageServiceLike>('storage')\n const files = storageService ? await storageService.getUserFiles(userId) : []\n\n return {\n exportDate: new Date().toISOString(),\n profile: userProfile,\n roles: roles.map(r => ({ id: r.id, name: r.name, description: r.description })),\n sessions,\n auditLog,\n files,\n _meta: {\n format: 'GDPR Art. 15 Data Export',\n retentionPolicy: {\n auditLog: '90 days',\n sessions: '7 days (auto-expire)'\n }\n }\n }\n },\n\n /**\n * Delete user account and all associated data (GDPR Art. 17 - Right to Erasure)\n * Performs cascade deletion with audit trail anonymization\n */\n async deleteAccount(userId: string): Promise<{ deleted: true; summary: Record<string, number> }> {\n const user = await db(USERS_TABLE).where({ id: userId }).first() as User | undefined\n if (!user) {\n throw new errors.NotFoundError('Usuario')\n }\n\n const summary: Record<string, number> = {}\n\n // 1. Delete user roles (own table - users module owns this)\n const rolesDeleted = await db(USER_ROLES_TABLE)\n .where({ user_id: userId })\n .delete()\n summary['userRoles'] = rolesDeleted\n\n // 2. Revoke all sessions via auth service (optional - avoids circular dep)\n type AuthServiceLike = { revokeAllUserSessions: (userId: string) => Promise<number>; anonymizeUserAudit: (userId: string) => Promise<number> }\n const authService = ctx.services.getOptional<AuthServiceLike>('auth')\n summary['refreshTokens'] = authService ? await authService.revokeAllUserSessions(userId) : 0\n\n // 3. Anonymize audit log via auth service\n summary['auditRecordsAnonymized'] = authService ? await authService.anonymizeUserAudit(userId) : 0\n\n // 4. Delete uploaded files via storage service (optional)\n type StorageServiceLike = { deleteUserFiles: (userId: string) => Promise<number> }\n const storageService = ctx.services.getOptional<StorageServiceLike>('storage')\n summary['filesDeleted'] = storageService ? await storageService.deleteUserFiles(userId) : 0\n\n // 5. Delete user record\n await db(USERS_TABLE).where({ id: userId }).delete()\n summary['userDeleted'] = 1\n\n // 6. Emit audit event for account deletion\n ctx.events.notify('audit.log', {\n source: 'core:users',\n action: 'account_deleted',\n actorId: userId,\n resourceType: 'user',\n resourceId: userId\n })\n\n // 7. Emit event for custom hooks (e.g., external systems cleanup)\n ctx.events.notify('user:deleted', { userId, email: user.email, deletedAt: new Date().toISOString() })\n\n return { deleted: true as const, summary }\n },\n\n /**\n * Record user consent (GDPR compliance)\n * Updates consent_date, consent_version, and marketing_opt_in\n * Emits 'user:consent' event for audit logging\n */\n async recordConsent(\n userId: string,\n version: string,\n marketingOptIn: boolean,\n meta?: { ipAddress?: string; userAgent?: string }\n ): Promise<{ recorded: true; consentDate: string }> {\n const user = await db(USERS_TABLE).where({ id: userId }).first() as User | undefined\n if (!user) {\n throw new errors.NotFoundError('Usuario')\n }\n\n const consentDate = new Date().toISOString()\n\n await db(USERS_TABLE).where({ id: userId }).update({\n consent_date: consentDate,\n consent_version: version,\n marketing_opt_in: marketingOptIn,\n updated_at: nowTimestamp(db)\n })\n\n // Emit event with all data for compliance audit log\n ctx.events.notify('user:consent', {\n userId,\n email: user.email,\n version,\n marketingOptIn,\n consentDate,\n ipAddress: meta?.ipAddress,\n userAgent: meta?.userAgent\n })\n\n return { recorded: true, consentDate }\n },\n\n // ========================================================================\n // Bot/Service Account Helpers\n // ========================================================================\n\n /**\n * Create a service account (no email/password, SERVICE role by default)\n */\n async createServiceAccount(\n name: string,\n metadata?: Record<string, unknown>\n ): Promise<UserWithoutPassword> {\n // Get SERVICE role\n const serviceRole = await db(ROLES_TABLE).where({ name: 'SERVICE' }).first()\n if (!serviceRole) {\n throw new errors.AppError('SERVICE role not found. Run seed first.', 500)\n }\n\n const userId = generateId()\n await db(USERS_TABLE).insert({\n id: userId,\n type: 'service',\n email: null,\n password: null,\n name,\n metadata: metadata ? JSON.stringify(metadata) : null,\n marketing_opt_in: false,\n created_at: nowTimestamp(db),\n updated_at: nowTimestamp(db)\n })\n\n // Assign SERVICE role\n await db(USER_ROLES_TABLE).insert({\n id: generateId(),\n user_id: userId,\n role_id: serviceRole.id\n })\n\n const user = await db(USERS_TABLE).where({ id: userId }).first() as User\n return excludePassword(user)\n },\n\n /**\n * Create a bot user (no email/password, BOT role, linked to owner)\n */\n async createBotUser(\n name: string,\n ownerId: string,\n metadata?: Record<string, unknown>\n ): Promise<UserWithoutPassword> {\n // Verify owner exists and is human\n const owner = await db(USERS_TABLE).where({ id: ownerId }).first() as User | undefined\n if (!owner) {\n throw new errors.NotFoundError('Owner user')\n }\n if (owner.type !== 'human') {\n throw new errors.ValidationError('Bot owner must be a human user')\n }\n\n // Get BOT role\n const botRole = await db(ROLES_TABLE).where({ name: 'BOT' }).first()\n if (!botRole) {\n throw new errors.AppError('BOT role not found. Run seed first.', 500)\n }\n\n const userId = generateId()\n const botMetadata = {\n ...metadata,\n owner_id: ownerId // Track who owns this bot\n }\n\n await db(USERS_TABLE).insert({\n id: userId,\n type: 'bot',\n email: null,\n password: null,\n name,\n metadata: JSON.stringify(botMetadata),\n marketing_opt_in: false,\n created_by: ownerId,\n created_at: nowTimestamp(db),\n updated_at: nowTimestamp(db)\n })\n\n // Assign BOT role\n await db(USER_ROLES_TABLE).insert({\n id: generateId(),\n user_id: userId,\n role_id: botRole.id\n })\n\n const user = await db(USERS_TABLE).where({ id: userId }).first() as User\n return excludePassword(user)\n }\n }\n\n // ============================================================================\n // ROLES SERVICE (composición)\n // ============================================================================\n\n const baseRolesService = ctx.runtime.createEntityService<Role>(roleEntity, {\n hooks: {\n beforeCreate: async (data) => {\n if (data.name) {\n const existing = await db(ROLES_TABLE).where({ name: data.name }).first()\n if (existing) {\n throw new errors.ConflictError('Ya existe un rol con ese nombre')\n }\n }\n return data\n },\n\n beforeUpdate: async (id, data) => {\n const role = await db(ROLES_TABLE).where({ id }).first() as Role | undefined\n\n if (role?.is_system) {\n throw new errors.ForbiddenError('No se pueden modificar roles del sistema')\n }\n\n if (data.name && data.name !== role?.name) {\n const existing = await db(ROLES_TABLE).where({ name: data.name }).first()\n if (existing) {\n throw new errors.ConflictError('Ya existe un rol con ese nombre')\n }\n }\n\n return data\n },\n\n beforeDelete: async (id) => {\n const role = await db(ROLES_TABLE).where({ id }).first() as Role | undefined\n\n if (role?.is_system) {\n throw new errors.ForbiddenError('No se pueden eliminar roles del sistema')\n }\n\n // Check if any users have this role assigned\n const usersCount = await db(USER_ROLES_TABLE)\n .where({ role_id: id })\n .count('* as count')\n .first<{ count: string | number }>()\n\n if (Number(usersCount?.count ?? 0) > 0) {\n throw new errors.ConflictError('No se puede eliminar un rol con usuarios asignados')\n }\n }\n }\n }) as CollectionEntityService<Role>\n\n const rolesService = {\n /**\n * findAll with user counts\n */\n async findAll(query?: EntityQuery): Promise<PaginatedResult<RoleWithCounts>> {\n const pagination = ctx.db.getPagination(query)\n\n // Count users per role via pivot table\n const usersCountSubquery = db(USER_ROLES_TABLE)\n .count('*')\n .whereRaw(`${USER_ROLES_TABLE}.role_id = ${ROLES_TABLE}.id`)\n .as('users_count')\n\n const baseQuery = db(ROLES_TABLE)\n .select(`${ROLES_TABLE}.*`, usersCountSubquery)\n\n const countQuery = db(ROLES_TABLE).count('* as count')\n\n // Apply search filter (uses meta.searchable from entity definition)\n if (query?.search) {\n ctx.db.applySearchFilter(baseQuery, roleEntity, query.search)\n ctx.db.applySearchFilter(countQuery, roleEntity, query.search)\n }\n\n const [roles, countResult] = await Promise.all([\n baseQuery.clone().orderBy(query?.sort ?? 'name', query?.sort ? (query.order ?? 'asc') : 'asc').limit(pagination.limit).offset(pagination.offset),\n countQuery.first<{ count: string | number }>()\n ])\n\n const total = Number(countResult?.count ?? 0)\n const items = (roles as Record<string, unknown>[]).map((role) => ({\n ...role,\n users_count: Number(role['users_count'] ?? 0)\n })) as RoleWithCounts[]\n\n return ctx.db.buildPaginatedResult(items, total, pagination)\n },\n\n /**\n * findById\n */\n async findById(id: string): Promise<Role> {\n const role = await baseRolesService.findById(id)\n if (!role) throw new errors.NotFoundError('Rol')\n return role\n },\n\n /**\n * findByName\n */\n async findByName(name: string): Promise<Role | undefined> {\n return db(ROLES_TABLE).where({ name }).first() as Promise<Role | undefined>\n },\n\n /**\n * create role\n * Note: roles table has no audit fields (created_by/updated_by)\n */\n async create(data: Record<string, unknown>) {\n return baseRolesService.create(data)\n },\n\n /**\n * update role\n * Note: roles table has no audit fields (created_by/updated_by)\n */\n async update(id: string, data: Record<string, unknown>) {\n return baseRolesService.update(id, data)\n },\n\n /**\n * delete\n */\n async delete(id: string) {\n return baseRolesService.delete(id)\n }\n }\n\n // ============================================================================\n // RETURN\n // ============================================================================\n\n return {\n // Definition para compatibilidad con BaseEntityService (requerido por createEntityController)\n definition: userEntity,\n\n // Users methods\n findAll: usersService.findAll,\n findById: usersService.findById,\n findByIdWithRoles: usersService.findByIdWithRoles,\n findByIdWithPassword: usersService.findByIdWithPassword,\n create: usersService.create,\n update: usersService.update,\n delete: usersService.delete,\n\n // Multi-role methods\n getRoleIds: usersService.getRoleIds,\n getRoleNames: usersService.getRoleNames,\n getUserRoles: usersService.getUserRoles,\n assignRole: usersService.assignRole,\n removeRole: usersService.removeRole,\n setRoles: usersService.setRoles,\n\n // GDPR / Privacy methods\n exportUserData: usersService.exportUserData,\n deleteAccount: usersService.deleteAccount,\n recordConsent: usersService.recordConsent,\n\n // Bot/Service account helpers\n createServiceAccount: usersService.createServiceAccount,\n createBotUser: usersService.createBotUser,\n\n // Roles nested service (con definition para createEntityController)\n roles: {\n definition: roleEntity,\n ...rolesService\n }\n }\n}\n","/**\n * @module users\n * @description User management, roles, and profile operations\n */\n\nimport type { ModuleManifest, ModuleContext } from '@gzl10/nexus-sdk'\nimport { createUsersRoutes } from './users.routes.js'\nimport { createUsersService } from './users.service.js'\nimport { userEntity, roleEntity, userRoleEntity } from './users.entity.js'\n\n// Re-exports\nexport { createUsersService } from './users.service.js'\nexport { userEntity, roleEntity, userRoleEntity } from './users.entity.js'\n\n// Types from users.types.ts\nexport type {\n User,\n Role,\n UserWithoutPassword,\n UserPresence,\n UserWithRoles,\n RoleWithCounts,\n UsersService,\n RolesService\n} from './users.types.js'\n\nexport const usersModule: ModuleManifest = {\n name: 'users',\n label: { en: 'Users & Roles', es: 'Usuarios y roles' },\n icon: 'mdi:account-group-outline',\n description: { en: 'User accounts, authentication profiles, roles and permissions', es: 'Cuentas de usuario, perfiles de autenticación, roles y permisos' },\n type: 'core',\n category: 'security',\n dependencies: ['logger'],\n // Import dinámico para evitar posibles ciclos\n seed: async (ctx) => {\n const { seed } = await import('./users.seed.js')\n await seed(ctx)\n },\n\n init: (ctx: ModuleContext) => {\n // Registrar servicio de usuarios\n ctx.services.register('users', createUsersService(ctx))\n ctx.core.logger.debug('Users service registered')\n },\n\n routes: createUsersRoutes,\n routePrefix: '/users',\n // Orden: roles primero, luego users, finalmente user_roles (FK a ambos)\n definitions: [roleEntity, userEntity, userRoleEntity]\n}\n","import type { CollectionEntityDefinition } from '@gzl10/nexus-sdk'\nimport { useIdField, useTextField, useSelectField, useDatetimeField, useEmailField, useMetadataField, useExpiresAtField } from '@gzl10/nexus-sdk/fields'\n\n/**\n * EntityDefinition for Refresh Tokens.\n *\n * Uses 'temp' type for automatic cleanup of expired tokens.\n * Retention: 7 days - same as jwtRefreshExpires default.\n */\nexport const refreshTokenEntity: CollectionEntityDefinition = {\n type: 'temp',\n table: 'auth_refresh_tokens',\n label: { en: 'Refresh Token', es: 'Token de refresco' },\n labelPlural: { en: 'Refresh Tokens', es: 'Tokens de refresco' },\n labelField: 'id',\n retention: { days: 7, expiresField: 'expires_at' },\n expose: false,\n\n fields: {\n id: useIdField(),\n token: useTextField({\n label: { en: 'Token', es: 'Token' },\n hidden: true,\n size: 255,\n unique: true,\n nullable: false,\n meta: { exportable: false }\n }),\n user_id: {\n label: { en: 'User', es: 'Usuario' },\n input: 'select',\n required: true,\n db: { type: 'string', size: 26, nullable: false, index: true },\n // Note: No 'relation' - temp entities use Redis/InMemory adapters that don't support FKs\n // The UI dropdown still works via options.endpoint\n options: { endpoint: '/users', valueField: 'id', labelField: 'name' },\n meta: { searchable: true }\n },\n expires_at: useExpiresAtField({\n required: true,\n nullable: false\n }),\n last_used_at: useDatetimeField({\n label: { en: 'Last Used', es: 'Último uso' },\n nullable: true,\n meta: { sortable: true }\n }),\n device_id: useTextField({\n label: { en: 'Device ID', es: 'ID de dispositivo' },\n size: 64,\n index: true,\n nullable: true,\n meta: { searchable: true }\n }),\n device_name: useTextField({\n label: { en: 'Device', es: 'Dispositivo' },\n size: 100,\n nullable: true,\n hint: { en: 'Device readable name (e.g. John\\'s iPhone)', es: 'Nombre legible del dispositivo (ej: iPhone de Juan)' }\n }),\n created_at: useDatetimeField({\n label: { en: 'Created', es: 'Creado' },\n disabled: true,\n nullable: true,\n meta: { sortable: true }\n })\n },\n\n casl: {\n subject: 'AuthRefreshToken',\n permissions: {\n SUPPORT: { actions: ['read'] }\n }\n }\n}\n\n/**\n * EntityDefinition for Auth Identities.\n *\n * Centralized linking table between Nexus users and external auth providers\n * (OIDC/OAuth). Plugins use AuthService methods instead of creating their\n * own {provider}_users tables.\n */\nexport const authIdentitiesEntity: CollectionEntityDefinition = {\n type: 'collection',\n table: 'auth_identities',\n label: { en: 'Auth Identity', es: 'Identidad de autenticación' },\n labelPlural: { en: 'Auth Identities', es: 'Identidades de autenticación' },\n labelField: 'provider',\n timestamps: true,\n hidden: true,\n expose: false,\n order: 5,\n\n fields: {\n id: useIdField(),\n user_id: useSelectField({\n label: { en: 'User', es: 'Usuario' },\n table: 'users',\n column: 'id',\n onDelete: 'CASCADE',\n required: true,\n index: true,\n endpoint: '/users',\n valueField: 'id',\n labelField: 'name',\n meta: { searchable: true }\n }),\n provider: useTextField({\n label: { en: 'Provider', es: 'Proveedor' },\n required: true,\n size: 50,\n nullable: false,\n disabled: true,\n hint: { en: 'e.g. pocketid, google, microsoft', es: 'ej. pocketid, google, microsoft' },\n meta: { sortable: true, searchable: true }\n }),\n provider_user_id: useTextField({\n label: { en: 'Provider User ID', es: 'ID de usuario del proveedor' },\n required: true,\n size: 255,\n nullable: false,\n disabled: true,\n hint: { en: 'OIDC sub claim or OAuth user ID', es: 'Claim sub OIDC o ID de usuario OAuth' },\n meta: { searchable: true }\n }),\n provider_email: useEmailField({\n label: { en: 'Provider Email', es: 'Email del proveedor' },\n nullable: true,\n disabled: true,\n }),\n metadata: useMetadataField(),\n linked_at: useDatetimeField({\n label: { en: 'Linked At', es: 'Vinculado en' },\n disabled: true,\n nullable: false,\n meta: { sortable: true }\n }),\n last_login_at: useDatetimeField({\n label: { en: 'Last Login', es: 'Último acceso' },\n nullable: true,\n disabled: true,\n meta: { sortable: true }\n }),\n },\n\n indexes: [\n { columns: ['provider', 'provider_user_id'], unique: true },\n { columns: ['user_id', 'provider'] }\n ],\n\n casl: {\n subject: 'AuthIdentity',\n permissions: {\n '*': [\n { actions: ['read'], conditions: { user_id: '${user.id}' } }\n ]\n }\n }\n}\n","import type { ModuleContext } from '@gzl10/nexus-sdk'\nimport { refreshTokenEntity } from './auth.entity.js'\n\n/**\n * Auth Routes\n *\n * Auto-montaje desde definitions:\n * - All auth actions (login, register, logout, me, etc.) - via authActions\n *\n * Aquí montamos solo entidades que necesitan personalización:\n * - refreshTokenEntity (temp con personalización: sin create/update vía API)\n */\nexport function createAuthRoutes(ctx: ModuleContext) {\n const router = ctx.core.createRouter()\n\n // ============================================================================\n // TOKENS (temp - personalización: sin create/update vía API)\n // ============================================================================\n\n const tokensService = ctx.runtime.createEntityService(refreshTokenEntity)\n const tokensController = ctx.runtime.createEntityController(tokensService, refreshTokenEntity)\n\n // Tokens solo se crean/rotan internamente\n delete tokensController.create\n delete tokensController.update\n\n const tokensRouter = ctx.runtime.createEntityRouter(tokensController, refreshTokenEntity)\n router.use(refreshTokenEntity.routePrefix ?? '/tokens', tokensRouter)\n\n ctx.services.register('refreshTokens', tokensService)\n\n return router\n}\n","import { z } from 'zod'\n\n/** Shows configuration errors and exits (inline to avoid modules/ → core/ dependency) */\nfunction configError(module: string, errors: string[]): never {\n console.error(`\\n${'═'.repeat(60)}`)\n console.error(`NEXUS BACKEND - Configuration Error [${module}]`)\n console.error('═'.repeat(60))\n errors.forEach((err) => console.error(` ${err}`))\n console.error('═'.repeat(60) + '\\n')\n process.exit(1)\n}\n\n/**\n * Environment variable schema for auth.\n * Completely independent from global configuration.\n */\nconst authEnvSchema = z.object({\n AUTH_SECRET: z.string().min(32, 'AUTH_SECRET must be at least 32 characters'),\n AUTH_ACCESS_EXPIRES: z.string().default('15m'),\n AUTH_REFRESH_EXPIRES: z.string().default('7d'),\n // Rate limiting (default: 5 requests per 15 minutes)\n AUTH_RATE_LIMIT_MAX: z.coerce.number().default(5),\n AUTH_RATE_LIMIT_WINDOW: z.coerce.number().default(900), // seconds\n // Cookie domain for SSO across subdomains (e.g., '.example.com')\n AUTH_COOKIE_DOMAIN: z.string().optional(),\n // Challenge threshold: failed attempts before requiring OTP (default: 2)\n AUTH_CHALLENGE_THRESHOLD: z.coerce.number().default(2),\n // Skip OTP verification for registration (DEVELOPMENT ONLY - rejected in production)\n AUTH_SKIP_REGISTER_OTP: z.coerce.boolean().default(false),\n // Disable self-registration via POST /auth/register (default: false)\n AUTH_DISABLE_REGISTRATION: z.coerce.boolean().default(false),\n // Disable auto-creation of users on first OIDC login (default: false)\n AUTH_DISABLE_AUTO_CREATE: z.coerce.boolean().default(false)\n})\n\nfunction parseAuthEnv() {\n const result = authEnvSchema.safeParse(process.env)\n\n if (!result.success) {\n const errors = result.error.issues.map((issue) => {\n const path = issue.path.join('.')\n if (path === 'AUTH_SECRET' && issue.code === 'invalid_type') {\n return 'AUTH_SECRET is required. Set it in your .env file or environment.'\n }\n if (path === 'AUTH_SECRET' && issue.code === 'too_small') {\n return `AUTH_SECRET must be at least 32 characters (current: ${String(process.env['AUTH_SECRET']).length})`\n }\n return `${path}: ${issue.message}`\n })\n configError('auth', errors)\n }\n\n // Block AUTH_SKIP_REGISTER_OTP in production\n if (result.data?.AUTH_SKIP_REGISTER_OTP && process.env['NODE_ENV'] === 'production') {\n configError('auth', ['AUTH_SKIP_REGISTER_OTP cannot be enabled in production'])\n }\n\n return result.data\n}\n\n// Lazy-loaded auth env (only parsed on first getAuthConfig() call)\nlet _authEnv: z.infer<typeof authEnvSchema> | undefined\n\nfunction getAuthEnv() {\n if (!_authEnv) {\n _authEnv = parseAuthEnv()\n }\n return _authEnv!\n}\n\n\n/**\n * Typed auth configuration\n */\nexport interface AuthConfig {\n secret: string\n accessExpires: string\n refreshExpires: string\n rateLimitMax: number\n rateLimitWindowMs: number\n /** Cookie domain for SSO across subdomains (e.g., '.example.com') */\n cookieDomain?: string\n /** Number of failed attempts before requiring OTP challenge */\n challengeThreshold: number\n /** Skip OTP verification for registration (only for testing) */\n skipRegisterOtp: boolean\n /** Disable self-registration via POST /auth/register */\n disableRegistration: boolean\n /** Disable auto-creation of users on first OIDC login */\n disableAutoCreate: boolean\n}\n\n/**\n * Gets auth configuration from environment variables.\n * Validation only runs on first call (lazy).\n */\nexport function getAuthConfig(): AuthConfig {\n const authEnv = getAuthEnv()\n return {\n secret: authEnv.AUTH_SECRET,\n accessExpires: authEnv.AUTH_ACCESS_EXPIRES,\n refreshExpires: authEnv.AUTH_REFRESH_EXPIRES,\n rateLimitMax: authEnv.AUTH_RATE_LIMIT_MAX,\n rateLimitWindowMs: authEnv.AUTH_RATE_LIMIT_WINDOW * 1000, // convert to ms\n cookieDomain: authEnv.AUTH_COOKIE_DOMAIN,\n challengeThreshold: authEnv.AUTH_CHALLENGE_THRESHOLD,\n skipRegisterOtp: authEnv.AUTH_SKIP_REGISTER_OTP,\n disableRegistration: authEnv.AUTH_DISABLE_REGISTRATION,\n disableAutoCreate: authEnv.AUTH_DISABLE_AUTO_CREATE\n }\n}\n","import jwt from 'jsonwebtoken'\nimport type { Request, Response, NextFunction, RequestHandler, ModuleContext, ManagedCache } from '@gzl10/nexus-sdk'\nimport type { JwtPayload } from './jwt.utils.js'\nimport type { AuthUserRecord } from './auth.types.js'\nimport type { AuthService } from './auth.service.js'\nimport type { UsersService } from '../users/users.types.js'\nimport { getAuthConfig } from './auth.config.js'\n\n// Table name (hardcoded for module isolation)\nconst USERS = 'users'\n\n/**\n * Extract JWT token from request.\n * Checks Authorization header first, then query param _token (for SSE/EventSource).\n */\nfunction extractToken(req: Request): string | null {\n // 1. Authorization header (standard)\n const authHeader = req.headers.authorization\n if (authHeader?.startsWith('Bearer ')) {\n return authHeader.slice(7)\n }\n\n // 2. Query param _token (for SSE/EventSource which doesn't support custom headers)\n const queryToken = req.query['_token']\n if (typeof queryToken === 'string' && queryToken.length > 0) {\n return queryToken\n }\n\n return null\n}\n\n/** Build read-only ability by filtering rules to read-only actions */\nasync function buildReadOnlyAbility(\n defineAbilityFor: ModuleContext['core']['abilities']['defineAbilityFor'],\n user: AuthUserRecord,\n roleNames: string[]\n): Promise<Request['ability']> {\n const fullAbility = await defineAbilityFor(user, roleNames) as Request['ability']\n \n const readOnlyRules = (fullAbility as any).rules\n .filter((rule: any) => !rule.inverted)\n .map((rule: any) => {\n const action = rule.action\n if (action === 'manage' || (Array.isArray(action) && action.includes('manage'))) {\n return { ...rule, action: 'read' }\n }\n if (action === 'read' || (Array.isArray(action) && action.includes('read'))) {\n return rule\n }\n return null\n })\n .filter(Boolean)\n\n const { createMongoAbility } = await import('@casl/ability')\n return createMongoAbility(readOnlyRules) as Request['ability']\n}\n\n/** Get ability from cache or build and cache it */\nasync function getCachedAbility(\n abilityCache: ManagedCache<Request['ability']>,\n defineAbilityFor: ModuleContext['core']['abilities']['defineAbilityFor'],\n user: AuthUserRecord,\n roleNames: string[]\n): Promise<Request['ability']> {\n const cacheKey = `${user.id}:${roleNames.sort().join(',')}`\n const cached = await abilityCache.get(cacheKey)\n if (cached) return cached as Request['ability']\n\n const ability = await defineAbilityFor(user, roleNames) as Request['ability']\n await abilityCache.set(cacheKey, ability)\n return ability\n}\n\n/**\n * Creates both auth middlewares (required + optional) sharing a single ability cache.\n */\nexport function createAuthMiddlewares(ctx: ModuleContext): { auth: RequestHandler; optionalAuth: RequestHandler } {\n const { errors, abilities } = ctx.core\n const { defineAbilityFor } = abilities\n const { secret } = getAuthConfig()\n const usersService = ctx.services.get<UsersService>('users')\n\n // Ability cache — invalidated when roles/permissions change\n const abilityCache = ctx.core.cache.create<Request['ability']>('auth:ability', {\n maxEntries: 500,\n defaultTTL: 60,\n invalidateOn: ['db.roles.*', 'db.permissions.*', 'db.user_roles.*']\n })\n\n const auth: RequestHandler = async (req: Request, _res: Response, next: NextFunction) => {\n const token = extractToken(req)\n\n if (!token) {\n throw new errors.UnauthorizedError('Token no proporcionado')\n }\n\n // PAT path: tokens starting with 'nxs_' are Personal Access Tokens\n if (token.startsWith('nxs_')) {\n const authService = ctx.services.get<AuthService>('auth')\n const result = await authService.validatePersonalToken(token)\n\n if (!result) {\n throw new errors.UnauthorizedError('Token inválido o expirado')\n }\n\n const user = await ctx.db.knex<AuthUserRecord>(USERS).where({ id: result.userId }).first()\n if (!user) {\n throw new errors.UnauthorizedError('Usuario no encontrado')\n }\n\n req.user = user\n const roleNames = await usersService.getRoleNames(user.id)\n\n if (result.scope === 'readonly') {\n req.ability = await buildReadOnlyAbility(defineAbilityFor, user, roleNames)\n } else {\n req.ability = await getCachedAbility(abilityCache, defineAbilityFor, user, roleNames)\n }\n\n req.patTokenId = result.tokenId\n return next()\n }\n\n // JWT path\n try {\n const decoded = jwt.verify(token, secret) as unknown as JwtPayload\n\n const user = await ctx.db.knex<AuthUserRecord>(USERS).where({ id: decoded.userId }).first()\n\n if (!user) {\n throw new errors.UnauthorizedError('Usuario no encontrado')\n }\n\n const roleNames = await usersService.getRoleNames(user.id)\n req.user = user\n req.ability = await getCachedAbility(abilityCache, defineAbilityFor, user, roleNames)\n\n if (decoded.impersonatedBy) {\n req.impersonation = {\n originalUserId: decoded.impersonatedBy,\n isImpersonating: true\n }\n }\n\n next()\n } catch (error) {\n if (error instanceof jwt.TokenExpiredError) {\n throw new errors.UnauthorizedError('Token expirado')\n }\n if (error instanceof jwt.JsonWebTokenError) {\n throw new errors.UnauthorizedError('Token inválido')\n }\n throw error\n }\n }\n\n const optionalAuth: RequestHandler = async (req: Request, _res: Response, next: NextFunction) => {\n const token = extractToken(req)\n\n if (!token) {\n return next()\n }\n\n // PAT path\n if (token.startsWith('nxs_')) {\n try {\n const authService = ctx.services.get<AuthService>('auth')\n const result = await authService.validatePersonalToken(token)\n\n if (result) {\n const user = await ctx.db.knex<AuthUserRecord>(USERS).where({ id: result.userId }).first()\n if (user) {\n req.user = user\n const roleNames = await usersService.getRoleNames(user.id)\n\n if (result.scope === 'readonly') {\n req.ability = await buildReadOnlyAbility(defineAbilityFor, user, roleNames)\n } else {\n req.ability = await getCachedAbility(abilityCache, defineAbilityFor, user, roleNames)\n }\n\n req.patTokenId = result.tokenId\n }\n }\n } catch {\n // Token inválido, continuar sin autenticación\n }\n return next()\n }\n\n try {\n const decoded = jwt.verify(token, secret) as unknown as JwtPayload\n\n const user = await ctx.db.knex<AuthUserRecord>(USERS).where({ id: decoded.userId }).first()\n\n if (user) {\n const roleNames = await usersService.getRoleNames(user.id)\n req.user = user\n req.ability = await getCachedAbility(abilityCache, defineAbilityFor, user, roleNames)\n\n if (decoded.impersonatedBy) {\n req.impersonation = {\n originalUserId: decoded.impersonatedBy,\n isImpersonating: true\n }\n }\n }\n } catch {\n // Token inválido, continuar sin autenticación\n }\n\n next()\n }\n\n return { auth, optionalAuth }\n}\n","import type { CollectionEntityDefinition } from '@gzl10/nexus-sdk'\nimport { useIdField, useTextField, useSelectField, useDatetimeField, useExpiresAtField } from '@gzl10/nexus-sdk/fields'\n\n/**\n * EntityDefinition for Personal Access Tokens.\n *\n * Persistent tokens for API authentication.\n * Each user manages their own tokens (CASL self-access).\n */\nexport const personalTokenEntity: CollectionEntityDefinition = {\n type: 'collection',\n table: 'auth_personal_tokens',\n label: { en: 'Personal Access Token', es: 'Token de acceso personal' },\n labelPlural: { en: 'Personal Access Tokens', es: 'Tokens de acceso personal' },\n labelField: 'name',\n timestamps: true,\n hidden: true,\n order: 6,\n routePrefix: '/personal-tokens',\n\n fields: {\n id: useIdField(),\n user_id: useSelectField({\n label: { en: 'User', es: 'Usuario' },\n table: 'users',\n column: 'id',\n onDelete: 'CASCADE',\n required: true,\n index: true,\n endpoint: '/users',\n valueField: 'id',\n labelField: 'name',\n meta: { searchable: true }\n }),\n name: useTextField({\n label: { en: 'Name', es: 'Nombre' },\n required: true,\n size: 100,\n nullable: false,\n hint: { en: 'Descriptive name for this token', es: 'Nombre descriptivo para este token' },\n meta: { searchable: true }\n }),\n token_prefix: useTextField({\n label: { en: 'Token', es: 'Token' },\n size: 20,\n disabled: true,\n nullable: false,\n hint: { en: 'Partial token for identification', es: 'Token parcial para identificación' }\n }),\n token_hash: useTextField({\n label: { en: 'Token Hash', es: 'Hash del token' },\n size: 64,\n hidden: true,\n nullable: false,\n unique: true,\n meta: { exportable: false }\n }),\n scope: useSelectField({\n label: { en: 'Permission', es: 'Permiso' },\n required: true,\n options: [\n { value: 'readonly', label: { en: 'Read-only', es: 'Solo lectura' } },\n { value: 'readwrite', label: { en: 'Read & Write', es: 'Lectura y escritura' } }\n ],\n meta: { sortable: true }\n }),\n expires_at: useExpiresAtField(),\n last_used_at: useDatetimeField({\n label: { en: 'Last Used', es: 'Último uso' },\n nullable: true,\n disabled: true,\n meta: { sortable: true }\n }),\n },\n\n indexes: [\n { columns: ['user_id'] }\n ],\n\n casl: {\n subject: 'AuthPersonalToken',\n permissions: {\n '*': [\n { actions: ['read', 'delete'], conditions: { user_id: '${user.id}' } }\n ]\n }\n }\n}\n","/**\n * GET /auth/providers - List available auth providers\n */\n\nimport type { ActionDefinition } from '@gzl10/nexus-sdk'\nimport { getAuthConfig } from '../auth.config.js'\n\nexport const providersAction: ActionDefinition = {\n key: 'providers',\n label: { en: 'Get Auth Providers', es: 'Obtener proveedores de autenticación' },\n icon: 'mdi:account-key',\n scope: 'module',\n hidden: true,\n method: 'GET',\n skipAuth: true,\n handler: async (ctx) => {\n const providerServices = ctx.services.getBySuffix<{ getInfo(): Promise<unknown> }>('.provider')\n\n const results = await Promise.all(\n providerServices.map(async ({ service }) => {\n try {\n return await service.getInfo()\n } catch {\n return null\n }\n })\n )\n\n const providers = results.filter((info): info is NonNullable<typeof info> => info !== null)\n const config = getAuthConfig()\n\n return {\n providers,\n registrationEnabled: !config.disableRegistration\n }\n }\n}\n","/**\n * Auth Actions - Shared helpers\n */\n\nimport type { Request, CookieOptions, ModuleContext } from '@gzl10/nexus-sdk'\nimport { getAuthConfig } from '../auth.config.js'\nimport type { RequestInfo } from '../auth.types.js'\n\nexport function getCookieOptions(req: Request): CookieOptions {\n const config = getAuthConfig()\n const isSecure = req.secure || req.get('x-forwarded-proto') === 'https'\n return {\n httpOnly: true,\n secure: isSecure,\n sameSite: isSecure ? 'strict' : 'lax',\n path: '/api/v1',\n maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days\n ...(config.cookieDomain && { domain: config.cookieDomain })\n }\n}\n\nexport function getRequestInfo(req: Request, deviceInfo?: { deviceId?: string; deviceName?: string }): RequestInfo {\n return {\n ip: req.ip ?? req.socket?.remoteAddress,\n userAgent: req.get('user-agent'),\n deviceId: deviceInfo?.deviceId,\n deviceName: deviceInfo?.deviceName\n }\n}\n\nexport function createRateLimits(ctx: ModuleContext) {\n const authConfig = getAuthConfig()\n const { rateLimit } = ctx.core.middleware\n\n return {\n login: rateLimit({\n windowMs: authConfig.rateLimitWindowMs,\n max: authConfig.rateLimitMax,\n message: 'Too many login attempts, please try again later',\n skipSuccessfulRequests: true,\n keyGenerator: (req) => {\n const username = req.body?.username || req.body?.email || ''\n return `${req.ip}:${username}`\n }\n }),\n refresh: rateLimit({\n windowMs: 60 * 1000,\n max: authConfig.rateLimitMax * 2,\n message: 'Too many refresh requests'\n }),\n logout: rateLimit({\n windowMs: 60 * 1000,\n max: authConfig.rateLimitMax,\n message: 'Too many logout requests'\n }),\n admin: rateLimit({\n windowMs: 60 * 1000,\n max: 10,\n message: 'Too many admin requests, please try again later'\n })\n }\n}\n","/**\n * POST /auth/login - Authenticate user\n */\n\nimport type { ActionDefinition } from '@gzl10/nexus-sdk'\nimport type { AuthService } from '../auth.service.js'\nimport type { LoginInput } from '../auth.types.js'\nimport { getCookieOptions, getRequestInfo, createRateLimits } from './helpers.js'\n\nexport const loginAction: ActionDefinition = {\n key: 'login',\n label: { en: 'Login', es: 'Iniciar sesión' },\n icon: 'mdi:login',\n scope: 'module',\n group: { en: 'Authentication', es: 'Autenticación' },\n method: 'POST',\n skipAuth: true,\n middleware: (ctx) => createRateLimits(ctx).login,\n input: {\n email: { input: 'email', required: true, label: { en: 'Email', es: 'Correo electrónico' } },\n password: { input: 'password', required: true, label: { en: 'Password', es: 'Contraseña' } },\n otp: { input: 'text', label: { en: 'OTP Code', es: 'Código OTP' }, validation: { min: 6, max: 6 } },\n deviceId: { input: 'text', label: { en: 'Device ID', es: 'ID de dispositivo' }, validation: { max: 64 } },\n deviceName: { input: 'text', label: { en: 'Device Name', es: 'Nombre del dispositivo' }, validation: { max: 100 } }\n },\n handler: async (ctx, input, req, res) => {\n const body = input as LoginInput\n const authService = ctx.services.get<AuthService>('auth')\n const result = await authService.login(body, getRequestInfo(req!, { deviceId: body.deviceId, deviceName: body.deviceName }))\n\n // Set refresh token in HttpOnly cookie\n res!.cookie('refreshToken', result.refreshToken, getCookieOptions(req!))\n\n return {\n user: result.user,\n accessToken: result.accessToken,\n abilities: result.abilities\n }\n }\n}\n","/**\n * POST /auth/register - Register new user\n */\n\nimport type { ActionDefinition } from '@gzl10/nexus-sdk'\nimport type { AuthService } from '../auth.service.js'\nimport type { RegisterInput } from '../auth.types.js'\nimport { getAuthConfig } from '../auth.config.js'\nimport { getCookieOptions, getRequestInfo, createRateLimits } from './helpers.js'\n\nexport const registerAction: ActionDefinition = {\n key: 'register',\n label: { en: 'Register', es: 'Registrarse' },\n icon: 'mdi:account-plus',\n scope: 'module',\n group: { en: 'Authentication', es: 'Autenticación' },\n method: 'POST',\n skipAuth: true,\n middleware: (ctx) => createRateLimits(ctx).login,\n input: {\n email: { input: 'email', required: true, label: { en: 'Email', es: 'Correo electrónico' } },\n password: { input: 'password', required: true, label: { en: 'Password', es: 'Contraseña' }, validation: { min: 8 } },\n name: { input: 'text', required: true, label: { en: 'Name', es: 'Nombre' }, validation: { min: 2 } },\n otp: { input: 'text', label: { en: 'OTP Code', es: 'Código OTP' }, validation: { min: 6, max: 6 } },\n deviceId: { input: 'text', label: { en: 'Device ID', es: 'ID de dispositivo' }, validation: { max: 64 } },\n deviceName: { input: 'text', label: { en: 'Device Name', es: 'Nombre del dispositivo' }, validation: { max: 100 } }\n },\n handler: async (ctx, input, req, res) => {\n if (getAuthConfig().disableRegistration) {\n throw new ctx.core.errors.ForbiddenError('AUTH_REGISTRATION_DISABLED', 'Registration is disabled')\n }\n\n const body = input as RegisterInput\n const authService = ctx.services.get<AuthService>('auth')\n const result = await authService.register(body, getRequestInfo(req!, { deviceId: body.deviceId, deviceName: body.deviceName }))\n\n // Set refresh token in HttpOnly cookie (auto-login after register)\n res!.cookie('refreshToken', result.refreshToken, getCookieOptions(req!))\n\n // Return 201 status\n res!.status(201)\n\n return {\n user: result.user,\n accessToken: result.accessToken,\n abilities: result.abilities\n }\n }\n}\n","/**\n * POST /auth/forgot-password - Request password reset\n */\n\nimport type { ActionDefinition } from '@gzl10/nexus-sdk'\nimport type { AuthService } from '../auth.service.js'\nimport type { ForgotPasswordInput } from '../auth.types.js'\nimport { getRequestInfo, createRateLimits } from './helpers.js'\n\nexport const forgotPasswordAction: ActionDefinition = {\n key: 'forgot-password',\n label: { en: 'Forgot Password', es: 'Olvidé mi contraseña' },\n icon: 'mdi:lock-question',\n scope: 'module',\n group: { en: 'Authentication', es: 'Autenticación' },\n method: 'POST',\n skipAuth: true,\n middleware: (ctx) => createRateLimits(ctx).login,\n input: {\n email: { input: 'email', required: true, label: { en: 'Email', es: 'Correo electrónico' } }\n },\n handler: async (ctx, input, req) => {\n const body = input as ForgotPasswordInput\n const authService = ctx.services.get<AuthService>('auth')\n await authService.forgotPassword(body, getRequestInfo(req!))\n return { message: 'If this email exists, a verification code has been sent' }\n }\n}\n","/**\n * POST /auth/reset-password - Reset password with OTP\n */\n\nimport type { ActionDefinition } from '@gzl10/nexus-sdk'\nimport type { AuthService } from '../auth.service.js'\nimport type { ResetPasswordInput } from '../auth.types.js'\nimport { getRequestInfo, createRateLimits } from './helpers.js'\n\nexport const resetPasswordAction: ActionDefinition = {\n key: 'reset-password',\n label: { en: 'Reset Password', es: 'Restablecer contraseña' },\n icon: 'mdi:lock-reset',\n scope: 'module',\n group: { en: 'Authentication', es: 'Autenticación' },\n method: 'POST',\n skipAuth: true,\n middleware: (ctx) => createRateLimits(ctx).login,\n input: {\n email: { input: 'email', required: true, label: { en: 'Email', es: 'Correo electrónico' } },\n otp: { input: 'text', required: true, label: { en: 'OTP Code', es: 'Código OTP' }, validation: { min: 6, max: 6 } },\n newPassword: { input: 'password', required: true, label: { en: 'New Password', es: 'Nueva contraseña' }, validation: { min: 8 } }\n },\n handler: async (ctx, input, req) => {\n const body = input as ResetPasswordInput\n const authService = ctx.services.get<AuthService>('auth')\n await authService.resetPassword(body, getRequestInfo(req!))\n return { message: 'Password reset successfully' }\n }\n}\n","/**\n * POST /auth/refresh - Refresh access token\n */\n\nimport type { ActionDefinition } from '@gzl10/nexus-sdk'\nimport type { AuthService } from '../auth.service.js'\nimport { getCookieOptions, getRequestInfo, createRateLimits } from './helpers.js'\n\nexport const refreshAction: ActionDefinition = {\n key: 'refresh',\n label: { en: 'Refresh Token', es: 'Refrescar token' },\n icon: 'mdi:refresh',\n scope: 'module',\n hidden: true,\n method: 'POST',\n skipAuth: true,\n middleware: (ctx) => createRateLimits(ctx).refresh,\n handler: async (ctx, input, req, res) => {\n const refreshToken = req!.cookies?.['refreshToken'] as string | undefined\n if (!refreshToken) {\n throw new ctx.core.errors.UnauthorizedError('Refresh token required')\n }\n\n const authService = ctx.services.get<AuthService>('auth')\n const result = await authService.refresh(refreshToken, getRequestInfo(req!))\n\n res!.cookie('refreshToken', result.refreshToken, getCookieOptions(req!))\n\n return {\n accessToken: result.accessToken,\n abilities: result.abilities\n }\n }\n}\n","/**\n * GET /auth/me - Get current user info\n */\n\nimport type { ActionDefinition, AuthRequest } from '@gzl10/nexus-sdk'\nimport type { AuthService } from '../auth.service.js'\n\nexport const meAction: ActionDefinition = {\n key: 'me',\n label: { en: 'Get Current User', es: 'Obtener usuario actual' },\n icon: 'mdi:account',\n scope: 'module',\n hidden: true,\n method: 'GET',\n middleware: (ctx) => ctx.core.middleware['auth']!,\n handler: async (ctx, _input, req) => {\n const authReq = req as AuthRequest\n const authService = ctx.services.get<AuthService>('auth')\n const result = await authService.me(authReq.user.id)\n\n if (req!.impersonation?.isImpersonating) {\n return { ...result, impersonation: req!.impersonation }\n }\n return result\n }\n}\n","/**\n * POST /auth/logout - Logout current session\n */\n\nimport type { ActionDefinition, AuthRequest } from '@gzl10/nexus-sdk'\nimport type { AuthService } from '../auth.service.js'\nimport { getRequestInfo, createRateLimits } from './helpers.js'\n\nexport const logoutAction: ActionDefinition = {\n key: 'logout',\n label: { en: 'Logout', es: 'Cerrar sesión' },\n icon: 'mdi:logout',\n scope: 'module',\n group: { en: 'Sessions', es: 'Sesiones' },\n method: 'POST',\n middleware: (ctx) => [ctx.core.middleware['auth']!, createRateLimits(ctx).logout],\n input: {\n refreshToken: { input: 'text', label: { en: 'Refresh Token (optional)', es: 'Token de refresco (opcional)' } }\n },\n handler: async (ctx, input, req, res) => {\n const body = input as { refreshToken?: string }\n const refreshToken = body.refreshToken || (req!.cookies?.['refreshToken'] as string | undefined)\n const authReq = req as AuthRequest\n\n const authService = ctx.services.get<AuthService>('auth')\n if (refreshToken) {\n await authService.logout(refreshToken, authReq.user?.id, getRequestInfo(req!))\n }\n\n res!.clearCookie('refreshToken', { path: '/api/v1' })\n res!.status(204).end()\n return undefined\n }\n}\n","/**\n * POST /auth/logout-all - Logout all sessions\n */\n\nimport type { ActionDefinition, AuthRequest } from '@gzl10/nexus-sdk'\nimport type { AuthService } from '../auth.service.js'\nimport { getRequestInfo, createRateLimits } from './helpers.js'\n\nexport const logoutAllAction: ActionDefinition = {\n key: 'logout-all',\n label: { en: 'Logout All Sessions', es: 'Cerrar todas las sesiones' },\n icon: 'mdi:logout-variant',\n scope: 'module',\n group: { en: 'Sessions', es: 'Sesiones' },\n method: 'POST',\n confirm: {\n type: 'simple',\n message: { en: 'This will close all your active sessions on all devices.', es: 'Esto cerrará todas tus sesiones activas en todos los dispositivos.' },\n severity: 'warning'\n },\n middleware: (ctx) => [ctx.core.middleware['auth']!, createRateLimits(ctx).logout],\n handler: async (ctx, _input, req, res) => {\n const authReq = req as AuthRequest\n const authService = ctx.services.get<AuthService>('auth')\n const sessionsRevoked = await authService.logoutAll(authReq.user.id, getRequestInfo(req!))\n\n res!.clearCookie('refreshToken', { path: '/api/v1' })\n\n return { sessionsRevoked }\n }\n}\n","/**\n * GET /auth/sessions - Get active sessions\n */\n\nimport type { ActionDefinition, AuthRequest } from '@gzl10/nexus-sdk'\nimport type { AuthService } from '../auth.service.js'\n\nexport const sessionsAction: ActionDefinition = {\n key: 'sessions',\n label: { en: 'Get Sessions', es: 'Obtener sesiones' },\n icon: 'mdi:devices',\n scope: 'module',\n hidden: true,\n method: 'GET',\n middleware: (ctx) => ctx.core.middleware['auth']!,\n handler: async (ctx, _input, req) => {\n const authReq = req as AuthRequest\n const currentToken = req!.cookies?.['refreshToken'] as string | undefined\n const authService = ctx.services.get<AuthService>('auth')\n return authService.getSessions(authReq.user.id, currentToken)\n }\n}\n","/**\n * DELETE /auth/revoke-session - Revoke specific session\n */\n\nimport type { ActionDefinition, AuthRequest } from '@gzl10/nexus-sdk'\nimport type { AuthService } from '../auth.service.js'\nimport { getRequestInfo } from './helpers.js'\n\nexport const revokeSessionAction: ActionDefinition = {\n key: 'revoke-session',\n label: { en: 'Revoke Session', es: 'Revocar sesión' },\n icon: 'mdi:close-circle',\n scope: 'module',\n hidden: true,\n method: 'DELETE',\n middleware: (ctx) => ctx.core.middleware['auth']!,\n input: {\n sessionId: { input: 'text', required: true, label: { en: 'Session ID', es: 'ID de sesión' } }\n },\n handler: async (ctx, input, req) => {\n const { sessionId } = input as { sessionId: string }\n const authReq = req as AuthRequest\n const currentToken = req!.cookies?.['refreshToken'] as string | undefined\n\n const authService = ctx.services.get<AuthService>('auth')\n\n // Don't allow revoking current session (use logout for that)\n const sessions = await authService.getSessions(authReq.user.id, currentToken)\n const targetSession = sessions.find(s => s.id === sessionId)\n\n if (targetSession?.is_current) {\n throw new ctx.core.errors.AppError('Cannot revoke current session. Use logout instead.', 400)\n }\n\n const revoked = await authService.revokeSession(authReq.user.id, sessionId, getRequestInfo(req!))\n\n if (!revoked) {\n throw new ctx.core.errors.NotFoundError('Session')\n }\n\n return { success: true }\n }\n}\n","/**\n * POST /auth/stop-impersonate - Stop impersonating and restore admin session\n */\n\nimport type { ActionDefinition, AuthRequest } from '@gzl10/nexus-sdk'\nimport type { AuthService } from '../auth.service.js'\nimport { getRequestInfo, createRateLimits } from './helpers.js'\n\nexport const stopImpersonateAction: ActionDefinition = {\n key: 'stop-impersonate',\n label: { en: 'Stop Impersonating', es: 'Dejar de suplantar' },\n icon: 'mdi:account-switch-outline',\n scope: 'module',\n group: { en: 'Sessions', es: 'Sesiones' },\n disabled: (_ctx, req) => !(req as any).impersonation?.isImpersonating,\n disabledReason: { en: 'Not currently impersonating any user', es: 'No está suplantando a ningún usuario' },\n method: 'POST',\n middleware: (ctx) => [ctx.core.middleware['auth']!, createRateLimits(ctx).admin],\n handler: async (ctx, _input, req) => {\n const _authReq = req as AuthRequest\n const impersonation = (req as any).impersonation\n\n if (!impersonation?.isImpersonating) {\n throw new ctx.core.errors.ForbiddenError('Not currently impersonating')\n }\n\n const authService = ctx.services.get<AuthService>('auth')\n return authService.stopImpersonate(impersonation.originalUserId, getRequestInfo(req!))\n }\n}\n","/**\n * POST /auth/create-token - Create a personal access token\n */\n\nimport type { ActionDefinition, AuthRequest } from '@gzl10/nexus-sdk'\nimport type { AuthService } from '../auth.service.js'\nimport { getRequestInfo } from './helpers.js'\n\nexport const createTokenAction: ActionDefinition = {\n key: 'create-token',\n label: { en: 'Create Personal Token', es: 'Crear token personal' },\n icon: 'mdi:key-plus',\n scope: 'module',\n group: { en: 'Personal Tokens', es: 'Tokens personales' },\n method: 'POST',\n middleware: (ctx) => ctx.core.middleware['auth']!,\n input: {\n name: {\n input: 'text',\n required: true,\n label: { en: 'Token Name', es: 'Nombre del token' },\n validation: { max: 100 }\n },\n scope: {\n input: 'text',\n required: true,\n label: { en: 'Permission (readonly or readwrite)', es: 'Permiso (readonly o readwrite)' }\n },\n expires_at: {\n input: 'datetime',\n label: { en: 'Expiration', es: 'Expiración' }\n }\n },\n handler: async (ctx, input, req) => {\n const authReq = req as AuthRequest\n const authService = ctx.services.get<AuthService>('auth')\n return authService.createPersonalToken(\n authReq.user.id,\n input as { name: string; scope: 'readonly' | 'readwrite'; expires_at?: string },\n getRequestInfo(req!)\n )\n }\n}\n","/**\n * GET /auth/list-tokens - List personal access tokens\n */\n\nimport type { ActionDefinition, AuthRequest } from '@gzl10/nexus-sdk'\nimport type { AuthService } from '../auth.service.js'\n\nexport const listTokensAction: ActionDefinition = {\n key: 'list-tokens',\n label: { en: 'List Personal Tokens', es: 'Listar tokens personales' },\n icon: 'mdi:key-chain',\n scope: 'module',\n group: { en: 'Personal Tokens', es: 'Tokens personales' },\n method: 'GET',\n middleware: (ctx) => ctx.core.middleware['auth']!,\n handler: async (ctx, _input, req) => {\n const authReq = req as AuthRequest\n const authService = ctx.services.get<AuthService>('auth')\n return authService.listPersonalTokens(authReq.user.id)\n }\n}\n","/**\n * DELETE /auth/revoke-token - Revoke a personal access token\n */\n\nimport type { ActionDefinition, AuthRequest } from '@gzl10/nexus-sdk'\nimport type { AuthService } from '../auth.service.js'\nimport { getRequestInfo } from './helpers.js'\n\nexport const revokeTokenAction: ActionDefinition = {\n key: 'revoke-token',\n label: { en: 'Revoke Token', es: 'Revocar token' },\n icon: 'mdi:key-remove',\n scope: 'module',\n group: { en: 'Personal Tokens', es: 'Tokens personales' },\n method: 'DELETE',\n confirm: {\n type: 'simple',\n message: {\n en: 'This token will be permanently revoked. Any applications using it will lose access.',\n es: 'Este token será revocado permanentemente. Las aplicaciones que lo usen perderán acceso.'\n },\n severity: 'warning'\n },\n middleware: (ctx) => ctx.core.middleware['auth']!,\n input: {\n tokenId: {\n input: 'text',\n required: true,\n label: { en: 'Token ID', es: 'ID del token' }\n }\n },\n handler: async (ctx, input, req) => {\n const { tokenId } = input as { tokenId: string }\n const authReq = req as AuthRequest\n const authService = ctx.services.get<AuthService>('auth')\n\n const revoked = await authService.revokePersonalToken(\n authReq.user.id,\n tokenId,\n getRequestInfo(req!)\n )\n\n if (!revoked) {\n throw new ctx.core.errors.NotFoundError('Token')\n }\n\n return { success: true }\n }\n}\n","/**\n * Impersonate user - Row action injected into users entity via injectActions.\n * Route: POST /users/impersonate/:id\n */\n\nimport type { ActionDefinition, AuthRequest } from '@gzl10/nexus-sdk'\nimport type { AuthService } from '../auth.service.js'\nimport { getRequestInfo, createRateLimits } from './helpers.js'\n\nexport const impersonateAction: ActionDefinition = {\n key: 'impersonate',\n label: { en: 'Impersonate', es: 'Suplantar' },\n icon: 'mdi:account-switch',\n scope: 'row',\n method: 'POST',\n select: ['id'],\n confirm: {\n type: 'simple',\n message: { en: 'You will impersonate this user. Your admin session will be preserved.', es: 'Suplantarás a este usuario. Tu sesión de admin se preservará.' },\n severity: 'warning'\n },\n casl: { action: 'manage' },\n middleware: (ctx) => createRateLimits(ctx).admin,\n handler: async (ctx, input, req) => {\n const { _record } = input as { _record: { id: string } }\n const authReq = req as AuthRequest\n const authService = ctx.services.get<AuthService>('auth')\n return authService.impersonate(authReq.user.id, _record.id, getRequestInfo(req!))\n }\n}\n","/**\n * Auth Actions - All authentication endpoints as declarative actions\n *\n * Public (no auth):\n * - providers, login, register, forgot-password, reset-password, refresh\n *\n * Protected (requires auth):\n * - me, logout, logout-all, sessions, revoke-session, stop-impersonate\n *\n * Injected into users entity (via injectActions):\n * - impersonate (row action)\n */\n\nimport type { ActionDefinition } from '@gzl10/nexus-sdk'\n\n// Public actions\nexport { providersAction } from './providers.action.js'\nexport { loginAction } from './login.action.js'\nexport { registerAction } from './register.action.js'\nexport { forgotPasswordAction } from './forgot-password.action.js'\nexport { resetPasswordAction } from './reset-password.action.js'\nexport { refreshAction } from './refresh.action.js'\n\n// Protected actions\nexport { meAction } from './me.action.js'\nexport { logoutAction } from './logout.action.js'\nexport { logoutAllAction } from './logout-all.action.js'\nexport { sessionsAction } from './sessions.action.js'\nexport { revokeSessionAction } from './revoke-session.action.js'\nexport { stopImpersonateAction } from './stop-impersonate.action.js'\nexport { createTokenAction } from './create-token.action.js'\nexport { listTokensAction } from './list-tokens.action.js'\nexport { revokeTokenAction } from './revoke-token.action.js'\n\n// Injected row actions (not in authActions array - delivered via injectActions)\nexport { impersonateAction } from './impersonate.action.js'\n\n// Import for array\nimport { providersAction } from './providers.action.js'\nimport { loginAction } from './login.action.js'\nimport { registerAction } from './register.action.js'\nimport { forgotPasswordAction } from './forgot-password.action.js'\nimport { resetPasswordAction } from './reset-password.action.js'\nimport { refreshAction } from './refresh.action.js'\nimport { meAction } from './me.action.js'\nimport { logoutAction } from './logout.action.js'\nimport { logoutAllAction } from './logout-all.action.js'\nimport { sessionsAction } from './sessions.action.js'\nimport { revokeSessionAction } from './revoke-session.action.js'\nimport { stopImpersonateAction } from './stop-impersonate.action.js'\nimport { createTokenAction } from './create-token.action.js'\nimport { listTokensAction } from './list-tokens.action.js'\nimport { revokeTokenAction } from './revoke-token.action.js'\n\n/** All auth actions typed for module actions array */\nexport const authActions: ActionDefinition[] = [\n providersAction,\n loginAction,\n registerAction,\n forgotPasswordAction,\n resetPasswordAction,\n refreshAction,\n meAction,\n logoutAction,\n logoutAllAction,\n sessionsAction,\n revokeSessionAction,\n stopImpersonateAction,\n createTokenAction,\n listTokensAction,\n revokeTokenAction\n]\n","import jwt from 'jsonwebtoken'\nimport crypto from 'crypto'\nimport { getAuthConfig } from './auth.config.js'\n\nexport interface JwtPayload {\n userId: string\n email: string\n roleIds: string[] // Multi-role support (IDs for DB lookups)\n roleNames: string[] // Role names for ability building (ADMIN, EDITOR, etc.)\n impersonatedBy?: string // Admin user ID that started the impersonation\n}\n\nexport interface TokenPair {\n accessToken: string\n refreshToken: string\n}\n\nfunction parseExpiration(exp: string): number {\n const match = exp.match(/^(\\d+)([smhd])$/)\n if (!match) return 900 // 15 min default\n\n const value = parseInt(match[1]!, 10)\n const unit = match[2]\n\n switch (unit) {\n case 's': return value\n case 'm': return value * 60\n case 'h': return value * 60 * 60\n case 'd': return value * 60 * 60 * 24\n default: return 900\n }\n}\n\nexport function generateAccessToken(payload: JwtPayload): string {\n const config = getAuthConfig()\n return jwt.sign(payload, config.secret, {\n expiresIn: config.accessExpires as jwt.SignOptions['expiresIn']\n })\n}\n\nexport function generateRefreshToken(): string {\n return crypto.randomBytes(64).toString('hex')\n}\n\nexport function getRefreshTokenExpiration(): Date {\n const seconds = parseExpiration(getAuthConfig().refreshExpires)\n return new Date(Date.now() + seconds * 1000)\n}\n\nexport function generateTokenPair(payload: JwtPayload): TokenPair {\n return {\n accessToken: generateAccessToken(payload),\n refreshToken: generateRefreshToken()\n }\n}\n\nexport function verifyAccessToken(token: string): JwtPayload {\n return jwt.verify(token, getAuthConfig().secret) as JwtPayload\n}\n","import nodeCrypto from 'node:crypto'\nimport type { Logger } from 'pino'\nimport type { ManagedCache } from '@gzl10/nexus-sdk'\nimport type { MailService } from '../mail/mail.service.js'\nimport type { RequestInfo } from './auth.types.js'\nimport type { ModuleContext } from '@gzl10/nexus-sdk'\n\n/**\n * OTP generation options\n */\nexport interface OTPOptions {\n /** Email address to send OTP to */\n email: string\n /** Type of OTP operation */\n type: 'login' | 'register' | 'reset'\n /** Request metadata (IP, device info) */\n requestInfo?: RequestInfo\n /** TTL in seconds (default 300 = 5 min) */\n ttl?: number\n}\n\n/**\n * Email template configuration for OTP\n */\ninterface OTPEmailTemplate {\n subject: string\n title: string\n message: (code: string) => string\n}\n\n/**\n * OTP Manager - Centralizes OTP generation, validation, and email sending.\n *\n * Eliminates code duplication across login, register, and password reset flows.\n * Handles OTP generation, caching, email formatting, and validation.\n */\nexport class OTPManager {\n private readonly DEFAULT_TTL = 300 // 5 minutes\n\n /** Max failed attempts before OTP is invalidated */\n private readonly MAX_ATTEMPTS = 5\n\n constructor(\n private authCache: ManagedCache<number | { code: string; ip: string; attempts?: number }>,\n private mailService: MailService | undefined,\n private logger: Logger,\n private errors?: ModuleContext['core']['errors']\n ) {}\n\n /**\n * Generates and sends an OTP code via email.\n *\n * @param options - OTP generation options\n * @throws {Error} If OTP generation fails\n * @returns Object with OTP sent status and expiration time\n */\n async generateAndSend(options: OTPOptions): Promise<{ otpSent: boolean; expiresIn: number }> {\n const { email, type, requestInfo, ttl = this.DEFAULT_TTL } = options\n const otpKey = this.getOTPKey(type, email)\n\n // Fail fast if mail service is not available\n if (!this.mailService) {\n throw new Error('Mail service not available')\n }\n\n // Reuse existing OTP code if already generated (resend instead of blocking)\n const existingOtp = await this.authCache.get(otpKey) as { code: string; ip: string; attempts?: number } | null\n const code = existingOtp\n ? existingOtp.code\n : nodeCrypto.randomInt(100000, 999999).toString()\n\n // Store in cache (refreshes TTL on resend)\n await this.authCache.set(otpKey, { code, ip: requestInfo?.ip ?? '' }, ttl)\n\n // Send email (throw on failure so caller knows it wasn't delivered)\n try {\n const template = this.getTemplate(type)\n await this.mailService.send({\n to: email,\n subject: template.subject,\n title: template.title,\n message: template.message(code)\n })\n this.logger.debug({ email, type, ttl, resend: !!existingOtp }, 'OTP sent successfully')\n } catch (mailError) {\n this.logger.warn({ err: mailError, email, type }, 'Failed to send OTP email')\n // Clean up cached OTP since user won't receive it\n await this.authCache.delete(otpKey)\n throw new Error('Failed to send verification email')\n }\n\n return { otpSent: true, expiresIn: ttl }\n }\n\n /**\n * Validates an OTP code.\n *\n * @param email - User email address\n * @param otp - OTP code to validate\n * @param type - OTP operation type\n * @returns True if OTP is valid, false otherwise\n */\n async validate(email: string, otp: string, type: OTPOptions['type']): Promise<boolean> {\n const otpKey = this.getOTPKey(type, email)\n const storedOtp = await this.authCache.get(otpKey) as { code: string; ip: string; attempts?: number } | null\n\n if (!storedOtp) {\n this.logger.debug({ email, type }, 'OTP not found for email')\n return false\n }\n\n if (storedOtp.code !== otp) {\n storedOtp.attempts = (storedOtp.attempts ?? 0) + 1\n if (storedOtp.attempts >= this.MAX_ATTEMPTS) {\n await this.authCache.delete(otpKey)\n this.logger.warn({ email, type, attempts: storedOtp.attempts }, 'OTP invalidated after max failed attempts')\n } else {\n this.logger.warn({ email, type, attempts: storedOtp.attempts }, 'Invalid OTP code attempted')\n }\n return false\n }\n\n return true\n }\n\n /**\n * Clears an OTP from cache after successful validation.\n *\n * @param email - User email address\n * @param type - OTP operation type\n */\n async clear(email: string, type: OTPOptions['type']): Promise<void> {\n const otpKey = this.getOTPKey(type, email)\n await this.authCache.delete(otpKey)\n this.logger.debug({ email, type }, 'OTP cleared from cache')\n }\n\n /**\n * Checks if an OTP exists for a given email and type.\n *\n * @param email - User email address\n * @param type - OTP operation type\n * @returns True if OTP exists, false otherwise\n */\n async exists(email: string, type: OTPOptions['type']): Promise<boolean> {\n const otpKey = this.getOTPKey(type, email)\n return (await this.authCache.get(otpKey)) !== null\n }\n\n /**\n * Gets the cache key for an OTP.\n *\n * @param type - OTP operation type\n * @param email - User email address\n * @returns Cache key string\n */\n private getOTPKey(type: OTPOptions['type'], email: string): string {\n return `${type}_otp:${email}`\n }\n\n /**\n * Gets the email template for an OTP type.\n *\n * @param type - OTP operation type\n * @returns Email template configuration\n */\n private getTemplate(type: OTPOptions['type']): OTPEmailTemplate {\n const codeDisplay = (code: string) => `\n <div style=\"text-align: center; margin: 24px 0;\">\n <span style=\"font-size: 32px; font-weight: bold; letter-spacing: 8px; font-family: monospace; background: #f3f4f6; padding: 16px 24px; border-radius: 8px; display: inline-block;\">${code}</span>\n </div>\n <p style=\"color: #6b7280; font-size: 14px;\">This code expires in 5 minutes. If you didn't request this, ignore this email.</p>\n `\n\n switch (type) {\n case 'login':\n return {\n subject: 'Login Verification Code',\n title: 'Verify Your Identity',\n message: (code) => `<p>Your verification code is:</p>${codeDisplay(code)}`\n }\n\n case 'register':\n return {\n subject: 'Verify Your Email',\n title: 'Complete Your Registration',\n message: (code) => `<p>Your verification code is:</p>${codeDisplay(code)}`\n }\n\n case 'reset':\n return {\n subject: 'Password Reset Code',\n title: 'Reset Your Password',\n message: (code) => `<p>Your password reset code is:</p>${codeDisplay(code)}`\n }\n\n default:\n // Fallback (never reached with correct typing)\n return {\n subject: 'Verification Code',\n title: 'Verify Your Email',\n message: (code) => `<p>Your verification code is:</p>${codeDisplay(code)}`\n }\n }\n }\n}\n","import { createHash, randomBytes } from 'node:crypto'\nimport type { ModuleContext, CollectionEntityDefinition } from '@gzl10/nexus-sdk'\nimport { generateTokenPair, generateAccessToken, getRefreshTokenExpiration } from './jwt.utils.js'\nimport { refreshTokenEntity, authIdentitiesEntity } from './auth.entity.js'\nimport { personalTokenEntity } from './auth.pat.entity.js'\nimport { getAuthConfig } from './auth.config.js'\nimport { OTPManager } from './otp-manager.js'\nimport type { LoginInput, RegisterInput, ForgotPasswordInput, ResetPasswordInput, RequestInfo, UserSession, AuthUserRecord, AuthIdentity, LinkIdentityInput, CreatePersonalTokenInput, PersonalToken, PersonalTokenScope } from './auth.types.js'\nimport type { UsersService } from '../users/users.types.js'\nimport type { MailService } from '../mail/mail.service.js'\nimport type { AuditService } from '../audit/audit.types.js'\n\n// Table names (hardcoded for module isolation - auth depends on users via ctx.services)\nconst USERS = 'users'\nconst REFRESH_TOKENS = (refreshTokenEntity as CollectionEntityDefinition).table\nconst AUTH_IDENTITIES = authIdentitiesEntity.table\nconst PERSONAL_TOKENS = personalTokenEntity.table\n\nexport type AuthService = ReturnType<typeof createAuthService>\n\nexport function createAuthService(ctx: ModuleContext) {\n const { errors, abilities, crypto } = ctx.core\n const { generateId } = ctx.core\n const { nowTimestamp } = ctx.db\n const { verifyPassword, DUMMY_HASH } = crypto\nconst { defineAbilityFor, packRules } = abilities\n const ErrorCodes = errors.codes\n const db = ctx.db.knex\n\n // Rate-limit and OTP cache (no event invalidation — TTL-based only)\n const authCache = ctx.core.cache.create<number | { code: string; ip: string }>('auth:rate-limit', {\n maxEntries: 1000,\n defaultTTL: 900 // 15 min default\n })\n\n // PAT validation cache — invalidated when tokens change\n const patValidationCache = ctx.core.cache.create<{ userId: string; scope: PersonalTokenScope; tokenId: string }>('auth:pat', {\n maxEntries: 500,\n defaultTTL: 60,\n invalidateOn: ['db.personal_access_tokens.*']\n })\n\n // Obtener servicio de usuarios (type-safe desde ctx)\n const usersService = ctx.services.get<UsersService>('users')\n const { roles: rolesService } = usersService\n\n // Inicializar OTP Manager (centraliza generación, validación y envío de OTP)\n const mailService = ctx.services.getOptional<MailService>('mail')\n const otpManager = new OTPManager(authCache, mailService, ctx.core.logger)\n\n // Audit service (core module)\n const auditService = ctx.services.get<AuditService>('audit')\n\n /** Get user with roles (multi-role support) */\n async function getUserWithRoles(userId: string) {\n return usersService.findByIdWithRoles(userId)\n }\n\n /**\n * Checks rate limit and validates OTP if threshold exceeded\n * @returns true if OTP was validated (password check should be skipped)\n * @throws AppError if OTP required but not provided\n * @throws ForbiddenError if OTP invalid\n */\n async function checkRateLimit(\n email: string,\n otp: string | undefined,\n requestInfo?: RequestInfo\n ): Promise<boolean> {\n const authConfig = getAuthConfig()\n const attemptKey = `login_attempts:${email}`\n const ipKey = `login_attempts:ip:${requestInfo?.ip ?? 'unknown'}`\n\n const emailAttempts = (await authCache.get(attemptKey) as number) ?? 0\n const ipAttempts = (await authCache.get(ipKey) as number) ?? 0\n const threshold = authConfig.challengeThreshold\n\n // If threshold exceeded, require OTP\n if (emailAttempts >= threshold || ipAttempts >= threshold) {\n if (!otp) {\n const { otpSent, expiresIn } = await otpManager.generateAndSend({\n email,\n type: 'login',\n requestInfo\n })\n throw new errors.AppError('OTP required', 403, { otpSent, expiresIn })\n }\n\n if (!await otpManager.validate(email, otp, 'login')) {\n throw new errors.ForbiddenError('Invalid OTP')\n }\n\n await otpManager.clear(email, 'login')\n\n // OTP validated - password check should be skipped\n return true\n }\n\n // No OTP required - normal password validation\n return false\n }\n\n /**\n * Authenticates user credentials (timing-safe)\n * @param skipPasswordCheck - If true (OTP was validated), skip password verification\n * @returns Authenticated user record\n * @throws UnauthorizedError if credentials invalid\n */\n async function authenticateUser(\n email: string,\n password: string,\n requestInfo?: RequestInfo,\n skipPasswordCheck = false\n ): Promise<AuthUserRecord> {\n const attemptKey = `login_attempts:${email}`\n const ipKey = `login_attempts:ip:${requestInfo?.ip ?? 'unknown'}`\n\n const user = await db<AuthUserRecord>(USERS).where({ email }).first()\n\n if (!user) {\n // Timing-safe: always run bcrypt to prevent user enumeration via response time\n await verifyPassword(password, DUMMY_HASH)\n ctx.events.notify('auth.failed', { email, reason: 'user_not_found' })\n auditService.log({ source: 'core:auth', action: 'login_failed', actorEmail: email, ip: requestInfo?.ip, userAgent: requestInfo?.userAgent, metadata: { reason: 'user_not_found' } })\n throw new errors.UnauthorizedError(ErrorCodes['AUTH_INVALID_CREDENTIALS'], 'Invalid credentials')\n }\n\n // Block login for non-human accounts (bots and services cannot login via password)\n if (user.type !== 'human') {\n ctx.events.notify('auth.failed', { email, reason: 'non_human_login_attempt' })\n auditService.log({ source: 'core:auth', action: 'login_failed', actorEmail: email, ip: requestInfo?.ip, userAgent: requestInfo?.userAgent, metadata: { reason: 'non_human_login_attempt', userType: user.type } })\n throw new errors.UnauthorizedError(ErrorCodes['AUTH_INVALID_CREDENTIALS'], 'Invalid credentials')\n }\n\n // If OTP was validated, skip password check\n if (skipPasswordCheck) {\n // Clear attempt counters on success\n await authCache.delete(attemptKey)\n await authCache.delete(ipKey)\n return user\n }\n\n // Normal password validation (timing-safe)\n const emailAttempts = (await authCache.get(attemptKey) as number) ?? 0\n const ipAttempts = (await authCache.get(ipKey) as number) ?? 0\n\n // Timing-safe: siempre ejecutar bcrypt aunque usuario no exista\n // Note: password is guaranteed non-null for human users (validated above)\n const hashToCheck = user.password!\n const validPassword = await verifyPassword(password, hashToCheck)\n\n if (!validPassword) {\n ctx.events.notify('auth.failed', { email, reason: 'invalid_password' })\n\n // Increment attempt counters\n await authCache.set(attemptKey, emailAttempts + 1, 900) // 15 min TTL\n await authCache.set(ipKey, ipAttempts + 1, 900)\n\n auditService.log({ source: 'core:auth', action: 'login_failed', actorEmail: email, ip: requestInfo?.ip, userAgent: requestInfo?.userAgent, metadata: { reason: 'invalid_password' } })\n\n throw new errors.UnauthorizedError(ErrorCodes['AUTH_INVALID_CREDENTIALS'], 'Invalid credentials')\n }\n\n // Clear attempt counters on success\n await authCache.delete(attemptKey)\n await authCache.delete(ipKey)\n\n return user\n }\n\n /**\n * Creates user session with tokens and abilities\n */\n async function createUserSession(\n user: AuthUserRecord,\n requestInfo?: RequestInfo\n ) {\n const roleIds = await usersService.getRoleIds(user.id)\n const roleNames = await usersService.getRoleNames(user.id)\n\n // Note: email is guaranteed non-null for human users (only humans can login)\n const tokens = generateTokenPair({\n userId: user.id,\n email: user.email!,\n roleIds,\n roleNames\n })\n\n await db(REFRESH_TOKENS).insert({\n id: generateId(),\n token: tokens.refreshToken,\n user_id: user.id,\n expires_at: getRefreshTokenExpiration(),\n device_id: requestInfo?.deviceId ?? null,\n device_name: requestInfo?.deviceName ?? null\n })\n\n const ability = await defineAbilityFor(user, roleNames)\n const userWithRoles = await getUserWithRoles(user.id)\n\n ctx.events.notify('auth.login', { userId: user.id, email: user.email! })\n auditService.log({ source: 'core:auth', action: 'login', actorId: user.id, actorEmail: user.email ?? undefined, ip: requestInfo?.ip, userAgent: requestInfo?.userAgent })\n\n return {\n user: userWithRoles,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n abilities: packRules(ability)\n }\n }\n\n return {\n async login(input: LoginInput, requestInfo?: RequestInfo) {\n const otpValidated = await checkRateLimit(input.email, input.otp, requestInfo)\n const user = await authenticateUser(input.email, input.password, requestInfo, otpValidated)\n return createUserSession(user, requestInfo)\n },\n\n async register(input: RegisterInput, requestInfo?: RequestInfo) {\n const authConfig = getAuthConfig()\n\n // Require OTP for registration (email verification) unless skipped for tests\n if (!input.otp && !authConfig.skipRegisterOtp) {\n // Generate and send OTP if it doesn't exist\n const { otpSent, expiresIn } = await otpManager.generateAndSend({\n email: input.email,\n type: 'register',\n requestInfo\n })\n\n throw new errors.AppError('OTP required', 403, { otpSent, expiresIn })\n }\n\n // Validate OTP (skip if configured for testing)\n if (!authConfig.skipRegisterOtp) {\n if (!await otpManager.validate(input.email, input.otp!, 'register')) {\n throw new errors.ForbiddenError('Invalid OTP')\n }\n // OTP valid, clear it\n await otpManager.clear(input.email, 'register')\n }\n\n // Buscar rol por defecto (VIEWER)\n const defaultRole = await rolesService.findByName('VIEWER')\n if (!defaultRole) {\n throw new errors.AppError('Default role VIEWER not found. Run migrations.', 500)\n }\n\n // Crear usuario (usersService hace hash de password y valida email único)\n const user = await usersService.create({\n email: input.email,\n password: input.password,\n name: input.name\n })\n\n // Assign default role via pivot table\n await usersService.assignRole(user.id, defaultRole.id)\n\n // Generar tokens (email is guaranteed for registration)\n const tokens = generateTokenPair({\n userId: user.id,\n email: user.email!,\n roleIds: [defaultRole.id],\n roleNames: [defaultRole.name]\n })\n\n // Guardar refresh token en DB\n await db(REFRESH_TOKENS).insert({\n id: generateId(),\n token: tokens.refreshToken,\n user_id: user.id,\n expires_at: getRefreshTokenExpiration(),\n device_id: requestInfo?.deviceId ?? null,\n device_name: requestInfo?.deviceName ?? null\n })\n\n // Build ability from role names (permissions are static in entity definitions)\n const ability = await defineAbilityFor(user, [defaultRole.name])\n\n // Obtener usuario con roles\n const userWithRoles = await getUserWithRoles(user.id)\n\n ctx.events.notify('auth.login', { userId: user.id, email: user.email! })\n\n // Persistir evento de registro\n auditService.log({ source: 'core:auth', action: 'register', actorId: user.id, actorEmail: user.email ?? undefined, ip: requestInfo?.ip, userAgent: requestInfo?.userAgent })\n\n return {\n user: userWithRoles,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n abilities: packRules(ability)\n }\n },\n\n async refresh(refreshToken: string, requestInfo?: RequestInfo) {\n // Validate + delete old token atomically to prevent concurrent reuse.\n // The DELETE inside the transaction ensures a second request with the same\n // token will find it missing and fail immediately.\n const validated = await db.transaction(async (trx) => {\n let query = trx(REFRESH_TOKENS).where({ token: refreshToken })\n const client = db.client.config.client as string\n if (!client.includes('sqlite')) {\n query = query.forUpdate()\n }\n const storedToken = await query.first()\n\n if (!storedToken) {\n throw new errors.UnauthorizedError('Refresh token inválido')\n }\n\n if (new Date(storedToken.expires_at) < new Date()) {\n await trx(REFRESH_TOKENS).where({ id: storedToken.id }).delete()\n throw new errors.UnauthorizedError('Refresh token expirado')\n }\n\n const user = await trx<AuthUserRecord>(USERS).where({ id: storedToken.user_id }).first()\n if (!user) {\n throw new errors.UnauthorizedError('Usuario no encontrado')\n }\n\n // Delete consumed token inside the same transaction (closes race condition)\n await trx(REFRESH_TOKENS).where({ id: storedToken.id }).delete()\n\n return { user, storedToken }\n })\n\n // Roles and token generation outside transaction to avoid SQLite deadlock\n const roleIds = await usersService.getRoleIds(validated.user.id)\n const roleNames = await usersService.getRoleNames(validated.user.id)\n\n const tokens = generateTokenPair({\n userId: validated.user.id,\n email: validated.user.email!,\n roleIds,\n roleNames\n })\n\n // Insert new refresh token (no race risk — old token already consumed)\n await db(REFRESH_TOKENS).insert({\n id: generateId(),\n token: tokens.refreshToken,\n user_id: validated.user.id,\n expires_at: getRefreshTokenExpiration(),\n device_id: validated.storedToken.device_id ?? null,\n device_name: validated.storedToken.device_name ?? null\n })\n\n const ability = await defineAbilityFor(validated.user, roleNames)\n\n ctx.events.notify('auth.refresh', { userId: validated.user.id })\n\n auditService.log({ source: 'core:auth', action: 'refresh', actorId: validated.user.id, ip: requestInfo?.ip, userAgent: requestInfo?.userAgent })\n\n return {\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n abilities: packRules(ability)\n }\n },\n\n async logout(refreshToken: string, userId?: string, requestInfo?: RequestInfo) {\n const deleted = await db(REFRESH_TOKENS).where({ token: refreshToken }).delete()\n if (deleted > 0 && userId) {\n ctx.events.notify('auth.logout', { userId })\n\n auditService.log({ source: 'core:auth', action: 'logout', actorId: userId, ip: requestInfo?.ip, userAgent: requestInfo?.userAgent })\n }\n return deleted > 0\n },\n\n async logoutAll(userId: string, requestInfo?: RequestInfo) {\n const deleted = await db(REFRESH_TOKENS).where({ user_id: userId }).delete()\n\n ctx.events.notify('auth.logout', { userId })\n\n auditService.log({ source: 'core:auth', action: 'logout', actorId: userId, ip: requestInfo?.ip, userAgent: requestInfo?.userAgent, metadata: { allDevices: true, sessionsRevoked: deleted } })\n\n return deleted\n },\n\n async me(userId: string) {\n const userWithRoles = await getUserWithRoles(userId)\n\n if (!userWithRoles) {\n throw new errors.NotFoundError('Usuario')\n }\n\n // Load role names for ability building\n const roleNames = await usersService.getRoleNames(userId)\n const ability = await defineAbilityFor(userWithRoles, roleNames)\n\n return {\n user: userWithRoles,\n abilities: packRules(ability)\n }\n },\n\n async forgotPassword(input: ForgotPasswordInput, requestInfo?: RequestInfo) {\n // Always show success message for security (don't reveal if email exists)\n const user = await db<AuthUserRecord>(USERS).where({ email: input.email }).first()\n\n if (user) {\n // Generate and send password reset OTP\n await otpManager.generateAndSend({\n email: input.email,\n type: 'reset',\n requestInfo\n })\n }\n\n // Always return success (security: don't reveal if email exists)\n return { sent: true }\n },\n\n async resetPassword(input: ResetPasswordInput, requestInfo?: RequestInfo) {\n // Validate OTP\n if (!await otpManager.validate(input.email, input.otp, 'reset')) {\n throw new errors.ForbiddenError('Invalid or expired verification code')\n }\n\n // Find user\n const user = await db<AuthUserRecord>(USERS).where({ email: input.email }).first()\n if (!user) {\n throw new errors.NotFoundError('User')\n }\n\n // Hash new password and update\n const hashedPassword = await crypto.hashPassword(input.newPassword)\n await db(USERS).where({ id: user.id }).update({\n password: hashedPassword,\n updated_at: nowTimestamp(db)\n })\n\n // Clear OTP\n await otpManager.clear(input.email, 'reset')\n\n // Invalidate all refresh tokens (force re-login on all devices)\n await db(REFRESH_TOKENS).where({ user_id: user.id }).delete()\n\n auditService.log({ source: 'core:auth', action: 'password_reset', actorId: user.id, actorEmail: user.email ?? undefined, ip: requestInfo?.ip, userAgent: requestInfo?.userAgent })\n\n return { success: true }\n },\n\n async cleanupExpiredTokens() {\n const deleted = await db(REFRESH_TOKENS)\n .where('expires_at', '<', nowTimestamp(db))\n .delete()\n return deleted\n },\n\n async getSessions(userId: string, currentToken?: string): Promise<UserSession[]> {\n const tokens = await db(REFRESH_TOKENS)\n .where({ user_id: userId })\n .where('expires_at', '>', new Date())\n .select('id', 'device_id', 'device_name', 'token', 'created_at', 'expires_at')\n .orderBy('created_at', 'desc')\n\n return tokens.map(t => ({\n id: t.id,\n device_id: t.device_id ?? null,\n device_name: t.device_name ?? null,\n created_at: t.created_at,\n expires_at: t.expires_at,\n is_current: currentToken ? t.token === currentToken : false\n }))\n },\n\n async revokeSession(userId: string, sessionId: string, requestInfo?: RequestInfo): Promise<boolean> {\n // Solo permite revocar sesiones del propio usuario\n const deleted = await db(REFRESH_TOKENS)\n .where({ id: sessionId, user_id: userId })\n .delete()\n\n if (deleted > 0) {\n auditService.log({ source: 'core:auth', action: 'logout', actorId: userId, ip: requestInfo?.ip, userAgent: requestInfo?.userAgent, metadata: { sessionId, revokedByUser: true } })\n }\n\n return deleted > 0\n },\n\n async impersonate(adminUserId: string, targetUserId: string, requestInfo?: RequestInfo) {\n // Verify admin has ADMIN/OWNER role\n const adminRoleNames = await usersService.getRoleNames(adminUserId)\n const SUPERUSER_ROLES = ['ADMIN', 'OWNER']\n if (!adminRoleNames.some(r => SUPERUSER_ROLES.includes(r))) {\n throw new errors.ForbiddenError('Only administrators can impersonate users')\n }\n\n // Cannot impersonate yourself\n if (adminUserId === targetUserId) {\n throw new errors.ForbiddenError('Cannot impersonate yourself')\n }\n\n // Verify target is not an admin\n const targetRoleNames = await usersService.getRoleNames(targetUserId)\n if (targetRoleNames.some(r => SUPERUSER_ROLES.includes(r))) {\n throw new errors.ForbiddenError('Cannot impersonate administrators')\n }\n\n // Load target user\n const targetUser = await db<AuthUserRecord>(USERS).where({ id: targetUserId }).first()\n if (!targetUser) {\n throw new errors.NotFoundError('User')\n }\n\n // Generate access token with impersonation marker (no refresh token)\n const targetRoleIds = await usersService.getRoleIds(targetUserId)\n const accessToken = generateAccessToken({\n userId: targetUserId,\n email: targetUser.email!,\n roleIds: targetRoleIds,\n roleNames: targetRoleNames,\n impersonatedBy: adminUserId\n })\n\n // Build abilities for target user\n const ability = await defineAbilityFor(targetUser, targetRoleNames)\n const targetWithRoles = await getUserWithRoles(targetUserId)\n\n auditService.log({ source: 'core:auth', action: 'impersonate_start', actorId: adminUserId, ip: requestInfo?.ip, userAgent: requestInfo?.userAgent, resourceType: 'user', resourceId: targetUserId, metadata: { targetEmail: targetUser.email } })\n\n return {\n user: targetWithRoles,\n accessToken,\n abilities: packRules(ability),\n impersonation: { isImpersonating: true, originalUserId: adminUserId }\n }\n },\n\n async stopImpersonate(adminUserId: string, requestInfo?: RequestInfo) {\n // Reload admin data\n const adminUser = await db<AuthUserRecord>(USERS).where({ id: adminUserId }).first()\n if (!adminUser) {\n throw new errors.NotFoundError('Admin user')\n }\n\n const adminRoleNames = await usersService.getRoleNames(adminUserId)\n const ability = await defineAbilityFor(adminUser, adminRoleNames)\n const adminWithRoles = await getUserWithRoles(adminUserId)\n\n auditService.log({ source: 'core:auth', action: 'impersonate_stop', actorId: adminUserId, ip: requestInfo?.ip, userAgent: requestInfo?.userAgent })\n\n return {\n user: adminWithRoles,\n abilities: packRules(ability)\n }\n },\n\n async resetCache() { await authCache.clear() },\n\n // === GDPR / Cross-module isolation methods ===\n\n /** Revoke all user sessions (for account deletion) */\n async revokeAllUserSessions(userId: string): Promise<number> {\n return db(REFRESH_TOKENS).where({ user_id: userId }).delete()\n },\n\n /** Anonymize user audit records (GDPR Art. 17 - keep for stats, remove PII) */\n async anonymizeUserAudit(userId: string): Promise<number> {\n return db('audit_log')\n .where({ actor_id: userId })\n .update({\n actor_id: null,\n actor_email: null,\n ip_address: '[ANONYMIZED]',\n user_agent: '[ANONYMIZED]'\n })\n },\n\n /** Get user auth data for GDPR export (Art. 15) */\n async getUserAuthData(userId: string): Promise<{ sessions: unknown[]; auditLog: unknown[] }> {\n const sessions = await db(REFRESH_TOKENS)\n .select('id', 'device_name', 'device_id', 'last_used_at', 'created_at', 'expires_at')\n .where({ user_id: userId })\n .orderBy('created_at', 'desc')\n\n const auditLog = await auditService.query({\n actorId: userId,\n source: 'core:auth',\n limit: 100\n })\n\n return { sessions, auditLog }\n },\n\n // === Identity Management (for auth plugins) ===\n\n /** Find an external identity by provider and provider user ID */\n async findIdentity(provider: string, providerUserId: string): Promise<AuthIdentity | undefined> {\n return db(AUTH_IDENTITIES)\n .where({ provider, provider_user_id: providerUserId })\n .first()\n },\n\n /** Find all identities for a user, optionally filtered by provider */\n async findIdentitiesByUser(userId: string, provider?: string): Promise<AuthIdentity[]> {\n const query = db(AUTH_IDENTITIES).where({ user_id: userId })\n if (provider) query.where({ provider })\n return query\n },\n\n /** Link an external identity to a Nexus user */\n async linkIdentity(input: LinkIdentityInput): Promise<AuthIdentity> {\n const now = nowTimestamp(db)\n const record = {\n id: generateId(),\n user_id: input.userId,\n provider: input.provider,\n provider_user_id: input.providerUserId,\n provider_email: input.providerEmail ?? null,\n metadata: input.metadata ? JSON.stringify(input.metadata) : null,\n linked_at: now,\n last_login_at: now,\n }\n try {\n await db(AUTH_IDENTITIES).insert(record)\n } catch (e: unknown) {\n const msg = e instanceof Error ? e.message : String(e)\n if (msg.includes('UNIQUE') || msg.includes('unique') || msg.includes('duplicate')) {\n throw new errors.ConflictError(`Identity already linked: ${input.provider}/${input.providerUserId}`)\n }\n throw e\n }\n return record as unknown as AuthIdentity\n },\n\n /** Unlink an external identity */\n async unlinkIdentity(provider: string, providerUserId: string): Promise<boolean> {\n const deleted = await db(AUTH_IDENTITIES)\n .where({ provider, provider_user_id: providerUserId })\n .delete()\n return deleted > 0\n },\n\n /** Update last login timestamp for an identity */\n async updateIdentityLogin(provider: string, providerUserId: string): Promise<void> {\n await db(AUTH_IDENTITIES)\n .where({ provider, provider_user_id: providerUserId })\n .update({ last_login_at: nowTimestamp(db) })\n },\n\n // === Personal Access Tokens ===\n\n async createPersonalToken(userId: string, input: CreatePersonalTokenInput, requestInfo?: RequestInfo) {\n const rawToken = 'nxs_' + randomBytes(32).toString('hex')\n const tokenHash = createHash('sha256').update(rawToken).digest('hex')\n const tokenPrefix = 'nxs_...' + rawToken.slice(-6)\n\n const id = generateId()\n const now = nowTimestamp(db)\n\n await db(PERSONAL_TOKENS).insert({\n id,\n user_id: userId,\n name: input.name,\n token_prefix: tokenPrefix,\n token_hash: tokenHash,\n scope: input.scope,\n expires_at: input.expires_at ?? null,\n last_used_at: null,\n created_at: now,\n updated_at: now\n })\n\n auditService.log({ source: 'core:auth', action: 'pat_created', actorId: userId, ip: requestInfo?.ip, userAgent: requestInfo?.userAgent, metadata: { tokenName: input.name, scope: input.scope } })\n\n return {\n id,\n token: rawToken,\n name: input.name,\n scope: input.scope,\n expires_at: input.expires_at ?? null,\n created_at: now\n }\n },\n\n async listPersonalTokens(userId: string) {\n return db(PERSONAL_TOKENS)\n .where({ user_id: userId })\n .select('id', 'name', 'token_prefix', 'scope', 'expires_at', 'last_used_at', 'created_at')\n .orderBy('created_at', 'desc')\n },\n\n async revokePersonalToken(userId: string, tokenId: string, requestInfo?: RequestInfo): Promise<boolean> {\n const deleted = await db(PERSONAL_TOKENS)\n .where({ id: tokenId, user_id: userId })\n .delete()\n\n if (deleted > 0) {\n auditService.log({ source: 'core:auth', action: 'pat_revoked', actorId: userId, ip: requestInfo?.ip, userAgent: requestInfo?.userAgent, metadata: { tokenId } })\n }\n\n return deleted > 0\n },\n\n async validatePersonalToken(rawToken: string): Promise<{ userId: string; scope: PersonalTokenScope; tokenId: string } | null> {\n const tokenHash = createHash('sha256').update(rawToken).digest('hex')\n\n // Check cache first (key: hash, value: validation result, TTL: 60s)\n const cacheKey = `pat:${tokenHash}`\n const cached = await patValidationCache.get(cacheKey)\n if (cached) {\n return cached\n }\n\n const token = await db<PersonalToken>(PERSONAL_TOKENS)\n .where({ token_hash: tokenHash })\n .first()\n\n if (!token) return null\n\n // Check expiration\n if (token.expires_at && new Date(token.expires_at) < new Date()) {\n return null\n }\n\n const result = { userId: token.user_id, scope: token.scope, tokenId: token.id }\n\n // Cache result (60s TTL)\n await patValidationCache.set(cacheKey, result, 60)\n\n // Update last_used_at (fire-and-forget, throttled: only if not recently updated)\n const usageKey = `pat_usage:${token.id}`\n if (!await authCache.get(usageKey)) {\n await authCache.set(usageKey, 1 as any, 60) // Throttle: max once per 60s\n db(PERSONAL_TOKENS)\n .where({ id: token.id })\n .update({ last_used_at: nowTimestamp(db) })\n .catch(() => {}) // Fire-and-forget\n }\n\n return result\n },\n\n /** Revoke all personal tokens for a user (for account deletion) */\n async revokeAllPersonalTokens(userId: string): Promise<number> {\n return db(PERSONAL_TOKENS).where({ user_id: userId }).delete()\n },\n\n // === Auth Plugin Helpers ===\n\n /** Find a user by email (for auth plugins to check if user exists) */\n async findUserByEmail(email: string): Promise<{ id: string; email: string; name?: string } | null> {\n const user = await db<AuthUserRecord>(USERS).where({ email }).first()\n if (!user) return null\n return { id: user.id, email: user.email!, name: user.name ?? undefined }\n },\n\n /** Find a user by ID (for auth plugins) */\n async findUserById(id: string): Promise<{ id: string; email: string; name?: string } | null> {\n const user = await db<AuthUserRecord>(USERS).where({ id }).first()\n if (!user) return null\n return { id: user.id, email: user.email!, name: user.name ?? undefined }\n },\n\n /** Create a new user without password (for auth plugins using external providers) */\n async createUser(data: { email: string; name?: string; role?: string }): Promise<{ id: string; email: string; name?: string }> {\n if (getAuthConfig().disableAutoCreate) {\n throw new errors.ForbiddenError('AUTH_AUTO_CREATE_DISABLED', 'Auto-creation of users is disabled')\n }\n\n const user = await usersService.create({\n email: data.email,\n password: null, // OIDC users authenticate via external provider, no password needed\n name: data.name,\n type: 'human'\n })\n\n // Assign role if specified\n if (data.role) {\n const role = await rolesService.findByName(data.role)\n if (role) {\n await usersService.assignRole(user.id, role.id)\n }\n } else {\n // Assign default VIEWER role\n const viewerRole = await rolesService.findByName('VIEWER')\n if (viewerRole) {\n await usersService.assignRole(user.id, viewerRole.id)\n }\n }\n\n auditService.log({ source: 'core:auth', action: 'register', actorId: user.id, actorEmail: data.email, metadata: { source: 'oauth_plugin', role: data.role || 'VIEWER' } })\n\n return { id: user.id, email: user.email!, name: user.name ?? undefined }\n },\n\n /** Create session tokens for a user (for auth plugins after external authentication) */\n async createTokens(user: { id: string }): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {\n const fullUser = await db<AuthUserRecord>(USERS).where({ id: user.id }).first()\n if (!fullUser) throw new errors.NotFoundError('User')\n\n const roleIds = await usersService.getRoleIds(user.id)\n const roleNames = await usersService.getRoleNames(user.id)\n\n const tokens = generateTokenPair({\n userId: fullUser.id,\n email: fullUser.email!,\n roleIds,\n roleNames\n })\n\n await db(REFRESH_TOKENS).insert({\n id: generateId(),\n token: tokens.refreshToken,\n user_id: fullUser.id,\n expires_at: getRefreshTokenExpiration()\n })\n\n // Parse expiresIn from config (e.g., '15m' → 900)\n const config = getAuthConfig()\n const expMatch = config.accessExpires.match(/^(\\d+)([smhd])$/)\n let expiresIn = 900\n if (expMatch) {\n const val = parseInt(expMatch[1]!, 10)\n const unit = expMatch[2]\n expiresIn = val * (unit === 's' ? 1 : unit === 'm' ? 60 : unit === 'h' ? 3600 : 86400)\n }\n\n auditService.log({ source: 'core:auth', action: 'oauth_login', actorId: fullUser.id, actorEmail: fullUser.email! })\n\n return {\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresIn\n }\n },\n\n async updateUser(id: string, data: { email?: string; name?: string; is_active?: boolean }): Promise<void> {\n const updates: Record<string, unknown> = {}\n if (data.email !== undefined) updates['email'] = data.email\n if (data.name !== undefined) updates['name'] = data.name\n if (data.is_active !== undefined) updates['is_active'] = data.is_active\n if (Object.keys(updates).length > 0) {\n await db(USERS).where({ id }).update(updates)\n }\n },\n\n async deleteUser(id: string): Promise<void> {\n await db(USERS).where({ id }).delete()\n }\n }\n}\n","import { z } from 'zod'\n\n/**\n * Auth module types and schemas\n */\n\n// ============================================================================\n// Validation Schemas\n// ============================================================================\n\nexport const loginSchema = z.object({\n email: z.string().email('Invalid email'),\n password: z.string().min(1, 'Password required'),\n otp: z.string().length(6).optional(),\n deviceId: z.string().max(64).optional(),\n deviceName: z.string().max(100).optional()\n})\n\nexport type LoginInput = z.infer<typeof loginSchema>\n\nexport const registerSchema = z.object({\n email: z.string().email('Invalid email'),\n password: z.string().min(8, 'Password must be at least 8 characters'),\n name: z.string().min(2, 'Name must be at least 2 characters'),\n otp: z.string().length(6).optional(),\n deviceId: z.string().max(64).optional(),\n deviceName: z.string().max(100).optional()\n})\n\nexport type RegisterInput = z.infer<typeof registerSchema>\n\nexport const forgotPasswordSchema = z.object({\n email: z.string().email('Invalid email')\n})\n\nexport type ForgotPasswordInput = z.infer<typeof forgotPasswordSchema>\n\nexport const resetPasswordSchema = z.object({\n email: z.string().email('Invalid email'),\n otp: z.string().length(6, 'OTP must be 6 digits'),\n newPassword: z.string().min(8, 'Password must be at least 8 characters')\n})\n\nexport type ResetPasswordInput = z.infer<typeof resetPasswordSchema>\n\n// ============================================================================\n// Interfaces\n// ============================================================================\n\n/**\n * Request info for audit and device tracking\n */\nexport interface RequestInfo {\n ip?: string\n userAgent?: string\n deviceId?: string\n deviceName?: string\n}\n\n/**\n * Local type for user data - auth does not import from users module.\n * Defines the fields auth needs for authentication.\n * Note: roles are loaded separately via users service (multi-role support).\n */\nexport interface AuthUserRecord {\n id: string\n type: 'human' | 'bot' | 'service'\n email: string | null\n password: string | null\n name: string\n created_at: Date\n updated_at: Date\n created_by: string | null\n updated_by: string | null\n [key: string]: unknown\n}\n\n/**\n * Refresh token for JWT rotation\n */\nexport interface RefreshToken {\n id: string\n token: string\n user_id: string\n expires_at: Date\n device_id?: string | null\n device_name?: string | null\n created_at?: Date\n}\n\n\n\n/**\n * Active user session (for listing devices)\n */\nexport interface UserSession {\n id: string\n device_id: string | null\n device_name: string | null\n created_at: Date\n expires_at: Date\n is_current: boolean\n}\n\n/**\n * External auth identity linking a Nexus user to an OIDC/OAuth provider\n */\nexport interface AuthIdentity {\n id: string\n user_id: string\n provider: string\n provider_user_id: string\n provider_email: string | null\n metadata: Record<string, unknown> | null\n linked_at: string\n last_login_at: string | null\n created_at?: string\n updated_at?: string\n}\n\n/**\n * Input for linking an external identity to a Nexus user\n */\nexport interface LinkIdentityInput {\n userId: string\n provider: string\n providerUserId: string\n providerEmail?: string | null\n metadata?: Record<string, unknown>\n}\n\n// ============================================================================\n// Personal Access Tokens\n// ============================================================================\n\nexport const createPersonalTokenSchema = z.object({\n name: z.string().min(1).max(100),\n scope: z.enum(['readonly', 'readwrite']),\n expires_at: z.string().datetime().optional()\n})\n\nexport type CreatePersonalTokenInput = z.infer<typeof createPersonalTokenSchema>\n\nexport type PersonalTokenScope = 'readonly' | 'readwrite'\n\nexport interface PersonalToken {\n id: string\n user_id: string\n name: string\n token_prefix: string\n token_hash: string\n scope: PersonalTokenScope\n expires_at: Date | null\n last_used_at: Date | null\n created_at: Date\n}\n","/**\n * @module auth\n * @description JWT authentication with refresh tokens, sessions, and impersonation\n */\n\nimport type { ModuleManifest } from '@gzl10/nexus-sdk'\nimport { createAuthRoutes } from './auth.routes.js'\nimport { createAuthMiddlewares } from './auth.middleware.js'\nimport { refreshTokenEntity, authIdentitiesEntity } from './auth.entity.js'\nimport { personalTokenEntity } from './auth.pat.entity.js'\nimport { authActions, impersonateAction } from './actions/index.js'\nimport { createAuthService } from './auth.service.js'\n\n// Re-export types del módulo\nexport type { RefreshToken, UserSession, AuthUserRecord, RequestInfo, AuthIdentity, LinkIdentityInput, CreatePersonalTokenInput, PersonalTokenScope, PersonalToken } from './auth.types.js'\n// Re-export schemas para validación externa\nexport { loginSchema, registerSchema, createPersonalTokenSchema } from './auth.types.js'\nexport type { LoginInput, RegisterInput } from './auth.types.js'\n\nexport const authModule: ModuleManifest = {\n name: 'auth',\n label: { en: 'Authentication', es: 'Autenticación' },\n icon: 'mdi:lock-outline',\n description: { en: 'JWT authentication with refresh tokens, session management, and password security', es: 'Autenticación JWT con tokens de refresco, gestión de sesiones y seguridad de contraseñas' },\n type: 'core',\n category: 'security',\n dependencies: ['logger', 'users', 'audit'],\n definitions: [\n refreshTokenEntity,\n authIdentitiesEntity,\n personalTokenEntity\n ],\n\n actions: authActions,\n\n pages: [\n {\n id: 'user-profile',\n label: { en: 'My Profile', es: 'Mi perfil' },\n icon: 'mdi:account-outline',\n type: 'custom',\n component: 'ProfilePage',\n standalone: true,\n order: 0\n }\n ],\n // migrate: usa migración generada desde definitions\n init: (ctx) => {\n // Create and register auth service singleton\n const authService = createAuthService(ctx)\n ctx.services.register('auth', authService)\n\n // Registrar middlewares para uso de otros módulos (shared ability cache)\n const { auth, optionalAuth } = createAuthMiddlewares(ctx)\n ctx.core.middleware['auth'] = auth\n ctx.core.middleware['optionalAuth'] = optionalAuth\n },\n // Custom routes only for entities that need special mounting (tokens, config)\n routes: createAuthRoutes,\n routePrefix: '/auth',\n\n // Import dinámico para evitar ciclos\n seed: async (ctx) => {\n const { seed } = await import('./auth.seed.js')\n await seed(ctx)\n },\n\n injectActions: [\n { target: { module: 'users', entity: 'users' }, action: impersonateAction }\n ]\n}\n","import { z } from 'zod'\nimport type { MailConfig } from './mail.types.js'\n\n// Re-export type for backwards compatibility\nexport type { MailConfig } from './mail.types.js'\n\n/**\n * Environment variable schema for mail.\n * Completely independent from global configuration.\n */\nconst mailEnvSchema = z.object({\n SMTP_HOST: z.string().default('localhost'),\n SMTP_PORT: z.coerce.number().default(1025),\n SMTP_SECURE: z.string().default('false').transform(v => v === 'true' || v === '1'),\n SMTP_USER: z.string().optional(),\n SMTP_PASS: z.string().optional(),\n SMTP_FROM: z.string().default('noreply@nexus.local')\n})\n\nexport const mailEnv = mailEnvSchema.parse(process.env)\n\n/**\n * Gets mail configuration from environment variables\n */\nexport function getMailConfig(): MailConfig {\n return {\n host: mailEnv.SMTP_HOST,\n port: mailEnv.SMTP_PORT,\n secure: mailEnv.SMTP_SECURE,\n auth: mailEnv.SMTP_USER\n ? { user: mailEnv.SMTP_USER, pass: mailEnv.SMTP_PASS! }\n : undefined,\n from: mailEnv.SMTP_FROM\n }\n}\n","import nodemailer, { type Transporter } from 'nodemailer'\nimport { readFileSync, existsSync } from 'fs'\nimport { join } from 'path'\nimport type { Logger } from 'pino'\nimport { getMailConfig } from './mail.config.js'\nimport type { LoggerReporter } from '@gzl10/nexus-sdk'\nimport type { MailConfig, SendMailOptions, SendMailResult } from './mail.types.js'\n\n// Re-export types for backwards compatibility\nexport type { MailAction, SendMailOptions, SendMailResult } from './mail.types.js'\n\n// Assets en public/ para que se publiquen con el paquete npm\nconst TEMPLATE_REL_PATH = join('public', 'mail', 'base.html')\n// Logo blanco para header azul (fondo #3B82F6)\nconst LOGO_REL_PATH = join('public', 'nexus', 'nexus-light-512.png')\n\n/**\n * Singleton mail service\n */\nlet mailServiceInstance: MailService | null = null\n\nexport function getMailService(): MailService {\n if (!mailServiceInstance) {\n throw new Error('MailService not initialized. Call initMailService() first.')\n }\n return mailServiceInstance\n}\n\nexport function initMailService(\n logger: Logger,\n loggerService?: LoggerReporter,\n options?: { libPath?: string }\n): MailService {\n const config = getMailConfig()\n mailServiceInstance = new MailService(config, logger, loggerService, options)\n return mailServiceInstance\n}\n\nexport class MailService {\n private transporter: Transporter\n private defaultFrom: string\n private defaultLogoUrl: string\n private logger: Logger\n private template: string\n private loggerService?: LoggerReporter\n\n constructor(\n config: MailConfig,\n logger: Logger,\n loggerService?: LoggerReporter,\n options?: { libPath?: string }\n ) {\n this.defaultFrom = config.from\n this.logger = logger.child({ service: 'mail' })\n this.loggerService = loggerService\n\n const libPath = options?.libPath ?? process.cwd()\n this.template = readFileSync(join(libPath, TEMPLATE_REL_PATH), 'utf-8')\n\n // Cargar logo como base64 data URI\n const logoPath = join(libPath, LOGO_REL_PATH)\n if (existsSync(logoPath)) {\n const logoBase64 = readFileSync(logoPath).toString('base64')\n this.defaultLogoUrl = `data:image/png;base64,${logoBase64}`\n } else {\n this.defaultLogoUrl = ''\n }\n\n this.transporter = nodemailer.createTransport({\n host: config.host,\n port: config.port,\n secure: config.secure,\n auth: config.auth,\n // MailHog y servidores dev no necesitan TLS\n ...(config.auth ? {} : { ignoreTLS: true })\n })\n }\n\n async send(options: SendMailOptions): Promise<SendMailResult | null> {\n const from = options.from ?? this.defaultFrom\n const to = Array.isArray(options.to) ? options.to.join(', ') : options.to\n\n // Si hay title o message, usar template; si no, usar html raw\n const html = (options.title || options.message)\n ? this.renderTemplate(options)\n : options.html\n\n this.logger.info({ to, subject: options.subject }, 'Sending email')\n\n try {\n const result = await this.transporter.sendMail({\n from,\n to,\n subject: options.subject,\n text: options.text,\n html,\n replyTo: options.replyTo,\n attachments: options.attachments\n })\n\n this.logger.info(\n { messageId: result.messageId, accepted: result.accepted },\n 'Email sent'\n )\n\n return {\n messageId: result.messageId,\n accepted: result.accepted as string[],\n rejected: result.rejected as string[]\n }\n } catch (error) {\n const err = error instanceof Error ? error : new Error('Failed to send email')\n this.logger.warn({ error, to, subject: options.subject }, 'Failed to send email (continuing)')\n this.loggerService?.captureException(err, { service: 'mail', action: 'send', toCount: options.to?.length ?? 0 })\n return null\n }\n }\n\n private renderTemplate(options: SendMailOptions): string {\n let html = this.template\n\n // Reemplazar variables simples\n html = html.replace(/\\{\\{subject\\}\\}/g, options.subject)\n html = html.replace(/\\{\\{logoUrl\\}\\}/g, options.logoUrl ?? this.defaultLogoUrl)\n html = html.replace(/\\{\\{year\\}\\}/g, new Date().getFullYear().toString())\n\n // Procesar bloques condicionales con patrón específico\n html = this.processConditionalBlock(html, 'title', options.title)\n html = this.processConditionalBlock(html, 'message', options.message)\n\n // Actions (botones) - Nexus Blue (#3B82F6)\n if (options.actions?.length) {\n const actionsHtml = options.actions.map(a =>\n `<a href=\"${a.url}\" style=\"display:inline-block; background-color:#3B82F6; color:#ffffff; padding:12px 24px; text-decoration:none; border-radius:6px; margin:4px; font-weight:500;\">${a.label}</a>`\n ).join('')\n html = html.replace(/\\{\\{#if actions\\}\\}([\\s\\S]*?)\\{\\{\\/if\\}\\}/g, (_, content) =>\n content.replace(/\\{\\{actions\\}\\}/g, actionsHtml)\n )\n } else {\n html = html.replace(/\\{\\{#if actions\\}\\}[\\s\\S]*?\\{\\{\\/if\\}\\}/g, '')\n }\n\n return html\n }\n\n private processConditionalBlock(html: string, name: string, value?: string): string {\n const pattern = new RegExp(`\\\\{\\\\{#if ${name}\\\\}\\\\}([\\\\s\\\\S]*?)\\\\{\\\\{\\\\/if\\\\}\\\\}`, 'g')\n if (value) {\n return html.replace(pattern, (_, content) =>\n content.replace(new RegExp(`\\\\{\\\\{${name}\\\\}\\\\}`, 'g'), value)\n )\n }\n return html.replace(pattern, '')\n }\n\n async verify(): Promise<boolean> {\n try {\n await this.transporter.verify()\n this.logger.info('SMTP connection verified')\n return true\n } catch (error) {\n const err = error instanceof Error ? error : new Error('SMTP connection failed')\n this.logger.warn({ error }, 'SMTP connection failed (emails may not work)')\n this.loggerService?.captureException(err, { service: 'mail', action: 'verify' })\n return false\n }\n }\n}\n","import type {\n SingleEntityDefinition,\n ActionDefinition,\n EventEntityDefinition,\n ModuleContext\n} from '@gzl10/nexus-sdk'\nimport { useIdField, useTextField, useSelectField, useNumberField, useSwitchField, useEmailField, usePasswordField, useTextareaField, useTagsField, useDatetimeField } from '@gzl10/nexus-sdk/fields'\nimport nodemailer from 'nodemailer'\nimport { mailEnv } from './mail.config.js'\nimport { getMailService } from './mail.service.js'\nimport type { SendMailInput, SendMailOptions, MailConfigRow } from './mail.types.js'\n\n/**\n * Get mail config via SingleService (single_records KV), fallback to env vars\n */\nasync function getMailConfigFromDB(ctx: ModuleContext): Promise<MailConfigRow> {\n const configService = ctx.services['config'] as import('@gzl10/nexus-sdk').BaseEntityService | undefined\n if (configService && 'get' in configService) {\n const row = await (configService as { get: () => Promise<Record<string, unknown> | null> }).get()\n if (row) {\n return {\n host: row['host'] as string ?? mailEnv.SMTP_HOST,\n port: row['port'] as number ?? mailEnv.SMTP_PORT,\n secure: row['secure'] === true || row['secure'] === 1,\n from: row['from'] as string ?? mailEnv.SMTP_FROM,\n auth_user: (row['auth_user'] as string) ?? null,\n auth_pass: (row['auth_pass'] as string) ?? null\n }\n }\n }\n\n // Fallback to env vars\n return {\n host: mailEnv.SMTP_HOST,\n port: mailEnv.SMTP_PORT,\n secure: mailEnv.SMTP_SECURE,\n from: mailEnv.SMTP_FROM,\n auth_user: mailEnv.SMTP_USER ?? null,\n auth_pass: mailEnv.SMTP_PASS ?? null\n }\n}\n\n/**\n * Create nodemailer transporter from config\n */\nfunction createTransporter(config: MailConfigRow) {\n const auth = config.auth_user\n ? { user: config.auth_user, pass: config.auth_pass! }\n : undefined\n\n return nodemailer.createTransport({\n host: config.host,\n port: config.port,\n secure: config.secure as boolean,\n auth,\n ...(auth ? {} : { ignoreTLS: true })\n })\n}\n\n/** Normalize to to array */\nfunction normalizeRecipients(to: string | string[]): string[] {\n if (Array.isArray(to)) return to\n return to.split(',').map(s => s.trim()).filter(Boolean)\n}\n\n/**\n * Config Entity: config\n * SMTP configuration persisted in the DB\n */\nexport const mailConfigEntity: SingleEntityDefinition = {\n type: 'config',\n key: 'mail_config',\n label: { en: 'Mail Config', es: 'Configuración de correo' },\n routePrefix: '/config',\n timestamps: true,\n\n // Defaults desde variables de entorno\n defaults: {\n host: mailEnv.SMTP_HOST,\n port: mailEnv.SMTP_PORT,\n secure: mailEnv.SMTP_SECURE,\n from: mailEnv.SMTP_FROM,\n auth_user: mailEnv.SMTP_USER ?? '',\n auth_pass: ''\n },\n\n fields: {\n id: useIdField(),\n host: useTextField({\n label: { en: 'SMTP Host', es: 'Host SMTP' },\n size: 255,\n nullable: false,\n hint: { en: 'Default: SMTP_HOST env var', es: 'Por defecto: variable SMTP_HOST' }\n }),\n port: useNumberField({\n label: { en: 'Port', es: 'Puerto' },\n nullable: false,\n hint: { en: 'Default: SMTP_PORT env var', es: 'Por defecto: variable SMTP_PORT' }\n }),\n secure: useSwitchField({\n label: { en: 'TLS/SSL', es: 'TLS/SSL' },\n hint: { en: 'Default: SMTP_SECURE env var', es: 'Por defecto: variable SMTP_SECURE' }\n }),\n from: useEmailField({\n label: { en: 'Sender', es: 'Remitente' },\n nullable: false,\n hint: { en: 'Default: SMTP_FROM env var', es: 'Por defecto: variable SMTP_FROM' }\n }),\n auth_user: useTextField({\n label: { en: 'Auth User', es: 'Usuario de autenticación' },\n size: 255,\n nullable: true,\n hint: { en: 'SMTP username (optional)', es: 'Usuario SMTP (opcional)' }\n }),\n auth_pass: usePasswordField({\n label: { en: 'Auth Password', es: 'Contraseña de autenticación' },\n size: 255,\n nullable: true,\n hint: { en: 'SMTP password (optional)', es: 'Contraseña SMTP (opcional)' }\n })\n },\n\n casl: {\n subject: 'MailConfig',\n permissions: {\n ADMIN: { actions: ['read', 'update'] }\n }\n }\n}\n\n/**\n * Action Entity: send\n * Endpoint for sending emails\n */\nexport const mailSendAction: ActionDefinition = {\n key: 'send',\n scope: 'module',\n label: { en: 'Send Email', es: 'Enviar correo' },\n output: {},\n\n input: {\n to: useTagsField({\n label: { en: 'Recipient(s)', es: 'Destinatario(s)' },\n placeholder: { en: 'Add email address', es: 'Añadir dirección de correo' },\n required: true,\n validation: { format: 'email' }\n }),\n subject: useTextField({\n label: { en: 'Subject', es: 'Asunto' },\n required: true,\n validation: { min: 1, max: 255 }\n }),\n title: useTextField({\n label: { en: 'Title', es: 'Título' },\n hint: { en: 'Large title in email header', es: 'Título grande en la cabecera del correo' }\n }),\n message: {\n label: { en: 'Message', es: 'Mensaje' },\n input: 'markdown',\n hint: { en: 'Email body (supports markdown)', es: 'Cuerpo del correo (soporta markdown)' }\n },\n html: useTextareaField({\n label: { en: 'HTML', es: 'HTML' },\n hidden: true,\n hint: { en: 'Alternative HTML (ignores title/message if provided)', es: 'HTML alternativo (ignora título/mensaje si se proporciona)' }\n })\n },\n\n handler: async (ctx: ModuleContext, input: unknown) => {\n const { to: rawTo, subject, title, message, html, _authUserId } = input as SendMailInput & { _authUserId?: string }\n const recipients = normalizeRecipients(rawTo)\n\n // Get mail config from DB\n const config = await getMailConfigFromDB(ctx)\n const transporter = createTransporter(config)\n\n // Use MailService for template rendering if available\n const mailService = getMailService()\n\n // Build email options\n const mailOptions: SendMailOptions = {\n to: recipients,\n subject,\n title,\n message,\n html,\n from: config.from\n }\n\n // Send using dynamic transporter but delegate to service for template\n let result: { messageId: string; accepted: string[]; rejected: string[] } | null = null\n try {\n // If using title/message, let service render template\n if (title || message) {\n result = await mailService.send(mailOptions)\n } else {\n // Direct send with raw html\n const sendResult = await transporter.sendMail({\n from: config.from,\n to: recipients.join(', '),\n subject,\n html\n })\n result = {\n messageId: sendResult.messageId,\n accepted: sendResult.accepted as string[],\n rejected: sendResult.rejected as string[]\n }\n }\n } catch (error) {\n ctx.core.logger.warn({ error }, 'Failed to send email')\n\n // Log del fallo\n await ctx.db.knex('mail_log').insert({\n id: ctx.core.generateId(),\n to: recipients.join(', '),\n subject,\n status: 'failed',\n message_id: null,\n error: error instanceof Error ? error.message : 'Failed to send email',\n sent_by: _authUserId ?? null,\n created_at: ctx.db.nowTimestamp(ctx.db.knex)\n })\n\n throw new ctx.core.errors.AppError('Failed to send email', 500)\n }\n\n // Guard: result siempre definido si llegamos aquí\n if (!result) {\n throw new ctx.core.errors.AppError('Failed to send email', 500)\n }\n\n // Log del éxito\n await ctx.db.knex('mail_log').insert({\n id: ctx.core.generateId(),\n to: recipients.join(', '),\n subject,\n status: 'sent',\n message_id: result.messageId,\n error: null,\n sent_by: _authUserId ?? null,\n created_at: ctx.db.nowTimestamp(ctx.db.knex)\n })\n\n const acceptedCount = result.accepted.length\n const rejectedCount = result.rejected.length\n let resultMessage = `Email sent to ${acceptedCount} recipient${acceptedCount !== 1 ? 's' : ''}`\n if (rejectedCount > 0) {\n resultMessage += ` (${rejectedCount} rejected)`\n }\n\n return {\n message: resultMessage,\n messageId: result.messageId,\n accepted: result.accepted,\n rejected: result.rejected\n }\n },\n\n casl: {\n subject: 'MailSend',\n permissions: {\n SERVICE: { actions: ['execute'] }\n }\n }\n}\n\n/**\n * Action Entity: test\n * Probar conexión SMTP\n */\nexport const mailTestAction: ActionDefinition = {\n key: 'test',\n scope: 'module',\n label: { en: 'Test Connection', es: 'Probar conexión' },\n order: 1,\n output: {},\n\n handler: async (ctx: ModuleContext) => {\n // Get mail config from DB\n const config = await getMailConfigFromDB(ctx)\n const transporter = createTransporter(config)\n\n try {\n await transporter.verify()\n ctx.core.logger.info({ host: config.host, port: config.port }, 'SMTP connection verified')\n } catch (error) {\n ctx.core.logger.warn({ error }, 'SMTP connection failed')\n throw new ctx.core.errors.AppError(\n `Failed to connect to SMTP server ${config.host}:${config.port}`,\n 500\n )\n }\n\n return {\n host: config.host,\n port: config.port,\n message: `SMTP connection to ${config.host}:${config.port} verified successfully`\n }\n },\n\n casl: {\n subject: 'MailTest',\n permissions: {\n ADMIN: { actions: ['execute'] }\n }\n }\n}\n\n/**\n * Event Entity: log\n * Append-only log of sent emails\n */\nexport const mailLogEntity: EventEntityDefinition = {\n type: 'event',\n immutable: true,\n table: 'mail_log',\n label: { en: 'Mail Log', es: 'Registro de correos' },\n labelPlural: { en: 'Mail Logs', es: 'Registros de correos' },\n labelField: 'subject',\n routePrefix: '/log',\n // timestamps: false - created_at está definido explícitamente en fields\n retention: { days: 90 },\n calendarFrom: 'created_at',\n\n fields: {\n id: useIdField(),\n created_at: useDatetimeField({\n label: { en: 'Date', es: 'Fecha' },\n disabled: true,\n nullable: false,\n meta: { sortable: true }\n }),\n to: useTextField({\n label: { en: 'Recipient(s)', es: 'Destinatario(s)' },\n size: 1000,\n nullable: false,\n meta: { searchable: true }\n }),\n subject: useTextField({\n label: { en: 'Subject', es: 'Asunto' },\n size: 255,\n nullable: false,\n meta: { searchable: true, sortable: true }\n }),\n status: {\n ...useSelectField({\n label: { en: 'Status', es: 'Estado' },\n options: [\n { value: 'pending', label: { en: 'Pending', es: 'Pendiente' } },\n { value: 'sent', label: { en: 'Sent', es: 'Enviado' } },\n { value: 'failed', label: { en: 'Failed', es: 'Fallido' } },\n { value: 'bounced', label: { en: 'Bounced', es: 'Rebotado' } }\n ],\n nullable: false,\n index: true,\n meta: { sortable: true }\n }),\n validation: { enum: ['pending', 'sent', 'failed', 'bounced'] }\n },\n message_id: useTextField({\n label: { en: 'Message ID', es: 'ID de mensaje' },\n hidden: true,\n size: 255,\n nullable: true\n }),\n error: useTextareaField({\n label: { en: 'Error', es: 'Error' },\n nullable: true\n }),\n sent_by: useSelectField({\n label: { en: 'Sent by', es: 'Enviado por' },\n table: 'users',\n column: 'id',\n onDelete: 'SET NULL',\n endpoint: '/users',\n valueField: 'id',\n labelField: 'name',\n nullable: true,\n index: true\n })\n },\n\n casl: {\n subject: 'MailLog',\n permissions: {\n SUPPORT: { actions: ['read'] },\n SERVICE: { actions: ['read'] }\n }\n }\n}\n","import type { ModuleContext } from '@gzl10/nexus-sdk'\nimport { mailConfigEntity } from './mail.entity.js'\n\n/**\n * Mail Routes\n *\n * Auto-montaje desde definitions:\n * - mailSendAction, mailTestAction (action)\n * - mailLogEntity (event)\n *\n * Aquí solo montamos mailConfigEntity (config - no auto-monta).\n */\nexport function createMailRoutes(ctx: ModuleContext) {\n const router = ctx.core.createRouter()\n\n // ============================================================================\n // CONFIG (config entity - no auto-monta)\n // ============================================================================\n\n const configService = ctx.runtime.createEntityService(mailConfigEntity)\n const configController = ctx.runtime.createEntityController(configService, mailConfigEntity)\n const configRouter = ctx.runtime.createEntityRouter(configController, mailConfigEntity)\n\n router.use(mailConfigEntity.routePrefix ?? '/config', configRouter)\n\n ctx.services.register('mail-config', configService)\n\n return router\n}\n","/**\n * @module mail\n * @description Email service with Nodemailer, template rendering, and queue\n */\n\nimport type { ModuleManifest, ModuleContext, LoggerReporter } from '@gzl10/nexus-sdk'\nimport { mailConfigEntity, mailSendAction, mailTestAction, mailLogEntity } from './mail.entity.js'\nimport { initMailService } from './mail.service.js'\nimport { createMailRoutes } from './mail.routes.js'\n\nexport { MailService, getMailService } from './mail.service.js'\nexport { getMailConfig } from './mail.config.js'\nexport type {\n MailConfig,\n MailConfigRow,\n MailAction,\n SendMailInput,\n SendMailOptions,\n SendMailResult\n} from './mail.types.js'\n\nexport const mailModule: ModuleManifest = {\n name: 'mail',\n label: { en: 'Mail', es: 'Correo' },\n icon: 'mdi:email-outline',\n description: { en: 'Email sending and logging', es: 'Envío y registro de correos electrónicos' },\n type: 'core',\n category: 'messaging',\n dependencies: ['logger'],\n\n definitions: [\n mailConfigEntity,\n mailLogEntity\n ],\n\n actions: [\n mailSendAction,\n mailTestAction\n ],\n\n routePrefix: '/mail',\n routes: createMailRoutes,\n\n init: (ctx: ModuleContext) => {\n // Inicializar y registrar el servicio de mail\n const loggerService = ctx.services.getOptional<LoggerReporter>('logger')\n const mailService = initMailService(ctx.core.logger, loggerService, { libPath: ctx.core.getLibPath() })\n ctx.services.register('mail', mailService)\n ctx.core.logger.debug('Mail service registered')\n },\n\n // Import dinámico para evitar ciclos\n seed: async (ctx) => {\n const { seed } = await import('./mail.seed.js')\n await seed(ctx)\n }\n}\n","/**\n * Health Registry - Central registry for health checks.\n *\n * Modules register health checks during init(). The registry aggregates\n * results for the /api/ready endpoint (K8s readiness probe).\n *\n * Built-in check: database connectivity (SELECT 1).\n * Extensible: any module can register additional checks via ctx.services.get('health').\n */\n\nexport type HealthStatus = 'ok' | 'degraded' | 'error'\n\nexport interface HealthCheckResult {\n status: HealthStatus\n detail?: string\n}\n\nexport interface HealthCheck {\n name: string\n check: () => Promise<HealthCheckResult>\n}\n\nexport interface HealthCheckReport {\n status: HealthStatus\n checks: Record<string, { status: HealthStatus; detail?: string; latency_ms: number }>\n timestamp: string\n}\n\nexport class HealthRegistry {\n private checks = new Map<string, HealthCheck>()\n\n register(check: HealthCheck): void {\n this.checks.set(check.name, check)\n }\n\n unregister(name: string): void {\n this.checks.delete(name)\n }\n\n async checkAll(): Promise<HealthCheckReport> {\n const results: HealthCheckReport['checks'] = {}\n let overallStatus: HealthStatus = 'ok'\n\n const entries = Array.from(this.checks.values())\n\n // Run all checks in parallel\n const settled = await Promise.allSettled(\n entries.map(async (hc) => {\n const start = performance.now()\n try {\n const result = await hc.check()\n const latency = Math.round(performance.now() - start)\n return { name: hc.name, ...result, latency_ms: latency }\n } catch (err) {\n const latency = Math.round(performance.now() - start)\n return {\n name: hc.name,\n status: 'error' as HealthStatus,\n detail: err instanceof Error ? err.message : 'Unknown error',\n latency_ms: latency\n }\n }\n })\n )\n\n for (const result of settled) {\n const value = result.status === 'fulfilled'\n ? result.value\n : { name: 'unknown', status: 'error' as HealthStatus, detail: 'Check rejected', latency_ms: 0 }\n\n results[value.name] = {\n status: value.status,\n ...(value.detail ? { detail: value.detail } : {}),\n latency_ms: value.latency_ms\n }\n\n // Aggregate: error > degraded > ok\n if (value.status === 'error') {\n overallStatus = 'error'\n } else if (value.status === 'degraded' && overallStatus !== 'error') {\n overallStatus = 'degraded'\n }\n }\n\n return {\n status: overallStatus,\n checks: results,\n timestamp: new Date().toISOString()\n }\n }\n\n /** Get number of registered checks */\n get size(): number {\n return this.checks.size\n }\n}\n\n/** Singleton registry instance (created once, shared across modules) */\nlet registry: HealthRegistry | null = null\n\nexport function getHealthRegistry(): HealthRegistry {\n if (!registry) {\n registry = new HealthRegistry()\n }\n return registry\n}\n\n/** Reset registry (for testing) */\nexport function resetHealthRegistry(): void {\n registry = null\n}\n","import { z } from 'zod'\n\nconst otelEnvSchema = z.object({\n OTEL_ENABLED: z.coerce.boolean().default(false),\n OTEL_SERVICE_NAME: z.string().default('nexus'),\n OTEL_PROMETHEUS_PORT: z.coerce.number().int().min(0).max(65535).default(9464),\n OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url().optional(),\n OTEL_TRACE_SAMPLE_RATE: z.coerce.number().min(0).max(1).default(1.0)\n})\n\nexport interface OtelConfig {\n enabled: boolean\n serviceName: string\n prometheusPort: number\n otlpEndpoint?: string\n traceSampleRate: number\n}\n\nlet cachedConfig: OtelConfig | null = null\n\nexport function getOtelConfig(): OtelConfig {\n if (cachedConfig) return cachedConfig\n\n const env = otelEnvSchema.parse(process.env)\n cachedConfig = {\n enabled: env.OTEL_ENABLED,\n serviceName: env.OTEL_SERVICE_NAME,\n prometheusPort: env.OTEL_PROMETHEUS_PORT,\n otlpEndpoint: env.OTEL_EXPORTER_OTLP_ENDPOINT,\n traceSampleRate: env.OTEL_TRACE_SAMPLE_RATE\n }\n return cachedConfig\n}\n\n/** Reset cached config (for testing) */\nexport function resetOtelConfig(): void {\n cachedConfig = null\n}\n","import type { ActionDefinition, AuthRequest } from '@gzl10/nexus-sdk'\nimport { getHealthRegistry } from './observability.service.js'\nimport { getOtelConfig } from './observability.config.js'\n\n/**\n * Action: ready\n * GET /observability/ready - Readiness probe (K8s)\n *\n * Returns 200 if all health checks pass, 503 otherwise.\n * No auth required (external probes).\n */\nexport const readyAction: ActionDefinition = {\n key: 'ready',\n label: { en: 'Readiness Check', es: 'Verificación de disponibilidad' },\n icon: 'mdi:check-circle-outline',\n scope: 'module',\n method: 'GET',\n skipAuth: true,\n output: {},\n handler: async (_ctx, _input, _req, res) => {\n const registry = getHealthRegistry()\n const result = await registry.checkAll()\n const httpStatus = result.status === 'ok' ? 200 : 503\n res!.status(httpStatus).json(result)\n }\n}\n\n/**\n * Action: metrics-summary\n * GET /observability/metrics-summary - OTel/runtime metrics summary\n *\n * Returns process metrics (uptime, memory, OTel status).\n * Admin-only.\n */\nexport const metricsSummaryAction: ActionDefinition = {\n key: 'metrics-summary',\n label: { en: 'Metrics Summary', es: 'Resumen de métricas' },\n icon: 'mdi:chart-bar',\n scope: 'module',\n method: 'GET',\n output: {},\n handler: async (_ctx, _input, req) => {\n // Only admins can see metrics summary\n const authReq = req as AuthRequest\n if (!authReq.ability?.can('manage', 'all')) {\n throw new Error('Forbidden')\n }\n\n const config = getOtelConfig()\n const mem = process.memoryUsage()\n\n return {\n uptime_seconds: Math.round(process.uptime()),\n memory: {\n rss_mb: Math.round(mem.rss / 1024 / 1024),\n heap_used_mb: Math.round(mem.heapUsed / 1024 / 1024),\n heap_total_mb: Math.round(mem.heapTotal / 1024 / 1024)\n },\n otel: {\n enabled: config.enabled,\n service_name: config.serviceName,\n prometheus_port: config.enabled ? config.prometheusPort : null,\n otlp_endpoint: config.enabled ? (config.otlpEndpoint ?? null) : null\n }\n }\n }\n}\n","/**\n * Diagnostics action - aggregated system diagnostics.\n * GET /observability/diagnostics (admin-only)\n */\nimport type { ActionDefinition, AuthRequest, ModuleContext } from '@gzl10/nexus-sdk'\nimport type { Knex } from 'knex'\nimport type { Redis } from 'ioredis'\nimport type { CacheManager } from '../../core/cache/cache-manager.js'\n\ninterface DiagnosticsInput {\n knex: Knex\n cacheManager: CacheManager | null\n redisClient: Redis | null\n socketInfo: { connected_users: number; total_connections: number }\n}\n\ninterface PoolStats {\n used: number\n free: number\n pendingAcquires: number\n pendingCreates: number\n}\n\ninterface DiagnosticsResult {\n database: {\n pool: PoolStats | null\n pending_migrations: number\n }\n cache: Record<string, unknown>\n redis: { status: string; memory_human?: string; connected_clients?: number } | { status: 'not_configured' }\n sockets: { connected_users: number; total_connections: number }\n process: { uptime_seconds: number; memory_mb: { rss: number; heap_used: number; heap_total: number } }\n}\n\nfunction getPoolStats(knex: Knex): PoolStats | null {\n const pool = (knex.client as any)?.pool\n if (!pool || typeof pool.numUsed !== 'function') return null\n return {\n used: pool.numUsed(),\n free: pool.numFree(),\n pendingAcquires: pool.numPendingAcquires(),\n pendingCreates: pool.numPendingCreates()\n }\n}\n\nfunction parseRedisInfo(info: string, key: string): string | undefined {\n const match = info.match(new RegExp(`^${key}:(.+)$`, 'm'))\n return match?.[1]?.trim()\n}\n\nasync function getRedisStats(client: Redis | null): Promise<DiagnosticsResult['redis']> {\n if (!client) return { status: 'not_configured' }\n try {\n const info = await client.info('memory')\n const clientsInfo = await client.info('clients')\n return {\n status: (client as any).status ?? 'unknown',\n memory_human: parseRedisInfo(info, 'used_memory_human'),\n connected_clients: parseInt(parseRedisInfo(clientsInfo, 'connected_clients') ?? '0', 10)\n }\n } catch {\n return { status: (client as any).status ?? 'error' }\n }\n}\n\nfunction getSocketStats(ctx: ModuleContext): DiagnosticsResult['sockets'] {\n try {\n if (!ctx.core.socket.isInitialized()) return { connected_users: 0, total_connections: 0 }\n return {\n connected_users: ctx.core.socket.getConnectedUsers().length,\n total_connections: (ctx.core.socket.getIO() as any)?.engine?.clientsCount ?? 0\n }\n } catch {\n return { connected_users: 0, total_connections: 0 }\n }\n}\n\nexport async function buildDiagnostics(input: DiagnosticsInput): Promise<DiagnosticsResult> {\n const mem = process.memoryUsage()\n\n const [pendingMigrations, redisStats] = await Promise.all([\n input.knex.migrate.status().then((r: unknown) => Array.isArray(r) ? r.length : 0).catch(() => -1),\n getRedisStats(input.redisClient)\n ])\n\n return {\n database: {\n pool: getPoolStats(input.knex),\n pending_migrations: pendingMigrations\n },\n cache: input.cacheManager?.stats() ?? {},\n redis: redisStats,\n sockets: input.socketInfo,\n process: {\n uptime_seconds: Math.round(process.uptime()),\n memory_mb: {\n rss: Math.round(mem.rss / 1024 / 1024),\n heap_used: Math.round(mem.heapUsed / 1024 / 1024),\n heap_total: Math.round(mem.heapTotal / 1024 / 1024)\n }\n }\n }\n}\n\nexport const diagnosticsAction: ActionDefinition = {\n key: 'diagnostics',\n label: { en: 'System Diagnostics', es: 'Diagnóstico del sistema' },\n icon: 'mdi:stethoscope',\n scope: 'module',\n method: 'GET',\n output: {},\n handler: async (ctx, _input, req) => {\n const authReq = req as AuthRequest\n if (!authReq.ability?.can('manage', 'all')) {\n throw new ctx.core.errors.ForbiddenError('Admin access required')\n }\n\n const cacheManager = ctx.services.getOptional<CacheManager>('cacheManager') ?? null\n const redisClient = cacheManager?.redis ?? null\n\n return buildDiagnostics({\n knex: ctx.db.knex,\n cacheManager,\n redisClient,\n socketInfo: getSocketStats(ctx)\n })\n }\n}\n","/**\n * @module observability\n * @description Health registry, readiness probes, and OTel metrics summary\n */\n\nimport type { ModuleManifest } from '@gzl10/nexus-sdk'\nimport { getHealthRegistry } from './observability.service.js'\nimport { readyAction, metricsSummaryAction } from './observability.entity.js'\nimport { diagnosticsAction } from './diagnostics.action.js'\n\nexport { HealthRegistry, getHealthRegistry, resetHealthRegistry } from './observability.service.js'\nexport type { HealthCheck, HealthCheckResult, HealthCheckReport, HealthStatus } from './observability.service.js'\nexport { getOtelConfig, resetOtelConfig } from './observability.config.js'\nexport type { OtelConfig } from './observability.config.js'\n\nexport const observabilityModule: ModuleManifest = {\n name: 'observability',\n label: { en: 'Observability', es: 'Observabilidad' },\n icon: 'mdi:chart-line',\n description: { en: 'Health checks, readiness probes, and metrics', es: 'Health checks, probes de disponibilidad y métricas' },\n type: 'core',\n category: 'settings',\n dependencies: ['logger'],\n\n init: (ctx) => {\n const registry = getHealthRegistry()\n ctx.services.register('health', registry)\n\n // Auto-register DB health check\n registry.register({\n name: 'database',\n check: async () => {\n try {\n await ctx.db.knex.raw('SELECT 1')\n return { status: 'ok' }\n } catch (e) {\n return { status: 'error', detail: (e as Error).message }\n }\n }\n })\n\n // Auto-register Redis health check (if configured)\n if (process.env['REDIS_URL']) {\n registry.register({\n name: 'redis',\n check: async () => {\n try {\n const cacheManager = ctx.services.getOptional<import('../../core/cache/cache-manager.js').CacheManager>('cacheManager')\n const redisClient = cacheManager?.redis\n if (!redisClient) return { status: 'degraded', detail: 'Redis configured but not connected (using memory fallback)' }\n await redisClient.ping()\n return { status: 'ok' }\n } catch (e) {\n return { status: 'error', detail: (e as Error).message }\n }\n }\n })\n }\n\n ctx.core.logger.debug('Health registry initialized with database check')\n },\n\n definitions: [],\n\n actions: [readyAction, metricsSummaryAction, diagnosticsAction],\n\n routePrefix: '/observability'\n}\n","import type { PluginManifest, ModuleContext, PluginDTO } from '@gzl10/nexus-sdk'\nimport { toModuleDTO } from '../system/system.helpers.js'\n\n/**\n * Converts PluginManifest to a serializable PluginDTO\n */\nexport function toPluginDTO(plugin: PluginManifest, ctx: ModuleContext): PluginDTO {\n const state = ctx.core.plugins.getState(plugin.name)\n return {\n name: plugin.name,\n code: plugin.code,\n label: plugin.label,\n icon: plugin.icon,\n // URL matches route: {apiPrefix}/{module.routePrefix}/:code/image\n image: plugin.image ? `/api/plugins/${plugin.code}/image` : undefined,\n category: plugin.category,\n version: plugin.version,\n description: plugin.description,\n installed: true,\n enabled: state?.enabled ?? null,\n modules: plugin.modules.map(mod => toModuleDTO(mod, ctx)),\n envVars: plugin.envVars,\n peerDependencies: plugin.peerDependencies,\n setup: plugin.setup ? {\n steps: plugin.setup.steps,\n docsUrl: plugin.setup.docsUrl,\n prerequisites: plugin.setup.prerequisites,\n caveats: plugin.setup.caveats\n } : undefined\n }\n}\n","import type { ActionDefinition } from '@gzl10/nexus-sdk'\n\n/**\n * Row action — installs a plugin via ctx.core.plugins.install()\n */\nexport const installPluginAction: ActionDefinition = {\n key: 'install',\n scope: 'row',\n group: { en: 'Plugin Management', es: 'Gestión de plugins' },\n label: { en: 'Install', es: 'Instalar' },\n icon: 'mdi:download',\n output: {},\n\n input: {\n version: {\n label: { en: 'Version', es: 'Versión' },\n input: 'text',\n hint: { en: 'Optional (defaults to latest)', es: 'Opcional (por defecto latest)' }\n }\n },\n\n confirm: {\n type: 'simple',\n message: { en: 'This will install the plugin and restart the server.', es: 'Esto instalará el plugin y reiniciará el servidor.' },\n severity: 'warning'\n },\n\n select: ['name'],\n\n handler: async (ctx, input) => {\n const { version, _record } = input as { version?: string; _record: { name: string } }\n const name = _record.name\n\n ctx.core.logger.info({ name, version }, 'Installing plugin')\n\n ctx.events.notify('audit.log', {\n source: 'core:plugins',\n action: 'plugin_installed',\n resourceType: 'plugin',\n resourceId: name\n })\n\n try {\n await ctx.core.plugins.install(name, { version: version || undefined })\n } catch (error) {\n ctx.core.logger.error({ error, name }, 'Failed to install plugin')\n throw new ctx.core.errors.AppError(`Failed to install plugin: ${(error as Error).message}`, 500)\n }\n\n ctx.core.logger.info({ name }, 'Plugin installed, scheduling restart')\n\n setTimeout(() => {\n process.exit(0)\n }, 500)\n\n return {\n success: true,\n message: `Plugin ${name} installed. Server restarting...`\n }\n }\n}\n","import type { ActionDefinition } from '@gzl10/nexus-sdk'\n\n/**\n * Row action — uninstalls an installed plugin via ctx.core.plugins.uninstall()\n */\nexport const uninstallPluginAction: ActionDefinition = {\n key: 'uninstall',\n scope: 'row',\n group: { en: 'Plugin Management', es: 'Gestión de plugins' },\n label: { en: 'Uninstall', es: 'Desinstalar' },\n icon: 'mdi:delete',\n output: {},\n\n confirm: {\n type: 'verify',\n message: { en: 'This will uninstall the plugin, remove its data, and restart the server.', es: 'Esto desinstalará el plugin, eliminará sus datos y reiniciará el servidor.' },\n verifyText: 'UNINSTALL',\n severity: 'error'\n },\n\n select: ['name'],\n\n handler: async (ctx, input) => {\n const { _record } = input as { _record: { name: string } }\n const name = _record.name\n\n ctx.core.logger.info({ name }, 'Uninstalling plugin')\n\n ctx.events.notify('audit.log', {\n source: 'core:plugins',\n action: 'plugin_uninstalled',\n resourceType: 'plugin',\n resourceId: name\n })\n\n try {\n await ctx.core.plugins.uninstall(name)\n } catch (error) {\n ctx.core.logger.error({ error, name }, 'Failed to uninstall plugin')\n throw new ctx.core.errors.AppError(`Failed to uninstall plugin: ${(error as Error).message}`, 500)\n }\n\n ctx.core.logger.info({ name }, 'Plugin uninstalled, scheduling restart')\n\n setTimeout(() => {\n process.exit(0)\n }, 500)\n\n return {\n success: true,\n message: `Plugin ${name} uninstalled. Server restarting...`\n }\n }\n}\n","import type { ActionDefinition } from '@gzl10/nexus-sdk'\n\n/**\n * Row action — toggles plugin enabled/disabled state via ctx.core.plugins.enable/disable()\n */\nexport const togglePluginAction: ActionDefinition = {\n key: 'toggle',\n scope: 'row',\n group: { en: 'Plugin Management', es: 'Gestión de plugins' },\n label: { en: 'Toggle', es: 'Activar/Desactivar' },\n icon: 'mdi:toggle-switch',\n output: {},\n\n confirm: {\n type: 'simple',\n message: { en: 'Changing plugin state will restart the server.', es: 'Cambiar el estado del plugin reiniciará el servidor.' },\n severity: 'warning'\n },\n\n input: {\n enabled: {\n label: { en: 'Enabled', es: 'Habilitado' },\n input: 'checkbox',\n required: true\n }\n },\n\n select: ['name', 'enabled'],\n\n handler: async (ctx, input) => {\n const { enabled, _record } = input as { enabled: boolean; _record: { name: string } }\n const name = _record.name\n\n ctx.events.notify('audit.log', {\n source: 'core:plugins',\n action: 'plugin_toggled',\n resourceType: 'plugin',\n resourceId: name,\n metadata: { enabled }\n })\n\n try {\n if (enabled) {\n ctx.core.plugins.enable(name)\n } else {\n ctx.core.plugins.disable(name)\n }\n } catch (error) {\n throw new ctx.core.errors.AppError((error as Error).message, 404)\n }\n\n ctx.core.logger.info({ name, enabled }, 'Plugin toggled, scheduling restart')\n\n setTimeout(() => {\n process.exit(0)\n }, 500)\n\n return {\n success: true,\n message: `Plugin ${name} ${enabled ? 'enabled' : 'disabled'}. Server restarting...`\n }\n }\n}\n","import type { ComputedEntityDefinition, ModuleContext, PluginDTO } from '@gzl10/nexus-sdk'\nimport { useTextField, useSelectField, useCheckboxField } from '@gzl10/nexus-sdk/fields'\nimport { OFFICIAL_PLUGINS } from '@gzl10/nexus-sdk'\nimport { toPluginDTO } from './plugins.helpers.js'\nimport { installPluginAction } from './actions/install-plugin.action.js'\nimport { uninstallPluginAction } from './actions/uninstall-plugin.action.js'\nimport { togglePluginAction } from './actions/toggle-plugin.action.js'\n\nconst allowPluginManagement = process.env['NEXUS_ALLOW_PLUGIN_INSTALL'] !== 'false'\n\nexport const pluginsEntity: ComputedEntityDefinition = {\n type: 'computed',\n label: 'Plugins',\n icon: 'mdi:puzzle',\n labelField: 'code',\n routePrefix: '/',\n defaultSort: { field: 'name', order: 'asc' },\n\n fields: {\n name: useTextField({\n label: { en: 'Name', es: 'Nombre' },\n size: 50,\n nullable: false,\n meta: { sortable: true, searchable: true }\n }),\n code: useTextField({\n label: { en: 'Code', es: 'Código' },\n size: 10,\n nullable: false,\n meta: { sortable: true }\n }),\n label: useTextField({\n label: { en: 'Label', es: 'Etiqueta' },\n size: 100,\n nullable: false,\n meta: { sortable: true }\n }),\n version: useTextField({\n label: { en: 'Version', es: 'Versión' },\n size: 20,\n nullable: false\n }),\n category: useSelectField({\n label: { en: 'Category', es: 'Categoría' },\n options: [\n { value: 'content', label: { en: 'Content', es: 'Contenido' } },\n { value: 'data', label: { en: 'Data', es: 'Datos' } },\n { value: 'assets', label: { en: 'Assets', es: 'Activos' } },\n { value: 'messaging', label: { en: 'Messaging', es: 'Mensajería' } },\n { value: 'jobs', label: { en: 'Jobs', es: 'Trabajos' } },\n { value: 'ai', label: { en: 'AI', es: 'IA' } },\n { value: 'analytics', label: { en: 'Analytics', es: 'Analítica' } },\n { value: 'integrations', label: { en: 'Integrations', es: 'Integraciones' } },\n { value: 'commerce', label: { en: 'Commerce', es: 'Comercio' } },\n { value: 'security', label: { en: 'Security', es: 'Seguridad' } }\n ],\n nullable: true,\n meta: { sortable: true }\n }),\n description: {\n label: { en: 'Description', es: 'Descripción' },\n input: 'markdown',\n db: { type: 'json' },\n meta: { searchable: true }\n },\n installed: useCheckboxField({\n label: { en: 'Installed', es: 'Instalado' }\n }),\n enabled: useCheckboxField({\n label: { en: 'Enabled', es: 'Habilitado' }\n })\n },\n\n actions: allowPluginManagement ? [\n { ...installPluginAction, disabled: { field: 'installed', $eq: true } },\n { ...uninstallPluginAction, disabled: { field: 'installed', $eq: false } },\n { ...togglePluginAction, disabled: { field: 'installed', $eq: false } },\n ] : undefined,\n\n casl: {\n subject: 'Plugin',\n permissions: {\n ADMIN: { actions: ['read', 'execute'] },\n MANAGER: { actions: ['read'] },\n EDITOR: { actions: ['read'] },\n USER: { actions: ['read'] },\n VIEWER: { actions: ['read'] },\n SUPPORT: { actions: ['read'] },\n AUDITOR: { actions: ['read'] }\n }\n },\n\n compute: async (ctx: ModuleContext): Promise<PluginDTO[]> => {\n const discovered = await ctx.core.plugins.discover()\n const discoveredMap = new Map(discovered.map(p => [p.name, p]))\n const states = ctx.core.plugins.getAllStates()\n\n return OFFICIAL_PLUGINS.map(name => {\n const manifest = discoveredMap.get(name)\n if (manifest) {\n return toPluginDTO(manifest, ctx)\n }\n const short = ctx.core.plugins.shortName(name)\n return {\n name,\n code: short,\n label: short,\n icon: 'mdi:puzzle-outline',\n version: '–',\n description: '',\n installed: false,\n enabled: states[name]?.enabled ?? null,\n modules: [],\n }\n })\n },\n\n cache: {\n ttl: 60\n }\n}\n","import { Router } from 'express'\nimport { existsSync } from 'node:fs'\nimport type { ModuleContext } from '@gzl10/nexus-sdk'\n\n/**\n * Custom routes for plugins module.\n * GET /:code/image — serves plugin image file with lazy-cached map\n */\nexport function createPluginRoutes(ctx: ModuleContext): Router {\n const router = Router()\n\n // Lazy cache: built once on first request, avoids re-scanning on every image load\n let imageMap: Map<string, string> | null = null\n\n async function getImageMap(): Promise<Map<string, string>> {\n if (!imageMap) {\n const discovered = await ctx.core.plugins.discover()\n imageMap = new Map(\n discovered\n .filter(p => p.image && existsSync(p.image))\n .map(p => [p.code, p.image!])\n )\n }\n return imageMap\n }\n\n router.get('/:code/image', async (req, res) => {\n const { code } = req.params\n\n if (!/^[a-z]{3}$/.test(code)) {\n res.status(400).json({ error: { code: 'INVALID_CODE', message: 'Invalid plugin code' } })\n return\n }\n\n const map = await getImageMap()\n const imagePath = map.get(code)\n\n if (!imagePath) {\n res.status(404).json({ error: { code: 'NOT_FOUND', message: 'Plugin image not found' } })\n return\n }\n\n res.set('Cache-Control', 'public, max-age=86400, immutable')\n res.sendFile(imagePath)\n })\n\n return router\n}\n","/**\n * @module plugins\n * @description Plugin store — lists official plugins and provides install/uninstall/toggle actions\n */\n\nimport type { ModuleManifest, ModuleContext } from '@gzl10/nexus-sdk'\nimport { pluginsEntity } from './plugins.entity.js'\nimport { createPluginRoutes } from './plugins.routes.js'\n\nexport { toPluginDTO } from './plugins.helpers.js'\n\nexport const pluginsModule: ModuleManifest = {\n name: 'plugins',\n label: { en: 'Plugins', es: 'Plugins' },\n icon: 'mdi:puzzle',\n description: { en: 'Plugin store and management', es: 'Tienda y gestión de plugins' },\n type: 'core',\n category: 'settings',\n dependencies: ['logger'],\n definitions: [pluginsEntity],\n routes: (ctx: ModuleContext) => createPluginRoutes(ctx),\n routePrefix: '/plugins'\n}\n","import type { EventEntityDefinition } from '@gzl10/nexus-sdk'\nimport {\n useIdField,\n useTextField,\n useSelectField,\n useTextareaField,\n useJsonField,\n useDatetimeField,\n useEmailField\n} from '@gzl10/nexus-sdk/fields'\n\n/**\n * Generic audit log — immutable, append-only.\n * Consumed by any module/plugin via AuditService.\n * Retention: 90 days by default.\n */\nexport const auditLogEntity: EventEntityDefinition = {\n type: 'event',\n immutable: true,\n table: 'audit_log',\n label: { en: 'Audit Log', es: 'Registro de auditoría' },\n labelPlural: { en: 'Audit Logs', es: 'Registros de auditoría' },\n labelField: 'action',\n routePrefix: '/log',\n retention: { days: 90 },\n calendarFrom: 'created_at',\n\n fields: {\n id: useIdField(),\n source: {\n ...useTextField({\n label: { en: 'Source', es: 'Origen' },\n size: 100,\n nullable: false,\n index: true,\n meta: { sortable: true, searchable: true }\n }),\n validation: { min: 1, max: 100 }\n },\n action: {\n ...useTextField({\n label: { en: 'Action', es: 'Acción' },\n size: 100,\n nullable: false,\n index: true,\n meta: { sortable: true, searchable: true }\n }),\n validation: { min: 1, max: 100 }\n },\n actor_id: useSelectField({\n label: { en: 'Actor', es: 'Actor' },\n table: 'users',\n column: 'id',\n onDelete: 'SET NULL',\n endpoint: '/users',\n valueField: 'id',\n labelField: 'name',\n nullable: true,\n index: true,\n meta: { searchable: true }\n }),\n actor_email: useEmailField({\n label: { en: 'Actor Email', es: 'Email del actor' },\n nullable: true,\n meta: { searchable: true }\n }),\n resource_type: useTextField({\n label: { en: 'Resource Type', es: 'Tipo de recurso' },\n size: 100,\n nullable: true,\n index: true,\n meta: { searchable: true }\n }),\n resource_id: useTextField({\n label: { en: 'Resource ID', es: 'ID del recurso' },\n size: 100,\n nullable: true,\n meta: { searchable: true }\n }),\n ip_address: useTextField({\n label: { en: 'IP Address', es: 'Dirección IP' },\n size: 45,\n nullable: true,\n meta: { searchable: true }\n }),\n user_agent: useTextareaField({\n label: { en: 'User Agent', es: 'Agente de usuario' },\n nullable: true,\n meta: { exportable: false }\n }),\n metadata: useJsonField({\n label: { en: 'Metadata', es: 'Metadatos' },\n nullable: true,\n meta: { exportable: false }\n }),\n created_at: useDatetimeField({\n label: { en: 'Date', es: 'Fecha' },\n disabled: true,\n nullable: false,\n meta: { sortable: true }\n })\n },\n\n casl: {\n subject: 'AuditLog',\n permissions: {\n SUPPORT: { actions: ['read'] },\n AUDITOR: { actions: ['read'] }\n }\n }\n}\n","import type { Knex } from 'knex'\nimport type { AuditEntry, AuditQuery, AuditLogRow, AuditService } from './audit.types.js'\n\nconst TABLE = 'audit_log'\n\ninterface AuditServiceDeps {\n knex: Knex\n generateId: () => string\n nowTimestamp: (knex: Knex) => Knex.Raw | string\n logger: { warn: (msg: string, ...args: unknown[]) => void }\n}\n\nexport function createAuditService(deps: AuditServiceDeps): AuditService {\n const { knex, generateId, nowTimestamp, logger } = deps\n\n function toRow(entry: AuditEntry): Record<string, unknown> {\n return {\n id: generateId(),\n source: entry.source,\n action: entry.action,\n actor_id: entry.actorId ?? null,\n actor_email: entry.actorEmail ?? null,\n resource_type: entry.resourceType ?? null,\n resource_id: entry.resourceId ?? null,\n ip_address: entry.ip ?? null,\n user_agent: entry.userAgent ?? null,\n metadata: entry.metadata ? JSON.stringify(entry.metadata) : null,\n created_at: nowTimestamp(knex)\n }\n }\n\n function log(entry: AuditEntry): void {\n knex(TABLE).insert(toRow(entry)).catch(() => {\n logger.warn(`Failed to persist audit event: ${entry.source}/${entry.action}`)\n })\n }\n\n function logBatch(entries: AuditEntry[]): void {\n if (entries.length === 0) return\n const rows = entries.map(toRow)\n knex(TABLE).insert(rows).catch(() => {\n logger.warn(`Failed to persist ${entries.length} audit events`)\n })\n }\n\n async function query(filters: AuditQuery): Promise<AuditLogRow[]> {\n let q = knex(TABLE).select('*')\n\n if (filters.source) q = q.where('source', filters.source)\n if (filters.action) q = q.where('action', filters.action)\n if (filters.actorId) q = q.where('actor_id', filters.actorId)\n if (filters.resourceType) q = q.where('resource_type', filters.resourceType)\n if (filters.resourceId) q = q.where('resource_id', filters.resourceId)\n if (filters.since) q = q.where('created_at', '>=', filters.since)\n if (filters.until) q = q.where('created_at', '<=', filters.until)\n\n q = q.orderBy('created_at', 'desc')\n if (filters.limit) q = q.limit(filters.limit)\n if (filters.offset) q = q.offset(filters.offset)\n\n return q as Promise<AuditLogRow[]>\n }\n\n return { log, logBatch, query }\n}\n","/**\n * @module audit\n * @description Generic audit logging for modules and plugins\n */\n\nimport type { ModuleManifest, ModuleContext } from '@gzl10/nexus-sdk'\nimport { auditLogEntity } from './audit.entity.js'\nimport { createAuditService } from './audit.service.js'\nimport type { AuditEntry } from './audit.types.js'\n\nexport type { AuditService, AuditEntry, AuditQuery, AuditLogRow } from './audit.types.js'\nexport { createAuditService } from './audit.service.js'\n\nexport const auditModule: ModuleManifest = {\n name: 'audit',\n label: { en: 'Audit', es: 'Auditoría' },\n icon: 'mdi:shield-search',\n description: {\n en: 'Generic audit logging for modules and plugins',\n es: 'Registro de auditoría genérico para módulos y plugins'\n },\n type: 'core',\n category: 'security',\n dependencies: ['logger'],\n\n definitions: [\n auditLogEntity\n ],\n\n routePrefix: '/audit',\n\n init: (ctx: ModuleContext) => {\n const auditService = createAuditService({\n knex: ctx.db.knex,\n generateId: ctx.core.generateId,\n nowTimestamp: ctx.db.nowTimestamp,\n logger: ctx.core.logger\n })\n ctx.services.register('audit', auditService)\n\n // Listen for audit events (decoupled — emitters don't need to know about audit module)\n ctx.events.on('audit.log', (entry) => {\n auditService.log(entry as AuditEntry)\n })\n\n ctx.core.logger.debug('Audit service registered')\n }\n}\n","/**\n * @module modules\n * @description Core business modules: auth, users, storage, notifications, etc.\n *\n * @dependencies\n * - NONE (fully isolated from db/, core/, engine/, runtime/)\n *\n * @note Modules are intentionally isolated. All dependencies are injected via\n * ModuleContext (ctx). This ensures modules are portable and testable.\n * NEVER import from ../db/, ../core/, ../engine/, or ../runtime/.\n * Use ctx.db, ctx.helpers, ctx.services['name'] instead.\n *\n * @note tags, links, charts migrated to plugins (NEX-49):\n * @gzl10/nexus-plugin-tags, @gzl10/nexus-plugin-links,\n * @gzl10/nexus-plugin-charts\n * @note compliance migrated to plugin (NEX-78):\n * @gzl10/nexus-plugin-compliance\n * @note webhooks migrated to plugin (NEX-50): @gzl10/nexus-plugin-webhooks\n * @note schedules migrated to plugin (NEX-50): @gzl10/nexus-plugin-schedules\n * @note notifications migrated to plugin (NEX-51):\n * @gzl10/nexus-plugin-notifications\n * @note remote migrated to plugin (NEX-51):\n * @gzl10/nexus-plugin-remote\n */\n\nimport { loggerModule } from './logger/index.js'\nimport { mastersModule } from './masters/index.js'\nimport { systemModule } from './system/index.js'\nimport { uiSettingsModule } from './ui-settings/index.js'\nimport { storageModule } from './storage/index.js'\nimport { usersModule } from './users/index.js'\nimport { authModule } from './auth/index.js'\nimport { mailModule } from './mail/index.js'\nimport { observabilityModule } from './observability/index.js'\nimport { pluginsModule } from './plugins/index.js'\nimport { auditModule } from './audit/index.js'\n\n// Individual module exports\nexport {\n loggerModule,\n mastersModule,\n systemModule,\n pluginsModule,\n uiSettingsModule,\n storageModule,\n usersModule,\n auditModule,\n authModule,\n mailModule,\n observabilityModule\n}\n\n// Re-export UI settings entities for backward compatibility\nexport {\n uiBrandingEntity,\n uiThemeEntity,\n uiEffectsEntity,\n uiAccessibilityEntity\n} from './ui-settings/index.js'\n\n/** All core modules (for engine loader) */\nexport const CORE_MODULES = [\n loggerModule,\n mastersModule,\n systemModule,\n pluginsModule,\n uiSettingsModule,\n storageModule,\n usersModule,\n auditModule,\n authModule,\n mailModule,\n observabilityModule\n]\n","/**\n * Core module loader.\n *\n * Loads all core modules in dependency order using topological sort.\n */\n\nimport { registerModule } from './registry.js'\nimport { topologicalSort, validateModuleDependencies } from './definition-extractors.js'\nimport { CORE_MODULES } from '../modules/index.js'\n\n/**\n * Loads the backend core modules in dependency order.\n * Uses topological sort to ensure dependencies are registered first.\n *\n * @throws Error if circular dependencies are detected\n */\nexport function loadCoreModules(): void {\n validateModuleDependencies(CORE_MODULES)\n const ordered = topologicalSort(CORE_MODULES)\n\n for (const mod of ordered) {\n registerModule(mod, { source: 'core' })\n }\n}\n","/**\n * Module subject extractor.\n *\n * Extracts CASL subjects from module definitions.\n */\n\nimport type { ModuleManifest } from '@gzl10/nexus-sdk'\nimport { getTableAndSubject } from './definition-extractors.js'\n\n/**\n * Gets all CASL subjects of a module (from definitions).\n */\nexport function getModuleSubjects(mod: ModuleManifest): string[] {\n const subjects = new Set<string>()\n for (const def of mod.definitions ?? []) {\n const { subject } = getTableAndSubject(def)\n if (subject) subjects.add(subject)\n }\n return [...subjects]\n}\n","/**\n * SQL parsing utilities for extracting table names from queries.\n *\n * Centralizes regex patterns for INSERT, UPDATE, DELETE, SELECT queries.\n * Used by query-interceptor.ts and database.ts for CRUD event detection\n * and SQLite boolean conversion.\n */\n\ntype SqlOperationType = 'select' | 'insert' | 'update' | 'delete'\n\nconst SQL_PATTERNS: Record<SqlOperationType, RegExp> = {\n select: /from\\s+[\"'`]?(\\w+)[\"'`]?/i,\n insert: /insert into\\s+[\"'`]?(\\w+)[\"'`]?/i,\n update: /update\\s+[\"'`]?(\\w+)[\"'`]?/i,\n delete: /delete from\\s+[\"'`]?(\\w+)[\"'`]?/i\n}\n\n/**\n * Extracts the table name from a SQL query based on operation type.\n */\nexport function extractTableFromSql(sql: string, type: SqlOperationType): string | undefined {\n const match = sql.match(SQL_PATTERNS[type])\n return match?.[1]\n}\n\n/**\n * Extracts table name from a SELECT query.\n * @example extractTableFromSelect(\"SELECT * FROM users WHERE id = 1\") // \"users\"\n */\nexport function extractTableFromSelect(sql: string): string | undefined {\n return extractTableFromSql(sql, 'select')\n}\n\n/**\n * Extracts table name from an INSERT query.\n * @example extractTableFromInsert(\"INSERT INTO users (name) VALUES ('John')\") // \"users\"\n */\nexport function extractTableFromInsert(sql: string): string | undefined {\n return extractTableFromSql(sql, 'insert')\n}\n\n/**\n * Extracts table name from an UPDATE query.\n * @example extractTableFromUpdate(\"UPDATE users SET name = 'Jane' WHERE id = 1\") // \"users\"\n */\nexport function extractTableFromUpdate(sql: string): string | undefined {\n return extractTableFromSql(sql, 'update')\n}\n\n/**\n * Extracts table name from a DELETE query.\n * @example extractTableFromDelete(\"DELETE FROM users WHERE id = 1\") // \"users\"\n */\nexport function extractTableFromDelete(sql: string): string | undefined {\n return extractTableFromSql(sql, 'delete')\n}\n","/**\n * SQLite compatibility utilities.\n *\n * SQLite stores booleans as 0/1 (INTEGER).\n * This module handles the conversion to true/false.\n *\n * Each Knex instance gets its own scoped registry via createSqliteBooleanProcessor().\n * The module-level exports (registerBooleanColumn, sqlitePostProcess) use a shared\n * default instance for backward compatibility.\n */\n\nimport { extractTableFromSelect } from './sql-utils.js'\n\nexport interface SqliteBooleanProcessor {\n registerBooleanColumn(table: string, column: string): void\n convertBooleans(table: string, row: Record<string, unknown>): Record<string, unknown>\n postProcess(result: unknown, queryContext: { sql?: string }): unknown\n clear(): void\n}\n\n/**\n * Creates a scoped SQLite boolean processor with its own registry.\n * Use this for per-connection isolation (avoids test interference).\n */\nexport function createSqliteBooleanProcessor(): SqliteBooleanProcessor {\n const booleanColumns = new Map<string, Set<string>>()\n\n function registerBooleanColumn(table: string, column: string): void {\n if (!booleanColumns.has(table)) {\n booleanColumns.set(table, new Set())\n }\n booleanColumns.get(table)!.add(column)\n }\n\n function convertBooleans(\n table: string,\n row: Record<string, unknown>\n ): Record<string, unknown> {\n const columns = booleanColumns.get(table)\n if (!columns || columns.size === 0) return row\n\n const result = { ...row }\n for (const column of columns) {\n if (column in result) {\n const value = result[column]\n if (value === 0 || value === 1) {\n result[column] = value === 1\n }\n }\n }\n return result\n }\n\n function postProcess(result: unknown, queryContext: { sql?: string }): unknown {\n const sql = queryContext?.sql?.toLowerCase() ?? ''\n if (!sql.startsWith('select')) return result\n\n const table = extractTableFromSelect(sql)\n if (!table) return result\n\n if (Array.isArray(result)) {\n return result.map(row =>\n typeof row === 'object' && row !== null\n ? convertBooleans(table, row as Record<string, unknown>)\n : row\n )\n }\n\n if (typeof result === 'object' && result !== null) {\n return convertBooleans(table, result as Record<string, unknown>)\n }\n\n return result\n }\n\n function clear(): void {\n booleanColumns.clear()\n }\n\n return { registerBooleanColumn, convertBooleans, postProcess, clear }\n}\n\n// Default shared instance for backward compatibility\nconst defaultProcessor = createSqliteBooleanProcessor()\n\nexport const registerBooleanColumn = defaultProcessor.registerBooleanColumn\nexport const convertBooleans = defaultProcessor.convertBooleans\nexport const sqlitePostProcess = defaultProcessor.postProcess\n","/**\n * Runtime types for entity services\n */\n\nimport type { Router, Request, Response, NextFunction, RequestHandler } from 'express'\nimport type {\n EntityDefinition,\n PaginatedResult,\n EntityQuery\n} from '@gzl10/nexus-sdk'\n\nexport type { Request, Response, NextFunction, RequestHandler }\n\n// Re-export EntityQuery from SDK for backwards compatibility\nexport type { EntityQuery }\n\n/**\n * Entity service interface - base contract for all entity services\n */\nexport interface EntityService<T = unknown> {\n /** Get all entities with pagination */\n findAll(query?: EntityQuery): Promise<PaginatedResult<T>>\n\n /** Get single entity by ID */\n findById(id: string): Promise<T | null>\n\n /** Create new entity (optional - throws if not supported) */\n create?(data: Partial<T>): Promise<T>\n\n /** Update entity (optional - throws if not supported) */\n update?(id: string, data: Partial<T>): Promise<T>\n\n /** Delete entity (optional - throws if not supported) */\n delete?(id: string, options?: { actorId?: string }): Promise<void>\n\n /** Count entities matching optional filters */\n count?(filters?: Record<string, unknown>): Promise<number>\n\n /** Get entity definition */\n readonly definition: EntityDefinition\n}\n\n/**\n * Entity controller handler type\n */\nexport type EntityHandler = (req: Request, res: Response) => Promise<void>\n\n/**\n * Entity controller interface\n */\nexport interface EntityController {\n /** List entities */\n list: EntityHandler\n\n /** Get single entity */\n get: EntityHandler\n\n /** Create entity (optional) */\n create?: EntityHandler\n\n /** Update entity (optional) */\n update?: EntityHandler\n\n /** Delete entity (optional) */\n delete?: EntityHandler\n\n /** Count entities */\n count?: EntityHandler\n\n /** Bulk create entities */\n bulkCreate?: EntityHandler\n\n /** Bulk update entities */\n bulkUpdate?: EntityHandler\n\n /** Bulk delete entities */\n bulkDelete?: EntityHandler\n\n /** Execute action (for action entities) */\n execute?: EntityHandler\n\n /** Recompute computed entity (clears cache and recomputes) */\n recompute?: EntityHandler\n}\n\n/**\n * Complete runtime for an entity\n */\nexport interface EntityRuntime<T = unknown> {\n service: EntityService<T>\n controller: EntityController\n router: Router\n}\n\n","/**\n * Compose two sets of entity service hooks into one.\n *\n * Used to merge hooks declared on an EntityDefinition with hooks\n * passed via CreateEntityServiceOptions. Definition hooks run first,\n * options hooks run second.\n *\n * - before hooks: pipeline (data flows definition → options)\n * - after hooks: sequential (both run, definition first)\n * - find hooks: pipeline (result flows definition → options)\n */\n\nimport type { EntityServiceHooks } from '@gzl10/nexus-sdk'\n\nexport function composeHooks<T = unknown>(\n first?: EntityServiceHooks<T>,\n second?: EntityServiceHooks<T>\n): EntityServiceHooks<T> | undefined {\n if (!first && !second) return undefined\n if (!first) return second\n if (!second) return first\n\n return {\n beforeCreate: first.beforeCreate || second.beforeCreate\n ? async (data) => {\n let result = data\n if (first.beforeCreate) result = await first.beforeCreate(result)\n if (second.beforeCreate) result = await second.beforeCreate(result)\n return result\n }\n : undefined,\n\n beforeUpdate: first.beforeUpdate || second.beforeUpdate\n ? async (id, data) => {\n let result = data\n if (first.beforeUpdate) result = await first.beforeUpdate(id, result)\n if (second.beforeUpdate) result = await second.beforeUpdate(id, result)\n return result\n }\n : undefined,\n\n beforeDelete: first.beforeDelete || second.beforeDelete\n ? async (id) => {\n if (first.beforeDelete) await first.beforeDelete(id)\n if (second.beforeDelete) await second.beforeDelete(id)\n }\n : undefined,\n\n afterCreate: first.afterCreate || second.afterCreate\n ? async (record) => {\n if (first.afterCreate) await first.afterCreate(record)\n if (second.afterCreate) await second.afterCreate(record)\n }\n : undefined,\n\n afterUpdate: first.afterUpdate || second.afterUpdate\n ? async (record) => {\n if (first.afterUpdate) await first.afterUpdate(record)\n if (second.afterUpdate) await second.afterUpdate(record)\n }\n : undefined,\n\n afterDelete: first.afterDelete || second.afterDelete\n ? async (id) => {\n if (first.afterDelete) await first.afterDelete(id)\n if (second.afterDelete) await second.afterDelete(id)\n }\n : undefined,\n\n afterFindById: first.afterFindById || second.afterFindById\n ? async (record) => {\n let result = record\n if (first.afterFindById) result = await first.afterFindById(result)\n if (second.afterFindById) result = await second.afterFindById(result)\n return result\n }\n : undefined,\n\n afterFindAll: first.afterFindAll || second.afterFindAll\n ? async (paginatedResult) => {\n let result = paginatedResult\n if (first.afterFindAll) result = await first.afterFindAll(result)\n if (second.afterFindAll) result = await second.afterFindAll(result)\n return result\n }\n : undefined\n }\n}\n","/**\n * Sensitive fields filtering utilities.\n *\n * Shared between controllers and services to filter out sensitive fields\n * (passwords, tokens, secrets) from API responses.\n */\n\nimport type { PaginatedResult } from '@gzl10/nexus-sdk'\n\n/**\n * Exclude sensitive fields from a single entity.\n *\n * @param data - Entity data\n * @param sensitiveFields - Field names to exclude\n * @returns Entity without sensitive fields\n */\nexport function excludeSensitiveFields<T extends Record<string, unknown>>(\n data: T,\n sensitiveFields: string[]\n): Partial<T> {\n if (sensitiveFields.length === 0) return data\n\n const result = { ...data }\n for (const field of sensitiveFields) {\n delete result[field]\n }\n return result\n}\n\n/**\n * Exclude sensitive fields from an array of entities.\n *\n * @param items - Array of entities\n * @param sensitiveFields - Field names to exclude\n * @returns Array of entities without sensitive fields\n */\nexport function excludeSensitiveFieldsFromArray<T extends Record<string, unknown>>(\n items: T[],\n sensitiveFields: string[]\n): Partial<T>[] {\n if (sensitiveFields.length === 0) return items\n return items.map(item => excludeSensitiveFields(item, sensitiveFields))\n}\n\n/**\n * Exclude sensitive fields from a paginated result.\n *\n * @param result - Paginated result\n * @param sensitiveFields - Field names to exclude\n * @returns Paginated result with filtered items\n */\nexport function excludeSensitiveFieldsFromResult<T extends Record<string, unknown>>(\n result: PaginatedResult<T>,\n sensitiveFields: string[]\n): PaginatedResult<Partial<T>> {\n if (sensitiveFields.length === 0) return result\n return {\n ...result,\n items: excludeSensitiveFieldsFromArray(result.items, sensitiveFields)\n }\n}\n","/**\n * CASL field-level permission filtering.\n *\n * Utilities for filtering entity data based on CASL abilities,\n * supporting both read and write field-level permissions.\n */\n\nimport { permittedFieldsOf } from '@casl/ability/extra'\nimport type { MongoAbility, RawRuleOf } from '@casl/ability'\nimport type { PaginatedResult, ModuleContext } from '@gzl10/nexus-sdk'\n\n/**\n * Filter entity data to only include permitted fields based on CASL ability.\n * If no field restrictions exist, returns all fields.\n */\nexport function filterByPermittedFields<T extends Record<string, unknown>>(\n data: T,\n ability: unknown,\n action: 'read' | 'update',\n subject: string,\n allFields: string[]\n): Partial<T> {\n if (!ability) return data\n\n // Get permitted fields from CASL ability\n const permitted = permittedFieldsOf(ability as MongoAbility, action, subject, {\n fieldsFrom: (rule) => rule.fields || allFields\n })\n\n // If no restrictions (empty array with fieldsFrom means all allowed)\n if (permitted.length === 0 || permitted.length === allFields.length) {\n return data\n }\n\n // Filter to only permitted fields\n const result: Partial<T> = {}\n for (const field of permitted) {\n if (field in data) {\n result[field as keyof T] = data[field as keyof T]\n }\n }\n // Always include id if present\n if ('id' in data && !('id' in result)) {\n result['id' as keyof T] = data['id' as keyof T]\n }\n return result\n}\n\n/**\n * Filter paginated result by permitted fields.\n */\nexport function filterResultByPermittedFields<T extends Record<string, unknown>>(\n result: PaginatedResult<T>,\n ability: unknown,\n subject: string,\n allFields: string[]\n): PaginatedResult<Partial<T>> {\n if (!ability) return result\n return {\n ...result,\n items: result.items.map(item => filterByPermittedFields(item, ability, 'read', subject, allFields))\n }\n}\n\n/**\n * Validate that user can write all fields in payload.\n * @throws ForbiddenError if user tries to write restricted fields\n */\nexport function validateWritePermissions(\n payload: Record<string, unknown>,\n ability: unknown,\n action: 'create' | 'update',\n subject: string,\n allFields: string[],\n ctx: ModuleContext\n): void {\n if (!ability) return\n\n const permitted = permittedFieldsOf(ability as MongoAbility, action, subject, {\n fieldsFrom: (rule) => rule.fields || allFields\n })\n\n // If no restrictions, all fields are allowed\n if (permitted.length === 0 || permitted.length === allFields.length) {\n return\n }\n\n // Check each field in payload\n const forbiddenFields = Object.keys(payload).filter(\n field => !permitted.includes(field) && field !== 'id'\n )\n\n if (forbiddenFields.length > 0) {\n throw new ctx.core.errors.ForbiddenError(\n `Not allowed to modify fields: ${forbiddenFields.join(', ')}`\n )\n }\n}\n\n/**\n * Extract CASL conditions to apply as query filters (row-level security).\n *\n * Analyzes the ability rules for the given action/subject and returns\n * conditions that should be added as WHERE clauses to the query.\n *\n * Returns null when no filtering is needed (user has unconditional access\n * or no matching rules exist).\n */\nexport function getCaslConditionsForQuery(\n ability: unknown,\n action: string,\n subject: string\n): Record<string, unknown> | null {\n const mongoAbility = ability as MongoAbility | null\n if (!mongoAbility?.rules) return null\n\n const rules = mongoAbility.rules as RawRuleOf<MongoAbility>[]\n\n // Filter rules that allow this action on this subject\n const matchingRules = rules.filter(rule => {\n if (rule.inverted) return false\n\n const actions = Array.isArray(rule.action) ? rule.action : [rule.action]\n if (!actions.includes(action) && !actions.includes('manage')) return false\n\n const subjects = Array.isArray(rule.subject) ? rule.subject : [rule.subject]\n if (!subjects.includes(subject) && !subjects.includes('all')) return false\n\n return true\n })\n\n if (matchingRules.length === 0) return null\n\n // If any matching rule has NO conditions, user has full access (no filter)\n if (matchingRules.some(rule => !rule.conditions)) return null\n\n // All matching rules have conditions — merge them\n const merged: Record<string, unknown> = {}\n for (const rule of matchingRules) {\n if (rule.conditions) {\n Object.assign(merged, rule.conditions as Record<string, unknown>)\n }\n }\n\n return Object.keys(merged).length > 0 ? merged : null\n}\n","/**\n * Seed data loader - handles both inline arrays and SeedConfig objects\n */\n\nimport type { SeedConfig } from '@gzl10/nexus-sdk'\nimport type { Logger } from 'pino'\n\ntype SeedInput = Array<Record<string, unknown>> | SeedConfig | undefined\n\n/**\n * Load seed data from various sources (inline array, URL, file, or API)\n *\n * @param seed - Inline array or SeedConfig object\n * @param logger - Logger instance for error reporting\n * @param entityType - Entity type name for logging context\n * @returns Array of seed data items, or empty array if no data\n */\nexport async function loadSeedData(\n seed: SeedInput,\n logger: Logger,\n entityType: string\n): Promise<Array<Record<string, unknown>>> {\n if (!seed) {\n return []\n }\n\n // If it's already an array, return it directly\n if (Array.isArray(seed)) {\n return seed\n }\n\n // Otherwise it's a SeedConfig object\n const config = seed as SeedConfig\n\n switch (config.source) {\n case 'inline':\n return config.data ?? []\n\n case 'url':\n case 'api':\n if (!config.url) {\n logger.warn({ entityType }, 'Seed config has source=url but no URL provided')\n return []\n }\n try {\n const response = await fetch(config.url, {\n headers: config.headers\n })\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`)\n }\n const data = await response.json() as Record<string, unknown> | Record<string, unknown>[]\n // If transform function is provided as string, it's not supported in runtime\n // Just return the data as-is\n return Array.isArray(data) ? data : [data]\n } catch (error) {\n logger.error({ entityType, url: config.url, error }, 'Failed to load seed data from URL')\n return []\n }\n\n case 'file':\n // File loading would require fs access, not commonly needed\n logger.warn({ entityType }, 'Seed source=file is not supported at runtime')\n return []\n\n default:\n logger.warn({ entityType, source: config.source }, 'Unknown seed source type')\n return []\n }\n}\n","/**\n * Runtime helpers barrel export.\n */\n\nexport {\n excludeSensitiveFields,\n excludeSensitiveFieldsFromArray,\n excludeSensitiveFieldsFromResult\n} from './sensitive-fields.js'\n\nexport {\n filterByPermittedFields,\n filterResultByPermittedFields,\n validateWritePermissions,\n getCaslConditionsForQuery\n} from './casl-filter.js'\n\nexport { loadSeedData } from './seed-loader.js'\n\nexport { composeHooks } from './compose-hooks.js'\n","/**\n * Query filter helpers.\n *\n * Shared logic for applying filters to Knex queries.\n * Used by both KnexAdapter and BaseService for DRY.\n */\n\nimport type { Knex } from 'knex'\nimport type { FilterOperators } from '@gzl10/nexus-sdk'\n\n/**\n * Escape LIKE wildcard characters (%, _, \\) in user input.\n * Prevents wildcard injection in $contains, $startswith, $endswith operators.\n */\nexport function escapeLikeWildcards(value: string): string {\n return value.replace(/[%_\\\\]/g, '\\\\$&')\n}\n\n/**\n * Apply filters to a Knex query builder.\n *\n * Supports:\n * - Simple equality: `{ name: 'John' }`\n * - Operators: `{ age: { $gte: 18 } }`\n * - Array shorthand for $in: `{ status: ['active', 'pending'] }`\n * - Null check: `{ deletedAt: null }`\n *\n * @example\n * let qb = db('users')\n * qb = applyFilters(qb, { status: 'active', age: { $gte: 18 } })\n */\nexport function applyFilters(qb: Knex.QueryBuilder, filters: Record<string, unknown>): Knex.QueryBuilder {\n for (const [key, value] of Object.entries(filters)) {\n if (value === undefined) continue\n\n // $or: OR logic between filter groups\n if (key === '$or') {\n if (Array.isArray(value) && value.length > 0) {\n qb.where(function () {\n for (const group of value as Record<string, unknown>[]) {\n this.orWhere(function () {\n applyFilters(this, group)\n })\n }\n })\n }\n continue\n }\n\n // Null value - direct comparison\n if (value === null) {\n qb.whereNull(key)\n continue\n }\n\n // Empty string - skip\n if (value === '') continue\n\n // Array shorthand for $in\n if (Array.isArray(value) && value.length > 0) {\n qb.whereIn(key, value as (string | number)[])\n continue\n }\n\n // Object with operators\n if (typeof value === 'object' && !Array.isArray(value)) {\n applyFilterOperators(qb, key, value as FilterOperators)\n continue\n }\n\n // Simple equality\n qb.where(key, value)\n }\n return qb\n}\n\n/**\n * Apply filter operators for a single field.\n *\n * Supported operators:\n * - $eq: Equal\n * - $ne: Not equal\n * - $gt: Greater than\n * - $gte: Greater than or equal\n * - $lt: Less than\n * - $lte: Less than or equal\n * - $contains: LIKE %val%\n * - $startswith: LIKE val%\n * - $endswith: LIKE %val\n * - $in: IN (array)\n * - $nin: NOT IN (array)\n * - $isnull: IS NULL / IS NOT NULL\n */\nexport function applyFilterOperators(qb: Knex.QueryBuilder, field: string, operators: FilterOperators): void {\n for (const [op, val] of Object.entries(operators)) {\n if (val === undefined) continue\n\n switch (op) {\n case '$eq':\n if (val === null) qb.whereNull(field)\n else qb.where(field, val)\n break\n\n case '$ne':\n if (val === null) qb.whereNotNull(field)\n else qb.whereNot(field, val)\n break\n\n case '$gt':\n qb.where(field, '>', val)\n break\n\n case '$gte':\n qb.where(field, '>=', val)\n break\n\n case '$lt':\n qb.where(field, '<', val)\n break\n\n case '$lte':\n qb.where(field, '<=', val)\n break\n\n case '$contains':\n qb.where(field, 'like', `%${escapeLikeWildcards(String(val))}%`)\n break\n\n case '$startswith':\n qb.where(field, 'like', `${escapeLikeWildcards(String(val))}%`)\n break\n\n case '$endswith':\n qb.where(field, 'like', `%${escapeLikeWildcards(String(val))}`)\n break\n\n case '$in':\n if (Array.isArray(val) && val.length > 0) {\n qb.whereIn(field, val as (string | number)[])\n }\n break\n\n case '$nin':\n if (Array.isArray(val) && val.length > 0) {\n qb.whereNotIn(field, val as (string | number)[])\n }\n break\n\n case '$isnull':\n if (val === true) qb.whereNull(field)\n else if (val === false) qb.whereNotNull(field)\n break\n\n case '$between':\n if (Array.isArray(val) && val.length === 2) {\n qb.whereBetween(field, val as [string | number | Date, string | number | Date])\n }\n break\n }\n }\n}\n\n// ============================================================================\n// In-Memory Filter Helpers\n// ============================================================================\n\n/**\n * Apply filters in memory to an array of items.\n *\n * Supports same operators as Knex version:\n * $eq, $ne, $gt, $gte, $lt, $lte, $contains, $startswith, $endswith, $in, $nin, $isnull\n *\n * @example\n * const filtered = applyInMemoryFilters(users, { status: 'active', age: { $gte: 18 } })\n */\nexport function applyInMemoryFilters<T = unknown>(items: T[], filters: Record<string, unknown>): T[] {\n return items.filter(item => {\n const record = item as Record<string, unknown>\n return matchRecordFilters(record, filters)\n })\n}\n\n/**\n * Check if a single record matches all filters (AND logic).\n * Extracted for reuse by $or groups.\n */\nfunction matchRecordFilters(record: Record<string, unknown>, filters: Record<string, unknown>): boolean {\n for (const [key, value] of Object.entries(filters)) {\n if (value === undefined) continue\n\n // $or: at least one group must match\n if (key === '$or') {\n if (Array.isArray(value) && value.length > 0) {\n const orMatch = (value as Record<string, unknown>[]).some(group =>\n matchRecordFilters(record, group)\n )\n if (!orMatch) return false\n }\n continue\n }\n\n const itemValue = record[key]\n\n // Null value - check if field is null\n if (value === null) {\n if (itemValue !== null && itemValue !== undefined) return false\n continue\n }\n\n // Empty string - skip\n if (value === '') continue\n\n // Array shorthand for $in\n if (Array.isArray(value)) {\n // Empty array = no filter (return all)\n if (value.length === 0) continue\n if (!value.includes(itemValue)) return false\n continue\n }\n\n // Object with operators\n if (typeof value === 'object' && !Array.isArray(value)) {\n if (!matchFilterOperators(itemValue, value as FilterOperators)) return false\n continue\n }\n\n // Simple equality\n if (itemValue !== value) return false\n }\n\n return true\n}\n\n/**\n * Match filter operators against a single value (in-memory).\n */\nexport function matchFilterOperators(itemValue: unknown, operators: FilterOperators): boolean {\n for (const [op, val] of Object.entries(operators)) {\n if (val === undefined) continue\n\n switch (op) {\n case '$eq':\n if (val === null) {\n if (itemValue !== null && itemValue !== undefined) return false\n } else if (itemValue !== val) return false\n break\n\n case '$ne':\n if (val === null) {\n if (itemValue === null || itemValue === undefined) return false\n } else if (itemValue === val) return false\n break\n\n case '$gt':\n if (typeof itemValue !== 'number' && typeof itemValue !== 'string') return false\n if (!(itemValue > val)) return false\n break\n\n case '$gte':\n if (typeof itemValue !== 'number' && typeof itemValue !== 'string') return false\n if (!(itemValue >= val)) return false\n break\n\n case '$lt':\n if (typeof itemValue !== 'number' && typeof itemValue !== 'string') return false\n if (!(itemValue < val)) return false\n break\n\n case '$lte':\n if (typeof itemValue !== 'number' && typeof itemValue !== 'string') return false\n if (!(itemValue <= val)) return false\n break\n\n case '$contains':\n if (typeof itemValue !== 'string' || typeof val !== 'string') return false\n if (!itemValue.toLowerCase().includes(val.toLowerCase())) return false\n break\n\n case '$startswith':\n if (typeof itemValue !== 'string' || typeof val !== 'string') return false\n if (!itemValue.toLowerCase().startsWith(val.toLowerCase())) return false\n break\n\n case '$endswith':\n if (typeof itemValue !== 'string' || typeof val !== 'string') return false\n if (!itemValue.toLowerCase().endsWith(val.toLowerCase())) return false\n break\n\n case '$in':\n if (!Array.isArray(val) || val.length === 0) break\n if (!val.includes(itemValue)) return false\n break\n\n case '$nin':\n if (!Array.isArray(val) || val.length === 0) break\n if (val.includes(itemValue)) return false\n break\n\n case '$isnull':\n if (val === true && itemValue !== null && itemValue !== undefined) return false\n if (val === false && (itemValue === null || itemValue === undefined)) return false\n break\n\n case '$between':\n if (!Array.isArray(val) || val.length !== 2) break\n if (typeof itemValue !== 'number' && typeof itemValue !== 'string') return false\n if (!(itemValue >= val[0] && itemValue <= val[1])) return false\n break\n }\n }\n return true\n}\n","/**\n * Base Entity Service - Abstract class for all entity services\n */\n\nimport type { Knex } from 'knex'\nimport type {\n EntityDefinition,\n PaginatedResult,\n ModuleContext,\n EntityServiceHooks,\n ActionDefinition\n} from '@gzl10/nexus-sdk'\nimport { resolveLocalized } from '@gzl10/nexus-sdk'\nimport type { EntityQuery, EntityService } from '../types.js'\nimport type { LoggerService } from '../../core/logger/types.js'\nimport {\n excludeSensitiveFields as excludeSensitive,\n excludeSensitiveFieldsFromArray as excludeSensitiveArray\n} from '../helpers/index.js'\nimport {\n applyFilters as applyFiltersHelper,\n applyInMemoryFilters as applyInMemoryFiltersHelper\n} from '../../db/filter-helpers.js'\n\n/**\n * Abstract base class for entity services\n *\n * Provides common functionality:\n * - Access to ModuleContext (db, logger, etc.)\n * - Entity definition\n * - Default pagination\n * - Hook system for lifecycle events (internos + externos via createEntityService)\n */\nexport abstract class BaseEntityService<T = unknown> implements EntityService<T> {\n protected readonly db: Knex\n protected readonly logger: ModuleContext['core']['logger']\n protected readonly generateId: () => string\n protected readonly generateIdByType: (type?: 'ulid' | 'uuid' | 'nanoid' | 'cuid2' | 'auto' | 'custom') => string | undefined\n protected readonly errors: ModuleContext['core']['errors']\n /** Module name for entity event emission (stamped by registry) */\n protected readonly moduleName: string\n /** Stable entity identifier for real-time rooms and events (table, key, or label) */\n protected readonly entityKey: string\n /** Hooks externos inyectados via createEntityService */\n protected readonly externalHooks?: EntityServiceHooks<T>\n /** Temporary storage for delete actor ID (used by audit event emission) */\n protected _deleteActorId?: string\n\n constructor(\n protected readonly ctx: ModuleContext,\n public readonly definition: EntityDefinition,\n hooks?: EntityServiceHooks<T>\n ) {\n const adapterName = (definition as unknown as { adapter?: string }).adapter\n this.db = ctx.db.getKnex(adapterName)\n this.logger = ctx.core.logger\n this.generateId = ctx.core.generateId\n this.generateIdByType = ctx.core.generateIdByType\n this.errors = ctx.core.errors\n this.moduleName = (definition as unknown as { _moduleName?: string })._moduleName ?? 'unknown'\n const def = definition as Record<string, unknown>\n this.entityKey = (def['table'] as string) ?? (def['key'] as string) ?? resolveLocalized(definition.label, 'en').toLowerCase().replace(/\\s+/g, '_')\n this.externalHooks = hooks\n }\n\n /**\n * Get the ID type configured for this entity.\n * @returns ID type from definition or 'ulid' as default\n */\n protected getIdType(): 'ulid' | 'uuid' | 'nanoid' | 'cuid2' | 'auto' | 'custom' | 'pattern' {\n const idField = this.definition.fields?.['id']\n return (idField?.db?.idType as 'ulid' | 'uuid' | 'nanoid' | 'cuid2' | 'auto' | 'custom' | 'pattern') ?? 'ulid'\n }\n\n /**\n * Get the pattern configuration for this entity (if using pattern IDs).\n */\n protected getPatternConfig(): { pattern: string; prefix?: string; suffix?: string; resetOn?: 'never' | 'year' | 'month' | 'day'; startAt?: number } | undefined {\n const idField = this.definition.fields?.['id']\n return idField?.db?.patternConfig\n }\n\n /**\n * Resolve the ID for a new entity (async version).\n * - If user provides ID, use it (for all types)\n * - For 'auto': return undefined (DB generates)\n * - For 'custom' without user ID: throw error\n * - For 'pattern': generate using sequence\n * - For other types: generate ID\n *\n * @param providedId - ID provided by user (optional)\n * @returns Resolved ID or undefined for auto-increment\n */\n protected async resolveEntityId(providedId?: unknown): Promise<string | undefined> {\n // User provided ID takes precedence\n if (providedId !== undefined && providedId !== null) {\n return String(providedId)\n }\n\n const idType = this.getIdType()\n\n // Auto-increment: let DB handle it\n if (idType === 'auto') {\n return undefined\n }\n\n // Custom: user must provide ID\n if (idType === 'custom') {\n throw new this.errors.ValidationError('ID is required for custom ID type')\n }\n\n // Pattern: generate using sequence\n if (idType === 'pattern') {\n const patternConfig = this.getPatternConfig()\n if (!patternConfig) {\n throw new this.errors.ValidationError('Pattern configuration is required for pattern ID type')\n }\n // Import dynamically to avoid circular dependency\n const { generatePatternId } = await import('../../core/utils/sequence.js')\n return generatePatternId(this.db, this.table, patternConfig)\n }\n\n // Generate ID for other types\n return this.generateIdByType(idType)\n }\n\n /**\n * Get table name for persistent entities\n */\n protected get table(): string {\n if ('table' in this.definition && this.definition.table) {\n return this.definition.table\n }\n throw new Error(`Entity ${resolveLocalized(this.definition.label, 'en')} has no table`)\n }\n\n protected get loggerService(): LoggerService | undefined {\n return this.ctx.services.getOptional<LoggerService>('logger')\n }\n\n /**\n * Get all entities with pagination\n */\n abstract findAll(query?: EntityQuery): Promise<PaginatedResult<T>>\n\n /**\n * Get entity by ID\n */\n abstract findById(id: string): Promise<T | null>\n\n // ============================================================================\n // Count\n // ============================================================================\n\n /**\n * Count entities matching optional filters.\n * Available for all persistent entity types.\n * Subclasses can override for soft delete or adapter support.\n */\n async count(filters?: Record<string, unknown>): Promise<number> {\n let qb = this.db(this.table)\n\n if (filters) {\n qb = this.applyFilters(qb, filters)\n }\n\n const result = await qb.count('* as count').first<{ count: string | number }>()\n return Number(result?.count ?? 0)\n }\n\n // ============================================================================\n // Template Method for findAll\n // ============================================================================\n\n /**\n * Template method for findAll with customizable hooks.\n * Subclasses can override hooks instead of reimplementing the whole method.\n *\n * Hooks:\n * - applyTypeFilters(qb): Add type-specific filters (softDelete, TTL, etc.)\n * - getDefaultSort(): Return default sort config { field, order }\n * - afterFindAll(items): Post-process items before returning\n */\n protected async baseFindAll(query?: EntityQuery): Promise<PaginatedResult<T>> {\n const { page, limit, offset } = this.getPagination(query)\n\n // Build base query with type-specific filters\n let qb = this.applyTypeFilters(this.db(this.table))\n\n // Apply search\n if (query?.search) {\n qb = this.applySearch(qb.clone(), query.search)\n }\n\n // Apply filters\n if (query?.filters) {\n qb = this.applyFilters(qb.clone(), query.filters)\n }\n\n // Get total count\n const countResult = await qb.clone().count('* as count').first<{ count: string | number }>()\n const total = Number(countResult?.count ?? 0)\n\n // Apply sorting (with type-specific defaults)\n qb = this.applySortingWithDefaults(qb, query)\n // limit=0 means unpaginated — skip limit/offset\n if (limit > 0) {\n qb = qb.limit(limit).offset(offset)\n }\n\n const rawItems = await qb as T[]\n\n // Parse JSON fields\n const items = this.parseJsonFieldsFromArray(rawItems as Record<string, unknown>[]) as T[]\n\n // Post-process items\n const processedItems = await this.afterFindAll(items)\n\n let result = this.buildPaginatedResult(processedItems, total, page, limit)\n if (this.externalHooks?.afterFindAll) {\n result = await this.externalHooks.afterFindAll(result)\n }\n return result\n }\n\n /**\n * Apply type-specific filters to query builder.\n * Override in subclasses for softDelete, TTL, etc.\n */\n protected applyTypeFilters(qb: Knex.QueryBuilder): Knex.QueryBuilder {\n return qb\n }\n\n /**\n * Get default sort configuration for this entity type.\n * Uses definition.defaultSort if defined, otherwise null.\n * Override in subclasses for type-specific defaults (e.g., events = created_at desc).\n */\n protected getDefaultSort(): { field: string; order: 'asc' | 'desc' } | null {\n return (this.definition as { defaultSort?: { field: string; order: 'asc' | 'desc' } }).defaultSort ?? null\n }\n\n /**\n * Apply sorting with type-specific defaults.\n */\n protected applySortingWithDefaults(qb: Knex.QueryBuilder, query?: EntityQuery): Knex.QueryBuilder {\n if (query?.sort) {\n return qb.orderBy(query.sort, query.order ?? 'asc')\n }\n\n const defaultSort = this.getDefaultSort()\n if (defaultSort) {\n return qb.orderBy(defaultSort.field, defaultSort.order)\n }\n\n // Fall back to base applySorting (uses labelField)\n return this.applySorting(qb, query)\n }\n\n /**\n * Post-process items after query.\n * Override in subclasses for cleanup tasks, etc.\n */\n protected async afterFindAll(items: T[]): Promise<T[]> {\n return items\n }\n\n /**\n * Standard findById implementation for DB-backed entities.\n * Subclasses can override findById to add custom behavior (e.g., soft delete filter, default merge).\n */\n protected async baseFindById(id: string): Promise<T | null> {\n const entity = await this.db(this.table).where('id', id).first()\n if (!entity) return null\n const parsed = this.parseJsonFields(entity as Record<string, unknown>)\n return this.processAfterFindById(parsed as T)\n }\n\n /**\n * In-memory findAll for services without DB tables (computed, virtual).\n * Applies filters, search, sort, and pagination to a raw array.\n */\n protected inMemoryFindAll(\n items: unknown[],\n query?: EntityQuery\n ): PaginatedResult<T> {\n const { page, limit, offset } = this.getPagination(query)\n let result = [...items]\n\n // Apply in-memory filters\n if (query?.filters) {\n result = this.applyInMemoryFilters(result, query.filters)\n }\n\n // Apply search by labelField\n const labelField = (this.definition as { labelField?: string }).labelField\n if (query?.search && labelField) {\n const searchLower = query.search.toLowerCase()\n result = result.filter(item => {\n const value = (item as Record<string, unknown>)[labelField]\n return value != null && String(value).toLowerCase().includes(searchLower)\n })\n }\n\n // Apply sorting (query takes precedence, then definition.defaultSort)\n const sortField = query?.sort ?? this.getDefaultSort()?.field\n const sortOrder = query?.sort ? (query.order ?? 'asc') : (this.getDefaultSort()?.order ?? 'asc')\n if (sortField) {\n result.sort((a, b) => {\n const aVal = (a as Record<string, unknown>)[sortField]\n const bVal = (b as Record<string, unknown>)[sortField]\n if (String(aVal) < String(bVal)) return sortOrder === 'asc' ? -1 : 1\n if (String(aVal) > String(bVal)) return sortOrder === 'asc' ? 1 : -1\n return 0\n })\n }\n\n // Paginate (limit=0 means unpaginated)\n const total = result.length\n const paginatedItems = limit === 0 ? result as T[] : result.slice(offset, offset + limit) as T[]\n\n return this.buildPaginatedResult(paginatedItems, total, page, limit)\n }\n\n /**\n * Get current timestamp formatted for the database.\n * Avoids importing schema-helpers at module level (circular dependency).\n */\n protected getNowTimestamp(): string {\n const client = (this.db as unknown as { client: { config: { client: string } } }).client?.config?.client ?? ''\n if (client === 'mysql' || client === 'mysql2') {\n return new Date().toISOString().slice(0, 19).replace('T', ' ')\n }\n return new Date().toISOString()\n }\n\n // ============================================================================\n // Base CRUD Methods\n // ============================================================================\n\n /**\n * Standard create implementation for DB-backed entities.\n * @param data - Validated input data\n * @param options.timestamps - Add created_at/updated_at\n */\n protected async baseCreate(\n data: Record<string, unknown>,\n options: { timestamps?: boolean } = {}\n ): Promise<T> {\n // Strip internal fields injected by controller\n const { _authUserId, ...cleanData } = data\n // Resolve ID\n const resolvedId = await this.resolveEntityId(cleanData['id'])\n const entityData: Record<string, unknown> = { ...cleanData }\n if (resolvedId !== undefined) {\n entityData['id'] = resolvedId\n }\n\n // Timestamps\n if (options.timestamps) {\n const now = this.getNowTimestamp()\n entityData['created_at'] = now\n entityData['updated_at'] = now\n }\n\n // Hooks\n const processedData = await this.beforeCreate(entityData as Partial<T>)\n\n // Serialize and insert\n const dbData = this.serializeForDb(processedData as Record<string, unknown>)\n const idType = this.getIdType()\n let entityId: string\n\n if (idType === 'auto') {\n const [insertedId] = await this.db(this.table).insert(dbData).returning('id')\n entityId = String(typeof insertedId === 'object' ? insertedId.id : insertedId)\n } else {\n await this.db(this.table).insert(dbData)\n entityId = (dbData as Record<string, unknown>)['id'] as string\n }\n\n // Re-fetch\n const created = await this.findById(entityId)\n if (!created) {\n throw new this.errors.AppError(`${resolveLocalized(this.definition.label, 'en')} not found after create`, 404)\n }\n\n await this.afterCreate(created)\n return created\n }\n\n /**\n * Standard update implementation for DB-backed entities.\n */\n protected async baseUpdate(\n id: string,\n data: Record<string, unknown>,\n options: { timestamps?: boolean } = {}\n ): Promise<T> {\n // Verify existence\n const existing = await this.findById(id)\n if (!existing) {\n throw new this.errors.NotFoundError(resolveLocalized(this.definition.label, 'en'))\n }\n\n // Strip internal fields injected by controller\n const { _authUserId, ...cleanData } = data\n const updateData: Record<string, unknown> = { ...cleanData }\n\n // Timestamps\n if (options.timestamps) {\n updateData['updated_at'] = this.getNowTimestamp()\n }\n\n // Hooks\n const processedData = await this.beforeUpdate(id, updateData as Partial<T>)\n\n // Serialize and update\n const dbData = this.serializeForDb(processedData as Record<string, unknown>)\n await this.db(this.table).where('id', id).update(dbData)\n\n // Re-fetch\n const updated = await this.findById(id)\n if (!updated) {\n throw new this.errors.AppError(`${resolveLocalized(this.definition.label, 'en')} not found after update`, 404)\n }\n\n await this.afterUpdate(updated)\n return updated\n }\n\n /**\n * Standard hard-delete implementation for DB-backed entities.\n */\n protected async baseDelete(id: string): Promise<void> {\n const existing = await this.findById(id)\n if (!existing) {\n throw new this.errors.NotFoundError(resolveLocalized(this.definition.label, 'en'))\n }\n\n await this.beforeDelete(id)\n await this.db(this.table).where('id', id).delete()\n await this.afterDelete(id)\n }\n\n /**\n * Process a record after findById.\n * Calls external afterFindById hook if defined.\n */\n protected async processAfterFindById(record: T | null): Promise<T | null> {\n if (record && this.externalHooks?.afterFindById) {\n return await this.externalHooks.afterFindById(record)\n }\n return record\n }\n\n // ============================================================================\n // Hooks - Llaman primero a hooks externos, luego internos (subclases)\n // ============================================================================\n\n /**\n * Called before creating an entity.\n * Runs the external hook (if any) and then allows subclass override.\n * @returns Modified data\n */\n protected async beforeCreate(data: Partial<T>): Promise<Partial<T>> {\n let result = data\n if (this.externalHooks?.beforeCreate) {\n result = await this.externalHooks.beforeCreate(result)\n }\n return this.internalBeforeCreate(result)\n }\n\n /** Override in subclasses for custom beforeCreate logic */\n protected async internalBeforeCreate(data: Partial<T>): Promise<Partial<T>> {\n return this.sanitizeForeignKeys(data)\n }\n\n /**\n * Convert empty strings to null for foreign key fields.\n * Prevents FK constraint violations when selects are cleared.\n */\n protected sanitizeForeignKeys(data: Partial<T>): Partial<T> {\n const result = { ...data } as Record<string, unknown>\n\n for (const [key, field] of Object.entries(this.definition.fields ?? {})) {\n // Fields with relation or dynamic options (relation selects)\n const hasRelation = 'relation' in field && field.relation !== undefined\n const isDynamicSelect = field.options?.endpoint !== undefined\n\n if ((hasRelation || isDynamicSelect) && result[key] === '') {\n result[key] = null\n }\n }\n\n return result as Partial<T>\n }\n\n /**\n * Called after creating an entity\n */\n protected async afterCreate(entity: T): Promise<void> {\n await this.internalAfterCreate(entity)\n if (this.externalHooks?.afterCreate) {\n await this.externalHooks.afterCreate(entity)\n }\n }\n\n /** Override in subclasses for custom afterCreate logic */\n protected async internalAfterCreate(entity: T): Promise<void> {\n this.ctx.core.logger.debug({ entityKey: this.entityKey }, '[DEBUG-RT] internalAfterCreate called')\n this.emitEntityEvent('created', entity)\n this.emitAuditEvent('record.created', entity)\n }\n\n /**\n * Called before updating an entity\n * @returns Modified data\n */\n protected async beforeUpdate(id: string, data: Partial<T>): Promise<Partial<T>> {\n let result = data\n if (this.externalHooks?.beforeUpdate) {\n result = await this.externalHooks.beforeUpdate(id, result)\n }\n return this.internalBeforeUpdate(id, result)\n }\n\n /** Override in subclasses for custom beforeUpdate logic */\n protected async internalBeforeUpdate(_id: string, data: Partial<T>): Promise<Partial<T>> {\n return this.sanitizeForeignKeys(data)\n }\n\n /**\n * Called after updating an entity\n */\n protected async afterUpdate(entity: T): Promise<void> {\n await this.internalAfterUpdate(entity)\n if (this.externalHooks?.afterUpdate) {\n await this.externalHooks.afterUpdate(entity)\n }\n }\n\n /** Override in subclasses for custom afterUpdate logic */\n protected async internalAfterUpdate(entity: T): Promise<void> {\n this.emitEntityEvent('updated', entity)\n this.emitAuditEvent('record.updated', entity)\n }\n\n /**\n * Called before deleting an entity\n */\n protected async beforeDelete(id: string): Promise<void> {\n if (this.externalHooks?.beforeDelete) {\n await this.externalHooks.beforeDelete(id)\n }\n await this.internalBeforeDelete(id)\n }\n\n /** Override in subclasses for custom beforeDelete logic */\n protected async internalBeforeDelete(_id: string): Promise<void> {\n // Override in subclasses\n }\n\n /**\n * Called after deleting an entity\n */\n protected async afterDelete(id: string): Promise<void> {\n await this.internalAfterDelete(id)\n if (this.externalHooks?.afterDelete) {\n await this.externalHooks.afterDelete(id)\n }\n }\n\n /** Override in subclasses for custom afterDelete logic */\n protected async internalAfterDelete(id: string): Promise<void> {\n this.emitEntityEvent('deleted', undefined, id)\n this.emitAuditEvent('record.deleted', undefined, id, this._deleteActorId)\n }\n\n // ============================================================================\n // Real-time event emission\n // ============================================================================\n\n /**\n * Emit entity change event via ctx.core.events if realtime mode is enabled.\n * The event bridge routes these to Socket.IO rooms automatically.\n * Fire-and-forget: does not block the HTTP response.\n */\n protected emitEntityEvent(action: 'created' | 'updated' | 'deleted', entity?: T, id?: string): void {\n const def = this.definition as Record<string, unknown>\n this.ctx.core.logger.debug({ action, realtime: def['realtime'], moduleName: this.moduleName, entityKey: this.entityKey }, '[DEBUG-RT] emitEntityEvent called')\n if (!def['realtime']) return\n\n const recordId = id ?? (entity as Record<string, unknown>)?.['id'] as string\n if (!recordId) {\n this.ctx.core.logger.debug({ action }, '[DEBUG-RT] no recordId, skipping')\n return\n }\n\n this.ctx.core.logger.debug({ action, recordId, module: this.moduleName, entity: this.entityKey }, '[DEBUG-RT] emitting entity event')\n this.ctx.core.events.emit(`entity.${action}`, {\n tenantId: this.ctx.tenantId,\n module: this.moduleName,\n entity: this.entityKey,\n action,\n id: recordId,\n data: action !== 'deleted' ? (entity as Record<string, unknown>) : undefined,\n userId: (entity as Record<string, unknown>)?.['_authUserId'] as string | undefined,\n timestamp: new Date().toISOString()\n })\n }\n\n /**\n * Emit audit.log event for entities with audit: true.\n * Fire-and-forget via event bus — audit module picks it up if loaded.\n */\n protected emitAuditEvent(action: 'record.created' | 'record.updated' | 'record.deleted', entity?: T, id?: string, actorId?: string): void {\n const def = this.definition as Record<string, unknown>\n if (!def['audit']) return\n\n const recordId = id ?? (entity as Record<string, unknown>)?.['id'] as string\n if (!recordId) return\n\n const resolvedActorId = actorId\n ?? (entity as Record<string, unknown>)?.['created_by'] as string | undefined\n ?? (entity as Record<string, unknown>)?.['updated_by'] as string | undefined\n\n this.ctx.events.notify('audit.log', {\n source: `entity:${this.moduleName}.${this.entityKey}`,\n action,\n actorId: resolvedActorId,\n resourceType: this.entityKey,\n resourceId: recordId\n })\n }\n\n // ============================================================================\n // Helpers\n // ============================================================================\n\n /**\n * Build paginated result from items and count\n */\n protected buildPaginatedResult(\n items: T[],\n total: number,\n page: number,\n limit: number\n ): PaginatedResult<T> {\n // limit=0 means unpaginated — single page with all items\n const totalPages = limit === 0 ? 1 : Math.ceil(total / limit)\n return {\n items,\n total,\n page: limit === 0 ? 1 : page,\n limit: limit === 0 ? total : limit,\n totalPages,\n hasNext: limit === 0 ? false : page < totalPages\n }\n }\n\n /**\n * Get pagination params with defaults\n */\n protected getPagination(query?: EntityQuery): { page: number; limit: number; offset: number } {\n const maxLimit = query?.maxLimit ?? 100\n const page = Math.max(1, query?.page ?? 1)\n // limit=0 means unpaginated (return all items)\n const limit = query?.limit === 0 ? 0 : Math.min(maxLimit, Math.max(1, query?.limit ?? 20))\n const offset = limit === 0 ? 0 : (page - 1) * limit\n return { page, limit, offset }\n }\n\n /**\n * Apply sorting to query builder\n */\n protected applySorting(qb: Knex.QueryBuilder, query?: EntityQuery): Knex.QueryBuilder {\n if (query?.sort) {\n const order = query.order ?? 'asc'\n qb.orderBy(query.sort, order)\n } else if ('labelField' in this.definition && this.definition.labelField) {\n // Default sort by label field\n qb.orderBy(this.definition.labelField as string, 'asc')\n }\n return qb\n }\n\n /**\n * Apply search filter to query builder\n * Override in subclasses for entity-specific search\n */\n protected applySearch(qb: Knex.QueryBuilder, search: string): Knex.QueryBuilder {\n const searchTerm = `%${search}%`\n const searchableFields: string[] = []\n\n // Collect fields marked searchable\n const fields = 'fields' in this.definition ? this.definition.fields : {}\n for (const [name, field] of Object.entries(fields)) {\n if (field.meta?.searchable) {\n searchableFields.push(name)\n }\n }\n\n // Add labelField if not already in searchable\n if ('labelField' in this.definition && this.definition.labelField) {\n const lf = this.definition.labelField as string\n if (!searchableFields.includes(lf)) {\n searchableFields.unshift(lf)\n }\n }\n\n if (searchableFields.length === 0) return qb\n\n // JSON/localized fields need cast to text for LIKE\n qb.where(function () {\n for (const fieldName of searchableFields) {\n const field = fields[fieldName]\n if (field?.db?.type === 'json') {\n this.orWhereRaw(`CAST(?? AS TEXT) LIKE ?`, [fieldName, searchTerm])\n } else {\n this.orWhere(fieldName, 'like', searchTerm)\n }\n }\n })\n\n return qb\n }\n\n /**\n * Apply filters to query builder\n * Supports operators: $eq, $ne, $gt, $gte, $lt, $lte, $contains, $startswith, $endswith, $in, $nin, $isnull\n *\n * @example\n * // Simple equality\n * { status: 'active' }\n *\n * // With operators\n * { name: { $contains: 'john' } }\n * { age: { $gte: 18, $lt: 65 } }\n *\n * // Array shorthand for $in\n * { status: ['active', 'pending'] }\n */\n protected applyFilters(qb: Knex.QueryBuilder, filters: Record<string, unknown>): Knex.QueryBuilder {\n return applyFiltersHelper(qb, filters)\n }\n\n // ============================================================================\n // In-Memory Filters (for computed/virtual entities)\n // ============================================================================\n\n /**\n * Apply filters in memory (for non-persistent entities)\n * Supports same operators as Knex: $eq, $ne, $gt, $gte, $lt, $lte, $contains, $startswith, $endswith, $in, $nin, $isnull\n */\n protected applyInMemoryFilters(items: unknown[], filters: Record<string, unknown>): unknown[] {\n return applyInMemoryFiltersHelper(items, filters)\n }\n\n // ============================================================================\n // CASL Helpers\n // ============================================================================\n\n /**\n * Get sensitive fields from CASL config.\n * Only available for EntityCaslConfig, not simplified action CASL.\n */\n protected get sensitiveFields(): string[] {\n const casl = this.definition.casl\n if (!casl) return []\n return 'sensitiveFields' in casl ? (casl.sensitiveFields ?? []) : []\n }\n\n /**\n * Exclude sensitive fields from entity data.\n * Uses definition.casl.sensitiveFields if configured.\n */\n protected excludeSensitiveFields<D extends Record<string, unknown>>(data: D): Partial<D> {\n return excludeSensitive(data, this.sensitiveFields)\n }\n\n /**\n * Exclude sensitive fields from array of entities.\n */\n protected excludeSensitiveFieldsFromArray<D extends Record<string, unknown>>(items: D[]): Partial<D>[] {\n return excludeSensitiveArray(items, this.sensitiveFields)\n }\n\n // ============================================================================\n // Field Type Parsing (JSON, Boolean)\n // ============================================================================\n\n /**\n * Parse fields in an entity based on field definitions.\n * - Converts JSON strings to objects for fields with db.type = 'json'\n * - Converts numeric 0/1 to native booleans for fields with db.type = 'boolean'\n *\n * SQLite and MySQL store booleans as integers (0/1), so we need to convert them.\n */\n protected parseJsonFields<E extends Record<string, unknown>>(entity: E): E {\n if (!entity || typeof entity !== 'object') return entity\n\n const fields = 'fields' in this.definition ? (this.definition.fields ?? {}) : {}\n const result: Record<string, unknown> = { ...entity }\n\n for (const [fieldName, fieldDef] of Object.entries(fields)) {\n const value = result[fieldName]\n\n // JSON: parse string to object\n if (fieldDef.db?.type === 'json' && typeof value === 'string') {\n try {\n result[fieldName] = JSON.parse(value)\n } catch {\n this.logger.warn({ field: fieldName, entity: this.table }, 'Failed to parse JSON field')\n }\n }\n\n // Boolean: convert 0/1 to true/false (SQLite/MySQL store booleans as integers)\n if (fieldDef.db?.type === 'boolean' && typeof value === 'number') {\n result[fieldName] = value !== 0\n }\n }\n\n return result as E\n }\n\n /**\n * Parse fields in array of entities\n */\n protected parseJsonFieldsFromArray<E extends Record<string, unknown>>(items: E[]): E[] {\n return items.map(item => this.parseJsonFields(item))\n }\n\n /**\n * Serialize entity data for DB insertion/update.\n * - Converts objects/arrays to JSON strings for fields with db.type = 'json'\n * (needed for SQLite/better-sqlite3 which doesn't auto-serialize JSON)\n * - Converts booleans to 0/1 for SQLite\n */\n protected serializeForDb<E extends Record<string, unknown>>(data: E): E {\n if (!data || typeof data !== 'object') return data\n\n const fields = 'fields' in this.definition ? (this.definition.fields ?? {}) : {}\n const result: Record<string, unknown> = { ...data }\n const client = (this.db as unknown as { client: { config: { client: string } } }).client?.config?.client ?? ''\n const isPg = client === 'pg' || client === 'postgresql'\n const isSQLite = client === 'better-sqlite3' || client === 'sqlite3'\n\n for (const [fieldName, fieldDef] of Object.entries(fields)) {\n const value = result[fieldName]\n if (value === undefined) continue\n\n // JSON: stringify objects/arrays for non-PG (PG handles JSON natively, SQLite/MySQL don't)\n if (fieldDef.db?.type === 'json' && !isPg && value !== null && typeof value === 'object') {\n result[fieldName] = JSON.stringify(value)\n }\n\n // Boolean: convert to 0/1 for SQLite\n if (fieldDef.db?.type === 'boolean' && isSQLite && typeof value === 'boolean') {\n result[fieldName] = value ? 1 : 0\n }\n }\n\n return result as E\n }\n\n // ============================================================================\n // Action Execution\n // ============================================================================\n\n /**\n * Execute an action defined in this entity programmatically\n *\n * Allows invoking EntityActions from other services without HTTP context.\n * NOTE: Does NOT verify CASL permissions - caller is responsible for authorization.\n *\n * @param actionKey - Key of the action to execute\n * @param input - Input for the handler (without _record if recordId is provided)\n * @param recordId - ID of the record (optional, loads automatically)\n * @param options - Additional options\n * @returns Result from the action handler\n *\n * @example\n * // From another service\n * const storageService = ctx.services['storage_files'] as CollectionService\n * const result = await storageService.executeAction('thumbnail', { width: 200 }, fileId)\n */\n async executeAction(\n actionKey: string,\n input: Record<string, unknown> = {},\n recordId?: string,\n options?: { userId?: string; skipValidation?: boolean }\n ): Promise<unknown> {\n // 1. Find action in definition.actions\n const actions = (this.definition as { actions?: ActionDefinition[] }).actions\n if (!actions?.length) {\n throw new this.errors.NotFoundError('Entity has no actions')\n }\n\n const action = actions.find(a => a.key === actionKey)\n if (!action) {\n throw new this.errors.NotFoundError(`Action \"${actionKey}\" not found`)\n }\n\n // 2. Load record if recordId provided\n let record: Record<string, unknown> | undefined = input['_record'] as Record<string, unknown>\n if (recordId && !record) {\n let query = this.db(this.table).where('id', recordId)\n if (action.select?.length) {\n query = query.select(action.select)\n }\n record = await query.first()\n if (!record) {\n throw new this.errors.NotFoundError(`${resolveLocalized(this.definition.label, 'en')} not found`)\n }\n }\n\n // 3. Validate input if schema exists and not skipped\n let validatedInput = input\n if (action.inputSchema && !options?.skipValidation) {\n validatedInput = action.inputSchema.parse(input) as Record<string, unknown>\n }\n\n // 4. Extend input with _record and _authUserId\n const extendedInput: Record<string, unknown> = { ...validatedInput }\n if (record) {\n extendedInput['_record'] = record\n }\n if (options?.userId) {\n extendedInput['_authUserId'] = options.userId\n }\n\n // 5. Execute handler (without req/res)\n if (!action.handler) {\n throw new this.errors.AppError(`No handler defined for action ${actionKey}`, 500)\n }\n return action.handler(this.ctx, extendedInput)\n }\n}\n","/**\n * Collection Service - Full CRUD for collection entities\n *\n * Supports:\n * - Full CRUD operations (create, read, update, delete)\n * - Soft delete (optional)\n * - Timestamps (created_at, updated_at)\n * - Audit fields (created_by, updated_by)\n * - Pagination, sorting, search, filters\n */\n\nimport type {\n CollectionEntityDefinition,\n PaginatedResult,\n ModuleContext,\n EntityServiceHooks,\n SeedConfig\n} from '@gzl10/nexus-sdk'\nimport { resolveLocalized } from '@gzl10/nexus-sdk'\nimport type { EntityQuery } from '../types.js'\nimport { BaseEntityService } from './base.service.js'\nimport { formatTimestamp } from '../../db/schema-helpers.js'\nimport { loadSeedData } from '../helpers/index.js'\n\nexport class CollectionService<T extends Record<string, unknown> = Record<string, unknown>>\n extends BaseEntityService<T> {\n\n declare readonly definition: CollectionEntityDefinition\n\n constructor(ctx: ModuleContext, definition: CollectionEntityDefinition, hooks?: EntityServiceHooks<T>) {\n super(ctx, definition, hooks)\n }\n\n /**\n * Get all entities with pagination\n */\n async findAll(query?: EntityQuery): Promise<PaginatedResult<T>> {\n return this.baseFindAll(query)\n }\n\n /**\n * Apply soft delete filter\n */\n protected override applyTypeFilters(qb: import('knex').Knex.QueryBuilder): import('knex').Knex.QueryBuilder {\n if (this.definition.softDelete) {\n return qb.whereNull('deleted_at')\n }\n return qb\n }\n\n /**\n * Get entity by ID\n */\n async findById(id: string): Promise<T | null> {\n let qb = this.db(this.table).where('id', id)\n\n // Apply soft delete filter\n if (this.definition.softDelete) {\n qb = qb.whereNull('deleted_at')\n }\n\n const entity = await qb.first<T>()\n if (!entity) return null\n\n // Parse JSON fields\n const parsed = this.parseJsonFields(entity as Record<string, unknown>) as T\n return this.processAfterFindById(parsed)\n }\n\n /**\n * Create new entity\n */\n async create(data: Partial<T>): Promise<T> {\n // Resolve ID based on entity's idType configuration\n const dataRecord = data as Record<string, unknown>\n const resolvedId = await this.resolveEntityId(dataRecord['id'])\n const entityData: Record<string, unknown> = { ...data }\n if (resolvedId !== undefined) {\n entityData['id'] = resolvedId\n }\n\n // Add timestamps\n if (this.definition.timestamps) {\n const now = this.getNowTimestamp()\n entityData['created_at'] = now\n entityData['updated_at'] = now\n }\n\n // Auto-fill audit fields from _authUserId (injected by controller)\n if (this.definition.audit) {\n const authUserId = entityData['_authUserId'] as string | undefined\n if (authUserId) {\n entityData['created_by'] = authUserId\n entityData['updated_by'] = authUserId\n }\n }\n // Remove internal field before DB insert\n delete entityData['_authUserId']\n\n // Run beforeCreate hook\n const processedData = await this.beforeCreate(entityData as Partial<T>)\n\n const dbData = this.serializeForDb(processedData as Record<string, unknown>)\n const idType = this.getIdType()\n let entityId: string\n\n if (idType === 'auto') {\n // Auto-increment: get the generated ID from the insert\n const [insertedId] = await this.db(this.table).insert(dbData).returning('id')\n // Knex returns different formats: { id: X } for PG, number for SQLite/MySQL\n entityId = String(typeof insertedId === 'object' ? insertedId.id : insertedId)\n } else {\n await this.db(this.table).insert(dbData)\n entityId = (dbData as Record<string, unknown>)['id'] as string\n }\n\n // Fetch created entity\n const entity = await this.findById(entityId)\n if (!entity) {\n throw new this.errors.AppError(`${resolveLocalized(this.definition.label, 'en')} not found after create`, 404)\n }\n\n // Run afterCreate hook\n await this.afterCreate(entity)\n\n // Run retention cleanup asynchronously (absorbed from EventService)\n if (this.definition.retention) {\n this.runRetentionCleanup().catch((err: unknown) => {\n const error = err instanceof Error ? err : new Error('Retention cleanup failed')\n this.logger.error({ err }, 'Retention cleanup failed')\n this.loggerService?.captureException(error, { table: this.table, type: 'collection', action: 'retention' })\n })\n }\n\n return entity\n }\n\n /**\n * Update entity\n */\n async update(id: string, data: Partial<T>): Promise<T> {\n // Immutable entities cannot be updated (absorbed from EventService)\n if (this.definition.immutable) {\n throw new this.errors.ForbiddenError('Immutable entities cannot be updated')\n }\n\n // Check entity exists\n const existing = await this.findById(id)\n if (!existing) {\n throw new this.errors.NotFoundError(resolveLocalized(this.definition.label, 'en'))\n }\n\n const updateData = { ...data } as Record<string, unknown>\n\n // Update timestamp\n if (this.definition.timestamps) {\n updateData['updated_at'] = this.getNowTimestamp()\n }\n\n // Auto-fill audit fields\n if (this.definition.audit) {\n const authUserId = updateData['_authUserId'] as string | undefined\n if (authUserId) {\n updateData['updated_by'] = authUserId\n }\n }\n // Remove internal field before DB update\n delete updateData['_authUserId']\n\n // Run beforeUpdate hook\n const processedData = await this.beforeUpdate(id, updateData as Partial<T>)\n\n const dbData = this.serializeForDb(processedData as Record<string, unknown>)\n await this.db(this.table).where('id', id).update(dbData)\n\n // Fetch updated entity\n const entity = await this.findById(id)\n if (!entity) {\n throw new this.errors.AppError(`${resolveLocalized(this.definition.label, 'en')} not found after update`, 404)\n }\n\n // Run afterUpdate hook\n await this.afterUpdate(entity)\n\n return entity\n }\n\n /**\n * Delete entity (soft delete if enabled)\n */\n async delete(id: string, options?: { actorId?: string }): Promise<void> {\n // Immutable entities cannot be deleted (absorbed from EventService)\n if (this.definition.immutable) {\n throw new this.errors.ForbiddenError('Immutable entities cannot be deleted. Use retention policy.')\n }\n\n // Check entity exists\n const existing = await this.findById(id)\n if (!existing) {\n throw new this.errors.NotFoundError(resolveLocalized(this.definition.label, 'en'))\n }\n\n // Run beforeDelete hook\n await this.beforeDelete(id)\n\n // Store actorId for audit event in afterDelete\n this._deleteActorId = options?.actorId\n\n if (this.definition.softDelete) {\n // Soft delete\n await this.db(this.table).where('id', id).update({\n deleted_at: this.getNowTimestamp()\n })\n } else {\n // Hard delete\n await this.db(this.table).where('id', id).delete()\n }\n\n // Run afterDelete hook\n await this.afterDelete(id)\n this._deleteActorId = undefined\n }\n\n /**\n * Restore soft-deleted entity.\n */\n async restore(id: string): Promise<T> {\n if (!this.definition.softDelete) {\n throw new this.errors.AppError('Soft delete not enabled for this entity', 400)\n }\n\n // Find including deleted\n const entity = await this.db(this.table).where('id', id).first<T>()\n if (!entity) {\n throw new this.errors.NotFoundError(resolveLocalized(this.definition.label, 'en'))\n }\n\n // Restore\n await this.db(this.table).where('id', id).update({\n deleted_at: null,\n updated_at: this.getNowTimestamp()\n })\n\n const restored = await this.findById(id)\n if (!restored) {\n throw new this.errors.AppError(`${resolveLocalized(this.definition.label, 'en')} not found after restore`, 404)\n }\n\n return restored\n }\n\n /**\n * Find multiple entities by IDs\n */\n async findByIds(ids: string[]): Promise<T[]> {\n if (ids.length === 0) return []\n\n let qb = this.db(this.table).whereIn('id', ids)\n\n if (this.definition.softDelete) {\n qb = qb.whereNull('deleted_at')\n }\n\n const rawItems = await qb as T[]\n return this.parseJsonFieldsFromArray(rawItems as Record<string, unknown>[]) as T[]\n }\n\n /**\n * Count entities matching filters (with soft delete and adapter support)\n */\n override async count(filters?: Record<string, unknown>): Promise<number> {\n let qb = this.db(this.table)\n\n if (this.definition.softDelete) {\n qb = qb.whereNull('deleted_at')\n }\n\n if (filters) {\n qb = this.applyFilters(qb, filters)\n }\n\n const result = await qb.count('* as count').first<{ count: string | number }>()\n return Number(result?.count ?? 0)\n }\n\n /**\n * Check if entity exists\n */\n async exists(id: string): Promise<boolean> {\n const entity = await this.findById(id)\n return entity !== null\n }\n\n /**\n * Seed initial data from definition.seed.\n * Supports inline arrays and SeedConfig objects.\n * Idempotent: only inserts records whose ID doesn't exist yet.\n */\n async seed(): Promise<number> {\n const seedDef = this.definition as CollectionEntityDefinition & { seed?: Array<Record<string, unknown>> | SeedConfig }\n const seedData = await loadSeedData(seedDef.seed, this.logger as unknown as import('pino').Logger, 'collection')\n if (seedData.length === 0) {\n return 0\n }\n\n let seeded = 0\n\n for (const item of seedData) {\n const id = item['id'] as string | undefined\n if (!id) continue\n\n const exists = await this.db(this.table).where('id', id).first()\n if (!exists) {\n const data = { ...item, id } as Record<string, unknown>\n\n if (this.definition.timestamps) {\n const now = this.getNowTimestamp()\n data['created_at'] = now\n data['updated_at'] = now\n }\n\n const dbData = this.serializeForDb(data)\n await this.db(this.table).insert(dbData)\n seeded++\n }\n }\n\n const log = seeded > 0 ? this.logger.info.bind(this.logger) : this.logger.debug.bind(this.logger)\n log({ table: this.table, seeded }, 'Seed data applied')\n return seeded\n }\n\n /**\n * Run retention cleanup based on definition (absorbed from EventService).\n * Deletes old records by age and/or caps total rows.\n */\n async runRetentionCleanup(): Promise<number> {\n const retention = this.definition.retention\n if (!retention) return 0\n\n let deleted = 0\n\n // Delete by age (seconds for testing, days for production)\n if (retention.seconds || retention.days) {\n const cutoff = new Date()\n if (retention.seconds) {\n cutoff.setSeconds(cutoff.getSeconds() - retention.seconds)\n } else if (retention.days) {\n cutoff.setDate(cutoff.getDate() - retention.days)\n }\n\n const result = await this.db(this.table)\n .where('created_at', '<', formatTimestamp(this.db, cutoff))\n .delete()\n\n deleted += result\n }\n\n // Delete by max rows (keep newest)\n if (retention.maxRows) {\n const countResult = await this.db(this.table)\n .count('* as count')\n .first<{ count: string | number }>()\n\n const total = Number(countResult?.count ?? 0)\n const toDelete = total - retention.maxRows\n\n if (toDelete > 0) {\n const oldestIds = await this.db(this.table)\n .select('id')\n .orderBy('created_at', 'asc')\n .limit(toDelete)\n\n const ids = oldestIds.map((r: Record<string, unknown>) => r['id'] as string)\n const result = await this.db(this.table)\n .whereIn('id', ids)\n .delete()\n\n deleted += result\n }\n }\n\n if (deleted > 0) {\n this.logger.info({ table: this.table, deleted }, 'Retention cleanup completed')\n }\n\n return deleted\n }\n}\n","/**\n * Single Service - Singleton entities stored in single_records\n *\n * Supports:\n * - Read single config\n * - Update config\n * - Defaults on first read\n * - Scoped mode (absorbed from ConfigService): multiple records keyed by scope\n *\n * Uses single_records table with key-value storage:\n * { key: 'site_config', value: { siteName: '...', logo: '...' } }\n *\n * Scoped mode (when scopeField is set):\n * { key: 'mail_config:default', value: { host: '...', port: 587 } }\n * { key: 'mail_config:tenant-123', value: { host: '...', port: 465 } }\n */\n\nimport type {\n SingleEntityDefinition,\n PaginatedResult,\n ModuleContext,\n EntityServiceHooks\n} from '@gzl10/nexus-sdk'\nimport type { EntityQuery } from '../types.js'\nimport { BaseEntityService } from './base.service.js'\n\nconst SINGLE_RECORDS_TABLE = 'single_records'\n\nexport class SingleService<T extends Record<string, unknown> = Record<string, unknown>>\n extends BaseEntityService<T> {\n\n declare readonly definition: SingleEntityDefinition\n\n constructor(ctx: ModuleContext, definition: SingleEntityDefinition, hooks?: EntityServiceHooks<T>) {\n super(ctx, definition, hooks)\n }\n\n /**\n * Get table - overridden since single entities use single_records\n */\n protected override get table(): string {\n return SINGLE_RECORDS_TABLE\n }\n\n /**\n * Get the setting key\n */\n private get key(): string {\n return this.definition.key\n }\n\n /**\n * Whether this entity operates in scoped mode (multiple records)\n */\n private get isScoped(): boolean {\n return !!this.definition.scopeField\n }\n\n /**\n * Build the full key for a scope value\n */\n private scopedKey(scopeValue: string): string {\n return `${this.key}:${scopeValue}`\n }\n\n // ── findAll ──────────────────────────────────────────────────────────\n\n /**\n * Get all - returns single item in paginated format (singleton mode)\n * or all scoped records (scoped mode)\n */\n async findAll(_query?: EntityQuery): Promise<PaginatedResult<T>> {\n if (this.isScoped) {\n return this.findAllScoped()\n }\n\n const value = await this.findById(this.key)\n\n return {\n items: value ? [value] : [],\n total: value ? 1 : 0,\n page: 1,\n limit: 1,\n totalPages: value ? 1 : 0,\n hasNext: false\n }\n }\n\n /**\n * Get all scoped records\n */\n private async findAllScoped(): Promise<PaginatedResult<T>> {\n const prefix = `${this.key}:`\n const rows = await this.db(SINGLE_RECORDS_TABLE)\n .where('key', 'like', `${prefix}%`)\n .orderBy('created_at', 'asc') as Array<{ id: string; key: string; value: string; created_at: string; updated_at: string }>\n\n const items: T[] = []\n for (const row of rows) {\n try {\n const value = JSON.parse(row.value)\n const scopeValue = row.key.slice(prefix.length)\n const merged = this.mergeWithDefaults({\n ...value,\n id: row.id,\n [this.definition.scopeField!]: scopeValue\n })\n items.push(merged as T)\n } catch {\n this.logger.warn({ key: row.key }, 'Failed to parse scoped setting value')\n }\n }\n\n return {\n items,\n total: items.length,\n page: 1,\n limit: items.length || 1,\n totalPages: 1,\n hasNext: false\n }\n }\n\n // ── findById ─────────────────────────────────────────────────────────\n\n /**\n * Get the singleton value (singleton mode)\n * or get a scoped record by ID (scoped mode)\n */\n async findById(id: string): Promise<T | null> {\n if (this.isScoped) {\n return this.findByIdScoped(id)\n }\n\n const row = await this.db(SINGLE_RECORDS_TABLE)\n .where('key', this.key)\n .first<{ key: string; value: string }>()\n\n if (!row) {\n if (this.definition.defaults) {\n return this.definition.defaults as T\n }\n return null\n }\n\n try {\n const value = JSON.parse(row.value)\n const result = this.mergeWithDefaults(value)\n return this.processAfterFindById(result as T)\n } catch {\n this.logger.warn({ key: this.key }, 'Failed to parse setting value')\n return this.definition.defaults as T ?? null\n }\n }\n\n /**\n * Find a scoped record by its DB id\n */\n private async findByIdScoped(id: string): Promise<T | null> {\n const row = await this.db(SINGLE_RECORDS_TABLE)\n .where('id', id)\n .first<{ id: string; key: string; value: string }>()\n\n if (!row || !row.key.startsWith(`${this.key}:`)) {\n return null\n }\n\n try {\n const value = JSON.parse(row.value)\n const scopeValue = row.key.slice(`${this.key}:`.length)\n const merged = this.mergeWithDefaults({\n ...value,\n id: row.id,\n [this.definition.scopeField!]: scopeValue\n })\n return this.processAfterFindById(merged as T)\n } catch {\n this.logger.warn({ id, key: this.key }, 'Failed to parse scoped setting value')\n return null\n }\n }\n\n /**\n * Get the setting - alias for findById\n */\n async get(): Promise<T | null> {\n return this.findById(this.key)\n }\n\n // ── Scoped methods (absorbed from ConfigService) ─────────────────────\n\n /**\n * Get config by scope value\n */\n async findByScope(scopeValue: string): Promise<T | null> {\n const row = await this.db(SINGLE_RECORDS_TABLE)\n .where('key', this.scopedKey(scopeValue))\n .first<{ id: string; key: string; value: string }>()\n\n if (!row) {\n if (this.definition.defaults) {\n return this.definition.defaults as T\n }\n return null\n }\n\n try {\n const value = JSON.parse(row.value)\n return this.mergeWithDefaults({\n ...value,\n id: row.id,\n [this.definition.scopeField!]: scopeValue\n }) as T\n } catch {\n this.logger.warn({ key: this.scopedKey(scopeValue) }, 'Failed to parse scoped setting value')\n return this.definition.defaults as T ?? null\n }\n }\n\n /**\n * Update or create config by scope\n */\n async upsertByScope(scopeValue: string, data: Partial<T>): Promise<T> {\n const fullKey = this.scopedKey(scopeValue)\n const existing = await this.db(SINGLE_RECORDS_TABLE)\n .where('key', fullKey)\n .first<{ id: string; key: string; value: string }>()\n\n const now = this.getNowTimestamp()\n\n if (existing) {\n // Merge with current value\n let currentValue: Record<string, unknown> = {}\n try {\n currentValue = JSON.parse(existing.value)\n } catch { /* empty */ }\n\n const newValue = { ...currentValue, ...data }\n // Remove internal fields\n delete (newValue as Record<string, unknown>)[this.definition.scopeField!]\n delete (newValue as Record<string, unknown>)['id']\n\n const processedData = await this.beforeUpdate(existing.id, newValue as Partial<T>)\n\n await this.db(SINGLE_RECORDS_TABLE)\n .where('key', fullKey)\n .update({\n value: JSON.stringify(processedData),\n updated_at: now\n })\n\n const result = await this.findByScope(scopeValue)\n if (result) await this.afterUpdate(result)\n return result!\n } else {\n // Create new scoped record\n const entityData = { ...this.definition.defaults, ...data }\n // Remove scope field and id from stored value\n delete (entityData as Record<string, unknown>)[this.definition.scopeField!]\n delete (entityData as Record<string, unknown>)['id']\n\n const processedData = await this.beforeCreate(entityData as Partial<T>)\n const id = this.generateId()\n\n await this.db(SINGLE_RECORDS_TABLE).insert({\n id,\n key: fullKey,\n value: JSON.stringify(processedData),\n created_at: now,\n updated_at: now\n })\n\n const result = await this.findByScope(scopeValue)\n if (result) await this.afterCreate(result)\n return result!\n }\n }\n\n /**\n * Set a scoped config as the default\n */\n async setAsDefault(scopeValue: string): Promise<void> {\n const metaKey = `${this.key}:__default`\n const now = this.getNowTimestamp()\n const existing = await this.db(SINGLE_RECORDS_TABLE)\n .where('key', metaKey)\n .first()\n\n if (existing) {\n await this.db(SINGLE_RECORDS_TABLE)\n .where('key', metaKey)\n .update({ value: JSON.stringify(scopeValue), updated_at: now })\n } else {\n await this.db(SINGLE_RECORDS_TABLE).insert({\n id: this.generateId(),\n key: metaKey,\n value: JSON.stringify(scopeValue),\n created_at: now,\n updated_at: now\n })\n }\n }\n\n /**\n * Get the default scoped config\n */\n async getDefault(): Promise<T | null> {\n const metaKey = `${this.key}:__default`\n const meta = await this.db(SINGLE_RECORDS_TABLE)\n .where('key', metaKey)\n .first<{ value: string }>()\n\n if (meta) {\n try {\n const defaultScope = JSON.parse(meta.value)\n return this.findByScope(defaultScope)\n } catch { /* fall through */ }\n }\n\n // Fallback: return first scoped record\n const prefix = `${this.key}:`\n const first = await this.db(SINGLE_RECORDS_TABLE)\n .where('key', 'like', `${prefix}%`)\n .whereNot('key', metaKey)\n .orderBy('created_at', 'asc')\n .first<{ id: string; key: string; value: string }>()\n\n if (first) {\n const scopeValue = first.key.slice(prefix.length)\n return this.findByScope(scopeValue)\n }\n\n // No records: return defaults\n if (this.definition.defaults) {\n return this.definition.defaults as T\n }\n\n return null\n }\n\n // ── update ───────────────────────────────────────────────────────────\n\n /**\n * Update the singleton value (singleton mode)\n * or update a scoped record by ID (scoped mode)\n */\n async update(id: string, data: Partial<T>): Promise<T> {\n if (this.isScoped) {\n return this.updateScoped(id, data)\n }\n\n // Get current value\n const current = await this.get() ?? {}\n\n // Merge with new data\n const newValue = { ...current, ...data }\n\n // Run beforeUpdate hook\n const processedData = await this.beforeUpdate(this.key, newValue as Partial<T>)\n\n // Check if exists\n const exists = await this.db(SINGLE_RECORDS_TABLE)\n .where('key', this.key)\n .first()\n\n const now = this.getNowTimestamp()\n\n if (exists) {\n // Update\n await this.db(SINGLE_RECORDS_TABLE)\n .where('key', this.key)\n .update({\n value: JSON.stringify(processedData),\n updated_at: now\n })\n } else {\n // Insert\n await this.db(SINGLE_RECORDS_TABLE).insert({\n id: this.generateId(),\n key: this.key,\n value: JSON.stringify(processedData),\n created_at: now,\n updated_at: now\n })\n }\n\n const result = await this.get()\n if (!result) {\n throw new this.errors.AppError('Failed to save setting', 500)\n }\n\n // Run afterUpdate hook\n await this.afterUpdate(result)\n\n return result\n }\n\n /**\n * Update a scoped record by its DB id\n */\n private async updateScoped(id: string, data: Partial<T>): Promise<T> {\n const row = await this.db(SINGLE_RECORDS_TABLE)\n .where('id', id)\n .first<{ id: string; key: string; value: string }>()\n\n if (!row || !row.key.startsWith(`${this.key}:`)) {\n throw new this.errors.NotFoundError('Config not found')\n }\n\n let currentValue: Record<string, unknown> = {}\n try {\n currentValue = JSON.parse(row.value)\n } catch { /* empty */ }\n\n const newValue = { ...currentValue, ...data }\n // Remove internal fields from stored value\n delete (newValue as Record<string, unknown>)[this.definition.scopeField!]\n delete (newValue as Record<string, unknown>)['id']\n\n const processedData = await this.beforeUpdate(id, newValue as Partial<T>)\n const now = this.getNowTimestamp()\n\n await this.db(SINGLE_RECORDS_TABLE)\n .where('id', id)\n .update({\n value: JSON.stringify(processedData),\n updated_at: now\n })\n\n const scopeValue = row.key.slice(`${this.key}:`.length)\n const result = await this.findByScope(scopeValue)\n if (!result) {\n throw new this.errors.AppError('Config not found after update', 404)\n }\n\n await this.afterUpdate(result)\n return result\n }\n\n /**\n * Set the setting - alias for update\n */\n async set(data: Partial<T>): Promise<T> {\n return this.update(this.key, data)\n }\n\n /**\n * Reset to defaults\n */\n async reset(): Promise<T | null> {\n if (!this.definition.defaults) {\n // Delete if no defaults\n await this.db(SINGLE_RECORDS_TABLE)\n .where('key', this.key)\n .delete()\n return null\n }\n\n // Set to defaults\n return this.update(this.key, this.definition.defaults as Partial<T>)\n }\n\n /**\n * Seed defaults into the database if record doesn't exist.\n * This ensures endpoints return persisted data instead of in-memory defaults.\n *\n * @returns 1 if seeded, 0 if already exists or no defaults\n */\n async seed(): Promise<number> {\n // No defaults to seed\n if (!this.definition.defaults) {\n return 0\n }\n\n // Check if already exists\n const exists = await this.db(SINGLE_RECORDS_TABLE)\n .where('key', this.key)\n .first()\n\n if (exists) {\n return 0\n }\n\n // Insert defaults\n const now = this.getNowTimestamp()\n await this.db(SINGLE_RECORDS_TABLE).insert({\n id: this.generateId(),\n key: this.key,\n value: JSON.stringify(this.definition.defaults),\n created_at: now,\n updated_at: now\n })\n\n this.logger.info({ key: this.key }, 'Single entity defaults seeded')\n return 1\n }\n\n // ── Helpers ──────────────────────────────────────────────────────────\n\n /**\n * Merge entity with defaults\n */\n private mergeWithDefaults(entity: Record<string, unknown>): Record<string, unknown> {\n if (!this.definition.defaults) {\n return entity\n }\n return { ...this.definition.defaults, ...entity }\n }\n}\n","/**\n * View Service - Read-only views with custom queries\n *\n * Supports:\n * - Read all (paginated)\n * - Read by ID\n * - Custom query builder or SQL VIEW\n */\n\nimport type {\n ViewEntityDefinition,\n PaginatedResult,\n ModuleContext,\n EntityServiceHooks\n} from '@gzl10/nexus-sdk'\nimport { entityRoom } from '@gzl10/nexus-sdk'\nimport type { EntityQuery } from '../types.js'\nimport { BaseEntityService } from './base.service.js'\n\nexport class ViewService<T extends Record<string, unknown> = Record<string, unknown>>\n extends BaseEntityService<T> {\n\n declare readonly definition: ViewEntityDefinition\n\n constructor(ctx: ModuleContext, definition: ViewEntityDefinition, hooks?: EntityServiceHooks<T>) {\n super(ctx, definition, hooks)\n\n // Register live mode observer: push updates when source tables change\n if (definition.realtime === 'live' && definition.liveOn?.length) {\n this.registerLiveObserver(definition.liveOn)\n }\n }\n\n /**\n * Register observer for live mode: when source tables emit db.* events,\n * push entity.updated to subscribed clients.\n */\n private registerLiveObserver(tables: string[]): void {\n import('../../core/events-hub/socket.js').then(({ getRoomSize, isSocketIOInitialized }) => {\n const handler = async () => {\n if (!isSocketIOInitialized()) return\n\n const room = entityRoom(this.ctx.tenantId, this.moduleName, this.entityKey)\n const roomSize = await getRoomSize(room)\n if (roomSize === 0) return\n\n this.logger.debug({ entity: this.definition.label, room, roomSize }, 'View live push triggered')\n\n this.ctx.core.events.emit('entity.updated', {\n tenantId: this.ctx.tenantId,\n module: this.moduleName,\n entity: this.entityKey,\n action: 'updated' as const,\n id: '*',\n data: undefined,\n timestamp: new Date().toISOString()\n })\n }\n\n // Listen to db.{table}.* events for each source table\n for (const table of tables) {\n const events = [`db.${table}.created`, `db.${table}.updated`, `db.${table}.deleted`]\n let timer: ReturnType<typeof setTimeout> | null = null\n for (const event of events) {\n ;(this.ctx.core.events as unknown as { on: (e: string, fn: () => void) => void }).on(event, () => {\n if (timer) clearTimeout(timer)\n timer = setTimeout(handler, 500)\n })\n }\n }\n })\n }\n\n /**\n * Get all items with pagination\n */\n async findAll(query?: EntityQuery): Promise<PaginatedResult<T>> {\n const { page, limit, offset } = this.getPagination(query)\n\n // For views, we need to handle counting differently\n if (this.definition.query && typeof this.definition.query !== 'string') {\n // Use query builder\n let baseQuery = this.definition.query(this.db)\n\n // Apply search\n if (query?.search && this.definition.labelField) {\n baseQuery = baseQuery.where(this.definition.labelField, 'like', `%${query.search}%`)\n }\n\n // Apply filters (reuse applyFilters for full operator support)\n if (query?.filters) {\n baseQuery = this.applyFilters(baseQuery.clone(), query.filters)\n }\n\n // Get count (clone baseQuery which already has search/filters applied)\n const countResult = await baseQuery.clone().count('* as count').first<{ count: string | number }>()\n const total = Number(countResult?.count ?? 0)\n\n // Apply sorting\n if (query?.sort) {\n baseQuery = baseQuery.orderBy(query.sort, query.order ?? 'asc')\n } else if (this.definition.labelField) {\n baseQuery = baseQuery.orderBy(this.definition.labelField, 'asc')\n }\n\n // Apply pagination\n baseQuery = baseQuery.limit(limit).offset(offset)\n\n const items = await baseQuery as T[]\n\n return this.buildPaginatedResult(items, total, page, limit)\n } else {\n // Use table (SQL VIEW or regular table)\n let qb = this.db(this.table)\n\n // Apply search\n if (query?.search) {\n qb = this.applySearch(qb.clone(), query.search)\n }\n\n // Apply filters\n if (query?.filters) {\n qb = this.applyFilters(qb.clone(), query.filters)\n }\n\n // Get count\n const countResult = await qb.clone().count('* as count').first<{ count: string | number }>()\n const total = Number(countResult?.count ?? 0)\n\n // Apply sorting\n qb = this.applySorting(qb, query)\n qb = qb.limit(limit).offset(offset)\n\n const items = await qb as T[]\n\n return this.buildPaginatedResult(items, total, page, limit)\n }\n }\n\n /**\n * Get item by ID\n */\n async findById(id: string): Promise<T | null> {\n if (this.definition.query && typeof this.definition.query !== 'string') {\n // Use query builder\n const entity = await this.definition.query(this.db)\n .where('id', id)\n .first<T>()\n\n return this.processAfterFindById(entity ?? null)\n } else {\n // Use table\n const entity = await this.db(this.table).where('id', id).first<T>()\n return this.processAfterFindById(entity ?? null)\n }\n }\n\n /**\n * Create SQL VIEW from definition (for migrations)\n */\n async createView(): Promise<void> {\n const query = this.definition.query\n\n if (!query) {\n this.logger.warn({ table: this.table }, 'No query defined for view')\n return\n }\n\n let sql: string\n\n if (typeof query === 'string') {\n sql = query\n } else {\n // Build query and convert to SQL\n const qb = query(this.db)\n sql = qb.toQuery()\n }\n\n // Create or replace view\n // Note: SQL is either a static string from entity definition (developer-authored, trusted)\n // or built via Knex query builder (parameterized). Never from user input.\n await this.db.raw(`CREATE OR REPLACE VIEW ?? AS ${sql}`, [this.table])\n\n this.logger.info({ table: this.table }, 'View created')\n }\n\n /**\n * Drop SQL VIEW\n */\n async dropView(): Promise<void> {\n await this.db.raw('DROP VIEW IF EXISTS ??', [this.table])\n this.logger.info({ table: this.table }, 'View dropped')\n }\n\n /**\n * Views are read-only\n */\n async create(_data: Partial<T>): Promise<T> {\n throw new this.errors.ForbiddenError('Views are read-only')\n }\n\n async update(_id: string, _data: Partial<T>): Promise<T> {\n throw new this.errors.ForbiddenError('Views are read-only')\n }\n\n async delete(_id: string): Promise<void> {\n throw new this.errors.ForbiddenError('Views are read-only')\n }\n}\n","/**\n * Computed Service - On-demand calculations with optional caching\n *\n * Supports:\n * - Read all (computed on-demand)\n * - Read by ID\n * - Source aggregation with resolver (absorbed from VirtualService)\n * - LRU caching with TTL\n * - Reactive invalidation via events\n */\n\nimport type {\n ComputedEntityDefinition,\n PaginatedResult,\n ModuleContext,\n EntityServiceHooks,\n ManagedCache\n} from '@gzl10/nexus-sdk'\nimport { resolveLocalized, entityRoom } from '@gzl10/nexus-sdk'\nimport type { EntityQuery, EntityService } from '../types.js'\nimport { BaseEntityService } from './base.service.js'\n\nexport class ComputedService<T extends Record<string, unknown> = Record<string, unknown>>\n extends BaseEntityService<T> {\n\n declare readonly definition: ComputedEntityDefinition\n private cache: ManagedCache<unknown[]>\n\n constructor(ctx: ModuleContext, definition: ComputedEntityDefinition, hooks?: EntityServiceHooks<T>) {\n super(ctx, definition, hooks)\n\n // Shared cache across all computed entities, with per-entity invalidation rules\n this.cache = ctx.core.cache.getOrCreate<unknown[]>('runtime:computed', {\n maxEntries: 200,\n defaultTTL: definition.cache?.ttl ?? 60\n })\n\n if (definition.cache?.invalidateOn?.length) {\n this.cache.addInvalidationRules(definition.cache.invalidateOn)\n }\n\n // Register live mode observer: recompute and push when sources change\n if (definition.realtime === 'live' && definition.cache?.invalidateOn?.length) {\n this.registerLiveObserver(definition.cache.invalidateOn)\n }\n\n // Warm cache on startup if configured\n if (definition.warmCache) {\n this.compute().catch(err => {\n this.logger.warn({ err, entity: resolveLocalized(this.definition.label, 'en') }, 'Cache warm failed')\n })\n }\n }\n\n /**\n * Register observer for live mode: when invalidation events fire,\n * recompute and push entity.updated to subscribed clients.\n */\n private registerLiveObserver(events: string[]): void {\n // Lazy import to avoid circular deps (socket.ts imports from core)\n import('../../core/events-hub/socket.js').then(({ getRoomSize, isSocketIOInitialized }) => {\n const handler = async () => {\n if (!isSocketIOInitialized()) return\n\n const room = entityRoom(this.ctx.tenantId, this.moduleName, this.entityKey)\n const roomSize = await getRoomSize(room)\n if (roomSize === 0) return\n\n this.logger.debug({ entity: this.definition.label, room, roomSize }, 'Live recompute triggered')\n\n try {\n await this.clearCache()\n\n this.ctx.core.events.emit('entity.updated', {\n tenantId: this.ctx.tenantId,\n module: this.moduleName,\n entity: this.entityKey,\n action: 'updated' as const,\n id: '*',\n data: undefined,\n timestamp: new Date().toISOString()\n })\n } catch (err) {\n this.logger.error({ err, entity: this.definition.label }, 'Live recompute failed')\n }\n }\n\n // Debounced listener for each invalidation event\n for (const event of events) {\n let timer: ReturnType<typeof setTimeout> | null = null\n ;(this.ctx.core.events as unknown as { on: (e: string, fn: () => void) => void }).on(event, () => {\n if (timer) clearTimeout(timer)\n timer = setTimeout(handler, 500)\n })\n }\n })\n }\n\n /**\n * Get table - computed entities don't have a local table\n */\n protected override get table(): string {\n throw new Error('Computed entities do not have a local table')\n }\n\n /**\n * Get cache key\n */\n private getCacheKey(params?: Record<string, unknown>): string {\n const base = `computed:${this.moduleName}.${resolveLocalized(this.definition.label, 'en')}`\n if (!params || Object.keys(params).length === 0) {\n return base\n }\n return `${base}:${JSON.stringify(params)}`\n }\n\n /**\n * Fetch data from source services and apply resolver (absorbed from VirtualService)\n */\n private async computeFromSources(): Promise<unknown[]> {\n const sources = this.definition.sources!\n const data: Record<string, unknown[]> = {}\n\n await Promise.all(\n sources.map(async (sourceName) => {\n const service = this.ctx.services.getOptional<EntityService>(sourceName)\n if (service) {\n try {\n const result = await service.findAll()\n data[sourceName] = result.items\n } catch (err) {\n this.logger.error({ source: sourceName, err }, 'Failed to fetch source data')\n data[sourceName] = []\n }\n } else {\n this.logger.warn({ source: sourceName }, 'Source service not found')\n data[sourceName] = []\n }\n })\n )\n\n if (this.definition.resolver) {\n const resolved = this.definition.resolver(data, this.ctx)\n return resolved instanceof Promise ? await resolved : resolved\n }\n\n // Default: flat merge all sources\n return Object.values(data).flat()\n }\n\n /**\n * Execute compute function or source aggregation\n */\n private async compute(params?: Record<string, unknown>): Promise<unknown[]> {\n // Check cache first (applies to both compute and sources modes)\n const cacheKey = this.getCacheKey(params)\n const cached = await this.cache.get(cacheKey)\n if (cached) {\n this.ctx.core.logger.debug({ entity: this.definition.label }, 'Computed entity cache hit')\n return cached\n }\n\n let result: unknown[]\n\n // Source-based computation (absorbed from virtual entity type)\n if (this.definition.sources?.length) {\n result = await this.computeFromSources()\n } else if (this.definition.compute) {\n // Function-based computation (original behavior)\n this.ctx.core.logger.debug({ entity: this.definition.label }, 'Computing entity data')\n try {\n const computed = await this.definition.compute(this.ctx, params)\n\n if (!Array.isArray(computed)) {\n this.ctx.core.logger.error(\n { entity: this.definition.label, result: typeof computed },\n 'Compute function did not return an array'\n )\n return []\n }\n\n result = computed\n } catch (error) {\n this.ctx.core.logger.error(\n { entity: this.definition.label, error },\n 'Failed to compute entity data'\n )\n throw error\n }\n } else {\n throw new this.errors.AppError(\n `No compute function or sources defined for ${resolveLocalized(this.definition.label, 'en')}`,\n 500\n )\n }\n\n this.ctx.core.logger.info(\n { entity: this.definition.label, count: result.length },\n 'Computed entity data'\n )\n\n // Cache result with entity-specific TTL\n const ttl = this.definition.cache?.ttl\n if (ttl && ttl > 0) {\n await this.cache.set(cacheKey, result, ttl)\n }\n\n return result\n }\n\n /**\n * Get all computed items with pagination\n */\n async findAll(query?: EntityQuery): Promise<PaginatedResult<T>> {\n // Compute data - pass filters so compute function can optimize if needed\n const items = await this.compute(query?.filters)\n\n // Apply in-memory filters, search, sort, and pagination\n return this.inMemoryFindAll(items, query)\n }\n\n // applyInMemoryFilters inherited from BaseEntityService with full operator support\n\n /**\n * Get computed item by ID\n */\n async findById(id: string): Promise<T | null> {\n // For source-based entities, search in sources directly (more efficient)\n if (this.definition.sources?.length && !this.definition.compute) {\n for (const sourceName of this.definition.sources) {\n const service = this.ctx.services.getOptional<EntityService>(sourceName)\n if (service) {\n try {\n const item = await service.findById(id)\n if (item) return this.processAfterFindById(item as T)\n } catch { /* continue to next source */ }\n }\n }\n return null\n }\n\n // Original: search in computed results\n const items = await this.compute()\n const item = items.find(i => (i as Record<string, unknown>)['id'] === id)\n return this.processAfterFindById((item as T) ?? null)\n }\n\n /**\n * Recompute with specific params\n */\n async recompute(params?: Record<string, unknown>): Promise<T[]> {\n // Clear cache for this computation\n const cacheKey = this.getCacheKey(params)\n await this.cache.delete(cacheKey)\n\n return this.compute(params) as Promise<T[]>\n }\n\n /**\n * Clear all cache for this entity\n */\n async clearCache(): Promise<void> {\n const prefix = `computed:${this.moduleName}.${resolveLocalized(this.definition.label, 'en')}`\n await this.cache.deleteByPrefix(prefix)\n }\n\n /**\n * Get cache statistics\n */\n getCacheStats() {\n return this.cache.getStats()\n }\n\n /**\n * Computed entities are read-only\n */\n async create(_data: Partial<T>): Promise<T> {\n throw new this.errors.ForbiddenError('Computed entities are read-only')\n }\n\n async update(_id: string, _data: Partial<T>): Promise<T> {\n throw new this.errors.ForbiddenError('Computed entities are read-only')\n }\n\n async delete(_id: string): Promise<void> {\n throw new this.errors.ForbiddenError('Computed entities are read-only')\n }\n}\n","/**\n * External Service - Data from external APIs via adapters\n *\n * Supports:\n * - Read all (paginated)\n * - Read by ID\n * - LRU caching with TTL\n * - Reactive invalidation via events\n * - CRUD via adapter (if supported)\n *\n * Uses DatabaseAdapter from SDK (unified adapter interface).\n */\n\nimport type {\n ExternalEntityDefinition,\n PaginatedResult,\n ModuleContext,\n EntityServiceHooks,\n DatabaseAdapter,\n ManagedCache\n} from '@gzl10/nexus-sdk'\nimport { entityRoom } from '@gzl10/nexus-sdk'\nimport type { EntityQuery } from '../types.js'\nimport { BaseEntityService } from './base.service.js'\n\nexport class ExternalService<T extends Record<string, unknown> = Record<string, unknown>>\n extends BaseEntityService<T> {\n\n declare readonly definition: ExternalEntityDefinition\n private adapter: DatabaseAdapter | undefined\n private cache: ManagedCache<unknown>\n\n constructor(ctx: ModuleContext, definition: ExternalEntityDefinition, hooks?: EntityServiceHooks<T>) {\n super(ctx, definition, hooks)\n\n // Get adapter from context registry (unified system)\n if (ctx.adapters.has(definition.adapter)) {\n this.adapter = ctx.adapters.get(definition.adapter)\n }\n\n // Shared cache across all external entities, with per-entity invalidation rules\n this.cache = ctx.core.cache.getOrCreate<unknown>('runtime:external', {\n maxEntries: 100,\n defaultTTL: definition.cache?.ttl ?? 60\n })\n\n if (definition.cache?.invalidateOn?.length) {\n this.cache.addInvalidationRules(definition.cache.invalidateOn)\n }\n\n // Register live mode observer: push updates when cache is invalidated\n if (definition.realtime === 'live' && definition.cache?.invalidateOn?.length) {\n this.registerLiveObserver(definition.cache.invalidateOn)\n }\n }\n\n /**\n * Register observer for live mode: when invalidation events fire,\n * push entity.updated to subscribed clients.\n */\n private registerLiveObserver(events: string[]): void {\n import('../../core/events-hub/socket.js').then(({ getRoomSize, isSocketIOInitialized }) => {\n const handler = async () => {\n if (!isSocketIOInitialized()) return\n\n const room = entityRoom(this.ctx.tenantId, this.moduleName, this.entityKey)\n const roomSize = await getRoomSize(room)\n if (roomSize === 0) return\n\n this.logger.debug({ entity: this.definition.label, room, roomSize }, 'External live push triggered')\n\n this.ctx.core.events.emit('entity.updated', {\n tenantId: this.ctx.tenantId,\n module: this.moduleName,\n entity: this.entityKey,\n action: 'updated' as const,\n id: '*',\n data: undefined,\n timestamp: new Date().toISOString()\n })\n }\n\n for (const event of events) {\n let timer: ReturnType<typeof setTimeout> | null = null\n ;(this.ctx.core.events as unknown as { on: (e: string, fn: () => void) => void }).on(event, () => {\n if (timer) clearTimeout(timer)\n timer = setTimeout(handler, 500)\n })\n }\n })\n }\n\n /**\n * Get table - the resource identifier in the external system\n */\n protected override get table(): string {\n return this.definition.table\n }\n\n /**\n * Get cache key for a request\n */\n private getCacheKey(operation: string, id?: string, query?: Record<string, unknown>): string {\n const keyField = this.definition.cache?.key ?? 'id'\n const base = `external:${this.definition.adapter}:${this.table}:${operation}`\n if (id) return `${base}:${keyField}:${id}`\n if (query && Object.keys(query).length > 0) return `${base}:${JSON.stringify(query)}`\n return base\n }\n\n /**\n * Ensure adapter is available\n */\n private ensureAdapter(): DatabaseAdapter {\n if (!this.adapter) {\n throw new this.errors.AppError(\n `Adapter '${this.definition.adapter}' not registered. Register it with ctx.registerAdapter().`,\n 500\n )\n }\n return this.adapter\n }\n\n /**\n * Get all items with pagination\n */\n async findAll(query?: EntityQuery): Promise<PaginatedResult<T>> {\n const adapter = this.ensureAdapter()\n\n // Check cache (include query params to avoid stale results)\n const cacheKey = this.getCacheKey('findAll', undefined, query as unknown as Record<string, unknown>)\n const cached = await this.cache.get(cacheKey) as PaginatedResult<T> | null\n if (cached) {\n return cached\n }\n\n const result = await adapter.findMany<T>(this.table, query)\n\n // Cache result with entity-specific TTL\n const ttl = this.definition.cache?.ttl\n if (ttl && ttl > 0) {\n await this.cache.set(cacheKey, result, ttl)\n }\n\n return result\n }\n\n /**\n * Get item by ID\n */\n async findById(id: string): Promise<T | null> {\n const adapter = this.ensureAdapter()\n\n // Check cache\n const cacheKey = this.getCacheKey('findById', id)\n const cached = await this.cache.get(cacheKey) as T | null\n if (cached) {\n return cached\n }\n\n const result = await adapter.findById<T>(this.table, id)\n\n // Cache result with entity-specific TTL\n if (result) {\n const ttl = this.definition.cache?.ttl\n if (ttl && ttl > 0) {\n await this.cache.set(cacheKey, result, ttl)\n }\n }\n\n return this.processAfterFindById(result)\n }\n\n /**\n * Create via adapter\n */\n async create(data: Partial<T>): Promise<T> {\n const adapter = this.ensureAdapter()\n\n const processedData = await this.beforeCreate(data)\n const result = await adapter.insert<T>(this.table, processedData as Record<string, unknown>)\n await this.afterCreate(result)\n\n // Invalidate list cache\n await this.cache.delete(this.getCacheKey('findAll'))\n\n return result\n }\n\n /**\n * Update via adapter\n */\n async update(id: string, data: Partial<T>): Promise<T> {\n const adapter = this.ensureAdapter()\n\n const processedData = await this.beforeUpdate(id, data)\n const result = await adapter.update<T>(this.table, id, processedData as Record<string, unknown>)\n await this.afterUpdate(result)\n\n // Invalidate caches\n await this.cache.delete(this.getCacheKey('findAll'))\n await this.cache.delete(this.getCacheKey('findById', id))\n\n return result\n }\n\n /**\n * Delete via adapter\n */\n async delete(id: string): Promise<void> {\n const adapter = this.ensureAdapter()\n\n await this.beforeDelete(id)\n await adapter.delete(this.table, id)\n await this.afterDelete(id)\n\n // Invalidate caches\n await this.cache.delete(this.getCacheKey('findAll'))\n await this.cache.delete(this.getCacheKey('findById', id))\n }\n\n /**\n * Clear all cache for this entity\n */\n async clearCache(): Promise<void> {\n const prefix = `external:${this.definition.adapter}:${this.table}:`\n await this.cache.deleteByPrefix(prefix)\n }\n\n /**\n * Refresh data from external source (bypass cache)\n */\n async refresh(query?: EntityQuery): Promise<PaginatedResult<T>> {\n this.clearCache()\n return this.findAll(query)\n }\n\n /**\n * Get cache statistics\n */\n getCacheStats() {\n return this.cache.getStats()\n }\n}\n","/**\n * Schema Builder - Generates Zod schemas from FieldDefinition at runtime\n *\n * Converts FieldValidationConfig to Zod validations automatically\n */\n\nimport { z, type ZodSchema, type ZodObject, type ZodRawShape } from 'zod'\nimport type { FieldDefinition, EntityDefinition, InputType, DbType } from '@gzl10/nexus-sdk'\n\n/**\n * Infer db type from input type for fields without db config.\n * DEPRECATED: Prefer using db: { type: 'array', virtual: true } for virtual fields.\n * This fallback exists for backwards compatibility.\n */\nfunction inferTypeFromInput(input: InputType): DbType {\n switch (input) {\n case 'number':\n case 'decimal':\n return 'decimal'\n case 'checkbox':\n case 'switch':\n return 'boolean'\n case 'date':\n return 'date'\n case 'datetime':\n return 'datetime'\n case 'json':\n return 'json'\n case 'textarea':\n case 'markdown':\n return 'text'\n case 'multiselect':\n case 'transfer':\n return 'array' // Virtual field for multi-select/transfer (array of IDs)\n default:\n return 'string'\n }\n}\n\n/**\n * Build Zod schema for a single field based on its definition\n */\nfunction buildFieldSchema(field: FieldDefinition): ZodSchema {\n const { db, validation } = field\n let schema: ZodSchema\n\n // Use db.type for validation. For virtual fields, use db: { type: 'array', virtual: true }\n // Fallback to inferTypeFromInput for backwards compatibility with fields without db config\n const dbType = db?.type ?? inferTypeFromInput(field.input)\n\n // Determine base type from db.type\n switch (dbType) {\n case 'string':\n case 'text':\n schema = z.string()\n\n // Apply string validations\n if (validation?.min !== undefined) {\n schema = (schema as z.ZodString).min(validation.min)\n }\n if (validation?.max !== undefined) {\n schema = (schema as z.ZodString).max(validation.max)\n }\n if (db?.size && !validation?.max) {\n // Use db.size as max length if no explicit max\n schema = (schema as z.ZodString).max(db.size)\n }\n\n // Auto-validate based on input type (unless custom pattern overrides)\n if (!validation?.pattern) {\n if (field.input === 'email') {\n schema = (schema as z.ZodString).email()\n } else if (field.input === 'url') {\n schema = (schema as z.ZodString).url()\n } else if (field.input === 'uuid') {\n schema = (schema as z.ZodString).uuid()\n } else if (field.input === 'slug') {\n schema = (schema as z.ZodString).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)\n }\n }\n\n if (validation?.pattern) {\n schema = (schema as z.ZodString).regex(new RegExp(validation.pattern))\n }\n if (validation?.enum) {\n schema = z.enum(validation.enum as [string, ...string[]])\n }\n break\n\n case 'integer':\n schema = z.number().int()\n if (validation?.min !== undefined) {\n schema = (schema as z.ZodNumber).min(validation.min)\n }\n if (validation?.max !== undefined) {\n schema = (schema as z.ZodNumber).max(validation.max)\n }\n break\n\n case 'decimal':\n schema = z.number()\n if (validation?.min !== undefined) {\n schema = (schema as z.ZodNumber).min(validation.min)\n }\n if (validation?.max !== undefined) {\n schema = (schema as z.ZodNumber).max(validation.max)\n }\n break\n\n case 'boolean':\n // Coerce numeric/string values to boolean (SQLite stores as 0/1)\n schema = z.preprocess((val) => {\n if (val === 0 || val === '0' || val === 'false') return false\n if (val === 1 || val === '1' || val === 'true') return true\n return val\n }, z.boolean())\n break\n\n case 'date':\n case 'datetime':\n // Accept string (ISO) or Date\n schema = z.union([z.string(), z.date()])\n break\n\n case 'json':\n schema = z.unknown()\n break\n\n case 'uuid':\n schema = z.string().uuid()\n break\n\n case 'array':\n // For multiselect inputs - array of string IDs\n schema = z.array(z.string())\n break\n\n default:\n schema = z.unknown()\n }\n\n // Determine if field is required:\n // - field.required: true → required\n // - db.nullable: false (without default) → required\n // - Otherwise → optional/nullable\n const hasDefault = db?.default !== undefined\n // Infer nullable from required if not explicitly set\n const isNullable = db?.nullable ?? (field.required === true ? false : true)\n const isDbRequired = !isNullable && !hasDefault\n const isRequired = field.required === true || isDbRequired\n\n if (!isRequired) {\n // Field is optional\n if (isNullable) {\n schema = schema.nullable().optional()\n } else {\n schema = schema.optional()\n }\n }\n // If isRequired, schema remains as-is (not optional)\n\n return schema\n}\n\n/**\n * Fields to exclude from create/update schemas\n */\nconst AUTO_FIELDS = ['id', 'created_at', 'updated_at', 'created_by', 'updated_by']\n\n/**\n * Build Zod schema for entity create operation\n *\n * - Excludes auto-generated fields (id, timestamps, audit) from required validation\n * - Applies required validation\n * - Uses passthrough() to allow custom fields (like custom id)\n */\nexport function buildCreateSchema(definition: EntityDefinition): ZodObject<ZodRawShape> {\n const shape: ZodRawShape = {}\n\n if (!('fields' in definition) || !definition.fields) {\n return z.object(shape).passthrough()\n }\n\n for (const [name, field] of Object.entries(definition.fields)) {\n // Skip auto fields - they're optional and handled by service\n if (AUTO_FIELDS.includes(name)) continue\n\n shape[name] = buildFieldSchema(field)\n }\n\n // passthrough allows custom fields like id to be passed to service\n return z.object(shape).passthrough()\n}\n\n/**\n * Build Zod schema for entity update operation\n *\n * - Excludes auto-generated fields\n * - All fields are optional (partial update)\n * - Uses passthrough() to allow custom fields\n */\nexport function buildUpdateSchema(definition: EntityDefinition): ZodObject<ZodRawShape> {\n const shape: ZodRawShape = {}\n\n if (!('fields' in definition) || !definition.fields) {\n return z.object(shape).passthrough()\n }\n\n for (const [name, field] of Object.entries(definition.fields)) {\n // Skip auto fields\n if (AUTO_FIELDS.includes(name)) continue\n\n // Make all fields optional for update\n let schema = buildFieldSchema(field)\n if (!schema.isOptional()) {\n schema = schema.optional()\n }\n\n shape[name] = schema\n }\n\n return z.object(shape).passthrough()\n}\n\n/**\n * Cache for compiled schemas (per entity definition)\n */\nconst schemaCache = new WeakMap<EntityDefinition, {\n create: ZodObject<ZodRawShape>\n update: ZodObject<ZodRawShape>\n}>()\n\n/**\n * Get cached schemas for an entity definition\n */\nexport function getSchemas(definition: EntityDefinition): {\n create: ZodObject<ZodRawShape>\n update: ZodObject<ZodRawShape>\n} {\n let cached = schemaCache.get(definition)\n\n if (!cached) {\n cached = {\n create: buildCreateSchema(definition),\n update: buildUpdateSchema(definition)\n }\n schemaCache.set(definition, cached)\n }\n\n return cached\n}\n","export { buildCreateSchema, buildUpdateSchema, getSchemas } from './schema-builder.js'\n","/**\n * Entity Controller - Auto-generated controllers based on entity type\n *\n * Soporte automático:\n * - CASL: Si definition.casl está definido, verifica permisos\n * - Validación: Genera Zod schemas desde definition.fields.validation\n */\n\nimport type { Request, Response } from 'express'\nimport type {\n EntityDefinition,\n ModuleContext,\n AuthRequest,\n ActionDefinition,\n CollectionEntityDefinition,\n SingleEntityDefinition\n} from '@gzl10/nexus-sdk'\nimport { resolveLocalized } from '@gzl10/nexus-sdk'\nimport { ZodError, type ZodIssue } from 'zod'\nimport type { EntityController, EntityService, EntityQuery, EntityHandler } from '../types.js'\nimport type { ValidationDetail } from '../../core/errors/app-error.js'\nimport { getSchemas } from '../validation/index.js'\nimport {\n excludeSensitiveFields,\n excludeSensitiveFieldsFromResult,\n filterByPermittedFields,\n filterResultByPermittedFields,\n validateWritePermissions,\n getCaslConditionsForQuery\n} from '../helpers/index.js'\n\n/**\n * Converts ZodIssue[] to ValidationDetail[]\n */\nfunction zodToValidationDetails(issues: ZodIssue[]): ValidationDetail[] {\n return issues.map(e => ({\n path: e.path.join('.'),\n message: e.message\n }))\n}\n\n/**\n * Capitalize first letter (for inferring CASL subject from table name)\n */\nfunction capitalizeFirst(str: string): string {\n return str.charAt(0).toUpperCase() + str.slice(1)\n}\n\n/**\n * Whether CASL debug logging is enabled.\n * Set NEXUS_CASL_DEBUG=true for detailed permit/deny logs.\n */\nconst CASL_DEBUG = process.env['NEXUS_CASL_DEBUG'] === 'true'\n\n/**\n * Get all field names from entity definition\n */\nfunction getAllFieldNames(definition: EntityDefinition): string[] {\n if (!('fields' in definition)) return []\n return Object.keys(definition.fields ?? {})\n}\n\n/**\n * Safely extract CASL subject from definition.casl\n * Handles both EntityCaslConfig and simplified action CASL config\n */\nfunction getCaslSubject(casl: EntityDefinition['casl']): string | undefined {\n if (!casl) return undefined\n // EntityCaslConfig has 'subject', simplified action casl has 'action'\n return 'subject' in casl ? casl.subject : undefined\n}\n\n/**\n * Safely extract sensitiveFields from definition.casl\n * Only EntityCaslConfig has sensitiveFields\n */\nfunction getCaslSensitiveFields(casl: EntityDefinition['casl']): string[] {\n if (!casl) return []\n return 'sensitiveFields' in casl ? (casl.sensitiveFields ?? []) : []\n}\n\n/** Query params reserved for pagination/sorting — never treated as field filters */\nconst RESERVED_QUERY_PARAMS = new Set(['page', 'limit', 'sort', 'order', 'search', 'filters', 'locale']) // Reserved for future per-request locale resolution\n\n/** Max items per bulk operation */\nconst MAX_BULK_SIZE = 100\n\n/**\n * Parse and validate filters from query string.\n * Shared between list and count handlers.\n *\n * @throws ValidationError if filters are invalid\n */\nfunction parseFilters(raw: string, errors: ModuleContext['core']['errors']): Record<string, unknown> {\n if (raw.length > 2048) {\n throw new errors.ValidationError('Filters too large (max 2KB)')\n }\n let filters: Record<string, unknown>\n try {\n filters = JSON.parse(raw)\n } catch {\n throw new errors.ValidationError('Invalid filters JSON')\n }\n if (typeof filters !== 'object' || Array.isArray(filters) || filters === null) {\n throw new errors.ValidationError('Filters must be a JSON object')\n }\n const keys = Object.keys(filters)\n if (keys.length > 20) {\n throw new errors.ValidationError('Too many filter clauses (max 20)')\n }\n for (const key of keys) {\n if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(key)) {\n throw new errors.ValidationError(`Invalid filter key: ${key}`)\n }\n }\n return filters\n}\n\n/**\n * Create controller for an entity service with optional CASL authorization\n */\nexport function createEntityController(\n service: EntityService,\n definition: EntityDefinition,\n ctx: ModuleContext\n): EntityController {\n const type = definition.type ?? 'collection'\n\n // CASL subject (from definition or inferred from table/key name)\n const entityName = 'table' in definition ? definition.table : ('key' in definition ? definition.key : 'entity')\n const caslSubject = getCaslSubject(definition.casl) ?? capitalizeFirst(entityName ?? 'Entity')\n const hasCasl = !!definition.casl\n const isPublic = 'public' in definition && (definition as { public?: boolean }).public === true\n const sensitiveFields = getCaslSensitiveFields(definition.casl)\n const allFields = getAllFieldNames(definition)\n\n // Warn about entities with DB persistence but no CASL or public flag\n if (!hasCasl && !isPublic && ['collection', 'single', 'tree', 'dag', 'reference', 'config', 'event'].includes(type)) {\n ctx.core.logger.warn(\n { entity: entityName, type },\n `Entity \"${entityName}\" has no CASL config — authenticated users have full access (no role-based restrictions)`\n )\n }\n\n // Get validation schemas (cached)\n const schemas = getSchemas(definition)\n\n /**\n * Verify CASL permission and log the authorization decision.\n *\n * Deny decisions are always logged at warn level.\n * Permit decisions are logged at debug level when NEXUS_CASL_DEBUG=true.\n * No PII is included — only userId, action, subject, and requestId.\n *\n * @throws UnauthorizedError if not authenticated\n * @throws ForbiddenError if permission denied\n */\n function checkPermission(\n req: Request,\n action: 'read' | 'create' | 'update' | 'delete' | 'manage' | 'execute',\n instance?: unknown\n ): void {\n // Public entities skip all authorization\n if (isPublic) return\n\n const authReq = req as AuthRequest\n const userId = authReq.user?.id ?? null\n const reqId = req.requestId\n\n if (hasCasl) {\n // CASL-protected: require authentication + check ability\n if (!authReq.ability) {\n ctx.core.logger.warn({ action, subject: caslSubject, reqId, decision: 'deny', reason: 'unauthenticated' }, 'authz:deny')\n throw new ctx.core.errors.UnauthorizedError('Authentication required')\n }\n\n const { subject, ForbiddenError: CASLForbiddenError } = ctx.core.abilities\n const target = instance ? subject(caslSubject, instance as Record<string, unknown>) : caslSubject\n\n try {\n CASLForbiddenError.from(authReq.ability).throwUnlessCan(action, target)\n } catch {\n ctx.core.logger.warn({ action, subject: caslSubject, userId, reqId, decision: 'deny', reason: 'forbidden' }, 'authz:deny')\n throw new ctx.core.errors.ForbiddenError(`Not allowed to ${action} ${resolveLocalized(definition.label, 'en')}`)\n }\n\n if (CASL_DEBUG) {\n ctx.core.logger.debug({ action, subject: caslSubject, userId, reqId, decision: 'permit' }, 'authz:permit')\n }\n return\n }\n\n // Default-deny: no CASL, no public flag → require authentication (no RBAC)\n if (!authReq.ability) {\n ctx.core.logger.warn({ action, subject: caslSubject, reqId, decision: 'deny', reason: 'unauthenticated' }, 'authz:deny')\n throw new ctx.core.errors.UnauthorizedError('Authentication required')\n }\n\n if (CASL_DEBUG) {\n ctx.core.logger.debug({ action, subject: caslSubject, userId, reqId, decision: 'permit', reason: 'authenticated-no-rbac' }, 'authz:permit')\n }\n }\n\n // Base handlers\n const controller: EntityController = {\n /**\n * List entities\n */\n async list(req: Request, res: Response): Promise<void> {\n checkPermission(req, 'read')\n\n // Parse and validate pagination params (limit=0 means unpaginated)\n const page = Math.max(1, parseInt(req.query['page'] as string) || 1)\n const rawLimit = parseInt(req.query['limit'] as string)\n const limit = rawLimit === 0 ? 0 : Math.min(100, Math.max(1, rawLimit || 20))\n\n // Parse and validate filters\n let filters: Record<string, unknown> | undefined\n if (req.query['filters']) {\n filters = parseFilters(req.query['filters'] as string, ctx.core.errors)\n }\n\n // Extract field-name query params as shorthand filters (e.g. ?type=timezones)\n for (const key of allFields) {\n if (key in req.query && !RESERVED_QUERY_PARAMS.has(key)) {\n filters = filters ?? {}\n filters[key] = req.query[key]\n }\n }\n\n const query: EntityQuery = {\n page,\n limit,\n sort: req.query['sort'] as string | undefined,\n order: (req.query['order'] as 'asc' | 'desc' | undefined) ?? 'asc',\n search: req.query['search'] ? String(req.query['search']).slice(0, 200) : undefined,\n filters\n }\n\n // Apply CASL conditions as query filters (row-level security)\n const authReq = req as AuthRequest\n if (hasCasl && authReq.ability) {\n const caslConditions = getCaslConditionsForQuery(authReq.ability, 'read', caslSubject)\n if (caslConditions) {\n query.filters = { ...query.filters, ...caslConditions }\n }\n }\n\n const result = await service.findAll(query)\n\n // Apply field-level filtering: sensitiveFields first, then CASL field permissions\n let filtered = excludeSensitiveFieldsFromResult(\n result as { items: Record<string, unknown>[]; total: number; page: number; limit: number; totalPages: number; hasNext: boolean },\n sensitiveFields\n )\n filtered = filterResultByPermittedFields(\n filtered as { items: Record<string, unknown>[]; total: number; page: number; limit: number; totalPages: number; hasNext: boolean },\n authReq.ability,\n caslSubject,\n allFields\n )\n res.json(filtered)\n },\n\n /**\n * Get single entity\n */\n async get(req: Request, res: Response): Promise<void> {\n const id = String(req.params['id'] ?? '')\n\n const entity = await service.findById(id)\n\n if (!entity) {\n throw new ctx.core.errors.NotFoundError(`${resolveLocalized(definition.label, 'en')} not found`)\n }\n\n // Check permission with instance (for conditions like { id: '${user.id}' })\n checkPermission(req, 'read', entity)\n\n const authReq = req as AuthRequest\n // Apply field-level filtering: sensitiveFields first, then CASL field permissions\n let filtered = excludeSensitiveFields(entity as Record<string, unknown>, sensitiveFields)\n filtered = filterByPermittedFields(\n filtered as Record<string, unknown>,\n authReq.ability,\n 'read',\n caslSubject,\n allFields\n )\n res.json(filtered)\n }\n }\n\n // Add create handler if supported\n // Check allowCreate flag (default: true)\n const allowCreate = 'allowCreate' in definition ? definition.allowCreate !== false : true\n if (supportsCreate(type) && allowCreate) {\n controller.create = async (req: Request, res: Response): Promise<void> => {\n checkPermission(req, 'create')\n\n if (!service.create) {\n throw new ctx.core.errors.ForbiddenError(`${resolveLocalized(definition.label, 'en')} does not support create`)\n }\n\n // Validate input\n let validated: Record<string, unknown>\n try {\n validated = schemas.create.parse(req.body)\n } catch (error) {\n if (error instanceof ZodError) {\n throw new ctx.core.errors.ValidationError('Validation failed', zodToValidationDetails(error.errors))\n }\n throw error\n }\n\n // Validate field-level write permissions\n const authReq = req as AuthRequest\n validateWritePermissions(validated, authReq.ability, 'create', caslSubject, allFields, ctx)\n\n // Inject auth user ID for audit fields\n if (authReq.user?.id) {\n validated['_authUserId'] = authReq.user.id\n }\n\n const entity = await service.create(validated)\n\n // Apply field-level filtering to response\n let filtered = excludeSensitiveFields(entity as Record<string, unknown>, sensitiveFields)\n filtered = filterByPermittedFields(filtered as Record<string, unknown>, authReq.ability, 'read', caslSubject, allFields)\n res.status(201).json(filtered)\n }\n }\n\n // Add update handler if supported\n // Check allowEdit flag (default: true)\n const allowEdit = 'allowEdit' in definition ? definition.allowEdit !== false : true\n if (supportsUpdate(type) && allowEdit) {\n controller.update = async (req: Request, res: Response): Promise<void> => {\n if (!service.update) {\n throw new ctx.core.errors.ForbiddenError(`${resolveLocalized(definition.label, 'en')} does not support update`)\n }\n\n const id = String(req.params['id'] ?? '')\n\n // First fetch the entity to check permission with instance\n const existing = await service.findById(id)\n if (!existing) {\n throw new ctx.core.errors.NotFoundError(`${resolveLocalized(definition.label, 'en')} not found`)\n }\n\n checkPermission(req, 'update', existing)\n\n // Validate input\n let validated: Record<string, unknown>\n try {\n validated = schemas.update.parse(req.body)\n } catch (error) {\n if (error instanceof ZodError) {\n throw new ctx.core.errors.ValidationError('Validation failed', zodToValidationDetails(error.errors))\n }\n throw error\n }\n\n // Validate field-level write permissions\n const authReq = req as AuthRequest\n validateWritePermissions(validated, authReq.ability, 'update', caslSubject, allFields, ctx)\n\n // Inject auth user ID for audit fields\n if (authReq.user?.id) {\n validated['_authUserId'] = authReq.user.id\n }\n\n const entity = await service.update(id, validated)\n\n // Apply field-level filtering to response\n let filtered = excludeSensitiveFields(entity as Record<string, unknown>, sensitiveFields)\n filtered = filterByPermittedFields(filtered as Record<string, unknown>, authReq.ability, 'read', caslSubject, allFields)\n res.json(filtered)\n }\n }\n\n // Add delete handler if supported\n // Check allowDelete flag (default: true)\n const allowDelete = 'allowDelete' in definition ? definition.allowDelete !== false : true\n if (supportsDelete(type) && allowDelete) {\n controller.delete = async (req: Request, res: Response): Promise<void> => {\n if (!service.delete) {\n throw new ctx.core.errors.ForbiddenError(`${resolveLocalized(definition.label, 'en')} does not support delete`)\n }\n\n const id = String(req.params['id'] ?? '')\n\n // First fetch the entity to check permission with instance\n const existing = await service.findById(id)\n if (!existing) {\n throw new ctx.core.errors.NotFoundError(`${resolveLocalized(definition.label, 'en')} not found`)\n }\n\n checkPermission(req, 'delete', existing)\n\n const authReq = req as AuthRequest\n await service.delete(id, { actorId: authReq.user?.id })\n res.status(204).send()\n }\n }\n\n // Add count handler for non-singleton entities\n const isSingleton = (type === 'single' || type === 'config') && !('scopeField' in definition && (definition as SingleEntityDefinition).scopeField)\n const serviceCount = service.count?.bind(service)\n if (!isSingleton && serviceCount) {\n controller.count = async (req: Request, res: Response): Promise<void> => {\n checkPermission(req, 'read')\n\n let filters: Record<string, unknown> | undefined\n if (req.query['filters']) {\n filters = parseFilters(req.query['filters'] as string, ctx.core.errors)\n }\n\n // Extract field-name query params as shorthand filters (e.g. ?type=timezones)\n for (const key of allFields) {\n if (key in req.query && !RESERVED_QUERY_PARAMS.has(key)) {\n filters = filters ?? {}\n filters[key] = req.query[key]\n }\n }\n\n // Apply CASL conditions as filters (row-level security)\n const authReq = req as AuthRequest\n if (hasCasl && authReq.ability) {\n const caslConditions = getCaslConditionsForQuery(authReq.ability, 'read', caslSubject)\n if (caslConditions) {\n filters = { ...filters, ...caslConditions }\n }\n }\n\n const count = await serviceCount(filters)\n res.json({ count })\n }\n }\n\n // Bulk Create - POST /bulk\n if (supportsCreate(type) && allowCreate && service.create) {\n controller.bulkCreate = async (req: Request, res: Response): Promise<void> => {\n checkPermission(req, 'create')\n\n const items = req.body as Record<string, unknown>[]\n if (!Array.isArray(items)) {\n throw new ctx.core.errors.ValidationError('Body must be an array')\n }\n if (items.length === 0) {\n throw new ctx.core.errors.ValidationError('Array must not be empty')\n }\n if (items.length > MAX_BULK_SIZE) {\n throw new ctx.core.errors.ValidationError(`Max ${MAX_BULK_SIZE} items per bulk operation`)\n }\n\n const authReq = req as AuthRequest\n const results: { created: unknown[]; errors: Array<{ index: number; error: string }> } = {\n created: [],\n errors: []\n }\n\n for (let i = 0; i < items.length; i++) {\n try {\n let validated: Record<string, unknown>\n try {\n validated = schemas.create.parse(items[i])\n } catch (error) {\n if (error instanceof ZodError) {\n results.errors.push({ index: i, error: error.errors.map(e => e.message).join(', ') })\n continue\n }\n throw error\n }\n\n validateWritePermissions(validated, authReq.ability, 'create', caslSubject, allFields, ctx)\n\n if (authReq.user?.id) {\n validated['_authUserId'] = authReq.user.id\n }\n\n const entity = await service.create!(validated)\n let filtered = excludeSensitiveFields(entity as Record<string, unknown>, sensitiveFields)\n filtered = filterByPermittedFields(filtered as Record<string, unknown>, authReq.ability, 'read', caslSubject, allFields)\n results.created.push(filtered)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n results.errors.push({ index: i, error: message })\n }\n }\n\n res.status(results.errors.length === 0 ? 201 : 207).json(results)\n }\n }\n\n // Bulk Update - PUT /bulk\n if (supportsUpdate(type) && allowEdit && service.update) {\n controller.bulkUpdate = async (req: Request, res: Response): Promise<void> => {\n const items = req.body as Array<{ id: string; data: Record<string, unknown> }>\n if (!Array.isArray(items)) {\n throw new ctx.core.errors.ValidationError('Body must be an array of { id, data }')\n }\n if (items.length === 0) {\n throw new ctx.core.errors.ValidationError('Array must not be empty')\n }\n if (items.length > MAX_BULK_SIZE) {\n throw new ctx.core.errors.ValidationError(`Max ${MAX_BULK_SIZE} items per bulk operation`)\n }\n\n const authReq = req as AuthRequest\n const results: { updated: unknown[]; errors: Array<{ index: number; id: string; error: string }> } = {\n updated: [],\n errors: []\n }\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n if (!item?.id || !item?.data) {\n results.errors.push({ index: i, id: item?.id ?? '', error: 'Each item must have id and data' })\n continue\n }\n\n try {\n const existing = await service.findById(item.id)\n if (!existing) {\n results.errors.push({ index: i, id: item.id, error: 'Not found' })\n continue\n }\n\n checkPermission(req, 'update', existing)\n\n let validated: Record<string, unknown>\n try {\n validated = schemas.update.parse(item.data)\n } catch (error) {\n if (error instanceof ZodError) {\n results.errors.push({ index: i, id: item.id, error: error.errors.map(e => e.message).join(', ') })\n continue\n }\n throw error\n }\n\n validateWritePermissions(validated, authReq.ability, 'update', caslSubject, allFields, ctx)\n\n if (authReq.user?.id) {\n validated['_authUserId'] = authReq.user.id\n }\n\n const entity = await service.update!(item.id, validated)\n let filtered = excludeSensitiveFields(entity as Record<string, unknown>, sensitiveFields)\n filtered = filterByPermittedFields(filtered as Record<string, unknown>, authReq.ability, 'read', caslSubject, allFields)\n results.updated.push(filtered)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n results.errors.push({ index: i, id: item.id, error: message })\n }\n }\n\n res.status(results.errors.length === 0 ? 200 : 207).json(results)\n }\n }\n\n // Bulk Delete - DELETE /bulk\n if (supportsDelete(type) && allowDelete && service.delete) {\n controller.bulkDelete = async (req: Request, res: Response): Promise<void> => {\n const { ids } = req.body as { ids: string[] }\n if (!Array.isArray(ids)) {\n throw new ctx.core.errors.ValidationError('Body must have { ids: string[] }')\n }\n if (ids.length === 0) {\n throw new ctx.core.errors.ValidationError('IDs array must not be empty')\n }\n if (ids.length > MAX_BULK_SIZE) {\n throw new ctx.core.errors.ValidationError(`Max ${MAX_BULK_SIZE} items per bulk operation`)\n }\n\n const results: { deleted: string[]; errors: Array<{ id: string; error: string }> } = {\n deleted: [],\n errors: []\n }\n\n for (const id of ids) {\n try {\n const existing = await service.findById(id)\n if (!existing) {\n results.errors.push({ id, error: 'Not found' })\n continue\n }\n\n checkPermission(req, 'delete', existing)\n await service.delete!(id)\n results.deleted.push(id)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n results.errors.push({ id, error: message })\n }\n }\n\n res.status(results.errors.length === 0 ? 200 : 207).json(results)\n }\n }\n\n // Recompute - POST /recompute (computed entities only)\n const serviceAny = service as unknown as Record<string, unknown>\n const serviceRecompute = serviceAny['recompute'] as\n ((params?: Record<string, unknown>) => Promise<unknown[]>) | undefined\n if (type === 'computed' && typeof serviceRecompute === 'function') {\n controller.recompute = async (req: Request, res: Response): Promise<void> => {\n checkPermission(req, 'read')\n const items = await serviceRecompute()\n res.json({ recomputed: items.length })\n }\n }\n\n return controller\n}\n\n/**\n * Check if entity type supports create via API\n * Note: 'event' entities are append-only INTERNALLY, not via API\n */\nfunction supportsCreate(type: string): boolean {\n return ['collection', 'external', 'temp', 'reference', 'config', 'tree', 'dag'].includes(type)\n}\n\n/**\n * Check if entity type supports update\n */\nfunction supportsUpdate(type: string): boolean {\n return ['collection', 'external', 'single', 'temp', 'reference', 'config', 'tree', 'dag'].includes(type)\n}\n\n/**\n * Check if entity type supports delete\n */\nfunction supportsDelete(type: string): boolean {\n return ['collection', 'external', 'temp', 'reference', 'tree', 'dag'].includes(type)\n}\n\n/**\n * Create handler for an ActionDefinition defined in a collection/single entity\n *\n * Scope behavior:\n * - 'row': Loads record from DB, injects _record into input\n * - 'entity': No record context, just executes handler\n * - 'module': Not used here (standalone actions in manifest.actions[])\n */\nexport function createActionHandler(\n action: ActionDefinition,\n definition: CollectionEntityDefinition | SingleEntityDefinition,\n ctx: ModuleContext,\n scope: 'row' | 'entity' = 'row'\n): EntityHandler {\n // Get table name for querying the record (only for row scope)\n const tableName = 'table' in definition ? definition.table : undefined\n\n // CASL subject: action's own subject takes priority over entity's subject\n const entityName = 'table' in definition ? definition.table : ('key' in definition ? definition.key : 'entity')\n const caslSubject = (action.casl && 'subject' in action.casl ? action.casl.subject : undefined)\n ?? definition.casl?.subject\n ?? capitalizeFirst(entityName ?? 'Entity')\n // skipAuth bypasses all authentication/authorization checks\n const hasCasl = !action.skipAuth && (!!definition.casl || !!action.casl)\n\n return async (req: Request, res: Response): Promise<void> => {\n const recordId = scope === 'row' ? String(req.params['id'] ?? '') : undefined\n const authReq = req as AuthRequest\n\n // 1. Early auth check: if CASL is required but user is not authenticated, return 401\n // This prevents information disclosure (404 vs 401 reveals resource existence)\n // Skipped if action.skipAuth is true (public endpoints)\n if (hasCasl && !authReq.ability) {\n ctx.core.logger.warn({ action: 'execute', subject: caslSubject, actionKey: action.key, reqId: req.requestId, decision: 'deny', reason: 'unauthenticated' }, 'authz:deny')\n throw new ctx.core.errors.UnauthorizedError('Authentication required')\n }\n\n // 2. Load record from database (only for row scope)\n let record: Record<string, unknown> | undefined\n if (scope === 'row' && tableName && recordId) {\n let query = ctx.db.knex(tableName).where('id', recordId)\n if (action.select?.length) {\n query = query.select(action.select)\n }\n record = await query.first()\n\n if (!record) {\n throw new ctx.core.errors.NotFoundError(`${resolveLocalized(definition.label, 'en')} not found`)\n }\n }\n\n // 3. Check CASL permission (user is authenticated at this point)\n if (hasCasl && authReq.ability) {\n const { subject, ForbiddenError: CASLForbiddenError } = ctx.core.abilities\n const caslAction = (action.casl && 'action' in action.casl) ? action.casl.action ?? 'execute' : 'execute'\n\n // Use record as subject for conditions support (row scope only)\n const target = record ? subject(caslSubject, record) : caslSubject\n\n try {\n CASLForbiddenError.from(authReq.ability).throwUnlessCan(caslAction, target)\n } catch {\n ctx.core.logger.warn({ action: caslAction, subject: caslSubject, actionKey: action.key, userId: authReq.user?.id, reqId: req.requestId, decision: 'deny', reason: 'forbidden' }, 'authz:deny')\n throw new ctx.core.errors.ForbiddenError(`Not allowed to execute ${resolveLocalized(action.label, 'en')}`)\n }\n\n if (CASL_DEBUG) {\n ctx.core.logger.debug({ action: caslAction, subject: caslSubject, actionKey: action.key, userId: authReq.user?.id, reqId: req.requestId, decision: 'permit' }, 'authz:permit')\n }\n }\n\n // 4. Validate input\n let input: Record<string, unknown> = req.body ?? {}\n if (action.inputSchema) {\n try {\n input = action.inputSchema.parse(input) as Record<string, unknown>\n } catch (error) {\n if (error instanceof ZodError) {\n throw new ctx.core.errors.ValidationError('Validation failed', zodToValidationDetails(error.errors))\n }\n throw error\n }\n }\n\n // 5. Extend input with _record (row scope) and _authUserId\n const extendedInput: Record<string, unknown> = { ...input }\n if (record) {\n extendedInput['_record'] = record\n }\n const userReq = req as { user?: { id?: string } }\n if (userReq.user?.id) {\n extendedInput['_authUserId'] = userReq.user.id\n }\n\n // 6. Execute handler\n if (!action.handler) {\n throw new ctx.core.errors.AppError(`No handler defined for action ${action.key}`, 500)\n }\n const result = await action.handler(ctx, extendedInput, req, res)\n\n // 7. Send response if not already sent\n if (result !== undefined && !res.headersSent) {\n res.json(result)\n }\n }\n}\n","/**\n * Batch Reporter - Helper for reporting progress in batch/long-running actions via SSE\n * @module core/sse/batch-reporter\n */\n\nimport type { SSESender, BatchProgressEvent, BatchLogEntry } from '@gzl10/nexus-sdk'\n\n/**\n * Reporter interface for batch actions to emit structured progress events\n */\nexport interface BatchReporter {\n /** Signal that the batch process has started */\n start(message?: string): void\n /** Report progress with percentage and optional step info */\n progress(percent: number, message?: string, step?: number, totalSteps?: number): void\n /** Append a log entry (sent incrementally to client) */\n log(level: 'info' | 'warn' | 'error', message: string): void\n /** Signal successful completion with optional result data */\n complete(result?: unknown): void\n /** Signal failure with error code and message */\n fail(code: string, message: string): void\n}\n\n/**\n * Create a BatchReporter that emits structured progress events via SSE.\n * All events use the named event 'batch' so the client can listen with addEventListener('batch', ...).\n *\n * @param sender - SSE sender from ctx.core.sse.stream()\n * @returns BatchReporter instance\n *\n * @example\n * ```typescript\n * // In an action handler:\n * const reporter = input._batchReporter as BatchReporter\n * reporter.start('Importing records...')\n * for (let i = 0; i < records.length; i++) {\n * await processRecord(records[i])\n * reporter.progress(Math.round((i + 1) / records.length * 100), `Processed ${i + 1}/${records.length}`, i + 1, records.length)\n * }\n * reporter.complete({ imported: records.length })\n * ```\n */\nexport function createBatchReporter(sender: SSESender): BatchReporter {\n const logs: BatchLogEntry[] = []\n\n function send(event: BatchProgressEvent): void {\n sender.send(event, 'batch')\n }\n\n return {\n start(message?: string): void {\n send({ status: 'started', percent: 0, message })\n },\n\n progress(percent: number, message?: string, step?: number, totalSteps?: number): void {\n send({ status: 'progress', percent, message, step, totalSteps })\n },\n\n log(level: 'info' | 'warn' | 'error', message: string): void {\n const entry: BatchLogEntry = { level, message, timestamp: new Date().toISOString() }\n logs.push(entry)\n send({ status: 'progress', logs: [entry] })\n },\n\n complete(result?: unknown): void {\n send({ status: 'completed', percent: 100, result, logs })\n sender.close()\n },\n\n fail(code: string, message: string): void {\n send({ status: 'error', error: { code, message }, logs })\n sender.close()\n }\n }\n}\n","/**\n * @module core/sse\n * @description SSE (Server-Sent Events) helpers for streaming responses\n *\n * @exports createSSEHelper - Factory for SSE stream connections (keep-alive, retry, cleanup)\n * @exports createBatchReporter - Factory for batch action progress reporting via named SSE events\n */\n\nimport type { Response } from 'express'\nimport type { SSEHelper, SSESender, SSEOptions } from '@gzl10/nexus-sdk'\nimport { logger } from '../logger/index.js'\n\nexport { createBatchReporter } from './batch-reporter.js'\nexport type { BatchReporter } from './batch-reporter.js'\n\nfunction safeWrite(res: Response, chunk: string): boolean {\n try {\n if (!res.writable) return false\n return res.write(chunk)\n } catch {\n return false\n }\n}\n\n/**\n * Create an SSE helper instance\n */\nexport function createSSEHelper(): SSEHelper {\n return {\n async stream(\n res: Response,\n handler: (send: SSESender) => Promise<void>,\n options: SSEOptions = {}\n ): Promise<void> {\n const { retry = 3000, keepAlive = 30000, onClose } = options\n\n // Set SSE headers\n res.setHeader('Content-Type', 'text/event-stream')\n res.setHeader('Cache-Control', 'no-cache')\n res.setHeader('Connection', 'keep-alive')\n res.setHeader('X-Accel-Buffering', 'no') // Disable nginx buffering\n res.flushHeaders()\n\n // Send retry interval\n res.write(`retry: ${retry}\\n\\n`)\n\n // Keep-alive timer\n const keepAliveTimer =\n keepAlive > 0 ? setInterval(() => safeWrite(res, ': ping\\n\\n'), keepAlive) : null\n\n // Create sender object\n const sender: SSESender = {\n send(data: unknown, event?: string, id?: string): void {\n let chunk = ''\n if (event) chunk += `event: ${event}\\n`\n if (id) chunk += `id: ${id}\\n`\n chunk += `data: ${JSON.stringify(data)}\\n\\n`\n safeWrite(res, chunk)\n },\n comment(text: string): void {\n safeWrite(res, `: ${text}\\n\\n`)\n },\n close(): void {\n res.end()\n }\n }\n\n res.on('error', (err) => {\n logger.debug({ err }, 'SSE stream error')\n if (keepAliveTimer) clearInterval(keepAliveTimer)\n })\n\n // Cleanup on client disconnect\n res.on('close', () => {\n if (keepAliveTimer) clearInterval(keepAliveTimer)\n onClose?.()\n })\n\n // Execute handler\n await handler(sender)\n }\n }\n}\n","/**\n * Entity Routes - Auto-generated routes based on entity type\n *\n * Soporte CASL automático:\n * - Si definition.casl está definido, aplica auth middleware\n * - La verificación de permisos se hace en el controller\n */\n\nimport type { Router, Request, Response, NextFunction, RequestHandler } from 'express'\nimport type {\n EntityDefinition,\n ModuleContext,\n CollectionEntityDefinition,\n SingleEntityDefinition,\n ActionDefinition\n} from '@gzl10/nexus-sdk'\nimport type { EntityController, EntityHandler, EntityService } from '../types.js'\nimport { createActionHandler } from '../controllers/entity.controller.js'\nimport { createBatchReporter } from '../../core/sse/index.js'\n\n/** Service interface for tree-like operations (tree and dag) */\ninterface TreeLikeService {\n getTree(rootId?: string, maxDepth?: number): Promise<unknown[]>\n findRoots(): Promise<unknown[]>\n findChildren(parentId: string | null): Promise<unknown[]>\n getAncestors(id: string): Promise<unknown[]>\n getDescendants(id: string): Promise<unknown[]>\n move(id: string, newParentId: string | null): Promise<unknown>\n}\n\n/** Service interface for DAG-specific operations */\ninterface DagLikeService extends TreeLikeService {\n getParents(nodeId: string): Promise<unknown[]>\n setParents(nodeId: string, parentIds: string[]): Promise<void>\n addParent(nodeId: string, parentId: string): Promise<void>\n removeParent(nodeId: string, parentId: string): Promise<void>\n}\n\n/**\n * Create router for an entity controller with optional auth middleware\n */\nexport function createEntityRouter(\n controller: EntityController,\n definition: EntityDefinition,\n ctx: ModuleContext,\n service?: EntityService\n): Router {\n const router = ctx.createRouter()\n\n // Skip all route mounting if entity is not exposed (service still works)\n const expose = 'expose' in definition ? (definition as { expose?: boolean }).expose : true\n if (expose === false) return router\n\n const type = definition.type ?? 'collection'\n // Singleton mode: single without scopeField (true singleton, not scoped config)\n const isSingleton = (type === 'single' || type === 'config') && !('scopeField' in definition && (definition as SingleEntityDefinition).scopeField)\n\n // Check if any action has skipAuth (needs selective auth, not global)\n const entityDef = definition as CollectionEntityDefinition | SingleEntityDefinition\n const hasSkipAuthActions = entityDef.actions?.some(a => a.skipAuth)\n const isPublic = 'public' in definition && (definition as { public?: boolean }).public === true\n\n // Default-deny: apply auth middleware unless entity is explicitly public.\n // When skipAuth actions exist, auth is applied per-route instead of globally.\n if (!isPublic && ctx.core.middleware['auth'] && !hasSkipAuthActions) {\n router.use(ctx.core.middleware['auth'])\n }\n\n // When hasSkipAuthActions, auth is not global - apply per-route for CRUD\n const authMiddleware = !isPublic && hasSkipAuthActions && ctx.core.middleware['auth']\n ? [ctx.core.middleware['auth']]\n : []\n\n // Apply entity-level middleware if defined (e.g., rate limiting)\n const entityMiddleware: RequestHandler[] = []\n if ('middleware' in definition && typeof definition.middleware === 'function') {\n const middleware = definition.middleware(ctx)\n if (Array.isArray(middleware)) {\n entityMiddleware.push(...middleware)\n } else {\n entityMiddleware.push(middleware)\n }\n }\n\n // Entity actions - MUST be registered BEFORE CRUD routes to avoid /:id capturing /action/:id\n // Supports 'row' and 'entity' scopes\n // - row (default): /{key}/:id - operates on a specific record\n // - entity: /{key} - operates on the collection (e.g., upload, import)\n // Note: 'module' scope actions should be in module.definitions[], not entity.actions[]\n const entityActions = entityDef.actions\n if (entityActions?.length) {\n for (const action of entityActions) {\n // Determine scope: default is 'row' for entity.actions[]\n // Skip 'module' scope actions (they shouldn't be here)\n const rawScope = action.scope ?? 'row'\n if (rawScope === 'module') continue\n const scope = rawScope as 'row' | 'entity'\n const actionPath = scope === 'row' ? `/${action.key}/:id` : `/${action.key}`\n const handlers: RequestHandler[] = []\n\n // Apply entity-level middleware first\n handlers.push(...entityMiddleware)\n\n // Apply auth middleware for actions that don't have skipAuth\n // (when hasSkipAuthActions, global auth is not applied)\n if (!isPublic && hasSkipAuthActions && !action.skipAuth && ctx.core.middleware['auth']) {\n handlers.push(ctx.core.middleware['auth'])\n }\n\n // For actions with skipAuth, apply optional auth to decode token if present\n // This allows handlers to check req.user for conditional logic (e.g., public vs private files)\n if (action.skipAuth && ctx.core.middleware['optionalAuth']) {\n handlers.push(ctx.core.middleware['optionalAuth'])\n }\n\n // Apply custom middleware if defined\n if (action.middleware) {\n const middleware = action.middleware(ctx)\n if (Array.isArray(middleware)) {\n handlers.push(...middleware)\n } else {\n handlers.push(middleware)\n }\n }\n\n // Add the action handler\n const actionHandler = createActionHandler(action, entityDef, ctx, scope)\n handlers.push(asyncHandler(actionHandler))\n\n // Register route with appropriate HTTP method\n const method = action.method ?? 'POST'\n switch (method) {\n case 'GET':\n router.get(actionPath, ...handlers)\n break\n case 'DELETE':\n router.delete(actionPath, ...handlers)\n break\n case 'PUT':\n router.put(actionPath, ...handlers)\n break\n case 'PATCH':\n router.patch(actionPath, ...handlers)\n break\n default:\n router.post(actionPath, ...handlers)\n }\n\n // Register SSE stream route for batch entity actions\n if (action.batch) {\n const streamPath = scope === 'row' ? `/${action.key}/stream/:id` : `/${action.key}/stream`\n const batchHandlers: RequestHandler[] = [...entityMiddleware]\n\n if (!isPublic && hasSkipAuthActions && !action.skipAuth && ctx.core.middleware['auth']) {\n batchHandlers.push(ctx.core.middleware['auth'])\n }\n if (action.skipAuth && ctx.core.middleware['optionalAuth']) {\n batchHandlers.push(ctx.core.middleware['optionalAuth'])\n }\n if (action.middleware) {\n const middleware = action.middleware(ctx)\n if (Array.isArray(middleware)) {\n batchHandlers.push(...middleware)\n } else {\n batchHandlers.push(middleware)\n }\n }\n\n batchHandlers.push(asyncHandler(createBatchActionHandler(action, entityDef, ctx, scope)))\n router.get(streamPath, ...batchHandlers)\n }\n }\n }\n\n // Tree-specific routes (type === 'tree' or 'dag')\n // These MUST be registered BEFORE /:id to avoid Express capturing /tree as :id\n if ((type === 'tree' || type === 'dag') && service) {\n const treeSvc = service as unknown as TreeLikeService\n\n router.get('/tree', ...entityMiddleware, ...authMiddleware, asyncHandler(async (req: Request, res: Response) => {\n const rootId = req.query['rootId'] as string | undefined\n const rawDepth = req.query['maxDepth'] as string | undefined\n const maxDepth = rawDepth !== undefined ? parseInt(rawDepth, 10) : undefined\n res.json(await treeSvc.getTree(rootId, Number.isNaN(maxDepth) ? undefined : maxDepth))\n }))\n\n router.get('/roots', ...entityMiddleware, ...authMiddleware, asyncHandler(async (_req: Request, res: Response) => {\n res.json(await treeSvc.findRoots())\n }))\n\n router.post('/:id/move', ...entityMiddleware, ...authMiddleware, asyncHandler(async (req: Request, res: Response) => {\n const id = String(req.params['id'] ?? '')\n const { parentId } = req.body as { parentId: string | null }\n res.json(await treeSvc.move(id, parentId ?? null))\n }))\n\n router.get('/:id/ancestors', ...entityMiddleware, ...authMiddleware, asyncHandler(async (req: Request, res: Response) => {\n const id = String(req.params['id'] ?? '')\n res.json(await treeSvc.getAncestors(id))\n }))\n\n router.get('/:id/descendants', ...entityMiddleware, ...authMiddleware, asyncHandler(async (req: Request, res: Response) => {\n const id = String(req.params['id'] ?? '')\n res.json(await treeSvc.getDescendants(id))\n }))\n\n router.get('/:id/children', ...entityMiddleware, ...authMiddleware, asyncHandler(async (req: Request, res: Response) => {\n const id = String(req.params['id'] ?? '')\n res.json(await treeSvc.findChildren(id))\n }))\n }\n\n // DAG-specific routes\n if (type === 'dag' && service) {\n const dagSvc = service as unknown as DagLikeService\n\n router.get('/:id/parents', ...entityMiddleware, ...authMiddleware, asyncHandler(async (req: Request, res: Response) => {\n const id = String(req.params['id'] ?? '')\n res.json(await dagSvc.getParents(id))\n }))\n\n router.put('/:id/parents', ...entityMiddleware, ...authMiddleware, asyncHandler(async (req: Request, res: Response) => {\n const id = String(req.params['id'] ?? '')\n const { parentIds } = req.body as { parentIds: string[] }\n await dagSvc.setParents(id, parentIds)\n res.status(204).send()\n }))\n\n router.post('/:id/parents', ...entityMiddleware, ...authMiddleware, asyncHandler(async (req: Request, res: Response) => {\n const id = String(req.params['id'] ?? '')\n const { parentId } = req.body as { parentId: string }\n await dagSvc.addParent(id, parentId)\n res.status(204).send()\n }))\n\n router.delete('/:id/parents/:parentId', ...entityMiddleware, ...authMiddleware, asyncHandler(async (req: Request, res: Response) => {\n const id = String(req.params['id'] ?? '')\n const parentId = String(req.params['parentId'] ?? '')\n await dagSvc.removeParent(id, parentId)\n res.status(204).send()\n }))\n }\n\n // CRUD routes - registered AFTER actions and tree/dag routes to avoid /:id capturing them\n\n // Bulk operations - MUST be registered BEFORE /:id to avoid capture\n if (controller.bulkCreate) {\n router.post('/bulk', ...entityMiddleware, ...authMiddleware, asyncHandler(controller.bulkCreate))\n }\n if (controller.bulkUpdate) {\n router.put('/bulk', ...entityMiddleware, ...authMiddleware, asyncHandler(controller.bulkUpdate))\n }\n if (controller.bulkDelete) {\n router.delete('/bulk', ...entityMiddleware, ...authMiddleware, asyncHandler(controller.bulkDelete))\n }\n\n // Count - GET /count (before /:id to avoid capture)\n if (controller.count) {\n router.get('/count', ...entityMiddleware, ...authMiddleware, asyncHandler(controller.count))\n }\n\n // Recompute - POST /recompute (computed entities only, before /:id)\n if (controller.recompute) {\n router.post('/recompute', ...entityMiddleware, ...authMiddleware, asyncHandler(controller.recompute))\n }\n\n // List - GET / (not for singleton entities - they only have one record)\n if (!isSingleton) {\n router.get('/', ...entityMiddleware, ...authMiddleware, asyncHandler(controller.list))\n }\n\n // Get by ID - GET /:id (or GET / for singleton entities)\n if (isSingleton) {\n router.get('/', ...entityMiddleware, ...authMiddleware, asyncHandler(controller.get))\n } else {\n router.get('/:id', ...entityMiddleware, ...authMiddleware, asyncHandler(controller.get))\n }\n\n // Create - POST /\n if (controller.create) {\n router.post('/', ...entityMiddleware, ...authMiddleware, asyncHandler(controller.create))\n }\n\n // Update - PUT /:id\n if (controller.update) {\n if (isSingleton) {\n // Singleton entities update without ID param\n router.put('/', ...entityMiddleware, ...authMiddleware, asyncHandler(controller.update))\n } else {\n router.put('/:id', ...entityMiddleware, ...authMiddleware, asyncHandler(controller.update))\n }\n }\n\n // Delete - DELETE /:id\n if (controller.delete) {\n router.delete('/:id', ...entityMiddleware, ...authMiddleware, asyncHandler(controller.delete))\n }\n\n return router\n}\n\n/**\n * Create SSE stream handler for entity-level batch actions (row/entity scope)\n * Reuses auth/CASL/record loading logic from createActionHandler\n */\nfunction createBatchActionHandler(\n action: ActionDefinition,\n definition: CollectionEntityDefinition | SingleEntityDefinition,\n ctx: ModuleContext,\n scope: 'row' | 'entity'\n): EntityHandler {\n const baseHandler = createActionHandler(action, definition, ctx, scope)\n\n return async (req: Request, res: Response): Promise<void> => {\n await ctx.core.sse.stream(res, async (sender) => {\n const reporter = createBatchReporter(sender)\n // Inject _batchReporter into body so createActionHandler includes it in extendedInput\n req.body = { ...req.body, ...req.query, _batchReporter: reporter }\n try {\n await baseHandler(req, res)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n ctx.core.logger.error({ action: action.key, error: message }, 'Batch action failed')\n reporter.fail('BATCH_ERROR', message)\n }\n })\n }\n}\n\n/**\n * Wrap async handler to catch errors\n * Express 5 handles this natively, but this is for compatibility\n */\nfunction asyncHandler(\n fn: EntityHandler\n): (req: Request, res: Response, next: NextFunction) => void {\n return (req, res, next) => {\n Promise.resolve(fn(req, res)).catch(next)\n }\n}\n","/**\n * Entity Factory - Creates service, controller, and router from EntityDefinition\n *\n * Single source of truth for entity service instantiation.\n */\n\nimport type { Router } from 'express'\nimport type {\n EntityDefinition,\n ModuleContext,\n BaseEntityService,\n CollectionEntityDefinition,\n SingleEntityDefinition,\n ViewEntityDefinition,\n ComputedEntityDefinition,\n ExternalEntityDefinition,\n TreeEntityDefinition,\n DagEntityDefinition,\n CreateEntityServiceOptions,\n EntityServiceHooks\n} from '@gzl10/nexus-sdk'\nimport { resolveLocalized } from '@gzl10/nexus-sdk'\nimport type { EntityRuntime, EntityService } from './types.js'\n\nimport { composeHooks } from './helpers/compose-hooks.js'\nimport { CollectionService } from './services/collection.service.js'\nimport { SingleService } from './services/single.service.js'\nimport { ViewService } from './services/view.service.js'\nimport { ComputedService } from './services/computed.service.js'\nimport { ExternalService } from './services/external.service.js'\n\n// Import controller and route factory\nimport { createEntityController } from './controllers/entity.controller.js'\nimport { createEntityRouter } from './routes/entity.routes.js'\n\n// Lazy-loaded services cache for tree/dag (avoid circular dependency)\nlet _TreeService: typeof import('./services/tree.service.js').TreeService | null = null\nlet _DagService: typeof import('./services/dag.service.js').DagService | null = null\n\nasync function getTreeService() {\n if (!_TreeService) {\n const mod = await import('./services/tree.service.js')\n _TreeService = mod.TreeService\n }\n return _TreeService\n}\n\nasync function getDagService() {\n if (!_DagService) {\n const mod = await import('./services/dag.service.js')\n _DagService = mod.DagService\n }\n return _DagService\n}\n\n/**\n * Create service based on entity type (sync - throws for tree/dag)\n */\nexport function createEntityService<T extends Record<string, unknown> = Record<string, unknown>>(\n ctx: ModuleContext,\n definition: EntityDefinition,\n options?: CreateEntityServiceOptions<T>\n): BaseEntityService<T> {\n const definitionHooks = 'hooks' in definition && typeof definition.hooks === 'function'\n ? definition.hooks(ctx) as EntityServiceHooks<T>\n : undefined\n const hooks = composeHooks(definitionHooks, options?.hooks)\n\n switch (definition.type) {\n case 'collection':\n case undefined:\n if ((definition as CollectionEntityDefinition).adapter) {\n return new ExternalService<T>(ctx, definition as unknown as ExternalEntityDefinition, hooks) as unknown as BaseEntityService<T>\n }\n return new CollectionService<T>(ctx, definition as CollectionEntityDefinition, hooks) as unknown as BaseEntityService<T>\n case 'temp':\n case 'event':\n case 'reference':\n return new CollectionService<T>(ctx, definition as CollectionEntityDefinition, hooks) as unknown as BaseEntityService<T>\n case 'single':\n case 'config':\n return new SingleService<T>(ctx, definition as SingleEntityDefinition, hooks) as unknown as BaseEntityService<T>\n case 'view':\n return new ViewService<T>(ctx, definition as ViewEntityDefinition, hooks) as unknown as BaseEntityService<T>\n case 'computed':\n case 'virtual':\n return new ComputedService<T>(ctx, definition as unknown as ComputedEntityDefinition, hooks) as unknown as BaseEntityService<T>\n case 'external':\n return new ExternalService<T>(ctx, definition as ExternalEntityDefinition, hooks) as unknown as BaseEntityService<T>\n default:\n throw new Error(`Unknown entity type: ${(definition as EntityDefinition).type}`)\n }\n}\n\n/**\n * Create service based on entity type (async - supports all types including tree/dag)\n */\nexport async function createEntityServiceAsync<T extends Record<string, unknown> = Record<string, unknown>>(\n ctx: ModuleContext,\n definition: EntityDefinition,\n options?: CreateEntityServiceOptions<T>\n): Promise<BaseEntityService<T>> {\n const definitionHooks = 'hooks' in definition && typeof definition.hooks === 'function'\n ? definition.hooks(ctx) as EntityServiceHooks<T>\n : undefined\n const hooks = composeHooks(definitionHooks, options?.hooks)\n\n if (definition.type === 'tree') {\n const TreeService = await getTreeService()\n return new TreeService<T>(ctx, definition as TreeEntityDefinition, hooks) as unknown as BaseEntityService<T>\n }\n\n if (definition.type === 'dag') {\n const DagService = await getDagService()\n return new DagService<T>(ctx, definition as DagEntityDefinition, hooks) as unknown as BaseEntityService<T>\n }\n\n return createEntityService(ctx, definition, options)\n}\n\n/**\n * Create complete runtime (service + controller + router) for an entity\n * Note: For tree/dag entities, use createEntityRuntimeAsync\n */\nexport function createEntityRuntime<T = unknown>(\n ctx: ModuleContext,\n definition: EntityDefinition\n): EntityRuntime<T> {\n const service = createEntityService(ctx, definition) as unknown as EntityService<T>\n const controller = createEntityController(service, definition, ctx)\n const router = createEntityRouter(controller, definition, ctx, service)\n return { service, controller, router }\n}\n\n/**\n * Create complete runtime (service + controller + router) for an entity (async version)\n * Required for tree and dag entity types\n */\nexport async function createEntityRuntimeAsync<T = unknown>(\n ctx: ModuleContext,\n definition: EntityDefinition\n): Promise<EntityRuntime<T>> {\n const service = await createEntityServiceAsync(ctx, definition) as unknown as EntityService<T>\n const controller = createEntityController(service, definition, ctx)\n const router = createEntityRouter(controller, definition, ctx, service)\n return { service, controller, router }\n}\n\n/**\n * Create services for all definitions in a module\n */\nexport async function createModuleServices(\n ctx: ModuleContext,\n definitions: EntityDefinition[]\n): Promise<Map<string, EntityService>> {\n const services = new Map<string, EntityService>()\n\n for (const definition of definitions) {\n const key = getServiceKey(definition)\n const service = await createEntityServiceAsync(ctx, definition) as unknown as EntityService\n services.set(key, service)\n }\n\n return services\n}\n\n/**\n * Get the key used to store service in ctx.services\n */\nexport function getServiceKey(definition: EntityDefinition): string {\n if ('table' in definition && definition.table) {\n return definition.table\n }\n if ('key' in definition && definition.key) {\n return definition.key\n }\n return resolveLocalized(definition.label, 'en').toLowerCase().replace(/\\s+/g, '_')\n}\n\n/**\n * Create routers for all definitions in a module\n */\nexport async function createModuleRouters(\n ctx: ModuleContext,\n definitions: EntityDefinition[],\n modulePrefix?: string\n): Promise<Router> {\n const router = ctx.createRouter()\n const routeMap = new Map<string, string>()\n\n for (const definition of definitions) {\n const runtime = await createEntityRuntimeAsync(ctx, definition)\n\n // Mount at entity route\n const route = inferEntityRoutePath(definition)\n const entityLabel = resolveLocalized(definition.label, 'en')\n\n // Skip route validation for non-exposed entities (no HTTP routes mounted)\n const isExposed = !('expose' in definition && definition.expose === false)\n\n // Warn if entity route duplicates the module prefix (common bug: table name = module name)\n if (isExposed && modulePrefix && route === modulePrefix) {\n ctx.core.logger.warn(\n `Entity \"${entityLabel}\" inferred route \"${route}\" duplicates module prefix — add routePrefix to the entity definition to fix`\n )\n }\n\n // Warn if two entities in the same module resolve to the same route\n if (isExposed && routeMap.has(route)) {\n ctx.core.logger.warn(\n `Entity \"${entityLabel}\" route \"${route}\" collides with \"${routeMap.get(route)}\" in the same module`\n )\n }\n if (isExposed) routeMap.set(route, entityLabel)\n\n router.use(route, runtime.router)\n\n // Register service in context (skip if already exists to avoid overwriting custom services)\n const key = getServiceKey(definition)\n if (ctx.services.has(key)) {\n ctx.core.logger.debug(\n { key, entity: definition.label },\n 'Service key already registered, skipping auto-registration'\n )\n } else {\n ctx.services.register(key, runtime.service)\n }\n }\n\n return router\n}\n\n/**\n * Get route path for entity\n * Uses routePrefix/key if defined, otherwise infers from table/label\n */\nfunction inferEntityRoutePath(definition: EntityDefinition): string {\n // Use explicit routePrefix if defined\n if ('routePrefix' in definition && definition.routePrefix !== undefined) {\n const routePrefix = definition.routePrefix as string\n return routePrefix.startsWith('/') ? routePrefix : `/${routePrefix}`\n }\n\n // Infer from table name: cms_posts -> /posts\n if ('table' in definition && definition.table) {\n const tableName = definition.table\n const parts = tableName.split('_')\n // Remove prefix if present\n const name = parts.length > 1 ? parts.slice(1).join('_') : tableName\n return `/${name}`\n }\n\n // Infer from key (for single entities): site_config -> /site_config\n if ('key' in definition && definition.key) {\n return `/${definition.key}`\n }\n\n // Fallback to label: \"My Entity\" -> /my-entity\n return `/${resolveLocalized(definition.label, 'en').toLowerCase().replace(/\\s+/g, '-')}`\n}\n","/**\n * Action Routes - Mount standalone module-scope actions\n *\n * Unlike entity routes, actions don't go through the entity pipeline\n * (no service factory, no BaseEntityService, no entity controller).\n * Each action mounts directly: auth → middleware → handler.\n */\n\nimport type { Router, Request, Response, NextFunction, RequestHandler } from 'express'\nimport type { ActionDefinition, ModuleContext, AuthRequest } from '@gzl10/nexus-sdk'\nimport { resolveLocalized } from '@gzl10/nexus-sdk'\nimport { createBatchReporter } from '../../core/sse/index.js'\n\n/**\n * Create router for all standalone actions in a module\n */\nexport function createActionRouters(\n ctx: ModuleContext,\n actions: ActionDefinition[]\n): Router {\n const router = ctx.createRouter()\n\n for (const action of actions) {\n const actionRouter = createActionRouter(ctx, action)\n router.use(`/${action.key}`, actionRouter)\n }\n\n return router\n}\n\n/**\n * Create router for a single standalone action\n */\nfunction createActionRouter(\n ctx: ModuleContext,\n action: ActionDefinition\n): Router {\n const router = ctx.createRouter()\n const method = action.method ?? 'POST'\n const handlers: RequestHandler[] = []\n\n // Auth middleware\n if (action.skipAuth) {\n if (ctx.core.middleware['optionalAuth']) {\n handlers.push(ctx.core.middleware['optionalAuth'])\n }\n } else if (ctx.core.middleware['auth']) {\n router.use(ctx.core.middleware['auth'])\n }\n\n // Custom middleware\n if (action.middleware) {\n const middleware = action.middleware(ctx)\n if (Array.isArray(middleware)) {\n handlers.push(...middleware)\n } else {\n handlers.push(middleware)\n }\n }\n\n // Execute handler\n const executeHandler = createExecuteHandler(ctx, action)\n handlers.push(asyncHandler(executeHandler))\n\n // Register route with appropriate HTTP method\n switch (method) {\n case 'GET': router.get('/', ...handlers); break\n case 'DELETE': router.delete('/', ...handlers); break\n case 'PUT': router.put('/', ...handlers); break\n case 'PATCH': router.patch('/', ...handlers); break\n default: router.post('/', ...handlers)\n }\n\n // SSE stream route for batch actions\n if (action.batch) {\n const batchHandlers: RequestHandler[] = []\n if (action.middleware) {\n const middleware = action.middleware(ctx)\n if (Array.isArray(middleware)) {\n batchHandlers.push(...middleware)\n } else {\n batchHandlers.push(middleware)\n }\n }\n batchHandlers.push(asyncHandler(createBatchHandler(ctx, action)))\n router.get('/stream', ...batchHandlers)\n }\n\n return router\n}\n\n/**\n * Create execute handler for a standalone action\n * Handles: CASL check → input validation → handler execution → response\n */\nfunction createExecuteHandler(\n ctx: ModuleContext,\n action: ActionDefinition\n): (req: Request, res: Response) => Promise<void> {\n const caslSubject = action.casl && 'subject' in action.casl\n ? action.casl.subject\n : `Action:${action.key}`\n const hasCasl = !action.skipAuth && !!action.casl\n\n return async (req: Request, res: Response): Promise<void> => {\n const authReq = req as AuthRequest\n\n // CASL authorization\n if (hasCasl && authReq.ability) {\n const { ForbiddenError: CASLForbiddenError } = ctx.core.abilities\n const caslAction = (action.casl && 'action' in action.casl)\n ? action.casl.action ?? 'execute'\n : 'execute'\n try {\n CASLForbiddenError.from(authReq.ability).throwUnlessCan(caslAction, caslSubject as any)\n } catch {\n throw new ctx.core.errors.ForbiddenError(\n `Not allowed to execute ${resolveLocalized(action.label, 'en')}`\n )\n }\n }\n\n // Validate input\n let input: unknown = req.body ?? {}\n if (action.inputSchema) {\n try {\n input = action.inputSchema.parse(input)\n } catch (err) {\n const label = resolveLocalized(action.label, 'en')\n throw new ctx.core.errors.ValidationError(\n `Invalid input for action ${label}`,\n (err as { errors?: Array<{ path: string[]; message: string }> }).errors?.map(e => ({\n path: e.path.join('.'),\n message: e.message,\n code: 'VALIDATION_FIELD_INVALID'\n })) ?? []\n )\n }\n }\n\n // Inject _authUserId\n let extendedInput = (input ?? {}) as Record<string, unknown>\n if (authReq.user?.id) {\n extendedInput = { ...extendedInput, _authUserId: authReq.user.id }\n }\n\n // Execute handler\n if (!action.handler) {\n throw new ctx.core.errors.AppError(\n `No handler defined for action ${resolveLocalized(action.label, 'en')}`,\n 500\n )\n }\n\n const result = await action.handler(ctx, extendedInput, req, res)\n\n // Send response if not already sent\n if (result !== undefined && !res.headersSent) {\n const successStatus = action.successStatus ?? 200\n res.status(successStatus).json(result)\n }\n }\n}\n\n/**\n * Create SSE batch stream handler\n */\nfunction createBatchHandler(\n ctx: ModuleContext,\n action: ActionDefinition\n): (req: Request, res: Response) => Promise<void> {\n return async (req: Request, res: Response): Promise<void> => {\n await ctx.core.sse.stream(res, async (sender) => {\n const reporter = createBatchReporter(sender)\n try {\n const input: Record<string, unknown> = { ...req.query, _batchReporter: reporter }\n const userReq = req as { user?: { id?: string } }\n if (userReq.user?.id) {\n input['_authUserId'] = userReq.user.id\n }\n if (action.handler) {\n await action.handler(ctx, input, req, res)\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error'\n ctx.core.logger.error({ action: action.key, error: message }, 'Batch action failed')\n reporter.fail('BATCH_ERROR', message)\n }\n })\n }\n}\n\nfunction asyncHandler(\n fn: (req: Request, res: Response) => Promise<void>\n): (req: Request, res: Response, next: NextFunction) => void {\n return (req, res, next) => {\n Promise.resolve(fn(req, res)).catch(next)\n }\n}\n","/**\n * @module runtime\n * @description Auto-generated services, controllers, and routes for entities\n *\n * @dependencies\n * - NONE (fully isolated from db/, core/, engine/)\n *\n * @note This module is intentionally isolated to ensure services are portable\n * and testable. All external dependencies are injected via ModuleContext.\n *\n * @usage\n * ```typescript\n * import { createEntityRuntime, createModuleRouters } from './runtime/index.js'\n *\n * // Create runtime for a single entity\n * const runtime = createEntityRuntime(ctx, postEntity)\n * app.use('/posts', runtime.router)\n *\n * // Create routers for all entities in a module\n * const router = createModuleRouters(ctx, module.definitions)\n * app.use('/api/cms', router)\n * ```\n */\n\n// Types\nexport * from './types.js'\n\n// Entity Factory\nexport {\n createEntityService,\n createEntityServiceAsync,\n createEntityRuntime,\n createEntityRuntimeAsync,\n createModuleServices,\n createModuleRouters,\n getServiceKey\n} from './entity-factory.js'\n\n// Services\nexport { BaseEntityService } from './services/base.service.js'\nexport { CollectionService } from './services/collection.service.js'\nexport { SingleService } from './services/single.service.js'\n/** @deprecated Use CollectionService instead. Reference entities are now collections with seed. */\nexport { CollectionService as ReferenceService } from './services/collection.service.js'\n/** @deprecated Use CollectionService with immutable: true instead */\nexport { CollectionService as EventService } from './services/collection.service.js'\n/** @deprecated Use SingleService with scopeField instead */\nexport { SingleService as ConfigService } from './services/single.service.js'\n/** @deprecated Use CollectionService with retention instead */\nexport { CollectionService as TempService } from './services/collection.service.js'\nexport { ViewService } from './services/view.service.js'\nexport { ExternalService } from './services/external.service.js'\n/** @deprecated Use ComputedService with sources instead */\nexport { ComputedService as VirtualService } from './services/computed.service.js'\nexport { ComputedService } from './services/computed.service.js'\n// TreeService and DagService are exported separately to avoid circular dependency issues\n// Import directly from ./services/tree.service.js or ./services/dag.service.js\n\n// Controllers\nexport { createEntityController } from './controllers/entity.controller.js'\n\n// Routes\nexport { createEntityRouter } from './routes/entity.routes.js'\nexport { createActionRouters } from './routes/action.routes.js'\n\n// Validation\nexport { buildCreateSchema, buildUpdateSchema, getSchemas } from './validation/index.js'\n\n// Helpers\nexport * from './helpers/index.js'\n","/**\n * Module execution utilities for seeds and migrations.\n *\n * Centralizes the pattern of running module-defined functions\n * with fallback to auto-detected files.\n *\n * NOTE: TreeService and DagService are lazily imported to avoid circular dependency\n */\n\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport type {\n ModuleManifest,\n CollectionEntityDefinition,\n TreeEntityDefinition,\n DagEntityDefinition,\n SingleEntityDefinition,\n SeedConfig,\n ModuleContext\n} from '@gzl10/nexus-sdk'\n// Import services that don't have circular dependency issues\nimport { CollectionService, SingleService } from '../runtime/index.js'\nimport { getLibPath } from '../config/paths.js'\nimport { logger } from '../core/logger/index.js'\n\n/**\n * Attempts to run a module seed.\n * 1. If mod.seed is defined, uses it directly\n * 2. Otherwise, tries to auto-detect {name}.seed.js in the module directory\n * 3. Auto-seeds 'collection', 'reference', 'tree', 'dag' entities that have seed data defined\n * 4. Auto-creates 'single' entities with defaults if record doesn't exist\n *\n * @returns true if a seed was executed, false otherwise\n */\nexport async function runModuleSeed(mod: ModuleManifest, ctx: ModuleContext): Promise<boolean> {\n let seeded = false\n\n // Seed explícito en manifest\n if (mod.seed) {\n await mod.seed(ctx)\n seeded = true\n } else {\n // Auto-detección: buscar {name}.seed.js en dist/modules/{name}/\n const seedPath = join(getLibPath(), 'dist', 'modules', mod.name, `${mod.name}.seed.js`)\n if (existsSync(seedPath)) {\n const seedModule = await import(pathToFileURL(seedPath).href)\n if (typeof seedModule.seed === 'function') {\n await seedModule.seed(ctx)\n seeded = true\n }\n }\n }\n\n // Auto-seed entities with seed data (collection, reference, tree, dag)\n const entitySeeds = await seedEntitiesWithData(mod, ctx)\n if (entitySeeds > 0) {\n seeded = true\n }\n\n return seeded\n}\n\n/**\n * Auto-seeds entities with data or defaults:\n * - 'collection', 'reference', 'tree', 'dag': entities with seed arrays\n * - 'single': entities with defaults (creates record at startup)\n * Called automatically by runModuleSeed.\n */\nasync function seedEntitiesWithData(mod: ModuleManifest, ctx: ModuleContext): Promise<number> {\n if (!mod.definitions?.length) {\n return 0\n }\n\n let totalSeeded = 0\n\n for (const def of mod.definitions) {\n // Collection/reference entities with seed\n if ((!def.type || def.type === 'collection' || def.type === 'reference') && hasSeedData((def as CollectionEntityDefinition).seed)) {\n const colDef = def as unknown as ConstructorParameters<typeof CollectionService>[1]\n const service = new CollectionService(ctx, colDef)\n const count = await service.seed()\n totalSeeded += count\n }\n\n // Tree entities with seed (lazy import to avoid circular dependency)\n if (def.type === 'tree' && hasSeedData((def as TreeEntityDefinition).seed)) {\n const { TreeService } = await import('../runtime/services/tree.service.js')\n const treeDef = def as unknown as ConstructorParameters<typeof TreeService>[1]\n const service = new TreeService(ctx, treeDef)\n const count = await service.seed()\n totalSeeded += count\n }\n\n // DAG entities with seed (lazy import to avoid circular dependency)\n if (def.type === 'dag' && hasSeedData((def as DagEntityDefinition).seed)) {\n const { DagService } = await import('../runtime/services/dag.service.js')\n const dagDef = def as unknown as ConstructorParameters<typeof DagService>[1]\n const service = new DagService(ctx, dagDef)\n const count = await service.seed()\n totalSeeded += count\n }\n\n // Single entities with defaults - persist to database at startup\n if (def.type === 'single' && (def as SingleEntityDefinition).defaults) {\n const singleDef = def as unknown as ConstructorParameters<typeof SingleService>[1]\n const service = new SingleService(ctx, singleDef)\n const count = await service.seed()\n totalSeeded += count\n }\n }\n\n if (totalSeeded > 0) {\n logger.info({ module: mod.name, seeded: totalSeeded }, 'Entity seeds applied')\n }\n\n return totalSeeded\n}\n\n\n/**\n * Check if seed data is defined (inline array or SeedConfig)\n */\nfunction hasSeedData(seed: Record<string, unknown>[] | SeedConfig | undefined): boolean {\n if (!seed) return false\n // SeedConfig with URL\n if (!Array.isArray(seed) && 'source' in seed && seed.source === 'url') return true\n // Inline array\n return Array.isArray(seed) && seed.length > 0\n}\n","/**\n * Sequence generator for pattern-based IDs.\n *\n * Manages sequential counters stored in the database with support for:\n * - Multiple sequences per entity (scoped by table name)\n * - Periodic resets (yearly, monthly, daily)\n * - Atomic increment operations\n */\n\nimport type { Knex } from 'knex'\n\n/**\n * Table name for storing sequence counters\n */\nexport const SEQUENCES_TABLE = '_nexus_sequences'\n\n/**\n * Pattern configuration for ID generation\n */\nexport interface PatternConfig {\n pattern: string\n prefix?: string\n suffix?: string\n resetOn?: 'never' | 'year' | 'month' | 'day'\n startAt?: number\n}\n\n/**\n * Ensures the sequences table exists in the database.\n */\nexport async function ensureSequencesTable(knex: Knex): Promise<void> {\n const exists = await knex.schema.hasTable(SEQUENCES_TABLE)\n if (!exists) {\n await knex.schema.createTable(SEQUENCES_TABLE, (table) => {\n table.string('id', 100).primary()\n table.string('table_name', 100).notNullable()\n table.string('scope', 20).notNullable() // e.g., '2025', '2025-01', '2025-01-15', or 'global'\n table.integer('current_value').notNullable().defaultTo(0)\n table.timestamp('created_at').defaultTo(knex.fn.now())\n table.timestamp('updated_at').defaultTo(knex.fn.now())\n\n table.unique(['table_name', 'scope'])\n })\n }\n}\n\n/**\n * Get the scope string based on reset configuration.\n */\nfunction getScope(resetOn: PatternConfig['resetOn']): string {\n const now = new Date()\n const year = now.getFullYear()\n const month = String(now.getMonth() + 1).padStart(2, '0')\n const day = String(now.getDate()).padStart(2, '0')\n\n switch (resetOn) {\n case 'year':\n return String(year)\n case 'month':\n return `${year}-${month}`\n case 'day':\n return `${year}-${month}-${day}`\n case 'never':\n default:\n return 'global'\n }\n}\n\n/**\n * Get the next sequence number for a table, atomically incrementing the counter.\n *\n * @param knex - Knex instance\n * @param tableName - Entity table name (used as sequence key)\n * @param config - Pattern configuration\n * @returns Next sequence number\n */\nexport async function getNextSequence(\n knex: Knex,\n tableName: string,\n config: PatternConfig\n): Promise<number> {\n const scope = getScope(config.resetOn)\n const id = `${tableName}:${scope}`\n const startAt = config.startAt ?? 1\n\n // Use transaction for atomic increment\n return await knex.transaction(async (trx) => {\n // Try to get existing sequence\n const existing = await trx(SEQUENCES_TABLE)\n .where('id', id)\n .first()\n\n if (existing) {\n // Increment and return\n const nextValue = existing.current_value + 1\n await trx(SEQUENCES_TABLE)\n .where('id', id)\n .update({\n current_value: nextValue,\n updated_at: trx.fn.now()\n })\n return nextValue\n } else {\n // Create new sequence\n await trx(SEQUENCES_TABLE).insert({\n id,\n table_name: tableName,\n scope,\n current_value: startAt,\n created_at: trx.fn.now(),\n updated_at: trx.fn.now()\n })\n return startAt\n }\n })\n}\n\n/**\n * Generate an ID based on a pattern template.\n *\n * Supported tokens:\n * - {prefix} - Static prefix string\n * - {suffix} - Static suffix string\n * - {year} - Current year (4 digits)\n * - {year2} - Current year (2 digits)\n * - {month} - Current month (01-12)\n * - {day} - Current day (01-31)\n * - {seq} - Sequence number\n * - {seq:N} - Sequence with N-digit zero-padding\n *\n * @param knex - Knex instance\n * @param tableName - Entity table name\n * @param config - Pattern configuration\n * @returns Generated ID string\n *\n * @example\n * ```typescript\n * // Pattern: '{prefix}-{year}-{seq:6}' with prefix: 'TP'\n * // Result: 'TP-2025-000001'\n *\n * // Pattern: '{prefix}{year2}{month}-{seq:4}' with prefix: 'INV'\n * // Result: 'INV2501-0001'\n * ```\n */\nexport async function generatePatternId(\n knex: Knex,\n tableName: string,\n config: PatternConfig\n): Promise<string> {\n const now = new Date()\n const year = String(now.getFullYear())\n const year2 = year.slice(-2)\n const month = String(now.getMonth() + 1).padStart(2, '0')\n const day = String(now.getDate()).padStart(2, '0')\n\n // Get next sequence number\n const seq = await getNextSequence(knex, tableName, config)\n\n // Replace tokens in pattern\n let result = config.pattern\n\n // Replace simple tokens\n result = result.replace(/\\{prefix\\}/g, config.prefix ?? '')\n result = result.replace(/\\{suffix\\}/g, config.suffix ?? '')\n result = result.replace(/\\{year\\}/g, year)\n result = result.replace(/\\{year2\\}/g, year2)\n result = result.replace(/\\{month\\}/g, month)\n result = result.replace(/\\{day\\}/g, day)\n\n // Replace sequence tokens with padding\n result = result.replace(/\\{seq:(\\d+)\\}/g, (_match, padding) => {\n const padLength = parseInt(padding, 10)\n return String(seq).padStart(padLength, '0')\n })\n\n // Replace simple sequence token\n result = result.replace(/\\{seq\\}/g, String(seq))\n\n return result\n}\n\n","import type { Knex } from 'knex'\nimport { ensureSequencesTable } from '../core/utils/sequence.js'\n\n/**\n * Ensures system infrastructure tables exist.\n * Runs BEFORE module migrations.\n */\nexport async function ensureSystemTables(db: Knex): Promise<void> {\n // single_records: almacena entities tipo \"single\" (singletons)\n if (!await db.schema.hasTable('single_records')) {\n await db.schema.createTable('single_records', (table) => {\n table.string('id').primary()\n table.string('key').notNullable().unique()\n table.text('value')\n table.timestamp('created_at').defaultTo(db.fn.now())\n table.timestamp('updated_at').defaultTo(db.fn.now())\n table.string('created_by').nullable()\n table.string('updated_by').nullable()\n })\n }\n\n // _nexus_sequences: almacena contadores para pattern IDs\n await ensureSequencesTable(db)\n\n // _nexus_migrations and _nexus_migration_lock: migration tracking\n await ensureMigrationTables(db)\n}\n\n/**\n * Ensures migration tracking tables exist.\n */\nexport async function ensureMigrationTables(db: Knex): Promise<void> {\n // _nexus_migrations: tracks executed migrations\n if (!(await db.schema.hasTable('_nexus_migrations'))) {\n await db.schema.createTable('_nexus_migrations', (table) => {\n table.string('id', 26).primary()\n table.string('name', 255).notNullable().unique()\n table.integer('batch').notNullable()\n table.string('status', 20).notNullable() // 'running' | 'completed' | 'failed' | 'rolled_back'\n table.string('source', 100).notNullable().defaultTo('project') // 'core' | 'plugin:{name}' | 'project'\n table.timestamp('executed_at').nullable()\n table.timestamp('rolled_back_at').nullable()\n table.integer('execution_time_ms').nullable()\n table.text('error').nullable()\n table.timestamp('created_at').defaultTo(db.fn.now())\n table.timestamp('updated_at').defaultTo(db.fn.now())\n\n table.index(['batch'])\n table.index(['status'])\n table.index(['source'])\n table.index(['executed_at'])\n })\n } else {\n // Backwards compat: add source column if missing (pre-multisource databases)\n const hasSource = await db.schema.hasColumn('_nexus_migrations', 'source')\n if (!hasSource) {\n await db.schema.alterTable('_nexus_migrations', (table) => {\n table.string('source', 100).notNullable().defaultTo('project')\n })\n }\n }\n\n // _nexus_migration_lock: prevents concurrent migration execution\n if (!(await db.schema.hasTable('_nexus_migration_lock'))) {\n await db.schema.createTable('_nexus_migration_lock', (table) => {\n table.integer('id').primary().defaultTo(1)\n table.boolean('is_locked').defaultTo(false)\n table.timestamp('locked_at').nullable()\n table.string('locked_by', 100).nullable() // PID of the process\n\n // Ensure only one row exists\n table.check('?? = 1', ['id'])\n })\n\n // Insert the single lock row\n await db('_nexus_migration_lock').insert({\n id: 1,\n is_locked: false,\n })\n }\n}\n","import { getCoreMigrationsDir, getProjectMigrationsDir } from '../config/paths.js'\nimport { moduleStore } from '../engine/module-store.js'\n\n/**\n * Migration source identifier.\n * - 'core': nexus-backend core modules\n * - 'plugin:{name}': plugin-owned migrations\n * - 'project': user project modules\n */\nexport type MigrationSourceId = 'core' | `plugin:${string}` | 'project'\n\n/**\n * Describes a migration source with its directory and file prefix.\n */\nexport interface MigrationSourceDescriptor {\n id: MigrationSourceId\n dir: string\n /** File prefix for this source (e.g. 'core__', 'cms__', '' for project) */\n prefix: string\n}\n\n/**\n * Build ordered migration sources: core → plugins (topological) → project.\n * Each source has a directory and a file prefix for collision avoidance.\n */\nexport function buildMigrationSources(): MigrationSourceDescriptor[] {\n const sources: MigrationSourceDescriptor[] = [\n { id: 'core', dir: getCoreMigrationsDir(), prefix: 'core__' },\n ]\n\n for (const [name, plugin] of moduleStore.plugins) {\n if (!plugin.migrationsDir) continue\n sources.push({\n id: `plugin:${name}`,\n dir: plugin.migrationsDir,\n prefix: `${plugin.code}__`,\n })\n }\n\n sources.push({ id: 'project', dir: getProjectMigrationsDir(), prefix: '' })\n return sources\n}\n\n","import { z } from 'zod'\nimport type { ResolvedConfig } from './types.js'\n\nconst envSchema = z.object({\n NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),\n PORT: z.coerce.number().default(3000),\n CORS_ORIGIN: z.string().default('*'),\n BACKEND_URL: z.string().optional(),\n DATABASE_URL: z.string().default('file:./dev.db'),\n REDIS_URL: z.string().url().optional(),\n REDIS_PREFIX: z.string().default('nexus'),\n ADMIN_EMAIL: z.string().email().optional(),\n ADMIN_PASSWORD: z.string().min(6).optional(),\n COOKIE_DOMAIN: z.string().optional(),\n TZ: z.string().default('UTC'),\n TRUST_PROXY: z.coerce.boolean().default(false),\n NEXUS_UI_ENABLED: z.coerce.boolean().default(true),\n NEXUS_UI_BASE: z.string().default('/'),\n NEXUS_UI_PATH: z.string().default('../ui/dist'),\n NEXUS_CHECK_DRIFT: z.coerce.boolean().optional(),\n NEXUS_FAIL_ON_DRIFT: z.coerce.boolean().optional(),\n FRPC_SERVER: z.string().optional(),\n FRPC_SERVER_PORT: z.coerce.number().default(7000),\n FRPC_TOKEN: z.string().optional(),\n FRPC_SUBDOMAIN: z.string().optional()\n})\n\n// Parse env vars. Re-parsed on each resolveConfig() to pick up runtime changes (tests).\nlet env = envSchema.parse(process.env)\nexport { env }\n\n// Aplicar TZ inmediatamente al cargar el módulo\n// UTC por defecto para consistencia con bases de datos\nprocess.env['TZ'] = env.TZ\n\n// Configuración global resuelta\nlet resolvedConfig: ResolvedConfig | null = null\n\nexport function resolveConfig(): ResolvedConfig {\n // Re-parse env to pick up changes made after module load (e.g. tests setting process.env.PORT)\n env = envSchema.parse(process.env)\n process.env['TZ'] = env.TZ\n\n resolvedConfig = {\n nodeEnv: env.NODE_ENV,\n port: env.PORT,\n host: '0.0.0.0',\n ui: {\n enabled: env.NEXUS_UI_ENABLED,\n base: env.NEXUS_UI_BASE,\n path: env.NEXUS_UI_PATH\n },\n corsOrigin: env.CORS_ORIGIN,\n databaseUrl: env.DATABASE_URL,\n adminEmail: env.ADMIN_EMAIL,\n adminPassword: env.ADMIN_PASSWORD,\n timezone: env.TZ\n }\n\n return resolvedConfig\n}\n\nexport function getConfig(): ResolvedConfig {\n if (!resolvedConfig) {\n return resolveConfig()\n }\n return resolvedConfig\n}\n\nexport function resetConfig(): void {\n resolvedConfig = null\n}\n","/**\n * Database configuration.\n *\n * Builds Knex configuration based on DATABASE_URL.\n * Supports: SQLite, PostgreSQL, MySQL.\n */\n\nimport type { Knex } from 'knex'\nimport { join, dirname, isAbsolute } from 'path'\nimport { mkdirSync } from 'fs'\nimport { getConfig } from './env.js'\nimport { getProjectPath } from './paths.js'\nimport { sqlitePostProcess } from '../db/sqlite-compat.js'\n\n/** Matches all in-memory URL variants: :memory:, file::memory:, sqlite::memory: */\nconst IN_MEMORY_RE = /^((file:|sqlite:)?:memory:?)$/\n\n/**\n * Builds Knex configuration based on DATABASE_URL\n */\nexport function getDatabaseConfig(): Knex.Config {\n const url = getConfig().databaseUrl\n\n // Normalize all in-memory variants to canonical form\n if (IN_MEMORY_RE.test(url)) {\n return {\n client: 'better-sqlite3',\n connection: { filename: ':memory:' },\n useNullAsDefault: true,\n postProcessResponse: sqlitePostProcess,\n pool: { min: 0, max: 1 }\n }\n }\n\n // SQLite file-based\n if (url.startsWith('file:') || url.startsWith('sqlite:')) {\n let filename = url.replace(/^(file:|sqlite:)/, '')\n\n // Rutas relativas se resuelven desde getProjectPath()/data/\n if (!isAbsolute(filename)) {\n filename = join(getProjectPath(), 'data', filename)\n }\n // Asegurar que el directorio existe\n mkdirSync(dirname(filename), { recursive: true })\n return {\n client: 'better-sqlite3',\n connection: { filename },\n useNullAsDefault: true,\n postProcessResponse: sqlitePostProcess\n }\n }\n\n // PostgreSQL\n if (url.startsWith('postgresql://') || url.startsWith('postgres://')) {\n return {\n client: 'pg',\n connection: url,\n pool: { min: 2, max: 10 }\n }\n }\n\n // MySQL\n if (url.startsWith('mysql://')) {\n // Calcular offset del timezone actual (process.env.TZ ya aplicado)\n const offsetMinutes = new Date().getTimezoneOffset()\n const offsetHours = Math.abs(Math.floor(offsetMinutes / 60))\n const offsetMins = Math.abs(offsetMinutes % 60)\n const sign = offsetMinutes <= 0 ? '+' : '-'\n const tzOffset = `${sign}${String(offsetHours).padStart(2, '0')}:${String(offsetMins).padStart(2, '0')}`\n\n return {\n client: 'mysql2',\n connection: {\n uri: url,\n timezone: tzOffset // mysql2 requiere formato \"+HH:MM\"\n },\n pool: { min: 2, max: 10 }\n }\n }\n\n throw new Error(`Unsupported database URL: ${url}`)\n}\n\n/**\n * Gets the database type from DATABASE_URL\n */\nexport function getDatabaseType(): 'sqlite' | 'postgresql' | 'mysql' {\n const url = getConfig().databaseUrl\n if (IN_MEMORY_RE.test(url) || url.startsWith('file:') || url.startsWith('sqlite:')) return 'sqlite'\n if (url.startsWith('postgresql://') || url.startsWith('postgres://')) return 'postgresql'\n if (url.startsWith('mysql://')) return 'mysql'\n return 'sqlite'\n}\n\n/**\n * Gets the database path/URL (credentials masked for display)\n */\nexport function getDatabasePath(): string {\n const url = getConfig().databaseUrl\n if (IN_MEMORY_RE.test(url)) return ':memory:'\n if (url.startsWith('file:') || url.startsWith('sqlite:')) {\n let filename = url.replace(/^(file:|sqlite:)/, '')\n if (!isAbsolute(filename)) {\n filename = join(getProjectPath(), 'data', filename)\n }\n return filename\n }\n // Para PostgreSQL/MySQL, ocultar credenciales\n try {\n const parsed = new URL(url)\n parsed.password = '***'\n return parsed.toString()\n } catch {\n return url.replace(/:\\/\\/[^@]+@/, '://***@')\n }\n}\n","import type { Knex } from 'knex'\nimport { nexusEvents } from '../core/index.js'\nimport { logger } from '../core/logger/index.js'\nimport {\n extractTableFromInsert,\n extractTableFromUpdate,\n extractTableFromDelete,\n extractTableFromSelect\n} from './sql-utils.js'\n\n// Tablas a ignorar (internas del sistema)\nconst IGNORED_TABLES = new Set(['refresh_tokens', 'knex_migrations', 'knex_migrations_lock'])\n\n/** Default slow query threshold in ms */\nconst DEFAULT_SLOW_QUERY_MS = 500\n\nexport interface DbEventPayload {\n table: string\n action: 'created' | 'updated' | 'deleted'\n data: unknown\n timestamp: Date\n}\n\nexport function setupQueryInterceptor(knexInstance: Knex): Knex {\n const queryTimings = new Map<string, number>()\n const slowQueryMs = Number(process.env['NEXUS_SLOW_QUERY_MS']) || DEFAULT_SLOW_QUERY_MS\n\n // Track query start time\n knexInstance.on('query', (query) => {\n if (query.__knexQueryUid) {\n queryTimings.set(query.__knexQueryUid, Date.now())\n }\n })\n\n // Interceptar eventos de query para emitir eventos CRUD + slow query detection\n knexInstance.on('query-response', (response, query) => {\n const sql = query.sql?.toLowerCase() ?? ''\n\n // Slow query detection\n const startTime = queryTimings.get(query.__knexQueryUid)\n if (startTime) {\n queryTimings.delete(query.__knexQueryUid)\n const duration = Date.now() - startTime\n if (duration >= slowQueryMs) {\n const table = extractTableFromSelect(sql)\n ?? extractTableFromInsert(sql)\n ?? extractTableFromUpdate(sql)\n ?? extractTableFromDelete(sql)\n logger.warn({ sql: query.sql?.substring(0, 200), duration, table }, 'Slow query detected')\n }\n }\n\n // Detectar tipo de operación desde SQL\n let action: string | undefined\n let table: string | undefined\n\n if (sql.startsWith('insert into')) {\n action = 'created'\n table = extractTableFromInsert(sql)\n } else if (sql.startsWith('update')) {\n action = 'updated'\n table = extractTableFromUpdate(sql)\n } else if (sql.startsWith('delete from')) {\n action = 'deleted'\n table = extractTableFromDelete(sql)\n }\n\n // Emitir evento si es operación CRUD válida\n if (action && table && !IGNORED_TABLES.has(table)) {\n nexusEvents.emit(`db.${table}.${action}`, {\n table,\n action,\n data: response,\n timestamp: new Date()\n } as DbEventPayload)\n }\n })\n\n // Cleanup on query error to prevent memory leaks\n knexInstance.on('query-error', (_error, query) => {\n queryTimings.delete(query.__knexQueryUid)\n })\n\n return knexInstance\n}\n","/**\n * Database connection management.\n *\n * Handles Knex instance lifecycle: initialization, access, and cleanup.\n */\n\nimport knex, { type Knex } from 'knex'\nimport { getDatabaseConfig } from '../config/database.js'\nimport { setupQueryInterceptor } from './query-interceptor.js'\n\nconst globalForKnex = globalThis as unknown as { db: Knex | undefined }\n\n/**\n * Initializes or reinitializes the DB connection\n */\nexport function initDb(): Knex {\n if (!globalForKnex.db) {\n const knexInstance = knex(getDatabaseConfig())\n globalForKnex.db = setupQueryInterceptor(knexInstance)\n }\n return globalForKnex.db\n}\n\n/**\n * Gets the current DB instance (lazy init).\n * Use instead of `db` to support restarts.\n */\nexport function getDb(): Knex {\n return initDb()\n}\n\n/**\n * @deprecated Use getDb() to support restart.\n * Proxy provides lazy initialization to avoid circular imports at module load time.\n */\n \nexport const db: Knex = new Proxy({} as Knex, {\n get(_, prop) { return (getDb() as unknown as Record<string | symbol, unknown>)[prop] }\n})\n\n/**\n * Closes the DB connection (for stop/cleanup)\n */\nexport async function destroyDb(): Promise<void> {\n if (globalForKnex.db) {\n await globalForKnex.db.destroy()\n globalForKnex.db = undefined\n }\n}\n","import type { Knex } from 'knex'\nimport { logger } from '../core/logger/index.js'\n\n/**\n * Check if a process is running.\n */\nfunction isProcessRunning(pid: number): boolean {\n try {\n // Signal 0 doesn't kill the process, just checks if it exists\n process.kill(pid, 0)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Acquire migration lock.\n * Prevents concurrent migration executions.\n */\nexport async function acquireMigrationLock(\n knex: Knex,\n options: { timeout?: number } = {}\n): Promise<void> {\n const timeout = options.timeout ?? 60000\n const startTime = Date.now()\n const _log = logger\n\n while (true) {\n try {\n // Try to acquire lock with SELECT FOR UPDATE\n const acquired = await knex.transaction(async (trx) => {\n const row = await trx('_nexus_migration_lock').where({ id: 1 }).forUpdate().first()\n\n if (!row) {\n // Create lock for the first time\n await trx('_nexus_migration_lock').insert({\n id: 1,\n is_locked: true,\n locked_at: trx.fn.now(),\n locked_by: String(process.pid),\n })\n return true\n }\n\n if (!row.is_locked) {\n // Lock available\n await trx('_nexus_migration_lock').where({ id: 1 }).update({\n is_locked: true,\n locked_at: trx.fn.now(),\n locked_by: String(process.pid),\n })\n return true\n }\n\n // Lock occupied - check if process is still alive\n const lockedPid = parseInt(row.locked_by ?? '0', 10)\n if (lockedPid && !isProcessRunning(lockedPid)) {\n // Process died - release lock automatically\n logger.warn({ pid: lockedPid }, 'Stale lock detected, releasing')\n await trx('_nexus_migration_lock').where({ id: 1 }).update({\n is_locked: false,\n locked_by: null,\n })\n return false // Retry\n }\n\n return false\n })\n\n if (acquired) {\n logger.debug({ pid: process.pid }, 'Migration lock acquired')\n return\n }\n } catch (err) {\n // Lock timeout or error - will retry\n logger.debug({ err }, 'Failed to acquire lock, retrying')\n }\n\n // Check timeout before sleeping to avoid unnecessary delay\n if (Date.now() - startTime >= timeout) {\n throw new Error(\n 'Migration lock timeout - another process is running migrations. ' +\n 'If no other process is running, check for stale locks in _nexus_migration_lock table.'\n )\n }\n\n // Wait before retrying\n await new Promise((resolve) => setTimeout(resolve, 1000))\n }\n}\n\n/**\n * Release migration lock.\n */\nexport async function releaseMigrationLock(knex: Knex): Promise<void> {\n const _log = logger\n\n try {\n await knex('_nexus_migration_lock').where({ id: 1 }).update({\n is_locked: false,\n locked_by: null,\n })\n\n logger.debug({ pid: process.pid }, 'Migration lock released')\n } catch (err) {\n logger.error({ err }, 'Failed to release migration lock')\n // Don't throw - we're likely in a finally block\n }\n}\n","import type { Knex } from 'knex'\nimport path from 'node:path'\nimport fs from 'node:fs/promises'\nimport { generateId } from '../core/utils/id.js'\nimport { getDb } from './connection.js'\nimport { logger } from '../core/logger/index.js'\nimport { getMigrationsDir } from '../config/paths.js'\nimport type { Logger } from 'pino'\nimport { acquireMigrationLock, releaseMigrationLock } from './migration-lock.js'\nimport { buildMigrationSources, type MigrationSourceDescriptor, type MigrationSourceId } from './migration-sources.js'\n\n/**\n * Migration file interface.\n * Each migration file must export up() and down() functions.\n */\nexport interface MigrationFile {\n name: string\n filepath: string\n source: MigrationSourceId\n up: (knex: Knex) => Promise<void>\n down: (knex: Knex) => Promise<void>\n}\n\n/**\n * Migration record in _nexus_migrations table.\n */\nexport interface MigrationRecord {\n id: string\n name: string\n batch: number\n status: 'running' | 'completed' | 'failed' | 'rolled_back'\n source: MigrationSourceId\n executed_at: Date | null\n rolled_back_at: Date | null\n execution_time_ms: number | null\n error: string | null\n created_at: Date\n updated_at: Date\n}\n\n/**\n * Load migration files from all sources (core → plugins → project).\n * Validates naming conventions and detects name collisions.\n */\nexport async function loadAllMigrationFiles(\n sources?: MigrationSourceDescriptor[]\n): Promise<MigrationFile[]> {\n const effectiveSources = sources ?? buildMigrationSources()\n const allMigrations: MigrationFile[] = []\n const nameSet = new Set<string>()\n\n // Collect all non-empty prefixes to filter project source in dev mode\n // (when core/plugin dirs overlap with project dir)\n const reservedPrefixes = effectiveSources\n .filter(s => s.prefix)\n .map(s => s.prefix)\n\n for (const source of effectiveSources) {\n const files = await loadMigrationFilesFromDir(source.dir, source.id)\n\n for (const migration of files) {\n // Non-project sources: skip files that don't match their prefix\n if (source.prefix && !migration.name.startsWith(source.prefix)) {\n logger.warn(\n { file: migration.name, source: source.id, expectedPrefix: source.prefix },\n 'Migration file name does not match expected prefix — skipping'\n )\n continue\n }\n\n // Project source: skip files that belong to core/plugin sources (by prefix)\n if (!source.prefix && reservedPrefixes.some(p => migration.name.startsWith(p))) {\n continue\n }\n\n // Detect name collisions across sources\n if (nameSet.has(migration.name)) {\n throw new Error(\n `Migration name collision: \"${migration.name}\" exists in multiple sources. ` +\n `Each migration name must be globally unique.`\n )\n }\n\n nameSet.add(migration.name)\n allMigrations.push(migration)\n }\n }\n\n return allMigrations\n}\n\n/**\n * Load migration files from a single directory.\n */\nasync function loadMigrationFilesFromDir(\n dir: string,\n source: MigrationSourceId\n): Promise<MigrationFile[]> {\n try {\n await fs.access(dir)\n } catch {\n return []\n }\n\n const files = await fs.readdir(dir)\n const migrations: MigrationFile[] = []\n\n for (const file of files.sort()) {\n if (!file.endsWith('.ts') && !file.endsWith('.js')) continue\n\n const name = file.replace(/\\.(ts|js)$/, '')\n const filepath = path.join(dir, file)\n\n try {\n const module = await import(filepath)\n\n if (typeof module.up !== 'function' || typeof module.down !== 'function') {\n throw new Error(`Invalid migration file: ${file}. Must export up() and down()`)\n }\n\n migrations.push({\n name,\n filepath,\n source,\n up: module.up,\n down: module.down,\n })\n } catch (err) {\n logger.error({ file, source, err }, 'Failed to load migration file')\n throw err\n }\n }\n\n return migrations\n}\n\n/**\n * Run all pending migrations from files (multi-source).\n * @param knexInstance - Optional Knex instance (uses getDb() if not provided)\n * @param sources - Optional migration sources (uses buildMigrationSources() if not provided)\n */\nexport async function runMigrations(\n knexInstance?: Knex,\n sources?: MigrationSourceDescriptor[]\n): Promise<void> {\n const knex = knexInstance ?? getDb()\n\n await acquireMigrationLock(knex, { timeout: 60000 })\n\n try {\n const migrationFiles = await loadAllMigrationFiles(sources)\n\n // Get already-executed migration names\n const executedMigrations = await knex('_nexus_migrations')\n .where({ status: 'completed' })\n .select('name')\n .then((rows) => new Set(rows.map((r) => r.name)))\n\n const pendingMigrations = migrationFiles.filter((m) => !executedMigrations.has(m.name))\n\n if (pendingMigrations.length === 0) {\n logger.debug('No pending migrations')\n return\n }\n\n const batch = await getNextBatch(knex)\n\n logger.info({ count: pendingMigrations.length, batch }, 'Running migrations')\n\n for (const migration of pendingMigrations) {\n await runMigration(knex, migration, batch, logger)\n }\n\n const total = await getCompletedCount(knex)\n logger.info({ batch, total }, 'Migrations completed')\n } finally {\n await releaseMigrationLock(knex)\n }\n}\n\n/**\n * Run a single migration.\n */\nasync function runMigration(\n knex: Knex,\n migration: MigrationFile,\n batch: number,\n logger: Logger\n): Promise<void> {\n const startTime = Date.now()\n\n // Check for existing record (rolled_back or failed) to avoid UNIQUE constraint\n const existing = await knex('_nexus_migrations')\n .where({ name: migration.name })\n .whereIn('status', ['rolled_back', 'failed'])\n .first()\n\n let id: string\n if (existing) {\n id = existing.id\n await knex('_nexus_migrations').where({ id }).update({\n batch,\n status: 'running',\n source: migration.source,\n error: null,\n updated_at: knex.fn.now(),\n })\n } else {\n id = generateId()\n await knex('_nexus_migrations').insert({\n id,\n name: migration.name,\n batch,\n status: 'running',\n source: migration.source,\n created_at: knex.fn.now(),\n updated_at: knex.fn.now(),\n })\n }\n\n try {\n await knex.transaction(async (trx) => {\n await migration.up(trx)\n })\n\n const executionTime = Date.now() - startTime\n await knex('_nexus_migrations').where({ id }).update({\n status: 'completed',\n executed_at: knex.fn.now(),\n execution_time_ms: executionTime,\n updated_at: knex.fn.now(),\n })\n\n logger.info({ migration: migration.name, source: migration.source, time: executionTime }, 'Migration applied')\n } catch (err) {\n await knex('_nexus_migrations').where({ id }).update({\n status: 'failed',\n error: err instanceof Error ? err.message : String(err),\n updated_at: knex.fn.now(),\n })\n\n logger.error({ migration: migration.name, source: migration.source, err }, 'Migration failed')\n throw err\n }\n}\n\n/**\n * Rollback last batch of migrations.\n * Loads from all sources to find down() functions.\n * @param knexInstance - Optional Knex instance (uses getDb() if not provided)\n * @param source - Optional source filter (e.g. 'core', 'plugin:@gzl10/nexus-plugin-tags', 'project')\n * @param sources - Optional migration sources for loading files (uses buildMigrationSources() if not provided)\n */\nexport async function rollbackLastBatch(knexInstance?: Knex, source?: MigrationSourceId, sources?: MigrationSourceDescriptor[]): Promise<void> {\n const knex = knexInstance ?? getDb()\n\n await acquireMigrationLock(knex)\n\n try {\n const maxQuery = knex('_nexus_migrations')\n .max('batch as batch')\n .where({ status: 'completed' })\n if (source) maxQuery.where({ source })\n\n const result = await maxQuery.first()\n const lastBatch = result?.['batch']\n\n if (!lastBatch) {\n logger.info(source ? { source } : {}, 'No migrations to rollback')\n return\n }\n\n const batchQuery = knex('_nexus_migrations')\n .where({ batch: lastBatch, status: 'completed' })\n .orderBy('executed_at', 'desc')\n if (source) batchQuery.where({ source })\n\n const migrations = await batchQuery.select<MigrationRecord[]>()\n\n logger.info({ batch: lastBatch, count: migrations.length, ...(source && { source }) }, 'Rolling back batch')\n\n // Load from all sources to find down() functions\n const migrationFiles = await loadAllMigrationFiles(sources)\n const migrationMap = new Map(migrationFiles.map((m) => [m.name, m]))\n\n for (const record of migrations) {\n const migration = migrationMap.get(record.name)\n if (!migration) {\n logger.warn({ migration: record.name }, 'Migration file not found, marking as rolled back')\n await knex('_nexus_migrations').where({ id: record.id }).update({\n status: 'rolled_back',\n rolled_back_at: knex.fn.now(),\n updated_at: knex.fn.now(),\n })\n continue\n }\n\n await rollbackMigration(knex, migration, record, logger)\n }\n } finally {\n await releaseMigrationLock(knex)\n }\n}\n\n/**\n * Rollback a specific migration by name.\n * @param name - Migration name (e.g. '20240315_001_create_users')\n * @param knexInstance - Optional Knex instance (uses getDb() if not provided)\n */\nexport async function rollbackByName(name: string, knexInstance?: Knex): Promise<void> {\n const knex = knexInstance ?? getDb()\n\n await acquireMigrationLock(knex)\n\n try {\n const record = await knex('_nexus_migrations')\n .where({ name, status: 'completed' })\n .first<MigrationRecord>()\n\n if (!record) {\n throw new Error(`Migration \"${name}\" not found or not in completed state`)\n }\n\n const migrationFiles = await loadAllMigrationFiles()\n const migration = migrationFiles.find(m => m.name === name)\n if (!migration) {\n throw new Error(`Migration file \"${name}\" not found in any source`)\n }\n\n logger.info({ migration: name }, 'Rolling back by name')\n await rollbackMigration(knex, migration, record, logger)\n } finally {\n await releaseMigrationLock(knex)\n }\n}\n\n/**\n * Rollback a single migration.\n */\nasync function rollbackMigration(\n knex: Knex,\n migration: MigrationFile,\n record: MigrationRecord,\n logger: Logger\n): Promise<void> {\n try {\n await knex.transaction(async (trx) => {\n await migration.down(trx)\n })\n\n await knex('_nexus_migrations').where({ id: record.id }).update({\n status: 'rolled_back',\n rolled_back_at: knex.fn.now(),\n updated_at: knex.fn.now(),\n })\n\n logger.info({ migration: migration.name }, 'Migration rolled back')\n } catch (err) {\n logger.error({ migration: migration.name, err }, 'Rollback failed')\n throw err\n }\n}\n\n/**\n * Load migration files from the project migrations directory only.\n * @deprecated Use loadAllMigrationFiles() for multi-source support.\n */\nexport async function loadMigrationFiles(): Promise<MigrationFile[]> {\n return loadMigrationFilesFromDir(getMigrationsDir(), 'project')\n}\n\n/**\n * Get next batch number.\n */\nasync function getNextBatch(knex: Knex): Promise<number> {\n const result = await knex('_nexus_migrations').max('batch as batch').first()\n return (result?.['batch'] ?? 0) + 1\n}\n\n/**\n * Get count of completed migrations.\n */\nasync function getCompletedCount(knex: Knex): Promise<number> {\n const result = await knex('_nexus_migrations')\n .where({ status: 'completed' })\n .count('* as count')\n .first()\n return Number(result?.['count'] ?? 0)\n}\n\n/**\n * Show migration status (multi-source aware).\n */\nexport interface MigrationStatus {\n completed: Array<{ name: string; batch: number; executionTime: number | null; status: string; source: string }>\n pending: Array<{ name: string; source: string }>\n bySource: Map<string, Array<{ name: string; status: string; batch?: number; executionTime?: number | null }>>\n total: number\n totalFiles: number\n}\n\nexport async function showMigrationStatus(knexInstance?: Knex, sources?: MigrationSourceDescriptor[]): Promise<MigrationStatus> {\n const knex = knexInstance ?? getDb()\n\n // Load from all sources\n const migrationFiles = await loadAllMigrationFiles(sources)\n\n // Load executed migrations\n const executedMigrations = await knex('_nexus_migrations')\n .select<MigrationRecord[]>()\n .orderBy('executed_at', 'desc')\n\n const executedMap = new Map(executedMigrations.map((m) => [m.name, m]))\n\n if (migrationFiles.length === 0) {\n return { completed: [], pending: [], bySource: new Map(), total: 0, totalFiles: 0 }\n }\n\n // Group by source\n const filesBySource = new Map<string, MigrationFile[]>()\n for (const m of migrationFiles) {\n const list = filesBySource.get(m.source) ?? []\n list.push(m)\n filesBySource.set(m.source, list)\n }\n\n const pending: Array<{ name: string; source: string }> = []\n const completed: Array<{ name: string; batch: number; executionTime: number | null; status: string; source: string }> = []\n const bySource = new Map<string, Array<{ name: string; status: string; batch?: number; executionTime?: number | null }>>()\n\n for (const [source, files] of filesBySource) {\n const sourceRows: Array<{ name: string; status: string; batch?: number; executionTime?: number | null }> = []\n\n for (const m of files) {\n const record = executedMap.get(m.name)\n if (!record || record.status !== 'completed') {\n pending.push({ name: m.name, source })\n sourceRows.push({ name: m.name, status: 'pending' })\n } else {\n completed.push({\n name: m.name,\n batch: record.batch,\n executionTime: record.execution_time_ms,\n status: record.status,\n source,\n })\n sourceRows.push({ name: m.name, status: record.status, batch: record.batch, executionTime: record.execution_time_ms })\n }\n }\n\n bySource.set(source, sourceRows)\n }\n\n const total = await getCompletedCount(knex)\n\n return { completed, pending, bySource, total, totalFiles: migrationFiles.length }\n}\n\n/**\n * Rollback ALL completed migrations from a source (all batches).\n * Does NOT re-run migrations afterwards — tables are dropped.\n * @param knexInstance - Optional Knex instance (uses getDb() if not provided)\n * @param source - Source filter (e.g. 'core', 'plugin:@gzl10/nexus-plugin-tags', 'project')\n * @param sources - Optional migration sources for loading files (uses buildMigrationSources() if not provided)\n */\nexport async function rollbackAll(knexInstance?: Knex, source?: MigrationSourceId, sources?: MigrationSourceDescriptor[]): Promise<void> {\n const knex = knexInstance ?? getDb()\n\n if (process.env['NODE_ENV'] === 'production') {\n throw new Error('Cannot rollback all in production')\n }\n\n logger.info(source ? { source } : {}, 'Rolling back all migrations...')\n\n while (true) {\n const maxQuery = knex('_nexus_migrations')\n .max('batch as batch')\n .where({ status: 'completed' })\n if (source) maxQuery.where({ source })\n\n const result = await maxQuery.first()\n if (!result?.['batch']) break\n\n await rollbackLastBatch(knex, source, sources)\n }\n\n logger.info(source ? { source } : {}, 'All migrations rolled back')\n}\n\n/**\n * Reset database (dev only).\n * Rollback all migrations and re-run.\n * @param knexInstance - Optional Knex instance (uses getDb() if not provided)\n * @param source - Optional source filter to reset only migrations from a specific source\n */\nexport async function resetDatabase(knexInstance?: Knex, source?: MigrationSourceId): Promise<void> {\n const knex = knexInstance ?? getDb()\n\n if (process.env['NODE_ENV'] === 'production') {\n throw new Error('Cannot reset database in production')\n }\n\n logger.info(source ? { source } : {}, 'Resetting database...')\n\n await rollbackAll(knex, source)\n\n // Re-run migrations (filtered by source if provided)\n if (source) {\n const allSources = buildMigrationSources()\n const filtered = allSources.filter(s => s.id === source)\n if (filtered.length === 0) {\n throw new Error(`Migration source \"${source}\" not found. Available: ${allSources.map(s => s.id).join(', ')}`)\n }\n await runMigrations(knex, filtered)\n } else {\n await runMigrations(knex)\n }\n\n logger.info('Database reset complete')\n}\n\n/**\n * Undo the last completed migration: rollback + delete file + delete DB record.\n * DEV ONLY — refuses to run in production.\n */\nexport async function undoLastMigration(\n knexInstance?: Knex,\n source?: MigrationSourceId,\n sources?: MigrationSourceDescriptor[]\n): Promise<{ name: string; filepath: string } | null> {\n if (process.env['NODE_ENV'] === 'production') {\n throw new Error('Cannot undo migrations in production')\n }\n\n const knex = knexInstance ?? getDb()\n\n await acquireMigrationLock(knex)\n\n try {\n // Find the last completed migration (optionally filtered by source)\n const query = knex('_nexus_migrations')\n .where({ status: 'completed' })\n if (source) query.where({ source })\n const record = await query\n .orderBy('executed_at', 'desc')\n .orderBy('batch', 'desc')\n .first<MigrationRecord>()\n\n if (!record) {\n logger.info(source ? { source } : {}, 'No migrations to undo')\n return null\n }\n\n // Load migration files to find the down() function and filepath\n const migrationFiles = await loadAllMigrationFiles(sources)\n const migration = migrationFiles.find(m => m.name === record.name)\n\n if (migration) {\n // Execute down() in transaction\n await knex.transaction(async (trx) => {\n await migration.down(trx)\n })\n logger.info({ migration: record.name }, 'Migration rolled back')\n\n // Delete the file from disk (tolerate already-deleted)\n try {\n await fs.unlink(migration.filepath)\n logger.info({ filepath: migration.filepath }, 'Migration file deleted')\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err\n logger.warn({ filepath: migration.filepath }, 'Migration file already deleted')\n }\n } else {\n logger.warn({ migration: record.name }, 'Migration file not found on disk — deleting record only')\n }\n\n // Delete the DB record entirely (not just mark as rolled_back)\n await knex('_nexus_migrations').where({ id: record.id }).delete()\n logger.info({ migration: record.name }, 'Migration record deleted')\n\n return { name: record.name, filepath: migration?.filepath ?? 'unknown' }\n } finally {\n await releaseMigrationLock(knex)\n }\n}\n","import type { Knex } from 'knex'\n\n/**\n * Supported database types.\n */\nexport type DatabaseType = 'sqlite' | 'postgresql' | 'mysql'\n\n/**\n * Column definition from database.\n */\nexport interface ColumnInfo {\n name: string\n type: string\n nullable: boolean\n defaultValue: string | null\n primaryKey: boolean\n}\n\n/**\n * Table definition from database.\n */\nexport interface TableInfo {\n name: string\n columns: Map<string, ColumnInfo>\n indexes: Array<{ name: string; columns: string[]; unique: boolean }>\n foreignKeys: Array<{\n column: string\n referencedTable: string\n referencedColumn: string\n onDelete?: string\n onUpdate?: string\n }>\n}\n\n/**\n * Database schema.\n */\nexport interface DatabaseSchema {\n tables: Map<string, TableInfo>\n}\n\n/**\n * Detect database type from Knex client.\n */\nexport function detectDatabaseType(knex: Knex): DatabaseType {\n const client = knex.client.config.client\n if (client === 'better-sqlite3' || client === 'sqlite3' || client === 'sqlite') {\n return 'sqlite'\n }\n if (client === 'pg' || client === 'postgresql') {\n return 'postgresql'\n }\n if (client === 'mysql' || client === 'mysql2') {\n return 'mysql'\n }\n throw new Error(`Unsupported database client: ${client}`)\n}\n\n/**\n * Read current database schema.\n * Supports SQLite, PostgreSQL, and MySQL.\n * @param knex - Knex instance\n * @param dbType - Optional database type override (auto-detected if not provided)\n */\nexport async function readDatabaseSchema(knex: Knex, dbType?: DatabaseType): Promise<DatabaseSchema> {\n const effectiveType = dbType ?? detectDatabaseType(knex)\n\n switch (effectiveType) {\n case 'sqlite':\n return readSQLiteSchema(knex)\n case 'postgresql':\n return readPostgreSQLSchema(knex)\n case 'mysql':\n return readMySQLSchema(knex)\n default:\n throw new Error(`Unsupported database type: ${effectiveType}`)\n }\n}\n\n/**\n * Read SQLite schema.\n * Uses PRAGMA commands to introspect the database.\n */\nasync function readSQLiteSchema(knex: Knex): Promise<DatabaseSchema> {\n const tables = new Map<string, TableInfo>()\n\n // Get all tables (excluding system tables)\n const tableList = await knex.raw<{ name: string; type: string }[]>(\n \"SELECT name, type FROM sqlite_master WHERE type='table' ORDER BY name\"\n )\n\n for (const tableRow of tableList) {\n const tableName = tableRow.name\n\n // Skip system tables\n if (isSystemTable(tableName)) continue\n\n // Get columns\n const columnsRaw = await knex.raw<\n Array<{\n cid: number\n name: string\n type: string\n notnull: number\n dflt_value: string | null\n pk: number\n }>\n >(`PRAGMA table_info('${tableName}')`)\n\n const columns = new Map<string, ColumnInfo>()\n for (const col of columnsRaw) {\n columns.set(col.name, {\n name: col.name,\n type: col.type.toUpperCase(),\n nullable: col.notnull === 0,\n defaultValue: col.dflt_value,\n primaryKey: col.pk > 0,\n })\n }\n\n // Get indexes\n const indexListRaw = await knex.raw<\n Array<{\n seq: number\n name: string\n unique: number\n origin: string\n partial: number\n }>\n >(`PRAGMA index_list('${tableName}')`)\n\n const indexes: TableInfo['indexes'] = []\n for (const idx of indexListRaw) {\n // Skip auto-generated primary key indexes\n if (idx.origin === 'pk') continue\n\n const indexInfoRaw = await knex.raw<Array<{ seqno: number; cid: number; name: string }>>(\n `PRAGMA index_info('${idx.name}')`\n )\n\n const indexColumns = indexInfoRaw.map((info) => info.name)\n\n indexes.push({\n name: idx.name,\n columns: indexColumns,\n unique: idx.unique === 1,\n })\n }\n\n // Get foreign keys\n const foreignKeysRaw = await knex.raw<\n Array<{\n id: number\n seq: number\n table: string\n from: string\n to: string\n on_update: string\n on_delete: string\n }>\n >(`PRAGMA foreign_key_list('${tableName}')`)\n\n const foreignKeys: TableInfo['foreignKeys'] = []\n const processedFks = new Set<number>()\n\n for (const fk of foreignKeysRaw) {\n if (processedFks.has(fk.id)) continue\n processedFks.add(fk.id)\n\n foreignKeys.push({\n column: fk.from,\n referencedTable: fk.table,\n referencedColumn: fk.to,\n onDelete: fk.on_delete !== 'NO ACTION' ? fk.on_delete : undefined,\n onUpdate: fk.on_update !== 'NO ACTION' ? fk.on_update : undefined,\n })\n }\n\n tables.set(tableName, {\n name: tableName,\n columns,\n indexes,\n foreignKeys,\n })\n }\n\n return { tables }\n}\n\n/**\n * Read PostgreSQL schema using information_schema.\n */\nasync function readPostgreSQLSchema(knex: Knex): Promise<DatabaseSchema> {\n const tables = new Map<string, TableInfo>()\n\n // Get all tables\n const tableRows = await knex('information_schema.tables')\n .select('table_name')\n .where({ table_schema: 'public', table_type: 'BASE TABLE' })\n .orderBy('table_name')\n\n for (const row of tableRows) {\n const tableName = row.table_name\n if (isSystemTable(tableName)) continue\n\n // Get columns\n const columnRows = await knex('information_schema.columns')\n .select('column_name', 'data_type', 'is_nullable', 'column_default')\n .where({ table_schema: 'public', table_name: tableName })\n .orderBy('ordinal_position')\n\n const columns = new Map<string, ColumnInfo>()\n for (const col of columnRows) {\n columns.set(col.column_name, {\n name: col.column_name,\n type: col.data_type.toUpperCase(),\n nullable: col.is_nullable === 'YES',\n defaultValue: col.column_default,\n primaryKey: false, // Will be set below\n })\n }\n\n // Get primary keys\n const pkRows = await knex('information_schema.key_column_usage')\n .select('column_name')\n .where({\n table_schema: 'public',\n table_name: tableName,\n constraint_name: `${tableName}_pkey`,\n })\n\n for (const pk of pkRows) {\n const col = columns.get(pk.column_name)\n if (col) col.primaryKey = true\n }\n\n // Get indexes\n const indexes: TableInfo['indexes'] = []\n const idxResult = await knex.raw(\n `\n SELECT\n i.relname AS index_name,\n ix.indisunique AS is_unique,\n array_agg(a.attname ORDER BY k.n) AS column_names\n FROM pg_index ix\n JOIN pg_class t ON t.oid = ix.indrelid\n JOIN pg_class i ON i.oid = ix.indexrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n CROSS JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n)\n JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum\n WHERE n.nspname = 'public'\n AND t.relname = ?\n AND NOT ix.indisprimary\n GROUP BY i.relname, ix.indisunique\n `,\n [tableName]\n )\n const idxRows = idxResult.rows || []\n for (const idx of idxRows) {\n // pg driver returns array_agg as string \"{col1,col2}\" instead of JS array\n const cols = Array.isArray(idx.column_names)\n ? idx.column_names\n : typeof idx.column_names === 'string'\n ? idx.column_names.replace(/^\\{|\\}$/g, '').split(',').filter(Boolean)\n : []\n indexes.push({\n name: idx.index_name,\n columns: cols,\n unique: idx.is_unique,\n })\n }\n\n // Get foreign keys\n const foreignKeys: TableInfo['foreignKeys'] = []\n const fkResult = await knex.raw(\n `\n SELECT\n kcu.column_name,\n ccu.table_name AS foreign_table_name,\n ccu.column_name AS foreign_column_name\n FROM information_schema.table_constraints AS tc\n JOIN information_schema.key_column_usage AS kcu\n ON tc.constraint_name = kcu.constraint_name\n JOIN information_schema.constraint_column_usage AS ccu\n ON ccu.constraint_name = tc.constraint_name\n WHERE tc.table_schema = 'public'\n AND tc.table_name = ?\n AND tc.constraint_type = 'FOREIGN KEY'\n `,\n [tableName]\n )\n\n // PostgreSQL raw returns { rows: [...] }\n const fkRows = fkResult.rows || []\n for (const fk of fkRows) {\n foreignKeys.push({\n column: fk.column_name,\n referencedTable: fk.foreign_table_name,\n referencedColumn: fk.foreign_column_name,\n })\n }\n\n tables.set(tableName, {\n name: tableName,\n columns,\n indexes,\n foreignKeys,\n })\n }\n\n return { tables }\n}\n\n/**\n * Read MySQL schema using information_schema.\n */\nasync function readMySQLSchema(knex: Knex): Promise<DatabaseSchema> {\n const tables = new Map<string, TableInfo>()\n\n // Get database name - MySQL raw returns [[rows], fields]\n const dbResult = await knex.raw('SELECT DATABASE() as db_name')\n const dbRows = dbResult[0] || dbResult\n const dbName = (Array.isArray(dbRows) ? dbRows[0] : dbRows)?.db_name || ''\n\n if (!dbName) {\n throw new Error('Could not determine MySQL database name')\n }\n\n // Get all tables - use raw query for better control\n const tablesResult = await knex.raw(\n `SELECT TABLE_NAME FROM information_schema.tables WHERE table_schema = ? AND table_type = 'BASE TABLE' ORDER BY TABLE_NAME`,\n [dbName]\n )\n const tableRows = tablesResult[0] || tablesResult\n\n for (const row of tableRows) {\n const tableName = row.TABLE_NAME || row.table_name\n if (!tableName || isSystemTable(tableName)) continue\n\n // Get columns using raw query\n const colsResult = await knex.raw(\n `SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY\n FROM information_schema.columns\n WHERE table_schema = ? AND table_name = ?\n ORDER BY ORDINAL_POSITION`,\n [dbName, tableName]\n )\n const columnRows = colsResult[0] || colsResult\n\n const columns = new Map<string, ColumnInfo>()\n for (const col of columnRows) {\n const colName = col.COLUMN_NAME || col.column_name\n columns.set(colName, {\n name: colName,\n type: (col.DATA_TYPE || col.data_type || '').toUpperCase(),\n nullable: (col.IS_NULLABLE || col.is_nullable) === 'YES',\n defaultValue: col.COLUMN_DEFAULT || col.column_default,\n primaryKey: (col.COLUMN_KEY || col.column_key) === 'PRI',\n })\n }\n\n // Get indexes\n const indexes: TableInfo['indexes'] = []\n const idxResult = await knex.raw(\n `SELECT INDEX_NAME, NON_UNIQUE, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS column_names\n FROM information_schema.statistics\n WHERE table_schema = ? AND table_name = ? AND INDEX_NAME != 'PRIMARY'\n GROUP BY INDEX_NAME, NON_UNIQUE`,\n [dbName, tableName]\n )\n const idxRows = idxResult[0] || idxResult\n for (const idx of idxRows) {\n const idxName = idx.INDEX_NAME || idx.index_name\n const nonUnique = idx.NON_UNIQUE ?? idx.non_unique\n const colNames = (idx.column_names || '').split(',')\n indexes.push({\n name: idxName,\n columns: colNames,\n unique: nonUnique === 0,\n })\n }\n\n // Get foreign keys using raw query\n const fkResult = await knex.raw(\n `SELECT COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME\n FROM information_schema.key_column_usage\n WHERE table_schema = ? AND table_name = ? AND REFERENCED_TABLE_NAME IS NOT NULL`,\n [dbName, tableName]\n )\n const fkRows = fkResult[0] || fkResult\n\n const foreignKeys: TableInfo['foreignKeys'] = []\n for (const fk of fkRows) {\n foreignKeys.push({\n column: fk.COLUMN_NAME || fk.column_name,\n referencedTable: fk.REFERENCED_TABLE_NAME || fk.referenced_table_name,\n referencedColumn: fk.REFERENCED_COLUMN_NAME || fk.referenced_column_name,\n })\n }\n\n tables.set(tableName, {\n name: tableName,\n columns,\n indexes,\n foreignKeys,\n })\n }\n\n return { tables }\n}\n\n/**\n * Check if table is a system table (should be ignored in diff).\n */\nexport function isSystemTable(tableName: string): boolean {\n const systemTables = new Set([\n '_nexus_migrations',\n '_nexus_migration_lock',\n '_nexus_sequences',\n 'single_records',\n 'sqlite_sequence', // SQLite internal\n 'knex_migrations', // Knex legacy\n 'knex_migrations_lock', // Knex legacy\n ])\n\n return systemTables.has(tableName)\n}\n","import type { FieldDefinition } from '@gzl10/nexus-sdk'\n\n/**\n * Get expected column type for a field definition.\n * Maps Nexus field types to database column types.\n */\nexport function getExpectedColumnType(field: FieldDefinition, fieldName?: string): string {\n const dbConfig = field.db\n\n if (!dbConfig) {\n throw new Error(`Field ${fieldName ?? 'unknown'} has no db configuration`)\n }\n\n const type = dbConfig.type\n\n switch (type) {\n case 'string':\n return 'VARCHAR'\n case 'text':\n return 'TEXT'\n case 'integer':\n return 'INTEGER'\n case 'decimal':\n return 'DECIMAL'\n case 'boolean':\n return 'BOOLEAN'\n case 'datetime':\n return 'TIMESTAMP'\n case 'date':\n return 'DATE'\n case 'json':\n return 'JSON'\n case 'uuid':\n return 'UUID'\n case 'array':\n return 'JSON' // Arrays are stored as JSON\n default:\n // This should never happen if DbType is properly typed, but handle it for safety\n return String(type).toUpperCase()\n }\n}\n\n/**\n * Generate Knex column definition code from field.\n */\nexport function generateColumnDefinition(fieldName: string, field: FieldDefinition): string {\n const dbConfig = field.db\n if (!dbConfig) {\n throw new Error(`Field ${fieldName} has no db configuration`)\n }\n\n const parts: string[] = []\n const type = dbConfig.type\n\n // Column type and size\n switch (type) {\n case 'string':\n if (dbConfig.size) {\n parts.push(`table.string('${fieldName}', ${dbConfig.size})`)\n } else {\n parts.push(`table.string('${fieldName}')`)\n }\n break\n\n case 'text':\n parts.push(`table.text('${fieldName}')`)\n break\n\n case 'integer':\n parts.push(`table.integer('${fieldName}')`)\n break\n\n case 'decimal':\n if (dbConfig.precision) {\n parts.push(`table.decimal('${fieldName}', ${dbConfig.precision[0]}, ${dbConfig.precision[1]})`)\n } else {\n parts.push(`table.decimal('${fieldName}')`)\n }\n break\n\n case 'boolean':\n parts.push(`table.boolean('${fieldName}')`)\n break\n\n case 'datetime':\n parts.push(`table.timestamp('${fieldName}')`)\n break\n\n case 'date':\n parts.push(`table.date('${fieldName}')`)\n break\n\n case 'json':\n parts.push(`table.json('${fieldName}')`)\n break\n\n case 'array':\n parts.push(`table.json('${fieldName}')`)\n break\n\n case 'uuid':\n parts.push(`table.uuid('${fieldName}')`)\n break\n\n default:\n parts.push(`table.specificType('${fieldName}', '${type}')`)\n }\n\n // Nullable/NotNullable\n if (dbConfig.nullable === false) {\n parts.push('.notNullable()')\n } else if (dbConfig.nullable === true) {\n parts.push('.nullable()')\n }\n\n // Default function takes priority over static default\n if (dbConfig.defaultFn) {\n if (dbConfig.defaultFn === 'now') {\n parts.push('.defaultTo(knex.fn.now())')\n } else if (dbConfig.defaultFn === 'uuid') {\n parts.push('.defaultTo(knex.raw(\"gen_random_uuid()\"))')\n }\n } else if (dbConfig.default !== undefined) {\n // Static default value (only if no defaultFn)\n if (typeof dbConfig.default === 'string') {\n if (dbConfig.default === 'CURRENT_TIMESTAMP' || dbConfig.default === 'now()') {\n parts.push('.defaultTo(knex.fn.now())')\n } else {\n parts.push(`.defaultTo('${dbConfig.default}')`)\n }\n } else if (typeof dbConfig.default === 'boolean') {\n parts.push(`.defaultTo(${dbConfig.default})`)\n } else {\n parts.push(`.defaultTo(${dbConfig.default})`)\n }\n }\n\n // Primary key\n if (dbConfig.primary) {\n parts.push('.primary()')\n }\n\n // Unique\n if (dbConfig.unique) {\n parts.push('.unique()')\n }\n\n // Foreign key (from relation config)\n if (field.relation?.table && field.relation?.column) {\n parts.push(`.references('${field.relation.table}.${field.relation.column}')`)\n\n if (field.relation.onDelete) {\n parts.push(`.onDelete('${field.relation.onDelete}')`)\n }\n if (field.relation.onUpdate) {\n parts.push(`.onUpdate('${field.relation.onUpdate}')`)\n }\n }\n\n return parts.join('')\n}\n\n/**\n * Get all field names that should exist in the database.\n */\nexport function getExpectedColumnNames(entity: { fields: Record<string, FieldDefinition> }): Set<string> {\n return new Set(Object.keys(entity.fields).filter((name) => entity.fields[name]?.db && !entity.fields[name]?.db?.virtual))\n}\n\n/**\n * Check if field is nullable.\n */\nexport function isFieldNullable(field: FieldDefinition): boolean {\n return field.db?.nullable !== false\n}\n","import type { Knex } from 'knex'\nimport path from 'node:path'\nimport fs from 'node:fs/promises'\nimport { readFileSync, mkdirSync, realpathSync } from 'node:fs'\nimport { getDb } from './connection.js'\nimport { logger } from '../core/logger/index.js'\nimport { getMigrationsDir, getCoreMigrationsDir, getProjectMigrationsDir, getProjectPath } from '../config/paths.js'\nimport { readDatabaseSchema, type DatabaseSchema, isSystemTable } from './schema-reader.js'\nimport { getOrderedModules } from '../engine/index.js'\nimport { getOrderedModulesInternal } from '../engine/module-queries.js'\nimport type { RegisteredModule } from '../engine/module-store.js'\nimport type { EntityDefinition, EntityIndex, FieldDefinition } from '@gzl10/nexus-sdk'\nimport {\n getExpectedColumnType,\n getExpectedColumnNames,\n generateColumnDefinition,\n} from './migration-helpers.js'\nimport { moduleStore } from '../engine/module-store.js'\n\n/**\n * Migration generation scope.\n * - 'core': only entities from core modules\n * - 'project': only entities from standalone (user) modules\n * - 'plugin:{name}': only entities from a specific plugin\n * - 'all': all entities (default, backwards compat)\n */\nexport type MigrationScope = 'core' | 'project' | 'all' | `plugin:${string}`\n\n/**\n * MySQL utf8mb4 max index key length = 3072 bytes (768 chars × 4 bytes/char).\n * Warn when a unique/composite index exceeds this limit.\n */\nconst MYSQL_MAX_INDEX_BYTES = 3072\nconst MYSQL_BYTES_PER_CHAR = 4\n\nfunction getColumnIndexBytes(field: FieldDefinition | undefined): number {\n if (!field?.db) return 255 * MYSQL_BYTES_PER_CHAR // default varchar(255)\n const size = field.db.size ?? 255\n if (field.db.type === 'text') return 0 // text can't be in index without prefix\n if (field.db.type === 'string') return size * MYSQL_BYTES_PER_CHAR\n if (field.db.type === 'integer') return 4\n if (field.db.type === 'boolean') return 1\n if (field.db.type === 'datetime' || field.db.type === 'date') return 8\n if (field.db.type === 'uuid') return 16\n return size * MYSQL_BYTES_PER_CHAR\n}\n\nfunction warnIfIndexExceedsMySQLLimit(\n table: string,\n columns: string[],\n unique: boolean,\n fields: Record<string, FieldDefinition> | undefined\n): void {\n if (!fields) return\n const totalBytes = columns.reduce((sum, col) => sum + getColumnIndexBytes(fields[col]), 0)\n if (totalBytes > MYSQL_MAX_INDEX_BYTES) {\n const indexType = unique ? 'UNIQUE' : 'INDEX'\n logger.warn(\n `${indexType} on ${table}(${columns.join(', ')}) requires ${totalBytes} bytes — ` +\n `exceeds MySQL utf8mb4 limit of ${MYSQL_MAX_INDEX_BYTES} bytes. ` +\n `Reduce column sizes or restructure the index for MySQL compatibility.`\n )\n }\n}\n\n/**\n * Schema diff result.\n */\nexport interface IndexDiff {\n table: string\n newIndexes: Array<{ columns: string[]; unique: boolean }>\n droppedIndexes: Array<{ name: string; columns: string[]; unique: boolean }>\n}\n\nexport interface SchemaDiff {\n newTables: Array<{ table: string; entity: EntityDefinition }>\n droppedTables: string[]\n alteredTables: Array<{\n table: string\n newColumns: Array<{ name: string; type: string; field: FieldDefinition }>\n droppedColumns: Array<{ name: string; type: string }>\n modifiedColumns: Array<{ name: string; oldType: string; newType: string; field: FieldDefinition }>\n }>\n indexChanges: IndexDiff[]\n}\n\n/**\n * Generate a migration for a specific source scope.\n *\n * @param name - Migration name (without prefix or extension)\n * @param scope - 'core' | 'project' | 'all'\n * @param targetDir - Directory to write the migration file to (auto-detected if not provided)\n * @param prefix - File name prefix (e.g. 'core__') — auto-detected if not provided\n * @returns Filepath of the generated migration, or empty string if no changes\n */\nexport async function generateMigrationForSource(\n name: string,\n scope: MigrationScope = 'all',\n targetDir?: string,\n prefix?: string,\n): Promise<string> {\n const knex = getDb()\n\n logger.info({ name, scope }, 'Generating migration...')\n\n // 1. Get entities filtered by scope\n const allEntities = scope === 'all'\n ? getAllPersistentEntities()\n : getPersistentEntitiesByScope(scope)\n\n if (allEntities.length === 0) {\n logger.warn({ scope }, 'No persistent entities found for scope')\n return ''\n }\n\n // 2. Read current database schema\n const currentSchema = await readDatabaseSchema(knex)\n\n // 3. Compare and detect changes\n // Build the set of tables owned by this scope so that drop detection\n // only considers tables that belong to the current scope.\n const ownedTables = collectOwnedTables(allEntities)\n const changes = computeSchemaDiff(allEntities, currentSchema, { ownedTables })\n\n if (isEmptyDiff(changes)) {\n logger.info('No schema changes detected')\n console.log('\\n✅ No schema changes detected\\n')\n return ''\n }\n\n // 4. Generate code for up() and down()\n const upCode = generateUpCode(changes)\n const downCode = generateDownCode(changes)\n\n // 5. Resolve target dir and prefix\n const effectiveDir = targetDir ?? resolveTargetDir(scope)\n const effectivePrefix = prefix ?? resolvePrefix(scope)\n\n // 6. Create migration file (.js — no compilation needed, natively importable)\n const timestamp = Date.now()\n const filename = `${effectivePrefix}${timestamp}_${name}.js`\n const filepath = path.join(effectiveDir, filename)\n\n await fs.mkdir(effectiveDir, { recursive: true })\n\n const content = `/** @param {import('knex').Knex} knex */\nexport async function up(knex) {\n${upCode}\n}\n\n/** @param {import('knex').Knex} knex */\nexport async function down(knex) {\n${downCode}\n}\n`\n\n await fs.writeFile(filepath, content, 'utf-8')\n\n logger.info({ filename, scope }, 'Migration created')\n console.log(`\\n✅ Migration created: ${filename}\\n`)\n console.log(`Generated changes:`)\n logDiffSummary(changes)\n console.log('')\n\n return filepath\n}\n\n\n/**\n * Resolve target directory for a migration scope.\n */\nfunction resolveTargetDir(scope: MigrationScope): string {\n if (scope === 'core') return getCoreMigrationsDir()\n if (scope === 'project') return getProjectMigrationsDir()\n if (scope === 'all') return getMigrationsDir()\n\n // plugin:{name}\n const pluginName = scope.slice('plugin:'.length)\n const plugin = moduleStore.plugins.get(pluginName)\n if (!plugin) throw new Error(`Plugin '${pluginName}' not registered`)\n\n if (!plugin.migrationsDir) {\n // Auto-create migrations dir in the plugin's package source\n const pkgDir = realpathSync(path.join(getProjectPath(), 'node_modules', plugin.name))\n const defaultDir = path.join(pkgDir, 'migrations')\n mkdirSync(defaultDir, { recursive: true })\n plugin.migrationsDir = defaultDir\n }\n return plugin.migrationsDir\n}\n\n/**\n * Resolve file name prefix for a migration scope.\n */\nfunction resolvePrefix(scope: MigrationScope): string {\n if (scope === 'core') return 'core__'\n if (scope === 'project' || scope === 'all') return ''\n\n // plugin:{name}\n const pluginName = scope.slice('plugin:'.length)\n const plugin = moduleStore.plugins.get(pluginName)\n if (!plugin) throw new Error(`Plugin '${pluginName}' not registered — cannot resolve migration prefix`)\n return `${plugin.code}__`\n}\n\n/**\n * Detect the default migration scope based on the current project.\n * If cwd is a plugin package (package.json name matches a registered plugin),\n * returns 'plugin:{name}'. Otherwise returns 'project'.\n */\nexport function detectDefaultScope(): MigrationScope {\n try {\n const pkgPath = path.join(getProjectPath(), 'package.json')\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))\n const pkgName = pkg?.name as string | undefined\n\n // Running from nexus-backend itself → core scope\n if (pkgName === '@gzl10/nexus-backend') {\n return 'core'\n }\n\n if (pkgName && moduleStore.plugins.has(pkgName)) {\n return `plugin:${pkgName}`\n }\n } catch {\n // No package.json or unreadable — default to project\n }\n return 'project'\n}\n\n/**\n * Generate and apply migration automatically (dev workflow).\n * Auto-detects scope: if running inside a plugin package, generates for that plugin.\n * Otherwise generates for project scope.\n * Similar to `prisma migrate dev`.\n */\nexport async function devMigration(name?: string, scope?: MigrationScope): Promise<void> {\n const migrationName = name\n ? name.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '')\n : 'auto'\n const effectiveScope = scope ?? detectDefaultScope()\n\n if (effectiveScope !== 'project') {\n logger.info({ scope: effectiveScope }, 'Auto-detected plugin scope')\n }\n\n const { runMigrations, loadAllMigrationFiles } = await import('./migration-runner.js')\n const { getDb } = await import('./connection.js')\n\n // 1. Apply pending migrations FIRST so the DB schema is up-to-date\n // before we compare. Without this, a fresh DB would see all existing\n // tables as \"new\" and generate duplicate CREATE TABLE migrations.\n const db = getDb()\n const migrationFiles = await loadAllMigrationFiles()\n const executed = await db('_nexus_migrations')\n .where({ status: 'completed' })\n .select('name')\n .then((rows) => new Set(rows.map((r) => r.name)))\n\n const pendingBefore = migrationFiles.filter((m) => !executed.has(m.name))\n\n if (pendingBefore.length > 0) {\n console.log(`\\nApplying ${pendingBefore.length} pending migration(s)...\\n`)\n await runMigrations()\n console.log('✅ Pending migrations applied\\n')\n }\n\n // 2. Now generate migration by comparing entities vs the up-to-date schema\n const filepath = await generateMigrationForSource(migrationName, effectiveScope)\n\n // 3. If a new migration was generated, apply it immediately\n if (filepath) {\n console.log('Applying new migration...\\n')\n try {\n await runMigrations()\n } catch (applyError) {\n // Clean up the generated file so we don't leave an inconsistent state\n // (file on disk but migration not registered as completed)\n console.error('\\n❌ Migration apply failed — cleaning up generated file...\\n')\n try {\n await fs.unlink(filepath)\n console.error(`Removed: ${path.basename(filepath)}\\n`)\n } catch {\n console.error(`Could not remove ${filepath} — please delete it manually.\\n`)\n }\n throw applyError\n }\n console.log('\\n✅ Migration applied successfully\\n')\n } else if (pendingBefore.length === 0) {\n logger.info({ scope: effectiveScope }, 'No changes detected and no pending migrations')\n }\n}\n\n/**\n * Detect schema drift between registered entities and current database schema.\n * Returns null if no drift detected, otherwise returns the SchemaDiff.\n *\n * Similar to Prisma's drift detection - used to verify that the database\n * schema matches the expected state from entity definitions.\n *\n * @param knexInstance - Optional Knex instance (defaults to getDb())\n * @returns SchemaDiff if drift detected, null otherwise\n */\nexport async function detectSchemaDrift(knexInstance?: Knex): Promise<SchemaDiff | null> {\n const knex = knexInstance ?? getDb()\n\n // Get all persistent entities from registered modules\n const entities = getAllPersistentEntities()\n\n // If no entities, no drift possible\n if (entities.length === 0) {\n return null\n }\n\n // Read current database schema\n const currentSchema = await readDatabaseSchema(knex)\n\n // Compare and detect changes (all entities loaded — detect drops for all)\n const ownedTables = collectOwnedTables(entities)\n const diff = computeSchemaDiff(entities, currentSchema, { ownedTables })\n\n // Return null if no drift, otherwise return the diff\n return isEmptyDiff(diff) ? null : diff\n}\n\n/** Entity types that have their own database table */\nconst PERSISTENT_TYPES = new Set([\n 'collection',\n 'tree',\n 'dag',\n 'event', // deprecated alias → collection\n 'reference', // deprecated alias → collection\n 'temp', // deprecated alias → collection\n // 'config' excluded: deprecated alias → single (shared single_records table)\n])\n\n/**\n * Get all persistent entities from all modules.\n * Only includes entities that have their own database table.\n *\n * Excluded types:\n * - single: stored in shared 'single_records' table\n * - action: no persistence\n * - external/virtual/computed: no database storage\n *\n * @remarks\n * Exported for testing purposes to verify plugin entity detection.\n */\nexport function getAllPersistentEntities(): EntityDefinition[] {\n const modules = getOrderedModules()\n return extractEntitiesFromModules(modules)\n}\n\n/**\n * Get persistent entities filtered by module source scope.\n * - 'core': entities from modules with _source === 'core'\n * - 'project': entities from modules with _source === 'standalone'\n * - 'plugin:{name}': entities from a specific plugin\n */\nexport function getPersistentEntitiesByScope(scope: 'core' | 'project' | `plugin:${string}`): EntityDefinition[] {\n const modules = getOrderedModulesInternal()\n\n let filtered: RegisteredModule[]\n\n if (scope.startsWith('plugin:')) {\n const pluginName = scope.slice('plugin:'.length)\n filtered = modules.filter((m) => m._source === 'plugin' && m._pluginName === pluginName)\n } else {\n const sourceFilter = scope === 'core' ? 'core' : 'standalone'\n filtered = modules.filter((m) => m._source === sourceFilter)\n }\n\n return extractEntitiesFromModules(filtered)\n}\n\n/**\n * Extract persistent entities from a list of modules.\n */\nfunction extractEntitiesFromModules(modules: readonly { definitions?: EntityDefinition[] }[]): EntityDefinition[] {\n const entities: EntityDefinition[] = []\n\n for (const mod of modules) {\n if (!mod.definitions) continue\n\n for (const def of mod.definitions) {\n const entityType = def.type ?? 'collection'\n if (!PERSISTENT_TYPES.has(entityType)) continue\n\n // Skip entities with external adapter (e.g., in-memory masters)\n if ((def as any).adapter) continue\n\n if (!(def as any).table) {\n logger.warn({ entity: def, type: def.type }, 'Persistent entity missing table property')\n continue\n }\n\n entities.push(def)\n }\n }\n\n return entities\n}\n\n/**\n * Collect the set of table names owned by the given entities.\n * Includes DAG junction tables which are auto-generated.\n */\nexport function collectOwnedTables(entities: EntityDefinition[]): Set<string> {\n const tables = new Set<string>()\n for (const entity of entities) {\n const tableName = (entity as any).table as string\n if (tableName) tables.add(tableName)\n // DAG entities auto-create a junction table\n if (entity.type === 'dag') {\n const parentsTable = (entity as EntityDefinition & { parentsTable?: string }).parentsTable ?? `${tableName}_parents`\n tables.add(parentsTable)\n }\n }\n return tables\n}\n\n/**\n * Compare entities vs current schema and detect changes.\n *\n * @remarks Exported for testing purposes.\n */\nexport function computeSchemaDiff(\n entities: EntityDefinition[],\n currentSchema: DatabaseSchema,\n options?: { ownedTables?: Set<string> }\n): SchemaDiff {\n const changes: SchemaDiff = {\n newTables: [],\n droppedTables: [],\n alteredTables: [],\n indexChanges: [],\n }\n\n // 1. Detect new tables and altered tables\n for (const entity of entities) {\n const tableName = (entity as any).table as string\n const currentTable = currentSchema.tables.get(tableName)\n\n if (!currentTable) {\n // New table (skip system tables - managed by ensureSystemTables)\n if (!isSystemTable(tableName)) {\n changes.newTables.push({ table: tableName, entity })\n }\n } else {\n // Table exists - compare columns\n const alteration = compareTable(entity, currentTable)\n if (\n alteration.newColumns.length > 0 ||\n alteration.droppedColumns.length > 0 ||\n alteration.modifiedColumns.length > 0\n ) {\n changes.alteredTables.push(alteration)\n }\n }\n }\n\n // 2. Detect dropped tables — only within ownedTables scope.\n // ownedTables contains the set of tables that belong to the current scope\n // (core, project, or a specific plugin). Only tables in this set that no\n // longer have a matching entity are considered \"dropped\".\n if (options?.ownedTables && options.ownedTables.size > 0) {\n const entityTables = new Set(entities.map((e) => (e as any).table as string))\n\n // Include DAG junction tables (auto-generated, not in entity definitions)\n for (const entity of entities) {\n if (entity.type === 'dag') {\n const parentsTable = (entity as EntityDefinition & { parentsTable?: string }).parentsTable ?? `${(entity as any).table}_parents`\n entityTables.add(parentsTable)\n }\n }\n\n for (const tableName of options.ownedTables) {\n // Table is owned by this scope, exists in DB, but has no entity → dropped\n if (!entityTables.has(tableName) && currentSchema.tables.has(tableName) && !isSystemTable(tableName)) {\n changes.droppedTables.push(tableName)\n }\n }\n }\n\n // 3. Detect index changes on existing tables\n for (const entity of entities) {\n const tableName = (entity as any).table as string\n const currentTable = currentSchema.tables.get(tableName)\n if (!currentTable) continue // new table — indexes handled in table creation\n\n const entityIndexes: EntityIndex[] = (entity as EntityDefinition & { indexes?: EntityIndex[] }).indexes || []\n const dbIndexes = currentTable.indexes || []\n\n // Collect single-column unique/index constraints from field definitions\n // These generate indexes in the DB but aren't in entity.indexes[]\n const fieldIndexKeys = new Set<string>()\n if (entity.fields) {\n for (const [fieldName, field] of Object.entries(entity.fields)) {\n const f = field as FieldDefinition\n if (f.db?.unique) {\n fieldIndexKeys.add(`U:${fieldName}`)\n }\n if (f.db?.index) {\n fieldIndexKeys.add(`I:${fieldName}`)\n }\n }\n }\n\n // Normalize: sort columns for comparison\n const normalizeKey = (cols: string[], unique: boolean) =>\n `${unique ? 'U' : 'I'}:${[...cols].sort().join(',')}`\n\n const expectedKeys = new Set([\n ...entityIndexes.map(idx => normalizeKey(idx.columns, !!idx.unique)),\n ...fieldIndexKeys,\n ])\n const currentKeys = new Map(dbIndexes.map(idx => [normalizeKey(idx.columns, idx.unique), idx]))\n\n const diff: IndexDiff = { table: tableName, newIndexes: [], droppedIndexes: [] }\n\n // New indexes (in entity but not in DB)\n for (const idx of entityIndexes) {\n const key = normalizeKey(idx.columns, !!idx.unique)\n if (!currentKeys.has(key)) {\n warnIfIndexExceedsMySQLLimit(tableName, idx.columns, !!idx.unique, entity.fields as Record<string, FieldDefinition> | undefined)\n diff.newIndexes.push({ columns: idx.columns, unique: !!idx.unique })\n }\n }\n\n // Dropped indexes (in DB but not in entity) — only drop indexes that look\n // like they were generated by Knex (table_col1_col2_unique / table_col1_col2_index)\n // to avoid dropping manually created indexes or framework indexes.\n for (const [key, idx] of currentKeys) {\n if (!expectedKeys.has(key)) {\n const looksManaged = idx.name.startsWith(tableName) || idx.name.startsWith('sqlite_autoindex')\n if (looksManaged) {\n diff.droppedIndexes.push({ name: idx.name, columns: idx.columns, unique: idx.unique })\n }\n }\n }\n\n if (diff.newIndexes.length > 0 || diff.droppedIndexes.length > 0) {\n changes.indexChanges.push(diff)\n }\n }\n\n return changes\n}\n\n/**\n * Compare entity definition vs current table and detect column changes.\n */\nfunction compareTable(\n entity: EntityDefinition,\n currentTable: { name: string; columns: Map<string, { name: string; type: string; nullable: boolean }> }\n): {\n table: string\n newColumns: Array<{ name: string; type: string; field: FieldDefinition }>\n droppedColumns: Array<{ name: string; type: string }>\n modifiedColumns: Array<{ name: string; oldType: string; newType: string; field: FieldDefinition }>\n} {\n const result = {\n table: (entity as any).table || '',\n newColumns: [] as Array<{ name: string; type: string; field: FieldDefinition }>,\n droppedColumns: [] as Array<{ name: string; type: string }>,\n modifiedColumns: [] as Array<{ name: string; oldType: string; newType: string; field: FieldDefinition }>,\n }\n\n // Skip if entity has no fields\n if (!entity.fields) {\n return result\n }\n\n const expectedColumns = getExpectedColumnNames(entity as { fields: Record<string, FieldDefinition> })\n const currentColumnNames = new Set(currentTable.columns.keys())\n\n // Detect new columns\n for (const [fieldName, field] of Object.entries(entity.fields)) {\n if (!field.db || field.db.virtual) continue\n\n const columnName = fieldName\n\n if (!currentColumnNames.has(columnName)) {\n result.newColumns.push({\n name: columnName,\n type: getExpectedColumnType(field),\n field,\n })\n } else {\n // Column exists - check if type changed\n const currentCol = currentTable.columns.get(columnName)!\n const expectedType = getExpectedColumnType(field)\n\n // Normalize types for comparison\n const currentType = normalizeColumnType(currentCol.type)\n const normalizedExpectedType = normalizeColumnType(expectedType)\n\n if (currentType !== normalizedExpectedType) {\n result.modifiedColumns.push({\n name: columnName,\n oldType: currentCol.type,\n newType: expectedType,\n field,\n })\n }\n }\n }\n\n // Check auto-fields that aren't in entity.fields but expected in DB\n const hasSoftDelete = (entity as EntityDefinition & { softDelete?: boolean }).softDelete\n if (hasSoftDelete && !currentColumnNames.has('deleted_at')) {\n result.newColumns.push({\n name: 'deleted_at',\n type: 'TIMESTAMP',\n field: { label: { en: 'Deleted At', es: 'Eliminado' }, input: 'datetime', db: { type: 'datetime', nullable: true } } as FieldDefinition,\n })\n }\n\n if (entity.type === 'tree' && !currentColumnNames.has('parent_id')) {\n result.newColumns.push({\n name: 'parent_id',\n type: 'STRING',\n field: { label: { en: 'Parent', es: 'Padre' }, input: 'select', db: { type: 'string', size: 26, nullable: true } } as FieldDefinition,\n })\n }\n\n // Add auto-fields to expected columns for drop detection\n if ((entity as EntityDefinition & { timestamps?: boolean }).timestamps) {\n expectedColumns.add('created_at')\n expectedColumns.add('updated_at')\n }\n if ((entity as EntityDefinition & { audit?: boolean }).audit) {\n expectedColumns.add('created_by')\n expectedColumns.add('updated_by')\n }\n if (hasSoftDelete) expectedColumns.add('deleted_at')\n if (entity.type === 'tree') expectedColumns.add('parent_id')\n\n // Detect dropped columns (exclude system fields)\n const systemFields = new Set(['id', 'created_at', 'updated_at', 'created_by', 'updated_by', 'deleted_at', 'parent_id'])\n\n for (const columnName of currentColumnNames) {\n if (!expectedColumns.has(columnName) && !systemFields.has(columnName)) {\n const col = currentTable.columns.get(columnName)!\n result.droppedColumns.push({ name: columnName, type: col.type })\n }\n }\n\n return result\n}\n\n/**\n * Normalize column type for comparison.\n *\n * @remarks Exported for testing purposes.\n */\nexport function normalizeColumnType(type: string): string {\n // Strip size/precision params: VARCHAR(26) → VARCHAR, DECIMAL(10,2) → DECIMAL\n const normalized = type.replace(/\\(.*\\)/, '').trim().toUpperCase()\n\n // Map common variants\n const typeMap: Record<string, string> = {\n 'VARCHAR': 'STRING',\n 'CHAR': 'STRING',\n 'TEXT': 'TEXT',\n 'INT': 'INTEGER',\n 'INTEGER': 'INTEGER',\n 'BIGINT': 'BIGINTEGER',\n 'FLOAT': 'DECIMAL',\n 'DOUBLE': 'DECIMAL',\n 'DECIMAL': 'DECIMAL',\n 'NUMERIC': 'DECIMAL',\n 'BOOLEAN': 'BOOLEAN',\n 'BOOL': 'BOOLEAN',\n 'TIMESTAMP': 'DATETIME',\n 'DATETIME': 'DATETIME',\n 'DATE': 'DATE',\n 'TIME': 'TIME',\n 'JSON': 'JSON',\n 'JSONB': 'JSON',\n 'UUID': 'UUID',\n // PostgreSQL specific (information_schema.columns.data_type values)\n 'CHARACTER VARYING': 'STRING',\n 'CHARACTER': 'STRING',\n 'DOUBLE PRECISION': 'DECIMAL',\n 'REAL': 'DECIMAL',\n 'SMALLINT': 'INTEGER',\n 'TIMESTAMP WITHOUT TIME ZONE': 'DATETIME',\n 'TIMESTAMP WITH TIME ZONE': 'DATETIME',\n 'TIME WITHOUT TIME ZONE': 'TIME',\n 'TIME WITH TIME ZONE': 'TIME',\n 'BIT VARYING': 'STRING',\n // MySQL specific\n 'TINYINT': 'BOOLEAN',\n 'LONGTEXT': 'TEXT',\n 'MEDIUMTEXT': 'TEXT',\n }\n\n return typeMap[normalized] || normalized\n}\n\n/**\n * Map a raw DB column type to a Knex builder call for down() migration generation.\n * Uses the original DB type to produce a best-effort recreation statement.\n */\nfunction mapDbTypeToKnex(dbType: string, colName?: string): { code: string } {\n const name = colName ? `'${colName}'` : `'column'`\n const upper = dbType.replace(/\\(.*\\)/, '').trim().toUpperCase()\n\n // Extract size if present: VARCHAR(26) → 26\n const sizeMatch = dbType.match(/\\((\\d+)\\)/)\n const size = sizeMatch ? sizeMatch[1] : null\n\n const mapping: Record<string, string> = {\n 'VARCHAR': size ? `table.string(${name}, ${size}).nullable()` : `table.string(${name}).nullable()`,\n 'CHARACTER VARYING': size ? `table.string(${name}, ${size}).nullable()` : `table.string(${name}).nullable()`,\n 'CHAR': size ? `table.string(${name}, ${size}).nullable()` : `table.string(${name}).nullable()`,\n 'TEXT': `table.text(${name}).nullable()`,\n 'INT': `table.integer(${name}).nullable()`,\n 'INTEGER': `table.integer(${name}).nullable()`,\n 'BIGINT': `table.bigInteger(${name}).nullable()`,\n 'FLOAT': `table.float(${name}).nullable()`,\n 'DOUBLE': `table.float(${name}).nullable()`,\n 'DOUBLE PRECISION': `table.float(${name}).nullable()`,\n 'DECIMAL': `table.decimal(${name}).nullable()`,\n 'NUMERIC': `table.decimal(${name}).nullable()`,\n 'BOOLEAN': `table.boolean(${name}).nullable()`,\n 'BOOL': `table.boolean(${name}).nullable()`,\n 'TINYINT': `table.boolean(${name}).nullable()`,\n 'TIMESTAMP': `table.timestamp(${name}).nullable()`,\n 'DATETIME': `table.timestamp(${name}).nullable()`,\n 'TIMESTAMP WITHOUT TIME ZONE': `table.timestamp(${name}).nullable()`,\n 'TIMESTAMP WITH TIME ZONE': `table.timestamp(${name}).nullable()`,\n 'DATE': `table.date(${name}).nullable()`,\n 'TIME': `table.time(${name}).nullable()`,\n 'JSON': `table.json(${name}).nullable()`,\n 'JSONB': `table.jsonb(${name}).nullable()`,\n 'UUID': `table.uuid(${name}).nullable()`,\n }\n\n return { code: mapping[upper] || `table.specificType(${name}, '${dbType}').nullable()` }\n}\n\n/**\n * Check if diff is empty.\n */\nfunction isEmptyDiff(diff: SchemaDiff): boolean {\n return (\n diff.newTables.length === 0 &&\n diff.droppedTables.length === 0 &&\n diff.alteredTables.length === 0 &&\n diff.indexChanges.length === 0\n )\n}\n\n/**\n * Sort tables topologically based on FK dependencies.\n * Tables that are referenced by FKs come first.\n *\n * @remarks Exported for testing purposes.\n */\nexport function topologicalSortTables(\n tables: Array<{ table: string; entity: EntityDefinition }>\n): Array<{ table: string; entity: EntityDefinition }> {\n if (tables.length <= 1) return tables\n\n // Build dependency graph: table -> tables it depends on\n const dependencies = new Map<string, Set<string>>()\n const tableMap = new Map<string, { table: string; entity: EntityDefinition }>()\n\n for (const item of tables) {\n tableMap.set(item.table, item)\n dependencies.set(item.table, new Set())\n\n // Find FK dependencies from field relations\n if (item.entity.fields) {\n for (const field of Object.values(item.entity.fields) as FieldDefinition[]) {\n if (field.relation?.table && field.relation.table !== item.table) {\n dependencies.get(item.table)!.add(field.relation.table)\n }\n }\n }\n }\n\n // Kahn's algorithm for topological sort\n const sorted: Array<{ table: string; entity: EntityDefinition }> = []\n const inDegree = new Map<string, number>()\n\n // Calculate in-degree (only for tables in our list)\n for (const [table] of dependencies) {\n inDegree.set(table, 0)\n }\n for (const [table, deps] of dependencies) {\n for (const dep of deps) {\n if (inDegree.has(dep)) {\n inDegree.set(table, (inDegree.get(table) || 0) + 1)\n }\n }\n }\n\n // Start with tables that have no dependencies\n const queue: string[] = []\n for (const [table, degree] of inDegree) {\n if (degree === 0) queue.push(table)\n }\n\n while (queue.length > 0) {\n const table = queue.shift()!\n const item = tableMap.get(table)\n if (item) sorted.push(item)\n\n // Reduce in-degree of dependent tables\n for (const [otherTable, deps] of dependencies) {\n if (deps.has(table) && inDegree.has(otherTable)) {\n const newDegree = (inDegree.get(otherTable) || 0) - 1\n inDegree.set(otherTable, newDegree)\n if (newDegree === 0) queue.push(otherTable)\n }\n }\n }\n\n // If not all tables were sorted, there's a cycle - return original order\n if (sorted.length !== tables.length) {\n logger.warn('Circular FK dependency detected, using original order')\n return tables\n }\n\n return sorted\n}\n\n/**\n * Generate Knex code for up().\n */\nfunction generateUpCode(changes: SchemaDiff): string {\n const lines: string[] = []\n\n // Sort tables topologically to handle FK dependencies\n const sortedNewTables = topologicalSortTables(changes.newTables)\n\n // 1. Create new tables (with hasTable check for safety)\n for (const { table, entity } of sortedNewTables) {\n lines.push(` if (!(await knex.schema.hasTable('${table}'))) {`)\n lines.push(` await knex.schema.createTable('${table}', (table) => {`)\n\n // Generate columns from fields\n if (entity.fields) {\n for (const [fieldName, field] of Object.entries(entity.fields) as [string, FieldDefinition][]) {\n if (!field.db || field.db.virtual) continue\n\n let columnDef = generateColumnDefinition(fieldName, field)\n // Auto-add primary key to 'id' field if not already defined\n if (fieldName === 'id' && !field.db.primary && !columnDef.includes('.primary()')) {\n columnDef += '.primary()'\n }\n // Warn if single-column unique/index exceeds MySQL limit\n if (field.db.unique || field.db.index) {\n warnIfIndexExceedsMySQLLimit(table, [fieldName], !!field.db.unique, entity.fields)\n }\n lines.push(` ${columnDef}`)\n }\n }\n\n // Add timestamps if entity has timestamps: true\n const hasTimestamps = (entity as EntityDefinition & { timestamps?: boolean }).timestamps\n if (hasTimestamps) {\n lines.push(` // timestamps`)\n lines.push(` table.timestamp('created_at').nullable().defaultTo(knex.fn.now())`)\n lines.push(` table.timestamp('updated_at').nullable().defaultTo(knex.fn.now())`)\n }\n\n // Add audit fields if entity has audit: true\n const hasAudit = (entity as EntityDefinition & { audit?: boolean }).audit\n if (hasAudit) {\n lines.push(` // audit`)\n lines.push(` table.string('created_by', 26).nullable()`)\n lines.push(` table.string('updated_by', 26).nullable()`)\n }\n\n // Add soft delete column if entity has softDelete: true\n const hasSoftDelete = (entity as EntityDefinition & { softDelete?: boolean }).softDelete\n if (hasSoftDelete) {\n lines.push(` // soft delete`)\n lines.push(` table.timestamp('deleted_at').nullable()`)\n }\n\n // Add parent_id for tree entities (auto-field injected by TreeService at runtime)\n if (entity.type === 'tree') {\n lines.push(` // tree parent reference`)\n lines.push(` table.string('parent_id', 26).nullable().references('${table}.id').onDelete('SET NULL')`)\n }\n\n // Create indexes if defined (skip single-column unique indexes already declared on column)\n const entityWithIndexes = entity as EntityDefinition & { indexes?: EntityIndex[] }\n if (entityWithIndexes.indexes?.length) {\n const fieldsWithUnique = new Set<string>()\n if (entity.fields) {\n for (const [name, f] of Object.entries(entity.fields) as [string, FieldDefinition][]) {\n if (f.db?.unique) fieldsWithUnique.add(name)\n }\n }\n\n for (const idx of entityWithIndexes.indexes) {\n if (idx.unique && idx.columns.length === 1 && fieldsWithUnique.has(idx.columns[0]!)) {\n continue\n }\n warnIfIndexExceedsMySQLLimit(table, idx.columns, !!idx.unique, entity.fields)\n const cols = idx.columns.map(c => `'${c}'`).join(', ')\n if (idx.unique) {\n lines.push(` table.unique([${cols}])`)\n } else {\n lines.push(` table.index([${cols}])`)\n }\n }\n }\n\n lines.push(` })`)\n lines.push(` }`)\n lines.push('')\n\n // Create DAG junction table for parent relationships\n if (entity.type === 'dag') {\n const parentsTable = (entity as EntityDefinition & { parentsTable?: string }).parentsTable ?? `${table}_parents`\n lines.push(` if (!(await knex.schema.hasTable('${parentsTable}'))) {`)\n lines.push(` await knex.schema.createTable('${parentsTable}', (table) => {`)\n lines.push(` table.string('id', 26).notNullable().primary()`)\n lines.push(` table.string('node_id', 26).notNullable().references('${(entity as any).table}.id').onDelete('CASCADE')`)\n lines.push(` table.string('parent_id', 26).notNullable().references('${(entity as any).table}.id').onDelete('CASCADE')`)\n lines.push(` table.timestamp('created_at').nullable().defaultTo(knex.fn.now())`)\n lines.push(` })`)\n lines.push(` }`)\n lines.push('')\n }\n }\n\n // 2. Alter existing tables\n for (const alteration of changes.alteredTables) {\n const hasChanges =\n alteration.newColumns.length > 0 ||\n alteration.droppedColumns.length > 0 ||\n alteration.modifiedColumns.length > 0\n\n if (!hasChanges) continue\n\n lines.push(` await knex.schema.alterTable('${alteration.table}', (table) => {`)\n\n // Add new columns\n for (const col of alteration.newColumns) {\n const columnDef = generateColumnDefinition(col.name, col.field)\n lines.push(` ${columnDef}`)\n }\n\n // Modify columns: drop in this alterTable (recreate in separate one below)\n for (const col of alteration.modifiedColumns) {\n lines.push(` // Modify ${col.name}: ${col.oldType} → ${col.newType} (⚠️ DATA LOSS)`)\n lines.push(` table.dropColumn('${col.name}')`)\n }\n\n // Drop columns\n for (const col of alteration.droppedColumns) {\n lines.push(` table.dropColumn('${col.name}')`)\n }\n\n lines.push(` })`)\n lines.push('')\n\n // Recreate modified columns in a separate alterTable (required for SQLite)\n if (alteration.modifiedColumns.length > 0) {\n lines.push(` await knex.schema.alterTable('${alteration.table}', (table) => {`)\n for (const col of alteration.modifiedColumns) {\n const columnDef = generateColumnDefinition(col.name, col.field)\n lines.push(` ${columnDef}`)\n }\n lines.push(` })`)\n lines.push('')\n }\n }\n\n // 3. Index changes on existing tables\n for (const indexDiff of changes.indexChanges) {\n if (indexDiff.newIndexes.length === 0 && indexDiff.droppedIndexes.length === 0) continue\n\n lines.push(` await knex.schema.alterTable('${indexDiff.table}', (table) => {`)\n\n for (const idx of indexDiff.droppedIndexes) {\n const cols = idx.columns.map(c => `'${c}'`).join(', ')\n if (idx.unique) {\n lines.push(` table.dropUnique([${cols}])`)\n } else {\n lines.push(` table.dropIndex([${cols}])`)\n }\n }\n\n for (const idx of indexDiff.newIndexes) {\n const cols = idx.columns.map(c => `'${c}'`).join(', ')\n if (idx.unique) {\n lines.push(` table.unique([${cols}])`)\n } else {\n lines.push(` table.index([${cols}])`)\n }\n }\n\n lines.push(` })`)\n lines.push('')\n }\n\n // 4. Drop tables (usually dangerous, but included for completeness)\n for (const tableName of changes.droppedTables) {\n lines.push(` await knex.schema.dropTableIfExists('${tableName}')`)\n }\n\n if (lines.length === 0) {\n lines.push(' // No changes')\n }\n\n return lines.join('\\n')\n}\n\n/**\n * Generate Knex code for down() (inverse of up).\n */\nfunction generateDownCode(changes: SchemaDiff): string {\n const lines: string[] = []\n\n // Revert in reverse order\n\n // 1. Recreate dropped tables (reverse of drop)\n for (const tableName of changes.droppedTables.slice().reverse()) {\n lines.push(` // NOTE: Cannot recreate dropped table '${tableName}' without schema definition`)\n lines.push(` // await knex.schema.createTable('${tableName}', (table) => { /* ... */ })`)\n }\n\n // 2. Revert table alterations (reverse order)\n for (const alteration of changes.alteredTables.slice().reverse()) {\n const hasChanges =\n alteration.newColumns.length > 0 ||\n alteration.droppedColumns.length > 0 ||\n alteration.modifiedColumns.length > 0\n\n if (!hasChanges) continue\n\n lines.push(` await knex.schema.alterTable('${alteration.table}', (table) => {`)\n\n // Recreate dropped columns (using DB type from before the drop)\n for (const col of alteration.droppedColumns) {\n const knexType = mapDbTypeToKnex(col.type, col.name)\n lines.push(` ${knexType.code} // was: ${col.type}`)\n }\n\n // Revert modified columns: drop in this alterTable (recreate in separate one below)\n for (const col of alteration.modifiedColumns.slice().reverse()) {\n lines.push(` // Revert ${col.name}: ${col.newType} → ${col.oldType} (⚠️ DATA LOSS)`)\n lines.push(` table.dropColumn('${col.name}')`)\n }\n\n // Drop new columns\n for (const col of alteration.newColumns.slice().reverse()) {\n lines.push(` table.dropColumn('${col.name}')`)\n }\n\n lines.push(` })`)\n lines.push('')\n\n // Recreate modified columns with original types\n if (alteration.modifiedColumns.length > 0) {\n lines.push(` await knex.schema.alterTable('${alteration.table}', (table) => {`)\n for (const col of alteration.modifiedColumns.slice().reverse()) {\n const knexType = mapDbTypeToKnex(col.oldType, col.name)\n lines.push(` ${knexType.code} // was: ${col.oldType}`)\n }\n lines.push(` })`)\n lines.push('')\n }\n }\n\n // 3. Revert index changes (inverse: new→drop, dropped→create)\n for (const indexDiff of changes.indexChanges.slice().reverse()) {\n if (indexDiff.newIndexes.length === 0 && indexDiff.droppedIndexes.length === 0) continue\n\n lines.push(` await knex.schema.alterTable('${indexDiff.table}', (table) => {`)\n\n // Drop indexes that were added in up()\n for (const idx of indexDiff.newIndexes) {\n const cols = idx.columns.map(c => `'${c}'`).join(', ')\n if (idx.unique) {\n lines.push(` table.dropUnique([${cols}])`)\n } else {\n lines.push(` table.dropIndex([${cols}])`)\n }\n }\n\n // Recreate indexes that were dropped in up()\n for (const idx of indexDiff.droppedIndexes) {\n const cols = idx.columns.map(c => `'${c}'`).join(', ')\n if (idx.unique) {\n lines.push(` table.unique([${cols}])`)\n } else {\n lines.push(` table.index([${cols}])`)\n }\n }\n\n lines.push(` })`)\n lines.push('')\n }\n\n // 4. Drop DAG junction tables first (before entity tables due to FK)\n for (const { entity } of changes.newTables) {\n if (entity.type === 'dag') {\n const parentsTable = (entity as EntityDefinition & { parentsTable?: string }).parentsTable ?? `${(entity as any).table}_parents`\n lines.push(` await knex.schema.dropTableIfExists('${parentsTable}')`)\n }\n }\n\n // 4. Drop new tables (reverse of create)\n for (const { table } of changes.newTables.slice().reverse()) {\n lines.push(` await knex.schema.dropTableIfExists('${table}')`)\n }\n\n if (lines.length === 0) {\n lines.push(' // No changes')\n }\n\n return lines.join('\\n')\n}\n\n/**\n * Log diff summary to console.\n */\nfunction logDiffSummary(diff: SchemaDiff): void {\n if (diff.newTables.length > 0) {\n console.log(` 📦 ${diff.newTables.length} new tables`)\n }\n\n if (diff.droppedTables.length > 0) {\n console.log(` 🗑️ ${diff.droppedTables.length} dropped tables`)\n }\n\n if (diff.alteredTables.length > 0) {\n console.log(` 🔧 ${diff.alteredTables.length} altered tables`)\n }\n\n if (diff.indexChanges.length > 0) {\n console.log(` 🔑 ${diff.indexChanges.length} index changes`)\n }\n\n const modifiedCount = diff.alteredTables.reduce((sum, t) => sum + t.modifiedColumns.length, 0)\n if (modifiedCount > 0) {\n console.log(` ⚠️ ${modifiedCount} column type changes (DATA LOSS: drop + recreate)`)\n }\n}\n\n/**\n * Format schema drift into a human-readable message.\n * Used by server startup to display drift details before failing.\n */\nexport function formatDriftMessage(drift: SchemaDiff): string {\n const lines: string[] = []\n\n lines.push('Schema drift detected! Database schema does not match entity definitions.')\n lines.push('')\n\n if (drift.newTables.length > 0) {\n lines.push(`Missing tables (${drift.newTables.length}):`)\n for (const { table } of drift.newTables) {\n lines.push(` - ${table}`)\n }\n lines.push('')\n }\n\n if (drift.alteredTables.length > 0) {\n lines.push(`Tables with missing columns (${drift.alteredTables.length}):`)\n for (const alteration of drift.alteredTables) {\n const newCols = alteration.newColumns.map(c => c.name).join(', ')\n if (newCols) {\n lines.push(` - ${alteration.table}: missing columns [${newCols}]`)\n }\n }\n lines.push('')\n }\n\n if (drift.droppedTables.length > 0) {\n lines.push(`Extra tables in database (${drift.droppedTables.length}):`)\n for (const table of drift.droppedTables) {\n lines.push(` - ${table}`)\n }\n lines.push('')\n }\n\n if (drift.indexChanges.length > 0) {\n lines.push(`Tables with index changes (${drift.indexChanges.length}):`)\n for (const idx of drift.indexChanges) {\n const parts: string[] = []\n if (idx.newIndexes.length > 0) parts.push(`${idx.newIndexes.length} missing`)\n if (idx.droppedIndexes.length > 0) parts.push(`${idx.droppedIndexes.length} extra`)\n lines.push(` - ${idx.table}: ${parts.join(', ')} indexes`)\n }\n lines.push('')\n }\n\n lines.push('Run \"pnpm migrate:dev\" to generate and apply migrations.')\n\n return lines.join('\\n')\n}\n\n/**\n * Generate Knex createTable code for a single entity.\n * Useful for testing the code generation logic.\n *\n * @remarks Exported for testing purposes.\n */\nexport function generateTableCode(entity: EntityDefinition): string[] {\n const lines: string[] = []\n const tableName = (entity as EntityDefinition & { table?: string }).table\n\n if (!tableName) {\n return lines\n }\n\n lines.push(`await knex.schema.createTable('${tableName}', (table) => {`)\n\n // Generate columns from fields\n if (entity.fields) {\n for (const [fieldName, field] of Object.entries(entity.fields) as [string, FieldDefinition][]) {\n if (!field.db || field.db.virtual) continue\n const columnDef = generateColumnDefinition(fieldName, field)\n lines.push(` ${columnDef}`)\n }\n }\n\n // Add timestamps if entity has timestamps: true\n const hasTimestamps = (entity as EntityDefinition & { timestamps?: boolean }).timestamps\n if (hasTimestamps) {\n lines.push(` // timestamps`)\n lines.push(` table.timestamp('created_at').nullable().defaultTo(knex.fn.now())`)\n lines.push(` table.timestamp('updated_at').nullable().defaultTo(knex.fn.now())`)\n }\n\n // Add audit fields if entity has audit: true\n const hasAudit = (entity as EntityDefinition & { audit?: boolean }).audit\n if (hasAudit) {\n lines.push(` // audit`)\n lines.push(` table.string('created_by', 26).nullable()`)\n lines.push(` table.string('updated_by', 26).nullable()`)\n }\n\n lines.push(`})`)\n\n return lines\n}\n","/**\n * Error codes for i18n-friendly error handling.\n * Frontend translates these codes to localized messages.\n *\n * Format: CATEGORY_ACTION_REASON\n *\n * Categories: AUTH, USER, ROLE, VALIDATION, STORAGE, PERMISSION, RESOURCE, SYSTEM\n */\nexport const ErrorCodes = {\n // Auth\n AUTH_INVALID_CREDENTIALS: 'AUTH_INVALID_CREDENTIALS',\n AUTH_TOKEN_EXPIRED: 'AUTH_TOKEN_EXPIRED',\n AUTH_TOKEN_INVALID: 'AUTH_TOKEN_INVALID',\n AUTH_TOKEN_REQUIRED: 'AUTH_TOKEN_REQUIRED',\n AUTH_OTP_REQUIRED: 'AUTH_OTP_REQUIRED',\n AUTH_OTP_INVALID: 'AUTH_OTP_INVALID',\n AUTH_REFRESH_TOKEN_REQUIRED: 'AUTH_REFRESH_TOKEN_REQUIRED',\n AUTH_REFRESH_TOKEN_INVALID: 'AUTH_REFRESH_TOKEN_INVALID',\n AUTH_REFRESH_TOKEN_EXPIRED: 'AUTH_REFRESH_TOKEN_EXPIRED',\n AUTH_SESSION_NOT_FOUND: 'AUTH_SESSION_NOT_FOUND',\n AUTH_SESSION_SELF_REVOKE: 'AUTH_SESSION_SELF_REVOKE',\n AUTH_VERIFICATION_CODE_INVALID: 'AUTH_VERIFICATION_CODE_INVALID',\n AUTH_REGISTRATION_DISABLED: 'AUTH_REGISTRATION_DISABLED',\n AUTH_AUTO_CREATE_DISABLED: 'AUTH_AUTO_CREATE_DISABLED',\n\n // User\n USER_NOT_FOUND: 'USER_NOT_FOUND',\n USER_EMAIL_EXISTS: 'USER_EMAIL_EXISTS',\n USER_NOT_AUTHENTICATED: 'USER_NOT_AUTHENTICATED',\n\n // Role\n ROLE_NOT_FOUND: 'ROLE_NOT_FOUND',\n ROLE_NAME_EXISTS: 'ROLE_NAME_EXISTS',\n ROLE_SYSTEM_PROTECTED: 'ROLE_SYSTEM_PROTECTED',\n ROLE_HAS_USERS: 'ROLE_HAS_USERS',\n ROLE_DEFAULT_NOT_FOUND: 'ROLE_DEFAULT_NOT_FOUND',\n\n // Permission\n PERMISSION_DENIED: 'PERMISSION_DENIED',\n\n // Validation\n VALIDATION_ERROR: 'VALIDATION_ERROR',\n VALIDATION_FIELD_REQUIRED: 'VALIDATION_FIELD_REQUIRED',\n VALIDATION_FIELD_INVALID: 'VALIDATION_FIELD_INVALID',\n VALIDATION_JSON_MALFORMED: 'VALIDATION_JSON_MALFORMED',\n\n // Storage\n STORAGE_FILE_NOT_FOUND: 'STORAGE_FILE_NOT_FOUND',\n STORAGE_FILE_TOO_LARGE: 'STORAGE_FILE_TOO_LARGE',\n STORAGE_FILE_TYPE_NOT_ALLOWED: 'STORAGE_FILE_TYPE_NOT_ALLOWED',\n STORAGE_PAYLOAD_TOO_LARGE: 'STORAGE_PAYLOAD_TOO_LARGE',\n\n // Resource (generic)\n RESOURCE_NOT_FOUND: 'RESOURCE_NOT_FOUND',\n RESOURCE_CONFLICT: 'RESOURCE_CONFLICT',\n RESOURCE_CREATE_NOT_SUPPORTED: 'RESOURCE_CREATE_NOT_SUPPORTED',\n RESOURCE_UPDATE_NOT_SUPPORTED: 'RESOURCE_UPDATE_NOT_SUPPORTED',\n RESOURCE_DELETE_NOT_SUPPORTED: 'RESOURCE_DELETE_NOT_SUPPORTED',\n\n // Module\n MODULE_NOT_FOUND: 'MODULE_NOT_FOUND',\n\n // HTTP standard\n NOT_FOUND: 'NOT_FOUND',\n AUTH_UNAUTHORIZED: 'AUTH_UNAUTHORIZED',\n\n // Database\n DB_CONSTRAINT_UNIQUE: 'DB_CONSTRAINT_UNIQUE',\n DB_CONSTRAINT_FK: 'DB_CONSTRAINT_FK',\n DB_CONNECTION_ERROR: 'DB_CONNECTION_ERROR',\n DATABASE_NOT_READY: 'DATABASE_NOT_READY',\n\n // System\n SYSTEM_INTERNAL_ERROR: 'SYSTEM_INTERNAL_ERROR',\n} as const\n\nexport type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes]\n","import type { ErrorCode } from './error-codes.js'\nimport { ErrorCodes } from './error-codes.js'\n\n/**\n * Parameters for creating an AppError with i18n support\n */\nexport interface AppErrorParams {\n /** Error code for i18n translation (e.g., 'AUTH_INVALID_CREDENTIALS') */\n code: ErrorCode\n /** Fallback message in English (used if frontend doesn't have translation) */\n message?: string\n /** Interpolation values for the translated message (e.g., { resource: 'User' }) */\n interpolation?: Record<string, string | number>\n}\n\n/**\n * Base application error with i18n support.\n *\n * Can be created with:\n * - String message (legacy, backward compatible)\n * - AppErrorParams object (new, with code and interpolation)\n *\n * @example\n * // Legacy usage\n * throw new AppError('Something went wrong', 400)\n *\n * // New usage with i18n\n * throw new AppError({\n * code: ErrorCodes.USER_NOT_FOUND,\n * message: 'User not found',\n * interpolation: { resource: 'User' }\n * }, 404)\n */\nexport class AppError extends Error {\n public readonly statusCode: number\n public readonly code: ErrorCode\n public readonly interpolation?: Record<string, string | number>\n public readonly details?: unknown\n\n constructor(\n params: AppErrorParams | string,\n statusCode: number = 400,\n details?: unknown\n ) {\n if (typeof params === 'string') {\n // Legacy: string message (backward compatible)\n super(params)\n this.code = ErrorCodes.SYSTEM_INTERNAL_ERROR\n } else {\n // New: structured params with code\n super(params.message || params.code)\n this.code = params.code\n this.interpolation = params.interpolation\n }\n\n this.statusCode = statusCode\n this.details = details\n this.name = 'AppError'\n Error.captureStackTrace(this, this.constructor)\n }\n}\n\n/**\n * Resource not found error (404)\n */\nexport class NotFoundError extends AppError {\n constructor(resource: string = 'Resource') {\n super({\n code: ErrorCodes.RESOURCE_NOT_FOUND,\n message: `${resource} not found`,\n interpolation: { resource }\n }, 404)\n this.name = 'NotFoundError'\n }\n}\n\n/**\n * Authentication required error (401)\n */\nexport class UnauthorizedError extends AppError {\n constructor(codeOrMessage: ErrorCode | string = ErrorCodes.AUTH_TOKEN_REQUIRED, message?: string) {\n // Check if first param is an ErrorCode\n const isCode = Object.values(ErrorCodes).includes(codeOrMessage as ErrorCode)\n\n if (isCode) {\n super({\n code: codeOrMessage as ErrorCode,\n message: message || 'Unauthorized'\n }, 401)\n } else {\n // Legacy: string message\n super({\n code: ErrorCodes.AUTH_TOKEN_REQUIRED,\n message: codeOrMessage as string\n }, 401)\n }\n this.name = 'UnauthorizedError'\n }\n}\n\n/**\n * Access denied error (403)\n */\nexport class ForbiddenError extends AppError {\n constructor(codeOrMessage: ErrorCode | string = ErrorCodes.PERMISSION_DENIED, message?: string) {\n const isCode = Object.values(ErrorCodes).includes(codeOrMessage as ErrorCode)\n\n if (isCode) {\n super({\n code: codeOrMessage as ErrorCode,\n message: message || 'Access denied'\n }, 403)\n } else {\n super({\n code: ErrorCodes.PERMISSION_DENIED,\n message: codeOrMessage as string\n }, 403)\n }\n this.name = 'ForbiddenError'\n }\n}\n\n/**\n * Resource conflict error (409)\n */\nexport class ConflictError extends AppError {\n constructor(codeOrMessage: ErrorCode | string = ErrorCodes.RESOURCE_CONFLICT, message?: string) {\n const isCode = Object.values(ErrorCodes).includes(codeOrMessage as ErrorCode)\n\n if (isCode) {\n super({\n code: codeOrMessage as ErrorCode,\n message: message || 'Conflict'\n }, 409)\n } else {\n super({\n code: ErrorCodes.RESOURCE_CONFLICT,\n message: codeOrMessage as string\n }, 409)\n }\n this.name = 'ConflictError'\n }\n}\n\n/**\n * Standard validation error detail\n */\nexport interface ValidationDetail {\n path: string\n message: string\n /** Error code for i18n translation */\n code?: string\n /** Interpolation values */\n interpolation?: Record<string, string | number>\n}\n\n/**\n * Validation error with field-level details (400)\n */\nexport class ValidationError extends AppError {\n public override readonly details: ValidationDetail[]\n\n constructor(messageOrCode: string | ErrorCode = ErrorCodes.VALIDATION_ERROR, details: ValidationDetail[] = []) {\n const isCode = Object.values(ErrorCodes).includes(messageOrCode as ErrorCode)\n\n if (isCode) {\n super({\n code: messageOrCode as ErrorCode,\n message: 'Validation error'\n }, 400)\n } else {\n super({\n code: ErrorCodes.VALIDATION_ERROR,\n message: messageOrCode as string\n }, 400)\n }\n\n this.name = 'ValidationError'\n this.details = details\n }\n}\n\n// Re-export ErrorCodes for convenience\nexport { ErrorCodes } from './error-codes.js'\nexport type { ErrorCode } from './error-codes.js'\n","/**\n * Knex implementation of DatabaseAdapter.\n *\n * Provides normalized database access using Knex query builder.\n * Supports SQLite, MySQL, and PostgreSQL.\n */\n\nimport type { Knex } from 'knex'\nimport type {\n DatabaseAdapter,\n PaginatedResult,\n EntityQuery\n} from '@gzl10/nexus-sdk'\nimport { applyFilters } from './filter-helpers.js'\nimport { ConflictError, ValidationError, AppError, ErrorCodes } from '../core/errors/app-error.js'\nimport { logger } from '../core/logger/index.js'\n\n/**\n * Default pagination values\n */\nconst DEFAULT_PAGE = 1\nconst DEFAULT_LIMIT = 20\nconst DEFAULT_MAX_LIMIT = 100\n\n/**\n * Detect and rethrow database constraint errors as typed AppErrors.\n * Supports SQLite, MySQL, and PostgreSQL error patterns.\n */\nfunction handleDbError(err: unknown): never {\n if (err instanceof AppError) throw err\n\n const dbErr = err as { code?: string; errno?: number; message?: string }\n const msg = dbErr.message ?? ''\n const code = dbErr.code ?? ''\n\n // UNIQUE constraint: PG 23505, MySQL ER_DUP_ENTRY/1062, SQLite text\n if (code === '23505' || code === 'ER_DUP_ENTRY' || dbErr.errno === 1062 || msg.includes('UNIQUE constraint failed')) {\n throw new ConflictError(ErrorCodes.DB_CONSTRAINT_UNIQUE, 'Unique constraint violation')\n }\n\n // Foreign key: PG 23503, MySQL ER_NO_REFERENCED_ROW_2/1452, SQLite text\n if (code === '23503' || code === 'ER_NO_REFERENCED_ROW_2' || dbErr.errno === 1452 || msg.includes('FOREIGN KEY constraint failed')) {\n throw new ValidationError(ErrorCodes.DB_CONSTRAINT_FK, [\n { path: 'foreignKey', message: 'Foreign key constraint violation' }\n ])\n }\n\n // Table not found: PG 42P01, MySQL ER_NO_SUCH_TABLE/1146, SQLite text\n if (code === '42P01' || code === 'ER_NO_SUCH_TABLE' || dbErr.errno === 1146 || msg.includes('no such table')) {\n logger.error({ err }, 'Database table not found — database may need migration or was wiped')\n throw new AppError({ code: ErrorCodes.DATABASE_NOT_READY, message: 'Database not ready' }, 503)\n }\n\n logger.error({ err }, 'Unexpected database error')\n throw new AppError({ code: ErrorCodes.SYSTEM_INTERNAL_ERROR, message: 'Internal database error' }, 500)\n}\n\n/**\n * Knex-based implementation of DatabaseAdapter.\n *\n * @example\n * const adapter = new KnexAdapter(knexInstance)\n * const users = await adapter.findMany<User>('users', {\n * filters: { status: { $eq: 'active' } },\n * sort: 'createdAt',\n * order: 'desc'\n * })\n */\nexport class KnexAdapter implements DatabaseAdapter {\n constructor(private readonly knex: Knex) {}\n\n /**\n * Raw access to Knex instance.\n * Use sparingly - prefer typed methods.\n */\n get raw(): unknown {\n return this.knex\n }\n\n async findMany<T = unknown>(table: string, query?: EntityQuery): Promise<PaginatedResult<T>> {\n try {\n const maxLimit = query?.maxLimit ?? DEFAULT_MAX_LIMIT\n const page = Math.max(1, query?.page ?? DEFAULT_PAGE)\n const limit = Math.min(maxLimit, Math.max(1, query?.limit ?? DEFAULT_LIMIT))\n const offset = (page - 1) * limit\n\n // Build base query\n let qb = this.knex(table)\n\n // Apply filters\n if (query?.filters) {\n qb = applyFilters(qb, query.filters)\n }\n\n // Get total count\n const countResult = await qb.clone().count('* as count').first<{ count: string | number }>()\n const total = typeof countResult?.count === 'string'\n ? parseInt(countResult.count, 10)\n : (countResult?.count ?? 0)\n\n // Apply sorting\n if (query?.sort) {\n qb = qb.orderBy(query.sort, query.order ?? 'asc')\n }\n\n // Apply pagination\n const items = await qb.limit(limit).offset(offset) as T[]\n\n const totalPages = Math.ceil(total / limit)\n\n return {\n items,\n total,\n page,\n limit,\n totalPages,\n hasNext: page < totalPages\n }\n } catch (err) {\n handleDbError(err)\n }\n }\n\n async findOne<T = unknown>(table: string, filters: Record<string, unknown>): Promise<T | null> {\n try {\n let qb = this.knex(table)\n qb = applyFilters(qb, filters)\n const result = await qb.first()\n return (result as T) ?? null\n } catch (err) {\n handleDbError(err)\n }\n }\n\n async findById<T = unknown>(table: string, id: string): Promise<T | null> {\n try {\n const result = await this.knex(table).where('id', id).first()\n return (result as T) ?? null\n } catch (err) {\n handleDbError(err)\n }\n }\n\n async count(table: string, filters?: Record<string, unknown>): Promise<number> {\n let qb = this.knex(table)\n\n if (filters) {\n qb = applyFilters(qb, filters)\n }\n\n const result = await qb.count('* as count').first<{ count: string | number }>()\n return typeof result?.count === 'string'\n ? parseInt(result.count, 10)\n : (result?.count ?? 0)\n }\n\n async insert<T = unknown>(table: string, data: Record<string, unknown>): Promise<T> {\n try {\n const [id] = await this.knex(table).insert(data).returning('id')\n // For SQLite, returning() returns the id directly; for others, it's an object\n const insertedId = typeof id === 'object' ? (id as { id: string }).id : id\n const result = await this.findById<T>(table, insertedId)\n if (!result) {\n throw new AppError({ code: ErrorCodes.SYSTEM_INTERNAL_ERROR, message: `Insert succeeded but row not found: ${table}/${insertedId}` }, 500)\n }\n return result\n } catch (err) {\n handleDbError(err)\n }\n }\n\n async update<T = unknown>(table: string, id: string, data: Record<string, unknown>): Promise<T> {\n try {\n await this.knex(table).where('id', id).update(data)\n const result = await this.findById<T>(table, id)\n if (!result) {\n throw new AppError({ code: ErrorCodes.RESOURCE_NOT_FOUND, message: `Row not found after update: ${table}/${id}` }, 404)\n }\n return result\n } catch (err) {\n handleDbError(err)\n }\n }\n\n async delete(table: string, id: string): Promise<boolean> {\n try {\n const deleted = await this.knex(table).where('id', id).delete()\n return deleted > 0\n } catch (err) {\n handleDbError(err)\n }\n }\n\n async transaction<T>(fn: (trx: DatabaseAdapter) => Promise<T>): Promise<T> {\n return this.knex.transaction(async (trx) => {\n const trxAdapter = new KnexAdapter(trx as unknown as Knex)\n return fn(trxAdapter)\n })\n }\n}\n\n/**\n * Create a KnexAdapter instance.\n *\n * @param knex - Knex instance\n * @returns DatabaseAdapter implementation\n */\nexport function createKnexAdapter(knex: Knex): DatabaseAdapter {\n return new KnexAdapter(knex)\n}\n","/**\n * In-Memory adapter with TTL support.\n *\n * Used exclusively by TempService for temporary entities with automatic expiration.\n * Persistent entities (collection, reference, etc.) use Knex SQLite :memory: instead.\n */\n\nimport type {\n DatabaseAdapter,\n PaginatedResult,\n EntityQuery\n} from '@gzl10/nexus-sdk'\nimport { applyInMemoryFilters } from './filter-helpers.js'\n\nconst DEFAULT_PAGE = 1\nconst DEFAULT_LIMIT = 20\nconst DEFAULT_MAX_LIMIT = 10000\n\nfunction generateId(): string {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {\n const r = Math.random() * 16 | 0\n const v = c === 'x' ? r : (r & 0x3 | 0x8)\n return v.toString(16)\n })\n}\n\ninterface StoredRecord {\n id: string\n created_at: string\n updated_at: string\n [key: string]: unknown\n}\n\ninterface TtlMetadata {\n expiresAt: number\n timerId: ReturnType<typeof setTimeout>\n}\n\nexport class InMemoryAdapter implements DatabaseAdapter {\n private tables: Map<string, Map<string, StoredRecord>> = new Map()\n private ttlMap: Map<string, TtlMetadata> = new Map()\n\n get raw(): unknown {\n return this.tables\n }\n\n clear(): void {\n for (const metadata of this.ttlMap.values()) {\n clearTimeout(metadata.timerId)\n }\n this.ttlMap.clear()\n this.tables.clear()\n }\n\n clearTable(table: string): void {\n this.tables.delete(table)\n }\n\n seed<T extends Record<string, unknown>>(table: string, records: T[]): void {\n const tableData = this.getOrCreateTable(table)\n for (const record of records) {\n const id = (record['id'] as string) || generateId()\n const now = new Date().toISOString()\n tableData.set(id, {\n ...record,\n id,\n created_at: (record['created_at'] as string) || now,\n updated_at: (record['updated_at'] as string) || now\n })\n }\n }\n\n private getOrCreateTable(table: string): Map<string, StoredRecord> {\n let tableData = this.tables.get(table)\n if (!tableData) {\n tableData = new Map()\n this.tables.set(table, tableData)\n }\n return tableData\n }\n\n // ---- DatabaseAdapter interface ----\n\n async findMany<T = unknown>(table: string, query?: EntityQuery): Promise<PaginatedResult<T>> {\n const maxLimit = query?.maxLimit ?? DEFAULT_MAX_LIMIT\n const page = Math.max(1, query?.page ?? DEFAULT_PAGE)\n const limit = Math.min(maxLimit, Math.max(1, query?.limit ?? DEFAULT_LIMIT))\n const offset = (page - 1) * limit\n\n const tableData = this.tables.get(table)\n if (!tableData) {\n return { items: [], total: 0, page, limit, totalPages: 0, hasNext: false }\n }\n\n let items = Array.from(tableData.values()) as T[]\n\n // Apply filters\n if (query?.filters) {\n items = applyInMemoryFilters(items, query.filters)\n }\n\n // Basic sorting\n if (query?.sort) {\n const sortField = query.sort\n const order = query.order ?? 'asc'\n items.sort((a, b) => {\n const aVal = (a as Record<string, unknown>)[sortField]\n const bVal = (b as Record<string, unknown>)[sortField]\n if (aVal === bVal) return 0\n if (aVal === null || aVal === undefined) return 1\n if (bVal === null || bVal === undefined) return -1\n const comparison = aVal < bVal ? -1 : 1\n return order === 'asc' ? comparison : -comparison\n })\n }\n\n const total = items.length\n items = items.slice(offset, offset + limit)\n const totalPages = Math.ceil(total / limit)\n\n return { items, total, page, limit, totalPages, hasNext: page < totalPages }\n }\n\n async findOne<T = unknown>(table: string, filters: Record<string, unknown>): Promise<T | null> {\n const tableData = this.tables.get(table)\n if (!tableData) return null\n for (const record of tableData.values()) {\n const match = Object.entries(filters).every(([k, v]) => record[k] === v)\n if (match) return record as T\n }\n return null\n }\n\n async findById<T = unknown>(table: string, id: string): Promise<T | null> {\n const tableData = this.tables.get(table)\n if (!tableData) return null\n return (tableData.get(id) as T) ?? null\n }\n\n async count(table: string, filters?: Record<string, unknown>): Promise<number> {\n const tableData = this.tables.get(table)\n if (!tableData) return 0\n if (!filters || Object.keys(filters).length === 0) return tableData.size\n const items = applyInMemoryFilters(Array.from(tableData.values()), filters)\n return items.length\n }\n\n async insert<T = unknown>(table: string, data: Record<string, unknown>): Promise<T> {\n const tableData = this.getOrCreateTable(table)\n const id = (data['id'] as string) || generateId()\n const now = new Date().toISOString()\n const record: StoredRecord = { ...data, id, created_at: now, updated_at: now }\n tableData.set(id, record)\n return record as T\n }\n\n async update<T = unknown>(table: string, id: string, data: Record<string, unknown>): Promise<T> {\n const tableData = this.tables.get(table)\n if (!tableData) throw new Error(`Table \"${table}\" not found`)\n const existing = tableData.get(id)\n if (!existing) throw new Error(`Record \"${id}\" not found in table \"${table}\"`)\n const updated: StoredRecord = {\n ...existing, ...data,\n id,\n created_at: existing.created_at,\n updated_at: new Date().toISOString()\n }\n tableData.set(id, updated)\n return updated as T\n }\n\n async delete(table: string, id: string): Promise<boolean> {\n const tableData = this.tables.get(table)\n if (!tableData) return false\n return tableData.delete(id)\n }\n\n async transaction<T>(fn: (trx: DatabaseAdapter) => Promise<T>): Promise<T> {\n return fn(this)\n }\n\n // ---- TTL Methods ----\n\n private getTtlKey(table: string, id: string): string {\n return `${table}:${id}`\n }\n\n async insertWithTtl<T = unknown>(table: string, data: Record<string, unknown>, ttlSeconds: number): Promise<T> {\n const tableData = this.getOrCreateTable(table)\n const id = (data['id'] as string) || generateId()\n const now = new Date().toISOString()\n const expires_at = new Date(Date.now() + ttlSeconds * 1000).toISOString()\n const record: StoredRecord = { ...data, id, created_at: now, updated_at: now, expires_at }\n tableData.set(id, record)\n\n const ttlKey = this.getTtlKey(table, id)\n const timerId = setTimeout(() => this.deleteWithTtlCleanup(table, id), ttlSeconds * 1000)\n this.ttlMap.set(ttlKey, { expiresAt: Date.now() + ttlSeconds * 1000, timerId })\n\n return record as T\n }\n\n private deleteWithTtlCleanup(table: string, id: string): void {\n const tableData = this.tables.get(table)\n if (tableData) tableData.delete(id)\n const ttlKey = this.getTtlKey(table, id)\n const metadata = this.ttlMap.get(ttlKey)\n if (metadata) {\n clearTimeout(metadata.timerId)\n this.ttlMap.delete(ttlKey)\n }\n }\n\n async setTtl(table: string, id: string, ttlSeconds: number): Promise<boolean> {\n const tableData = this.tables.get(table)\n if (!tableData) return false\n const record = tableData.get(id)\n if (!record) return false\n\n record['expires_at'] = new Date(Date.now() + ttlSeconds * 1000).toISOString()\n record['updated_at'] = new Date().toISOString()\n\n const ttlKey = this.getTtlKey(table, id)\n const existing = this.ttlMap.get(ttlKey)\n if (existing) clearTimeout(existing.timerId)\n\n const timerId = setTimeout(() => this.deleteWithTtlCleanup(table, id), ttlSeconds * 1000)\n this.ttlMap.set(ttlKey, { expiresAt: Date.now() + ttlSeconds * 1000, timerId })\n return true\n }\n\n async getTtl(table: string, id: string): Promise<number> {\n const tableData = this.tables.get(table)\n if (!tableData || !tableData.has(id)) return -2\n\n const ttlKey = this.getTtlKey(table, id)\n const metadata = this.ttlMap.get(ttlKey)\n if (!metadata) return -1\n\n const remainingMs = metadata.expiresAt - Date.now()\n return remainingMs <= 0 ? -2 : Math.ceil(remainingMs / 1000)\n }\n\n async cleanupExpiredIds(_table: string): Promise<number> {\n return 0\n }\n}\n\nexport function createInMemoryAdapter(): InMemoryAdapter {\n return new InMemoryAdapter()\n}\n","/**\n * Redis implementation of DatabaseAdapter.\n *\n * Stores data in Redis with secondary indexes for efficient filtering.\n *\n * Data structure:\n * - {prefix}:{table}:{id} → JSON record\n * - {prefix}:{table}:_ids → Set with all IDs\n * - {prefix}:{table}:_idx:{field}:{value} → Set with IDs having that field value (secondary index)\n *\n * Performance characteristics:\n * - findById: O(1) - direct key lookup\n * - findMany with $eq filters: O(k) where k = matching records (uses index intersection)\n * - findMany with complex filters: O(n) where n = total records (loads all, filters in memory)\n */\n\nimport { Redis } from 'ioredis'\nimport type {\n DatabaseAdapter,\n PaginatedResult,\n EntityQuery,\n FilterOperators\n} from '@gzl10/nexus-sdk'\nimport { applyInMemoryFilters } from './filter-helpers.js'\nimport { NotFoundError } from '../core/errors/app-error.js'\n\nconst DEFAULT_PAGE = 1\nconst DEFAULT_LIMIT = 20\nconst DEFAULT_MAX_LIMIT = 100\n\n/** Fields that should not be indexed (internal/large) */\nconst NON_INDEXABLE_FIELDS = new Set(['id', 'createdAt', 'updatedAt', 'expires_at'])\n\n/** Maximum value length for indexing (avoid huge index keys) */\nconst MAX_INDEX_VALUE_LENGTH = 256\n\nfunction generateId(): string {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {\n const r = Math.random() * 16 | 0\n const v = c === 'x' ? r : (r & 0x3 | 0x8)\n return v.toString(16)\n })\n}\n\nexport interface RedisAdapterOptions {\n /** Redis connection URL (e.g., redis://localhost:6379) */\n url?: string\n /** Key prefix for all Redis keys (default: 'nexus') */\n prefix?: string\n /** Existing Redis client instance (takes precedence over url) */\n client?: Redis\n /** Enable secondary indexes for faster filtering (default: true) */\n enableIndexes?: boolean\n}\n\ninterface StoredRecord {\n id: string\n createdAt: string\n updatedAt: string\n [key: string]: unknown\n}\n\n/** Result of analyzing filters for index usage */\ninterface FilterAnalysis {\n /** Filters that can use indexes (field → values) */\n indexable: Map<string, unknown[]>\n /** Remaining filters that need in-memory processing */\n remaining: Record<string, unknown>\n /** Whether all filters can be handled by indexes */\n fullyIndexable: boolean\n}\n\n/**\n * Redis database adapter with secondary indexes.\n *\n * @example\n * const adapter = createRedisAdapter({ url: 'redis://localhost:6379' })\n *\n * // Insert (automatically creates indexes)\n * const user = await adapter.insert('users', { name: 'John', status: 'active' })\n *\n * // Fast query using index\n * const result = await adapter.findMany<User>('users', {\n * filters: { status: 'active' } // Uses index: O(k) where k = active users\n * })\n *\n * // Complex query (falls back to scan)\n * const result = await adapter.findMany<User>('users', {\n * filters: { name: { $contains: 'john' } } // Scans all: O(n)\n * })\n */\nexport class RedisAdapter implements DatabaseAdapter {\n private client: Redis\n private prefix: string\n private ownsClient: boolean\n private enableIndexes: boolean\n\n constructor(options: RedisAdapterOptions = {}) {\n this.prefix = options.prefix ?? 'nexus'\n this.enableIndexes = options.enableIndexes ?? true\n\n if (options.client) {\n this.client = options.client\n this.ownsClient = false\n } else {\n this.client = new Redis(options.url ?? 'redis://localhost:6379', {\n lazyConnect: true,\n maxRetriesPerRequest: 3\n })\n this.ownsClient = true\n }\n }\n\n get raw(): Redis {\n return this.client\n }\n\n async connect(): Promise<void> {\n if (this.client.status === 'ready') return\n if (this.client.status === 'connecting') {\n await new Promise<void>((resolve, reject) => {\n this.client.once('ready', resolve)\n this.client.once('error', reject)\n })\n return\n }\n await this.client.connect()\n }\n\n async disconnect(): Promise<void> {\n if (this.ownsClient && this.client.status !== 'end') {\n await this.client.quit()\n }\n }\n\n async clear(): Promise<void> {\n const pattern = `${this.prefix}:*`\n const keys = await this.client.keys(pattern)\n if (keys.length > 0) {\n await this.client.del(...keys)\n }\n }\n\n async clearTable(table: string): Promise<void> {\n const pattern = `${this.prefix}:${table}:*`\n const keys = await this.client.keys(pattern)\n if (keys.length > 0) {\n await this.client.del(...keys)\n }\n }\n\n async seed<T extends Record<string, unknown>>(table: string, records: T[]): Promise<void> {\n for (const record of records) {\n await this.insert(table, record)\n }\n }\n\n // ===========================================================================\n // Key helpers\n // ===========================================================================\n\n private getRecordKey(table: string, id: string): string {\n return `${this.prefix}:${table}:${id}`\n }\n\n private getIdsKey(table: string): string {\n return `${this.prefix}:${table}:_ids`\n }\n\n private getIndexKey(table: string, field: string, value: unknown): string {\n const normalizedValue = this.normalizeIndexValue(value)\n return `${this.prefix}:${table}:_idx:${field}:${normalizedValue}`\n }\n\n private normalizeIndexValue(value: unknown): string {\n if (value === null) return '__null__'\n if (value === undefined) return '__undefined__'\n if (typeof value === 'boolean') return value ? '__true__' : '__false__'\n const str = String(value)\n return str.length > MAX_INDEX_VALUE_LENGTH ? str.substring(0, MAX_INDEX_VALUE_LENGTH) : str\n }\n\n private isIndexableValue(value: unknown): boolean {\n if (value === null || value === undefined) return true\n if (typeof value === 'string') return value.length <= MAX_INDEX_VALUE_LENGTH\n if (typeof value === 'number' || typeof value === 'boolean') return true\n return false\n }\n\n private parseRecord<T>(data: string | null): T | null {\n if (!data) return null\n try {\n return JSON.parse(data) as T\n } catch {\n return null\n }\n }\n\n // ===========================================================================\n // Index management\n // ===========================================================================\n\n /**\n * Add record to all applicable secondary indexes\n */\n private async addToIndexes(table: string, id: string, record: Record<string, unknown>, pipeline?: ReturnType<Redis['pipeline']>): Promise<void> {\n if (!this.enableIndexes) return\n\n const pipe = pipeline ?? this.client.pipeline()\n const shouldExec = !pipeline\n\n for (const [field, value] of Object.entries(record)) {\n if (NON_INDEXABLE_FIELDS.has(field)) continue\n if (!this.isIndexableValue(value)) continue\n\n const indexKey = this.getIndexKey(table, field, value)\n pipe.sadd(indexKey, id)\n }\n\n if (shouldExec) {\n await pipe.exec()\n }\n }\n\n /**\n * Remove record from all its secondary indexes\n */\n private async removeFromIndexes(table: string, id: string, record: Record<string, unknown>, pipeline?: ReturnType<Redis['pipeline']>): Promise<void> {\n if (!this.enableIndexes) return\n\n const pipe = pipeline ?? this.client.pipeline()\n const shouldExec = !pipeline\n\n for (const [field, value] of Object.entries(record)) {\n if (NON_INDEXABLE_FIELDS.has(field)) continue\n if (!this.isIndexableValue(value)) continue\n\n const indexKey = this.getIndexKey(table, field, value)\n pipe.srem(indexKey, id)\n }\n\n if (shouldExec) {\n await pipe.exec()\n }\n }\n\n /**\n * Update indexes when record fields change\n */\n private async updateIndexes(\n table: string,\n id: string,\n oldRecord: Record<string, unknown>,\n newRecord: Record<string, unknown>,\n pipeline?: ReturnType<Redis['pipeline']>\n ): Promise<void> {\n if (!this.enableIndexes) return\n\n const pipe = pipeline ?? this.client.pipeline()\n const shouldExec = !pipeline\n\n for (const [field, newValue] of Object.entries(newRecord)) {\n if (NON_INDEXABLE_FIELDS.has(field)) continue\n\n const oldValue = oldRecord[field]\n if (oldValue === newValue) continue\n\n // Remove from old index\n if (this.isIndexableValue(oldValue)) {\n const oldIndexKey = this.getIndexKey(table, field, oldValue)\n pipe.srem(oldIndexKey, id)\n }\n\n // Add to new index\n if (this.isIndexableValue(newValue)) {\n const newIndexKey = this.getIndexKey(table, field, newValue)\n pipe.sadd(newIndexKey, id)\n }\n }\n\n if (shouldExec) {\n await pipe.exec()\n }\n }\n\n // ===========================================================================\n // Filter analysis\n // ===========================================================================\n\n /**\n * Analyze filters to determine which can use indexes\n */\n private analyzeFilters(filters: Record<string, unknown>): FilterAnalysis {\n const indexable = new Map<string, unknown[]>()\n const remaining: Record<string, unknown> = {}\n let fullyIndexable = true\n\n for (const [field, condition] of Object.entries(filters)) {\n if (NON_INDEXABLE_FIELDS.has(field)) {\n remaining[field] = condition\n fullyIndexable = false\n continue\n }\n\n // Direct value: { status: 'active' } → $eq\n if (this.isIndexableValue(condition)) {\n indexable.set(field, [condition])\n continue\n }\n\n // Array shorthand: { status: ['active', 'pending'] } → $in\n if (Array.isArray(condition)) {\n const indexableValues = condition.filter(v => this.isIndexableValue(v))\n if (indexableValues.length === condition.length) {\n indexable.set(field, indexableValues)\n continue\n }\n }\n\n // Operator object: { status: { $eq: 'active' } }\n if (typeof condition === 'object' && condition !== null) {\n const ops = condition as FilterOperators\n\n // $eq is indexable\n if ('$eq' in ops && this.isIndexableValue(ops.$eq)) {\n indexable.set(field, [ops.$eq])\n // Check if there are other operators\n const otherOps = Object.keys(ops).filter(k => k !== '$eq')\n if (otherOps.length > 0) {\n remaining[field] = condition\n fullyIndexable = false\n }\n continue\n }\n\n // $in is indexable\n if ('$in' in ops && Array.isArray(ops.$in)) {\n const indexableValues = ops.$in.filter(v => this.isIndexableValue(v))\n if (indexableValues.length === ops.$in.length) {\n indexable.set(field, indexableValues)\n // Check if there are other operators\n const otherOps = Object.keys(ops).filter(k => k !== '$in')\n if (otherOps.length > 0) {\n remaining[field] = condition\n fullyIndexable = false\n }\n continue\n }\n }\n }\n\n // Not indexable\n remaining[field] = condition\n fullyIndexable = false\n }\n\n return { indexable, remaining, fullyIndexable }\n }\n\n /**\n * Get candidate IDs using secondary indexes.\n * Uses Redis SINTER when all fields have single-value equality filters.\n * Falls back to JS intersection when $in (multi-value) filters are involved.\n */\n private async getCandidateIds(table: string, indexable: Map<string, unknown[]>): Promise<string[] | null> {\n if (indexable.size === 0) return null\n\n // Fast path: all fields have single value → use native SINTER\n const allSingleValue = [...indexable.values()].every(v => v.length === 1)\n if (allSingleValue && indexable.size >= 2) {\n const keys = [...indexable].map(([field, values]) => this.getIndexKey(table, field, values[0]))\n return this.client.sinter(...keys)\n }\n\n // Collect ID sets for each filter field\n const fieldSets: Set<string>[] = []\n\n for (const [field, values] of indexable) {\n if (values.length === 1) {\n // Single value: direct lookup\n const indexKey = this.getIndexKey(table, field, values[0])\n const ids = await this.client.smembers(indexKey)\n fieldSets.push(new Set(ids))\n } else {\n // Multiple values ($in): union of all value indexes\n const unionSet = new Set<string>()\n for (const value of values) {\n const indexKey = this.getIndexKey(table, field, value)\n const ids = await this.client.smembers(indexKey)\n for (const id of ids) unionSet.add(id)\n }\n fieldSets.push(unionSet)\n }\n }\n\n if (fieldSets.length === 0) return null\n if (fieldSets.length === 1) return Array.from(fieldSets[0]!)\n\n // Intersect all field sets (AND logic between different fields)\n let result = fieldSets[0]!\n for (let i = 1; i < fieldSets.length; i++) {\n const nextSet = fieldSets[i]!\n result = new Set([...result].filter(id => nextSet.has(id)))\n }\n\n return Array.from(result)\n }\n\n // ===========================================================================\n // CRUD operations\n // ===========================================================================\n\n async findMany<T = unknown>(table: string, query?: EntityQuery): Promise<PaginatedResult<T>> {\n const maxLimit = query?.maxLimit ?? DEFAULT_MAX_LIMIT\n const page = Math.max(1, query?.page ?? DEFAULT_PAGE)\n const limit = Math.min(maxLimit, Math.max(1, query?.limit ?? DEFAULT_LIMIT))\n const offset = (page - 1) * limit\n\n let candidateIds: string[] | null = null\n let remainingFilters: Record<string, unknown> | undefined\n\n // Try to use indexes for filtering\n if (query?.filters && this.enableIndexes) {\n const analysis = this.analyzeFilters(query.filters)\n\n if (analysis.indexable.size > 0) {\n candidateIds = await this.getCandidateIds(table, analysis.indexable)\n\n // No matches from index → empty result\n if (candidateIds && candidateIds.length === 0) {\n return { items: [], total: 0, page, limit, totalPages: 0, hasNext: false }\n }\n\n // If there are remaining filters, we need to apply them in memory\n if (Object.keys(analysis.remaining).length > 0) {\n remainingFilters = analysis.remaining\n }\n } else {\n // No indexable filters, will need full scan\n remainingFilters = query.filters\n }\n } else if (query?.filters) {\n remainingFilters = query.filters\n }\n\n // Get IDs to fetch\n let idsToFetch: string[]\n if (candidateIds !== null) {\n idsToFetch = candidateIds\n } else {\n const idsKey = this.getIdsKey(table)\n idsToFetch = await this.client.smembers(idsKey)\n }\n\n if (idsToFetch.length === 0) {\n return { items: [], total: 0, page, limit, totalPages: 0, hasNext: false }\n }\n\n // Fetch records\n const pipeline = this.client.pipeline()\n for (const id of idsToFetch) {\n pipeline.get(this.getRecordKey(table, id))\n }\n const results = await pipeline.exec()\n\n // Parse records\n let items: T[] = []\n if (results) {\n for (const result of results) {\n if (result) {\n const [err, data] = result\n if (!err && data) {\n const record = this.parseRecord<T>(data as string)\n if (record) items.push(record)\n }\n }\n }\n }\n\n // Apply remaining filters in memory (if any)\n if (remainingFilters && Object.keys(remainingFilters).length > 0) {\n items = applyInMemoryFilters(items, remainingFilters)\n }\n\n const total = items.length\n\n // Apply sorting\n if (query?.sort) {\n const sortField = query.sort\n const order = query.order ?? 'asc'\n items.sort((a, b) => {\n const aVal = (a as Record<string, unknown>)[sortField]\n const bVal = (b as Record<string, unknown>)[sortField]\n if (aVal === bVal) return 0\n if (aVal === null || aVal === undefined) return 1\n if (bVal === null || bVal === undefined) return -1\n const comparison = aVal < bVal ? -1 : 1\n return order === 'asc' ? comparison : -comparison\n })\n }\n\n // Apply pagination\n items = items.slice(offset, offset + limit)\n\n const totalPages = Math.ceil(total / limit)\n\n return {\n items,\n total,\n page,\n limit,\n totalPages,\n hasNext: page < totalPages\n }\n }\n\n async findOne<T = unknown>(table: string, filters: Record<string, unknown>): Promise<T | null> {\n const result = await this.findMany<T>(table, { filters, limit: 1 })\n return result.items[0] ?? null\n }\n\n async findById<T = unknown>(table: string, id: string): Promise<T | null> {\n const key = this.getRecordKey(table, id)\n const data = await this.client.get(key)\n return this.parseRecord<T>(data)\n }\n\n async count(table: string, filters?: Record<string, unknown>): Promise<number> {\n if (!filters) {\n const idsKey = this.getIdsKey(table)\n return await this.client.scard(idsKey)\n }\n\n // With filters, use the optimized findMany\n const result = await this.findMany(table, { filters, limit: 1 })\n return result.total\n }\n\n async insert<T = unknown>(table: string, data: Record<string, unknown>): Promise<T> {\n const id = (data['id'] as string) || generateId()\n const now = new Date().toISOString()\n\n const record: StoredRecord = {\n ...data,\n id,\n createdAt: now,\n updatedAt: now\n }\n\n const key = this.getRecordKey(table, id)\n const idsKey = this.getIdsKey(table)\n\n const pipeline = this.client.pipeline()\n pipeline.set(key, JSON.stringify(record))\n pipeline.sadd(idsKey, id)\n\n // Add to secondary indexes\n await this.addToIndexes(table, id, record, pipeline)\n\n await pipeline.exec()\n\n return record as T\n }\n\n async update<T = unknown>(table: string, id: string, data: Record<string, unknown>): Promise<T> {\n const key = this.getRecordKey(table, id)\n const existingData = await this.client.get(key)\n\n if (!existingData) {\n throw new NotFoundError(`Record \"${id}\" in table \"${table}\"`)\n }\n\n const existing = JSON.parse(existingData) as StoredRecord\n\n const updated: StoredRecord = {\n ...existing,\n ...data,\n id,\n createdAt: existing.createdAt,\n updatedAt: new Date().toISOString()\n }\n\n const pipeline = this.client.pipeline()\n pipeline.set(key, JSON.stringify(updated))\n\n // Update secondary indexes\n await this.updateIndexes(table, id, existing, updated, pipeline)\n\n await pipeline.exec()\n\n return updated as T\n }\n\n async delete(table: string, id: string): Promise<boolean> {\n const key = this.getRecordKey(table, id)\n const idsKey = this.getIdsKey(table)\n\n // Get record for index cleanup\n const existingData = await this.client.get(key)\n if (!existingData) return false\n\n const existing = JSON.parse(existingData) as StoredRecord\n\n const pipeline = this.client.pipeline()\n pipeline.del(key)\n pipeline.srem(idsKey, id)\n\n // Remove from secondary indexes\n await this.removeFromIndexes(table, id, existing, pipeline)\n\n const results = await pipeline.exec()\n return results !== null && results.length > 0\n }\n\n async transaction<T>(fn: (trx: DatabaseAdapter) => Promise<T>): Promise<T> {\n // Redis doesn't support SQL-style transactions with rollback\n return fn(this)\n }\n\n // ===========================================================================\n // TTL Methods\n // ===========================================================================\n\n async insertWithTtl<T = unknown>(table: string, data: Record<string, unknown>, ttlSeconds: number): Promise<T> {\n const id = (data['id'] as string) || generateId()\n const now = new Date().toISOString()\n const expires_at = new Date(Date.now() + ttlSeconds * 1000).toISOString()\n\n const record: StoredRecord = {\n ...data,\n id,\n createdAt: now,\n updatedAt: now,\n expires_at\n }\n\n const key = this.getRecordKey(table, id)\n const idsKey = this.getIdsKey(table)\n\n const pipeline = this.client.pipeline()\n pipeline.setex(key, ttlSeconds, JSON.stringify(record))\n pipeline.sadd(idsKey, id)\n\n // Add to secondary indexes (they'll need cleanup when record expires)\n await this.addToIndexes(table, id, record, pipeline)\n\n await pipeline.exec()\n\n return record as T\n }\n\n async setTtl(table: string, id: string, ttlSeconds: number): Promise<boolean> {\n const key = this.getRecordKey(table, id)\n\n const existingData = await this.client.get(key)\n if (!existingData) return false\n\n const existing = JSON.parse(existingData) as StoredRecord\n const expires_at = new Date(Date.now() + ttlSeconds * 1000).toISOString()\n\n const updated: StoredRecord = {\n ...existing,\n expires_at,\n updatedAt: new Date().toISOString()\n }\n\n await this.client.setex(key, ttlSeconds, JSON.stringify(updated))\n\n return true\n }\n\n async getTtl(table: string, id: string): Promise<number | null> {\n const key = this.getRecordKey(table, id)\n const ttl = await this.client.ttl(key)\n return ttl\n }\n\n async cleanupExpiredIds(table: string): Promise<number> {\n const idsKey = this.getIdsKey(table)\n const allIds = await this.client.smembers(idsKey)\n\n if (allIds.length === 0) return 0\n\n // Check which IDs still exist\n const pipeline = this.client.pipeline()\n for (const id of allIds) {\n pipeline.exists(this.getRecordKey(table, id))\n }\n const results = await pipeline.exec()\n\n // Collect expired IDs\n const expiredIds: string[] = []\n if (results) {\n for (let i = 0; i < results.length; i++) {\n const [err, exists] = results[i]!\n if (!err && exists === 0) {\n expiredIds.push(allIds[i]!)\n }\n }\n }\n\n // Remove expired IDs from the ID set\n // Note: Index cleanup for expired records happens lazily\n if (expiredIds.length > 0) {\n await this.client.srem(idsKey, ...expiredIds)\n }\n\n return expiredIds.length\n }\n\n /**\n * Rebuild all indexes for a table.\n * Useful after bulk imports or when indexes get out of sync.\n */\n async rebuildIndexes(table: string): Promise<number> {\n // Delete existing indexes\n const indexPattern = `${this.prefix}:${table}:_idx:*`\n const indexKeys = await this.client.keys(indexPattern)\n if (indexKeys.length > 0) {\n await this.client.del(...indexKeys)\n }\n\n // Rebuild from all records\n const idsKey = this.getIdsKey(table)\n const allIds = await this.client.smembers(idsKey)\n\n if (allIds.length === 0) return 0\n\n const pipeline = this.client.pipeline()\n for (const id of allIds) {\n pipeline.get(this.getRecordKey(table, id))\n }\n const results = await pipeline.exec()\n\n let indexedCount = 0\n if (results) {\n for (const result of results) {\n if (result) {\n const [err, data] = result\n if (!err && data) {\n const record = this.parseRecord<Record<string, unknown>>(data as string)\n if (record) {\n await this.addToIndexes(table, record['id'] as string, record)\n indexedCount++\n }\n }\n }\n }\n }\n\n return indexedCount\n }\n}\n\nexport function createRedisAdapter(options?: RedisAdapterOptions): RedisAdapter {\n return new RedisAdapter(options)\n}\n","/**\n * Knex implementation of SchemaAdapter.\n *\n * Provides normalized DDL operations using Knex schema builder.\n * Supports SQLite, MySQL, and PostgreSQL with database-specific adaptations.\n */\n\nimport type { Knex } from 'knex'\nimport type {\n SchemaAdapter,\n SchemaTableBuilder,\n SchemaAlterTableBuilder,\n SchemaColumnBuilder,\n SchemaColumnInfo,\n SchemaForeignKeyBuilder\n} from '@gzl10/nexus-sdk'\nimport { logger } from '../core/index.js'\nimport { getDbClient } from './schema-helpers.js'\n\n/**\n * Knex column builder wrapper implementing SchemaColumnBuilder.\n */\nclass KnexColumnBuilder implements SchemaColumnBuilder {\n constructor(private column: Knex.ColumnBuilder) {}\n\n primary(): SchemaColumnBuilder {\n this.column.primary()\n return this\n }\n\n unique(): SchemaColumnBuilder {\n this.column.unique()\n return this\n }\n\n index(): SchemaColumnBuilder {\n this.column.index()\n return this\n }\n\n nullable(): SchemaColumnBuilder {\n this.column.nullable()\n return this\n }\n\n notNullable(): SchemaColumnBuilder {\n this.column.notNullable()\n return this\n }\n\n defaultTo(value: unknown): SchemaColumnBuilder {\n this.column.defaultTo(value as Knex.Value)\n return this\n }\n\n references(column: string): SchemaColumnBuilder {\n this.column.references(column)\n return this\n }\n\n inTable(table: string): SchemaColumnBuilder {\n ;(this.column as Knex.ReferencingColumnBuilder).inTable(table)\n return this\n }\n\n onDelete(action: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION'): SchemaColumnBuilder {\n ;(this.column as Knex.ReferencingColumnBuilder).onDelete(action)\n return this\n }\n}\n\n/**\n * Knex foreign key builder wrapper implementing SchemaForeignKeyBuilder.\n * Note: Knex foreign() returns ForeignConstraintBuilder, references() returns ReferencingColumnBuilder\n */\nclass KnexForeignKeyBuilder implements SchemaForeignKeyBuilder {\n private refBuilder: Knex.ReferencingColumnBuilder | null = null\n\n constructor(private fk: Knex.ForeignConstraintBuilder) {}\n\n references(column: string): SchemaForeignKeyBuilder {\n this.refBuilder = this.fk.references(column)\n return this\n }\n\n inTable(table: string): SchemaForeignKeyBuilder {\n if (this.refBuilder) {\n this.refBuilder.inTable(table)\n }\n return this\n }\n\n onDelete(action: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION'): SchemaForeignKeyBuilder {\n if (this.refBuilder) {\n this.refBuilder.onDelete(action)\n }\n return this\n }\n\n onUpdate(action: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION'): SchemaForeignKeyBuilder {\n if (this.refBuilder) {\n this.refBuilder.onUpdate(action)\n }\n return this\n }\n}\n\n/**\n * Knex table builder wrapper implementing SchemaTableBuilder.\n */\nclass KnexTableBuilder implements SchemaTableBuilder {\n constructor(\n protected table: Knex.CreateTableBuilder | Knex.AlterTableBuilder,\n protected knex: Knex\n ) {}\n\n string(name: string, size?: number): SchemaColumnBuilder {\n return new KnexColumnBuilder(this.table.string(name, size))\n }\n\n text(name: string): SchemaColumnBuilder {\n return new KnexColumnBuilder(this.table.text(name))\n }\n\n integer(name: string): SchemaColumnBuilder {\n return new KnexColumnBuilder(this.table.integer(name))\n }\n\n decimal(name: string, precision?: number, scale?: number): SchemaColumnBuilder {\n return new KnexColumnBuilder(this.table.decimal(name, precision, scale))\n }\n\n boolean(name: string): SchemaColumnBuilder {\n return new KnexColumnBuilder(this.table.boolean(name))\n }\n\n date(name: string): SchemaColumnBuilder {\n return new KnexColumnBuilder(this.table.date(name))\n }\n\n datetime(name: string): SchemaColumnBuilder {\n return new KnexColumnBuilder(this.table.datetime(name))\n }\n\n json(name: string): SchemaColumnBuilder {\n return new KnexColumnBuilder(this.table.json(name))\n }\n\n uuid(name: string): SchemaColumnBuilder {\n return new KnexColumnBuilder(this.table.uuid(name))\n }\n\n bigInteger(name: string): SchemaColumnBuilder {\n return new KnexColumnBuilder(this.table.bigInteger(name))\n }\n\n timestamp(name: string): SchemaColumnBuilder {\n return new KnexColumnBuilder((this.table as Knex.CreateTableBuilder).timestamp(name))\n }\n\n jsonb(name: string): SchemaColumnBuilder {\n return new KnexColumnBuilder(this.table.jsonb(name))\n }\n\n enum(name: string, values: readonly string[]): SchemaColumnBuilder {\n return new KnexColumnBuilder(this.table.enum(name, values as string[]))\n }\n\n binary(name: string, length?: number): SchemaColumnBuilder {\n return new KnexColumnBuilder(this.table.binary(name, length))\n }\n\n float(name: string, precision?: number, scale?: number): SchemaColumnBuilder {\n return new KnexColumnBuilder(this.table.float(name, precision, scale))\n }\n\n specificType(name: string, type: string): SchemaColumnBuilder {\n return new KnexColumnBuilder(this.table.specificType(name, type))\n }\n\n index(columns: string | string[], indexName?: string): void {\n ;(this.table as Knex.CreateTableBuilder).index(columns, indexName)\n }\n\n unique(columns: string | string[], constraintName?: string): void {\n ;(this.table as Knex.CreateTableBuilder).unique(columns, { indexName: constraintName })\n }\n\n primary(columns: string | string[], constraintName?: string): void {\n const cols = Array.isArray(columns) ? columns : [columns]\n ;(this.table as Knex.CreateTableBuilder).primary(cols, { constraintName })\n }\n\n foreign(column: string): SchemaForeignKeyBuilder {\n return new KnexForeignKeyBuilder((this.table as Knex.CreateTableBuilder).foreign(column))\n }\n\n increments(name?: string): SchemaColumnBuilder {\n return new KnexColumnBuilder((this.table as Knex.CreateTableBuilder).increments(name))\n }\n\n bigIncrements(name?: string): SchemaColumnBuilder {\n return new KnexColumnBuilder((this.table as Knex.CreateTableBuilder).bigIncrements(name))\n }\n\n timestamps(): void {\n const client = getDbClient(this.knex)\n\n if (client === 'postgres') {\n ;(this.table as Knex.CreateTableBuilder).timestamp('created_at', { useTz: true }).defaultTo(this.knex.fn.now())\n ;(this.table as Knex.CreateTableBuilder).timestamp('updated_at', { useTz: true }).defaultTo(this.knex.fn.now())\n } else if (client === 'sqlite') {\n this.table.text('created_at').defaultTo(this.knex.raw(\"(datetime('now'))\"))\n this.table.text('updated_at').defaultTo(this.knex.raw(\"(datetime('now'))\"))\n } else {\n // MySQL\n ;(this.table as Knex.CreateTableBuilder).timestamp('created_at').defaultTo(this.knex.fn.now())\n ;(this.table as Knex.CreateTableBuilder).timestamp('updated_at').defaultTo(this.knex.fn.now())\n }\n }\n}\n\n/**\n * Knex alter table builder wrapper implementing SchemaAlterTableBuilder.\n */\nclass KnexAlterTableBuilder extends KnexTableBuilder implements SchemaAlterTableBuilder {\n constructor(table: Knex.AlterTableBuilder, knex: Knex) {\n super(table, knex)\n }\n\n dropColumn(name: string): void {\n ;(this.table as Knex.AlterTableBuilder).dropColumn(name)\n }\n\n renameColumn(from: string, to: string): void {\n ;(this.table as Knex.AlterTableBuilder).renameColumn(from, to)\n }\n}\n\n/**\n * Knex-based implementation of SchemaAdapter.\n *\n * @example\n * const schema = new KnexSchemaAdapter(knexInstance)\n *\n * if (!await schema.hasTable('users')) {\n * await schema.createTable('users', (t) => {\n * t.string('id').primary()\n * t.string('email').unique().notNullable()\n * t.timestamps()\n * })\n * }\n */\nexport class KnexSchemaAdapter implements SchemaAdapter {\n constructor(private knex: Knex) {}\n\n /**\n * Raw access to Knex schema builder.\n */\n get raw(): unknown {\n return this.knex.schema\n }\n\n async hasTable(tableName: string): Promise<boolean> {\n return this.knex.schema.hasTable(tableName)\n }\n\n async hasColumn(tableName: string, columnName: string): Promise<boolean> {\n return this.knex.schema.hasColumn(tableName, columnName)\n }\n\n async getColumns(tableName: string): Promise<SchemaColumnInfo[]> {\n const columnInfo = await this.knex(tableName).columnInfo()\n return Object.entries(columnInfo).map(([name, info]) => ({\n name,\n type: (info as { type: string }).type,\n nullable: (info as { nullable: boolean }).nullable,\n defaultValue: (info as { defaultValue: unknown }).defaultValue\n }))\n }\n\n async createTable(tableName: string, builder: (t: SchemaTableBuilder) => void): Promise<void> {\n await this.knex.schema.createTable(tableName, (table) => {\n builder(new KnexTableBuilder(table, this.knex))\n })\n logger.info(`Created table: ${tableName}`)\n }\n\n async alterTable(tableName: string, builder: (t: SchemaAlterTableBuilder) => void): Promise<void> {\n await this.knex.schema.alterTable(tableName, (table) => {\n builder(new KnexAlterTableBuilder(table, this.knex))\n })\n logger.info(`Altered table: ${tableName}`)\n }\n\n async dropTable(tableName: string): Promise<void> {\n await this.knex.schema.dropTableIfExists(tableName)\n logger.info(`Dropped table: ${tableName}`)\n }\n\n async addTimestamps(tableName: string): Promise<void> {\n const hasCreatedAt = await this.hasColumn(tableName, 'created_at')\n if (hasCreatedAt) return\n\n await this.knex.schema.alterTable(tableName, (table) => {\n const client = getDbClient(this.knex)\n\n if (client === 'postgres') {\n table.timestamp('created_at', { useTz: true }).defaultTo(this.knex.fn.now())\n table.timestamp('updated_at', { useTz: true }).defaultTo(this.knex.fn.now())\n } else if (client === 'sqlite') {\n table.text('created_at').defaultTo(this.knex.raw(\"(datetime('now'))\"))\n table.text('updated_at').defaultTo(this.knex.raw(\"(datetime('now'))\"))\n } else {\n table.timestamp('created_at').defaultTo(this.knex.fn.now())\n table.timestamp('updated_at').defaultTo(this.knex.fn.now())\n }\n })\n logger.info(`Added timestamps to: ${tableName}`)\n }\n\n async addAuditFields(tableName: string): Promise<void> {\n const hasCreatedBy = await this.hasColumn(tableName, 'created_by')\n if (hasCreatedBy) return\n\n await this.knex.schema.alterTable(tableName, (table) => {\n table.string('created_by').nullable()\n table.string('updated_by').nullable()\n })\n logger.info(`Added audit fields to: ${tableName}`)\n }\n\n async addColumnIfMissing(tableName: string, columnName: string, type: string): Promise<boolean> {\n const exists = await this.hasColumn(tableName, columnName)\n if (exists) return false\n\n await this.knex.schema.alterTable(tableName, (table) => {\n switch (type) {\n case 'string':\n table.string(columnName).nullable()\n break\n case 'text':\n table.text(columnName).nullable()\n break\n case 'integer':\n table.integer(columnName).nullable()\n break\n case 'boolean':\n table.boolean(columnName).nullable()\n break\n case 'date':\n table.date(columnName).nullable()\n break\n case 'datetime':\n table.datetime(columnName).nullable()\n break\n case 'json':\n table.json(columnName).nullable()\n break\n case 'uuid':\n table.uuid(columnName).nullable()\n break\n default:\n table.string(columnName).nullable()\n }\n })\n logger.info(`Added column: ${tableName}.${columnName}`)\n return true\n }\n}\n\n/**\n * Create a KnexSchemaAdapter instance.\n *\n * @param knex - Knex instance\n * @returns SchemaAdapter implementation\n */\nexport function createKnexSchemaAdapter(knex: Knex): SchemaAdapter {\n return new KnexSchemaAdapter(knex)\n}\n","/**\n * In-Memory implementation of SchemaAdapter.\n *\n * Stores schema metadata in memory. Useful for:\n * - Unit tests without database\n * - Mocking schema operations\n * - Testing migration logic\n */\n\nimport type {\n SchemaAdapter,\n SchemaTableBuilder,\n SchemaAlterTableBuilder,\n SchemaColumnBuilder,\n SchemaColumnInfo,\n SchemaForeignKeyBuilder\n} from '@gzl10/nexus-sdk'\n\n/**\n * In-memory column metadata\n */\ninterface MemoryColumnMeta {\n name: string\n type: string\n nullable: boolean\n defaultValue: unknown\n primary: boolean\n unique: boolean\n index: boolean\n references?: { column: string; table: string; onDelete?: string }\n}\n\n/**\n * In-memory table metadata\n */\ninterface MemoryTableMeta {\n name: string\n columns: Map<string, MemoryColumnMeta>\n}\n\n/**\n * In-memory column builder\n */\nclass MemoryColumnBuilder implements SchemaColumnBuilder {\n constructor(private meta: MemoryColumnMeta) {}\n\n primary(): SchemaColumnBuilder {\n this.meta.primary = true\n return this\n }\n\n unique(): SchemaColumnBuilder {\n this.meta.unique = true\n return this\n }\n\n index(): SchemaColumnBuilder {\n this.meta.index = true\n return this\n }\n\n nullable(): SchemaColumnBuilder {\n this.meta.nullable = true\n return this\n }\n\n notNullable(): SchemaColumnBuilder {\n this.meta.nullable = false\n return this\n }\n\n defaultTo(value: unknown): SchemaColumnBuilder {\n this.meta.defaultValue = value\n return this\n }\n\n references(column: string): SchemaColumnBuilder {\n this.meta.references = { column, table: '' }\n return this\n }\n\n inTable(table: string): SchemaColumnBuilder {\n if (this.meta.references) {\n this.meta.references.table = table\n }\n return this\n }\n\n onDelete(action: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION'): SchemaColumnBuilder {\n if (this.meta.references) {\n this.meta.references.onDelete = action\n }\n return this\n }\n}\n\n/**\n * In-memory foreign key builder (no-op for testing)\n */\nclass MemoryForeignKeyBuilder implements SchemaForeignKeyBuilder {\n references(_column: string): SchemaForeignKeyBuilder {\n return this\n }\n\n inTable(_table: string): SchemaForeignKeyBuilder {\n return this\n }\n\n onDelete(_action: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION'): SchemaForeignKeyBuilder {\n return this\n }\n\n onUpdate(_action: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION'): SchemaForeignKeyBuilder {\n return this\n }\n}\n\n/**\n * In-memory table builder\n */\nclass MemoryTableBuilder implements SchemaTableBuilder {\n constructor(protected tableMeta: MemoryTableMeta) {}\n\n private createColumnMeta(name: string, type: string): MemoryColumnMeta {\n const meta: MemoryColumnMeta = {\n name,\n type,\n nullable: true,\n defaultValue: undefined,\n primary: false,\n unique: false,\n index: false\n }\n this.tableMeta.columns.set(name, meta)\n return meta\n }\n\n string(name: string, _size?: number): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name, 'string'))\n }\n\n text(name: string): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name, 'text'))\n }\n\n integer(name: string): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name, 'integer'))\n }\n\n decimal(name: string, _precision?: number, _scale?: number): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name, 'decimal'))\n }\n\n boolean(name: string): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name, 'boolean'))\n }\n\n date(name: string): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name, 'date'))\n }\n\n datetime(name: string): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name, 'datetime'))\n }\n\n json(name: string): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name, 'json'))\n }\n\n uuid(name: string): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name, 'uuid'))\n }\n\n bigInteger(name: string): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name, 'bigInteger'))\n }\n\n timestamp(name: string): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name, 'timestamp'))\n }\n\n jsonb(name: string): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name, 'jsonb'))\n }\n\n enum(name: string, _values: readonly string[]): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name, 'enum'))\n }\n\n binary(name: string, _length?: number): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name, 'binary'))\n }\n\n float(name: string, _precision?: number, _scale?: number): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name, 'float'))\n }\n\n specificType(name: string, type: string): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name, type))\n }\n\n index(_columns: string | string[], _indexName?: string): void {\n // No-op for in-memory adapter\n }\n\n unique(_columns: string | string[], _constraintName?: string): void {\n // No-op for in-memory adapter\n }\n\n primary(_columns: string | string[], _constraintName?: string): void {\n // No-op for in-memory adapter\n }\n\n foreign(_column: string): SchemaForeignKeyBuilder {\n return new MemoryForeignKeyBuilder()\n }\n\n increments(name?: string): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name ?? 'id', 'increments'))\n }\n\n bigIncrements(name?: string): SchemaColumnBuilder {\n return new MemoryColumnBuilder(this.createColumnMeta(name ?? 'id', 'bigIncrements'))\n }\n\n timestamps(): void {\n const now = new Date().toISOString()\n const createdAt = this.createColumnMeta('created_at', 'datetime')\n createdAt.defaultValue = now\n const updatedAt = this.createColumnMeta('updated_at', 'datetime')\n updatedAt.defaultValue = now\n }\n}\n\n/**\n * In-memory alter table builder\n */\nclass MemoryAlterTableBuilder extends MemoryTableBuilder implements SchemaAlterTableBuilder {\n dropColumn(name: string): void {\n this.tableMeta.columns.delete(name)\n }\n\n renameColumn(from: string, to: string): void {\n const column = this.tableMeta.columns.get(from)\n if (column) {\n column.name = to\n this.tableMeta.columns.delete(from)\n this.tableMeta.columns.set(to, column)\n }\n }\n}\n\n/**\n * In-Memory schema adapter.\n *\n * @example\n * const schema = createInMemorySchemaAdapter()\n *\n * await schema.createTable('users', (t) => {\n * t.string('id').primary()\n * t.string('email').unique().notNullable()\n * t.timestamps()\n * })\n *\n * // Check state\n * console.log(await schema.hasTable('users')) // true\n * console.log(await schema.getColumns('users')) // [...]\n *\n * // Clear for test isolation\n * schema.clear()\n */\nexport class InMemorySchemaAdapter implements SchemaAdapter {\n private tables: Map<string, MemoryTableMeta> = new Map()\n\n /**\n * Raw access - returns the internal Map (for debugging/testing)\n */\n get raw(): unknown {\n return this.tables\n }\n\n /**\n * Clear all schema metadata (useful between tests)\n */\n clear(): void {\n this.tables.clear()\n }\n\n /**\n * Get table metadata (for testing assertions)\n */\n getTableMeta(tableName: string): MemoryTableMeta | undefined {\n return this.tables.get(tableName)\n }\n\n async hasTable(tableName: string): Promise<boolean> {\n return this.tables.has(tableName)\n }\n\n async hasColumn(tableName: string, columnName: string): Promise<boolean> {\n const table = this.tables.get(tableName)\n if (!table) return false\n return table.columns.has(columnName)\n }\n\n async getColumns(tableName: string): Promise<SchemaColumnInfo[]> {\n const table = this.tables.get(tableName)\n if (!table) return []\n\n return Array.from(table.columns.values()).map(col => ({\n name: col.name,\n type: col.type,\n nullable: col.nullable,\n defaultValue: col.defaultValue\n }))\n }\n\n async createTable(tableName: string, builder: (t: SchemaTableBuilder) => void): Promise<void> {\n if (this.tables.has(tableName)) {\n throw new Error(`Table \"${tableName}\" already exists`)\n }\n\n const tableMeta: MemoryTableMeta = {\n name: tableName,\n columns: new Map()\n }\n\n builder(new MemoryTableBuilder(tableMeta))\n this.tables.set(tableName, tableMeta)\n }\n\n async alterTable(tableName: string, builder: (t: SchemaAlterTableBuilder) => void): Promise<void> {\n const tableMeta = this.tables.get(tableName)\n if (!tableMeta) {\n throw new Error(`Table \"${tableName}\" not found`)\n }\n\n builder(new MemoryAlterTableBuilder(tableMeta))\n }\n\n async dropTable(tableName: string): Promise<void> {\n this.tables.delete(tableName)\n }\n\n async addTimestamps(tableName: string): Promise<void> {\n const hasCreatedAt = await this.hasColumn(tableName, 'created_at')\n if (hasCreatedAt) return\n\n await this.alterTable(tableName, (t) => {\n t.datetime('created_at').defaultTo(new Date().toISOString())\n t.datetime('updated_at').defaultTo(new Date().toISOString())\n })\n }\n\n async addAuditFields(tableName: string): Promise<void> {\n const hasCreatedBy = await this.hasColumn(tableName, 'created_by')\n if (hasCreatedBy) return\n\n await this.alterTable(tableName, (t) => {\n t.string('created_by').nullable()\n t.string('updated_by').nullable()\n })\n }\n\n async addColumnIfMissing(tableName: string, columnName: string, type: string): Promise<boolean> {\n const exists = await this.hasColumn(tableName, columnName)\n if (exists) return false\n\n await this.alterTable(tableName, (t) => {\n switch (type) {\n case 'string':\n t.string(columnName).nullable()\n break\n case 'text':\n t.text(columnName).nullable()\n break\n case 'integer':\n t.integer(columnName).nullable()\n break\n case 'boolean':\n t.boolean(columnName).nullable()\n break\n case 'date':\n t.date(columnName).nullable()\n break\n case 'datetime':\n t.datetime(columnName).nullable()\n break\n case 'json':\n t.json(columnName).nullable()\n break\n case 'uuid':\n t.uuid(columnName).nullable()\n break\n default:\n t.string(columnName).nullable()\n }\n })\n return true\n }\n}\n\n/**\n * Create an InMemorySchemaAdapter instance.\n *\n * @returns SchemaAdapter implementation backed by memory\n */\nexport function createInMemorySchemaAdapter(): InMemorySchemaAdapter {\n return new InMemorySchemaAdapter()\n}\n","/**\n * In-memory Knex connection using SQLite :memory:.\n * Replaces InMemoryAdapter for persistent entities with adapter: 'memory'.\n * Tables are ephemeral — recreated each boot.\n *\n * Note: serializeForDb in base.service.ts detects 'better-sqlite3' client\n * to decide JSON stringification. This client name is relied upon.\n */\nimport knex, { type Knex } from 'knex'\n\nlet memoryKnex: Knex | null = null\n\n/**\n * Get or create the shared SQLite :memory: Knex instance.\n * Singleton — persists for the process lifetime.\n */\nexport function getMemoryKnex(): Knex {\n if (!memoryKnex) {\n memoryKnex = knex({\n client: 'better-sqlite3',\n connection: { filename: ':memory:' },\n useNullAsDefault: true,\n pool: {\n afterCreate: (conn: any, cb: () => void) => {\n conn.pragma('foreign_keys = ON')\n cb()\n }\n }\n })\n }\n return memoryKnex\n}\n\n/**\n * Destroy the memory Knex connection and reset singleton.\n * Call from resetSharedAdapters() and test teardown.\n */\nexport async function destroyMemoryKnex(): Promise<void> {\n if (memoryKnex) {\n await memoryKnex.destroy()\n memoryKnex = null\n }\n}\n","/**\n * Query builder helpers for common operations\n */\nimport type { Knex } from 'knex'\nimport type { EntityDefinition, EntityQuery, PaginatedResult } from '@gzl10/nexus-sdk'\n\nconst DEFAULT_PAGE = 1\nconst DEFAULT_LIMIT = 20\nexport const DEFAULT_MAX_LIMIT = 100\n\nexport interface PaginationParams {\n page: number\n limit: number\n offset: number\n}\n\n/**\n * Extract pagination parameters from query.\n * Validates and normalizes page/limit values.\n */\nexport function getPagination(query?: EntityQuery): PaginationParams {\n const maxLimit = query?.maxLimit ?? DEFAULT_MAX_LIMIT\n const page = Math.max(1, query?.page ?? DEFAULT_PAGE)\n const limit = Math.min(maxLimit, Math.max(1, query?.limit ?? DEFAULT_LIMIT))\n const offset = (page - 1) * limit\n return { page, limit, offset }\n}\n\n/**\n * Build a PaginatedResult from items and total count.\n */\nexport function buildPaginatedResult<T>(\n items: T[],\n total: number,\n pagination: PaginationParams\n): PaginatedResult<T> {\n const { page, limit } = pagination\n const totalPages = Math.ceil(total / limit)\n return {\n items,\n total,\n page,\n limit,\n totalPages,\n hasNext: page < totalPages\n }\n}\n\n/**\n * Get searchable field names from entity definition\n */\nexport function getSearchableFields(definition: EntityDefinition): string[] {\n return Object.entries(definition.fields ?? {})\n .filter(([_, f]) => f.meta?.searchable === true)\n .map(([name]) => name)\n}\n\n/**\n * Apply search filter to query builder based on entity definition.\n * Searches in fields with meta.searchable: true, or falls back to labelField.\n */\nexport function applySearchFilter(\n qb: Knex.QueryBuilder,\n definition: EntityDefinition,\n search: string\n): Knex.QueryBuilder {\n const searchableFields = getSearchableFields(definition)\n\n // Fallback to labelField if no searchable fields\n if (searchableFields.length === 0 && 'labelField' in definition && definition.labelField) {\n searchableFields.push(definition.labelField as string)\n }\n\n // Apply OR search across all searchable fields\n if (searchableFields.length > 0) {\n const searchPattern = `%${search}%`\n qb.where(function () {\n for (const field of searchableFields) {\n this.orWhere(field, 'like', searchPattern)\n }\n })\n }\n\n return qb\n}\n","/**\n * Creates tables in SQLite :memory: for entities with adapter: 'memory'.\n * Called at boot time, before seeds.\n */\nimport type { Knex } from 'knex'\nimport type { EntityDefinition, FieldDefinition } from '@gzl10/nexus-sdk'\nimport { getMemoryKnex } from './memory-knex.js'\n\n/**\n * Create tables in the :memory: Knex for all entities with adapter: 'memory'.\n */\nexport async function createMemoryTables(definitions: EntityDefinition[]): Promise<void> {\n const memKnex = getMemoryKnex()\n\n for (const def of definitions) {\n const defAny = def as unknown as Record<string, unknown>\n const adapter = defAny['adapter'] as string | undefined\n if (adapter !== 'memory') continue\n const table = defAny['table'] as string | undefined\n if (!table) continue\n\n if (!await memKnex.schema.hasTable(table)) {\n await createTableFromDefinition(memKnex, def)\n }\n }\n}\n\n/**\n * Create a single table from an EntityDefinition using Knex schema builder.\n */\nasync function createTableFromDefinition(db: Knex, def: EntityDefinition): Promise<void> {\n const defAny = def as unknown as Record<string, unknown>\n const tableName = (defAny['table'] as string)\n\n await db.schema.createTable(tableName, (table) => {\n // ID column\n const idType = (defAny['idType'] as string) ?? 'ulid'\n if (idType === 'auto') {\n table.increments('id').primary()\n } else {\n table.string('id', 36).primary()\n }\n\n // Entity fields (skip 'id' — already created as PK above)\n for (const [name, field] of Object.entries(def.fields ?? {})) {\n if (name === 'id') continue\n buildColumn(table, db, name, field)\n }\n\n // Auto-fields: timestamps\n if (defAny['timestamps']) {\n table.timestamp('created_at').defaultTo(db.fn.now())\n table.timestamp('updated_at').defaultTo(db.fn.now())\n }\n\n // Auto-fields: soft delete\n if (defAny['softDelete']) {\n table.timestamp('deleted_at').nullable()\n }\n\n // Auto-fields: audit\n if (defAny['audit']) {\n table.string('created_by').nullable()\n table.string('updated_by').nullable()\n }\n\n // Tree: parent_id with FK\n if (def.type === 'tree') {\n table.string('parent_id', 36).nullable().references('id').inTable(tableName)\n }\n })\n\n // DAG: create parents junction table\n if (def.type === 'dag') {\n const parentsTable = (defAny['parentsTable'] as string) ?? `${tableName}_parents`\n if (!await db.schema.hasTable(parentsTable)) {\n await db.schema.createTable(parentsTable, (table) => {\n table.string('child_id', 36).notNullable().references('id').inTable(tableName).onDelete('CASCADE')\n table.string('parent_id', 36).notNullable().references('id').inTable(tableName).onDelete('CASCADE')\n table.primary(['child_id', 'parent_id'])\n })\n }\n }\n}\n\n/**\n * Build a single column from a FieldDefinition using Knex schema builder.\n */\nfunction buildColumn(table: Knex.CreateTableBuilder, db: Knex, name: string, field: FieldDefinition): void {\n const dbConfig = field.db\n if (!dbConfig) return\n if (dbConfig.virtual) return\n\n let col: Knex.ColumnBuilder\n\n switch (dbConfig.type) {\n case 'string':\n col = dbConfig.size ? table.string(name, dbConfig.size) : table.string(name)\n break\n case 'text':\n col = table.text(name)\n break\n case 'integer':\n col = table.integer(name)\n break\n case 'decimal':\n col = dbConfig.precision\n ? table.decimal(name, dbConfig.precision[0], dbConfig.precision[1])\n : table.decimal(name)\n break\n case 'boolean':\n col = table.boolean(name)\n break\n case 'datetime':\n col = table.timestamp(name)\n break\n case 'date':\n col = table.date(name)\n break\n case 'json':\n case 'array':\n col = table.json(name)\n break\n case 'uuid':\n col = table.uuid(name)\n break\n default:\n col = table.specificType(name, String(dbConfig.type))\n }\n\n // Nullable\n if (dbConfig.nullable === false) {\n col.notNullable()\n } else {\n col.nullable()\n }\n\n // Defaults\n if (dbConfig.defaultFn === 'now') {\n col.defaultTo(db.fn.now())\n } else if (dbConfig.default !== undefined) {\n col.defaultTo(dbConfig.default as any)\n }\n\n // Unique\n if (dbConfig.unique) {\n col.unique()\n }\n}\n","/**\n * @module db\n * @description Database utilities: adapters, schema helpers, migrations, and query interceptors\n *\n * @dependencies\n * - core/ (logger, events/emitter, utils/paths)\n * - engine/ (loader, context) - only in CLI scripts (migrate.ts)\n * - runtime/ (entity-factory) - only in seed-runner.ts\n *\n * @note This module should ideally only depend on core/ utilities.\n * Dependencies on engine/ and runtime/ are for CLI tooling only.\n */\n\n// Schema helpers (timestamps, audit fields, column utilities)\nexport {\n getDbClient,\n formatTimestamp,\n nowTimestamp,\n addTimestamps,\n addAuditFieldsIfMissing,\n addSoftDeleteFieldIfMissing,\n addConfigDefaultField,\n addColumnIfMissing\n} from './schema-helpers.js'\n\n// SQL parsing utilities\nexport {\n extractTableFromSql,\n extractTableFromSelect,\n extractTableFromInsert,\n extractTableFromUpdate,\n extractTableFromDelete\n} from './sql-utils.js'\n\n// SQLite compatibility (boolean 0/1 → true/false conversion)\nexport {\n createSqliteBooleanProcessor,\n registerBooleanColumn,\n convertBooleans\n} from './sqlite-compat.js'\nexport type { SqliteBooleanProcessor } from './sqlite-compat.js'\n\n// Module execution utilities\nexport { runModuleSeed } from './seed-runner.js'\n\n// System tables\nexport { ensureSystemTables } from './ensure-system-tables.js'\n\n// Migration sources (multi-source: core → plugins → project)\nexport {\n buildMigrationSources,\n type MigrationSourceId,\n type MigrationSourceDescriptor\n} from './migration-sources.js'\n\n// Migration system\nexport {\n runMigrations,\n loadAllMigrationFiles,\n rollbackLastBatch,\n rollbackByName,\n showMigrationStatus,\n resetDatabase,\n type MigrationFile,\n type MigrationStatus\n} from './migration-runner.js'\n\n// Migration generator\nexport { generateMigrationForSource, type MigrationScope } from './migration-generator.js'\n\n// Migration engine (direct table creation from entity definitions — for tests)\nexport { runGeneratedMigration, runAllGeneratedMigrations } from './migration-engine.js'\n\n// Query interceptor\nexport { setupQueryInterceptor, type DbEventPayload } from './query-interceptor.js'\n\n// Database adapters\nexport { KnexAdapter, createKnexAdapter } from './knex-adapter.js'\nexport { InMemoryAdapter, createInMemoryAdapter } from './memory-adapter.js'\nexport { RedisAdapter, createRedisAdapter, type RedisAdapterOptions } from './redis-adapter.js'\n\n// Schema adapters\nexport { KnexSchemaAdapter, createKnexSchemaAdapter } from './schema-adapter.js'\nexport { InMemorySchemaAdapter, createInMemorySchemaAdapter } from './memory-schema-adapter.js'\n\n// Filter helpers\nexport {\n applyFilters,\n applyFilterOperators,\n applyInMemoryFilters,\n matchFilterOperators\n} from './filter-helpers.js'\n\n// Memory Knex (SQLite :memory: for adapter: 'memory' entities)\nexport { getMemoryKnex, destroyMemoryKnex } from './memory-knex.js'\n\n// Connection (Knex instance)\nexport { db, getDb, initDb, destroyDb } from './connection.js'\n\n// Query helpers (pagination, search)\nexport {\n getPagination,\n buildPaginatedResult,\n getSearchableFields,\n applySearchFilter,\n type PaginationParams\n} from './query-helpers.js'\n\n// SQLite post-process (additional)\nexport { sqlitePostProcess } from './sqlite-compat.js'\n\n// Memory table builder (creates tables in :memory: at boot)\nexport { createMemoryTables } from './memory-table-builder.js'\n","/**\n * @file Plugin + module auto-discovery\n *\n * @description\n * Auto-discovers plugins from package.json dependencies and\n * modules from conventional `src/modules/` directory.\n *\n * **Plugin discovery:** scans dependencies/devDependencies for packages\n * matching `nexus-plugin-*`, imports them, and extracts the PluginManifest\n * via default export or named export (duck-typing).\n *\n * **Module discovery:** scans `src/modules/` (or `MODULES_DIR` env var)\n * for subdirectories with `index.{js,ts}`, imports them, and extracts\n * ModuleManifest exports via duck-typing.\n *\n * Factory plugins (e.g. prismaPlugin(opts)) are not auto-discovered\n * and must be passed to `start()` programmatically.\n *\n * @example\n * ```typescript\n * // No config file needed!\n * // Plugins: auto-discovered from package.json\n * // Modules: auto-discovered from src/modules/\n * // Factory plugins: passed to start()\n * import { start } from '@gzl10/nexus-backend'\n * import { prismaPlugin } from '@gzl10/nexus-plugin-prisma'\n *\n * await start({ plugins: [prismaPlugin({ client: new PrismaClient() })] })\n * ```\n */\nimport { join } from 'path'\nimport { existsSync, readFileSync, readdirSync, statSync } from 'fs'\nimport { pathToFileURL } from 'url'\nimport { getProjectPath } from './paths.js'\nimport { logger } from '../core/logger/index.js'\nimport type { PluginManifest, ModuleManifest } from '@gzl10/nexus-sdk'\nimport type { NexusConfig } from './types.js'\n\n/** Pattern to match plugin package names */\nconst PLUGIN_PATTERN = /nexus-plugin-/\n\nlet _cache: NexusConfig | null = null\n\n// ─── Plugin Discovery ────────────────────────────────────────────────\n\n/**\n * Checks if a value structurally matches PluginManifest (duck-typing).\n * Rejects functions (factory plugins), null, and primitives.\n */\nexport function isPluginManifest(value: unknown): value is PluginManifest {\n if (!value || typeof value !== 'object' || typeof value === 'function') return false\n const v = value as Record<string, unknown>\n return (\n typeof v['name'] === 'string' &&\n Array.isArray(v['modules']) &&\n typeof v['code'] === 'string' &&\n typeof v['version'] === 'string'\n )\n}\n\n/**\n * Extract PluginManifest from a dynamically imported module.\n * Checks default export first, then named exports.\n */\nexport function extractPluginManifest(mod: Record<string, unknown>): PluginManifest | null {\n if (isPluginManifest(mod['default'])) return mod['default']\n for (const [key, value] of Object.entries(mod)) {\n if (key === 'default') continue\n if (isPluginManifest(value)) return value\n }\n return null\n}\n\n/**\n * Resolve the entry point of a plugin package from node_modules.\n * Handles ESM-only packages (exports with only \"import\" condition)\n * which createRequire().resolve() cannot handle.\n */\nfunction resolvePluginEntry(projectPath: string, pkgName: string): string | null {\n const pkgDir = join(projectPath, 'node_modules', pkgName)\n const pkgJsonPath = join(pkgDir, 'package.json')\n\n if (!existsSync(pkgJsonPath)) return null\n\n try {\n const pluginPkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'))\n const exports = pluginPkg.exports\n\n // Resolve entry: exports.'.'.import > main > module > index.js\n const importEntry = exports?.['.']?.import\n const entry =\n (typeof importEntry === 'string' ? importEntry : importEntry?.default) ??\n (typeof exports === 'string' ? exports : null) ??\n pluginPkg.main ??\n pluginPkg.module ??\n 'index.js'\n\n const entryPath = join(pkgDir, entry)\n return existsSync(entryPath) ? entryPath : null\n } catch {\n return null\n }\n}\n\n/**\n * Discover plugins from package.json dependencies.\n * Scans dependencies and devDependencies for packages matching `nexus-plugin-*`.\n * Dynamically imports each and extracts the PluginManifest.\n *\n * Errors are logged as warnings and skipped (non-fatal).\n */\nexport async function discoverPlugins(projectPath: string): Promise<PluginManifest[]> {\n const pkgPath = join(projectPath, 'package.json')\n\n let pkg: Record<string, Record<string, string> | undefined>\n try {\n pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))\n } catch {\n return []\n }\n\n const allDeps = {\n ...pkg['dependencies'],\n ...pkg['devDependencies'],\n }\n\n const pluginNames = Object.keys(allDeps).filter((name) => PLUGIN_PATTERN.test(name))\n if (pluginNames.length === 0) return []\n\n const discovered: PluginManifest[] = []\n\n for (const pkgName of pluginNames) {\n try {\n const resolved = resolvePluginEntry(projectPath, pkgName)\n if (!resolved) {\n logger.debug({ plugin: pkgName }, 'Plugin discovery: not found in node_modules, skipping')\n continue\n }\n const mod = await import(pathToFileURL(resolved).href)\n const manifest = extractPluginManifest(mod)\n\n if (manifest) {\n // Default migrationsDir: <plugin>/migrations/ if it exists\n if (!manifest.migrationsDir) {\n const defaultMigrationsDir = join(projectPath, 'node_modules', pkgName, 'migrations')\n if (existsSync(defaultMigrationsDir)) {\n manifest.migrationsDir = defaultMigrationsDir\n }\n }\n // Auto-detect image: <plugin>/image.png if it exists\n if (!manifest.image) {\n const imagePath = join(projectPath, 'node_modules', pkgName, 'image.png')\n if (existsSync(imagePath)) {\n manifest.image = imagePath\n }\n }\n // Auto-detect llms.txt: <plugin>/llms.txt if it exists\n if (!manifest.llms) {\n const llmsPath = join(projectPath, 'node_modules', pkgName, 'llms.txt')\n if (existsSync(llmsPath)) {\n manifest.llms = readFileSync(llmsPath, 'utf-8')\n }\n }\n discovered.push(manifest)\n } else {\n logger.warn({ plugin: pkgName }, 'Plugin discovery: no PluginManifest export, skipping')\n }\n } catch (err) {\n const errorMsg = (err as Error).message\n logger.warn({ plugin: pkgName, err: errorMsg }, 'Plugin discovery: failed to import')\n console.error(` ⚠ Plugin '${pkgName}' failed to load: ${errorMsg}`)\n }\n }\n\n return discovered\n}\n\n// ─── Module Discovery ────────────────────────────────────────────────\n\n/**\n * Checks if a value structurally matches ModuleManifest (duck-typing).\n * Required fields: name (string) and category (string).\n * Excludes PluginManifest (which has code + modules[]).\n */\nexport function isModuleManifest(value: unknown): value is ModuleManifest {\n if (!value || typeof value !== 'object' || typeof value === 'function') return false\n const v = value as Record<string, unknown>\n return (\n typeof v['name'] === 'string' &&\n typeof v['category'] === 'string' &&\n !(typeof v['code'] === 'string' && Array.isArray(v['modules']))\n )\n}\n\n/**\n * Extract all ModuleManifest exports from a dynamically imported module.\n * A single index.ts can export multiple manifests (e.g. postsModule + commentsModule).\n */\nexport function extractModuleManifests(mod: Record<string, unknown>): ModuleManifest[] {\n const manifests: ModuleManifest[] = []\n\n // Check default export first\n if (isModuleManifest(mod['default'])) {\n manifests.push(mod['default'])\n }\n\n // Check all named exports\n for (const [key, value] of Object.entries(mod)) {\n if (key === 'default') continue\n if (isModuleManifest(value)) {\n // Avoid duplicates (same object exported as default + named)\n if (!manifests.some((m) => m.name === (value as ModuleManifest).name)) {\n manifests.push(value)\n }\n }\n }\n\n return manifests\n}\n\n/**\n * Discover modules from conventional directory structure.\n * Scans `src/modules/` (or `MODULES_DIR` env var) for subdirectories\n * containing `index.js` or `index.ts` that export ModuleManifest.\n *\n * Errors are logged as warnings and skipped (non-fatal).\n */\nexport async function discoverModules(projectPath: string): Promise<ModuleManifest[]> {\n const modulesDir = process.env['MODULES_DIR'] || join(projectPath, 'src', 'modules')\n\n if (!existsSync(modulesDir)) return []\n\n let entries: string[]\n try {\n entries = readdirSync(modulesDir)\n } catch {\n return []\n }\n\n const discovered: ModuleManifest[] = []\n\n for (const entry of entries) {\n const entryPath = join(modulesDir, entry)\n\n // Only process directories\n try {\n if (!statSync(entryPath).isDirectory()) continue\n } catch {\n continue\n }\n\n // Look for index.js or index.ts\n let indexPath: string | null = null\n for (const ext of ['index.js', 'index.ts']) {\n const candidate = join(entryPath, ext)\n if (existsSync(candidate)) {\n indexPath = candidate\n break\n }\n }\n\n if (!indexPath) continue\n\n try {\n let mod: Record<string, unknown>\n\n if (indexPath.endsWith('.ts')) {\n // Use tsx to import TypeScript files when running from compiled CLI\n try {\n const { tsImport } = await import('tsx/esm/api')\n mod = await tsImport(pathToFileURL(indexPath).href, import.meta.url) as Record<string, unknown>\n } catch {\n // tsx not available — fall back to native import (works when running via tsx itself)\n mod = await import(pathToFileURL(indexPath).href)\n }\n } else {\n mod = await import(pathToFileURL(indexPath).href)\n }\n\n const manifests = extractModuleManifests(mod)\n\n if (manifests.length > 0) {\n discovered.push(...manifests)\n }\n } catch (err) {\n const errorMsg = (err as Error).message\n logger.warn({ module: entry, err: errorMsg }, 'Module discovery: failed to import')\n // Also log to stderr so CLI users see the error (pino is silenced in CLI mode)\n console.error(` ⚠ Module '${entry}' failed to load: ${errorMsg}`)\n }\n }\n\n return discovered\n}\n\n// ─── Main API ────────────────────────────────────────────────────────\n\n/**\n * Discover plugins and modules via conventions.\n *\n * Flow:\n * 1. Auto-discover plugins from package.json dependencies\n * 2. Auto-discover modules from src/modules/ directory\n *\n * Results are cached for the lifetime of the process.\n */\nexport async function loadNexusConfig(): Promise<NexusConfig> {\n if (_cache) return _cache\n\n const projectPath = getProjectPath()\n const config: NexusConfig = {}\n\n // 1. Discover plugins from package.json\n const plugins = await discoverPlugins(projectPath)\n if (plugins.length > 0) {\n config.plugins = plugins\n }\n\n // 2. Discover modules from src/modules/\n const modules = await discoverModules(projectPath)\n if (modules.length > 0) {\n config.modules = modules\n }\n\n _cache = config\n return _cache\n}\n\n/**\n * Reset config cache. Used in tests and server stop() for idempotent restarts.\n */\nexport function resetConfigCache(): void {\n _cache = null\n}\n","import type { ContextEvents } from '@gzl10/nexus-sdk'\nimport type { TypedEventEmitter } from '../core/events-hub/emitter.js'\n\n/**\n * Creates the semantic event API (ctx.events) wrapping the raw EventEmitter2.\n *\n * Three patterns:\n * - notify: fire-and-forget (emit). Errors in listeners are caught and logged.\n * - query: request data from 0..N listeners (emitAsync). Returns flat array.\n * - command: request action from 0..1 handler (emitAsync). Returns first result.\n */\nexport function createEventsApi(\n emitter: TypedEventEmitter,\n logger: { warn: (obj: object, msg: string) => void }\n): ContextEvents {\n return {\n notify(event, payload) {\n try {\n if (payload !== undefined) {\n emitter.emit(event, payload)\n } else {\n emitter.emit(event)\n }\n } catch (err) {\n // LIMITATION: fire-and-forget contract — listener errors must not affect the emitter\n logger.warn({ event, err }, 'Event listener error (notify)')\n }\n },\n\n async query<T>(event: string, payload?: unknown): Promise<T[]> {\n const args = payload !== undefined ? [payload] : []\n const responses = await emitter.emitAsync(event, ...args)\n if (!responses?.length) return []\n // Flatten 1 level: if a listener returns an array, its elements are spread into the result\n return responses.flatMap(r =>\n r !== undefined ? (Array.isArray(r) ? r : [r]) : []\n ) as T[]\n },\n\n async command<T>(event: string, payload?: unknown): Promise<T | undefined> {\n const args = payload !== undefined ? [payload] : []\n const responses = await emitter.emitAsync(event, ...args)\n if (!responses?.length) return undefined\n const defined = responses.filter(r => r !== undefined)\n if (defined.length > 1) {\n logger.warn(\n { event, handlerCount: defined.length },\n 'Multiple handlers responded to command event — using first'\n )\n }\n return defined[0] as T | undefined\n },\n\n on(event, listener) { emitter.on(event, listener as (...args: unknown[]) => void) },\n off(event, listener) { emitter.off(event, listener as (...args: unknown[]) => void) },\n once(event, listener) { emitter.once(event, listener as (...args: unknown[]) => void) },\n onAny(listener) { emitter.onAny(listener as (event: string | string[], ...args: unknown[]) => void) },\n }\n}\n","/**\n * LRU Cache - Least Recently Used cache with TTL support\n *\n * Features:\n * - LRU eviction when max entries reached\n * - TTL expiration per entry\n * - Hit/miss statistics\n * - Prefix-based deletion for invalidation\n */\n\nimport type { CacheStats } from '@gzl10/nexus-sdk'\nexport type { CacheStats }\n\ninterface CacheEntry<T> {\n data: T\n expires: number\n}\n\nexport interface LRUCacheOptions {\n /** Maximum number of entries (default: 100) */\n maxEntries?: number\n /** Default TTL in seconds (default: 60) */\n defaultTTL?: number\n}\n\nexport class LRUCache<T = unknown> {\n private cache: Map<string, CacheEntry<T>>\n private readonly maxEntries: number\n private readonly defaultTTL: number\n\n private hits = 0\n private misses = 0\n\n constructor(options?: LRUCacheOptions) {\n this.cache = new Map()\n this.maxEntries = options?.maxEntries ?? 100\n this.defaultTTL = options?.defaultTTL ?? 60\n }\n\n /**\n * Get value from cache\n * Returns null if not found or expired\n * Moves entry to end (most recently used)\n */\n get(key: string): T | null {\n const entry = this.cache.get(key)\n\n if (!entry) {\n this.misses++\n return null\n }\n\n // Check expiration\n if (entry.expires > 0 && entry.expires < Date.now()) {\n this.cache.delete(key)\n this.misses++\n return null\n }\n\n // Move to end (most recently used)\n this.cache.delete(key)\n this.cache.set(key, entry)\n\n this.hits++\n return entry.data\n }\n\n /**\n * Set value in cache\n * Evicts oldest entry if max entries reached\n * @param ttl TTL in seconds (0 = no expiration, undefined = use default)\n */\n set(key: string, data: T, ttl?: number): void {\n // Delete existing to update position\n this.cache.delete(key)\n\n // Evict oldest if at capacity\n if (this.cache.size >= this.maxEntries) {\n const oldestKey = this.cache.keys().next().value\n if (oldestKey) {\n this.cache.delete(oldestKey)\n }\n }\n\n const actualTTL = ttl ?? this.defaultTTL\n const expires = actualTTL > 0 ? Date.now() + actualTTL * 1000 : 0\n\n this.cache.set(key, { data, expires })\n }\n\n /**\n * Delete specific key\n */\n delete(key: string): boolean {\n return this.cache.delete(key)\n }\n\n /**\n * Delete all keys matching prefix\n * @returns Number of deleted entries\n */\n deleteByPrefix(prefix: string): number {\n let deleted = 0\n for (const key of this.cache.keys()) {\n if (key.startsWith(prefix)) {\n this.cache.delete(key)\n deleted++\n }\n }\n return deleted\n }\n\n /**\n * Clear all entries\n */\n clear(): void {\n this.cache.clear()\n }\n\n /**\n * Check if key exists and is not expired\n */\n has(key: string): boolean {\n const entry = this.cache.get(key)\n if (!entry) return false\n if (entry.expires > 0 && entry.expires < Date.now()) {\n this.cache.delete(key)\n return false\n }\n return true\n }\n\n /**\n * Get current size\n */\n get size(): number {\n return this.cache.size\n }\n\n /**\n * Get cache statistics\n */\n getStats(): CacheStats {\n const total = this.hits + this.misses\n return {\n hits: this.hits,\n misses: this.misses,\n hitRate: total > 0 ? this.hits / total : 0,\n size: this.cache.size,\n maxEntries: this.maxEntries\n }\n }\n\n /**\n * Reset statistics\n */\n resetStats(): void {\n this.hits = 0\n this.misses = 0\n }\n\n /**\n * Prune expired entries\n * Call periodically to clean up memory\n */\n prune(): number {\n const now = Date.now()\n let pruned = 0\n\n for (const [key, entry] of this.cache.entries()) {\n if (entry.expires > 0 && entry.expires < now) {\n this.cache.delete(key)\n pruned++\n }\n }\n\n return pruned\n }\n}\n","import type { ManagedCache, CacheOptions, CacheStats } from '@gzl10/nexus-sdk'\nimport { LRUCache } from './lru-cache.js'\n\n/**\n * Thin wrapper over LRUCache implementing the ManagedCache SDK contract.\n * Adds name tracking and delegates to addRules callback for invalidation.\n */\nexport class ManagedCacheImpl<T = unknown> implements ManagedCache<T> {\n private cache: LRUCache<T>\n private onAddRules?: (events: string[]) => void\n\n constructor(\n public readonly name: string,\n options?: CacheOptions,\n onAddRules?: (events: string[]) => void\n ) {\n this.cache = new LRUCache<T>({\n maxEntries: options?.maxEntries,\n defaultTTL: options?.defaultTTL\n })\n this.onAddRules = onAddRules\n }\n\n async get(key: string): Promise<T | null> {\n return this.cache.get(key)\n }\n\n async set(key: string, value: T, ttl?: number): Promise<void> {\n this.cache.set(key, value, ttl)\n }\n\n async delete(key: string): Promise<boolean> {\n return this.cache.delete(key)\n }\n\n async deleteByPrefix(prefix: string): Promise<number> {\n return this.cache.deleteByPrefix(prefix)\n }\n\n async has(key: string): Promise<boolean> {\n return this.cache.has(key)\n }\n\n async clear(): Promise<void> {\n this.cache.clear()\n }\n\n getStats(): CacheStats {\n return this.cache.getStats()\n }\n\n resetStats(): void {\n this.cache.resetStats()\n }\n\n addInvalidationRules(events: string[]): void {\n this.onAddRules?.(events)\n }\n\n async prune(): Promise<number> {\n return this.cache.prune()\n }\n\n async getSize(): Promise<number> {\n return this.cache.size\n }\n}\n","import type { ManagedCache, CacheOptions, CacheStats } from '@gzl10/nexus-sdk'\nimport type { Redis } from 'ioredis'\n\n/**\n * Redis-backed implementation of ManagedCache<T>.\n *\n * Key schema: ${prefix}:cache:${cacheName}:${key}\n * The `cache:` namespace prevents collision with RedisAdapter (entity storage).\n *\n * Size tracking: Uses a local counter (trackedSize) instead of querying Redis\n * on every getSize() call. May drift from Redis reality if keys expire via TTL.\n */\nexport class RedisManagedCache<T = unknown> implements ManagedCache<T> {\n private readonly keyPrefix: string\n private hits = 0\n private misses = 0\n private trackedSize = 0\n private maxEntries: number\n private defaultTTL?: number\n private onAddRules?: (events: string[]) => void\n\n constructor(\n public readonly name: string,\n private readonly client: Redis,\n prefix: string,\n options?: CacheOptions,\n onAddRules?: (events: string[]) => void\n ) {\n this.keyPrefix = `${prefix}:cache:${name}`\n this.maxEntries = options?.maxEntries ?? 100\n this.defaultTTL = options?.defaultTTL\n this.onAddRules = onAddRules\n }\n\n async get(key: string): Promise<T | null> {\n const raw = await this.client.get(this.redisKey(key))\n if (raw === null) {\n this.misses++\n return null\n }\n this.hits++\n return JSON.parse(raw) as T\n }\n\n async set(key: string, value: T, ttl?: number): Promise<void> {\n const serialized = JSON.stringify(value)\n const effectiveTTL = ttl ?? this.defaultTTL\n\n const existed = await this.client.exists(this.redisKey(key))\n\n if (effectiveTTL) {\n await this.client.setex(this.redisKey(key), effectiveTTL, serialized)\n } else {\n await this.client.set(this.redisKey(key), serialized)\n }\n\n if (!existed) {\n this.trackedSize++\n }\n }\n\n async delete(key: string): Promise<boolean> {\n const deleted = await this.client.del(this.redisKey(key))\n if (deleted > 0) {\n this.trackedSize = Math.max(0, this.trackedSize - 1)\n return true\n }\n return false\n }\n\n async deleteByPrefix(prefix: string): Promise<number> {\n const pattern = `${this.keyPrefix}:${prefix}*`\n let cursor = '0'\n let deleted = 0\n do {\n const [next, keys] = await this.client.scan(cursor, 'MATCH', pattern, 'COUNT', 100)\n cursor = next\n if (keys.length > 0) {\n await this.client.del(...keys)\n deleted += keys.length\n }\n } while (cursor !== '0')\n this.trackedSize = Math.max(0, this.trackedSize - deleted)\n return deleted\n }\n\n async has(key: string): Promise<boolean> {\n return (await this.client.exists(this.redisKey(key))) === 1\n }\n\n async clear(): Promise<void> {\n await this.deleteByPrefix('')\n this.trackedSize = 0\n }\n\n async prune(): Promise<number> {\n return 0\n }\n\n async getSize(): Promise<number> {\n return this.trackedSize\n }\n\n getStats(): CacheStats {\n const total = this.hits + this.misses\n return {\n hits: this.hits,\n misses: this.misses,\n hitRate: total > 0 ? this.hits / total : 0,\n size: this.trackedSize,\n maxEntries: this.maxEntries\n }\n }\n\n resetStats(): void {\n this.hits = 0\n this.misses = 0\n }\n\n addInvalidationRules(events: string[]): void {\n this.onAddRules?.(events)\n }\n\n private redisKey(key: string): string {\n return `${this.keyPrefix}:${key}`\n }\n}\n","import type { ScopedCacheManager, ManagedCache, CacheOptions, CacheStats } from '@gzl10/nexus-sdk'\nimport type { CacheManager, CacheLogger } from './cache-manager.js'\n\nexport class ScopedCacheManagerImpl implements ScopedCacheManager {\n private localNames = new Set<string>()\n\n constructor(\n private scope: string,\n private manager: CacheManager,\n private maxCaches: number,\n private logger?: CacheLogger\n ) {}\n\n create<T = unknown>(name: string, options?: CacheOptions): ManagedCache<T> {\n if (this.localNames.has(name)) {\n throw new Error(`Cache \"${this.scope}:${name}\" already exists. Use getOrCreate() for idempotent access.`)\n }\n return this.createInternal<T>(name, options)\n }\n\n getOrCreate<T = unknown>(name: string, options?: CacheOptions): ManagedCache<T> {\n const existing = this.manager.get<T>(this.fullName(name))\n if (existing) return existing\n return this.createInternal<T>(name, options)\n }\n\n get<T = unknown>(name: string): ManagedCache<T> | undefined {\n return this.manager.get<T>(this.fullName(name))\n }\n\n async clearAll(): Promise<void> {\n for (const name of this.localNames) {\n await this.manager.get(this.fullName(name))?.clear()\n }\n }\n\n stats(): Record<string, CacheStats> {\n const result: Record<string, CacheStats> = {}\n for (const name of this.localNames) {\n const cache = this.manager.get(this.fullName(name))\n if (cache) result[name] = cache.getStats()\n }\n return result\n }\n\n async destroy(): Promise<void> {\n for (const name of this.localNames) {\n await this.manager.unregister(this.fullName(name))\n }\n this.localNames.clear()\n }\n\n private createInternal<T>(name: string, options?: CacheOptions): ManagedCache<T> {\n if (this.localNames.size >= this.maxCaches) {\n this.logger?.warn({ scope: this.scope, max: this.maxCaches }, 'Scope cache limit reached')\n throw new Error(`Scope \"${this.scope}\" exceeded max caches (${this.maxCaches})`)\n }\n\n const full = this.fullName(name)\n const cache = this.manager.createCache<T>(\n full,\n options,\n (events) => this.manager.addRulesForCache(full, cache, events)\n )\n\n this.localNames.add(name)\n this.manager.register(full, this.scope, cache, options?.invalidateOn)\n return cache\n }\n\n private fullName(name: string): string {\n return `${this.scope}:${name}`\n }\n}\n","import type { CacheManagerContract, CacheStats, ScopedCacheManager, ManagedCache, CacheOptions } from '@gzl10/nexus-sdk'\nimport type { EventEmitter2 } from 'eventemitter2'\nimport type { Redis } from 'ioredis'\nimport { ManagedCacheImpl } from './managed-cache.js'\nimport { RedisManagedCache } from './redis-managed-cache.js'\nimport { ScopedCacheManagerImpl } from './scoped-cache-manager.js'\n\ninterface InvalidationBinding {\n event: string\n handler: () => void | Promise<void>\n}\n\n/** Minimal logger interface to avoid coupling to pino */\nexport interface CacheLogger {\n debug: (obj: Record<string, unknown>, msg: string) => void\n info: (obj: Record<string, unknown>, msg: string) => void\n warn: (obj: Record<string, unknown>, msg: string) => void\n}\n\nexport interface CacheManagerOptions {\n maxCachesPerScope?: number\n adapter?: 'memory' | 'redis'\n redisClient?: Redis\n redisPrefix?: string\n}\n\nexport class CacheManager implements CacheManagerContract {\n private caches = new Map<string, ManagedCache<any>>()\n private rules = new Map<string, InvalidationBinding[]>()\n private scopeRegistry = new Map<string, Set<string>>()\n private listeners: InvalidationBinding[] = []\n private pruneInterval?: ReturnType<typeof setInterval>\n private defaultMaxCachesPerScope: number\n private adapterType: 'memory' | 'redis'\n private redisClient?: Redis\n private _redisPrefix: string\n\n constructor(\n private emitter: EventEmitter2,\n private logger?: CacheLogger,\n options?: CacheManagerOptions\n ) {\n this.defaultMaxCachesPerScope = options?.maxCachesPerScope ?? 20\n this.adapterType = options?.adapter ?? 'memory'\n this.redisClient = options?.redisClient\n this._redisPrefix = options?.redisPrefix ?? 'nexus'\n\n if (this.adapterType === 'redis' && !this.redisClient) {\n throw new Error('redisClient is required when adapter is \"redis\"')\n }\n }\n\n /** Redis client (if using Redis adapter). Used by diagnostics. */\n get redis(): Redis | undefined {\n return this.redisClient\n }\n\n scoped(scope: string, options?: { maxCaches?: number }): ScopedCacheManager {\n return new ScopedCacheManagerImpl(\n scope,\n this,\n options?.maxCaches ?? this.defaultMaxCachesPerScope,\n this.logger\n )\n }\n\n /** Factory method — creates the appropriate cache implementation */\n createCache<T>(name: string, options?: CacheOptions, onAddRules?: (events: string[]) => void): ManagedCache<T> {\n if (this.adapterType === 'redis' && this.redisClient) {\n return new RedisManagedCache<T>(name, this.redisClient, this._redisPrefix, options, onAddRules)\n }\n return new ManagedCacheImpl<T>(name, options, onAddRules)\n }\n\n /** @internal — called by ScopedCacheManager */\n register<T>(fullName: string, scope: string, cache: ManagedCache<T>, invalidateOn?: string[]): void {\n this.caches.set(fullName, cache)\n\n if (!this.scopeRegistry.has(scope)) {\n this.scopeRegistry.set(scope, new Set())\n }\n this.scopeRegistry.get(scope)!.add(fullName)\n\n if (invalidateOn?.length) {\n this.bindRules(fullName, cache, invalidateOn)\n }\n\n this.logger?.debug({ cache: fullName, scope, invalidateOn }, 'Cache registered')\n }\n\n /** @internal — add rules to existing cache */\n addRulesForCache<T>(fullName: string, cache: ManagedCache<T>, events: string[]): void {\n this.bindRules(fullName, cache, events)\n }\n\n async unregister(fullName: string): Promise<void> {\n const bindings = this.rules.get(fullName)\n if (bindings) {\n for (const { event, handler } of bindings) {\n this.emitter.off(event, handler)\n }\n this.listeners = this.listeners.filter(\n l => !bindings.some(b => b.event === l.event && b.handler === l.handler)\n )\n this.rules.delete(fullName)\n }\n\n await this.caches.get(fullName)?.clear()\n this.caches.delete(fullName)\n\n for (const [, names] of this.scopeRegistry) {\n names.delete(fullName)\n }\n\n this.logger?.debug({ cache: fullName }, 'Cache unregistered')\n }\n\n get<T = unknown>(fullName: string): ManagedCache<T> | undefined {\n return this.caches.get(fullName) as ManagedCache<T> | undefined\n }\n\n stats(): Record<string, CacheStats> {\n const result: Record<string, CacheStats> = {}\n for (const [name, cache] of this.caches) {\n result[name] = cache.getStats()\n }\n return result\n }\n\n async invalidate(eventPattern: string): Promise<void> {\n for (const [fullName, bindings] of this.rules) {\n if (bindings.some(b => b.event === eventPattern)) {\n await this.caches.get(fullName)?.clear()\n this.logger?.debug({ cache: fullName, event: eventPattern }, 'Cache invalidated')\n }\n }\n }\n\n startPruneScheduler(intervalMs = 300_000): void {\n if (this.pruneInterval) clearInterval(this.pruneInterval)\n\n const scopeEntries = () => [...this.scopeRegistry.keys()]\n let index = 0\n\n this.pruneInterval = setInterval(() => {\n const scopes = scopeEntries()\n if (scopes.length === 0) return\n const scope = scopes[index % scopes.length]!\n for (const fullName of this.scopeRegistry.get(scope) ?? []) {\n this.caches.get(fullName)?.prune().catch(() => {})\n }\n index++\n }, intervalMs)\n }\n\n async destroy(): Promise<void> {\n const count = this.caches.size\n for (const { event, handler } of this.listeners) {\n this.emitter.off(event, handler)\n }\n if (this.pruneInterval) clearInterval(this.pruneInterval)\n for (const cache of this.caches.values()) await cache.clear()\n this.caches.clear()\n this.rules.clear()\n this.scopeRegistry.clear()\n this.listeners = []\n this.logger?.info({ caches: count }, 'CacheManager destroyed')\n }\n\n private bindRules<T>(fullName: string, cache: ManagedCache<T>, events: string[]): void {\n const existing = this.rules.get(fullName) ?? []\n const newBindings: InvalidationBinding[] = []\n\n for (const event of events) {\n if (existing.some(b => b.event === event)) continue\n\n const handler = async () => {\n try {\n await cache.clear()\n this.logger?.debug({ cache: fullName, event }, 'Cache cleared by event')\n } catch (err) {\n this.logger?.warn({ cache: fullName, event, err }, 'Failed to clear cache by event')\n }\n }\n this.emitter.on(event, handler)\n newBindings.push({ event, handler })\n this.listeners.push({ event, handler })\n }\n\n this.rules.set(fullName, [...existing, ...newBindings])\n }\n}\n","/**\n * Module context factory.\n *\n * Creates the context injected into all Nexus modules, providing\n * database access, utilities, services registry, and entity factories.\n *\n * Structure:\n * - ctx.core - Logger, errors, crypto, socket, events, middleware\n * - ctx.db - Knex connection, adapters, migration helpers\n * - ctx.runtime - Entity service/controller/router factories\n * - ctx.config - Environment variables and resolved NexusConfig\n * - ctx.engine - Module/plugin introspection\n * - ctx.services - Inter-module service registry\n * - ctx.adapters - External data source adapters\n */\n\nimport { ForbiddenError as CASLForbiddenError, subject } from '@casl/ability'\n\n// SDK types\nimport type {\n ModuleContext,\n CoreContext,\n DbContext,\n RuntimeContext,\n ConfigContext,\n EngineContext,\n ServicesContext,\n AdaptersContext,\n ModuleMiddlewares,\n DatabaseAdapter,\n SchemaAdapter,\n EntityDefinition\n} from '@gzl10/nexus-sdk'\nimport { DEFAULT_TENANT_ID, DEFAULT_LOCALES } from '@gzl10/nexus-sdk'\nimport type { LocaleConfig } from '@gzl10/nexus-sdk'\n\n// Namespace imports\nimport * as core from '../core/index.js'\nimport * as db from '../db/index.js'\nimport { getMemoryKnex, destroyMemoryKnex } from '../db/memory-knex.js'\nimport * as engineNs from './index.js'\nimport * as runtime from '../runtime/index.js'\n\n// Config\nimport { getConfig, env } from '../config/env.js'\n\n// Plugin ops\nimport * as pluginOps from '../core/plugin-ops.js'\nimport { discoverPlugins } from '../config/load-config.js'\n\n// Events API\nimport { createEventsApi } from './events-api.js'\n\n// Seed\nimport { runModuleSeed } from '../db/seed-runner.js'\n\n// Cache\nimport { CacheManager } from '../core/cache/cache-manager.js'\nimport { Redis } from 'ioredis'\n\n// Types\nimport type { ModuleServices } from './types.js'\n\n// Platform locales (set via setLocales before createModuleContext)\nlet platformLocales: LocaleConfig[] = DEFAULT_LOCALES\n\n/** Set platform locales before creating context. Called by start(). */\nexport function setLocales(locales: LocaleConfig[]): void {\n platformLocales = locales\n}\n\n// Singleton CacheManager (shared across all contexts, one per server lifecycle)\nlet sharedCacheManager: CacheManager | null = null\n\nfunction getSharedCacheManager(): CacheManager {\n if (!sharedCacheManager) {\n const redisUrl = env.REDIS_URL\n let redisClient: Redis | undefined\n let adapter: 'redis' | 'memory' = 'memory'\n\n if (redisUrl) {\n try {\n redisClient = new Redis(redisUrl, {\n lazyConnect: false,\n maxRetriesPerRequest: 3,\n retryStrategy: (times: number) => {\n if (times > 3) return null\n return Math.min(times * 200, 2000)\n }\n })\n\n redisClient.on('error', (err) => {\n core.logger.warn({ err: err.message }, 'Redis connection error')\n })\n redisClient.on('reconnecting', () => {\n core.logger.info('Redis reconnecting...')\n })\n\n adapter = 'redis'\n core.logger.info('CacheManager: using Redis adapter')\n } catch (err) {\n core.logger.warn(\n { err: (err as Error).message },\n 'CacheManager: Redis unavailable, falling back to memory adapter'\n )\n redisClient = undefined\n }\n }\n\n sharedCacheManager = new CacheManager(\n core.nexusEvents as unknown as import('eventemitter2').EventEmitter2,\n core.logger.child({ service: 'cache' }),\n {\n adapter,\n redisClient,\n redisPrefix: env.REDIS_PREFIX\n }\n )\n sharedCacheManager.startPruneScheduler()\n }\n return sharedCacheManager\n}\n\n/**\n * Reset CacheManager (for tests and server restart).\n */\nexport async function resetCacheManager(): Promise<void> {\n await sharedCacheManager?.destroy()\n sharedCacheManager = null\n}\n\n// Singleton temp adapter (shared across all contexts so seeds persist into routing)\nlet sharedTempAdapter: DatabaseAdapter | null = null\n\nfunction getSharedTempAdapter(): DatabaseAdapter {\n if (env.REDIS_URL) {\n if (!sharedTempAdapter) {\n sharedTempAdapter = db.createRedisAdapter({\n url: env.REDIS_URL,\n prefix: env.REDIS_PREFIX\n })\n }\n return sharedTempAdapter\n }\n if (!sharedTempAdapter) {\n sharedTempAdapter = db.createInMemoryAdapter()\n }\n return sharedTempAdapter!\n}\n\n/**\n * Reset shared adapters (for tests and server restart).\n */\nexport async function resetSharedAdapters(): Promise<void> {\n sharedTempAdapter = null\n await resetCacheManager()\n await destroyMemoryKnex()\n}\n\n/**\n * Creates the context injected into modules.\n *\n * @example\n * ```typescript\n * const myModule: ModuleManifest = {\n * name: 'my-module',\n * init: (ctx) => {\n * ctx.core.logger.info('Module initialized')\n * ctx.services.register('myService', createMyService(ctx))\n * },\n * migrate: async (ctx) => {\n * const { knex } = ctx.db\n * if (!await knex.schema.hasTable('my_table')) {\n * await knex.schema.createTable('my_table', (t) => {\n * t.uuid('id').primary()\n * ctx.db.addTimestamps(t, knex)\n * })\n * }\n * }\n * }\n * ```\n */\nexport function createModuleContext(): ModuleContext {\n // Internal registries\n const servicesRegistry = {} as ModuleServices\n const adaptersRegistry: Record<string, { data: DatabaseAdapter; schema?: SchemaAdapter }> = {}\n\n // Database setup\n const knex = db.getDb()\n const defaultAdapter = db.createKnexAdapter(knex)\n const defaultSchemaAdapter = db.createKnexSchemaAdapter(knex)\n\n // Register 'temp' adapter for TempEntityDefinition\n // Uses Redis if REDIS_URL is defined, otherwise falls back to InMemory\n // Shared singleton so data persists across context instances (seed → routing)\n adaptersRegistry['temp'] = { data: getSharedTempAdapter() }\n core.logger.trace(env.REDIS_URL ? 'Temp adapter: Redis (shared)' : 'Temp adapter: InMemory (shared)')\n\n // Note: 'memory' adapter is now resolved via ctx.db.getKnex('memory') (SQLite :memory:)\n\n // Middleware factories\n const middleware: ModuleMiddlewares = {\n validate: core.validate,\n rateLimit: core.createRateLimit,\n requestTimeout: core.requestTimeout\n }\n\n // ==========================================================================\n // Core namespace\n // ==========================================================================\n\n // Tenant-scoped logger: all downstream logs include tenantId\n const tenantLogger = core.logger.child({ tenantId: DEFAULT_TENANT_ID })\n\n const coreContext: CoreContext = {\n logger: tenantLogger,\n\n errors: {\n AppError: core.AppError,\n NotFoundError: core.NotFoundError,\n UnauthorizedError: core.UnauthorizedError,\n ForbiddenError: core.ForbiddenError,\n ConflictError: core.ConflictError,\n ValidationError: core.ValidationError,\n codes: core.ErrorCodes\n },\n\n crypto: {\n hashPassword: core.hashPassword,\n verifyPassword: core.verifyPassword,\n DUMMY_HASH: core.DUMMY_HASH,\n encrypt: core.encrypt,\n decrypt: core.decrypt\n },\n\n cache: getSharedCacheManager().scoped('app'),\n\n socket: {\n getIO: core.getIO,\n isInitialized: core.isSocketIOInitialized,\n isUserConnected: core.isUserConnected,\n getUserSocketCount: core.getUserSocketCount,\n getConnectedUsers: core.getConnectedUsers,\n joinUserToRoom: core.joinUserToRoom,\n removeUserFromRoom: core.removeUserFromRoom,\n joinSocketToRoom: core.joinSocketToRoom,\n removeSocketFromRoom: core.removeSocketFromRoom,\n getRoomMembers: core.getRoomMembers,\n getRoomSize: core.getRoomSize,\n getUserRooms: core.getUserRooms,\n roomExists: core.roomExists,\n emitToRoom: core.emitToRoom,\n emitToUser: core.emitToUser,\n emitToRole: core.emitToRole,\n emitToAll: core.emitToAll,\n emitToAuthenticated: core.emitToAuthenticated,\n broadcastToRoom: core.broadcastToRoom,\n onReady: core.onSocketReady\n },\n\n sse: core.createSSEHelper(),\n\n abilities: {\n defineAbilityFor: core.defineAbilityFor as CoreContext['abilities']['defineAbilityFor'],\n packRules: core.packRules as CoreContext['abilities']['packRules'],\n subject,\n ForbiddenError: CASLForbiddenError\n },\n\n events: core.nexusEvents as unknown as CoreContext['events'],\n\n hub: {\n registerRule: core.eventBridge.registerRule.bind(core.eventBridge),\n removeRule: core.eventBridge.removeRule.bind(core.eventBridge)\n },\n\n middleware,\n\n createRouter: () => core.createRouter(),\n\n generateId: core.generateId,\n generateIdByType: core.generateIdByType,\n getLibPath: core.getLibPath,\n getProjectPath: core.getProjectPath,\n safeJsonParse: <T>(jsonString: string, fallback: T, context?: Record<string, unknown>): T =>\n core.safeJsonParse(core.logger, jsonString, fallback, context),\n\n plugins: {\n getState: pluginOps.getPluginState,\n getAllStates: pluginOps.getAllPluginStates,\n install: pluginOps.installPlugin,\n uninstall: pluginOps.uninstallPlugin,\n enable: pluginOps.enablePlugin,\n disable: pluginOps.disablePlugin,\n normalizeName: pluginOps.normalizePluginName,\n shortName: pluginOps.shortPluginName,\n discover: () => discoverPlugins(core.getProjectPath()),\n }\n }\n\n // ==========================================================================\n // Database namespace\n // ==========================================================================\n\n // Knex connections for adapters (memory = SQLite :memory:)\n const knexConnections: Record<string, import('knex').Knex> = {\n memory: getMemoryKnex()\n }\n\n const dbContext: DbContext = {\n knex,\n t: (name: string) => name,\n adapter: defaultAdapter,\n schema: defaultSchemaAdapter,\n\n // Migration helpers\n addTimestamps: db.addTimestamps,\n addAuditFieldsIfMissing: db.addAuditFieldsIfMissing,\n addSoftDeleteFieldIfMissing: db.addSoftDeleteFieldIfMissing,\n addConfigDefaultField: db.addConfigDefaultField,\n addColumnIfMissing: db.addColumnIfMissing,\n\n // Timestamp helpers\n nowTimestamp: db.nowTimestamp,\n formatTimestamp: db.formatTimestamp,\n\n // Query helpers\n applySearchFilter: db.applySearchFilter,\n getPagination: db.getPagination,\n buildPaginatedResult: db.buildPaginatedResult,\n\n // Adapter-aware Knex resolver\n getKnex(adapter?: string) {\n if (!adapter) return knex\n const conn = knexConnections[adapter]\n if (!conn) {\n throw new Error(`Knex connection for adapter \"${adapter}\" not found. Available: ${Object.keys(knexConnections).join(', ')}`)\n }\n return conn\n },\n\n // Placeholder — bound after ctx construction (needs full ModuleContext)\n seedModule: null as unknown as DbContext['seedModule']\n }\n\n // ==========================================================================\n // Runtime namespace\n // ==========================================================================\n // Factory functions use `this: ModuleContext` so they are bound after context creation\n\n // ==========================================================================\n // Config namespace\n // ==========================================================================\n const configContext: ConfigContext = {\n env,\n resolved: getConfig() as unknown as Record<string, unknown>\n }\n\n // ==========================================================================\n // Engine namespace\n // ==========================================================================\n const engineContext: EngineContext = {\n getModules: engineNs.getModules,\n getPlugins: engineNs.getPlugins,\n getModuleSubjects: engineNs.getModuleSubjects,\n getRegisteredSubjects: engineNs.getRegisteredSubjects,\n getCoreModules: engineNs.getCoreModules,\n getUserModules: engineNs.getUserModules,\n getCoreManifest: engineNs.getCoreManifest,\n getUserManifest: engineNs.getUserManifest,\n hasUserApp: engineNs.hasUserApp,\n getSubjectForTable: engineNs.getSubjectForTable,\n hasModule: engineNs.hasModule,\n hasPlugin: engineNs.hasPlugin,\n getPluginByCode: engineNs.getPluginByCode,\n hasPluginByCode: engineNs.hasPluginByCode\n }\n\n // ==========================================================================\n // Services namespace (with Proxy for direct access)\n // ==========================================================================\n const servicesMethods = {\n register(name: string, service: unknown): void {\n servicesRegistry[name] = service\n },\n\n get<T>(name: string): T {\n const service = servicesRegistry[name]\n if (!service) {\n throw new Error(`Service \"${name}\" not initialized. Ensure the module loads before accessing it.`)\n }\n return service as T\n },\n\n getOptional<T>(name: string): T | undefined {\n return servicesRegistry[name] as T | undefined\n },\n\n has(name: string): boolean {\n return !!servicesRegistry[name]\n },\n\n getBySuffix<T>(suffix: string): Array<{ name: string; service: T }> {\n return Object.entries(servicesRegistry)\n .filter(([name]) => name.endsWith(suffix))\n .map(([name, service]) => ({ name, service: service as T }))\n }\n }\n\n // Proxy for direct service access: ctx.services.users, ctx.services['mail.provider']\n const servicesContext = new Proxy(servicesMethods, {\n get(target, prop: string) {\n // First check if it's a method on the base object\n if (prop in target) {\n return target[prop as keyof typeof target]\n }\n // Otherwise, direct service access\n return servicesRegistry[prop]\n },\n set(_, prop: string, value) {\n servicesRegistry[prop] = value\n return true\n }\n }) as ServicesContext\n\n // ==========================================================================\n // Adapters namespace\n // ==========================================================================\n const adaptersContext: AdaptersContext = {\n register(name: string, dataAdapter: DatabaseAdapter, schemaAdapter?: SchemaAdapter): void {\n if (adaptersRegistry[name]) {\n core.logger.warn({ adapter: name }, 'Adapter already registered, overwriting')\n }\n adaptersRegistry[name] = { data: dataAdapter, schema: schemaAdapter }\n core.logger.debug({ adapter: name, hasSchema: !!schemaAdapter }, 'Adapter registered')\n },\n\n get<T extends DatabaseAdapter = DatabaseAdapter>(name?: string): T {\n if (!name) return defaultAdapter as T\n const pair = adaptersRegistry[name]\n if (!pair) {\n throw new Error(`Adapter \"${name}\" not found. Available: ${Object.keys(adaptersRegistry).join(', ') || 'none'}`)\n }\n return pair.data as T\n },\n\n getSchema<T extends SchemaAdapter = SchemaAdapter>(name?: string): T | undefined {\n if (!name) return defaultSchemaAdapter as T\n const pair = adaptersRegistry[name]\n if (!pair) return undefined\n return pair.schema as T | undefined\n },\n\n has(name: string): boolean {\n return name in adaptersRegistry\n }\n }\n\n // ==========================================================================\n // Compose final context\n // ==========================================================================\n const ctx: ModuleContext = {\n tenantId: DEFAULT_TENANT_ID,\n\n core: coreContext,\n db: dbContext,\n runtime: null as unknown as RuntimeContext, // Placeholder, set below\n config: configContext,\n engine: engineContext,\n services: servicesContext,\n adapters: adaptersContext,\n\n // Root-level shortcuts for frequently used utilities\n events: createEventsApi(core.nexusEvents, core.logger),\n createRouter: () => core.createRouter(),\n locales: platformLocales\n }\n\n // Pre-register shared CacheManager so modules can access it via ctx.services\n servicesRegistry['cacheManager'] = getSharedCacheManager()\n\n // Bind runtime factory functions to the context\n ctx.runtime = {\n createEntityService: ((def: EntityDefinition, opts?: unknown) =>\n runtime.createEntityService(ctx, def, opts as never)) as RuntimeContext['createEntityService'],\n createEntityServiceAsync: ((def: EntityDefinition, opts?: unknown) =>\n runtime.createEntityServiceAsync(ctx, def, opts as never)) as RuntimeContext['createEntityServiceAsync'],\n createEntityController: (service, def) => runtime.createEntityController(service, def, ctx),\n createEntityRouter: (controller, def) => runtime.createEntityRouter(controller, def, ctx)\n }\n\n // Bind seed function (needs full ctx for entity auto-seed)\n ctx.db.seedModule = (mod) => runModuleSeed(mod, ctx)\n\n return ctx\n}\n","/**\n * @module engine\n * @description Module and plugin registration system with dependency resolution\n *\n * @dependencies\n * - db/ (connection, schema-helpers, query-helpers, adapters)\n * - core/ (logger, events, utils, errors, middleware, crypto, socket, abilities, sse)\n * - runtime/ (entity-factory) - for createModuleContext()\n *\n * @exports registerModule - Register a module manifest\n * @exports registerPlugin - Register a plugin manifest\n * @exports shouldLoadPlugin - Check if plugin is enabled in nexus.plugins.json\n * @exports getModules - Get all registered modules\n * @exports getModule - Get module by name\n * @exports loadCoreModules - Load built-in core modules\n * @exports createModuleContext - Create context for module initialization\n */\n\n// Escritura\nexport { registerModule, registerPlugin, shouldLoadPlugin } from './registry.js'\n\n// Lectura\nexport {\n getModules,\n getOrderedModules,\n getOrderedModulesInternal,\n getModule,\n getPlugins,\n getPlugin,\n getRegisteredSubjects,\n isValidSubject,\n getSubjectForTable,\n getCoreModules,\n getUserModules,\n getCoreManifest,\n getUserManifest,\n hasUserApp,\n hasModule,\n hasPlugin,\n getPluginByCode,\n hasPluginByCode\n} from './module-queries.js'\n\nexport type { AppManifest } from './module-queries.js'\n\n// Loader\nexport { loadCoreModules } from './loader.js'\n\n// Store reset\nexport { resetStore } from './module-store.js'\n\n// Internal types (for module-routes.ts and similar internal use)\nexport type { RegisteredModule } from './module-store.js'\n\n// Extractors\nexport { getModuleSubjects } from './subject-extractor.js'\nexport { getTableAndSubject, topologicalSort, topologicalSortPlugins, validateModuleDependencies, validatePluginDependencies } from './definition-extractors.js'\n\n// Context factory\nexport { createModuleContext, resetSharedAdapters, resetCacheManager, setLocales } from './context.js'\n\n// Re-export types\nexport type { ModuleManifest, PluginManifest } from '@gzl10/nexus-sdk'\n","import type { OpenAPIObject, SchemaObject, PathItemObject } from 'openapi3-ts/oas31'\nimport type {\n CollectionEntityDefinition,\n SingleEntityDefinition,\n // ConfigEntityDefinition absorbed into SingleEntityDefinition\n EventEntityDefinition,\n TreeEntityDefinition,\n DagEntityDefinition,\n ViewEntityDefinition,\n ComputedEntityDefinition,\n ModuleManifest,\n EntityDefinition,\n CustomRouteDefinition\n} from '@gzl10/nexus-sdk'\nimport { resolveLocalized } from '@gzl10/nexus-sdk'\nimport {\n entityToSchema,\n entityToCreateSchema,\n entityToUpdateSchema,\n actionToInputSchema,\n fieldsToSchema\n} from './schema-builder.js'\nimport {\n collectionToPaths,\n singleToPaths,\n configToPaths,\n eventToPaths,\n treeToPaths,\n dagToPaths,\n viewToPaths,\n computedToPaths,\n actionToPaths,\n customRouteToPaths\n} from './path-builder.js'\nimport { getOrderedModules } from '../../engine/index.js'\nimport type { AppAbility } from '../abilities/ability.types.js'\n\nexport interface OpenAPIConfig {\n title: string\n version: string\n description?: string\n servers?: Array<{ url: string; description?: string }>\n}\n\n/**\n * Options for filtering OpenAPI spec based on CASL abilities\n */\nexport interface OpenAPIFilterOptions {\n /** User's CASL ability for filtering. If not provided, shows all endpoints. */\n ability?: AppAbility\n /** Cache key for role-based caching (e.g., role name) */\n cacheKey?: string\n}\n\n// Role-based cache for generated specs\nconst specCache = new Map<string, { spec: OpenAPIObject; timestamp: number }>()\nconst CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes\n\n\n/**\n * Generates OpenAPI spec from registered modules.\n * Optionally filters based on CASL abilities for role-based documentation.\n *\n * @param config - OpenAPI configuration (title, version, etc.)\n * @param filterOptions - Optional CASL ability for filtering endpoints by permissions\n */\nexport function generateOpenAPISpec(config: OpenAPIConfig, filterOptions?: OpenAPIFilterOptions): OpenAPIObject {\n const { ability, cacheKey } = filterOptions ?? {}\n\n // Check cache first (only if cacheKey provided)\n if (cacheKey) {\n const cached = specCache.get(cacheKey)\n if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {\n return cached.spec\n }\n }\n\n const modules = getOrderedModules()\n\n const schemas: Record<string, SchemaObject> = {}\n const paths: Record<string, PathItemObject> = {}\n const tags: Array<{ name: string; description?: string }> = []\n\n // Shared schemas\n schemas['ErrorResponse'] = {\n type: 'object',\n required: ['error'],\n properties: {\n error: {\n type: 'object',\n required: ['code', 'message'],\n properties: {\n code: { type: 'string' },\n message: { type: 'string' },\n requestId: { type: 'string' }\n }\n },\n details: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n field: { type: 'string' },\n message: { type: 'string' }\n }\n }\n }\n }\n }\n\n for (const module of modules) {\n processModule(module, schemas, paths, tags, ability)\n }\n\n const spec: OpenAPIObject = {\n openapi: '3.1.0',\n info: {\n title: config.title,\n version: config.version,\n description: config.description\n },\n servers: config.servers ?? [{ url: '/api', description: 'Nexus API' }],\n tags,\n paths,\n components: {\n schemas,\n parameters: {\n PageParam: { name: 'page', in: 'query', schema: { type: 'integer', minimum: 1, default: 1 } },\n LimitParam: { name: 'limit', in: 'query', schema: { type: 'integer', minimum: 1, maximum: 100, default: 20 } },\n SortParam: { name: 'sort', in: 'query', schema: { type: 'string' } },\n OrderParam: { name: 'order', in: 'query', schema: { type: 'string', enum: ['asc', 'desc'] } },\n SearchParam: { name: 'search', in: 'query', schema: { type: 'string' } },\n FiltersParam: { name: 'filters', in: 'query', schema: { type: 'string' }, description: 'URL-encoded JSON filter object' }\n },\n securitySchemes: {\n bearerAuth: {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT'\n }\n }\n },\n security: [{ bearerAuth: [] }]\n }\n\n // Cache the spec if cacheKey provided\n if (cacheKey) {\n specCache.set(cacheKey, { spec, timestamp: Date.now() })\n }\n\n return spec\n}\n\n/**\n * Processes a module and extracts schemas/paths from its definitions.\n * Filters based on CASL ability if provided.\n */\nfunction processModule(\n module: ModuleManifest,\n schemas: Record<string, SchemaObject>,\n paths: Record<string, PathItemObject>,\n tags: Array<{ name: string; description?: string }>,\n ability?: AppAbility\n): void {\n const hasDefinitions = module.definitions?.length\n const hasActions = module.actions?.length\n const hasCustomRoutes = module.customRoutes?.length\n\n if (!hasDefinitions && !hasActions && !hasCustomRoutes) return\n\n const modulePrefix = module.routePrefix ?? `/${module.name}`\n const moduleTag = resolveLocalized(module.label, 'en')\n\n // Add module tag\n if (!tags.find(t => t.name === moduleTag)) {\n const desc = module.description ? resolveLocalized(module.description, 'en') : undefined\n tags.push({ name: moduleTag, description: desc })\n }\n\n // Process custom routes\n if (hasCustomRoutes) {\n for (const route of module.customRoutes!) {\n processCustomRoute(route, modulePrefix, moduleTag, schemas, paths)\n }\n }\n\n // Process standalone module actions\n if (hasActions) {\n for (const action of module.actions!) {\n const actionSchemaName = `${capitalize(module.name)}${capitalize(action.key)}Input`\n if (action.input) {\n schemas[actionSchemaName] = actionToInputSchema(action)\n }\n const actionPaths = actionToPaths(action, modulePrefix, actionSchemaName, moduleTag, 'module')\n Object.assign(paths, actionPaths)\n }\n }\n\n // Process entity definitions\n if (!hasDefinitions) return\n\n for (const definition of module.definitions!) {\n const defType = definition.type\n\n // CASL filtering: skip if user doesn't have read permission on subject\n if (ability) {\n const subject = getCaslSubject(definition)\n // Cast to any for dynamic subject check (subjects are strings at runtime)\n if (subject && !ability.can('read', subject as any)) {\n continue\n }\n }\n\n // Process based on type - each type has different structure\n switch (defType) {\n case 'collection': {\n const entity = definition as CollectionEntityDefinition\n if (!entity.fields) continue\n const basePath = buildBasePath(modulePrefix, { routePrefix: entity.routePrefix, table: entity.table })\n const schemaName = getSchemaName({ table: entity.table })\n processCollectionEntity(entity, basePath, schemaName, moduleTag, schemas, paths)\n break\n }\n\n case 'single': {\n const entity = definition as SingleEntityDefinition\n if (!entity.fields) continue\n const basePath = buildBasePath(modulePrefix, { routePrefix: entity.routePrefix, key: entity.key })\n const schemaName = getSchemaName({ key: entity.key })\n processSingleEntity(entity, basePath, schemaName, moduleTag, schemas, paths)\n break\n }\n\n case 'config': {\n // Config absorbed into single — treat scoped singles like collections for OpenAPI\n const entity = definition as SingleEntityDefinition\n if (!entity.fields) continue\n const basePath = buildBasePath(modulePrefix, { routePrefix: entity.routePrefix, key: entity.key })\n const schemaName = getSchemaName({ key: entity.key })\n processConfigEntity(entity, basePath, schemaName, moduleTag, schemas, paths)\n break\n }\n\n case 'event': {\n const entity = definition as EventEntityDefinition\n if (!entity.fields) continue\n const basePath = buildBasePath(modulePrefix, { routePrefix: entity.routePrefix, table: entity.table })\n const schemaName = getSchemaName({ table: entity.table })\n processEventEntity(entity, basePath, schemaName, moduleTag, schemas, paths)\n break\n }\n\n case 'tree': {\n const entity = definition as TreeEntityDefinition\n if (!entity.fields) continue\n const basePath = buildBasePath(modulePrefix, { routePrefix: entity.routePrefix, table: entity.table })\n const schemaName = getSchemaName({ table: entity.table })\n processTreeEntity(entity, basePath, schemaName, moduleTag, schemas, paths)\n break\n }\n\n case 'dag': {\n const entity = definition as DagEntityDefinition\n if (!entity.fields) continue\n const basePath = buildBasePath(modulePrefix, { routePrefix: entity.routePrefix, table: entity.table })\n const schemaName = getSchemaName({ table: entity.table })\n processDagEntity(entity, basePath, schemaName, moduleTag, schemas, paths)\n break\n }\n\n case 'view': {\n const entity = definition as ViewEntityDefinition\n if (!entity.fields) continue\n const basePath = buildBasePath(modulePrefix, { routePrefix: entity.routePrefix, table: entity.table })\n const schemaName = getSchemaName({ table: entity.table })\n schemas[schemaName] = fieldsToSchema(entity.fields)\n Object.assign(paths, viewToPaths(entity, basePath, schemaName, moduleTag))\n break\n }\n\n case 'computed':\n case 'virtual': {\n const entity = definition as ComputedEntityDefinition\n if (!entity.fields) continue\n const basePath = inferComputedBasePath(entity, modulePrefix)\n const labelEn = resolveLocalized(entity.label, 'en')\n const schemaName = capitalize(labelEn.replace(/\\s+/g, ''))\n schemas[schemaName] = fieldsToSchema(entity.fields)\n Object.assign(paths, computedToPaths(entity, basePath, schemaName, moduleTag))\n\n if (entity.actions?.length) {\n for (const action of entity.actions) {\n const actionSchemaName = `${schemaName}${capitalize(action.key)}Input`\n if (action.input) schemas[actionSchemaName] = actionToInputSchema(action)\n const scope = action.scope ?? 'row'\n Object.assign(paths, actionToPaths(action, basePath, actionSchemaName, moduleTag, scope))\n }\n }\n break\n }\n\n default:\n break\n }\n }\n}\n\n/**\n * Process collection entity (full CRUD + actions)\n */\nfunction processCollectionEntity(\n entity: CollectionEntityDefinition,\n basePath: string,\n schemaName: string,\n tag: string,\n schemas: Record<string, SchemaObject>,\n paths: Record<string, PathItemObject>\n): void {\n // Schemas\n schemas[schemaName] = entityToSchema(entity)\n schemas[`${schemaName}Create`] = entityToCreateSchema(entity)\n schemas[`${schemaName}Update`] = entityToUpdateSchema(entity)\n\n // CRUD paths\n const entityPaths = collectionToPaths(entity, basePath, schemaName, tag)\n Object.assign(paths, entityPaths)\n\n // Entity actions (row-level and entity-level)\n if (entity.actions?.length) {\n for (const action of entity.actions) {\n const actionSchemaName = `${schemaName}${capitalize(action.key)}Input`\n if (action.input) {\n schemas[actionSchemaName] = actionToInputSchema(action)\n }\n\n const scope = action.scope ?? 'row'\n const actionPaths = actionToPaths(action, basePath, actionSchemaName, tag, scope)\n Object.assign(paths, actionPaths)\n }\n }\n}\n\n/**\n * Process single entity (GET/PUT only)\n */\nfunction processSingleEntity(\n entity: SingleEntityDefinition,\n basePath: string,\n schemaName: string,\n tag: string,\n schemas: Record<string, SchemaObject>,\n paths: Record<string, PathItemObject>\n): void {\n schemas[schemaName] = fieldsToSchema(entity.fields)\n schemas[`${schemaName}Update`] = fieldsToSchema(entity.fields, { allOptional: true })\n\n const entityPaths = singleToPaths(entity, basePath, schemaName, tag)\n Object.assign(paths, entityPaths)\n}\n\n/**\n * Process config entity (GET/PUT by scope)\n */\nfunction processConfigEntity(\n entity: SingleEntityDefinition,\n basePath: string,\n schemaName: string,\n tag: string,\n schemas: Record<string, SchemaObject>,\n paths: Record<string, PathItemObject>\n): void {\n schemas[schemaName] = entityToSchema(entity as unknown as CollectionEntityDefinition)\n schemas[`${schemaName}Update`] = entityToUpdateSchema(entity as unknown as CollectionEntityDefinition)\n\n const entityPaths = configToPaths(entity, basePath, schemaName, tag)\n Object.assign(paths, entityPaths)\n}\n\n/**\n * Process event entity (GET list only, read-only)\n */\nfunction processEventEntity(\n entity: EventEntityDefinition,\n basePath: string,\n schemaName: string,\n tag: string,\n schemas: Record<string, SchemaObject>,\n paths: Record<string, PathItemObject>\n): void {\n schemas[schemaName] = entityToSchema(entity as unknown as CollectionEntityDefinition)\n\n const entityPaths = eventToPaths(entity, basePath, schemaName, tag)\n Object.assign(paths, entityPaths)\n}\n\n/**\n * Process tree entity (full CRUD + hierarchy)\n */\nfunction processTreeEntity(\n entity: TreeEntityDefinition,\n basePath: string,\n schemaName: string,\n tag: string,\n schemas: Record<string, SchemaObject>,\n paths: Record<string, PathItemObject>\n): void {\n schemas[schemaName] = entityToSchema(entity as unknown as CollectionEntityDefinition)\n schemas[`${schemaName}Create`] = entityToCreateSchema(entity as unknown as CollectionEntityDefinition)\n schemas[`${schemaName}Update`] = entityToUpdateSchema(entity as unknown as CollectionEntityDefinition)\n\n const entityPaths = treeToPaths(entity, basePath, schemaName, tag)\n Object.assign(paths, entityPaths)\n\n if (entity.actions?.length) {\n for (const action of entity.actions) {\n const actionSchemaName = `${schemaName}${capitalize(action.key)}Input`\n if (action.input) schemas[actionSchemaName] = actionToInputSchema(action)\n const scope = action.scope ?? 'row'\n Object.assign(paths, actionToPaths(action, basePath, actionSchemaName, tag, scope))\n }\n }\n}\n\n/**\n * Process DAG entity (tree + multi-parent)\n */\nfunction processDagEntity(\n entity: DagEntityDefinition,\n basePath: string,\n schemaName: string,\n tag: string,\n schemas: Record<string, SchemaObject>,\n paths: Record<string, PathItemObject>\n): void {\n schemas[schemaName] = entityToSchema(entity as unknown as CollectionEntityDefinition)\n schemas[`${schemaName}Create`] = entityToCreateSchema(entity as unknown as CollectionEntityDefinition)\n schemas[`${schemaName}Update`] = entityToUpdateSchema(entity as unknown as CollectionEntityDefinition)\n\n const entityPaths = dagToPaths(entity, basePath, schemaName, tag)\n Object.assign(paths, entityPaths)\n\n if (entity.actions?.length) {\n for (const action of entity.actions) {\n const actionSchemaName = `${schemaName}${capitalize(action.key)}Input`\n if (action.input) schemas[actionSchemaName] = actionToInputSchema(action)\n const scope = action.scope ?? 'row'\n Object.assign(paths, actionToPaths(action, basePath, actionSchemaName, tag, scope))\n }\n }\n}\n\n/**\n * Process custom route definition\n */\nfunction processCustomRoute(\n route: CustomRouteDefinition,\n modulePrefix: string,\n tag: string,\n schemas: Record<string, SchemaObject>,\n paths: Record<string, PathItemObject>\n): void {\n // Generate schema name from path (e.g., /send -> Send, /:id/approve -> Approve)\n const pathSegments = route.path.split('/').filter(s => s && !s.startsWith(':'))\n const schemaBaseName = pathSegments.map(s => capitalize(s)).join('') || 'Custom'\n const inputSchemaName = `${schemaBaseName}Input`\n\n // Create input schema if route has input fields\n if (route.input && Object.keys(route.input).length > 0) {\n schemas[inputSchemaName] = fieldsToSchema(route.input)\n }\n\n // Generate paths\n const routePaths = customRouteToPaths(\n route,\n modulePrefix,\n tag,\n route.input ? inputSchemaName : undefined\n )\n Object.assign(paths, routePaths)\n}\n\n/**\n * Builds the base path for an entity\n */\nfunction buildBasePath(modulePrefix: string, entity: { routePrefix?: string; table?: string; key?: string }): string {\n const entityPrefix = entity.routePrefix ?? `/${entity.table ?? entity.key ?? 'unknown'}`\n\n if (entityPrefix === '/') {\n return modulePrefix\n }\n\n return `${modulePrefix}${entityPrefix}`\n}\n\n/**\n * Infer base path for computed entities (no table field).\n * Mirrors logic from runtime/entity-factory.ts inferEntityRoutePath.\n */\nfunction inferComputedBasePath(definition: ComputedEntityDefinition, modulePrefix: string): string {\n const def = definition as ComputedEntityDefinition & { routePrefix?: string }\n if (def.routePrefix) {\n const rp = def.routePrefix.startsWith('/') ? def.routePrefix : `/${def.routePrefix}`\n return `${modulePrefix}${rp}`\n }\n const slug = resolveLocalized(definition.label, 'en').toLowerCase().replace(/\\s+/g, '-')\n return `${modulePrefix}/${slug}`\n}\n\n/**\n * Get schema name from entity\n */\nfunction getSchemaName(entity: { table?: string; key?: string }): string {\n return capitalize(entity.table ?? entity.key ?? 'Unknown')\n}\n\nfunction capitalize(str: string): string {\n return str.charAt(0).toUpperCase() + str.slice(1).replace(/_([a-z])/g, (_, c) => c.toUpperCase())\n}\n\n/**\n * Extracts CASL subject from an entity definition.\n * Returns undefined if no subject is defined (entity won't be filtered).\n */\nfunction getCaslSubject(definition: EntityDefinition): string | undefined {\n // Entity definitions have casl.subject\n const entity = definition as CollectionEntityDefinition | SingleEntityDefinition | EventEntityDefinition\n return entity.casl?.subject\n}\n","export { generateOpenAPISpec, type OpenAPIConfig, type OpenAPIFilterOptions } from './generator.js'\nexport {\n entityToSchema,\n entityToCreateSchema,\n entityToUpdateSchema,\n actionToInputSchema,\n fieldsToSchema\n} from './schema-builder.js'\nexport {\n collectionToPaths,\n singleToPaths,\n configToPaths,\n eventToPaths,\n treeToPaths,\n dagToPaths,\n viewToPaths,\n computedToPaths,\n actionToPaths,\n customRouteToPaths,\n entityToPaths // backward compat\n} from './path-builder.js'\n","import { AbilityBuilder, createMongoAbility, type MongoAbility, type RawRuleOf } from '@casl/ability'\nimport type { Actions, Subjects, SubjectStrings, AppAbility } from './ability.types.js'\nimport type { CaslRulesFunction } from '../../config/types.js'\nimport { logger } from '../logger/index.js'\n\nlet customCaslRules: CaslRulesFunction | undefined\n\n/** Seed-defined permissions: Map<roleName, Map<subject, actions[]>> */\nlet seedPermissions: Map<string, Map<string, string[]>> | null = null\n\n/**\n * Registers seed-defined permissions from onSeed hook.\n * Applied as step 4 in defineAbilityFor (after entity definitions and custom rules).\n */\nexport function setSeedPermissions(perms: Map<string, Map<string, string[]>> | null): void {\n seedPermissions = perms\n}\n\n/** Clears seed permissions (called on stop/restart). */\nexport function clearSeedPermissions(): void {\n seedPermissions = null\n}\n\n/**\n * Registers a function to define custom CASL rules.\n * Runs after loading permissions from entity definitions.\n */\nexport function setCustomCaslRules(fn: CaslRulesFunction | undefined): void {\n customCaslRules = fn\n}\n\n/**\n * Clears registered custom CASL rules.\n */\nexport function clearCustomCaslRules(): void {\n customCaslRules = undefined\n}\n\n/**\n * Minimal user type for ability building.\n * Accepts any object with id (and optionally email).\n */\nexport interface MinimalUser {\n id: string\n email?: string\n}\n\n/**\n * Internal user type with index signature for dynamic property access.\n * Used only internally for condition interpolation.\n */\ninterface IndexableUser extends MinimalUser {\n [key: string]: unknown\n}\n\n/**\n * Interpolates variables in permission conditions.\n * Supports: ${user.id}, ${user.email}, etc.\n */\nfunction interpolateConditions(\n conditions: Record<string, unknown>,\n user: IndexableUser\n): Record<string, unknown> {\n const result: Record<string, unknown> = {}\n\n for (const [key, value] of Object.entries(conditions)) {\n if (typeof value === 'string' && value.startsWith('${user.')) {\n const prop = value.slice(7, -1)\n const resolved = user[prop]\n if (resolved === undefined) {\n logger.warn({ property: prop, condition: key }, 'CASL condition interpolation: user property is undefined, condition may not work as expected')\n }\n result[key] = resolved\n } else {\n result[key] = value\n }\n }\n\n return result\n}\n\n/**\n * Entity permission definition from casl.permissions\n */\ninterface EntityPermission {\n actions: string[]\n conditions?: Record<string, unknown>\n fields?: string[]\n inverted?: boolean\n}\n\n/**\n * Permission value can be a single permission or array of permissions.\n * Array allows different configs per action (e.g., read without fields, update with fields).\n */\ntype PermissionValue = EntityPermission | EntityPermission[]\n\n// Entity definitions registry (set by engine on init)\nlet entityDefinitionsRegistry: Map<string, { subject: string; permissions: Record<string, PermissionValue> }> | null = null\n\n/**\n * Registers entity definitions for ability building.\n * Called by engine during initialization.\n */\nexport function setEntityDefinitions(\n definitions: Map<string, { subject: string; permissions: Record<string, PermissionValue> }>\n): void {\n entityDefinitionsRegistry = definitions\n}\n\n/**\n * Returns the number of entity definitions registered, or -1 if not initialized.\n * Useful for tests to verify that the registry was populated at boot.\n */\nexport function getEntityDefinitionsCount(): number {\n return entityDefinitionsRegistry ? entityDefinitionsRegistry.size : -1\n}\n\n/**\n * Clears entity definitions registry.\n * Useful for tests.\n */\nexport function clearEntityDefinitions(): void {\n entityDefinitionsRegistry = null\n}\n\n// Roles with implicit manage:all (superusers)\nconst SUPERUSER_ROLES = ['ADMIN', 'OWNER']\n\n/**\n * Builds CASL abilities from role names and entity definitions.\n * Permissions are static in code (entity.casl.permissions[roleName]).\n *\n * ADMIN and OWNER roles automatically receive 'manage:all' permission.\n *\n * @param user - Authenticated user (needs id for condition interpolation)\n * @param roleNames - User's role names (e.g., ['ADMIN', 'EDITOR'])\n */\nexport async function defineAbilityFor(user: MinimalUser, roleNames: string[]): Promise<AppAbility> {\n const { can, cannot, build } = new AbilityBuilder<MongoAbility<[Actions, Subjects]>>(createMongoAbility)\n\n // Cast to IndexableUser for condition interpolation (dynamic property access)\n const indexableUser = user as IndexableUser\n\n // 1. Check for superuser roles (ADMIN, OWNER) - implicit manage:all\n const isSuperuser = roleNames.some(role => SUPERUSER_ROLES.includes(role))\n if (isSuperuser) {\n can('manage', 'all')\n }\n\n // 2. Apply permissions from entity definitions (for non-superuser roles)\n if (entityDefinitionsRegistry && !isSuperuser) {\n for (const [, entityDef] of entityDefinitionsRegistry) {\n const subject = entityDef.subject as SubjectStrings\n\n // Check each role the user has (fallback to '*' wildcard if no specific role perms)\n for (const roleName of roleNames) {\n const rolePermsValue = entityDef.permissions[roleName] ?? entityDef.permissions['*']\n if (!rolePermsValue) continue\n\n // Normalize to array (supports single object or array of permissions)\n const permsList = Array.isArray(rolePermsValue) ? rolePermsValue : [rolePermsValue]\n\n for (const rolePerms of permsList) {\n const { actions, conditions: rawConditions, fields, inverted } = rolePerms\n const conditions = rawConditions\n ? interpolateConditions(rawConditions, indexableUser)\n : undefined\n\n const builder = inverted ? cannot : can\n for (const action of actions) {\n if (fields?.length) {\n builder(action as Actions, subject, fields, conditions)\n } else {\n builder(action as Actions, subject, conditions)\n }\n }\n }\n }\n }\n }\n\n // 3. Apply custom rules (if registered)\n if (customCaslRules) {\n await customCaslRules(user as unknown as Record<string, unknown>, { can, cannot })\n }\n\n // 4. Apply seed-defined permissions (from onSeed roles.add with permissions)\n if (seedPermissions && !isSuperuser) {\n for (const roleName of roleNames) {\n const rolePerms = seedPermissions.get(roleName)\n if (!rolePerms) continue\n for (const [subject, actions] of rolePerms) {\n for (const action of actions) {\n can(action as Actions, subject as SubjectStrings)\n }\n }\n }\n }\n\n return build()\n}\n\n// Serialize abilities for sending to frontend\nexport function packRules(ability: AppAbility): RawRuleOf<AppAbility>[] {\n return ability.rules as RawRuleOf<AppAbility>[]\n}\n\n// Rebuild abilities from serialized rules\nexport function unpackRules(rules: RawRuleOf<AppAbility>[]): AppAbility {\n return createMongoAbility<[Actions, Subjects]>(rules)\n}\n","/**\n * @module core/jwt\n * @description JWT verification utilities for core/ (isolated from modules/)\n *\n * Uses AUTH_SECRET from env directly to avoid modules/ dependency.\n * For full JWT operations (generate tokens), use modules/auth/jwt.utils.js\n */\n\nimport jwt from 'jsonwebtoken'\n\n/** JWT payload shape (mirrors auth module) */\nexport interface JwtPayload {\n userId: string\n email: string\n roleIds: string[]\n roleNames: string[]\n}\n\n/**\n * Get JWT secret from env\n */\nfunction getSecret(): string {\n const secret = process.env['AUTH_SECRET']\n if (!secret) {\n throw new Error('AUTH_SECRET not configured. Set AUTH_SECRET env var.')\n }\n return secret\n}\n\n/**\n * Verify access token and return payload\n * @throws Error if token is invalid or expired\n */\nexport function verifyAccessToken(token: string): JwtPayload {\n return jwt.verify(token, getSecret()) as JwtPayload\n}\n","/**\n * Module routing setup - Mount module routes on Express app.\n *\n * Extracted from app.ts to simplify the main app setup.\n */\n\nimport type { Express } from 'express'\nimport type { ModuleContext, EntityCaslConfig, ActionDefinition } from '@gzl10/nexus-sdk'\nimport type { RegisteredModule } from '../engine/module-store.js'\nimport { getOrderedModulesInternal } from '../engine/index.js'\nimport { createModuleRouters, createActionRouters } from '../runtime/index.js'\nimport { createModuleContext } from '../engine/context.js'\nimport { setEntityDefinitions } from './abilities/ability.factory.js'\n\n/** Entity types that should always be auto-mounted regardless of custom routes */\nconst ALWAYS_MOUNT_TYPES = ['view', 'computed', 'virtual', 'external', 'reference', 'event', 'tree', 'dag', 'temp']\n\n/**\n * Creates a module-scoped context with plugin-aware table name resolver.\n * Core modules get the original context unchanged.\n */\nfunction createScopedContext(ctx: ModuleContext, mod: RegisteredModule): ModuleContext {\n if (!mod._tablePrefix || !mod._pluginTables) return ctx\n\n const prefix = mod._tablePrefix\n const pluginTables = mod._pluginTables\n\n return {\n ...ctx,\n db: {\n ...ctx.db,\n t: (name: string) => pluginTables.has(name) ? `${prefix}${name}` : name\n }\n }\n}\n\n/**\n * Setup module routes on Express app.\n *\n * 1. Initializes all modules (registers middlewares, services)\n * 2. Mounts custom routes (if module.routes defined)\n * 3. Mounts auto-generated entity routes\n *\n * @returns The module context used for initialization\n */\nexport async function setupModuleRoutes(app: Express): Promise<ModuleContext> {\n const ctx = createModuleContext()\n const modules = getOrderedModulesInternal()\n\n // 1. Initialize modules (register middlewares, services, etc.)\n for (const mod of modules) {\n if (mod.init) {\n mod.init(createScopedContext(ctx, mod))\n }\n }\n\n // 1b. Build CASL entity definitions registry from all loaded modules.\n // This connects casl.permissions from entity/action definitions to the ability factory,\n // enabling non-superuser roles to have granular permissions.\n type CaslRegistryEntry = Parameters<typeof setEntityDefinitions>[0] extends Map<string, infer V> ? V : never\n const caslRegistry = new Map<string, CaslRegistryEntry>()\n\n /** Merge action CASL permissions into an existing registry entry (append, don't replace) */\n function mergeActionPermissions(\n existing: CaslRegistryEntry['permissions'],\n incoming: Record<string, unknown>\n ): CaslRegistryEntry['permissions'] {\n const merged = { ...existing } as Record<string, unknown>\n for (const [role, perm] of Object.entries(incoming)) {\n if (!merged[role]) {\n merged[role] = perm\n } else {\n // Combine into array (both existing and incoming can be single or array)\n const existingArr = Array.isArray(merged[role]) ? merged[role] : [merged[role]]\n const incomingArr = Array.isArray(perm) ? perm : [perm]\n merged[role] = [...existingArr, ...incomingArr]\n }\n }\n return merged as CaslRegistryEntry['permissions']\n }\n\n /** Register CASL permissions from an action into the registry */\n function registerActionCasl(action: ActionDefinition): void {\n const casl = action.casl\n if (!casl || !('subject' in casl) || !casl.subject || !('permissions' in casl) || !casl.permissions) return\n const subject = casl.subject\n const existing = caslRegistry.get(subject)\n if (existing) {\n existing.permissions = mergeActionPermissions(existing.permissions, casl.permissions)\n } else {\n caslRegistry.set(subject, {\n subject,\n permissions: (casl.permissions ?? {}) as CaslRegistryEntry['permissions']\n })\n }\n }\n\n for (const mod of modules) {\n // Entity definitions\n for (const def of mod.definitions ?? []) {\n const casl = (def as { casl?: EntityCaslConfig }).casl\n if (!casl?.subject) continue\n const key = ('table' in def && (def as { table?: string }).table)\n ? (def as { table: string }).table\n : casl.subject\n caslRegistry.set(key, {\n subject: casl.subject,\n permissions: (casl.permissions ?? {}) as CaslRegistryEntry['permissions']\n })\n\n // Entity-scope actions (def.actions[])\n for (const action of (def as { actions?: ActionDefinition[] }).actions ?? []) {\n registerActionCasl(action)\n }\n }\n\n // Module-scope actions (manifest.actions[])\n for (const action of mod.actions ?? []) {\n registerActionCasl(action)\n }\n }\n setEntityDefinitions(caslRegistry)\n\n // 1c. Merge cross-module action injections.\n // Modules can declare injectActions to add actions to entities in other modules.\n // This runs after all modules are registered (targets known) and before route mounting.\n const moduleIndex = new Map(modules.map(m => [m.name, m]))\n for (const mod of modules) {\n if (!mod.injectActions?.length) continue\n for (const injection of mod.injectActions) {\n const targetMod = moduleIndex.get(injection.target.module)\n if (!targetMod) {\n ctx.core.logger.warn(`injectActions: target module '${injection.target.module}' not found (source: ${mod.name})`)\n continue\n }\n const targetDef = targetMod.definitions?.find(\n d => 'table' in d && (d as { table: string }).table === injection.target.entity\n ) as { actions?: ActionDefinition[] } | undefined\n if (!targetDef) {\n ctx.core.logger.warn(`injectActions: entity '${injection.target.entity}' not found in module '${injection.target.module}' (source: ${mod.name})`)\n continue\n }\n if (!targetDef.actions) targetDef.actions = []\n if (!targetDef.actions.some(a => a.key === injection.action.key)) {\n targetDef.actions.push(injection.action)\n }\n }\n }\n\n // 2. Register routes (middlewares are now available)\n for (const mod of modules) {\n // Skip route mounting if endpoints are disabled (services still available via init)\n if (mod._disableEndpoints) {\n continue\n }\n\n const routePrefix = mod.routePrefix ?? `/${mod.name}`\n const scopedCtx = createScopedContext(ctx, mod)\n\n // 2a. Custom routes first (have priority, handle special logic)\n if (mod.routes) {\n app.use(`/api/v1${routePrefix}`, mod.routes(scopedCtx))\n }\n\n // 2b. Standalone actions mount before entity routes to avoid /:id catch-all conflicts\n if (mod.actions && mod.actions.length > 0) {\n const actionRouter = createActionRouters(scopedCtx, mod.actions)\n app.use(`/api/v1${routePrefix}`, actionRouter)\n }\n\n // 2c. Auto-generated routes for entities\n if (mod.definitions && mod.definitions.length > 0) {\n // Actions, views, etc. always mount (even if custom routes exist)\n const alwaysMountable = mod.definitions.filter(d =>\n ALWAYS_MOUNT_TYPES.includes(d.type ?? '')\n )\n if (alwaysMountable.length > 0) {\n const actionRouter = await createModuleRouters(scopedCtx, alwaysMountable, routePrefix)\n app.use(`/api/v1${routePrefix}`, actionRouter)\n }\n\n // Collections and singles auto-mount (custom routes have priority for same paths)\n const autoMountable = mod.definitions.filter(d => {\n const type = d.type ?? 'collection'\n return type === 'collection' || type === 'single'\n })\n if (autoMountable.length > 0) {\n const entityRouter = await createModuleRouters(scopedCtx, autoMountable, routePrefix)\n app.use(`/api/v1${routePrefix}`, entityRouter)\n }\n }\n }\n\n return ctx\n}\n","import type { Request, Response, NextFunction } from 'express'\nimport { ZodError } from 'zod'\nimport { ForbiddenError as CASLForbiddenError } from '@casl/ability'\nimport { AppError, ValidationError, ErrorCodes, type ValidationDetail } from '../errors/app-error.js'\nimport { env } from '../../config/env.js'\nimport { logger } from '../logger/index.js'\n\n/** Exception capture function signature (injected to avoid modules/ dependency) */\nexport type CaptureExceptionFn = (error: Error, context?: Record<string, unknown>) => void\n\n/** Default no-op capture function */\nconst noopCapture: CaptureExceptionFn = () => {}\n\n/** Injected capture function (set via initErrorMiddleware) */\nlet captureException: CaptureExceptionFn = noopCapture\n\n/**\n * Initialize error middleware with exception capture function\n * Called from server.ts after logger module is initialized\n */\nexport function initErrorMiddleware(options?: { captureException?: CaptureExceptionFn }): void {\n captureException = options?.captureException ?? noopCapture\n}\n\n/**\n * Standard API error response with i18n support.\n * Frontend uses 'code' to translate the error message.\n */\ninterface ApiErrorResponse {\n error: {\n /** Error code for i18n translation */\n code: string\n /** Fallback message in English */\n message: string\n /** Interpolation values for the translated message */\n interpolation?: Record<string, string | number>\n /** Request ID for correlation/debugging */\n requestId?: string\n }\n /** Validation details (field-level errors) */\n details?: ValidationDetail[]\n}\n\n/**\n * Creates a standard error response body.\n * Use in route handlers: `res.status(404).json(createErrorResponse('NOT_FOUND', 'message'))`\n */\nexport function createErrorResponse(\n code: string,\n message: string,\n interpolation?: Record<string, string | number>,\n details?: ValidationDetail[],\n requestId?: string\n): ApiErrorResponse {\n const response: ApiErrorResponse = {\n error: { code, message }\n }\n if (interpolation) {\n response.error.interpolation = interpolation\n }\n if (requestId) {\n response.error.requestId = requestId\n }\n if (details && details.length > 0) {\n response.details = details\n }\n return response\n}\n\nexport function errorMiddleware(\n err: Error,\n req: Request,\n res: Response,\n _next: NextFunction\n) {\n const requestId = req.requestId\n\n // Log según tipo de error (4xx = warn sin stack, 5xx = error con stack)\n if (err instanceof AppError && err.statusCode < 500) {\n logger.warn({ code: err.code, status: err.statusCode, url: req.url, method: req.method, ip: req.ip, requestId }, err.message)\n } else if (!(err instanceof ZodError) && !(err instanceof CASLForbiddenError)) {\n // ZodError y CASLForbiddenError se manejan abajo, no duplicar log\n logger.error({ err, url: req.url, method: req.method, requestId }, 'Server error')\n }\n\n // JSON parse errors (malformed JSON body)\n if (err instanceof SyntaxError && 'body' in err) {\n logger.debug({ err, url: req.url, method: req.method }, 'Invalid JSON body')\n return res.status(400).json(\n createErrorResponse(ErrorCodes.VALIDATION_JSON_MALFORMED, `Malformed JSON: ${err.message}`, undefined, undefined, requestId)\n )\n }\n\n // Payload too large (from express.json limit)\n if (err.name === 'PayloadTooLargeError' || (err as { type?: string }).type === 'entity.too.large') {\n logger.debug({ err, url: req.url, method: req.method }, 'Payload too large')\n return res.status(413).json(\n createErrorResponse(ErrorCodes.STORAGE_PAYLOAD_TOO_LARGE, 'Payload too large: Request body exceeds the allowed limit (1MB)', undefined, undefined, requestId)\n )\n }\n\n // Multer file size limit exceeded\n if (err.name === 'MulterError' && (err as { code?: string }).code === 'LIMIT_FILE_SIZE') {\n logger.debug({ err, url: req.url, method: req.method }, 'File too large')\n return res.status(413).json(\n createErrorResponse(ErrorCodes.STORAGE_FILE_TOO_LARGE, 'File too large: Uploaded file exceeds the allowed limit', undefined, undefined, requestId)\n )\n }\n\n // Other Multer errors (file filter rejection, etc.)\n if (err.name === 'MulterError' || err.message?.includes('Invalid filename')) {\n logger.debug({ err, url: req.url, method: req.method }, 'Upload rejected')\n return res.status(400).json(\n createErrorResponse(ErrorCodes.STORAGE_FILE_TYPE_NOT_ALLOWED, `Invalid file: ${err.message}`, undefined, undefined, requestId)\n )\n }\n\n // Zod validation errors\n if (err instanceof ZodError) {\n const details: ValidationDetail[] = err.errors.map((e) => ({\n path: e.path.join('.'),\n message: e.message,\n code: mapZodCodeToErrorCode(e.code),\n interpolation: extractZodInterpolation(e)\n }))\n return res.status(400).json(\n createErrorResponse(ErrorCodes.VALIDATION_ERROR, 'Validation error', undefined, details, requestId)\n )\n }\n\n // CASL forbidden\n if (err instanceof CASLForbiddenError) {\n return res.status(403).json(\n createErrorResponse(ErrorCodes.PERMISSION_DENIED, 'Permission denied', undefined, undefined, requestId)\n )\n }\n\n // ValidationError (typed with details: ValidationDetail[])\n if (err instanceof ValidationError) {\n return res.status(400).json(\n createErrorResponse(err.code, err.message, err.interpolation, err.details, requestId)\n )\n }\n\n // Custom app errors\n if (err instanceof AppError) {\n // Capture server errors (5xx) in Sentry\n if (err.statusCode >= 500) {\n captureException(err, { url: req.url, method: req.method, code: err.code, requestId })\n }\n\n // If details is a string, include it in the error message\n if (typeof err.details === 'string') {\n return res.status(err.statusCode).json(\n createErrorResponse(err.code, `${err.message}: ${err.details}`, err.interpolation, undefined, requestId)\n )\n }\n\n // If details is an object with specific properties (OTP challenge, etc.)\n if (err.details && typeof err.details === 'object') {\n const response = createErrorResponse(err.code, err.message, err.interpolation, undefined, requestId)\n // Merge details into response for backward compatibility\n return res.status(err.statusCode).json({ ...response, ...err.details })\n }\n\n return res.status(err.statusCode).json(\n createErrorResponse(err.code, err.message, err.interpolation, undefined, requestId)\n )\n }\n\n // Unknown errors\n // TODO: error metrics (TASK-22 Sprint 3.5)\n logger.error({ err, url: req.url, method: req.method, stack: err.stack, requestId }, 'Unhandled error')\n captureException(err, { url: req.url, method: req.method, requestId })\n res.status(500).json(\n createErrorResponse(\n ErrorCodes.SYSTEM_INTERNAL_ERROR,\n env.NODE_ENV === 'production' ? 'Internal server error' : err.message,\n undefined,\n undefined,\n requestId\n )\n )\n}\n\n/**\n * Maps Zod error codes to our error codes\n */\nfunction mapZodCodeToErrorCode(zodCode: string): string {\n switch (zodCode) {\n case 'invalid_type':\n case 'invalid_string':\n case 'invalid_enum_value':\n return 'VALIDATION_FIELD_INVALID'\n case 'too_small':\n case 'too_big':\n return 'VALIDATION_FIELD_INVALID'\n default:\n return 'VALIDATION_FIELD_INVALID'\n }\n}\n\n/**\n * Extracts interpolation values from Zod errors (min, max, etc.)\n */\nfunction extractZodInterpolation(error: ZodError['errors'][number]): Record<string, string | number> | undefined {\n const interpolation: Record<string, string | number> = {}\n\n if ('minimum' in error) {\n interpolation['min'] = error.minimum as number\n }\n if ('maximum' in error) {\n interpolation['max'] = error.maximum as number\n }\n\n return Object.keys(interpolation).length > 0 ? interpolation : undefined\n}\n","import { randomUUID } from 'crypto'\nimport type { Request, Response, NextFunction } from 'express'\n\n/**\n * Assigns a unique request ID and correlation ID to each request.\n * - X-Request-Id: always a new UUID (or pass-through from load balancer)\n * - X-Correlation-ID: propagated from upstream or falls back to request ID\n */\nexport function requestIdMiddleware(req: Request, res: Response, next: NextFunction): void {\n const requestId = (req.get('X-Request-Id') as string) || randomUUID()\n const correlationId = req.get('X-Correlation-ID') || requestId\n\n res.setHeader('X-Request-ID', requestId)\n res.setHeader('X-Correlation-ID', correlationId)\n\n req.requestId = requestId\n req.correlationId = correlationId\n\n next()\n}\n","import type { RequestHandler } from 'express'\nimport { createErrorResponse } from './error.middleware.js'\n\n/**\n * Header name used to identify nexus-client requests.\n * This provides a lightweight security layer for internal endpoints.\n */\nexport const NEXUS_CLIENT_HEADER = 'X-Nexus-Client'\n\n/**\n * Middleware that requires the X-Nexus-Client header.\n * Used to protect internal endpoints that should only be accessed by nexus-client.\n *\n * Without the header, returns 404 (not 401/403) to avoid revealing endpoint existence.\n *\n * @example\n * app.get('/api/internal', requireNexusClient, handler)\n */\nexport const requireNexusClient: RequestHandler = (req, res, next) => {\n const nexusClient = req.get(NEXUS_CLIENT_HEADER)\n\n if (!nexusClient) {\n return res.status(404).json(\n createErrorResponse('NOT_FOUND', 'API endpoint not found')\n )\n }\n\n next()\n}\n\n/**\n * Creates a middleware that optionally requires X-Nexus-Client header.\n * Useful when the requirement depends on environment or config.\n *\n * @param required - Whether to require the header (default: true)\n */\nexport function createNexusClientMiddleware(required: boolean = true): RequestHandler {\n if (!required) {\n return (_req, _res, next) => next()\n }\n return requireNexusClient\n}\n","import type { Express } from 'express'\nimport type http from 'node:http'\nimport express from 'express'\nimport { resolve, join } from 'path'\nimport { existsSync, readFileSync } from 'fs'\nimport { getProjectPath, getLibPath } from '../config/paths.js'\nimport { logger } from './logger/index.js'\nimport { env } from '../config/env.js'\nimport type { ServeSPAOptions } from '../config/types.js'\n\nconst registeredEndpoints = new Set<string>()\n\n/** Vite dev server instances for cleanup on stop() */\nconst viteServers: Array<{ close: () => Promise<void> }> = []\n\n/**\n * Creates a serveSPA helper function bound to an Express app.\n *\n * In development with `viteSrc`, mounts Vite as Express middleware (HMR, on-demand compile).\n * In production (or without viteSrc), serves static files from dist/ as before.\n */\nexport function createServeSPA(app: Express, httpServer?: http.Server) {\n return async (\n endpoint: string,\n distPath: string,\n options: ServeSPAOptions & { viteSrc?: string } = {}\n ): Promise<void> => {\n const {\n maxAge = '1d',\n etag = true,\n immutable = false,\n index = 'index.html',\n absolute = false,\n viteSrc\n } = options\n\n // Prevent mounting SPA on /api (would capture API routes)\n if (endpoint === '/api' || endpoint.startsWith('/api/')) {\n logger.error(`Cannot mount SPA on ${endpoint} - reserved for API routes`)\n return\n }\n\n // Prevent duplicate endpoints\n if (registeredEndpoints.has(endpoint)) {\n logger.warn(`SPA endpoint ${endpoint} already registered — skipping duplicate`)\n return\n }\n registeredEndpoints.add(endpoint)\n\n // DEV + viteSrc → mount Vite middleware\n if (env.NODE_ENV === 'development' && viteSrc) {\n const srcPath = resolve(getProjectPath(), viteSrc)\n if (!existsSync(srcPath)) {\n logger.warn({ endpoint, viteSrc, resolved: srcPath }, 'Vite source not found — falling back to static')\n } else {\n const mounted = await mountViteDevMiddleware(app, endpoint, srcPath, httpServer)\n if (mounted) return\n }\n }\n\n // PROD (or fallback) → static files\n mountStaticSPA(app, endpoint, distPath, { maxAge, etag, immutable, index, absolute })\n }\n}\n\n/**\n * Mounts Vite dev server as Express middleware.\n * Returns true if successful, false if vite is not available.\n */\nasync function mountViteDevMiddleware(\n app: Express,\n endpoint: string,\n srcPath: string,\n httpServer?: http.Server\n): Promise<boolean> {\n try {\n const vite = await import('vite')\n\n const apiUrl = env.BACKEND_URL ? `${env.BACKEND_URL}/api/v1` : '/api/v1'\n\n const server = await vite.createServer({\n root: srcPath,\n server: {\n middlewareMode: true,\n allowedHosts: true,\n hmr: httpServer ? { server: httpServer } : true,\n },\n plugins: [{\n name: 'nexus-config-inject',\n transformIndexHtml(html: string) {\n const config = JSON.stringify({ apiUrl })\n return html.replace('</head>', `<script>window.__NEXUS__=${config}</script>\\n</head>`)\n }\n }],\n appType: 'spa',\n clearScreen: false\n })\n\n viteServers.push(server)\n\n if (endpoint === '/') {\n app.use(server.middlewares)\n } else {\n app.use(endpoint, server.middlewares)\n }\n\n logger.info({ path: srcPath }, `Vite dev server mounted at ${endpoint} (HMR enabled)`)\n return true\n } catch (err: any) {\n if (err.code === 'ERR_MODULE_NOT_FOUND' || err.code === 'MODULE_NOT_FOUND') {\n logger.warn(`vite not installed — falling back to static serving for ${endpoint}`)\n return false\n }\n logger.error({ err }, `Failed to mount Vite dev server at ${endpoint}`)\n return false\n }\n}\n\n/**\n * Mounts static SPA files with config injection (existing behavior).\n */\nfunction mountStaticSPA(\n app: Express,\n endpoint: string,\n distPath: string,\n options: { maxAge: string | number; etag: boolean; immutable: boolean; index: string; absolute: boolean }\n): void {\n const { maxAge, etag, immutable, index, absolute } = options\n\n let resolvedPath: string\n if (absolute) {\n resolvedPath = distPath\n } else {\n const projectPath = resolve(getProjectPath(), distPath)\n if (existsSync(projectPath)) {\n resolvedPath = projectPath\n } else {\n resolvedPath = resolve(getLibPath(), distPath)\n }\n }\n\n if (!existsSync(resolvedPath)) {\n logger.warn({ endpoint, distPath, hint: 'Build the frontend first' }, `SPA directory not found: ${resolvedPath}`)\n return\n }\n\n const indexPath = join(resolvedPath, index)\n if (!existsSync(indexPath)) {\n logger.warn({ endpoint, index }, `Index file not found: ${indexPath}`)\n }\n\n app.use(endpoint, express.static(resolvedPath, { maxAge, etag, immutable }))\n\n let injectedHtml = ''\n if (existsSync(indexPath)) {\n const rawHtml = readFileSync(indexPath, 'utf-8')\n const apiUrl = env.BACKEND_URL ? `${env.BACKEND_URL}/api/v1` : '/api/v1'\n const nexusConfig = JSON.stringify({ apiUrl })\n injectedHtml = rawHtml.replace(\n '</head>',\n `<script>window.__NEXUS__=${nexusConfig}</script>\\n</head>`\n )\n }\n\n const fallbackHandler = (_req: express.Request, res: express.Response) => {\n if (!injectedHtml) {\n res.status(404).send('index.html not found')\n return\n }\n res.set('Cache-Control', 'no-cache, no-store, must-revalidate')\n res.type('html').send(injectedHtml)\n }\n\n // Match exact endpoint and all subpaths\n // Note: For root endpoint '/', use '{*splat}' directly (not '/{*splat}')\n if (endpoint === '/') {\n app.get('{*splat}', fallbackHandler)\n } else {\n app.get(endpoint, fallbackHandler)\n app.get(`${endpoint}/{*splat}`, fallbackHandler)\n }\n\n logger.info({ path: resolvedPath }, `SPA mounted at ${endpoint}`)\n}\n\n/**\n * Closes all Vite dev servers and resets registered endpoints.\n * Single cleanup function — replaces separate resetServeSPAEndpoints + closeViteServers.\n */\nexport async function resetServeSPA(): Promise<void> {\n for (const server of viteServers) {\n try {\n await server.close()\n } catch {\n // Ignore close errors\n }\n }\n viteServers.length = 0\n registeredEndpoints.clear()\n}\n\n/**\n * @deprecated Use `resetServeSPA()` instead. Kept for backward compatibility.\n */\nexport function resetServeSPAEndpoints(): void {\n registeredEndpoints.clear()\n}\n","import type { SpaEntry } from '../../config/types.js'\n\n/**\n * Builds the effective CORS origins by merging the env CORS_ORIGIN\n * with the `origin` values declared in `spas` entries.\n *\n * Returns a string if there is only one unique origin (backward-compatible\n * with the cors middleware), or an array if there are multiple.\n */\nexport function buildEffectiveCorsOrigins(\n envOrigin: string,\n spas?: SpaEntry[]\n): string | string[] {\n const origins = new Set<string>()\n\n // Parse env CORS_ORIGIN (supports comma-separated values)\n for (const o of envOrigin.split(',')) {\n const trimmed = o.trim()\n if (trimmed) origins.add(trimmed)\n }\n\n // Add origins from SPA entries\n for (const spa of spas ?? []) {\n if (spa.origin?.trim()) origins.add(spa.origin.trim())\n }\n\n const arr = [...origins]\n return arr.length === 1 ? arr[0]! : arr\n}\n","import express from 'express'\nimport cors from 'cors'\nimport helmet from 'helmet'\nimport compression from 'compression'\nimport cookieParser from 'cookie-parser'\nimport pkg from '../../package.json' with { type: 'json' }\nimport path from 'path'\n\nimport { generateOpenAPISpec, type OpenAPIFilterOptions } from './openapi/index.js'\nimport { defineAbilityFor } from './abilities/ability.factory.js'\nimport { verifyAccessToken } from './jwt/index.js'\nimport { setupModuleRoutes } from './module-routes.js'\nimport { errorMiddleware, createErrorResponse } from './middleware/error.middleware.js'\nimport { requestIdMiddleware } from './middleware/request-id.middleware.js'\nimport { requireNexusClient } from './middleware/nexus-client.middleware.js'\nimport { env, getConfig } from '../config/env.js'\nimport { logger } from './logger/index.js'\nimport { getProjectPath } from '../config/paths.js'\nimport { createServeSPA } from './spa-handler.js'\nimport { buildEffectiveCorsOrigins } from './utils/cors.js'\nimport type { ServeSPAFunction, SpaEntry } from '../config/types.js'\nimport type { Express } from 'express'\n\n/** Express Router factory (exposed so engine/ doesn't import express directly) */\nexport const createRouter = () => express.Router()\n\nexport interface CreateAppOptions {\n beforeRoutes?: (app: Express, serveSPA: ServeSPAFunction) => void | Promise<void>\n afterRoutes?: (app: Express, serveSPA: ServeSPAFunction) => void | Promise<void>\n /** Declarative SPA entries (served + CORS). Passed from StartOptions.spas. */\n spas?: SpaEntry[]\n /** HTTP server for Vite HMR WebSocket (shared with Socket.IO) */\n httpServer?: import('node:http').Server\n}\n\nexport async function createApp(options: CreateAppOptions = {}) {\n const app = express()\n\n // Trust proxy (required behind reverse proxy for rate-limit, secure cookies, etc.)\n if (env.TRUST_PROXY) {\n app.set('trust proxy', true)\n }\n\n // Security\n app.use(helmet({\n contentSecurityPolicy: env.NODE_ENV === 'production' ? undefined : false\n }))\n\n // CORS — merge env CORS_ORIGIN with any origins declared in spas[]\n const effectiveCorsOrigins = buildEffectiveCorsOrigins(env.CORS_ORIGIN, options.spas)\n app.use(cors({\n origin: effectiveCorsOrigins,\n credentials: true,\n allowedHeaders: ['Content-Type', 'Authorization', 'X-Correlation-ID', 'X-Request-ID', 'X-Nexus-Client'],\n exposedHeaders: ['X-Correlation-ID', 'X-Request-ID']\n }))\n\n // Request ID y Correlation ID (siempre activo)\n app.use(requestIdMiddleware)\n\n // HTTP logging (API=debug, rest=trace)\n // Uses req/res object bindings so OTel PinoInstrumentation can inject trace_id/span_id\n const logLevel = process.env['LOG_LEVEL']\n if (logLevel === 'debug' || logLevel === 'trace') {\n app.use((req, res, next) => {\n const start = Date.now()\n res.on('finish', () => {\n const ms = Date.now() - start\n const logData = {\n reqId: req.correlationId?.slice(0, 8) ?? 'unknown',\n method: req.method,\n url: req.originalUrl,\n status: res.statusCode,\n ms\n }\n if (req.originalUrl.startsWith('/api/')) {\n logger.debug(logData, `${req.method} ${req.originalUrl} ${res.statusCode} ${ms}ms`)\n } else {\n logger.trace(logData, `${req.method} ${req.originalUrl} ${res.statusCode} ${ms}ms`)\n }\n })\n next()\n })\n }\n\n // Compression\n app.use(compression())\n\n // Body parsing with security limits\n app.use(express.json({\n limit: '1mb', // Prevent DOS with large payloads (returns 413 if exceeded)\n strict: true // Only accept arrays and objects\n }))\n app.use(cookieParser())\n\n // Create serveSPA helper\n const serveSPA = createServeSPA(app, options.httpServer)\n\n // beforeRoutes hook (rutas custom antes de módulos)\n if (options.beforeRoutes) {\n const result = options.beforeRoutes(app, serveSPA)\n if (result instanceof Promise) {\n logger.warn('beforeRoutes is async - consider using sync for predictable middleware order')\n }\n }\n\n // API routes (auto-discovery from modules)\n await setupModuleRoutes(app)\n\n // Liveness probe (K8s) - always responds 200 if process is alive\n app.get('/api/health', (_req, res) => {\n res.json({\n status: 'ok',\n version: pkg.version,\n timestamp: new Date().toISOString()\n })\n })\n\n // Readiness probe (K8s) - checks dependencies (DB, registered health checks)\n app.get('/api/ready', async (_req, res) => {\n try {\n const { getHealthRegistry } = await import('../modules/observability/observability.service.js')\n const registry = getHealthRegistry()\n const result = await registry.checkAll()\n const httpStatus = result.status === 'ok' ? 200 : 503\n res.status(httpStatus).json(result)\n } catch {\n // Registry not initialized yet (modules still loading)\n res.status(503).json({ status: 'error', checks: {}, timestamp: new Date().toISOString() })\n }\n })\n\n // OpenAPI spec endpoint\n // - Requires X-Nexus-Client header (for nexus-client autodiscover)\n // - Uses Bearer token to filter by user's actual permissions\n // - In development: ?role= param available for debugging (mock user)\n const openApiConfig = {\n title: 'Nexus API',\n version: process.env['npm_package_version'] || '0.0.0',\n description: 'API generada automáticamente desde EntityDefinitions'\n }\n\n app.get('/api/openapi.json', requireNexusClient, async (req, res) => {\n let filterOptions: OpenAPIFilterOptions | undefined\n const isDev = process.env['NODE_ENV'] !== 'production'\n\n // 1. Try to get roleNames from Bearer token\n const authHeader = req.headers.authorization\n if (authHeader?.startsWith('Bearer ')) {\n try {\n const token = authHeader.slice(7)\n const payload = verifyAccessToken(token)\n // Use roleNames from JWT to build ability\n const mockUser = { id: payload.userId }\n const ability = await defineAbilityFor(mockUser, payload.roleNames)\n filterOptions = {\n ability,\n cacheKey: `user:${payload.userId}`\n }\n } catch {\n // Invalid token - continue without filtering (dev) or return 401 (prod)\n if (!isDev) {\n return res.status(401).json(\n createErrorResponse('AUTH_UNAUTHORIZED', 'Invalid or expired token')\n )\n }\n }\n }\n\n // 2. Development only: ?role= param for debugging (mock user)\n if (isDev && !filterOptions) {\n const role = req.query['role'] as string | undefined\n if (role) {\n const mockUser = { id: 'openapi-mock-user' }\n const ability = await defineAbilityFor(mockUser, [role.toUpperCase()])\n filterOptions = {\n ability,\n cacheKey: `role:${role.toUpperCase()}`\n }\n }\n }\n\n // 3. Production without token: return 401\n if (!isDev && !filterOptions) {\n return res.status(401).json(\n createErrorResponse('AUTH_UNAUTHORIZED', 'Authentication required')\n )\n }\n\n const spec = generateOpenAPISpec(openApiConfig, filterOptions)\n res.json(spec)\n })\n\n // Serve static files from public/\n const publicPath = path.join(getProjectPath(), 'public')\n app.use('/public', express.static(publicPath))\n\n // afterRoutes hook (rutas custom después de módulos)\n if (options.afterRoutes) {\n const result = options.afterRoutes(app, serveSPA)\n if (result instanceof Promise) {\n logger.warn('afterRoutes is async - consider using sync for predictable middleware order')\n }\n }\n\n // API 404 handler - must be BEFORE SPA fallback\n // Returns JSON error for unmatched /api/* routes instead of SPA HTML\n app.use('/api', (_req, res) => {\n res.status(404).json(\n createErrorResponse('NOT_FOUND', 'API endpoint not found')\n )\n })\n\n // Serve declarative SPAs (sorted by specificity: longest endpoint first to avoid shadowing)\n const servedSpas = (options.spas ?? []).filter(spa => {\n if (!spa.endpoint && !spa.origin) {\n logger.warn({ spa }, 'SpaEntry ignored: must have endpoint+path or origin')\n return false\n }\n return spa.endpoint && spa.path\n })\n const sortedSpas = [...servedSpas].sort((a, b) => b.endpoint!.length - a.endpoint!.length)\n for (const spa of sortedSpas) {\n await serveSPA(spa.endpoint!, spa.path!, { ...spa, viteSrc: spa.viteSrc })\n }\n\n // Serve built-in UI SPA (Vite HMR in dev, static files in prod)\n const { ui } = getConfig()\n if (ui.enabled) {\n await serveSPA(ui.base, ui.path, { viteSrc: '../ui' })\n }\n\n // Global error handler (Express 5 async support)\n app.use(errorMiddleware)\n\n return app\n}\n","import pkg from 'eventemitter2'\nconst { EventEmitter2 } = pkg\n\nimport type { EntityChangePayload } from '@gzl10/nexus-sdk'\n\n// Payload genérico para eventos CRUD de DB\nexport interface DbEventPayload {\n table: string\n action: 'created' | 'updated' | 'deleted'\n data: unknown\n timestamp: Date\n}\n\n// Tipos de eventos\nexport interface NexusEvents {\n // Lifecycle\n 'server.starting': { port: number; host: string }\n 'server.started': { port: number; host: string }\n 'server.stopping': undefined\n 'server.stopped': undefined\n 'server.restarting': undefined\n\n // Database connection\n 'db.connected': { type: 'sqlite' | 'postgresql' | 'mysql' }\n 'db.disconnected': undefined\n\n // Database CRUD (automático via interceptor)\n // Uso: nexusEvents.on('db.users.created', ...) o nexusEvents.on('db.*.*', ...)\n [key: `db.${string}.created`]: DbEventPayload\n [key: `db.${string}.updated`]: DbEventPayload\n [key: `db.${string}.deleted`]: DbEventPayload\n\n // Auth (lógica de negocio, manual)\n 'auth.login': { userId: string; email: string }\n 'auth.logout': { userId: string }\n 'auth.refresh': { userId: string }\n 'auth.failed': { email: string; reason: string }\n\n // Socket.IO\n 'socket.initialized': undefined\n 'socket.user.connected': { userId: string; roleIds: string[]; socketId: string }\n 'socket.user.disconnected': { userId: string; socketId: string }\n\n // Notifications\n 'notifications.sent': { id: string; target_type: string; target_value?: string; sent: number }\n\n // Entity real-time (business-level CRUD, emitted by services)\n 'entity.created': EntityChangePayload\n 'entity.updated': EntityChangePayload\n 'entity.deleted': EntityChangePayload\n\n // Chat (for plugin hooks - e.g. AI bot auto-responses)\n 'chat.message.created': { channelId: string; messageId: string; userId: string; body: string }\n 'chat.message.deleted': { channelId: string; messageId: string; userId: string }\n 'chat.channel.created': { channelId: string; type: 'dm' | 'group'; createdBy: string }\n 'chat.member.joined': { channelId: string; userId: string; role: string }\n 'chat.member.left': { channelId: string; userId: string }\n\n // Audit (consumed by audit module if loaded, silently dropped otherwise)\n 'audit.log': {\n source: string\n action: string\n actorId?: string\n actorEmail?: string\n resourceType?: string\n resourceId?: string\n ip?: string\n userAgent?: string\n metadata?: Record<string, unknown>\n }\n}\n\n// Tipo helper para listeners tipados\nexport type NexusEventName = keyof NexusEvents\nexport type NexusEventPayload<T extends NexusEventName> = NexusEvents[T]\n\n// Emitter singleton\nclass TypedEventEmitter extends EventEmitter2 {\n emitEvent<T extends NexusEventName>(\n event: T,\n ...args: NexusEvents[T] extends undefined ? [] : [NexusEvents[T]]\n ): boolean {\n return this.emit(event, ...args)\n }\n\n onEvent<T extends NexusEventName>(\n event: T,\n listener: NexusEvents[T] extends undefined\n ? () => void\n : (payload: NexusEvents[T]) => void\n ): this {\n this.on(event, listener as (...args: unknown[]) => void)\n return this\n }\n\n onceEvent<T extends NexusEventName>(\n event: T,\n listener: NexusEvents[T] extends undefined\n ? () => void\n : (payload: NexusEvents[T]) => void\n ): this {\n this.once(event, listener as (...args: unknown[]) => void)\n return this\n }\n\n offEvent<T extends NexusEventName>(\n event: T,\n listener: NexusEvents[T] extends undefined\n ? () => void\n : (payload: NexusEvents[T]) => void\n ): this {\n this.off(event, listener as (...args: unknown[]) => void)\n return this\n }\n}\n\nexport const nexusEvents = new TypedEventEmitter({\n wildcard: true,\n delimiter: '.',\n maxListeners: 50,\n verboseMemoryLeak: true\n})\n\n// Re-export para uso directo\nexport { TypedEventEmitter }\n","import { Server as SocketServer, Socket } from 'socket.io'\nimport type { Server as HttpServer } from 'http'\nimport jwt from 'jsonwebtoken'\nimport { DEFAULT_TENANT_ID, entityRoom } from '@gzl10/nexus-sdk'\nimport { nexusEvents } from './emitter.js'\nimport { logger } from '../logger/index.js'\n\nlet io: SocketServer | null = null\nlet jwtSecret: string | null = null\n\n// Mapa de conexiones: userId -> Set<socketId>\nconst userSockets = new Map<string, Set<string>>()\n// Mapa inverso: socketId -> userId\nconst socketUsers = new Map<string, string>()\n\ninterface JwtPayload {\n userId: string\n roleIds: string[]\n}\n\nexport interface SocketIOOptions {\n /** JWT secret for token verification. If not provided, uses AUTH_SECRET env var */\n jwtSecret?: string\n /** CORS origin(s). If not provided, defaults to CORS_ORIGIN env var or '*' */\n corsOrigin?: string | string[]\n}\n\n/**\n * Initializes Socket.IO on the HTTP server\n * @param httpServer - HTTP server instance\n * @param options - Socket.IO initialization options (jwtSecret injected to avoid modules/ dependency)\n */\nexport function initSocketIO(httpServer: HttpServer, options?: SocketIOOptions): SocketServer {\n // Store secret from options or env (injected, not imported from modules/)\n jwtSecret = options?.jwtSecret ?? process.env['AUTH_SECRET'] ?? null\n\n if (!jwtSecret) {\n logger.warn('Socket.IO authentication disabled: AUTH_SECRET not configured. All connections will be treated as guests.')\n }\n\n const corsOrigin = options?.corsOrigin ?? process.env['CORS_ORIGIN'] ?? '*'\n\n io = new SocketServer(httpServer, {\n cors: { origin: corsOrigin, methods: ['GET', 'POST'] },\n path: '/socket.io',\n maxHttpBufferSize: 1e6 // 1MB - match Express json body limit\n })\n\n logger.info({ cors: corsOrigin }, 'Socket.IO initialized')\n\n // Middleware de autenticación (opcional - permite guests)\n io.use((socket, next) => {\n const token = socket.handshake.auth?.['token'] || socket.handshake.query?.['token']\n\n if (token && typeof token === 'string' && jwtSecret) {\n try {\n const payload = jwt.verify(token, jwtSecret) as JwtPayload\n socket.data.userId = payload.userId\n socket.data.roleIds = payload.roleIds ?? []\n socket.data.authenticated = true\n } catch (err) {\n logger.debug({ err, socketId: socket.id }, 'Socket JWT verification failed')\n socket.data.authenticated = false\n }\n } else {\n socket.data.authenticated = false\n }\n\n // Tenant resolution: single-tenant uses DEFAULT_TENANT_ID.\n // NEX-6 will replace this with tenant resolution from JWT/header/subdomain.\n socket.data.tenantId = DEFAULT_TENANT_ID\n\n next()\n })\n\n io.engine.on('connection_error', (err: { code: number; message: string; context: unknown }) => {\n logger.warn({ code: err.code, message: err.message }, 'Socket.IO connection error')\n })\n\n io.on('connection', handleConnection)\n\n nexusEvents.emitEvent('socket.initialized')\n\n return io\n}\n\nfunction handleConnection(socket: Socket) {\n const { userId, roleIds, authenticated } = socket.data\n\n socket.on('error', (err) => {\n logger.error({ err, socketId: socket.id, userId: socket.data.userId }, 'Socket error')\n })\n\n // Registrar conexión\n if (authenticated && userId) {\n if (!userSockets.has(userId)) {\n userSockets.set(userId, new Set())\n }\n userSockets.get(userId)!.add(socket.id)\n socketUsers.set(socket.id, userId)\n\n // Warn if user has too many connections\n const userSocketList = userSockets.get(userId)!\n if (userSocketList.size > 3) {\n logger.warn({ userId, count: userSocketList.size }, 'User has multiple socket connections')\n }\n\n // Unir a rooms del usuario y de cada role\n socket.join(`user:${userId}`)\n for (const roleId of (roleIds as string[]) ?? []) {\n socket.join(`role:${roleId}`)\n }\n socket.join('authenticated')\n\n logger.debug({ userId, roleIds, socketId: socket.id }, 'User connected')\n nexusEvents.emitEvent('socket.user.connected', { userId, roleIds, socketId: socket.id })\n }\n\n // Unir a room global (todos, incluyendo guests)\n socket.join('all')\n\n // Entity real-time subscription protocol\n socket.on('entity:subscribe', (entityKey: string) => {\n if (!entityKey || typeof entityKey !== 'string') {\n logger.warn({ socketId: socket.id }, 'Invalid room subscription attempt')\n return\n }\n if (entityKey.length > 0) {\n const dotIndex = entityKey.indexOf('.')\n if (dotIndex === -1) return\n const mod = entityKey.slice(0, dotIndex)\n const ent = entityKey.slice(dotIndex + 1)\n const room = entityRoom(socket.data.tenantId, mod, ent)\n socket.join(room)\n logger.debug({ entityKey, room, socketId: socket.id }, 'Socket subscribed to entity')\n }\n })\n\n socket.on('entity:unsubscribe', (entityKey: string) => {\n if (typeof entityKey === 'string' && entityKey.length > 0) {\n const dotIndex = entityKey.indexOf('.')\n if (dotIndex === -1) return\n const mod = entityKey.slice(0, dotIndex)\n const ent = entityKey.slice(dotIndex + 1)\n const room = entityRoom(socket.data.tenantId, mod, ent)\n socket.leave(room)\n logger.debug({ entityKey, room, socketId: socket.id }, 'Socket unsubscribed from entity')\n }\n })\n\n socket.on('disconnect', () => {\n if (userId) {\n userSockets.get(userId)?.delete(socket.id)\n if (userSockets.get(userId)?.size === 0) {\n userSockets.delete(userId)\n }\n socketUsers.delete(socket.id)\n\n logger.debug({ userId, socketId: socket.id }, 'User disconnected')\n nexusEvents.emitEvent('socket.user.disconnected', { userId, socketId: socket.id })\n }\n })\n}\n\n/**\n * Gets the Socket.IO instance\n * @throws Error if Socket.IO has not been initialized\n */\nexport function getIO(): SocketServer {\n if (!io) throw new Error('Socket.IO not initialized. Call initSocketIO first.')\n return io\n}\n\n/**\n * Checks whether Socket.IO is initialized\n */\nexport function isSocketIOInitialized(): boolean {\n return io !== null\n}\n\n/**\n * Gets IDs of connected users\n */\nexport function getConnectedUsers(): string[] {\n return Array.from(userSockets.keys())\n}\n\n/**\n * Checks whether a specific user is connected\n */\nexport function isUserConnected(userId: string): boolean {\n return userSockets.has(userId)\n}\n\n/**\n * Gets the number of sockets connected for a user\n */\nexport function getUserSocketCount(userId: string): number {\n return userSockets.get(userId)?.size ?? 0\n}\n\n// ============================================================================\n// Room Management (CRUD)\n// ============================================================================\n\n/**\n * Joins a user to a custom room (all their sockets)\n */\nexport function joinUserToRoom(userId: string, room: string): boolean {\n if (!io) return false\n const sockets = userSockets.get(userId)\n if (!sockets || sockets.size === 0) return false\n\n for (const socketId of sockets) {\n const socket = io.sockets.sockets.get(socketId)\n socket?.join(room)\n }\n logger.debug({ userId, room }, 'User joined room')\n return true\n}\n\n/**\n * Removes a user from a custom room (all their sockets)\n */\nexport function removeUserFromRoom(userId: string, room: string): boolean {\n if (!io) return false\n const sockets = userSockets.get(userId)\n if (!sockets || sockets.size === 0) return false\n\n for (const socketId of sockets) {\n const socket = io.sockets.sockets.get(socketId)\n socket?.leave(room)\n }\n logger.debug({ userId, room }, 'User left room')\n return true\n}\n\n/**\n * Joins a specific socket to a room\n */\nexport function joinSocketToRoom(socketId: string, room: string): boolean {\n if (!io) return false\n const socket = io.sockets.sockets.get(socketId)\n if (!socket) return false\n\n socket.join(room)\n return true\n}\n\n/**\n * Removes a specific socket from a room\n */\nexport function removeSocketFromRoom(socketId: string, room: string): boolean {\n if (!io) return false\n const socket = io.sockets.sockets.get(socketId)\n if (!socket) return false\n\n socket.leave(room)\n return true\n}\n\n/**\n * Gets all user IDs in a room\n */\nexport async function getRoomMembers(room: string): Promise<string[]> {\n if (!io) return []\n\n const sockets = await io.in(room).fetchSockets()\n const userIds = new Set<string>()\n\n for (const socket of sockets) {\n const userId = (socket as any).data?.userId\n if (userId) userIds.add(userId)\n }\n\n return Array.from(userIds)\n}\n\n/**\n * Gets the number of sockets in a room\n */\nexport async function getRoomSize(room: string): Promise<number> {\n if (!io) return 0\n const sockets = await io.in(room).fetchSockets()\n return sockets.length\n}\n\n/**\n * Gets all rooms a user is in (excluding system rooms)\n */\nexport function getUserRooms(userId: string): string[] {\n if (!io) return []\n\n const sockets = userSockets.get(userId)\n if (!sockets || sockets.size === 0) return []\n\n const rooms = new Set<string>()\n const systemRooms = ['all', 'authenticated', `user:${userId}`]\n\n for (const socketId of sockets) {\n const socket = io.sockets.sockets.get(socketId)\n if (socket) {\n for (const room of socket.rooms) {\n // Excluir el socketId (siempre está en su propia \"room\") y rooms del sistema\n if (room !== socketId && !systemRooms.includes(room) && !room.startsWith('role:')) {\n rooms.add(room)\n }\n }\n }\n }\n\n return Array.from(rooms)\n}\n\n/**\n * Checks if a room has any members\n */\nexport async function roomExists(room: string): Promise<boolean> {\n if (!io) return false\n const size = await getRoomSize(room)\n return size > 0\n}\n\n/**\n * Emits an event to a specific room\n */\nexport function emitToRoom(room: string, event: string, data: unknown): boolean {\n if (!io) return false\n const sockets = io.sockets.adapter.rooms.get(room)\n logger.debug({ room, event, socketsInRoom: sockets?.size ?? 0 }, '[DEBUG-RT] emitToRoom')\n io.to(room).emit(event, data)\n return true\n}\n\n// ============================================================================\n// Emission Helpers\n// ============================================================================\n\n/**\n * Emits an event to a specific user (all their sockets)\n */\nexport function emitToUser(userId: string, event: string, data: unknown): boolean {\n if (!io) return false\n io.to(`user:${userId}`).emit(event, data)\n return true\n}\n\n/**\n * Emits an event to all users with a specific role\n */\nexport function emitToRole(roleId: string, event: string, data: unknown): boolean {\n if (!io) return false\n io.to(`role:${roleId}`).emit(event, data)\n return true\n}\n\n/**\n * Emits an event to all connected sockets\n */\nexport function emitToAll(event: string, data: unknown): boolean {\n if (!io) return false\n io.to('all').emit(event, data)\n return true\n}\n\n/**\n * Emits an event to all authenticated users\n */\nexport function emitToAuthenticated(event: string, data: unknown): boolean {\n if (!io) return false\n io.to('authenticated').emit(event, data)\n return true\n}\n\n/**\n * Broadcasts to a room except specific user(s)\n */\nexport function broadcastToRoom(\n room: string,\n exceptUserIds: string | string[],\n event: string,\n data: unknown\n): boolean {\n if (!io) return false\n\n const excludeIds = Array.isArray(exceptUserIds) ? exceptUserIds : [exceptUserIds]\n const excludeRooms = excludeIds.map(id => `user:${id}`)\n\n io.to(room).except(excludeRooms).emit(event, data)\n return true\n}\n\n/**\n * Closes Socket.IO (for test cleanup or shutdown)\n */\nexport function closeSocketIO(): void {\n if (io) {\n io.close()\n io = null\n userSockets.clear()\n socketUsers.clear()\n logger.debug('Socket.IO closed')\n }\n}\n\n/**\n * Executes callback when Socket.IO is ready.\n * If already initialized, runs immediately. Otherwise waits for 'socket.initialized' event.\n */\nexport function onSocketReady(callback: () => void): void {\n if (io) {\n callback()\n } else {\n nexusEvents.onceEvent('socket.initialized', callback)\n }\n}\n","/**\n * Debounce utility for real-time event bridge.\n *\n * Prevents flooding Socket.IO when rapid db.* events fire\n * (e.g., bulk insert triggers many db.table.created in ms).\n * Groups by key and only fires the last event after the debounce window.\n */\n\ntype DebouncedCallback = (payload: unknown) => void\n\ninterface PendingTimer {\n timer: ReturnType<typeof setTimeout>\n payload: unknown\n}\n\nexport class RealtimeDebouncer {\n private pending = new Map<string, PendingTimer>()\n\n /**\n * Debounce a callback by key.\n * If called again with the same key before `ms` elapses,\n * the previous call is cancelled and replaced.\n *\n * @param key - Grouping key (e.g., 'entity:module.entity:id')\n * @param ms - Debounce window in milliseconds\n * @param payload - Event payload\n * @param callback - Function to call after debounce\n * @param onDiscard - Optional callback when a pending event is replaced\n */\n debounce(key: string, ms: number, payload: unknown, callback: DebouncedCallback, onDiscard?: (key: string) => void): void {\n const existing = this.pending.get(key)\n if (existing) {\n clearTimeout(existing.timer)\n onDiscard?.(key)\n }\n\n const timer = setTimeout(() => {\n this.pending.delete(key)\n callback(payload)\n }, ms)\n\n this.pending.set(key, { timer, payload })\n }\n\n /**\n * Cancel all pending debounced calls.\n */\n clear(): void {\n for (const { timer } of this.pending.values()) {\n clearTimeout(timer)\n }\n this.pending.clear()\n }\n\n /**\n * Number of pending debounced calls.\n */\n get size(): number {\n return this.pending.size\n }\n}\n","/**\n * Event Bridge - Declarative routing from nexusEvents to Socket.IO.\n *\n * Modules register BridgeRules to automatically forward internal events\n * to Socket.IO rooms. This replaces manual `io.to().emit()` calls\n * with a centralized, auditable routing layer.\n */\n\nimport type { EventEmitter2 } from 'eventemitter2'\nimport type { BridgeRule } from './types.js'\nimport { nexusEvents } from './emitter.js'\nimport {\n isSocketIOInitialized,\n emitToRoom,\n emitToUser,\n emitToAll,\n emitToAuthenticated\n} from './socket.js'\nimport { RealtimeDebouncer } from './realtime-debouncer.js'\nimport { logger } from '../logger/index.js'\n\ninterface RegisteredListener {\n event: string\n handler: (...args: unknown[]) => void\n}\n\nclass EventBridge {\n private rules: BridgeRule[] = []\n private listeners: RegisteredListener[] = []\n private debouncer = new RealtimeDebouncer()\n private initialized = false\n\n /**\n * Register a bridge rule.\n * If the bridge is already initialized, the listener is attached immediately.\n */\n registerRule(rule: BridgeRule): void {\n // Avoid duplicate rules for the same nexusEvent+socketEvent\n const exists = this.rules.some(\n r => r.nexusEvent === rule.nexusEvent && r.socketEvent === rule.socketEvent\n )\n if (exists) {\n logger.debug({ nexusEvent: rule.nexusEvent, socketEvent: rule.socketEvent }, 'Bridge rule already registered, skipping')\n return\n }\n\n this.rules.push(rule)\n\n if (this.initialized) {\n this.attachRule(rule)\n }\n }\n\n /**\n * Remove all rules for a specific nexusEvent.\n */\n removeRule(nexusEvent: string): void {\n // Remove listeners\n const toRemove = this.listeners.filter(l => l.event === nexusEvent)\n for (const { event, handler } of toRemove) {\n ;(nexusEvents as unknown as EventEmitter2).off(event, handler)\n }\n this.listeners = this.listeners.filter(l => l.event !== nexusEvent)\n\n // Remove rules\n this.rules = this.rules.filter(r => r.nexusEvent !== nexusEvent)\n }\n\n /**\n * Initialize the bridge — attach all registered rules to nexusEvents.\n * Should be called after Socket.IO is ready.\n */\n init(): void {\n if (this.initialized) return\n\n for (const rule of this.rules) {\n this.attachRule(rule)\n }\n\n this.initialized = true\n logger.debug({ ruleCount: this.rules.length }, 'Event bridge initialized')\n }\n\n /**\n * Destroy — remove all listeners and clear state.\n */\n destroy(): void {\n for (const { event, handler } of this.listeners) {\n ;(nexusEvents as unknown as EventEmitter2).off(event, handler)\n }\n this.listeners = []\n this.rules = []\n this.debouncer.clear()\n this.initialized = false\n }\n\n /**\n * Get all registered rules (for debugging/introspection).\n */\n getRules(): readonly BridgeRule[] {\n return this.rules\n }\n\n private attachRule(rule: BridgeRule): void {\n const handler = (payload: unknown) => {\n logger.debug({ nexusEvent: rule.nexusEvent, socketEvent: rule.socketEvent }, '[DEBUG-RT] Bridge rule fired')\n if (!isSocketIOInitialized()) {\n logger.debug('[DEBUG-RT] Socket.IO not initialized, skipping bridge')\n return\n }\n\n const emit = () => {\n const transformed = rule.transform ? rule.transform(payload) : payload\n this.routeToSocket(rule, transformed)\n }\n\n if (rule.debounceMs && rule.debounceMs > 0) {\n // Include room in key for granularity (different rooms debounce independently)\n const room = rule.target.type === 'room'\n ? (typeof rule.target.room === 'function' ? 'dynamic' : rule.target.room)\n : rule.target.type\n const key = `${rule.nexusEvent}:${rule.socketEvent}:${room}`\n this.debouncer.debounce(key, rule.debounceMs, payload, () => {\n const transformed = rule.transform ? rule.transform(payload) : payload\n this.routeToSocket(rule, transformed)\n }, (discardedKey) => {\n logger.debug({ key: discardedKey, nexusEvent: rule.nexusEvent }, 'Event debounced: replaced pending event')\n })\n } else {\n emit()\n }\n }\n\n ;(nexusEvents as unknown as EventEmitter2).on(rule.nexusEvent, handler)\n this.listeners.push({ event: rule.nexusEvent, handler })\n }\n\n private routeToSocket(rule: BridgeRule, payload: unknown): void {\n const { target, socketEvent } = rule\n logger.debug({ socketEvent, targetType: target.type }, '[DEBUG-RT] routeToSocket called')\n\n switch (target.type) {\n case 'all':\n emitToAll(socketEvent, payload)\n break\n case 'authenticated':\n emitToAuthenticated(socketEvent, payload)\n break\n case 'user': {\n const userId = typeof target.userId === 'function'\n ? target.userId(payload)\n : target.userId\n emitToUser(userId, socketEvent, payload)\n break\n }\n case 'room': {\n const room = typeof target.room === 'function'\n ? target.room(payload)\n : target.room\n logger.debug({ room, socketEvent }, '[DEBUG-RT] emitting to room')\n emitToRoom(room, socketEvent, payload)\n break\n }\n }\n }\n}\n\nexport const eventBridge = new EventBridge()\n","import net from 'node:net'\nimport { execSync } from 'child_process'\n\n/**\n * Finds the process name and PID using a port\n */\nfunction findProcessOnPort(port: number): { pid: number; name: string } | null {\n try {\n const output = execSync(`lsof -ti :${port} 2>/dev/null`, { encoding: 'utf-8' })\n const pid = parseInt(output.trim().split('\\n')[0] ?? '', 10)\n if (isNaN(pid)) return null\n\n let name = 'unknown'\n try {\n name = execSync(`ps -o comm= -p ${pid} 2>/dev/null`, { encoding: 'utf-8' }).trim()\n } catch { /* ignore */ }\n\n return { pid, name }\n } catch {\n return null\n }\n}\n\n/**\n * Checks whether a port is available.\n * If occupied, logs the offending process and fails with a clear message.\n */\nexport async function checkPortAvailable(port: number, host = '0.0.0.0'): Promise<void> {\n return new Promise((resolve, reject) => {\n const server = net.createServer()\n\n server.once('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n const proc = findProcessOnPort(port)\n const msg = proc\n ? `Port ${port} is already in use by \"${proc.name}\" (PID ${proc.pid}). Stop that process first.`\n : `Port ${port} is already in use`\n reject(new Error(msg))\n } else {\n reject(err)\n }\n })\n\n server.once('listening', () => {\n server.close(() => resolve())\n })\n\n server.listen(port, host)\n })\n}\n","import { spawn, execSync, type ChildProcess } from 'node:child_process'\nimport { writeFileSync, unlinkSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { logger } from './index.js'\n\nexport interface TunnelConfig {\n server: string\n serverPort: number\n token?: string\n subdomain: string\n localPort: number\n}\n\nlet tunnelProcess: ChildProcess | null = null\nlet tmpConfigPath: string | null = null\nlet outputBuffer = ''\n\nexport function buildFrpcConfig(config: TunnelConfig): string {\n const lines = [\n `serverAddr = \"${config.server}\"`,\n `serverPort = ${config.serverPort}`,\n ]\n\n if (config.token) {\n lines.push(`auth.token = \"${config.token}\"`)\n }\n\n lines.push(\n '',\n '[[proxies]]',\n `name = \"${config.subdomain}\"`,\n 'type = \"http\"',\n `localPort = ${config.localPort}`,\n `subdomain = \"${config.subdomain}\"`,\n )\n\n return lines.join('\\n') + '\\n'\n}\n\nexport function getTunnelUrl(subdomain: string, server: string): string {\n return `https://${subdomain}.${server}`\n}\n\nfunction isFrpcInstalled(): boolean {\n try {\n execSync('command -v frpc', { stdio: 'ignore' })\n return true\n } catch {\n return false\n }\n}\n\nexport async function startTunnel(config: TunnelConfig): Promise<boolean> {\n if (tunnelProcess) return true\n\n if (!isFrpcInstalled()) {\n logger.warn('frpc binary not found — tunnel disabled. Install with: brew install frp')\n return false\n }\n\n const toml = buildFrpcConfig(config)\n tmpConfigPath = join(tmpdir(), `nexus-frpc-${process.pid}.toml`)\n writeFileSync(tmpConfigPath, toml)\n\n tunnelProcess = spawn('frpc', ['-c', tmpConfigPath], {\n stdio: ['ignore', 'pipe', 'pipe']\n })\n\n outputBuffer = ''\n // Strip ANSI escape codes from frpc output\n // eslint-disable-next-line no-control-regex\n const stripAnsi = (s: string) => s.replace(/\\x1b\\[[0-9;]*m/g, '')\n\n return new Promise<boolean>((resolve) => {\n let settled = false\n\n const collectOutput = (data: Buffer) => {\n const msg = stripAnsi(data.toString()).trim()\n if (!msg) return\n outputBuffer += msg + '\\n'\n logger.debug({ component: 'tunnel' }, msg)\n // frpc logs \"start proxy success\" when connected\n if (!settled && msg.includes('start proxy success')) {\n settled = true\n const url = getTunnelUrl(config.subdomain, config.server)\n logger.info({ url, component: 'tunnel' }, `Tunnel active → ${url}`)\n resolve(true)\n }\n }\n\n tunnelProcess!.stdout?.on('data', collectOutput)\n tunnelProcess!.stderr?.on('data', collectOutput)\n\n tunnelProcess!.on('exit', (code) => {\n if (code !== 0 && code !== null) {\n const output = outputBuffer.trim()\n const hint = output.includes('authorization failed')\n || output.includes('auth failed')\n || output.includes('invalid token')\n ? ' — check FRPC_TOKEN is correct'\n : output.includes('login to the server failed')\n ? ` — ${output.split('\\n').pop()}`\n : ''\n logger.error(\n { code, reason: output || undefined, component: 'tunnel' },\n `frpc failed to start${hint}`,\n )\n }\n tunnelProcess = null\n outputBuffer = ''\n cleanupConfig()\n if (!settled) {\n settled = true\n resolve(false)\n }\n })\n\n // Timeout — don't block server startup forever\n setTimeout(() => {\n if (!settled) {\n settled = true\n logger.warn({ component: 'tunnel' }, 'frpc connection timeout (5s) — tunnel may not be active')\n resolve(false)\n }\n }, 5000)\n })\n}\n\nexport function stopTunnel(): void {\n if (tunnelProcess) {\n tunnelProcess.kill('SIGTERM')\n tunnelProcess = null\n }\n cleanupConfig()\n}\n\nfunction cleanupConfig(): void {\n if (tmpConfigPath) {\n try { unlinkSync(tmpConfigPath) } catch { /* already gone */ }\n tmpConfigPath = null\n }\n}\n","/**\n * OpenTelemetry instrumentation setup.\n *\n * Opt-in: Only activates when OTEL_ENABLED=true.\n * When disabled, zero overhead (no OTel packages loaded).\n *\n * Loaded by main.ts BEFORE Express/Knex to ensure proper ESM patching.\n *\n * Instruments: HTTP, Express, Knex (DB), Pino (log correlation).\n * Exports: Prometheus metrics on OTEL_PROMETHEUS_PORT (default 9464).\n * Optional: OTLP trace export to collector (Jaeger, Tempo, etc).\n */\n\nimport { getOtelConfig } from './modules/observability/observability.config.js'\n\ntype NodeSDKInstance = { start: () => void; shutdown: () => Promise<void> }\nlet sdk: NodeSDKInstance | null = null\n\n// ── Auto-init at import time (top-level await) ─────────────────────────────\n// main.ts imports this module BEFORE Express/Knex, so OTel hooks are in place\n// when those libraries load.\n\nconst config = getOtelConfig()\n\nif (config.enabled) {\n const [\n { NodeSDK },\n { HttpInstrumentation },\n { ExpressInstrumentation },\n { KnexInstrumentation },\n { PinoInstrumentation },\n { PrometheusExporter }\n ] = await Promise.all([\n import('@opentelemetry/sdk-node'),\n import('@opentelemetry/instrumentation-http'),\n import('@opentelemetry/instrumentation-express'),\n import('@opentelemetry/instrumentation-knex'),\n import('@opentelemetry/instrumentation-pino'),\n import('@opentelemetry/exporter-prometheus')\n ])\n\n const sdkOptions: ConstructorParameters<typeof NodeSDK>[0] = {\n serviceName: config.serviceName,\n metricReader: new PrometheusExporter({ port: config.prometheusPort }),\n instrumentations: [\n new HttpInstrumentation(),\n new ExpressInstrumentation(),\n new KnexInstrumentation(),\n new PinoInstrumentation()\n ]\n }\n\n if (config.otlpEndpoint) {\n const { OTLPTraceExporter } = await import('@opentelemetry/exporter-trace-otlp-http')\n sdkOptions.traceExporter = new OTLPTraceExporter({\n url: config.otlpEndpoint\n })\n }\n\n if (config.traceSampleRate < 1) {\n process.env['OTEL_TRACES_SAMPLER'] = 'traceidratio'\n process.env['OTEL_TRACES_SAMPLER_ARG'] = String(config.traceSampleRate)\n }\n\n sdk = new NodeSDK(sdkOptions)\n sdk.start()\n console.log(`[otel] Instrumentation active — metrics on :${config.prometheusPort}/metrics`)\n}\n\n/**\n * No-op: init happens at import time via top-level await.\n * Kept for server.ts lifecycle compatibility.\n */\nexport async function initTelemetry(): Promise<void> {}\n\n/**\n * Gracefully shutdown the OTel SDK, flushing pending telemetry.\n * No-op if OTel was not initialized.\n */\nexport async function shutdownTelemetry(): Promise<void> {\n if (!sdk) return\n await sdk.shutdown()\n sdk = null\n}\n","import http from 'node:http'\nimport { createApp } from './app.js'\nimport { getDatabaseType, getDatabasePath } from '../config/database.js'\nimport { resolveConfig, resetConfig, env } from '../config/env.js'\nimport { nexusEvents, logger, setLoggerInstance, getLibPath, getProjectPath, initSocketIO } from './index.js'\nimport { closeSocketIO } from './events-hub/socket.js'\nimport { entityRoom } from '@gzl10/nexus-sdk'\nimport { eventBridge } from './events-hub/event-bridge.js'\nimport { setCustomCaslRules, clearCustomCaslRules, setSeedPermissions, clearSeedPermissions } from './abilities/ability.factory.js'\nimport { resetServeSPA } from './spa-handler.js'\nimport { buildEffectiveCorsOrigins } from './utils/cors.js'\nimport { getLoggerConfig, initLoggerService, getPinoLogger, captureExceptionSafe } from './logger/index.js'\nimport { initErrorMiddleware } from './middleware/error.middleware.js'\nimport { checkPortAvailable } from './utils/port-check.js'\nimport { loadCoreModules, getOrderedModules, resetStore, registerPlugin, registerModule, topologicalSortPlugins, shouldLoadPlugin, resetSharedAdapters, resetCacheManager, setLocales } from '../engine/index.js'\nimport { DEFAULT_LOCALES } from '@gzl10/nexus-sdk'\nimport type { PluginManifest } from '../engine/index.js'\nimport { createModuleContext } from '../engine/context.js'\nimport { destroyDb, getDb, runModuleSeed, ensureSystemTables, runAllGeneratedMigrations, buildMigrationSources, loadAllMigrationFiles, createMemoryTables } from '../db/index.js'\nimport { runMigrations } from '../db/migration-runner.js'\nimport { detectSchemaDrift, formatDriftMessage } from '../db/migration-generator.js'\nimport { ensureMigrationTables } from '../db/ensure-system-tables.js'\nimport { loadNexusConfig, resetConfigCache } from '../config/load-config.js'\nimport type { NexusConfig, StartOptions } from '../config/types.js'\nimport { startTunnel, stopTunnel, getTunnelUrl } from './tunnel.js'\n\nlet server: http.Server | null = null\nlet currentConfig: StartOptions | undefined\nlet gracefulShutdownRegistered = false\n\nexport type { NexusConfig, StartOptions }\n\nasync function runMigrationsAndSeeds(config?: StartOptions): Promise<void> {\n // Inicializar logger antes de cualquier log para evitar múltiples instancias de pino-pretty\n initLoggerService(getLoggerConfig())\n setLoggerInstance(getPinoLogger())\n\n // Initialize error middleware with exception capture (injected to avoid core/ → modules/ dependency)\n initErrorMiddleware({ captureException: captureExceptionSafe })\n\n // Reset store to ensure idempotent start (prevents module duplication in tests)\n resetStore()\n\n // Cargar módulos core antes de usarlos\n loadCoreModules()\n\n logger.info({ type: getDatabaseType(), path: getDatabasePath() }, 'Database ready')\n\n // Asegurar tablas de sistema ANTES de consultar plugins\n await ensureSystemTables(getDb())\n\n // Load config file + auto-discovered plugins, merge with programmatic config\n const fileConfig = await loadNexusConfig()\n const effectiveConfig: StartOptions = { ...config }\n\n // Merge plugins: programmatic take precedence, file/discovered are appended\n const programmaticPluginNames = new Set((config?.plugins ?? []).map((p) => p.name))\n const additionalPlugins = (fileConfig.plugins ?? []).filter((p) => !programmaticPluginNames.has(p.name))\n if (additionalPlugins.length > 0) {\n effectiveConfig.plugins = [...(config?.plugins ?? []), ...additionalPlugins]\n logger.info({ count: additionalPlugins.length }, 'Plugins merged from config/discovery')\n }\n\n // Merge modules: same pattern, but also exclude already-registered core modules\n const programmaticModuleNames = new Set((config?.modules ?? []).map((m) => m.name))\n const coreModuleNames = new Set(getOrderedModules().map((m) => m.name))\n const additionalModules = (fileConfig.modules ?? []).filter(\n (m) => !programmaticModuleNames.has(m.name) && !coreModuleNames.has(m.name)\n )\n if (additionalModules.length > 0) {\n effectiveConfig.modules = [...(config?.modules ?? []), ...additionalModules]\n }\n\n // Filtrar plugins según nexus.plugins.json\n const pluginsToLoad: PluginManifest[] = []\n for (const plugin of effectiveConfig?.plugins ?? []) {\n if (shouldLoadPlugin(plugin.name)) {\n pluginsToLoad.push(plugin)\n logger.debug({ plugin: plugin.name }, 'Plugin enabled')\n } else {\n logger.info({ plugin: plugin.name }, 'Plugin disabled, skipping')\n }\n }\n\n // Registrar solo plugins habilitados (ordenados por dependencias)\n const orderedPlugins = topologicalSortPlugins(pluginsToLoad)\n for (const plugin of orderedPlugins) {\n registerPlugin(plugin)\n logger.debug({ plugin: plugin.name }, 'Plugin registered')\n }\n\n // Registrar módulos sueltos del usuario\n for (const mod of effectiveConfig?.modules ?? []) {\n registerModule(mod, { source: 'standalone' })\n logger.debug({ module: mod.name }, 'Standalone module registered')\n }\n\n const ctx = createModuleContext()\n const modules = getOrderedModules()\n\n // Asegurar tablas de migración\n await ensureMigrationTables(getDb())\n\n // === Migration handling ===\n // - test: create tables directly from entity definitions (no migration files needed)\n // - development: only detect drift (developer must use CLI to generate/deploy)\n // - production: deploy pending migrations from files, then verify\n if (env.NODE_ENV === 'test') {\n // Test: create tables directly from registered entities (fast, no files needed)\n logger.debug('Test mode — creating tables from entity definitions...')\n await runAllGeneratedMigrations(ctx, modules)\n } else if (env.NODE_ENV === 'development') {\n // Development: deploy pending migrations first (plugins ship their own), then detect drift\n const sources = buildMigrationSources()\n const migrationFiles = await loadAllMigrationFiles(sources)\n\n if (migrationFiles.length > 0) {\n logger.info({ sources: sources.length, files: migrationFiles.length }, 'Running migration deploy...')\n try {\n await runMigrations(undefined, sources)\n } catch (err) {\n logger.fatal({ err }, 'Migration deploy failed')\n captureExceptionSafe(err as Error, { scope: 'db', action: 'migrations', phase: 'dev-deploy' })\n throw err\n }\n }\n\n // After deploying, detect remaining drift (entities without migration files)\n logger.debug('Checking schema drift...')\n const drift = await detectSchemaDrift()\n if (drift && (drift.newTables.length > 0 || drift.alteredTables.length > 0)) {\n const message = formatDriftMessage(drift)\n logger.fatal(message)\n const driftError = new Error('Schema drift detected. Run \"nexus migrate:create\" and \"nexus migrate:deploy\" to update.')\n captureExceptionSafe(driftError, { scope: 'db', action: 'schema-drift', phase: 'dev-check' })\n throw driftError\n }\n if (drift?.droppedTables.length) {\n logger.warn({ tables: drift.droppedTables }, 'Extra tables found in database (not managed by current entities)')\n }\n logger.debug('No schema drift detected')\n } else {\n // Production: deploy pending migrations from all sources (core → plugins → project)\n const sources = buildMigrationSources()\n const migrationFiles = await loadAllMigrationFiles(sources)\n\n if (migrationFiles.length === 0) {\n const dirs = sources.map((s) => ` ${s.id}: ${s.dir}`).join('\\n')\n logger.fatal('No migration files found')\n const noMigrationsError = new Error(`No migration files found. Generate migrations first with the CLI.\\nSearched in:\\n${dirs}`)\n captureExceptionSafe(noMigrationsError, { scope: 'db', action: 'migrations', phase: 'deploy' })\n throw noMigrationsError\n }\n\n logger.info({ count: migrationFiles.length, sources: sources.map((s) => s.id) }, 'Deploying migrations...')\n try {\n await runMigrations(undefined, sources)\n } catch (err) {\n logger.fatal({ err }, 'Migration deploy failed')\n captureExceptionSafe(err instanceof Error ? err : new Error(String(err)), { scope: 'db', action: 'migrations', phase: 'deploy' })\n throw err\n }\n\n // Verify no drift after deployment (only missing tables/columns matter;\n // extra tables are harmless — could be from disabled plugins or manual additions)\n const drift = await detectSchemaDrift()\n if (drift && (drift.newTables.length > 0 || drift.alteredTables.length > 0)) {\n const message = formatDriftMessage(drift)\n logger.fatal(message)\n const postDeployDriftError = new Error('Schema drift detected after migration deploy. Some migrations may be missing.')\n captureExceptionSafe(postDeployDriftError, { scope: 'db', action: 'schema-drift', phase: 'post-deploy' })\n throw postDeployDriftError\n }\n if (drift?.droppedTables.length) {\n logger.warn({ tables: drift.droppedTables }, 'Extra tables found in database (not managed by current entities)')\n }\n }\n\n // Create in-memory tables for entities with adapter: 'memory'\n const allDefinitions = modules.flatMap(m => m.definitions ?? [])\n await createMemoryTables(allDefinitions)\n\n // Import user seed files from data/seeds/ (before module seeds so plugins respect if-empty)\n try {\n const { importSeedFiles } = await import('../cli/seed-commands.js')\n await importSeedFiles(ctx.db.knex, modules, {\n info: (msg: string) => logger.info(msg),\n debug: (msg: string) => logger.debug(msg),\n })\n } catch (err) {\n logger.debug({ err }, 'Seed file import skipped (no data/seeds/ or error)')\n }\n\n logger.debug('Running seeds...')\n for (const mod of modules) {\n try {\n await runModuleSeed(mod, ctx)\n } catch (err) {\n logger.error({ module: mod.name, err }, 'Seed failed - continuing with next module')\n }\n }\n\n // User-defined seed hook (runs after all module seeds)\n if (config?.onSeed) {\n const { getPlugins } = await import('../engine/module-queries.js')\n const { createMasterRegistry } = await import('../modules/masters/registry.js')\n\n // Get or create master registry\n const masterRegistry = ctx.services.has('masters')\n ? ctx.services.get('masters') as { register: (...args: any[]) => void; seed: (ctx: any) => Promise<void> }\n : createMasterRegistry()\n\n // Build plugin code → prefix map\n const pluginPrefixes = new Map<string, string>()\n for (const plugin of getPlugins()) {\n pluginPrefixes.set(plugin.code, `${plugin.code}_`)\n const shortName = plugin.name.replace(/^@[^/]+\\/nexus-plugin-/, '')\n if (shortName !== plugin.code) {\n pluginPrefixes.set(shortName, `${plugin.code}_`)\n }\n }\n\n const { createSeedContext } = await import('../db/seed-context.js')\n const { ctx: seedCtx, flushPermissions } = createSeedContext({\n knex: ctx.db.knex,\n generateId: ctx.core.generateId,\n hashPassword: ctx.core.crypto.hashPassword,\n masterRegistry,\n pluginPrefixes,\n logger: ctx.core.logger,\n onPermissionsCollected: (perms) => setSeedPermissions(perms)\n })\n\n await config.onSeed(seedCtx)\n\n // Flush lazy masters registered via seed.masters.register()\n await masterRegistry.seed(ctx)\n\n // Inject accumulated CASL permissions into ability factory (step 4)\n flushPermissions()\n }\n}\n\n/**\n * Starts the Nexus server.\n *\n * Lifecycle order:\n * 1. Logger, DB, migrations\n * 2. Module/plugin registration and seed\n * 3. `onSeed` hook (SeedContext with typed helpers)\n * 4. `beforeRoutes` hook → module routes → `afterRoutes` hook\n * 5. HTTP listen + Socket.IO\n * 6. `onReady` hook\n *\n * @param config - Optional startup configuration (plugins, modules, lifecycle hooks, SPAs)\n * @returns The underlying `http.Server` instance\n * @throws If the server is already running (call `stop()` first)\n *\n * @example\n * ```typescript\n * import { start } from '@gzl10/nexus-backend'\n *\n * const server = await start({\n * onSeed: async (seed) => {\n * await seed.roles.add({ name: 'SALES', permissions: { 'contacts': ['read', 'create'] } })\n * },\n * onReady: (_app, port) => console.log(`Ready on ${port}`)\n * })\n * ```\n */\nexport async function start(config?: StartOptions): Promise<http.Server> {\n if (server) {\n throw new Error('Server already running. Call stop() first.')\n }\n\n // Guardar config para hooks\n currentConfig = config\n\n // Set platform locales before context creation\n setLocales(config?.locales ?? DEFAULT_LOCALES)\n\n // Tunnel URL is set after frpc confirms connection (see below)\n // Auto-enable trust proxy when tunnel is configured (frpc is a reverse proxy)\n if (env.NODE_ENV === 'development' && env.FRPC_SERVER && !env.TRUST_PROXY) {\n process.env['TRUST_PROXY'] = 'true'\n }\n\n const resolved = resolveConfig()\n\n // Verificar puerto disponible antes de iniciar (si port > 0)\n // Port 0 significa que el SO asignará un puerto libre automáticamente\n if (resolved.port > 0) {\n await checkPortAvailable(resolved.port, resolved.host)\n }\n\n // Registrar event handlers\n if (config?.events) {\n for (const [eventName, handler] of Object.entries(config.events)) {\n if (handler && typeof handler === 'function') {\n nexusEvents.on(eventName as any, handler)\n logger.debug(`Event handler registered: ${eventName}`)\n }\n }\n }\n\n // Registrar CASL custom\n if (config?.casl) {\n setCustomCaslRules(config.casl)\n logger.debug('Custom CASL rules registered')\n }\n\n nexusEvents.emitEvent('server.starting', { port: resolved.port, host: resolved.host })\n\n // Initialize OpenTelemetry (opt-in, before app creation for proper instrumentation)\n const { initTelemetry } = await import('../instrumentation.js')\n await initTelemetry()\n\n // Ejecutar migraciones y seeds antes de iniciar\n await runMigrationsAndSeeds(config)\n\n // Compute effective CORS origins for Socket.IO (createApp computes its own internally)\n const effectiveCorsOrigins = buildEffectiveCorsOrigins(env.CORS_ORIGIN, config?.spas)\n\n // Create HTTP server first so Vite HMR WebSocket can attach to it\n const httpServer = http.createServer()\n\n const app = await createApp({\n beforeRoutes: config?.beforeRoutes,\n afterRoutes: config?.afterRoutes,\n spas: config?.spas,\n httpServer\n })\n\n // Connect Express to the HTTP server\n httpServer.on('request', app)\n\n return new Promise((resolve) => {\n server = httpServer\n httpServer.listen(resolved.port, resolved.host, async () => {\n // Request timeout (default 30s, configurable via REQUEST_TIMEOUT_MS)\n const timeoutMs = parseInt(process.env['REQUEST_TIMEOUT_MS'] || '30000', 10)\n if (timeoutMs > 0) {\n server!.setTimeout(timeoutMs)\n }\n\n // Inicializar Socket.IO después de que el servidor esté escuchando\n initSocketIO(server!, { corsOrigin: effectiveCorsOrigins })\n\n // Initialize event bridge (nexusEvents → Socket.IO routing)\n // Register default bridge rules for entity real-time events\n eventBridge.registerRule({\n nexusEvent: 'entity.created',\n socketEvent: 'entity:created',\n target: { type: 'room', room: (p: any) => entityRoom(p.tenantId, p.module, p.entity) },\n debounceMs: 50\n })\n eventBridge.registerRule({\n nexusEvent: 'entity.updated',\n socketEvent: 'entity:updated',\n target: { type: 'room', room: (p: any) => entityRoom(p.tenantId, p.module, p.entity) },\n debounceMs: 50\n })\n eventBridge.registerRule({\n nexusEvent: 'entity.deleted',\n socketEvent: 'entity:deleted',\n target: { type: 'room', room: (p: any) => entityRoom(p.tenantId, p.module, p.entity) }\n })\n eventBridge.init()\n\n // Obtener el puerto real (importante cuando se usa port: 0)\n const addr = server!.address()\n const actualPort = typeof addr === 'object' && addr ? addr.port : resolved.port\n\n // Start frpc tunnel in development — awaits connection confirmation\n let tunnelActive = false\n if (env.NODE_ENV === 'development' && env.FRPC_SERVER && env.FRPC_SUBDOMAIN) {\n tunnelActive = await startTunnel({\n server: env.FRPC_SERVER,\n serverPort: env.FRPC_SERVER_PORT,\n token: env.FRPC_TOKEN,\n subdomain: env.FRPC_SUBDOMAIN,\n localPort: actualPort\n })\n if (tunnelActive && !env.BACKEND_URL) {\n process.env['BACKEND_URL'] = getTunnelUrl(env.FRPC_SUBDOMAIN, env.FRPC_SERVER)\n }\n }\n\n const baseUrl = env.BACKEND_URL || `http://localhost:${actualPort}`\n logger.debug({ libPath: getLibPath(), projectPath: getProjectPath() }, 'Paths')\n const urls: Record<string, string> = { api: `${baseUrl}/api/v1` }\n if (resolved.ui.enabled) urls['ui'] = baseUrl\n logger.info({ port: actualPort, mode: resolved.nodeEnv, ...urls }, 'Server started')\n nexusEvents.emitEvent('server.started', { port: actualPort, host: resolved.host })\n\n // Ejecutar onReady hook\n if (config?.onReady) {\n try {\n await config.onReady(app, actualPort, resolved.host)\n logger.debug('onReady hook executed')\n } catch (err) {\n logger.error({ err }, 'Error in onReady hook')\n }\n }\n\n resolve(server!)\n })\n\n // Handle server-level errors (EADDRINUSE after bind, socket errors, etc.)\n // Registered immediately after listen() so errors during bind are caught\n server.on('error', (err) => {\n logger.fatal({ err }, 'HTTP server error')\n captureExceptionSafe(err, { scope: 'server', action: 'http-error' })\n })\n })\n}\n\n/**\n * Stops the Nexus server gracefully.\n *\n * Closes HTTP server, Socket.IO, database connections, and cleans up all state.\n * Calls `beforeClose` hook if registered. Safe to call multiple times.\n *\n * @example\n * ```typescript\n * await stop() // server stopped, port freed\n * ```\n */\nexport async function stop(): Promise<void> {\n // Siempre limpiar estado aunque el servidor no esté corriendo\n // Esto permite llamar stop() múltiples veces sin problemas\n if (!server) {\n await destroyDb()\n resetConfig()\n resetStore()\n await resetSharedAdapters()\n resetConfigCache()\n clearCustomCaslRules()\n clearSeedPermissions()\n await resetServeSPA()\n return\n }\n\n // Ejecutar beforeClose hook\n if (currentConfig?.beforeClose) {\n try {\n await currentConfig.beforeClose()\n logger.debug('beforeClose hook executed')\n } catch (err) {\n logger.error({ err }, 'Error in beforeClose hook')\n }\n }\n\n stopTunnel()\n\n nexusEvents.emitEvent('server.stopping')\n\n // Limpiar cache manager y event bridge\n await resetCacheManager()\n eventBridge.destroy()\n\n // Cerrar Socket.IO antes del servidor HTTP\n closeSocketIO()\n\n // Forzar cierre de conexiones HTTP activas (Node.js 18.2+)\n // Esto evita que el shutdown quede esperando conexiones keep-alive\n if (typeof server!.closeAllConnections === 'function') {\n server!.closeAllConnections()\n }\n\n await new Promise<void>((resolve, reject) => {\n server!.close((err) => {\n if (err) reject(err)\n else resolve()\n })\n })\n\n // Shutdown OpenTelemetry (flush pending telemetry)\n const { shutdownTelemetry } = await import('../instrumentation.js')\n await shutdownTelemetry()\n\n await destroyDb()\n nexusEvents.emitEvent('db.disconnected')\n\n resetConfig()\n resetStore()\n await resetSharedAdapters()\n resetConfigCache()\n clearCustomCaslRules()\n clearSeedPermissions()\n await resetServeSPA()\n currentConfig = undefined\n server = null\n nexusEvents.emitEvent('server.stopped')\n logger.info('Server stopped')\n}\n\n/**\n * Restarts the Nexus server. Calls `stop()` then `start()`.\n *\n * If no config is provided, reuses the config from the previous `start()` call.\n * Plugins, hooks, and SPAs are preserved across restarts.\n *\n * @param config - Optional new config. If omitted, reuses previous config.\n * @returns The new `http.Server` instance\n */\nexport async function restart(config?: StartOptions): Promise<http.Server> {\n const configToUse = config ?? currentConfig\n\n nexusEvents.emitEvent('server.restarting')\n await stop()\n\n // start() registra plugins desde configToUse.plugins\n return start(configToUse)\n}\n\n/** Returns `true` if the server is currently running. */\nexport function isRunning(): boolean {\n return server !== null\n}\n\n// Graceful shutdown handlers\nfunction setupGracefulShutdown() {\n if (gracefulShutdownRegistered) return\n gracefulShutdownRegistered = true\n\n let shuttingDown = false\n\n const shutdown = async (signal: string) => {\n if (shuttingDown) return\n shuttingDown = true\n\n logger.info({ signal }, 'Graceful shutdown initiated')\n\n try {\n await stop()\n process.exit(0)\n } catch (err) {\n logger.fatal({ err }, 'Error during shutdown')\n captureExceptionSafe(err instanceof Error ? err : new Error(String(err)), { scope: 'server', action: 'shutdown' })\n process.exit(1)\n }\n }\n\n process.on('SIGTERM', () => shutdown('SIGTERM'))\n process.on('SIGINT', () => shutdown('SIGINT'))\n\n process.on('uncaughtException', (err) => {\n logger.fatal({ err }, 'Uncaught exception — initiating shutdown')\n captureExceptionSafe(err, { scope: 'process', action: 'uncaughtException' })\n shutdown('uncaughtException')\n })\n\n process.on('unhandledRejection', (reason) => {\n logger.fatal({ err: reason }, 'Unhandled rejection — initiating shutdown')\n captureExceptionSafe(reason instanceof Error ? reason : new Error(String(reason)), { scope: 'process', action: 'unhandledRejection' })\n shutdown('unhandledRejection')\n })\n}\n\nsetupGracefulShutdown()\n","/**\n * Room naming utilities for Socket.IO real-time entities.\n *\n * Delegates to SDK entityRoom() for building room names.\n * Provides parsing utilities used only by the backend.\n */\n\n// Re-export SDK helper as the canonical room builder\nexport { entityRoom } from '@gzl10/nexus-sdk'\n\n/**\n * Checks whether a room name is a tenant-scoped entity room.\n * Format: `entity:{tenantId}:{module}.{entity}`\n */\nexport function isEntityRoom(room: string): boolean {\n return room.startsWith('entity:')\n}\n\n/**\n * Parses tenantId, module and entity from a room name.\n * Returns null if room is not an entity room or is malformed.\n */\nexport function parseEntityRoom(room: string): { tenantId: string; module: string; entity: string } | null {\n if (!isEntityRoom(room)) return null\n // entity:{tenantId}:{module}.{entity}\n const rest = room.slice(7) // remove 'entity:'\n const colonIndex = rest.indexOf(':')\n if (colonIndex === -1 || colonIndex === 0) return null\n const tenantId = rest.slice(0, colonIndex)\n const key = rest.slice(colonIndex + 1)\n const dotIndex = key.indexOf('.')\n if (dotIndex === -1 || dotIndex === 0 || dotIndex === key.length - 1) return null\n return { tenantId, module: key.slice(0, dotIndex), entity: key.slice(dotIndex + 1) }\n}\n","import type { Request, Response, NextFunction } from 'express'\nimport { ForbiddenError as CASLForbiddenError } from '@casl/ability'\nimport { ForbiddenError } from '../errors/app-error.js'\nimport type { Actions, Subjects } from '../abilities/ability.types.js'\n\nexport function checkAbility(action: Actions, subject: Subjects) {\n return (req: Request, _res: Response, next: NextFunction) => {\n if (!req.ability) {\n throw new ForbiddenError('No se han definido permisos')\n }\n\n try {\n CASLForbiddenError.from(req.ability).throwUnlessCan(action, subject)\n next()\n } catch {\n throw new ForbiddenError(`No tienes permiso para ${action} ${typeof subject === 'string' ? subject : 'este recurso'}`)\n }\n }\n}\n","import type { Request, RequestHandler } from 'express'\nimport rateLimit, { type Options } from 'express-rate-limit'\n\nexport interface RateLimitOptions {\n /** Time window in milliseconds (default: 60000 = 1 minute) */\n windowMs?: number\n /** Max requests per window (default: 100) */\n max?: number\n /** Message when limit is reached */\n message?: string\n /** Custom key generator (default: IP-based) */\n keyGenerator?: (req: Request) => string\n /** Don't count successful requests (2xx) toward the limit */\n skipSuccessfulRequests?: boolean\n}\n\nconst noopRateLimit: RequestHandler = (_req, _res, next) => next()\n\n/**\n * Creates a rate limit middleware.\n * Automatically disabled in NODE_ENV=test (returns no-op).\n *\n * @example\n * // 100 requests per minute (default)\n * router.use(createRateLimit())\n *\n * @example\n * // 5 requests per 15 minutes for login with user-based key\n * router.post('/login', createRateLimit({\n * windowMs: 15 * 60 * 1000,\n * max: 5,\n * skipSuccessfulRequests: true,\n * keyGenerator: (req) => `${req.ip}:${req.body?.username}`\n * }), handler)\n */\nexport function createRateLimit(options: RateLimitOptions = {}) {\n if (process.env['NODE_ENV'] === 'test') return noopRateLimit\n\n const {\n windowMs = 60 * 1000,\n max = 100,\n message = 'Too many requests, please try again later',\n keyGenerator,\n skipSuccessfulRequests\n } = options\n\n return rateLimit({\n windowMs,\n max,\n message: { error: message },\n standardHeaders: 'draft-7',\n legacyHeaders: false,\n validate: { trustProxy: false, ...( keyGenerator && { keyGeneratorIpFallback: false }) },\n ...(keyGenerator && { keyGenerator }),\n ...(skipSuccessfulRequests !== undefined && { skipSuccessfulRequests })\n } satisfies Partial<Options>)\n}\n","import type { Request, Response, NextFunction } from 'express'\nimport type { ValidateSchemas } from '@gzl10/nexus-sdk'\n\n// Extender Request para datos validados (Express 5: query/params son readonly)\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace Express {\n interface Request {\n validated?: {\n query?: unknown\n params?: unknown\n }\n }\n }\n}\n\nexport function validate(schemas: ValidateSchemas) {\n return (req: Request, _res: Response, next: NextFunction) => {\n req.validated = {}\n\n if (schemas.body) {\n req.body = schemas.body.parse(req.body)\n }\n if (schemas.query) {\n req.validated.query = schemas.query.parse(req.query)\n }\n if (schemas.params) {\n req.validated.params = schemas.params.parse(req.params)\n }\n next()\n }\n}\n","import type { Request, Response, NextFunction, RequestHandler } from 'express'\nimport { logger } from '../logger/index.js'\n\n/**\n * Per-route request timeout middleware.\n * Sets req.setTimeout and responds 408 if exceeded.\n *\n * @param ms - Timeout in milliseconds\n */\nexport function requestTimeout(ms: number): RequestHandler {\n return (req: Request, res: Response, next: NextFunction) => {\n req.setTimeout(ms, () => {\n if (!res.headersSent) {\n logger.warn({ method: req.method, url: req.originalUrl, timeoutMs: ms }, 'Request timeout exceeded')\n res.status(408).json({\n error: { code: 'REQUEST_TIMEOUT', message: `Request timeout after ${ms}ms` }\n })\n }\n })\n next()\n }\n}\n","import bcrypt from 'bcryptjs'\n\nconst SALT_ROUNDS = 10\n\n/**\n * Dummy hash for timing-safe login.\n * Used when the user does not exist to avoid timing attacks.\n */\nexport const DUMMY_HASH = '$2a$10$N9qo8uLOickgx2ZMRZoMyeUv9rT7dRVqTMtKYzOYQVxZi1qU2cFaW'\n\n/**\n * Hashes a password with bcrypt\n */\nexport function hashPassword(password: string): Promise<string> {\n return bcrypt.hash(password, SALT_ROUNDS)\n}\n\n/**\n * Verifies a password against its hash\n */\nexport function verifyPassword(password: string, hash: string): Promise<boolean> {\n return bcrypt.compare(password, hash)\n}\n","/**\n * Symmetric encryption utilities (AES-256-GCM).\n * Key derived from AUTH_SECRET via HKDF (RFC 5869).\n * Format: base64(iv):base64(authTag):base64(ciphertext)\n */\n\nimport { createCipheriv, createDecipheriv, randomBytes, hkdfSync } from 'node:crypto'\n\nconst ALGORITHM = 'aes-256-gcm'\nconst IV_LENGTH = 12\nconst KEY_LENGTH = 32\n/** Domain separation info string — ensures this key is unique to Nexus symmetric encryption */\nconst HKDF_INFO = 'nexus-symmetric-encryption-v1'\n\nlet derivedKey: Buffer | null = null\n\nfunction getKey(): Buffer {\n if (derivedKey) return derivedKey\n const secret = process.env['AUTH_SECRET']\n if (!secret) {\n throw new Error('AUTH_SECRET not configured. Required for symmetric encryption.')\n }\n // HKDF: extract-then-expand. Empty salt is valid per RFC 5869 §3.1\n // (uses hash-length zero bytes). Info string provides domain separation.\n const keyBytes = hkdfSync('sha256', secret, '', HKDF_INFO, KEY_LENGTH)\n derivedKey = Buffer.from(keyBytes)\n return derivedKey\n}\n\n/**\n * Encrypts plaintext using AES-256-GCM.\n * Returns format: base64(iv):base64(authTag):base64(ciphertext)\n */\nexport function encrypt(plaintext: string): string {\n const key = getKey()\n const iv = randomBytes(IV_LENGTH)\n const cipher = createCipheriv(ALGORITHM, key, iv)\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])\n const authTag = cipher.getAuthTag()\n return `${iv.toString('base64')}:${authTag.toString('base64')}:${encrypted.toString('base64')}`\n}\n\n/**\n * Decrypts ciphertext produced by encrypt().\n */\nexport function decrypt(ciphertext: string): string {\n const key = getKey()\n const parts = ciphertext.split(':')\n if (parts.length !== 3) {\n throw new Error('Invalid ciphertext format: expected base64(iv):base64(authTag):base64(data)')\n }\n const [ivB64, tagB64, dataB64] = parts\n const iv = Buffer.from(ivB64!, 'base64')\n const authTag = Buffer.from(tagB64!, 'base64')\n const data = Buffer.from(dataB64!, 'base64')\n const decipher = createDecipheriv(ALGORITHM, key, iv)\n decipher.setAuthTag(authTag)\n return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8')\n}\n\n/**\n * Resets derived key cache (for testing only).\n */\nexport function _resetKey(): void {\n derivedKey = null\n}\n","/**\n * Crypto utilities - shared across modules\n */\nexport { hashPassword, verifyPassword, DUMMY_HASH } from './hash.js'\nexport { encrypt, decrypt } from './symmetric.js'\n","/**\n * Error Handler Utility\n * Standardizes error logging and handling across the application\n */\n\nimport type { Logger } from 'pino'\n\n\n\n/**\n * Safely parses JSON with error logging\n *\n * @param logger - Pino logger instance\n * @param jsonString - JSON string to parse\n * @param fallback - Fallback value if parsing fails\n * @param context - Context for logging\n * @returns Parsed object or fallback\n *\n * @example\n * ```typescript\n * const userIds = safeJsonParse(ctx.logger, jsonString, [], { field: 'user_ids' })\n * ```\n */\nexport function safeJsonParse<T>(\n logger: Logger,\n jsonString: string,\n fallback: T,\n context?: Record<string, unknown>\n): T {\n try {\n return JSON.parse(jsonString) as T\n } catch {\n logger.warn({ ...context, jsonString }, 'Failed to parse JSON, using fallback')\n return fallback\n }\n}\n","/**\n * @module core/cache\n * @description In-memory caching utilities\n */\n\nexport { LRUCache, type LRUCacheOptions } from './lru-cache.js'\nexport type { CacheStats } from '@gzl10/nexus-sdk'\nexport { ManagedCacheImpl } from './managed-cache.js'\nexport { CacheManager, type CacheManagerOptions } from './cache-manager.js'\nexport { ScopedCacheManagerImpl } from './scoped-cache-manager.js'\nexport { RedisManagedCache } from './redis-managed-cache.js'\n","/**\n * @module core\n * @description Express app, middleware, abilities, events, and utilities\n *\n * @dependencies\n * - db/ (connection, seed-runner, ensure-system-tables)\n * - engine/ (loader, getOrderedModules, context, registry)\n * - runtime/ (cache, entity-factory)\n *\n * @note This is the orchestration layer that wires together all other modules.\n * It depends on db/, engine/, and runtime/ to start the server.\n */\n\n// Server lifecycle\nexport { start, stop, restart, isRunning } from './server.js'\nexport type { NexusConfig } from './server.js'\nexport { createApp, createRouter } from './app.js'\n\n// Logger\nexport { logger, createChildLogger, setLoggerInstance } from './logger/index.js'\n\n// Events Hub\nexport { nexusEvents } from './events-hub/emitter.js'\nexport type { NexusEvents, NexusEventName, NexusEventPayload, DbEventPayload } from './events-hub/emitter.js'\nexport { eventBridge } from './events-hub/event-bridge.js'\nexport { entityRoom, isEntityRoom, parseEntityRoom } from './events-hub/room-utils.js'\n\n// Errors\nexport {\n AppError,\n ValidationError,\n NotFoundError,\n UnauthorizedError,\n ForbiddenError,\n ConflictError\n} from './errors/app-error.js'\nexport { ErrorCodes, type ErrorCode } from './errors/error-codes.js'\n\n// Abilities (CASL)\nexport { defineAbilityFor, packRules, unpackRules } from './abilities/ability.factory.js'\nexport type { AppAbility, Actions, Subjects, SubjectStrings, SubjectRegistry } from './abilities/ability.types.js'\n\n// Middleware\nexport { checkAbility } from './middleware/ability.middleware.js'\nexport { createRateLimit, type RateLimitOptions } from './middleware/rate-limit.middleware.js'\nexport { validate } from './middleware/validate.middleware.js'\nexport { errorMiddleware, initErrorMiddleware, type CaptureExceptionFn } from './middleware/error.middleware.js'\nexport { requestTimeout } from './middleware/timeout.middleware.js'\n\n// Crypto\nexport { hashPassword, verifyPassword, DUMMY_HASH, encrypt, decrypt } from './crypto/index.js'\n\n// Socket.IO\nexport {\n getIO,\n initSocketIO,\n closeSocketIO,\n isSocketIOInitialized,\n emitToUser,\n emitToRoom,\n emitToRole,\n emitToAll,\n emitToAuthenticated,\n broadcastToRoom,\n getConnectedUsers,\n isUserConnected,\n getUserSocketCount,\n joinUserToRoom,\n removeUserFromRoom,\n joinSocketToRoom,\n removeSocketFromRoom,\n getRoomMembers,\n getRoomSize,\n getUserRooms,\n roomExists,\n onSocketReady\n} from './events-hub/socket.js'\n\n// SSE\nexport { createSSEHelper } from './sse/index.js'\n\n// Utils\nexport { generateId, generateIdByType, type IdType } from './utils/id.js'\nexport {\n generatePatternId,\n getNextSequence,\n ensureSequencesTable,\n SEQUENCES_TABLE,\n type PatternConfig\n} from './utils/sequence.js'\nexport { getLibPath, getProjectPath, findEnvFile } from '../config/paths.js'\nexport { safeJsonParse } from './utils/safe-json.js'\n\n// SPA Handler\nexport { createServeSPA, resetServeSPA, resetServeSPAEndpoints } from './spa-handler.js'\n\n// Cache\nexport { LRUCache, type LRUCacheOptions, type CacheStats } from './cache/index.js'\n\n// JWT (verification only - generation is in modules/auth/)\nexport { verifyAccessToken, type JwtPayload } from './jwt/index.js'\n\n// OpenAPI\nexport { generateOpenAPISpec, type OpenAPIConfig } from './openapi/index.js'\nexport {\n entityToSchema,\n entityToCreateSchema,\n entityToUpdateSchema,\n actionToInputSchema,\n fieldsToSchema\n} from './openapi/index.js'\nexport {\n collectionToPaths,\n singleToPaths,\n configToPaths,\n eventToPaths,\n actionToPaths,\n customRouteToPaths,\n entityToPaths\n} from './openapi/index.js'\n","import type { Knex } from 'knex'\nimport { logger } from '../core/index.js'\n\n/**\n * Get the database client type\n * Returns 'sqlite' as default when client type cannot be determined (e.g., in mocks)\n */\nexport function getDbClient(db: Knex): 'postgres' | 'mysql' | 'sqlite' {\n const client = (db.client?.config?.client ?? 'sqlite') as string\n if (client === 'pg' || client === 'postgresql') return 'postgres'\n if (client === 'mysql' || client === 'mysql2') return 'mysql'\n return 'sqlite'\n}\n\n/**\n * Format timestamp for database insert/update\n * - PostgreSQL: accepts ISO strings directly\n * - MySQL: requires 'YYYY-MM-DD HH:MM:SS' format\n * - SQLite: accepts ISO strings (stored as TEXT)\n *\n * @param db Knex instance to detect driver\n * @param date Date object (defaults to now)\n * @returns Formatted timestamp string\n */\nexport function formatTimestamp(db: Knex, date: Date = new Date()): string {\n const client = getDbClient(db)\n\n if (client === 'mysql') {\n // MySQL requires YYYY-MM-DD HH:MM:SS format\n return date.toISOString().slice(0, 19).replace('T', ' ')\n }\n\n // PostgreSQL and SQLite accept ISO strings\n return date.toISOString()\n}\n\n/**\n * Get current timestamp formatted for the database\n */\nexport function nowTimestamp(db: Knex): string {\n return formatTimestamp(db, new Date())\n}\n\n/**\n * Adds timestamp fields to a table during creation.\n * - PostgreSQL: uses timestamptz (timestamp with time zone)\n * - MySQL: uses standard timestamp\n * - SQLite: uses TEXT with datetime('now') for ISO string consistency\n */\nexport function addTimestamps(table: Knex.CreateTableBuilder, db: Knex) {\n const client = (db.client?.config?.client ?? 'sqlite3') as string\n const isPostgres = client === 'pg' || client === 'postgresql'\n const isSqlite = client === 'better-sqlite3' || client === 'sqlite3' || client === 'sqlite'\n\n if (isPostgres) {\n // PostgreSQL: usar timestamp with time zone\n table.timestamp('created_at', { useTz: true }).defaultTo(db.fn.now())\n table.timestamp('updated_at', { useTz: true }).defaultTo(db.fn.now())\n } else if (isSqlite) {\n // SQLite: usar datetime('now') que genera strings ISO directamente\n // Nota: SQLite requiere paréntesis extra para funciones en DEFAULT\n table.text('created_at').defaultTo(db.raw(\"(datetime('now'))\"))\n table.text('updated_at').defaultTo(db.raw(\"(datetime('now'))\"))\n } else {\n // MySQL: timestamp estándar\n table.timestamp('created_at').defaultTo(db.fn.now())\n table.timestamp('updated_at').defaultTo(db.fn.now())\n }\n}\n\n/**\n * Adds audit fields (created_by, updated_by) to an existing table if missing.\n * Note: SQLite does not support ALTER TABLE ADD COLUMN with a non-constant default.\n */\nexport async function addAuditFieldsIfMissing(db: Knex, tableName: string): Promise<void> {\n if (!(await db.schema.hasColumn(tableName, 'created_by'))) {\n await db.schema.alterTable(tableName, (table) => {\n // Sin FK para evitar dependencias circulares entre módulos\n table.string('created_by').nullable()\n table.string('updated_by').nullable()\n })\n logger.info(`Added audit fields to: ${tableName}`)\n }\n}\n\n/**\n * Adds is_default field to config tables.\n * Allows marking which configuration is the default at runtime.\n */\nexport async function addConfigDefaultField(db: Knex, tableName: string): Promise<void> {\n if (!(await db.schema.hasColumn(tableName, 'is_default'))) {\n await db.schema.alterTable(tableName, (table) => {\n table.boolean('is_default').defaultTo(false).nullable()\n })\n logger.info(`Added is_default field to config table: ${tableName}`)\n }\n}\n\n/**\n * Adds deleted_at field for soft delete support.\n * Called when entity has softDelete: true.\n */\nexport async function addSoftDeleteFieldIfMissing(db: Knex, tableName: string): Promise<void> {\n if (!(await db.schema.hasColumn(tableName, 'deleted_at'))) {\n await db.schema.alterTable(tableName, (table) => {\n table.datetime('deleted_at').nullable()\n })\n logger.info(`Added soft delete field to: ${tableName}`)\n }\n}\n\n/**\n * Adds a column if it does not exist\n */\nexport async function addColumnIfMissing(\n db: Knex,\n tableName: string,\n columnName: string,\n columnBuilder: (table: Knex.AlterTableBuilder) => void\n): Promise<boolean> {\n if (!(await db.schema.hasColumn(tableName, columnName))) {\n await db.schema.alterTable(tableName, columnBuilder)\n logger.info(`Added column: ${tableName}.${columnName}`)\n return true\n }\n return false\n}\n","/**\n * @module migration-helpers\n * @description Migration utilities for Nexus plugins.\n * Import from '@gzl10/nexus-backend/migrations' in plugin tests.\n */\n\nexport { runGeneratedMigration, runAllGeneratedMigrations } from '../db/migration-engine.js'\n\nexport {\n addTimestamps,\n addAuditFieldsIfMissing,\n addSoftDeleteFieldIfMissing,\n addColumnIfMissing,\n addConfigDefaultField,\n nowTimestamp,\n formatTimestamp,\n getDbClient\n} from '../db/schema-helpers.js'\n"],"mappings":"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","names":["db","resolveLocalized","dirname","join","join","readFileSync","require","pino","createRequire","loggerInstance","isDev","config","init_logger","useTextField","init_registry","useSelectField","type","useTextField","type","init_registry","join","existsSync","useTextField","useSelectField","useNumberField","cpus","useIdField","useTextField","useNumberField","useSelectField","useDatetimeField","useTextareaField","useTextField","useSelectField","useCheckboxField","useTextField","useSelectField","useSelectField","useSwitchField","useSelectField","useSwitchField","existsSync","mkdirSync","join","dirname","createHash","extname","basename","dirname","join","existsSync","mkdirSync","basename","createHash","useIdField","useTextField","useSelectField","useNumberField","useCheckboxField","useJsonField","DEFAULT_MAX_SIZE","multer","useIdField","useSelectField","useTextField","useDatetimeField","useCheckboxField","useImageField","useNameField","useMetadataField","useDescriptionField","z","verifyPassword","hashPassword","z","useIdField","useTextField","useSelectField","useDatetimeField","useEmailField","useMetadataField","z","useIdField","useTextField","useSelectField","useDatetimeField","useExpiresAtField","init_helpers","init_helpers","init_helpers","init_helpers","init_helpers","init_helpers","init_helpers","init_helpers","init_helpers","init_helpers","init_helpers","init_helpers","init_helpers","init_actions","jwt","createHash","z","init_actions","z","join","useIdField","useTextField","useSelectField","useNumberField","useSwitchField","useEmailField","usePasswordField","useTextareaField","useTagsField","useDatetimeField","nodemailer","config","subject","z","env","useTextField","useSelectField","useCheckboxField","existsSync","useIdField","useTextField","useSelectField","useTextareaField","useJsonField","useDatetimeField","useEmailField","init_logger","type","registerBooleanColumn","convertBooleans","init_helpers","resolveLocalized","init_helpers","resolveLocalized","init_helpers","resolveLocalized","entityRoom","entityRoom","z","init_schema_builder","init_schema_builder","resolveLocalized","init_helpers","resolveLocalized","resolveLocalized","init_helpers","existsSync","join","z","join","dirname","isAbsolute","mkdirSync","table","path","fs","readFileSync","mkdirSync","knex","Redis","resolveLocalized","jwt","ZodError","CASLForbiddenError","server","express","jwt","DEFAULT_TENANT_ID","entityRoom","writeFileSync","unlinkSync","join","entityRoom","DEFAULT_LOCALES","shutdownTelemetry","entityRoom","CASLForbiddenError","randomBytes","db"]}
|