@stratal/framework 0.0.26 → 0.1.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +596 -0
  2. package/README.md +166 -24
  3. package/dist/access-control/index.d.mts +43 -16
  4. package/dist/access-control/index.d.mts.map +1 -1
  5. package/dist/access-control/index.mjs +5 -5
  6. package/dist/access-control/index.mjs.map +1 -1
  7. package/dist/{access.service-BmDhE-re.mjs → access.service-BjmnWBEo.mjs} +35 -17
  8. package/dist/access.service-BjmnWBEo.mjs.map +1 -0
  9. package/dist/auth/index.d.mts +105 -103
  10. package/dist/auth/index.d.mts.map +1 -1
  11. package/dist/auth/index.mjs +123 -24
  12. package/dist/auth/index.mjs.map +1 -1
  13. package/dist/{auth-context-CGVbiSX3.d.mts → auth-context-C1om3Zsr.d.mts} +1 -2
  14. package/dist/auth-context-C1om3Zsr.d.mts.map +1 -0
  15. package/dist/{auth-context-C8NBfiMa.mjs → auth-context-cNSS1rmh.mjs} +2 -2
  16. package/dist/{auth-context-C8NBfiMa.mjs.map → auth-context-cNSS1rmh.mjs.map} +1 -1
  17. package/dist/auth.service-Onf3JkyL.d.mts +44 -0
  18. package/dist/auth.service-Onf3JkyL.d.mts.map +1 -0
  19. package/dist/context/index.d.mts +4 -5
  20. package/dist/context/index.d.mts.map +1 -1
  21. package/dist/context/index.mjs +1 -1
  22. package/dist/database/index.d.mts +3 -3
  23. package/dist/database/index.mjs +413 -34
  24. package/dist/database/index.mjs.map +1 -1
  25. package/dist/{decorate-B7nr7eBl.mjs → decorate-RQD1h28J.mjs} +1 -1
  26. package/dist/{decorateParam-DwV9LSPl.mjs → decorateParam-xwTkq9gO.mjs} +2 -2
  27. package/dist/{decorateParam-DwV9LSPl.mjs.map → decorateParam-xwTkq9gO.mjs.map} +1 -1
  28. package/dist/factory/index.d.mts +3 -5
  29. package/dist/factory/index.d.mts.map +1 -1
  30. package/dist/factory/index.mjs.map +1 -1
  31. package/dist/guards/index.d.mts +3 -4
  32. package/dist/guards/index.d.mts.map +1 -1
  33. package/dist/guards/index.mjs +4 -4
  34. package/dist/guards/index.mjs.map +1 -1
  35. package/dist/index-e_u1SRyd.d.mts +921 -0
  36. package/dist/index-e_u1SRyd.d.mts.map +1 -0
  37. package/dist/index.d.mts +1 -1
  38. package/dist/{types-CWZ9q74G.d.mts → types-B35g-lXi.d.mts} +18 -4
  39. package/dist/types-B35g-lXi.d.mts.map +1 -0
  40. package/package.json +31 -27
  41. package/dist/access.service-BmDhE-re.mjs.map +0 -1
  42. package/dist/auth-context-CGVbiSX3.d.mts.map +0 -1
  43. package/dist/index-Dt0YUA7r.d.mts +0 -446
  44. package/dist/index-Dt0YUA7r.d.mts.map +0 -1
  45. package/dist/types-CWZ9q74G.d.mts.map +0 -1
  46. package/dist/types-DabF8LGz.d.mts +0 -11
  47. package/dist/types-DabF8LGz.d.mts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/database/commands/zenstack.command.ts","../../src/database/commands/db-generate.command.ts","../../src/database/commands/db-pull.command.ts","../../src/database/commands/db-push.command.ts","../../src/database/commands/migrate-deploy.command.ts","../../src/database/commands/migrate-dev.command.ts","../../src/database/commands/migrate-reset.command.ts","../../src/database/commands/migrate-status.command.ts","../../src/database/errors/record-not-found.error.ts","../../src/database/errors/unique-constraint.error.ts","../../src/database/errors/from-zenstack-error.ts","../../src/database/plugins/error-handler.plugin.ts","../../src/database/plugins/event-emitter.plugin.ts","../../src/database/plugins/schema-switcher.ts","../../src/database/database.helpers.ts","../../src/database/database.tokens.ts","../../src/database/i18n/en.ts","../../src/database/database.module.ts","../../src/database/decorators/inject-db.decorator.ts"],"sourcesContent":["import { Command } from 'stratal/quarry'\n\n/**\n * Base command for ZenStack CLI wrappers.\n * Uses execFileSync with array arguments to prevent shell injection.\n */\nexport abstract class ZenStackCommand extends Command {\n protected async zenstack(args: string[]): Promise<number> {\n // Dynamic import — node:child_process is only available in the Quarry CLI (Node) context\n const { execFileSync } = await import('node:child_process')\n\n try {\n const output = execFileSync('npx', ['zenstack', ...args], {\n encoding: 'utf-8',\n stdio: 'pipe',\n })\n if (output) this.info(output.trim())\n return 0\n } catch (err) {\n const error = err as { stderr?: string; stdout?: string; status?: number }\n if (error.stderr) this.error(error.stderr.trim())\n if (error.stdout) this.info(error.stdout.trim())\n return error.status ?? 1\n }\n }\n}\n","import { ZenStackCommand } from './zenstack.command'\n\nexport class DbGenerateCommand extends ZenStackCommand {\n static command = 'db:generate {--schema= : Path to schema file} {--watch : Enable watch mode}'\n static description = 'Generate ZenStack ORM client'\n\n async handle(): Promise<number> {\n const args = ['generate']\n const schema = this.string('schema')\n\n if (schema) args.push('--schema', schema)\n if (this.boolean('watch')) args.push('--watch')\n\n return this.zenstack(args)\n }\n}\n","import { ZenStackCommand } from './zenstack.command'\n\nexport class DbPullCommand extends ZenStackCommand {\n static command = 'db:pull {--schema= : Path to schema file}'\n static description = 'Introspect database and generate schema'\n\n async handle(): Promise<number> {\n const args = ['db', 'pull']\n const schema = this.string('schema')\n\n if (schema) args.push('--schema', schema)\n\n return this.zenstack(args)\n }\n}\n","import { ZenStackCommand } from './zenstack.command'\n\nexport class DbPushCommand extends ZenStackCommand {\n static command = 'db:push {--schema= : Path to schema file} {--accept-data-loss : Accept data loss} {--force-reset : Force reset database}'\n static description = 'Push database schema changes'\n\n async handle(): Promise<number> {\n const args = ['db', 'push']\n const schema = this.string('schema')\n\n if (schema) args.push('--schema', schema)\n if (this.boolean('accept-data-loss')) args.push('--accept-data-loss')\n if (this.boolean('force-reset')) args.push('--force-reset')\n\n return this.zenstack(args)\n }\n}\n","import { ZenStackCommand } from './zenstack.command'\n\nexport class MigrateDeployCommand extends ZenStackCommand {\n static command = 'migrate:deploy {--schema= : Path to schema file}'\n static description = 'Deploy pending migrations'\n\n async handle(): Promise<number> {\n const args = ['migrate', 'deploy']\n const schema = this.string('schema')\n\n if (schema) args.push('--schema', schema)\n\n return this.zenstack(args)\n }\n}\n","import { ZenStackCommand } from './zenstack.command'\n\nexport class MigrateDevCommand extends ZenStackCommand {\n static command = 'migrate:dev {--schema= : Path to schema file} {--name= : Migration name} {--create-only : Create without applying}'\n static description = 'Create and apply migration'\n\n async handle(): Promise<number> {\n const args = ['migrate', 'dev']\n const schema = this.string('schema')\n const name = this.string('name')\n\n if (schema) args.push('--schema', schema)\n if (name) args.push('--name', name)\n if (this.boolean('create-only')) args.push('--create-only')\n\n return this.zenstack(args)\n }\n}\n","import { ZenStackCommand } from './zenstack.command'\n\nexport class MigrateResetCommand extends ZenStackCommand {\n static command = 'migrate:reset {--schema= : Path to schema file} {--force : Skip confirmation} {--skip-seed : Skip seeding}'\n static description = 'Reset database'\n\n async handle(): Promise<number> {\n const args = ['migrate', 'reset']\n const schema = this.string('schema')\n\n if (schema) args.push('--schema', schema)\n if (this.boolean('force')) args.push('--force')\n if (this.boolean('skip-seed')) args.push('--skip-seed')\n\n return this.zenstack(args)\n }\n}\n","import { ZenStackCommand } from './zenstack.command'\n\nexport class MigrateStatusCommand extends ZenStackCommand {\n static command = 'migrate:status {--schema= : Path to schema file}'\n static description = 'Check migration status'\n\n async handle(): Promise<number> {\n const args = ['migrate', 'status']\n const schema = this.string('schema')\n\n if (schema) args.push('--schema', schema)\n\n return this.zenstack(args)\n }\n}\n","import { HttpException } from 'stratal/errors'\n\nexport class RecordNotFoundError extends HttpException {\n constructor(public readonly details?: string, cause?: unknown) {\n super(404, 'Record not found', cause)\n }\n}\n","import { HttpException } from 'stratal/errors'\n\nexport class UniqueConstraintError extends HttpException {\n constructor(public readonly fields?: string[], cause?: unknown) {\n super(409, 'Record already exists', cause)\n }\n}\n","import { ORMError, ORMErrorReason } from '@zenstackhq/orm';\nimport { type ApplicationError, DatabaseError } from 'stratal/errors';\nimport { RecordNotFoundError } from './record-not-found.error';\nimport { UniqueConstraintError } from './unique-constraint.error';\n\nexport function fromZenStackError(error: unknown): ApplicationError {\n if (error instanceof ORMError) {\n switch (error.reason) {\n case ORMErrorReason.NOT_FOUND:\n return new RecordNotFoundError(error.model, error)\n case ORMErrorReason.DB_QUERY_ERROR:\n return parseDatabaseError(error)\n case ORMErrorReason.INVALID_INPUT:\n return new DatabaseError('Invalid database query', error)\n case ORMErrorReason.CONFIG_ERROR:\n return new DatabaseError('Database configuration error', error)\n case ORMErrorReason.NOT_SUPPORTED:\n return new DatabaseError('Operation not supported', error)\n case ORMErrorReason.INTERNAL_ERROR:\n return new DatabaseError('Database internal error', error)\n default:\n return new DatabaseError('Database error', error)\n }\n }\n return new DatabaseError('Database error', error)\n}\n\nfunction parseDatabaseError(error: ORMError): ApplicationError {\n const dbErrorCode = error.dbErrorCode as string | undefined\n if (dbErrorCode) {\n if (dbErrorCode === '23505') return new UniqueConstraintError([error.model ?? 'unknown'], error)\n if (dbErrorCode === '23503') return new DatabaseError('Foreign key constraint violation', error)\n if (dbErrorCode === '23502') return new DatabaseError('Required field is missing', error)\n if (dbErrorCode === '23514') return new DatabaseError('Database constraint violated', error)\n if (dbErrorCode === '42P01') return new DatabaseError('Table does not exist', error)\n if (dbErrorCode === '42703') return new DatabaseError('Column does not exist', error)\n if (dbErrorCode.startsWith('42')) return new DatabaseError('Database syntax or access error', error)\n if (dbErrorCode.startsWith('08')) return new DatabaseError('Database connection failed', error)\n if (dbErrorCode === '57014') return new DatabaseError('Database query timeout', error)\n if (dbErrorCode.startsWith('40')) return new DatabaseError('Transaction conflict or deadlock', error)\n if (dbErrorCode === '53300') return new DatabaseError('Too many database connections', error)\n }\n return new DatabaseError('Database error', error)\n}\n","import { type RuntimePlugin } from '@zenstackhq/orm'\nimport { type SchemaDef } from '@zenstackhq/orm/schema'\nimport { fromZenStackError } from '../errors'\n\n/**\n * ZenStack runtime plugin that transforms ORM errors into ApplicationError instances.\n *\n * @example\n * ```typescript\n * super(schema, {\n * dialect: new PostgresDialect({ pool }),\n * plugins: [new ErrorHandlerPlugin()]\n * })\n * ```\n */\nexport class ErrorHandlerPlugin implements RuntimePlugin<SchemaDef, Record<string, unknown>, Record<string, unknown>, {}> {\n readonly id = 'error-handler'\n\n onQuery = async ({ args, proceed }: {\n args: Record<string, unknown> | undefined\n proceed: (args: Record<string, unknown> | undefined) => Promise<unknown>\n }): Promise<unknown> => {\n try {\n return await proceed(args)\n } catch (error) {\n throw fromZenStackError(error)\n }\n }\n}\n","import { type EntityMutationHooksDef, type RuntimePlugin } from '@zenstackhq/orm';\nimport { type SchemaDef } from '@zenstackhq/orm/schema';\nimport type { EventContext, EventName, IEventRegistry } from 'stratal/events';\nimport type { ModelName } from '../event-types';\n\nexport interface EventEmitterPluginOptions {\n eventRegistry: IEventRegistry\n}\n\ntype EntityMutationAction = 'create' | 'update' | 'delete'\n\nconst ENTITY_ACTION_VERB = {\n create: 'created',\n update: 'updated',\n delete: 'deleted',\n} as const satisfies Record<EntityMutationAction, string>\n\ntype Entity = Record<string, unknown>\n\n/**\n * Pair before/after entity snapshots for a mutation. Rows are matched by\n * `id` when both sides carry one, falling back to positional pairing.\n */\nfunction pairEntities(\n before: Entity[] | undefined,\n after: Entity[] | undefined\n): { before: Entity | undefined; after: Entity | undefined }[] {\n const primary = after ?? before ?? []\n const counterpart = after ? before : undefined\n const counterpartById = counterpart\n ? new Map(counterpart.filter((c) => c.id !== undefined).map((c) => [c.id, c]))\n : undefined\n\n return primary.map((entity, index) => {\n const match = counterpart\n ? (entity.id !== undefined ? counterpartById?.get(entity.id) : undefined) ?? counterpart[index]\n : undefined\n\n return after\n ? { before: match, after: entity }\n : { before: entity, after: undefined }\n })\n}\n\n/**\n * ZenStack runtime plugin that emits before/after events for database operations.\n *\n * Emits events in the format:\n * - `before.{Model}.{operation}` - Before the database operation\n * - `after.{Model}.{operation}` - After the database operation\n *\n * Additionally emits entity-mutation events carrying full entity snapshots:\n * - `entity.{Model}.created` - `{ after }`\n * - `entity.{Model}.updated` - `{ before, after }`\n * - `entity.{Model}.deleted` - `{ before }`\n *\n * Entity events are listener-driven: the pre-mutation snapshot is only\n * loaded (inside the mutation's transaction) when `hasListeners()` reports\n * a matching subscription, so models nobody observes pay no cost. Note that\n * a wildcard subscription (`entity`) therefore makes every model pay the\n * pre-read — subscribe per model when cost matters.\n *\n * @example\n * ```typescript\n * super(schema, {\n * dialect: new PostgresDialect({ pool }),\n * plugins: [\n * new EventEmitterPlugin({\n * eventRegistry,\n * })\n * ]\n * })\n * ```\n */\nexport class EventEmitterPlugin implements RuntimePlugin<SchemaDef, Record<string, unknown>, Record<string, unknown>, {}> {\n readonly id = 'event-emitter'\n\n constructor(private options: EventEmitterPluginOptions) { }\n\n onEntityMutation: EntityMutationHooksDef<SchemaDef> = {\n // Run after-hooks inside the mutation's transaction boundary so they\n // execute within the caller's async context (AsyncLocalStorage intact):\n // listeners resolve from the live request scope and tenant schema\n // switching still applies. Post-commit hooks would run detached from the\n // request's ALS. Listeners registered `blocking: false` still do their\n // real work outside the transaction via waitUntil.\n runAfterMutationWithinTransaction: true,\n\n beforeEntityMutation: async (args) => {\n // Created rows have no prior state to snapshot\n if (args.action === 'create') return\n\n const event = `entity.${args.model}.${ENTITY_ACTION_VERB[args.action]}` as EventName\n if (!this.options.eventRegistry.hasListeners(event)) return\n\n // Runs inside the mutation's transaction; ZenStack hands the result\n // to afterEntityMutation as `beforeMutationEntities`\n await args.loadBeforeMutationEntities()\n },\n\n afterEntityMutation: async (args) => {\n const { model, action, beforeMutationEntities } = args\n const verb = ENTITY_ACTION_VERB[action]\n const event = `entity.${model}.${verb}` as EventName\n const { eventRegistry } = this.options\n\n if (!eventRegistry.hasListeners(event)) return\n\n const after = action === 'delete' ? undefined : await args.loadAfterMutationEntities()\n\n for (const pair of pairEntities(beforeMutationEntities, after)) {\n // Producer boundary: ZenStack types `model` as plain string, but at\n // runtime it is always a schema model name — assert that one fact.\n const context: EventContext<'entity'> = {\n model: model as ModelName,\n action: verb,\n before: pair.before,\n after: pair.after,\n }\n await eventRegistry.emit(event, context)\n }\n },\n }\n\n onQuery = async ({ model, operation, args, proceed }: {\n model: string\n operation: string\n args: Record<string, unknown> | undefined\n proceed: (args: Record<string, unknown> | undefined) => Promise<unknown>\n }): Promise<unknown> => {\n const { eventRegistry } = this.options\n const eventBase = `${model}.${operation}`\n\n // Emit BEFORE event\n await eventRegistry.emit(`before.${eventBase}` as EventName, {\n data: args,\n })\n\n // Execute the actual database operation\n const result = await proceed(args)\n\n // Emit AFTER event\n await eventRegistry.emit(`after.${eventBase}` as EventName, {\n data: args,\n result,\n })\n\n return result\n }\n}\n","interface SwitchableClient {\n $schema: { provider: { defaultSchema: string } } & Record<string, unknown>\n schema: unknown\n}\n\n/**\n * Switches the active schema on a ZenStack/Kysely database client by mutating\n * `$schema.provider.defaultSchema`. This causes ZenStack's QueryNameMapper to\n * generate fully-qualified table references (e.g. `\"tenant_123\".\"User\"`).\n *\n * Must be called BEFORE any queries are made on the client.\n *\n * Note: The ZenStack RuntimePlugin `onQuery` hook fires after table names are\n * already resolved, so a plugin-based approach cannot set the schema prefix.\n * Direct client mutation is the only supported method.\n */\nexport class SchemaSwitcher {\n static apply<T>(client: T, schemaName: string): T {\n const c = client as unknown as SwitchableClient\n const switched = {\n ...c.$schema,\n provider: { ...c.$schema.provider, defaultSchema: schemaName },\n }\n c.$schema = switched\n c.schema = switched\n return client\n }\n}\n","import { ZenStackClient, type AnyPlugin } from '@zenstackhq/orm';\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport { Transient } from 'stratal/di';\nimport type { IEventRegistry } from 'stratal/events';\nimport { withZodI18n, z } from 'stratal/validation';\nimport type { DatabaseConnectionConfig } from './database.module';\nimport { ErrorHandlerPlugin, EventEmitterPlugin } from './plugins';\n\nconst databaseConnectionSchema = z.object({\n name: z.string().min(1, withZodI18n('database.connectionNameRequired')),\n schema: z.object({}).loose(),\n dialect: z.function(),\n plugins: z.array(z.object({}).loose()).optional(),\n computedFields: z.object({}).loose().optional(),\n})\n\nexport const databaseModuleConfigSchema = z.object({\n default: z.string().min(1, withZodI18n('database.defaultConnectionRequired')),\n connections: z.array(databaseConnectionSchema).min(1, withZodI18n('database.connectionRequired')),\n}).refine(\n (config) => {\n const names = config.connections.map(c => c.name)\n return new Set(names).size === names.length\n },\n withZodI18n('database.duplicateConnections')\n).refine(\n (config) => config.connections.some(c => c.name === config.default),\n withZodI18n('database.defaultConnectionNotFound')\n)\n\ntype ZenStackClientInstance = InstanceType<typeof ZenStackClient>\n\n/**\n * Wrap a ZenStack client so `$transaction` is reentrant: when a transaction is\n * already open on this connection (tracked per-connection via\n * {@link AsyncLocalStorage}), nested calls run within the active transaction's\n * client instead of opening a new one. ZenStack only reuses a connection when\n * `$transaction` is called on a transaction client; callers holding the base\n * client (e.g. the better-auth adapter, which since better-auth 1.6.11 nests\n * transactions to atomically consume verification rows) would otherwise open a\n * fresh transaction. On a small pool (e.g. a Hyperdrive-fronted `max: 1` pg\n * pool) that inner transaction blocks forever waiting for the connection the\n * outer one holds — a deadlock surfacing as a backend stuck `idle in\n * transaction`. Reusing the active client is also the correct semantics: nested\n * transactions form a single atomic unit.\n *\n * ZenStackClient's constructor returns a Proxy (for dynamic model accessors), so\n * a subclass method override is shadowed — hence the proxy wrapper here.\n */\nexport function makeReentrantTransaction<T extends object>(\n client: T,\n activeTransaction: AsyncLocalStorage<ZenStackClientInstance>,\n): T {\n return new Proxy(client, {\n get(target, prop, receiver) {\n // DI disposal contract (stratal `Disposable`): release the underlying\n // pool/socket when the owning container shuts down (e.g. a Vite HMR\n // reload replacing the Application). Handled here because ZenStack's\n // own constructor proxy shadows subclass method definitions.\n if (prop === Symbol.asyncDispose) {\n return () => (target as ZenStackClientInstance).$disconnect()\n }\n if (prop !== '$transaction') {\n // Forward the receiver so getters/methods resolve `this` against the\n // proxy (correct for layered proxies / accessor properties).\n return Reflect.get(target, prop, receiver)\n }\n // Read the original `$transaction` off the target WITHOUT the receiver — a\n // receiver of the proxy would re-enter this trap and recurse infinitely.\n const transaction = Reflect.get(target, prop) as (\n input: unknown,\n options?: unknown,\n ) => unknown\n return (input: unknown, options?: unknown) => {\n const active = activeTransaction.getStore()\n if (active) {\n return typeof input === 'function'\n ? (input as (tx: ZenStackClientInstance) => unknown)(active)\n : (active.$transaction as (i: unknown, o?: unknown) => unknown)(input, options)\n }\n if (typeof input !== 'function') {\n return transaction.call(target, input, options)\n }\n return transaction.call(\n target,\n (tx: ZenStackClientInstance) =>\n activeTransaction.run(tx, () => (input as (t: ZenStackClientInstance) => unknown)(tx)),\n options,\n )\n }\n },\n })\n}\n\nexport interface DatabaseServiceClass {\n new (): InstanceType<typeof ZenStackClient>\n /**\n * Disconnects every still-live client created from this service class.\n * Called by `DatabaseModule.onShutdown` so pools/sockets are released when\n * the Application is torn down (e.g. a Vite HMR reload).\n */\n disposeInstances(): Promise<void>\n}\n\nexport function createDatabaseService(\n conn: DatabaseConnectionConfig,\n eventRegistry: IEventRegistry,\n): DatabaseServiceClass {\n const plugins: AnyPlugin[] = [\n new ErrorHandlerPlugin(),\n new EventEmitterPlugin({\n eventRegistry,\n }),\n ...(conn.plugins ?? []),\n ]\n\n // Tracks the in-flight interactive transaction client for this connection so\n // nested `$transaction` calls reuse it instead of acquiring a second\n // connection. ZenStack's own reuse only triggers when `$transaction` is\n // invoked on a transaction client; callers that hold the base client (e.g.\n // the better-auth adapter, which since better-auth 1.6.11 nests transactions\n // to atomically consume verification rows) instead open a fresh transaction.\n // On a small pool (e.g. a Hyperdrive-fronted `max: 1` pg pool) the inner\n // transaction then blocks forever waiting for the connection the outer one\n // holds — a deadlock that surfaces as a Postgres backend stuck `idle in\n // transaction`. Reusing the active client makes nested transactions share the\n // single connection, which is also the correct semantics (one atomic unit).\n const activeTransaction = new AsyncLocalStorage<InstanceType<typeof ZenStackClient>>()\n\n // Live clients created from this service class, tracked weakly: the client\n // is `@Transient`, so request-scoped resolutions must stay GC-able with\n // their request. Dead refs are pruned on each add; live ones are\n // disconnected by `disposeInstances()` on module shutdown.\n const instances = new Set<WeakRef<ZenStackClientInstance>>()\n\n @Transient()\n class DatabaseClient extends ZenStackClient<typeof conn.schema> {\n constructor() {\n const dialect = conn.dialect()\n // ZenStack 3+ requires `computedFields` whenever the schema declares any\n // `@computed` fields, so pass them through when the consumer provides them.\n super(conn.schema, {\n dialect,\n plugins,\n // @ts-expect-error - ZenStack 3+ requires `computedFields` whenever the schema declares any `@computed` fields, so pass them through when the consumer provides them.\n computedFields: conn.computedFields\n })\n // ZenStackClient's constructor returns a Proxy (for dynamic model\n // accessors), so subclass method overrides are shadowed. Wrap it in a\n // proxy that makes `$transaction` reentrant. Returning from the\n // constructor replaces the instance DI receives.\n const client = makeReentrantTransaction(this as InstanceType<typeof ZenStackClient>, activeTransaction)\n for (const ref of instances) {\n if (ref.deref() === undefined) instances.delete(ref)\n }\n instances.add(new WeakRef(client))\n return client\n }\n\n static async disposeInstances(): Promise<void> {\n const live = [...instances]\n instances.clear()\n await Promise.all(live.map(async (ref) => {\n const client = ref.deref()\n if (!client) return\n try {\n await client.$disconnect()\n } catch (error) {\n console.error(`[stratal] Failed to disconnect database client \"${conn.name}\":`, error)\n }\n }))\n }\n }\n\n return DatabaseClient\n}\n","export const DATABASE_TOKENS = {\n Options: Symbol.for('stratal:database:options'),\n Services: Symbol.for('stratal:database:services'),\n} as const\n\nimport type { ConnectionName } from './types'\n\nexport function connectionSymbol(name: ConnectionName): symbol {\n return Symbol.for(`stratal:database:connection:${name}`)\n}\n","export const databaseMessages = {\n en: {\n connectionNameRequired: 'Connection name is required',\n defaultConnectionRequired: 'Default connection name is required',\n connectionRequired: 'At least one connection is required',\n duplicateConnections: 'Duplicate connection names found',\n defaultConnectionNotFound: 'Default connection not found in connections',\n },\n} as const\n\ndeclare module 'stratal/i18n' {\n interface AppMessageNamespaces {\n database: typeof databaseMessages['en']\n }\n}\n","import type { AnyPlugin, ClientOptions, ComputedFieldsOptions } from '@zenstackhq/orm';\nimport type { SchemaDef } from '@zenstackhq/schema';\nimport { DI_TOKENS, lazy } from 'stratal/di';\nimport type { IEventRegistry } from 'stratal/events';\nimport { I18nModule } from 'stratal/i18n';\nimport {\n Module,\n type AsyncModuleOptions,\n type DynamicModule,\n type LazyModuleLoader,\n type ModuleContext,\n type OnInitialize,\n type OnShutdown,\n} from 'stratal/module';\nimport { DbGenerateCommand } from './commands/db-generate.command';\nimport { DbPullCommand } from './commands/db-pull.command';\nimport { DbPushCommand } from './commands/db-push.command';\nimport { MigrateDeployCommand } from './commands/migrate-deploy.command';\nimport { MigrateDevCommand } from './commands/migrate-dev.command';\nimport { MigrateResetCommand } from './commands/migrate-reset.command';\nimport { MigrateStatusCommand } from './commands/migrate-status.command';\nimport { createDatabaseService, type DatabaseServiceClass } from './database.helpers';\nimport { connectionSymbol, DATABASE_TOKENS } from './database.tokens';\nimport { databaseMessages } from './i18n';\nimport type { ConnectionName, DefaultConnectionName } from './types';\n\nexport interface DatabaseConnectionConfig<\n Schema extends SchemaDef = SchemaDef,\n Name extends ConnectionName = ConnectionName,\n> {\n name: Name\n schema: Schema\n dialect: () => ClientOptions<SchemaDef>['dialect']\n plugins?: AnyPlugin[]\n /**\n * Schema-level @computed field implementations. Required when the schema\n * declares any `@computed` fields. Keyed by uncapitalized model name; values\n * map field name to a Kysely-expression compute callback.\n */\n computedFields?: ComputedFieldsOptions<Schema>\n}\n\nexport interface DatabaseModuleConfig {\n default: DefaultConnectionName\n connections: DatabaseConnectionConfig[]\n}\n\n@Module({\n imports: [\n I18nModule.registerMessages({ en: { database: databaseMessages.en } }),\n ],\n providers: [\n DbGenerateCommand,\n DbPushCommand,\n DbPullCommand,\n MigrateDevCommand,\n MigrateDeployCommand,\n MigrateStatusCommand,\n MigrateResetCommand,\n ],\n})\nexport class DatabaseModule implements OnInitialize, OnShutdown {\n private readonly services: DatabaseServiceClass[] = []\n\n static forRoot(config: DatabaseModuleConfig): DynamicModule {\n return {\n module: DatabaseModule,\n providers: [\n { provide: DATABASE_TOKENS.Options, useValue: config as unknown as object },\n ],\n }\n }\n\n static forRootAsync(options: AsyncModuleOptions<DatabaseModuleConfig>): DynamicModule {\n return {\n module: DatabaseModule,\n providers: [\n {\n provide: DATABASE_TOKENS.Options,\n useFactory: options.useFactory,\n inject: options.inject,\n },\n ],\n }\n }\n\n async onInitialize(context: ModuleContext): Promise<void> {\n const config = context.container.resolve<DatabaseModuleConfig>(DATABASE_TOKENS.Options)\n // EventRegistry is loaded on demand — pull in EventsModule via the loader.\n const loader = context.container.resolve<LazyModuleLoader>(DI_TOKENS.LazyModuleLoader)\n const eventsRef = await loader.load(() => import('stratal/events').then((m) => m.EventsModule))\n const eventRegistry = eventsRef.get<IEventRegistry>(DI_TOKENS.EventRegistry)\n for (const conn of config.connections) {\n const Service = createDatabaseService(conn, eventRegistry);\n\n this.services.push(Service)\n context.container.register(connectionSymbol(conn.name), lazy(() => Service))\n }\n\n context.container.registerExisting(DI_TOKENS.Database, connectionSymbol(config.default))\n\n context.logger.info('DatabaseModule initialized')\n }\n\n async onShutdown(context: ModuleContext): Promise<void> {\n // Disconnect every live client so pools/sockets don't outlive the\n // Application across dev-server hot reloads.\n await Promise.all(this.services.map(service => service.disposeInstances()))\n this.services.length = 0\n context.logger.info('DatabaseModule shutdown')\n }\n}\n","import { inject } from 'stratal/di'\nimport type { ConnectionName } from '../types'\nimport { connectionSymbol } from '../database.tokens'\n\nexport function InjectDB(name: ConnectionName): ParameterDecorator {\n return inject(connectionSymbol(name))\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["RequestScoped"],"sources":["../../src/database/commands/zenstack.command.ts","../../src/database/commands/db-generate.command.ts","../../src/database/commands/db-pull.command.ts","../../src/database/commands/db-push.command.ts","../../src/database/commands/migrate-deploy.command.ts","../../src/database/commands/migrate-dev.command.ts","../../src/database/commands/migrate-reset.command.ts","../../src/database/commands/migrate-status.command.ts","../../src/database/errors/cursor-model-unavailable.error.ts","../../src/database/errors/cursor-ordering.error.ts","../../src/database/errors/malformed-cursor.error.ts","../../src/database/pagination/cursor.ts","../../src/database/pagination/cursor-reader.ts","../../src/database/errors/record-not-found.error.ts","../../src/database/errors/unique-constraint.error.ts","../../src/database/errors/from-zenstack-error.ts","../../src/database/plugins/error-handler.plugin.ts","../../src/database/plugins/event-emitter.plugin.ts","../../src/database/plugins/schema-switcher.ts","../../src/database/database.helpers.ts","../../src/database/database.tokens.ts","../../src/database/i18n/en.ts","../../src/database/database.module.ts","../../src/database/pool.ts","../../src/database/decorators/inject-db.decorator.ts"],"sourcesContent":["import { Command } from 'stratal/quarry'\n\n/**\n * Base command for ZenStack CLI wrappers.\n * Uses execFileSync with array arguments to prevent shell injection.\n */\nexport abstract class ZenStackCommand extends Command {\n protected async zenstack(args: string[]): Promise<number> {\n // Dynamic import — node:child_process is only available in the Quarry CLI (Node) context\n const { execFileSync } = await import('node:child_process')\n\n try {\n const output = execFileSync('npx', ['zenstack', ...args], {\n encoding: 'utf-8',\n stdio: 'pipe',\n })\n if (output) this.info(output.trim())\n return 0\n } catch (err) {\n const error = err as { stderr?: string; stdout?: string; status?: number }\n if (error.stderr) this.error(error.stderr.trim())\n if (error.stdout) this.info(error.stdout.trim())\n return error.status ?? 1\n }\n }\n}\n","import { ZenStackCommand } from './zenstack.command'\n\nexport class DbGenerateCommand extends ZenStackCommand {\n static command = 'db:generate {--schema= : Path to schema file} {--watch : Enable watch mode}'\n static description = 'Generate ZenStack ORM client'\n\n async handle(): Promise<number> {\n const args = ['generate']\n const schema = this.string('schema')\n\n if (schema) args.push('--schema', schema)\n if (this.boolean('watch')) args.push('--watch')\n\n return this.zenstack(args)\n }\n}\n","import { ZenStackCommand } from './zenstack.command'\n\nexport class DbPullCommand extends ZenStackCommand {\n static command = 'db:pull {--schema= : Path to schema file}'\n static description = 'Introspect database and generate schema'\n\n async handle(): Promise<number> {\n const args = ['db', 'pull']\n const schema = this.string('schema')\n\n if (schema) args.push('--schema', schema)\n\n return this.zenstack(args)\n }\n}\n","import { ZenStackCommand } from './zenstack.command'\n\nexport class DbPushCommand extends ZenStackCommand {\n static command = 'db:push {--schema= : Path to schema file} {--accept-data-loss : Accept data loss} {--force-reset : Force reset database}'\n static description = 'Push database schema changes'\n\n async handle(): Promise<number> {\n const args = ['db', 'push']\n const schema = this.string('schema')\n\n if (schema) args.push('--schema', schema)\n if (this.boolean('accept-data-loss')) args.push('--accept-data-loss')\n if (this.boolean('force-reset')) args.push('--force-reset')\n\n return this.zenstack(args)\n }\n}\n","import { ZenStackCommand } from './zenstack.command'\n\nexport class MigrateDeployCommand extends ZenStackCommand {\n static command = 'migrate:deploy {--schema= : Path to schema file}'\n static description = 'Deploy pending migrations'\n\n async handle(): Promise<number> {\n const args = ['migrate', 'deploy']\n const schema = this.string('schema')\n\n if (schema) args.push('--schema', schema)\n\n return this.zenstack(args)\n }\n}\n","import { ZenStackCommand } from './zenstack.command'\n\nexport class MigrateDevCommand extends ZenStackCommand {\n static command = 'migrate:dev {--schema= : Path to schema file} {--name= : Migration name} {--create-only : Create without applying}'\n static description = 'Create and apply migration'\n\n async handle(): Promise<number> {\n const args = ['migrate', 'dev']\n const schema = this.string('schema')\n const name = this.string('name')\n\n if (schema) args.push('--schema', schema)\n if (name) args.push('--name', name)\n if (this.boolean('create-only')) args.push('--create-only')\n\n return this.zenstack(args)\n }\n}\n","import { ZenStackCommand } from './zenstack.command'\n\nexport class MigrateResetCommand extends ZenStackCommand {\n static command = 'migrate:reset {--schema= : Path to schema file} {--force : Skip confirmation} {--skip-seed : Skip seeding}'\n static description = 'Reset database'\n\n async handle(): Promise<number> {\n const args = ['migrate', 'reset']\n const schema = this.string('schema')\n\n if (schema) args.push('--schema', schema)\n if (this.boolean('force')) args.push('--force')\n if (this.boolean('skip-seed')) args.push('--skip-seed')\n\n return this.zenstack(args)\n }\n}\n","import { ZenStackCommand } from './zenstack.command'\n\nexport class MigrateStatusCommand extends ZenStackCommand {\n static command = 'migrate:status {--schema= : Path to schema file}'\n static description = 'Check migration status'\n\n async handle(): Promise<number> {\n const args = ['migrate', 'status']\n const schema = this.string('schema')\n\n if (schema) args.push('--schema', schema)\n\n return this.zenstack(args)\n }\n}\n","import { ApplicationError } from 'stratal/errors'\n\n/**\n * Raised when `db.$cursor.<model>` cannot reach a model delegate on the client\n * it was built for — the schema declares the model, the client does not answer\n * for it.\n *\n * This is a **programming error**, not bad input: the reader is built from the\n * schema's own model list, so reaching it means the client and the schema it was\n * given have come apart — a model sliced out of the client's options, or a\n * hand-assembled client. No request recovers from it and no retry helps. It\n * deliberately carries no HTTP status, so it is never mistaken for something the\n * client sent wrong.\n *\n * `model` names the client key at fault, and is reported to observability so the\n * raise site stays distinguishable without matching on message text.\n */\nexport class CursorModelUnavailableError extends ApplicationError {\n constructor(public readonly model: string) {\n super(\n `[stratal:database] $cursor cannot reach the \"${model}\" model on this client. `\n + 'The schema declares it, so the client was built with a different schema or with this model sliced out.',\n )\n }\n\n public override reportContext(): Record<string, unknown> | undefined {\n return { model: this.model }\n }\n}\n","import { ApplicationError } from 'stratal/errors'\n\n/**\n * Raised when a `$cursor` read is asked for an ordering it cannot address a row\n * in: no `orderBy`, a clause with no `asc`/`desc` direction, an ordering\n * without the unique column that breaks ties, or an ordering column that is\n * absent from the returned row or null on it.\n *\n * This is a **programming error**, not bad input — no request can recover from\n * it and no retry helps, because the query as written cannot produce a stable\n * position. Handlers should let it surface as a `500` and report it; the fix is\n * always in the caller's `orderBy`, `uniqueBy` or `select`. It deliberately\n * carries no HTTP status, so it is never mistaken for something the client sent\n * wrong.\n *\n * `field` names the ordering column at fault where one is identifiable, and is\n * reported to observability so the raise sites stay distinguishable without\n * matching on message text.\n */\nexport class CursorOrderingError extends ApplicationError {\n constructor(message: string, public readonly field?: string) {\n super(message)\n }\n\n public override reportContext(): Record<string, unknown> | undefined {\n return this.field === undefined ? undefined : { field: this.field }\n }\n}\n","import { HttpException } from 'stratal/errors'\n\n/**\n * Raised when a pagination cursor cannot be read: it is not the base64url\n * payload `$cursor` mints, or it decodes to something that is not a cursor.\n *\n * This is **bad input**, not a broken query. A cursor travels in a query string,\n * so a truncated link, a hand-edited URL, or one minted by an older build all\n * land here, and the request itself is still answerable. It carries its own\n * `400`, so a handler lets it surface rather than catching it to serve the\n * first page — the same refusal any other damaged parameter gets.\n */\nexport class MalformedCursorError extends HttpException {\n constructor(cause?: unknown) {\n super(400, '[stratal:database] Malformed pagination cursor.', cause)\n }\n}\n","import { CursorOrderingError } from '../errors/cursor-ordering.error'\nimport { MalformedCursorError } from '../errors/malformed-cursor.error'\n\n/**\n * Reading rows from a position in an ordering.\n *\n * A primitive on its own terms, not a variant of anything: it answers \"the N\n * rows after this row\", and that is the whole contract. There are no page\n * numbers, no total, and no last page, because a cursor names a row rather than\n * an offset into a result set. It requires an ordering — a total one — which is\n * the price of addressing a row at all.\n *\n * What that buys: rows inserted or deleted around the reader do not move the\n * position. `LIMIT/OFFSET` addresses a place in a result set, so a row added\n * above the window shifts everything under it and the reader silently skips one\n * or sees one twice. Nothing here shifts.\n *\n * Knows nothing of HTTP or Inertia. A route can return the result as JSON, a\n * client can walk it, and `ctx.scroll()` can read it — none of which this file\n * is aware of.\n */\n\n/** Sort direction for one ordering column. */\nexport type CursorSortOrder = 'asc' | 'desc'\n\n/**\n * One ordering column. Several are combined left to right, as in SQL.\n *\n * Values may be `undefined` so an array of clause literals keeps its ordinary\n * inferred type — TypeScript widens `[{ updatedAt: 'desc' }, { id: 'desc' }]`\n * to a union whose members carry the other's key as `undefined`. A direction\n * that really is `undefined` at runtime still raises `CursorOrderingError`.\n */\nexport type CursorOrderBy = Record<string, CursorSortOrder | undefined>\n\nexport interface CursorPageArgs {\n /**\n * The cursor to read from, or `null` to start at the beginning of the\n * ordering. Opaque: it is minted by a previous result and passed back\n * verbatim. Never construct one.\n */\n cursor?: string | null\n /** Rows per page. The last page may hold fewer. */\n take: number\n /**\n * Ordering, applied left to right. Required — a cursor addresses a row's\n * position in an ordering, so without one there is no position to address.\n * The set must include {@link CursorPageArgs.uniqueBy}.\n */\n orderBy: CursorOrderBy | CursorOrderBy[]\n /**\n * The ordering column that makes the ordering total. Defaults to `id`.\n *\n * Ordering on a non-unique column alone leaves ties, and a cursor cannot\n * address a position inside a tie — which reintroduces exactly the skipping\n * that cursors exist to prevent. `updatedAt desc` needs an `id desc` after it.\n */\n uniqueBy?: string\n /** Filter, combined with the cursor's own condition. */\n where?: Record<string, unknown>\n /**\n * Relations to load, field selection, and fields to drop — handed to the\n * delegate untouched. `select` and `omit` must not drop an ordering column,\n * because the cursor is built from those.\n *\n * Deliberately not mutually exclusive here, unlike on `db.$cursor`, because\n * the delegate decides what they mean: a model delegate already refuses the\n * combination itself (`\"select\" and \"omit\" cannot be used together`), and a\n * hand-written one owns its `findMany` and may honour any combination.\n */\n include?: Record<string, unknown>\n select?: Record<string, unknown>\n omit?: Record<string, unknown>\n /** Query parameter name a caller should send the cursor under. Defaults to `cursor`. */\n cursorName?: string\n}\n\n/**\n * One page of rows read by cursor.\n *\n * Carries no `path` or page URLs, unlike Laravel's `CursorPaginator`. Building\n * a URL is a routing decision — which route, which of the current query\n * parameters to keep, whether a trailing slash is canonical — and a paginator\n * holds none of that. A route that wants links builds them from its own route\n * helper plus `nextCursor` / `prevCursor`.\n */\nexport interface CursorPageResult<TRow> {\n /** The rows for this page, in display order. */\n data: TRow[]\n /** Rows requested per page. The last page may hold fewer. */\n perPage: number\n /** Query parameter name the cursor travels under. */\n cursorName: string\n /**\n * The cursor this page was read from, or `null` on the first page.\n *\n * Laravel's `CursorPaginator::toArray()` omits both this and the cursor name\n * because it recovers them from the request. A result object has no request,\n * so it carries them.\n */\n cursor: string | null\n /** Cursor for the following page, or `null` when this is the last. */\n nextCursor: string | null\n /** Cursor for the preceding page, or `null` when this is the first. */\n prevCursor: string | null\n}\n\n/** Whether a cursor points forward (the next rows) or backward (the previous ones). */\ntype CursorDirection = 'next' | 'prev'\n\ninterface DecodedCursor {\n /** The ordering column values of the row the cursor addresses. */\n values: Record<string, unknown>\n direction: CursorDirection\n}\n\n/**\n * The query {@link readCursorPage} builds — the single declaration of it, used\n * both as the delegate's parameter type and as the type of the object handed to\n * `findMany`.\n *\n * Every key here must also be a key of a ZenStack `findMany` arg, because that\n * is what makes a model delegate satisfy {@link CursorPageDelegate}: ZenStack\n * types `findMany` as `<T extends FindManyArgs>(args?: SelectSubset<T, …>)`,\n * and `SelectSubset` maps each key of `T` to `never` unless the model's own\n * args declare it. A widened stand-in — `Record<string, unknown>` — infers `T`\n * to itself, collapses the whole parameter to `{ [x: string]: never }`, and no\n * delegate can satisfy it in either direction.\n */\nexport interface CursorFindManyArgs {\n where?: Record<string, unknown>\n orderBy?: CursorOrderBy[]\n take?: number\n include?: Record<string, unknown>\n select?: Record<string, unknown>\n omit?: Record<string, unknown>\n}\n\n/**\n * The minimum a model delegate has to offer.\n *\n * A ZenStack model delegate satisfies this structurally — `db.thread` is passed\n * as-is — and so does a hand-written `findMany`, which is how a query the ORM\n * cannot express (a `UNION`, a raw statement) is paged by the same primitive.\n */\nexport interface CursorPageDelegate<TRow> {\n findMany(args: CursorFindManyArgs): PromiseLike<TRow[]>\n}\n\nconst CURSOR_DIRECTION_KEY = '_next'\n\n/**\n * base64url, so a cursor survives a query string without escaping.\n *\n * The UTF-8 round trip is load-bearing, not ceremony: `btoa` accepts only\n * Latin-1, and `JSON.stringify` leaves non-ASCII characters as they are. An\n * ordering column is whatever the caller ordered by — a `title`, a `name`, a\n * `slug` — so one row whose value carries CJK, Cyrillic or an emoji would throw\n * `DOMException` while every ASCII row encoded fine. The first page would still\n * answer, because it mints no cursor; the 500 would land on the follow-up.\n */\nfunction base64UrlEncode(input: string): string {\n const bytes = new TextEncoder().encode(input)\n const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('')\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nfunction base64UrlDecode(input: string): string {\n const padded = input.replace(/-/g, '+').replace(/_/g, '/')\n const binary = atob(padded.padEnd(padded.length + ((4 - (padded.length % 4)) % 4), '='))\n return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)))\n}\n\n/**\n * Encodes the ordering values of a row plus the direction it points.\n *\n * The whole ordering tuple travels, not a row id, so the cursor still resolves\n * after the row it was built from is deleted — which is the case cursors are\n * for. The direction rides along because a caller sends every cursor under one\n * query parameter name and nothing else distinguishes forward from backward.\n */\nexport function encodeCursor(values: Record<string, unknown>, direction: CursorDirection): string {\n return base64UrlEncode(JSON.stringify({ ...values, [CURSOR_DIRECTION_KEY]: direction === 'next' }))\n}\n\n/** Reads a cursor minted by {@link encodeCursor}. Throws on anything else. */\nexport function decodeCursor(cursor: string): DecodedCursor {\n let parsed: unknown\n\n try {\n parsed = JSON.parse(base64UrlDecode(cursor))\n } catch (cause) {\n throw new MalformedCursorError(cause)\n }\n\n if (typeof parsed !== 'object' || parsed === null || !(CURSOR_DIRECTION_KEY in parsed)) {\n throw new MalformedCursorError()\n }\n\n const { [CURSOR_DIRECTION_KEY]: pointsToNext, ...values } = parsed as Record<string, unknown>\n\n return { values, direction: pointsToNext ? 'next' : 'prev' }\n}\n\nfunction normalizeOrderBy(orderBy: CursorOrderBy | CursorOrderBy[]): [string, CursorSortOrder][] {\n const entries = (Array.isArray(orderBy) ? orderBy : [orderBy]).flatMap((clause) => Object.entries(clause))\n\n return entries.map(([field, direction]) => {\n if (direction !== 'asc' && direction !== 'desc') {\n throw new CursorOrderingError(\n `[stratal:database] $cursor needs a direction for \"${field}\"; got ${JSON.stringify(direction)}.`,\n field,\n )\n }\n return [field, direction]\n })\n}\n\n/**\n * Builds the keyset condition for \"the rows after (or before) this position\".\n *\n * For `updatedAt desc, id desc` this is\n * `updatedAt < :updatedAt OR (updatedAt = :updatedAt AND id < :id)` — one\n * disjunct per ordering column, each pinning the columns to its left to\n * equality. Reading backwards flips every comparison.\n */\nfunction buildKeysetCondition(\n order: [string, CursorSortOrder][],\n values: Record<string, unknown>,\n direction: CursorDirection,\n): Record<string, unknown> {\n const disjuncts: Record<string, unknown>[] = []\n\n for (let i = 0; i < order.length; i++) {\n const conjunct: Record<string, unknown> = {}\n\n for (let j = 0; j < i; j++) {\n const [field] = order[j]\n conjunct[field] = values[field]\n }\n\n const [field, sort] = order[i]\n const readingForward = direction === 'next'\n const descending = sort === 'desc'\n // Reading forward down a descending column means smaller values; either\n // flip alone reverses the comparison, both flips cancel.\n const operator = descending === readingForward ? 'lt' : 'gt'\n conjunct[field] = { [operator]: values[field] }\n\n disjuncts.push(conjunct)\n }\n\n return { OR: disjuncts }\n}\n\n/** The ordering values of a row, which is what a cursor is made of. */\nfunction cursorValuesOf(row: Record<string, unknown>, order: [string, CursorSortOrder][]): Record<string, unknown> {\n const values: Record<string, unknown> = {}\n\n for (const [field] of order) {\n if (!(field in row)) {\n throw new CursorOrderingError(\n `[stratal:database] Cannot build a cursor: the ordering column \"${field}\" is not on the returned row. `\n + 'Either it is not a field of this model, or a `select` dropped it.',\n field,\n )\n }\n\n const value = row[field]\n if (value === null || value === undefined) {\n throw new CursorOrderingError(\n `[stratal:database] Cannot build a cursor: the ordering column \"${field}\" is null. `\n + '$cursor cannot order on a nullable column — null has no position in an ordering.',\n field,\n )\n }\n\n values[field] = value\n }\n\n return values\n}\n\n/**\n * Reads one page of rows by cursor.\n *\n * Fetches one row more than asked for, which is how the next page is known to\n * exist without a `COUNT` — and a count is what a growing list cannot give a\n * stable answer to anyway.\n *\n * @example\n * ```typescript\n * const page = await db.$cursor.$from(threadUnion, {\n * cursor: ctx.query('cursor'),\n * take: 20,\n * orderBy: [{ updatedAt: 'desc' }, { id: 'desc' }],\n * where: { boardId },\n * })\n * ```\n */\nexport async function readCursorPage<TRow extends Record<string, unknown>>(\n delegate: CursorPageDelegate<TRow>,\n args: CursorPageArgs,\n): Promise<CursorPageResult<TRow>> {\n const order = normalizeOrderBy(args.orderBy)\n\n if (order.length === 0) {\n throw new CursorOrderingError(\n '[stratal:database] $cursor requires `orderBy`. A cursor addresses a row\\'s position in an '\n + 'ordering, so there is no position to address without one.',\n )\n }\n\n const uniqueBy = args.uniqueBy ?? 'id'\n if (!order.some(([field]) => field === uniqueBy)) {\n throw new CursorOrderingError(\n `[stratal:database] $cursor requires the ordering to include the unique column \"${uniqueBy}\", `\n + 'otherwise tied rows share a position and paging over them skips or repeats. '\n + `Add it last — e.g. \\`orderBy: [{ ${order[0][0]}: '${order[0][1]}' }, { ${uniqueBy}: '${order[0][1]}' }]\\` `\n + '— or name a different unique column with `uniqueBy`.',\n )\n }\n\n const decoded = args.cursor ? decodeCursor(args.cursor) : null\n const readingBackwards = decoded?.direction === 'prev'\n\n // Reading backwards walks away from the cursor, so the query runs in reversed\n // order and the rows are flipped back into display order below.\n const queryOrder: [string, CursorSortOrder][] = readingBackwards\n ? order.map(([field, sort]) => [field, sort === 'asc' ? 'desc' : 'asc'])\n : order\n\n const conditions: Record<string, unknown>[] = []\n if (args.where) conditions.push(args.where)\n if (decoded) conditions.push(buildKeysetCondition(order, decoded.values, decoded.direction))\n\n const query: CursorFindManyArgs = {\n ...(conditions.length > 0 ? { where: conditions.length === 1 ? conditions[0] : { AND: conditions } } : {}),\n orderBy: queryOrder.map(([field, sort]) => ({ [field]: sort })),\n // One extra row is the whole \"is there another page\" mechanism.\n take: args.take + 1,\n ...(args.include ? { include: args.include } : {}),\n ...(args.select ? { select: args.select } : {}),\n ...(args.omit ? { omit: args.omit } : {}),\n }\n\n const rows = await delegate.findMany(query)\n\n const hasExtraRow = rows.length > args.take\n const page = hasExtraRow ? rows.slice(0, args.take) : rows\n const data = readingBackwards ? [...page].reverse() : page\n\n const first = data[0]\n const last = data[data.length - 1]\n\n // With no rows there is no position to mint a cursor from, so both ends close.\n const nextCursor = last === undefined\n ? null\n : readingBackwards\n ? encodeCursor(cursorValuesOf(last, order), 'next')\n : hasExtraRow ? encodeCursor(cursorValuesOf(last, order), 'next') : null\n\n const prevCursor = first === undefined\n ? null\n : readingBackwards\n ? hasExtraRow ? encodeCursor(cursorValuesOf(first, order), 'prev') : null\n : decoded ? encodeCursor(cursorValuesOf(first, order), 'prev') : null\n\n return {\n data,\n perPage: args.take,\n cursorName: args.cursorName ?? 'cursor',\n cursor: args.cursor ?? null,\n nextCursor,\n prevCursor,\n }\n}\n","import type { FindManyArgs, QueryOptions, SelectSubset, SimplifiedPlainResult } from '@zenstackhq/orm'\nimport type { GetModels, SchemaDef } from '@zenstackhq/orm/schema'\nimport { CursorModelUnavailableError } from '../errors/cursor-model-unavailable.error'\nimport type { DeclaredMembers } from '../types'\nimport { readCursorPage, type CursorPageDelegate, type CursorPageResult, type CursorSortOrder } from './cursor'\n\n/**\n * Per-model cursor reading: `db.$cursor.thread.findMany({ … })`.\n *\n * A namespace of its own rather than an argument on the model's own `findMany`,\n * because ZenStack already has a `cursor` there and it means something else. Its\n * `cursor` takes a `WhereUniqueInput` and resolves the row's ordering values by\n * subquery at query time, so the row must still exist, still pass the query's\n * own `where`, and still hold the ordering values it held when the cursor was\n * minted. Two arguments spelled `cursor` on one call, one of which answers an\n * empty page when its row is deleted, is not a distinction a caller should have\n * to hold. In here there is exactly one: {@link CursorReaderArgs} omits\n * ZenStack's `cursor` and the `skip` that goes with it.\n */\n\n/**\n * The query-relevant options the result and argument types read.\n *\n * `QueryOptions` is what ZenStack's own `SimplifiedPlainResult` and `FindManyArgs` default this\n * slot to. Narrowing `ClientOptions` to the keys they share yields that same type, but reaches\n * it through `keyof ClientOptions`, which instantiates the client-only `computedFields` map\n * across every model and field in the schema for a result that never reads it.\n */\ntype Options<Schema extends SchemaDef> = QueryOptions<Schema>\n\n/** The client keys that address a model — `Post` in the schema is `post` here. */\nexport type CursorModelKey<Schema extends SchemaDef> = Uncapitalize<GetModels<Schema>>\n\n/** The schema model a client key addresses. */\ntype ModelOf<Schema extends SchemaDef, Key extends CursorModelKey<Schema>> =\n Extract<GetModels<Schema>, { [M in GetModels<Schema>]: Uncapitalize<M> extends Key ? M : never }[GetModels<Schema>]>\n\n/** One row of a model, before `select`/`include` narrow it. */\ntype PlainRow<Schema extends SchemaDef, Key extends CursorModelKey<Schema>> =\n SimplifiedPlainResult<Schema, ModelOf<Schema, Key>, {}, Options<Schema>>\n\n/**\n * The ordering, restricted to the model's own scalar columns.\n *\n * Narrower than ZenStack's `orderBy` on purpose: a cursor is built from the\n * ordering values of a row, so an ordering that reaches through a relation or\n * sorts on a computed relevance score has no value to carry. Those are\n * unrepresentable here rather than accepted and failed on at runtime.\n */\nexport type CursorOrderByOf<Schema extends SchemaDef, Key extends CursorModelKey<Schema>> =\n Partial<Record<keyof PlainRow<Schema, Key> & string, CursorSortOrder>>\n\n/**\n * A model's `findMany` arguments, with cursor paging in place of offset paging.\n *\n * `skip` goes with ZenStack's `cursor` and addresses a place in a result set,\n * which is the thing a cursor exists not to do. `distinct` changes which row\n * represents a group, so no row has a stable position to address.\n *\n * Dropping an argument here only stops it being declared; what refuses it at a\n * call site is {@link CursorModelReader.findMany} passing this through\n * ZenStack's `SelectSubset`.\n */\nexport type CursorReaderArgs<Schema extends SchemaDef, Key extends CursorModelKey<Schema>> =\n & Omit<\n FindManyArgs<Schema, ModelOf<Schema, Key>, Options<Schema>>,\n 'cursor' | 'skip' | 'take' | 'orderBy' | 'distinct'\n >\n & {\n /**\n * The cursor to read from, or `null`/absent to start at the beginning of\n * the ordering. Opaque: it is minted by a previous result and passed back\n * verbatim. Never construct one.\n */\n cursor?: string | null\n /** Rows per page. The last page may hold fewer. */\n take: number\n /**\n * Ordering, applied left to right. Required, and must end in a unique\n * column — a cursor addresses a row's position in an ordering, and a\n * position inside a tie cannot be addressed.\n */\n orderBy: CursorOrderByOf<Schema, Key> | CursorOrderByOf<Schema, Key>[]\n /** The ordering column that makes the ordering total. Defaults to `id`. */\n uniqueBy?: keyof PlainRow<Schema, Key> & string\n /** Query parameter name a caller should send the cursor under. Defaults to `cursor`. */\n cursorName?: string\n }\n\n/**\n * One model's cursor reader.\n *\n * The argument goes through ZenStack's own `SelectSubset`, which is what a\n * model's `findMany` uses and is the whole of the refusal machinery here. It\n * maps any key the arguments do not declare to `never` — restoring the\n * excess-property refusal a bare type parameter switches off, so `skip`,\n * `distinct` and the native `cursor` are rejected — and it carries ZenStack's\n * conditional messages for `select` with `include` and `select` with `omit`.\n * Restating those exclusions by hand would answer the same mistake with a worse\n * message; rebuilding the argument type without it drops them silently, which\n * is what an earlier revision of this file did.\n *\n * `Args` is still inferred from the call, so `select` narrows the row type.\n */\nexport interface CursorModelReader<Schema extends SchemaDef, Key extends CursorModelKey<Schema>> {\n findMany<Args extends CursorReaderArgs<Schema, Key>>(\n args: SelectSubset<Args, CursorReaderArgs<Schema, Key>>,\n ): Promise<CursorPageResult<SimplifiedPlainResult<Schema, ModelOf<Schema, Key>, Args, Options<Schema>>>>\n}\n\n/**\n * Arguments for a page read from a caller-supplied `findMany`.\n *\n * The row type comes from the delegate — the framework has no schema for a\n * `UNION` — and the ordering is keyed to it, so ordering on a column the query\n * does not return is a compile error instead of the `CursorOrderingError` it\n * would raise once the first row came back.\n *\n * No `select`, `include` or `omit`. They are ZenStack model arguments; a\n * caller-written `findMany` decides for itself what to return, and the framework\n * can neither type them against the row nor make the delegate honour them — an\n * option that may be silently ignored is worse than one that is absent. A\n * caller that wants fewer columns selects fewer inside its own `findMany`.\n *\n * `where` stays, and is not a passthrough: the paginator combines it with the\n * keyset condition it builds, so it is how the page is positioned at all. It\n * cannot be typed here because its shape is the delegate's own filter language.\n */\nexport interface CursorSourceArgs<TRow> {\n /**\n * The cursor to read from, or `null`/absent to start at the beginning of the\n * ordering. Opaque: it is minted by a previous result and passed back\n * verbatim. Never construct one.\n */\n cursor?: string | null\n /** Rows per page. The last page may hold fewer. */\n take: number\n /**\n * Ordering, applied left to right. Required, and must end in a unique column\n * — a cursor addresses a row's position in an ordering, and a position inside\n * a tie cannot be addressed.\n */\n orderBy: CursorSourceOrderBy<TRow> | CursorSourceOrderBy<TRow>[]\n /** The ordering column that makes the ordering total. Defaults to `id`. */\n uniqueBy?: CursorSourceColumn<TRow>\n /** Filter, combined with the cursor's own condition and handed to the delegate. */\n where?: Record<string, unknown>\n /** Query parameter name a caller should send the cursor under. Defaults to `cursor`. */\n cursorName?: string\n}\n\n/**\n * A column of the delegate's row.\n *\n * `DeclaredMembers` strips the index signature that {@link CursorPageDelegate}'s\n * row constraint obliges a caller to write (`interface Row extends\n * Record<string, unknown>`). Without it `keyof` is `string` and every name\n * typechecks, which is the whole of the guarantee here.\n */\nexport type CursorSourceColumn<TRow> = keyof DeclaredMembers<TRow> & string\n\n/** An ordering clause over the delegate's own columns. */\nexport type CursorSourceOrderBy<TRow> = Partial<Record<CursorSourceColumn<TRow>, CursorSortOrder>>\n/**\n * Reads a page from something that is not a model — a `UNION`, a raw statement —\n * by supplying the `findMany` yourself.\n *\n * Sits beside the model keys rather than in a separate export because it is the\n * same operation on a different source, and because a free function has no\n * client to belong to: on a transaction client, `tx.$cursor.$from(…)` reads\n * inside the transaction, which an imported function could not do.\n *\n * The `$` prefix is what keeps it from colliding with a model: a client key is\n * an uncapitalized model name, and ZenStack reserves the prefix for members of\n * the client itself.\n */\nexport interface CursorSourceReader {\n $from<TRow extends Record<string, unknown>>(\n delegate: CursorPageDelegate<TRow>,\n args: CursorSourceArgs<NoInfer<TRow>>,\n ): Promise<CursorPageResult<TRow>>\n}\n\n/** `db.$cursor` — one reader per model in the schema, plus `$from` for anything else. */\nexport type CursorReader<Schema extends SchemaDef> =\n & { [Key in CursorModelKey<Schema>]: CursorModelReader<Schema, Key> }\n & CursorSourceReader\n\n/** The client members {@link createCursorReader} contributes. */\nexport interface CursorClientMembers<Schema extends SchemaDef> {\n /**\n * Reads one page of a model's rows by cursor, for a list that changes while\n * it is read.\n *\n * @example\n * ```typescript\n * const page = await db.$cursor.thread.findMany({\n * cursor: ctx.query('cursor'),\n * take: 20,\n * orderBy: [{ updatedAt: 'desc' }, { id: 'desc' }],\n * where: { boardId },\n * })\n * ```\n */\n $cursor: CursorReader<Schema>\n}\n\n/** The subset of a client this reader needs: a delegate per model key. */\ntype DelegateSource = Record<string, CursorPageDelegate<Record<string, unknown>> | undefined>\n\n/**\n * Builds the reader eagerly, one entry per model the schema declares.\n *\n * A plain object rather than a `Proxy`: the model list is known here, and a\n * proxy would answer every property — `then`, `constructor`, an inspector's\n * probe — with something that looks like a reader.\n *\n * The delegate is resolved per call, not captured: ZenStack builds a fresh CRUD\n * handler on each model access, and holding one would pin it for the life of\n * the client.\n */\nexport function createCursorReader<Schema extends SchemaDef>(\n client: unknown,\n schema: Schema,\n): CursorReader<Schema> {\n const source = client as DelegateSource\n const models = Object.keys(schema.models ?? {})\n\n const modelEntries = models.map((model) => {\n const key = (model.charAt(0).toLowerCase() + model.slice(1))\n\n const reader = {\n findMany: (args: Record<string, unknown>) => {\n const delegate = source[key]\n if (!delegate) throw new CursorModelUnavailableError(key)\n\n const { cursor, take, orderBy, uniqueBy, cursorName, where, include, select, omit } = args as {\n cursor?: string | null\n take: number\n orderBy: Record<string, CursorSortOrder> | Record<string, CursorSortOrder>[]\n uniqueBy?: string\n cursorName?: string\n where?: Record<string, unknown>\n include?: Record<string, unknown>\n select?: Record<string, unknown>\n omit?: Record<string, unknown>\n }\n\n return readCursorPage(delegate, {\n cursor,\n take,\n orderBy,\n uniqueBy,\n cursorName,\n where,\n include,\n select,\n omit,\n })\n },\n }\n\n return [key, reader] as const\n })\n\n const sourceReader: CursorSourceReader = {\n $from: (delegate, args) => readCursorPage(delegate, args),\n }\n\n return Object.assign(Object.fromEntries(modelEntries), sourceReader) as CursorReader<Schema>\n}\n","import { HttpException } from 'stratal/errors'\n\nexport class RecordNotFoundError extends HttpException {\n constructor(public readonly details?: string, cause?: unknown) {\n super(404, 'Record not found', cause)\n }\n}\n","import { HttpException } from 'stratal/errors'\n\nexport class UniqueConstraintError extends HttpException {\n constructor(public readonly fields?: string[], cause?: unknown) {\n super(409, 'Record already exists', cause)\n }\n}\n","import { ORMError, ORMErrorReason } from '@zenstackhq/orm';\nimport { type ApplicationError, DatabaseError } from 'stratal/errors';\nimport { RecordNotFoundError } from './record-not-found.error';\nimport { UniqueConstraintError } from './unique-constraint.error';\n\nexport function fromZenStackError(error: unknown): ApplicationError {\n if (error instanceof ORMError) {\n switch (error.reason) {\n case ORMErrorReason.NOT_FOUND:\n return new RecordNotFoundError(error.model, error)\n case ORMErrorReason.DB_QUERY_ERROR:\n return parseDatabaseError(error)\n case ORMErrorReason.INVALID_INPUT:\n return new DatabaseError('Invalid database query', error)\n case ORMErrorReason.CONFIG_ERROR:\n return new DatabaseError('Database configuration error', error)\n case ORMErrorReason.NOT_SUPPORTED:\n return new DatabaseError('Operation not supported', error)\n case ORMErrorReason.INTERNAL_ERROR:\n return new DatabaseError('Database internal error', error)\n default:\n return new DatabaseError('Database error', error)\n }\n }\n return new DatabaseError('Database error', error)\n}\n\nfunction parseDatabaseError(error: ORMError): ApplicationError {\n const dbErrorCode = error.dbErrorCode as string | undefined\n if (dbErrorCode) {\n if (dbErrorCode === '23505') return new UniqueConstraintError([error.model ?? 'unknown'], error)\n if (dbErrorCode === '23503') return new DatabaseError(withCode('Foreign key constraint violation', dbErrorCode), error)\n if (dbErrorCode === '23502') return new DatabaseError(withCode('Required field is missing', dbErrorCode), error)\n if (dbErrorCode === '23514') return new DatabaseError(withCode('Database constraint violated', dbErrorCode), error)\n if (dbErrorCode === '42P01') return new DatabaseError(withCode('Table does not exist', dbErrorCode), error)\n if (dbErrorCode === '42703') return new DatabaseError(withCode('Column does not exist', dbErrorCode), error)\n if (dbErrorCode.startsWith('42')) return new DatabaseError(withCode('Database syntax or access error', dbErrorCode), error)\n if (dbErrorCode.startsWith('22')) return new DatabaseError(withCode('Invalid data value for column', dbErrorCode), error)\n if (dbErrorCode.startsWith('08')) return new DatabaseError(withCode('Database connection failed', dbErrorCode), error)\n if (dbErrorCode === '57014') return new DatabaseError(withCode('Database query timeout', dbErrorCode), error)\n if (dbErrorCode.startsWith('40')) return new DatabaseError(withCode('Transaction conflict or deadlock', dbErrorCode), error)\n if (dbErrorCode === '53300') return new DatabaseError(withCode('Too many database connections', dbErrorCode), error)\n return new DatabaseError(withCode('Database error', dbErrorCode), error)\n }\n return new DatabaseError('Database error', error)\n}\n\n/**\n * Append the raw SQLSTATE to a human message so an operator can tell distinct\n * failures apart in production logs. The 5-character SQLSTATE is a fixed\n * PostgreSQL error code (e.g. `22021`), not query text, values, or schema — it\n * carries no sensitive data, so it is safe to surface where the full driver\n * message and stack are deliberately withheld.\n */\nfunction withCode(message: string, dbErrorCode: string): string {\n return `${message} [SQLSTATE ${dbErrorCode}]`\n}\n","import { type RuntimePlugin } from '@zenstackhq/orm'\nimport { type SchemaDef } from '@zenstackhq/orm/schema'\nimport { fromZenStackError } from '../errors'\n\n/**\n * ZenStack runtime plugin that transforms ORM errors into ApplicationError instances.\n *\n * @example\n * ```typescript\n * super(schema, {\n * dialect: new PostgresDialect({ pool }),\n * plugins: [new ErrorHandlerPlugin()]\n * })\n * ```\n */\nexport class ErrorHandlerPlugin implements RuntimePlugin<SchemaDef, Record<string, unknown>, Record<string, unknown>, {}> {\n readonly id = 'error-handler'\n\n onQuery = async ({ args, proceed }: {\n args: Record<string, unknown> | undefined\n proceed: (args: Record<string, unknown> | undefined) => Promise<unknown>\n }): Promise<unknown> => {\n try {\n return await proceed(args)\n } catch (error) {\n throw fromZenStackError(error)\n }\n }\n}\n","import { type EntityMutationHooksDef, type RuntimePlugin } from '@zenstackhq/orm';\nimport { type SchemaDef } from '@zenstackhq/orm/schema';\nimport type { EventContext, EventName, IEventRegistry } from 'stratal/events';\nimport type { ModelName } from '../event-types';\n\nexport interface EventEmitterPluginOptions {\n eventRegistry: IEventRegistry\n}\n\ntype EntityMutationAction = 'create' | 'update' | 'delete'\n\nconst ENTITY_ACTION_VERB = {\n create: 'created',\n update: 'updated',\n delete: 'deleted',\n} as const satisfies Record<EntityMutationAction, string>\n\ntype Entity = Record<string, unknown>\n\n/**\n * Pair before/after entity snapshots for a mutation. Rows are matched by\n * `id` when both sides carry one, falling back to positional pairing.\n */\nfunction pairEntities(\n before: Entity[] | undefined,\n after: Entity[] | undefined\n): { before: Entity | undefined; after: Entity | undefined }[] {\n const primary = after ?? before ?? []\n const counterpart = after ? before : undefined\n const counterpartById = counterpart\n ? new Map(counterpart.filter((c) => c.id !== undefined).map((c) => [c.id, c]))\n : undefined\n\n return primary.map((entity, index) => {\n const match = counterpart\n ? (entity.id !== undefined ? counterpartById?.get(entity.id) : undefined) ?? counterpart[index]\n : undefined\n\n return after\n ? { before: match, after: entity }\n : { before: entity, after: undefined }\n })\n}\n\n/**\n * ZenStack runtime plugin that emits before/after events for database operations.\n *\n * Emits events in the format:\n * - `before.{Model}.{operation}` - Before the database operation\n * - `after.{Model}.{operation}` - After the database operation\n *\n * Additionally emits entity-mutation events carrying full entity snapshots:\n * - `entity.{Model}.created` - `{ after }`\n * - `entity.{Model}.updated` - `{ before, after }`\n * - `entity.{Model}.deleted` - `{ before }`\n *\n * Entity events are listener-driven: the pre-mutation snapshot is only\n * loaded (inside the mutation's transaction) when `hasListeners()` reports\n * a matching subscription, so models nobody observes pay no cost. Note that\n * a wildcard subscription (`entity`) therefore makes every model pay the\n * pre-read — subscribe per model when cost matters.\n *\n * @example\n * ```typescript\n * super(schema, {\n * dialect: new PostgresDialect({ pool }),\n * plugins: [\n * new EventEmitterPlugin({\n * eventRegistry,\n * })\n * ]\n * })\n * ```\n */\nexport class EventEmitterPlugin implements RuntimePlugin<SchemaDef, Record<string, unknown>, Record<string, unknown>, {}> {\n readonly id = 'event-emitter'\n\n constructor(private options: EventEmitterPluginOptions) { }\n\n onEntityMutation: EntityMutationHooksDef<SchemaDef> = {\n // Run after-hooks inside the mutation's transaction boundary so they\n // execute within the caller's async context (AsyncLocalStorage intact):\n // listeners resolve from the live request scope and tenant schema\n // switching still applies. Post-commit hooks would run detached from the\n // request's ALS. Listeners registered `blocking: false` still do their\n // real work outside the transaction via waitUntil.\n runAfterMutationWithinTransaction: true,\n\n beforeEntityMutation: async (args) => {\n // Created rows have no prior state to snapshot\n if (args.action === 'create') return\n\n const event = `entity.${args.model}.${ENTITY_ACTION_VERB[args.action]}` as EventName\n if (!this.options.eventRegistry.hasListeners(event)) return\n\n // Runs inside the mutation's transaction; ZenStack hands the result\n // to afterEntityMutation as `beforeMutationEntities`\n await args.loadBeforeMutationEntities()\n },\n\n afterEntityMutation: async (args) => {\n const { model, action, beforeMutationEntities } = args\n const verb = ENTITY_ACTION_VERB[action]\n const event = `entity.${model}.${verb}` as EventName\n const { eventRegistry } = this.options\n\n if (!eventRegistry.hasListeners(event)) return\n\n const after = action === 'delete' ? undefined : await args.loadAfterMutationEntities()\n\n for (const pair of pairEntities(beforeMutationEntities, after)) {\n // Producer boundary: ZenStack types `model` as plain string, but at\n // runtime it is always a schema model name — assert that one fact.\n const context: EventContext<'entity'> = {\n model: model as ModelName,\n action: verb,\n before: pair.before,\n after: pair.after,\n }\n await eventRegistry.emit(event, context)\n }\n },\n }\n\n onQuery = async ({ model, operation, args, proceed }: {\n model: string\n operation: string\n args: Record<string, unknown> | undefined\n proceed: (args: Record<string, unknown> | undefined) => Promise<unknown>\n }): Promise<unknown> => {\n const { eventRegistry } = this.options\n const eventBase = `${model}.${operation}`\n\n // Emit BEFORE event\n await eventRegistry.emit(`before.${eventBase}` as EventName, {\n data: args,\n })\n\n // Execute the actual database operation\n const result = await proceed(args)\n\n // Emit AFTER event\n await eventRegistry.emit(`after.${eventBase}` as EventName, {\n data: args,\n result,\n })\n\n return result\n }\n}\n","interface SwitchableClient {\n $schema: { provider: { defaultSchema: string } } & Record<string, unknown>\n schema: unknown\n}\n\n/**\n * Switches the active schema on a ZenStack/Kysely database client by mutating\n * `$schema.provider.defaultSchema`. This causes ZenStack's QueryNameMapper to\n * generate fully-qualified table references (e.g. `\"tenant_123\".\"User\"`).\n *\n * Must be called BEFORE any queries are made on the client.\n *\n * Note: The ZenStack RuntimePlugin `onQuery` hook fires after table names are\n * already resolved, so a plugin-based approach cannot set the schema prefix.\n * Direct client mutation is the only supported method.\n */\nexport class SchemaSwitcher {\n static apply<T>(client: T, schemaName: string): T {\n const c = client as unknown as SwitchableClient\n const switched = {\n ...c.$schema,\n provider: { ...c.$schema.provider, defaultSchema: schemaName },\n }\n c.$schema = switched\n c.schema = switched\n return client\n }\n}\n","import { ZenStackClient, type AnyPlugin } from '@zenstackhq/orm';\nimport type { SchemaDef } from '@zenstackhq/orm/schema';\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport { array, custom, looseObject, minLength, object, optional, refine, string } from 'zod/mini';\nimport { Request as RequestScoped } from 'stratal/di';\nimport type { IEventRegistry } from 'stratal/events';\nimport type { LoggerService } from 'stratal/logger';\nimport { withZodI18n } from 'stratal/validation';\nimport type { DatabaseConnectionConfig } from './database.module';\nimport { createCursorReader } from './pagination/cursor-reader';\nimport { ErrorHandlerPlugin, EventEmitterPlugin } from './plugins';\n\nconst databaseConnectionSchema = object({\n name: string().check(minLength(1, withZodI18n('database.connectionNameRequired'))),\n schema: looseObject({}),\n dialect: custom((value) => typeof value === 'function'),\n plugins: optional(array(looseObject({}))),\n computedFields: optional(looseObject({})),\n})\n\nexport const databaseModuleConfigSchema = object({\n default: string().check(minLength(1, withZodI18n('database.defaultConnectionRequired'))),\n connections: array(databaseConnectionSchema).check(minLength(1, withZodI18n('database.connectionRequired'))),\n}).check(\n refine(\n (config: { connections: { name: string }[] }) => {\n const names = config.connections.map((c) => c.name)\n return new Set(names).size === names.length\n },\n withZodI18n('database.duplicateConnections'),\n ),\n refine(\n (config: { connections: { name: string }[]; default: string }) =>\n config.connections.some((c) => c.name === config.default),\n withZodI18n('database.defaultConnectionNotFound'),\n ),\n)\n\ntype ZenStackClientInstance = InstanceType<typeof ZenStackClient>\n\n/**\n * Puts `$cursor` on a client, base or transaction.\n *\n * Defined on the client rather than contributed as a ZenStack plugin because\n * the client proxy hands a plugin's member back unbound, and this one has to\n * reach the client's own model delegates. Non-enumerable so it stays out of\n * enumeration of the client, and skipped when already present — a reentrant\n * `$transaction` hands back a client that has been through here.\n */\nfunction defineCursorReader(client: object, schema: SchemaDef): void {\n if ('$cursor' in client) return\n\n Object.defineProperty(client, '$cursor', {\n value: createCursorReader(client, schema),\n enumerable: false,\n configurable: true,\n })\n}\n\n/**\n * Wrap a ZenStack client so `$transaction` is reentrant: when a transaction is\n * already open on this connection (tracked per-connection via\n * {@link AsyncLocalStorage}), nested calls run within the active transaction's\n * client instead of opening a new one. ZenStack only reuses a connection when\n * `$transaction` is called on a transaction client; callers holding the base\n * client (e.g. the better-auth adapter, which since better-auth 1.6.11 nests\n * transactions to atomically consume verification rows) would otherwise open a\n * fresh transaction. On a small pool (e.g. a Hyperdrive-fronted `max: 1` pg\n * pool) that inner transaction blocks forever waiting for the connection the\n * outer one holds — a deadlock surfacing as a backend stuck `idle in\n * transaction`. Reusing the active client is also the correct semantics: nested\n * transactions form a single atomic unit.\n *\n * ZenStackClient's constructor returns a Proxy (for dynamic model accessors), so\n * a subclass method override is shadowed — hence the proxy wrapper here.\n *\n * `prepareTransactionClient` runs against every transaction client before the\n * callback sees it, which is how the built-in members reach it. ZenStack builds\n * that client itself and types it with its own contract, so there is no other\n * point at which it passes through this package.\n */\nexport function makeReentrantTransaction<T extends object>(\n client: T,\n activeTransaction: AsyncLocalStorage<ZenStackClientInstance>,\n prepareTransactionClient?: (tx: ZenStackClientInstance) => void,\n): T {\n return new Proxy(client, {\n get(target, prop, receiver) {\n // DI disposal contract (stratal `Disposable`): release the underlying\n // pool/socket when the owning container shuts down (e.g. a Vite HMR\n // reload replacing the Application). Handled here because ZenStack's\n // own constructor proxy shadows subclass method definitions.\n if (prop === Symbol.asyncDispose) {\n return () => (target as ZenStackClientInstance).$disconnect()\n }\n if (prop !== '$transaction') {\n // Forward the receiver so getters/methods resolve `this` against the\n // proxy (correct for layered proxies / accessor properties).\n return Reflect.get(target, prop, receiver)\n }\n // Read the original `$transaction` off the target WITHOUT the receiver — a\n // receiver of the proxy would re-enter this trap and recurse infinitely.\n const transaction = Reflect.get(target, prop) as (\n input: unknown,\n options?: unknown,\n ) => unknown\n return (input: unknown, options?: unknown) => {\n const active = activeTransaction.getStore()\n if (active) {\n return typeof input === 'function'\n ? (input as (tx: ZenStackClientInstance) => unknown)(active)\n : (active.$transaction as (i: unknown, o?: unknown) => unknown)(input, options)\n }\n if (typeof input !== 'function') {\n return transaction.call(target, input, options)\n }\n return transaction.call(\n target,\n (tx: ZenStackClientInstance) => {\n prepareTransactionClient?.(tx)\n return activeTransaction.run(tx, () => (input as (t: ZenStackClientInstance) => unknown)(tx))\n },\n options,\n )\n }\n },\n })\n}\n\nexport interface DatabaseServiceClass {\n new (): InstanceType<typeof ZenStackClient>\n /**\n * Disconnects every still-live client created from this service class.\n * Called by `DatabaseModule.onShutdown` so pools/sockets are released when\n * the Application is torn down (e.g. a Vite HMR reload). The module passes its\n * {@link LoggerService} so disconnect failures are reported through the\n * application logger rather than the bare console.\n */\n disposeInstances(logger: LoggerService): Promise<void>\n}\n\nexport function createDatabaseService(\n conn: DatabaseConnectionConfig,\n eventRegistry: IEventRegistry,\n): DatabaseServiceClass {\n const plugins: AnyPlugin[] = [\n new ErrorHandlerPlugin(),\n new EventEmitterPlugin({\n eventRegistry,\n }),\n ...(conn.plugins ?? []),\n ]\n\n // Tracks the in-flight interactive transaction client for this connection so\n // nested `$transaction` calls reuse it instead of acquiring a second\n // connection. ZenStack's own reuse only triggers when `$transaction` is\n // invoked on a transaction client; callers that hold the base client (e.g.\n // the better-auth adapter, which since better-auth 1.6.11 nests transactions\n // to atomically consume verification rows) instead open a fresh transaction.\n // On a small pool (e.g. a Hyperdrive-fronted `max: 1` pg pool) the inner\n // transaction then blocks forever waiting for the connection the outer one\n // holds — a deadlock that surfaces as a Postgres backend stuck `idle in\n // transaction`. Reusing the active client makes nested transactions share the\n // single connection, which is also the correct semantics (one atomic unit).\n const activeTransaction = new AsyncLocalStorage<InstanceType<typeof ZenStackClient>>()\n\n // Live clients created from this service class, tracked weakly: one per\n // request scope, so each stays GC-able with its request. Dead refs are pruned\n // on each add; live ones are disconnected by `disposeInstances()` on module\n // shutdown.\n //\n // The dialect (and the pg pool it carries) is built FRESH per REQUEST —\n // `conn.dialect()` in the constructor below. This is MANDATORY on the Workers\n // runtime: a pool/socket opened inside one request's I/O context cannot be\n // reused by a later request — workerd cancels the cross-request I/O and the\n // request hangs forever (\"the Worker's code had hung and would never generate\n // a response\"). Memoizing one shared dialect across requests therefore breaks\n // every request after the first, which is why this is `@Request` rather than\n // `@Singleton`.\n //\n // It is `@Request` rather than `@Transient` because the scope is what decides\n // how many pools a request opens. Transient hands every injecting service its\n // own client, so a request resolving a controller, a guard and four services\n // built six ZenStack clients over six pools — each one a fresh schema walk and\n // its own set of connections — for work that shares a single I/O context. The\n // request scope is the widest one a pool may legally span here, so it is the\n // right one: one client, one pool, per request.\n //\n // Every entrypoint already runs inside a request scope — HTTP through\n // `createRequestScope`, and queues, cron, seeders, quarry commands, Durable\n // Objects, Workflows and WorkerEntrypoints through `runInRequestScope` /\n // `runInScope` — so this costs no caller a change.\n //\n // Pool cleanup is at MODULE SHUTDOWN only: `disposeInstances()` disconnects the\n // still-live clients tracked here. The framework does not dispose pools per\n // request, so a pool whose client is GC'd before shutdown is NOT explicitly\n // `$disconnect()`ed — its idle connections are reclaimed by `pg`'s\n // own `idleTimeoutMillis` instead. On the primary target (Workers + Hyperdrive)\n // Hyperdrive fronts these pools and multiplexes the real server connections, so\n // they never accumulate. Consumers deploying against a DIRECT Postgres on a\n // long-lived isolate should give `conn.dialect()` a pool with a short\n // `idleTimeoutMillis` so any leaked-idle connection self-closes promptly.\n const instances = new Set<WeakRef<ZenStackClientInstance>>()\n\n @RequestScoped()\n class DatabaseClient extends ZenStackClient<typeof conn.schema> {\n constructor() {\n const dialect = conn.dialect()\n // ZenStack 3+ requires `computedFields` whenever the schema declares any\n // `@computed` fields, so pass them through when the consumer provides them.\n super(conn.schema, {\n dialect,\n plugins,\n // @ts-expect-error - ZenStack 3+ requires `computedFields` whenever the schema declares any `@computed` fields, so pass them through when the consumer provides them.\n computedFields: conn.computedFields\n })\n defineCursorReader(this, conn.schema)\n\n // ZenStackClient's constructor returns a Proxy (for dynamic model\n // accessors), so subclass method overrides are shadowed. Wrap it in a\n // proxy that makes `$transaction` reentrant. Returning from the\n // constructor replaces the instance DI receives.\n const client = makeReentrantTransaction(\n this as InstanceType<typeof ZenStackClient>,\n activeTransaction,\n (tx) => defineCursorReader(tx, conn.schema),\n )\n for (const ref of instances) {\n if (ref.deref() === undefined) instances.delete(ref)\n }\n instances.add(new WeakRef(client))\n return client\n }\n\n static async disposeInstances(logger: LoggerService): Promise<void> {\n const live = [...instances]\n instances.clear()\n await Promise.all(live.map(async (ref) => {\n const client = ref.deref()\n if (!client) return\n try {\n await client.$disconnect()\n } catch (error) {\n logger.error(\n `Failed to disconnect database client \"${conn.name}\"`,\n error instanceof Error ? error : new Error(String(error)),\n )\n }\n }))\n }\n }\n\n return DatabaseClient\n}\n","export const DATABASE_TOKENS = {\n Options: Symbol.for('stratal:database:options'),\n Services: Symbol.for('stratal:database:services'),\n} as const\n\nimport type { ConnectionName } from './types'\n\nexport function connectionSymbol(name: ConnectionName): symbol {\n return Symbol.for(`stratal:database:connection:${name}`)\n}\n","export const databaseMessages = {\n en: {\n connectionNameRequired: 'Connection name is required',\n defaultConnectionRequired: 'Default connection name is required',\n connectionRequired: 'At least one connection is required',\n duplicateConnections: 'Duplicate connection names found',\n defaultConnectionNotFound: 'Default connection not found in connections',\n },\n} as const\n\ndeclare module 'stratal/i18n' {\n interface AppMessageNamespaces {\n database: typeof databaseMessages['en']\n }\n}\n","import type { AnyPlugin, ClientOptions, ComputedFieldsOptions } from '@zenstackhq/orm';\nimport type { SchemaDef } from '@zenstackhq/schema';\nimport { DI_TOKENS, lazy } from 'stratal/di';\nimport type { IEventRegistry } from 'stratal/events';\nimport { I18nModule } from 'stratal/i18n';\nimport {\n Module,\n type AsyncModuleOptions,\n type DynamicModule,\n type LazyModuleLoader,\n type ModuleContext,\n type OnInitialize,\n type OnShutdown,\n} from 'stratal/module';\nimport { DbGenerateCommand } from './commands/db-generate.command';\nimport { DbPullCommand } from './commands/db-pull.command';\nimport { DbPushCommand } from './commands/db-push.command';\nimport { MigrateDeployCommand } from './commands/migrate-deploy.command';\nimport { MigrateDevCommand } from './commands/migrate-dev.command';\nimport { MigrateResetCommand } from './commands/migrate-reset.command';\nimport { MigrateStatusCommand } from './commands/migrate-status.command';\nimport { createDatabaseService, type DatabaseServiceClass } from './database.helpers';\nimport { connectionSymbol, DATABASE_TOKENS } from './database.tokens';\nimport { databaseMessages } from './i18n';\nimport type { ConnectionName, DefaultConnectionName } from './types';\n\nexport interface DatabaseConnectionConfig<\n Schema extends SchemaDef = SchemaDef,\n Name extends ConnectionName = ConnectionName,\n> {\n name: Name\n schema: Schema\n dialect: () => ClientOptions<SchemaDef>['dialect']\n plugins?: AnyPlugin[]\n /**\n * Schema-level @computed field implementations. Required when the schema\n * declares any `@computed` fields. Keyed by uncapitalized model name; values\n * map field name to a Kysely-expression compute callback.\n */\n computedFields?: ComputedFieldsOptions<Schema>\n}\n\nexport interface DatabaseModuleConfig {\n default: DefaultConnectionName\n connections: DatabaseConnectionConfig[]\n}\n\n@Module({\n imports: [\n I18nModule.registerMessages({ en: { database: databaseMessages.en } }),\n ],\n providers: [\n DbGenerateCommand,\n DbPushCommand,\n DbPullCommand,\n MigrateDevCommand,\n MigrateDeployCommand,\n MigrateStatusCommand,\n MigrateResetCommand,\n ],\n})\nexport class DatabaseModule implements OnInitialize, OnShutdown {\n private readonly services: DatabaseServiceClass[] = []\n\n static forRoot(config: DatabaseModuleConfig): DynamicModule {\n return {\n module: DatabaseModule,\n providers: [\n { provide: DATABASE_TOKENS.Options, useValue: config as unknown as object },\n ],\n }\n }\n\n static forRootAsync(options: AsyncModuleOptions<DatabaseModuleConfig>): DynamicModule {\n return {\n module: DatabaseModule,\n providers: [\n {\n provide: DATABASE_TOKENS.Options,\n useFactory: options.useFactory,\n inject: options.inject,\n },\n ],\n }\n }\n\n async onInitialize(context: ModuleContext): Promise<void> {\n // Awaited, because `forRootAsync` takes a factory typed\n // `TOptions | Promise<TOptions>` and a container resolves it to whatever it\n // returned. Read without awaiting, an async factory hands this a Promise and\n // the loop below walks `undefined` connections — the type permitting exactly\n // what the runtime broke on.\n //\n // What that buys a consumer is a schema behind an `import()`: a generated\n // schema is a large object literal, and a static import evaluates it while\n // the isolate starts, where the runtime's startup budget is. Awaiting here\n // lets it be imported when the module initializes instead.\n const config = await context.container.resolve<DatabaseModuleConfig | Promise<DatabaseModuleConfig>>(\n DATABASE_TOKENS.Options,\n )\n // EventRegistry is loaded on demand — pull in EventsModule via the loader.\n const loader = context.container.resolve<LazyModuleLoader>(DI_TOKENS.LazyModuleLoader)\n const eventsRef = await loader.load(() => import('stratal/events').then((m) => m.EventsModule))\n const eventRegistry = eventsRef.get<IEventRegistry>(DI_TOKENS.EventRegistry)\n for (const conn of config.connections) {\n const Service = createDatabaseService(conn, eventRegistry);\n\n this.services.push(Service)\n context.container.register(connectionSymbol(conn.name), lazy(() => Service))\n }\n\n context.container.registerExisting(DI_TOKENS.Database, connectionSymbol(config.default))\n\n context.logger.info('DatabaseModule initialized')\n }\n\n async onShutdown(context: ModuleContext): Promise<void> {\n // Disconnect every live client so pools/sockets don't outlive the\n // Application across dev-server hot reloads.\n await Promise.all(this.services.map(service => service.disposeInstances(context.logger)))\n this.services.length = 0\n context.logger.info('DatabaseModule shutdown')\n }\n}\n","/**\n * Env binding the framework reads to decide DB connection topology. Set by\n * `@stratal/testing` (the test harness runs against a DIRECT Postgres with no\n * Hyperdrive in front); never set in dev / staging / production.\n */\nexport const DB_SHARED_POOL_ENV = 'STRATAL_DB_SHARED_POOL'\n\n/**\n * Build the lazy pool factory a consumer hands to its ZenStack dialect,\n * choosing the connection topology from the environment instead of hard-coding\n * one. The consumer writes `dialect: () => new PostgresDialect({ pool })` where\n * `pool = createPoolFactory(env, () => new Pool(poolConfig))` — so the prod-vs-\n * test decision lives here, in the framework, not as an `IS_TEST` branch in app\n * config.\n *\n * - **Default (dev / staging / production):** returns a FRESH pool on every\n * call. The dialect is rebuilt per request resolution, so each request owns\n * its own pool/socket — mandatory on workerd, where a pool opened in one\n * request's I/O context cannot be reused by another (the cross-request I/O is\n * cancelled and the request hangs forever). Hyperdrive fronts these pools and\n * multiplexes the real server connections, so they never accumulate.\n * - **Shared (`DB_SHARED_POOL_ENV === 'true'`):** memoizes ONE pool per\n * connection, reused across every resolution. `@stratal/testing` sets this\n * because the test harness hits a direct Postgres with no Hyperdrive — a fresh\n * pool per resolution would exhaust `max_connections` across parallel test\n * files. One shared pool per connection mirrors what Hyperdrive does in prod.\n *\n * This single pool is forced **persistent** ({@link withPersistentConnection}):\n * its idle reaper is disabled regardless of the config `makePool` passed. A pool\n * reused for the whole worker run must never idle-close — a non-zero\n * `idleTimeoutMillis` reaps its connection between operations, and under real\n * network latency (a CI Postgres service, not a local socket) that eviction\n * races in-flight and subsequent queries → \"Connection terminated unexpectedly\",\n * cascading into half-applied writes and cross-test row leakage. It passes on a\n * fast local socket and only surfaces under latency, so consumers can't be\n * trusted to configure it right — the shared branch enforces it. The consumer's\n * own idle settings still apply to the fresh-per-resolution prod pools (below).\n *\n * Either way the pool is created LAZILY: `makePool` is invoked by Kysely on the\n * first query, inside the request's I/O context — never at module-eval / global\n * scope, which workerd forbids (\"Disallowed operation within global scope\").\n */\nexport function createPoolFactory<TPool>(\n env: object,\n makePool: () => TPool,\n): () => Promise<TPool> {\n const shared = (env as Record<string, unknown>)[DB_SHARED_POOL_ENV] === 'true'\n // Returns `Promise<TPool>` (Kysely's dialect contract) without `async` — there\n // is nothing to await; the pool is constructed synchronously and lazily.\n if (!shared) return () => Promise.resolve(makePool())\n let pool: TPool | undefined\n return () => Promise.resolve(pool ??= withPersistentConnection(withIdempotentEnd(makePool())))\n}\n\n/**\n * Force a SHARED pool to keep its connection: disable the idle reaper so it is\n * never torn down between operations. The shared pool is ONE connection reused\n * for the whole worker run, so a non-zero `idleTimeoutMillis` (correct for the\n * fresh-per-resolution prod pools, wrong for this long-lived one) reaps it mid-\n * run; under real network latency that eviction races in-flight/next queries →\n * \"Connection terminated unexpectedly\", cascading into half-applied writes and\n * cross-test row leakage. `pg` reads `options.idleTimeoutMillis`/`allowExitOnIdle`\n * when a client is released, so clearing them on the constructed pool disables\n * future reaping without the consumer having to special-case their pool config.\n * A no-op for pools that don't expose `options` (non-`pg` implementations).\n */\nfunction withPersistentConnection<TPool>(pool: TPool): TPool {\n const candidate = pool as { options?: { idleTimeoutMillis?: number; allowExitOnIdle?: boolean } }\n if (candidate.options) {\n candidate.options.idleTimeoutMillis = 0\n candidate.options.allowExitOnIdle = false\n }\n return pool\n}\n\n/**\n * A SHARED pool is handed to every `@Transient` `DatabaseClient`'s dialect, so on\n * shutdown `DatabaseModule.onShutdown` → `disposeInstances` calls `$disconnect()`\n * (→ Kysely `destroy()` → `pool.end()`) once PER live client instance — all of\n * them targeting the single shared pool. pg-pool throws \"Called end on pool more\n * than once\" on the 2nd+ call, which `disposeInstances` then logs once per extra\n * instance (harmless but noisy, and a real correctness wart). Each owner releasing\n * its reference is legitimate, so make `end()` idempotent: tear the socket down\n * exactly once and have every caller await that same teardown. Fresh-per-resolution\n * pools (the dev/staging/prod default) are untouched — each is already ended once.\n */\nfunction withIdempotentEnd<TPool>(pool: TPool): TPool {\n const candidate = pool as { end?: (...args: unknown[]) => Promise<unknown> }\n if (typeof candidate.end !== 'function') return pool\n const end = candidate.end.bind(candidate)\n let ending: Promise<unknown> | undefined\n candidate.end = () => (ending ??= end())\n return pool\n}\n","import { inject } from 'stratal/di'\nimport type { ConnectionName } from '../types'\nimport { connectionSymbol } from '../database.tokens'\n\nexport function InjectDB(name: ConnectionName): ParameterDecorator {\n return inject(connectionSymbol(name))\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.133.0/helpers/esm/decorate.js
1
+ //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorate.js
2
2
  function __decorate(decorators, target, key, desc) {
3
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -6,7 +6,7 @@ const AC_TOKENS = {
6
6
  Options: Symbol.for("stratal:ac:options")
7
7
  };
8
8
  //#endregion
9
- //#region \0@oxc-project+runtime@0.133.0/helpers/esm/decorateParam.js
9
+ //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateParam.js
10
10
  function __decorateParam(paramIndex, decorator) {
11
11
  return function(target, key) {
12
12
  decorator(target, key, paramIndex);
@@ -15,4 +15,4 @@ function __decorateParam(paramIndex, decorator) {
15
15
  //#endregion
16
16
  export { AC_TOKENS as n, __decorateParam as t };
17
17
 
18
- //# sourceMappingURL=decorateParam-DwV9LSPl.mjs.map
18
+ //# sourceMappingURL=decorateParam-xwTkq9gO.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"decorateParam-DwV9LSPl.mjs","names":[],"sources":["../src/access-control/tokens.ts"],"sourcesContent":["export const AC_TOKENS = {\n /** Request-scoped access service */\n AccessService: Symbol.for('stratal:ac:service'),\n /** Access control module options (ac, roles) */\n Options: Symbol.for('stratal:ac:options'),\n} as const\n"],"mappings":";AAAA,MAAa,YAAY;;CAEvB,eAAe,OAAO,IAAI,oBAAoB;;CAE9C,SAAS,OAAO,IAAI,oBAAoB;AAC1C"}
1
+ {"version":3,"file":"decorateParam-xwTkq9gO.mjs","names":[],"sources":["../src/access-control/tokens.ts"],"sourcesContent":["export const AC_TOKENS = {\n /** Request-scoped access service */\n AccessService: Symbol.for('stratal:ac:service'),\n /** Access control module options (ac, roles) */\n Options: Symbol.for('stratal:ac:options'),\n} as const\n"],"mappings":";AAAA,MAAa,YAAY;;CAEvB,eAAe,OAAO,IAAI,oBAAoB;;CAE9C,SAAS,OAAO,IAAI,oBAAoB;AAC1C"}
@@ -1,6 +1,5 @@
1
- import { M as DatabaseService } from "../index-Dt0YUA7r.mjs";
1
+ import { L as DatabaseService } from "../index-e_u1SRyd.mjs";
2
2
  import { Faker } from "@faker-js/faker";
3
-
4
3
  //#region src/factory/factory.d.ts
5
4
  /**
6
5
  * Factory
@@ -38,7 +37,7 @@ import { Faker } from "@faker-js/faker";
38
37
  * const users = await new UserFactory().count(10).createManyAndReturn(ctx.db)
39
38
  * ```
40
39
  */
41
- declare abstract class Factory<TModel, TCreateInput> {
40
+ export declare abstract class Factory<TModel, TCreateInput> {
42
41
  protected readonly faker: Faker;
43
42
  protected abstract model: string;
44
43
  protected abstract definition(): TCreateInput;
@@ -86,7 +85,7 @@ declare abstract class Factory<TModel, TCreateInput> {
86
85
  * }
87
86
  * ```
88
87
  */
89
- declare class Sequence<T = number> {
88
+ export declare class Sequence<T = number> {
90
89
  private readonly generator?;
91
90
  private current;
92
91
  constructor(generator?: ((n: number) => T) | undefined);
@@ -95,5 +94,4 @@ declare class Sequence<T = number> {
95
94
  reset(): void;
96
95
  }
97
96
  //#endregion
98
- export { Factory, Sequence };
99
97
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/factory/factory.ts","../../src/factory/sequence.ts"],"mappings":";;;;;;AAuCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAAsB,OAAA;EAAA,mBACD,KAAA,EAAO,KAAA;EAAA,mBACP,KAAA;EAAA,mBACA,UAAA,IAAc,YAAA;EAAA,QAEzB,MAAA;EAAA,QACA,MAAA;EAER,KAAA,CAAM,QAAA,GAAW,KAAA,EAAO,YAAA,KAAiB,OAAA,CAAQ,YAAA;EAMjD,KAAA,CAAM,CAAA;EAMN,IAAA,IAAQ,YAAA;EAQR,QAAA,CAAS,KAAA,YAAiB,YAAA;EAKpB,MAAA,CAAO,EAAA,EAAI,eAAA,GAAkB,OAAA,CAAQ,MAAA;EAMrC,UAAA,CAAW,EAAA,EAAI,eAAA,EAAiB,KAAA,YAAiB,OAAA;IAAU,KAAA;EAAA;EAM3D,mBAAA,CAAoB,EAAA,EAAI,eAAA,EAAiB,KAAA,YAAiB,OAAA,CAAQ,MAAA;EAAA,UAM9D,KAAA;AAAA;;;;;;;AAnDZ;;;;;;;;;;;;;;;;;;;;;;;;;cCVa,QAAA;EAAA,iBAGkB,SAAA;EAAA,QAFrB,OAAA;cAEqB,SAAA,KAAa,CAAA,aAAc,CAAA;EAExD,IAAA,IAAQ,CAAA;EAQR,IAAA,IAAQ,CAAA;EAQR,KAAA;AAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/factory/factory.ts","../../src/factory/sequence.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8BAuCsB,QAAQ,QAAQ;qBACjB,OAAO;qBACP;qBACA,cAAc;UAEzB;UACA;EAER,MAAM,WAAW,OAAO,iBAAiB,QAAQ;EAMjD,MAAM;EAMN,QAAQ;EAQR,SAAS,iBAAiB;EAKpB,OAAO,IAAI,kBAAkB,QAAQ;EAMrC,WAAW,IAAI,iBAAiB,iBAAiB;IAAU;;EAM3D,oBAAoB,IAAI,iBAAiB,iBAAiB,QAAQ;YAM9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBC7DC,SAAS;mBAGS;UAFrB;EAER,YAA6B,cAAa,cAAc;EAExD,QAAQ;EAQR,QAAQ;EAQR"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/factory/factory.ts","../../src/factory/sequence.ts"],"sourcesContent":["import { faker, type Faker } from '@faker-js/faker'\nimport type { DatabaseService } from '../database'\n\n/**\n * Factory\n *\n * Abstract base class for creating test data.\n * Integrates with Faker.js for data generation.\n *\n * @example Define a factory\n * ```typescript\n * import { Factory } from '@stratal/framework/factory'\n * import type { User, UserCreateInput } from '@your-app/db'\n *\n * export class UserFactory extends Factory<User, UserCreateInput> {\n * protected model = 'user'\n *\n * protected definition(): UserCreateInput {\n * return {\n * email: this.faker.internet.email(),\n * firstName: this.faker.person.firstName(),\n * lastName: this.faker.person.lastName(),\n * emailVerified: true,\n * }\n * }\n *\n * admin() {\n * return this.state(attrs => ({ ...attrs, role: 'admin' }))\n * }\n * }\n * ```\n *\n * @example Usage\n * ```typescript\n * const user = await new UserFactory().create(ctx.db)\n * const admin = await new UserFactory().admin().create(ctx.db)\n * const users = await new UserFactory().count(10).createManyAndReturn(ctx.db)\n * ```\n */\nexport abstract class Factory<TModel, TCreateInput> {\n protected readonly faker: Faker = faker\n protected abstract model: string\n protected abstract definition(): TCreateInput\n\n private states: ((attrs: TCreateInput) => Partial<TCreateInput>)[] = []\n private _count = 1\n\n state(modifier: (attrs: TCreateInput) => Partial<TCreateInput>): this {\n const clone = this.clone()\n clone.states.push(modifier)\n return clone\n }\n\n count(n: number): this {\n const clone = this.clone()\n clone._count = n\n return clone\n }\n\n make(): TCreateInput {\n let attrs = this.definition()\n for (const modifier of this.states) {\n attrs = { ...attrs, ...modifier(attrs) }\n }\n return attrs\n }\n\n makeMany(count?: number): TCreateInput[] {\n const n = count ?? this._count\n return Array.from({ length: n }, () => this.make())\n }\n\n async create(db: DatabaseService): Promise<TModel> {\n const data = this.make()\n const model = (db as unknown as Record<string, { create: (args: { data: TCreateInput }) => Promise<TModel> }>)[this.model]\n return model.create({ data })\n }\n\n async createMany(db: DatabaseService, count?: number): Promise<{ count: number }> {\n const data = this.makeMany(count)\n const model = (db as unknown as Record<string, { createMany: (args: { data: TCreateInput[] }) => Promise<{ count: number }> }>)[this.model]\n return model.createMany({ data })\n }\n\n async createManyAndReturn(db: DatabaseService, count?: number): Promise<TModel[]> {\n const data = this.makeMany(count)\n const model = (db as unknown as Record<string, { createManyAndReturn: (args: { data: TCreateInput[] }) => Promise<TModel[]> }>)[this.model]\n return model.createManyAndReturn({ data })\n }\n\n protected clone(): this {\n const FactoryClass = this.constructor as new () => this\n const clone = new FactoryClass()\n clone.states = [...this.states]\n clone._count = this._count\n return clone\n }\n}\n","/**\n * Sequence\n *\n * Auto-incrementing sequence generator for creating unique values.\n *\n * @example Basic usage\n * ```typescript\n * const emailSeq = new Sequence((n) => `user${n}@example.com`)\n *\n * emailSeq.next() // 'user1@example.com'\n * emailSeq.next() // 'user2@example.com'\n * emailSeq.reset()\n * emailSeq.next() // 'user1@example.com'\n * ```\n *\n * @example With factory\n * ```typescript\n * const orderSeq = new Sequence((n) => `ORD-${String(n).padStart(6, '0')}`)\n *\n * export class OrderFactory extends Factory<Order, OrderCreateInput> {\n * protected definition() {\n * return {\n * orderNumber: orderSeq.next(),\n * // ...\n * }\n * }\n * }\n * ```\n */\nexport class Sequence<T = number> {\n private current = 0\n\n constructor(private readonly generator?: (n: number) => T) {}\n\n next(): T {\n this.current++\n if (this.generator) {\n return this.generator(this.current)\n }\n return this.current as T\n }\n\n peek(): T {\n const value = this.current + 1\n if (this.generator) {\n return this.generator(value)\n }\n return value as T\n }\n\n reset(): void {\n this.current = 0\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAsB,UAAtB,MAAoD;CAClD,QAAkC;CAIlC,SAAqE,CAAC;CACtE,SAAiB;CAEjB,MAAM,UAAgE;EACpE,MAAM,QAAQ,KAAK,MAAM;EACzB,MAAM,OAAO,KAAK,QAAQ;EAC1B,OAAO;CACT;CAEA,MAAM,GAAiB;EACrB,MAAM,QAAQ,KAAK,MAAM;EACzB,MAAM,SAAS;EACf,OAAO;CACT;CAEA,OAAqB;EACnB,IAAI,QAAQ,KAAK,WAAW;EAC5B,KAAK,MAAM,YAAY,KAAK,QAC1B,QAAQ;GAAE,GAAG;GAAO,GAAG,SAAS,KAAK;EAAE;EAEzC,OAAO;CACT;CAEA,SAAS,OAAgC;EACvC,MAAM,IAAI,SAAS,KAAK;EACxB,OAAO,MAAM,KAAK,EAAE,QAAQ,EAAE,SAAS,KAAK,KAAK,CAAC;CACpD;CAEA,MAAM,OAAO,IAAsC;EACjD,MAAM,OAAO,KAAK,KAAK;EAEvB,OADe,GAAgG,KAAK,OACvG,OAAO,EAAE,KAAK,CAAC;CAC9B;CAEA,MAAM,WAAW,IAAqB,OAA4C;EAChF,MAAM,OAAO,KAAK,SAAS,KAAK;EAEhC,OADe,GAAiH,KAAK,OACxH,WAAW,EAAE,KAAK,CAAC;CAClC;CAEA,MAAM,oBAAoB,IAAqB,OAAmC;EAChF,MAAM,OAAO,KAAK,SAAS,KAAK;EAEhC,OADe,GAAiH,KAAK,OACxH,oBAAoB,EAAE,KAAK,CAAC;CAC3C;CAEA,QAAwB;EACtB,MAAM,eAAe,KAAK;EAC1B,MAAM,QAAQ,IAAI,aAAa;EAC/B,MAAM,SAAS,CAAC,GAAG,KAAK,MAAM;EAC9B,MAAM,SAAS,KAAK;EACpB,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACpEA,IAAa,WAAb,MAAkC;CAGH;CAF7B,UAAkB;CAElB,YAAY,WAA+C;EAA9B,KAAA,YAAA;CAA+B;CAE5D,OAAU;EACR,KAAK;EACL,IAAI,KAAK,WACP,OAAO,KAAK,UAAU,KAAK,OAAO;EAEpC,OAAO,KAAK;CACd;CAEA,OAAU;EACR,MAAM,QAAQ,KAAK,UAAU;EAC7B,IAAI,KAAK,WACP,OAAO,KAAK,UAAU,KAAK;EAE7B,OAAO;CACT;CAEA,QAAc;EACZ,KAAK,UAAU;CACjB;AACF"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/factory/factory.ts","../../src/factory/sequence.ts"],"sourcesContent":["import { faker, type Faker } from '@faker-js/faker'\nimport type { DatabaseService } from '../database'\n\n/**\n * Factory\n *\n * Abstract base class for creating test data.\n * Integrates with Faker.js for data generation.\n *\n * @example Define a factory\n * ```typescript\n * import { Factory } from '@stratal/framework/factory'\n * import type { User, UserCreateInput } from '@your-app/db'\n *\n * export class UserFactory extends Factory<User, UserCreateInput> {\n * protected model = 'user'\n *\n * protected definition(): UserCreateInput {\n * return {\n * email: this.faker.internet.email(),\n * firstName: this.faker.person.firstName(),\n * lastName: this.faker.person.lastName(),\n * emailVerified: true,\n * }\n * }\n *\n * admin() {\n * return this.state(attrs => ({ ...attrs, role: 'admin' }))\n * }\n * }\n * ```\n *\n * @example Usage\n * ```typescript\n * const user = await new UserFactory().create(ctx.db)\n * const admin = await new UserFactory().admin().create(ctx.db)\n * const users = await new UserFactory().count(10).createManyAndReturn(ctx.db)\n * ```\n */\nexport abstract class Factory<TModel, TCreateInput> {\n protected readonly faker: Faker = faker\n protected abstract model: string\n protected abstract definition(): TCreateInput\n\n private states: ((attrs: TCreateInput) => Partial<TCreateInput>)[] = []\n private _count = 1\n\n state(modifier: (attrs: TCreateInput) => Partial<TCreateInput>): this {\n const clone = this.clone()\n clone.states.push(modifier)\n return clone\n }\n\n count(n: number): this {\n const clone = this.clone()\n clone._count = n\n return clone\n }\n\n make(): TCreateInput {\n let attrs = this.definition()\n for (const modifier of this.states) {\n attrs = { ...attrs, ...modifier(attrs) }\n }\n return attrs\n }\n\n makeMany(count?: number): TCreateInput[] {\n const n = count ?? this._count\n return Array.from({ length: n }, () => this.make())\n }\n\n async create(db: DatabaseService): Promise<TModel> {\n const data = this.make()\n const model = (db as unknown as Record<string, { create: (args: { data: TCreateInput }) => Promise<TModel> }>)[this.model]\n return model.create({ data })\n }\n\n async createMany(db: DatabaseService, count?: number): Promise<{ count: number }> {\n const data = this.makeMany(count)\n const model = (db as unknown as Record<string, { createMany: (args: { data: TCreateInput[] }) => Promise<{ count: number }> }>)[this.model]\n return model.createMany({ data })\n }\n\n async createManyAndReturn(db: DatabaseService, count?: number): Promise<TModel[]> {\n const data = this.makeMany(count)\n const model = (db as unknown as Record<string, { createManyAndReturn: (args: { data: TCreateInput[] }) => Promise<TModel[]> }>)[this.model]\n return model.createManyAndReturn({ data })\n }\n\n protected clone(): this {\n const FactoryClass = this.constructor as new () => this\n const clone = new FactoryClass()\n clone.states = [...this.states]\n clone._count = this._count\n return clone\n }\n}\n","/**\n * Sequence\n *\n * Auto-incrementing sequence generator for creating unique values.\n *\n * @example Basic usage\n * ```typescript\n * const emailSeq = new Sequence((n) => `user${n}@example.com`)\n *\n * emailSeq.next() // 'user1@example.com'\n * emailSeq.next() // 'user2@example.com'\n * emailSeq.reset()\n * emailSeq.next() // 'user1@example.com'\n * ```\n *\n * @example With factory\n * ```typescript\n * const orderSeq = new Sequence((n) => `ORD-${String(n).padStart(6, '0')}`)\n *\n * export class OrderFactory extends Factory<Order, OrderCreateInput> {\n * protected definition() {\n * return {\n * orderNumber: orderSeq.next(),\n * // ...\n * }\n * }\n * }\n * ```\n */\nexport class Sequence<T = number> {\n private current = 0\n\n constructor(private readonly generator?: (n: number) => T) {}\n\n next(): T {\n this.current++\n if (this.generator) {\n return this.generator(this.current)\n }\n return this.current as T\n }\n\n peek(): T {\n const value = this.current + 1\n if (this.generator) {\n return this.generator(value)\n }\n return value as T\n }\n\n reset(): void {\n this.current = 0\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAsB,UAAtB,MAAoD;CAClD,QAAkC;CAIlC,SAAqE,CAAC;CACtE,SAAiB;CAEjB,MAAM,UAAgE;EACpE,MAAM,QAAQ,KAAK,MAAM;EACzB,MAAM,OAAO,KAAK,QAAQ;EAC1B,OAAO;CACT;CAEA,MAAM,GAAiB;EACrB,MAAM,QAAQ,KAAK,MAAM;EACzB,MAAM,SAAS;EACf,OAAO;CACT;CAEA,OAAqB;EACnB,IAAI,QAAQ,KAAK,WAAW;EAC5B,KAAK,MAAM,YAAY,KAAK,QAC1B,QAAQ;GAAE,GAAG;GAAO,GAAG,SAAS,KAAK;EAAE;EAEzC,OAAO;CACT;CAEA,SAAS,OAAgC;EACvC,MAAM,IAAI,SAAS,KAAK;EACxB,OAAO,MAAM,KAAK,EAAE,QAAQ,EAAE,SAAS,KAAK,KAAK,CAAC;CACpD;CAEA,MAAM,OAAO,IAAsC;EACjD,MAAM,OAAO,KAAK,KAAK;EAEvB,OADe,GAAgG,KAAK,MACxG,CAAC,OAAO,EAAE,KAAK,CAAC;CAC9B;CAEA,MAAM,WAAW,IAAqB,OAA4C;EAChF,MAAM,OAAO,KAAK,SAAS,KAAK;EAEhC,OADe,GAAiH,KAAK,MACzH,CAAC,WAAW,EAAE,KAAK,CAAC;CAClC;CAEA,MAAM,oBAAoB,IAAqB,OAAmC;EAChF,MAAM,OAAO,KAAK,SAAS,KAAK;EAEhC,OADe,GAAiH,KAAK,MACzH,CAAC,oBAAoB,EAAE,KAAK,CAAC;CAC3C;CAEA,QAAwB;EACtB,MAAM,eAAe,KAAK;EAC1B,MAAM,QAAQ,IAAI,aAAa;EAC/B,MAAM,SAAS,CAAC,GAAG,KAAK,MAAM;EAC9B,MAAM,SAAS,KAAK;EACpB,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACpEA,IAAa,WAAb,MAAkC;CAGH;CAF7B,UAAkB;CAElB,YAAY,WAA+C;EAA9B,KAAA,YAAA;CAA+B;CAE5D,OAAU;EACR,KAAK;EACL,IAAI,KAAK,WACP,OAAO,KAAK,UAAU,KAAK,OAAO;EAEpC,OAAO,KAAK;CACd;CAEA,OAAU;EACR,MAAM,QAAQ,KAAK,UAAU;EAC7B,IAAI,KAAK,WACP,OAAO,KAAK,UAAU,KAAK;EAE7B,OAAO;CACT;CAEA,QAAc;EACZ,KAAK,UAAU;CACjB;AACF"}
@@ -1,5 +1,4 @@
1
- import { AuthGuardOptions, AuthGuardOptions as AuthGuardOptions$1, CanActivate, GUARD_METADATA_KEY, Guard, GuardClass, GuardClass as GuardClass$1, GuardExecutionService, GuardMetadata, UseGuards, getControllerGuards, getMethodGuards } from "stratal/guards";
2
-
1
+ import { AuthGuardOptions, AuthGuardOptions as AuthGuardOptions$1, CanActivate, GUARD_METADATA_KEY, Guard, GuardClass, GuardClass as GuardClass$1, GuardExecutionService, GuardMetadata, GuardRejectedError, UseGuards, getControllerGuards, getMethodGuards } from "stratal/guards";
3
2
  //#region src/guards/auth.guard.d.ts
4
3
  /**
5
4
  * AuthGuard Factory
@@ -32,7 +31,7 @@ import { AuthGuardOptions, AuthGuardOptions as AuthGuardOptions$1, CanActivate,
32
31
  * export class PostsController { }
33
32
  * ```
34
33
  */
35
- declare function AuthGuard(options?: AuthGuardOptions$1): GuardClass$1;
34
+ export declare function AuthGuard(options?: AuthGuardOptions$1): GuardClass$1;
36
35
  //#endregion
37
- export { AuthGuard, type AuthGuardOptions, type CanActivate, GUARD_METADATA_KEY, type Guard, type GuardClass, GuardExecutionService, type GuardMetadata, UseGuards, getControllerGuards, getMethodGuards };
36
+ export { type AuthGuardOptions, type CanActivate, GUARD_METADATA_KEY, type Guard, type GuardClass, GuardExecutionService, type GuardMetadata, GuardRejectedError, UseGuards, getControllerGuards, getMethodGuards };
38
37
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/guards/auth.guard.ts"],"mappings":";;;;;AAqDA;;;;;;;;AAAiE;;;;;;;;;;;;;;;;;;;;;iBAAjD,SAAA,CAAU,OAAA,GAAU,kBAAA,GAAmB,YAAU"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/guards/auth.guard.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAqDgB,UAAU,UAAU,qBAAmB"}
@@ -1,10 +1,10 @@
1
- import { n as AC_TOKENS, t as __decorateParam } from "../decorateParam-DwV9LSPl.mjs";
2
- import { t as __decorate } from "../decorate-B7nr7eBl.mjs";
1
+ import { n as AC_TOKENS, t as __decorateParam } from "../decorateParam-xwTkq9gO.mjs";
2
+ import { t as __decorate } from "../decorate-RQD1h28J.mjs";
3
3
  import { n as UserNotAuthenticatedError } from "../errors-BvJSaUTW.mjs";
4
4
  import { t as InsufficientPermissionsError } from "../insufficient-permissions.error-DeEyZRgy.mjs";
5
5
  import { DI_TOKENS, Transient, inject } from "stratal/di";
6
6
  import { LOGGER_TOKENS } from "stratal/logger";
7
- import { GUARD_METADATA_KEY, GuardExecutionService, UseGuards, getControllerGuards, getMethodGuards } from "stratal/guards";
7
+ import { GUARD_METADATA_KEY, GuardExecutionService, GuardRejectedError, UseGuards, getControllerGuards, getMethodGuards } from "stratal/guards";
8
8
  //#region src/guards/auth.guard.ts
9
9
  function parsePermissions(raw) {
10
10
  return (Array.isArray(raw) ? raw : [raw]).reduce((acc, perm) => {
@@ -93,6 +93,6 @@ function AuthGuard(options) {
93
93
  return ConfiguredAuthGuard;
94
94
  }
95
95
  //#endregion
96
- export { AuthGuard, GUARD_METADATA_KEY, GuardExecutionService, UseGuards, getControllerGuards, getMethodGuards };
96
+ export { AuthGuard, GUARD_METADATA_KEY, GuardExecutionService, GuardRejectedError, UseGuards, getControllerGuards, getMethodGuards };
97
97
 
98
98
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/guards/auth.guard.ts"],"sourcesContent":["import { DI_TOKENS, inject, Transient } from 'stratal/di'\nimport type { AuthGuardOptions, CanActivate, GuardClass } from 'stratal/guards'\nimport { LOGGER_TOKENS, type LoggerService } from 'stratal/logger'\nimport type { RouterContext } from 'stratal/router'\nimport { InsufficientPermissionsError } from '../access-control/errors/insufficient-permissions.error'\nimport type { AccessService } from '../access-control/services/access.service'\nimport { AC_TOKENS } from '../access-control/tokens'\nimport type { AuthContext } from '../context/auth-context'\nimport { UserNotAuthenticatedError } from '../context/errors'\n\nfunction parsePermissions(raw: string | string[]): Record<string, string[]> {\n const list = Array.isArray(raw) ? raw : [raw]\n return list.reduce<Record<string, string[]>>((acc, perm) => {\n const colon = perm.indexOf(':')\n const resource = colon === -1 ? perm : perm.slice(0, colon)\n const action = colon === -1 ? '*' : perm.slice(colon + 1)\n ;(acc[resource] ??= []).push(action)\n return acc\n }, {})\n}\n\n\n/**\n * AuthGuard Factory\n *\n * Creates a guard class that enforces authentication and optional authorization.\n *\n * **Authentication (no permissions):**\n * - Checks if user is authenticated via AuthContext.isAuthenticated()\n * - Throws UserNotAuthenticatedError (401) if not authenticated\n *\n * **Authorization (with permissions):**\n * - First verifies authentication\n * - Then checks permissions via AccessService (reads from AuthContext — no DB hit)\n * - Throws InsufficientPermissionsError (403) if unauthorized\n *\n * @param options - Configuration options\n * @param options.permissions - Required permissions keyed by resource\n * @returns Guard class for use with @UseGuards decorator\n *\n * @example Authentication only\n * ```typescript\n * @UseGuards(AuthGuard())\n * export class ProfileController { }\n * ```\n *\n * @example Authentication with permissions\n * ```typescript\n * @UseGuards(AuthGuard({ permissions: 'posts:update' }))\n * @UseGuards(AuthGuard({ permissions: ['posts:update', 'posts:delete'] }))\n * export class PostsController { }\n * ```\n */\nexport function AuthGuard(options?: AuthGuardOptions): GuardClass {\n const rawPermissions = options?.permissions\n const permissions = rawPermissions ? parsePermissions(rawPermissions) : undefined\n\n @Transient()\n class ConfiguredAuthGuard implements CanActivate {\n constructor(\n @inject(DI_TOKENS.AuthContext) private readonly authContext: AuthContext,\n @inject(LOGGER_TOKENS.LoggerService) private readonly logger: LoggerService,\n @inject(AC_TOKENS.AccessService, { isOptional: true }) private readonly accessService?: AccessService\n ) { }\n\n async canActivate(_context: RouterContext): Promise<boolean> {\n if (!this.authContext.isAuthenticated()) {\n this.logger.debug('Auth guard: User not authenticated')\n throw new UserNotAuthenticatedError()\n }\n\n if (!permissions || Object.keys(permissions).length === 0) {\n this.logger.debug('Auth guard: Authentication passed (no permissions required)')\n return true\n }\n\n const userId = this.authContext.getUserId()\n if (!userId) {\n this.logger.debug('Auth guard: No user ID in context')\n throw new InsufficientPermissionsError(rawPermissions!)\n }\n\n if (this.accessService) {\n const allowed = await this.accessService.hasPermission(userId, permissions)\n\n this.logger.debug('Auth guard: Authorization check', {\n userId,\n permissions,\n allowed,\n })\n\n if (!allowed) {\n throw new InsufficientPermissionsError(rawPermissions!, userId)\n }\n }\n\n return true\n }\n }\n\n return ConfiguredAuthGuard\n}\n"],"mappings":";;;;;;;;AAUA,SAAS,iBAAiB,KAAkD;CAE1E,QADa,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,GAAG,GAChC,QAAkC,KAAK,SAAS;EAC1D,MAAM,QAAQ,KAAK,QAAQ,GAAG;EAC9B,MAAM,WAAW,UAAU,KAAK,OAAO,KAAK,MAAM,GAAG,KAAK;EAC1D,MAAM,SAAS,UAAU,KAAK,MAAM,KAAK,MAAM,QAAQ,CAAC;EACvD,CAAC,IAAI,cAAc,CAAC,GAAG,KAAK,MAAM;EACnC,OAAO;CACT,GAAG,CAAC,CAAC;AACP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,UAAU,SAAwC;CAChE,MAAM,iBAAiB,SAAS;CAChC,MAAM,cAAc,iBAAiB,iBAAiB,cAAc,IAAI,KAAA;CAExE,IAAA,sBAAA,MACM,oBAA2C;EAEG;EACM;EACkB;EAH1E,YACE,aACA,QACA,eACA;GAHgD,KAAA,cAAA;GACM,KAAA,SAAA;GACkB,KAAA,gBAAA;EACtE;EAEJ,MAAM,YAAY,UAA2C;GAC3D,IAAI,CAAC,KAAK,YAAY,gBAAgB,GAAG;IACvC,KAAK,OAAO,MAAM,oCAAoC;IACtD,MAAM,IAAI,0BAA0B;GACtC;GAEA,IAAI,CAAC,eAAe,OAAO,KAAK,WAAW,EAAE,WAAW,GAAG;IACzD,KAAK,OAAO,MAAM,6DAA6D;IAC/E,OAAO;GACT;GAEA,MAAM,SAAS,KAAK,YAAY,UAAU;GAC1C,IAAI,CAAC,QAAQ;IACX,KAAK,OAAO,MAAM,mCAAmC;IACrD,MAAM,IAAI,6BAA6B,cAAe;GACxD;GAEA,IAAI,KAAK,eAAe;IACtB,MAAM,UAAU,MAAM,KAAK,cAAc,cAAc,QAAQ,WAAW;IAE1E,KAAK,OAAO,MAAM,mCAAmC;KACnD;KACA;KACA;IACF,CAAC;IAED,IAAI,CAAC,SACH,MAAM,IAAI,6BAA6B,gBAAiB,MAAM;GAElE;GAEA,OAAO;EACT;CACF;;EAzCC,UAAU;qBAGN,OAAO,UAAU,WAAW,CAAA;qBAC5B,OAAO,cAAc,aAAa,CAAA;qBAClC,OAAO,UAAU,eAAe,EAAE,YAAY,KAAK,CAAC,CAAA;;CAsCzD,OAAO;AACT"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/guards/auth.guard.ts"],"sourcesContent":["import { DI_TOKENS, inject, Transient } from 'stratal/di'\nimport type { AuthGuardOptions, CanActivate, GuardClass } from 'stratal/guards'\nimport { LOGGER_TOKENS, type LoggerService } from 'stratal/logger'\nimport type { RouterContext } from 'stratal/router'\nimport { InsufficientPermissionsError } from '../access-control/errors/insufficient-permissions.error'\nimport type { AccessService } from '../access-control/services/access.service'\nimport { AC_TOKENS } from '../access-control/tokens'\nimport type { AuthContext } from '../context/auth-context'\nimport { UserNotAuthenticatedError } from '../context/errors'\n\nfunction parsePermissions(raw: string | string[]): Record<string, string[]> {\n const list = Array.isArray(raw) ? raw : [raw]\n return list.reduce<Record<string, string[]>>((acc, perm) => {\n const colon = perm.indexOf(':')\n const resource = colon === -1 ? perm : perm.slice(0, colon)\n const action = colon === -1 ? '*' : perm.slice(colon + 1)\n ;(acc[resource] ??= []).push(action)\n return acc\n }, {})\n}\n\n\n/**\n * AuthGuard Factory\n *\n * Creates a guard class that enforces authentication and optional authorization.\n *\n * **Authentication (no permissions):**\n * - Checks if user is authenticated via AuthContext.isAuthenticated()\n * - Throws UserNotAuthenticatedError (401) if not authenticated\n *\n * **Authorization (with permissions):**\n * - First verifies authentication\n * - Then checks permissions via AccessService (reads from AuthContext — no DB hit)\n * - Throws InsufficientPermissionsError (403) if unauthorized\n *\n * @param options - Configuration options\n * @param options.permissions - Required permissions keyed by resource\n * @returns Guard class for use with @UseGuards decorator\n *\n * @example Authentication only\n * ```typescript\n * @UseGuards(AuthGuard())\n * export class ProfileController { }\n * ```\n *\n * @example Authentication with permissions\n * ```typescript\n * @UseGuards(AuthGuard({ permissions: 'posts:update' }))\n * @UseGuards(AuthGuard({ permissions: ['posts:update', 'posts:delete'] }))\n * export class PostsController { }\n * ```\n */\nexport function AuthGuard(options?: AuthGuardOptions): GuardClass {\n const rawPermissions = options?.permissions\n const permissions = rawPermissions ? parsePermissions(rawPermissions) : undefined\n\n @Transient()\n class ConfiguredAuthGuard implements CanActivate {\n constructor(\n @inject(DI_TOKENS.AuthContext) private readonly authContext: AuthContext,\n @inject(LOGGER_TOKENS.LoggerService) private readonly logger: LoggerService,\n @inject(AC_TOKENS.AccessService, { isOptional: true }) private readonly accessService?: AccessService\n ) { }\n\n async canActivate(_context: RouterContext): Promise<boolean> {\n if (!this.authContext.isAuthenticated()) {\n this.logger.debug('Auth guard: User not authenticated')\n throw new UserNotAuthenticatedError()\n }\n\n if (!permissions || Object.keys(permissions).length === 0) {\n this.logger.debug('Auth guard: Authentication passed (no permissions required)')\n return true\n }\n\n const userId = this.authContext.getUserId()\n if (!userId) {\n this.logger.debug('Auth guard: No user ID in context')\n throw new InsufficientPermissionsError(rawPermissions!)\n }\n\n if (this.accessService) {\n const allowed = await this.accessService.hasPermission(userId, permissions)\n\n this.logger.debug('Auth guard: Authorization check', {\n userId,\n permissions,\n allowed,\n })\n\n if (!allowed) {\n throw new InsufficientPermissionsError(rawPermissions!, userId)\n }\n }\n\n return true\n }\n }\n\n return ConfiguredAuthGuard\n}\n"],"mappings":";;;;;;;;AAUA,SAAS,iBAAiB,KAAkD;CAE1E,QADa,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,GAAG,EAAA,CAChC,QAAkC,KAAK,SAAS;EAC1D,MAAM,QAAQ,KAAK,QAAQ,GAAG;EAC9B,MAAM,WAAW,UAAU,KAAK,OAAO,KAAK,MAAM,GAAG,KAAK;EAC1D,MAAM,SAAS,UAAU,KAAK,MAAM,KAAK,MAAM,QAAQ,CAAC;EACvD,CAAC,IAAI,cAAc,CAAC,EAAA,CAAG,KAAK,MAAM;EACnC,OAAO;CACT,GAAG,CAAC,CAAC;AACP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,UAAU,SAAwC;CAChE,MAAM,iBAAiB,SAAS;CAChC,MAAM,cAAc,iBAAiB,iBAAiB,cAAc,IAAI,KAAA;CAExE,IACM,sBADN,MACM,oBAA2C;EAEG;EACM;EACkB;EAH1E,YACE,aACA,QACA,eACA;GAHgD,KAAA,cAAA;GACM,KAAA,SAAA;GACkB,KAAA,gBAAA;EACtE;EAEJ,MAAM,YAAY,UAA2C;GAC3D,IAAI,CAAC,KAAK,YAAY,gBAAgB,GAAG;IACvC,KAAK,OAAO,MAAM,oCAAoC;IACtD,MAAM,IAAI,0BAA0B;GACtC;GAEA,IAAI,CAAC,eAAe,OAAO,KAAK,WAAW,CAAC,CAAC,WAAW,GAAG;IACzD,KAAK,OAAO,MAAM,6DAA6D;IAC/E,OAAO;GACT;GAEA,MAAM,SAAS,KAAK,YAAY,UAAU;GAC1C,IAAI,CAAC,QAAQ;IACX,KAAK,OAAO,MAAM,mCAAmC;IACrD,MAAM,IAAI,6BAA6B,cAAe;GACxD;GAEA,IAAI,KAAK,eAAe;IACtB,MAAM,UAAU,MAAM,KAAK,cAAc,cAAc,QAAQ,WAAW;IAE1E,KAAK,OAAO,MAAM,mCAAmC;KACnD;KACA;KACA;IACF,CAAC;IAED,IAAI,CAAC,SACH,MAAM,IAAI,6BAA6B,gBAAiB,MAAM;GAElE;GAEA,OAAO;EACT;CACF;;EAzCC,UAAU;EAGN,gBAAA,GAAA,OAAO,UAAU,WAAW,CAAA;EAC5B,gBAAA,GAAA,OAAO,cAAc,aAAa,CAAA;EAClC,gBAAA,GAAA,OAAO,UAAU,eAAe,EAAE,YAAY,KAAK,CAAC,CAAA;;CAsCzD,OAAO;AACT"}