@geekmidas/constructs 3.0.12 → 3.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/{Construct-B14miJ6P.cjs → Construct-DXfLPGwg.cjs} +5 -42
- package/dist/{Construct-B14miJ6P.cjs.map → Construct-DXfLPGwg.cjs.map} +1 -1
- package/dist/{HonoEndpointAdaptor-DhphPq5r.cjs → HonoEndpointAdaptor-DFdgCwYU.cjs} +16 -16
- package/dist/{HonoEndpointAdaptor-DhphPq5r.cjs.map → HonoEndpointAdaptor-DFdgCwYU.cjs.map} +1 -1
- package/dist/adaptors/aws.cjs +13 -12
- package/dist/adaptors/aws.cjs.map +1 -1
- package/dist/adaptors/hono.cjs +5 -5
- package/dist/adaptors/testing.cjs +12 -11
- package/dist/adaptors/testing.cjs.map +1 -1
- package/dist/adaptors/trpc.cjs +65 -0
- package/dist/adaptors/trpc.cjs.map +1 -0
- package/dist/adaptors/trpc.d.cts +85 -0
- package/dist/adaptors/trpc.d.cts.map +1 -0
- package/dist/adaptors/trpc.d.mts +85 -0
- package/dist/adaptors/trpc.d.mts.map +1 -0
- package/dist/adaptors/trpc.mjs +63 -0
- package/dist/adaptors/trpc.mjs.map +1 -0
- package/dist/chunk-DWy1uDak.cjs +39 -0
- package/dist/crons/index.cjs +5 -4
- package/dist/crons/index.cjs.map +1 -1
- package/dist/endpoints/index.cjs +7 -6
- package/dist/endpoints/index.cjs.map +1 -1
- package/dist/functions/index.cjs +2 -2
- package/dist/{functions-DiaRAdfp.cjs → functions-CX21wstW.cjs} +11 -10
- package/dist/{functions-DiaRAdfp.cjs.map → functions-CX21wstW.cjs.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/{lazyAccessors-Bc1u42WC.cjs → lazyAccessors-zNy6P6Cg.cjs} +11 -10
- package/dist/{lazyAccessors-Bc1u42WC.cjs.map → lazyAccessors-zNy6P6Cg.cjs.map} +1 -1
- package/dist/{processAudits-CUko08pi.cjs → processAudits-B_a9bDPs.cjs} +3 -3
- package/dist/{processAudits-CUko08pi.cjs.map → processAudits-B_a9bDPs.cjs.map} +1 -1
- package/dist/subscribers/index.cjs +3 -2
- package/dist/subscribers/index.cjs.map +1 -1
- package/package.json +23 -8
- package/src/adaptors/__tests__/trpc.spec.ts +220 -0
- package/src/adaptors/trpc.ts +192 -0
- package/tsdown.config.ts +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"processAudits-CUko08pi.cjs","names":["endpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tany,\n\t\tany,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction,\n\t\tTDatabase,\n\t\tTDatabaseServiceName\n\t>","serviceDiscovery: ServiceDiscovery<any>","logger: TLogger","ctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t}","actor: AuditActor","DefaultAuditor","auditContext: AuditExecutionContext<TAuditAction> | undefined","handler: (auditor?: Auditor<TAuditAction>) => Promise<T>","onComplete?: (response: T, auditor: Auditor<TAuditAction>) => Promise<void>","options?: ExecuteWithAuditTransactionOptions","response"],"sources":["../src/endpoints/processAudits.ts"],"sourcesContent":["import type {\n\tAuditActor,\n\tAuditableAction,\n\tAuditor,\n\tAuditStorage,\n} from '@geekmidas/audit';\nimport { DefaultAuditor } from '@geekmidas/audit';\nimport type { Logger } from '@geekmidas/logger';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service, ServiceDiscovery } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { ActorExtractor, MappedAudit } from './audit';\nimport type { CookieFn, Endpoint, HeaderFn } from './Endpoint';\n\n/**\n * Process declarative audit definitions after successful endpoint execution.\n * Similar to publishConstructEvents for events.\n *\n * @param endpoint - The endpoint with audit configuration\n * @param response - The handler response to generate audit payloads from\n * @param serviceDiscovery - Service discovery for registering audit storage\n * @param logger - Logger for debug/error messages\n * @param ctx - Request context (session, headers, cookies, services)\n * @param existingAuditor - Optional existing auditor instance (e.g., from handler context).\n * If provided, uses this auditor (with its stored transaction).\n * If not provided, creates a new auditor.\n */\nexport async function processEndpointAudits<\n\tTServices extends Service[] = [],\n\tTSession = unknown,\n\tTLogger extends Logger = Logger,\n\tOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n>(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tOutSchema,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tany,\n\t\tany,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction\n\t>,\n\tresponse: InferStandardSchema<OutSchema>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n\tlogger: TLogger,\n\tctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t},\n\texistingAuditor?: Auditor<TAuditAction>,\n): Promise<void> {\n\ttry {\n\t\tconst audits = endpoint.audits as MappedAudit<TAuditAction, OutSchema>[];\n\n\t\t// If we have an existing auditor (from handler context), we need to flush\n\t\t// any manual audits it collected, even if there are no declarative audits\n\t\tconst hasExistingRecords =\n\t\t\texistingAuditor && existingAuditor.getRecords().length > 0;\n\n\t\t// Skip if no declarative audits and no existing records to flush\n\t\tif (!audits?.length && !hasExistingRecords) {\n\t\t\tlogger.debug('No audits to process');\n\t\t\treturn;\n\t\t}\n\n\t\t// If no auditor storage service and we have things to process, warn\n\t\tif (!endpoint.auditorStorageService) {\n\t\t\tif (hasExistingRecords || audits?.length) {\n\t\t\t\tlogger.warn('No auditor storage service available');\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Get or create auditor\n\t\tlet auditor: Auditor<TAuditAction>;\n\n\t\tif (existingAuditor) {\n\t\t\t// Use existing auditor (preserves stored transaction and manual audits)\n\t\t\tauditor = existingAuditor;\n\t\t\tlogger.debug('Using existing auditor from handler context');\n\t\t} else {\n\t\t\t// Create new auditor (backward compatibility)\n\t\t\tconst services = await serviceDiscovery.register([\n\t\t\t\tendpoint.auditorStorageService,\n\t\t\t]);\n\t\t\tconst storage = services[\n\t\t\t\tendpoint.auditorStorageService.serviceName\n\t\t\t] as AuditStorage;\n\n\t\t\t// Extract actor if configured\n\t\t\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\t\t\tif (endpoint.actorExtractor) {\n\t\t\t\ttry {\n\t\t\t\t\tactor = await (\n\t\t\t\t\t\tendpoint.actorExtractor as ActorExtractor<\n\t\t\t\t\t\t\tTServices,\n\t\t\t\t\t\t\tTSession,\n\t\t\t\t\t\t\tTLogger\n\t\t\t\t\t\t>\n\t\t\t\t\t)({\n\t\t\t\t\t\tservices: ctx.services as any,\n\t\t\t\t\t\tsession: ctx.session,\n\t\t\t\t\t\theader: ctx.header,\n\t\t\t\t\t\tcookie: ctx.cookie,\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t\t\t\t// Continue with system actor\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tauditor = new DefaultAuditor<TAuditAction>({\n\t\t\t\tactor,\n\t\t\t\tstorage,\n\t\t\t\tmetadata: {\n\t\t\t\t\tendpoint: endpoint.route,\n\t\t\t\t\tmethod: endpoint.method,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Process each declarative audit\n\t\tif (audits?.length) {\n\t\t\tfor (const audit of audits) {\n\t\t\t\tlogger.debug({ audit: audit.type }, 'Processing declarative audit');\n\n\t\t\t\t// Check when condition\n\t\t\t\tif (audit.when && !audit.when(response as any)) {\n\t\t\t\t\tlogger.debug(\n\t\t\t\t\t\t{ audit: audit.type },\n\t\t\t\t\t\t'Audit skipped due to when condition',\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Extract payload\n\t\t\t\tconst payload = audit.payload(response as any);\n\n\t\t\t\t// Extract entityId if configured\n\t\t\t\tconst entityId = audit.entityId?.(response as any);\n\n\t\t\t\t// Record the audit\n\t\t\t\tauditor.audit(audit.type as any, payload as any, {\n\t\t\t\t\ttable: audit.table,\n\t\t\t\t\tentityId,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\t// Flush audits to storage\n\t\t// Note: If existingAuditor has a stored transaction (via setTransaction),\n\t\t// flush() will use it automatically\n\t\tconst recordCount = auditor.getRecords().length;\n\t\tif (recordCount > 0) {\n\t\t\t// Check if auditor has a stored transaction (for logging purposes)\n\t\t\tconst trx =\n\t\t\t\t'getTransaction' in auditor\n\t\t\t\t\t? (auditor as { getTransaction(): unknown }).getTransaction()\n\t\t\t\t\t: undefined;\n\t\t\tlogger.debug(\n\t\t\t\t{ auditCount: recordCount, hasTransaction: !!trx },\n\t\t\t\t'Flushing audits',\n\t\t\t);\n\t\t\tawait auditor.flush();\n\t\t}\n\t} catch (error) {\n\t\tlogger.error(error as Error, 'Failed to process audits');\n\t\t// Don't rethrow - audit failures shouldn't fail the request\n\t}\n}\n\n/**\n * Context for audit-aware handler execution.\n */\nexport interface AuditExecutionContext<\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n> {\n\t/** The auditor instance for recording audits */\n\tauditor: Auditor<TAuditAction>;\n\t/** The audit storage instance */\n\tstorage: AuditStorage;\n}\n\n/**\n * Create audit context for handler execution.\n * Returns the auditor and storage for use in the handler.\n *\n * @param endpoint - The endpoint with audit configuration\n * @param serviceDiscovery - Service discovery for getting audit storage\n * @param logger - Logger for debug/error messages\n * @param ctx - Request context for actor extraction\n * @returns Audit context with auditor and storage, or undefined if not configured\n */\nexport async function createAuditContext<\n\tTServices extends Service[] = [],\n\tTSession = unknown,\n\tTLogger extends Logger = Logger,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n\tTDatabase = undefined,\n\tTDatabaseServiceName extends string = string,\n>(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tany,\n\t\tany,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction,\n\t\tTDatabase,\n\t\tTDatabaseServiceName\n\t>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n\tlogger: TLogger,\n\tctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t},\n): Promise<AuditExecutionContext<TAuditAction> | undefined> {\n\tif (!endpoint.auditorStorageService) {\n\t\treturn undefined;\n\t}\n\n\tconst services = await serviceDiscovery.register([\n\t\tendpoint.auditorStorageService,\n\t]);\n\tconst storage = services[\n\t\tendpoint.auditorStorageService.serviceName\n\t] as AuditStorage;\n\n\t// Extract actor if configured\n\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\tif (endpoint.actorExtractor) {\n\t\ttry {\n\t\t\tactor = await (\n\t\t\t\tendpoint.actorExtractor as ActorExtractor<TServices, TSession, TLogger>\n\t\t\t)({\n\t\t\t\tservices: ctx.services as any,\n\t\t\t\tsession: ctx.session,\n\t\t\t\theader: ctx.header,\n\t\t\t\tcookie: ctx.cookie,\n\t\t\t\tlogger,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t}\n\t}\n\n\tconst auditor = new DefaultAuditor<TAuditAction>({\n\t\tactor,\n\t\tstorage,\n\t\tmetadata: {\n\t\t\tendpoint: endpoint.route,\n\t\t\tmethod: endpoint.method,\n\t\t},\n\t});\n\n\treturn { auditor, storage };\n}\n\n/**\n * Options for executeWithAuditTransaction.\n */\nexport interface ExecuteWithAuditTransactionOptions {\n\t/**\n\t * Database connection to use for the transaction.\n\t * If this is already a transaction, it will be reused instead of creating a nested one.\n\t * If not provided, the storage's internal database is used.\n\t */\n\tdb?: unknown;\n}\n\n/**\n * Execute a handler with automatic audit transaction support.\n * If the audit storage provides a withTransaction method, wraps execution\n * in a transaction so audits are atomic with handler's database operations.\n *\n * This is database-agnostic - each storage implementation provides its own\n * transaction handling based on the underlying database (Kysely, Drizzle, etc.).\n *\n * If the db parameter is provided and is already a transaction, the storage\n * will reuse it instead of creating a nested transaction (similar to\n * packages/db/src/kysely.ts#withTransaction).\n *\n * @param auditContext - The audit context from createAuditContext\n * @param handler - The handler function to execute (receives auditor)\n * @param onComplete - Called after handler with response, to process declarative audits\n * @param options - Optional configuration including database connection\n * @returns The handler result\n */\nexport async function executeWithAuditTransaction<\n\tT,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n>(\n\tauditContext: AuditExecutionContext<TAuditAction> | undefined,\n\thandler: (auditor?: Auditor<TAuditAction>) => Promise<T>,\n\tonComplete?: (response: T, auditor: Auditor<TAuditAction>) => Promise<void>,\n\toptions?: ExecuteWithAuditTransactionOptions,\n): Promise<T> {\n\t// No audit context - just run handler\n\tif (!auditContext) {\n\t\treturn handler(undefined);\n\t}\n\n\tconst { auditor, storage } = auditContext;\n\n\t// Check if storage provides a transaction wrapper\n\tif (storage.withTransaction) {\n\t\t// Wrap in transaction - audits are atomic with handler operations\n\t\t// The storage's withTransaction handles setTransaction and flush\n\t\t// Pass db so existing transactions are reused\n\t\treturn storage.withTransaction(\n\t\t\tauditor,\n\t\t\tasync () => {\n\t\t\t\tconst response = await handler(auditor);\n\n\t\t\t\t// Process declarative audits within the transaction\n\t\t\t\tif (onComplete) {\n\t\t\t\t\tawait onComplete(response, auditor);\n\t\t\t\t}\n\n\t\t\t\treturn response;\n\t\t\t},\n\t\t\toptions?.db,\n\t\t);\n\t}\n\n\t// No transaction support - run handler and flush audits after\n\tconst response = await handler(auditor);\n\n\tif (onComplete) {\n\t\tawait onComplete(response, auditor);\n\t}\n\n\t// Flush audits (no transaction)\n\tawait auditor.flush();\n\n\treturn response;\n}\n"],"mappings":";;;;;;;;;;;;;;AAkNA,eAAsB,mBAarBA,UAgBAC,kBACAC,QACAC,KAM2D;AAC3D,MAAK,SAAS,sBACb;CAGD,MAAM,WAAW,MAAM,iBAAiB,SAAS,CAChD,SAAS,qBACT,EAAC;CACF,MAAM,UAAU,SACf,SAAS,sBAAsB;CAIhC,IAAIC,QAAoB;EAAE,IAAI;EAAU,MAAM;CAAU;AACxD,KAAI,SAAS,eACZ,KAAI;AACH,UAAQ,MAAM,AACb,SAAS,eACR;GACD,UAAU,IAAI;GACd,SAAS,IAAI;GACb,QAAQ,IAAI;GACZ,QAAQ,IAAI;GACZ;EACA,EAAC;CACF,SAAQ,OAAO;AACf,SAAO,MAAM,OAAgB,qCAAqC;CAClE;CAGF,MAAM,UAAU,IAAIC,iCAA6B;EAChD;EACA;EACA,UAAU;GACT,UAAU,SAAS;GACnB,QAAQ,SAAS;EACjB;CACD;AAED,QAAO;EAAE;EAAS;CAAS;AAC3B;;;;;;;;;;;;;;;;;;;AAgCD,eAAsB,4BAOrBC,cACAC,SACAC,YACAC,SACa;AAEb,MAAK,aACJ,QAAO,eAAkB;CAG1B,MAAM,EAAE,SAAS,SAAS,GAAG;AAG7B,KAAI,QAAQ,gBAIX,QAAO,QAAQ,gBACd,SACA,YAAY;EACX,MAAMC,aAAW,MAAM,QAAQ,QAAQ;AAGvC,MAAI,WACH,OAAM,WAAWA,YAAU,QAAQ;AAGpC,SAAOA;CACP,GACD,SAAS,GACT;CAIF,MAAM,WAAW,MAAM,QAAQ,QAAQ;AAEvC,KAAI,WACH,OAAM,WAAW,UAAU,QAAQ;AAIpC,OAAM,QAAQ,OAAO;AAErB,QAAO;AACP"}
|
|
1
|
+
{"version":3,"file":"processAudits-B_a9bDPs.cjs","names":["endpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tany,\n\t\tany,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction,\n\t\tTDatabase,\n\t\tTDatabaseServiceName\n\t>","serviceDiscovery: ServiceDiscovery<any>","logger: TLogger","ctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t}","actor: AuditActor","DefaultAuditor","auditContext: AuditExecutionContext<TAuditAction> | undefined","handler: (auditor?: Auditor<TAuditAction>) => Promise<T>","onComplete?: (response: T, auditor: Auditor<TAuditAction>) => Promise<void>","options?: ExecuteWithAuditTransactionOptions","response"],"sources":["../src/endpoints/processAudits.ts"],"sourcesContent":["import type {\n\tAuditActor,\n\tAuditableAction,\n\tAuditor,\n\tAuditStorage,\n} from '@geekmidas/audit';\nimport { DefaultAuditor } from '@geekmidas/audit';\nimport type { Logger } from '@geekmidas/logger';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service, ServiceDiscovery } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { ActorExtractor, MappedAudit } from './audit';\nimport type { CookieFn, Endpoint, HeaderFn } from './Endpoint';\n\n/**\n * Process declarative audit definitions after successful endpoint execution.\n * Similar to publishConstructEvents for events.\n *\n * @param endpoint - The endpoint with audit configuration\n * @param response - The handler response to generate audit payloads from\n * @param serviceDiscovery - Service discovery for registering audit storage\n * @param logger - Logger for debug/error messages\n * @param ctx - Request context (session, headers, cookies, services)\n * @param existingAuditor - Optional existing auditor instance (e.g., from handler context).\n * If provided, uses this auditor (with its stored transaction).\n * If not provided, creates a new auditor.\n */\nexport async function processEndpointAudits<\n\tTServices extends Service[] = [],\n\tTSession = unknown,\n\tTLogger extends Logger = Logger,\n\tOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n>(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tOutSchema,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tany,\n\t\tany,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction\n\t>,\n\tresponse: InferStandardSchema<OutSchema>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n\tlogger: TLogger,\n\tctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t},\n\texistingAuditor?: Auditor<TAuditAction>,\n): Promise<void> {\n\ttry {\n\t\tconst audits = endpoint.audits as MappedAudit<TAuditAction, OutSchema>[];\n\n\t\t// If we have an existing auditor (from handler context), we need to flush\n\t\t// any manual audits it collected, even if there are no declarative audits\n\t\tconst hasExistingRecords =\n\t\t\texistingAuditor && existingAuditor.getRecords().length > 0;\n\n\t\t// Skip if no declarative audits and no existing records to flush\n\t\tif (!audits?.length && !hasExistingRecords) {\n\t\t\tlogger.debug('No audits to process');\n\t\t\treturn;\n\t\t}\n\n\t\t// If no auditor storage service and we have things to process, warn\n\t\tif (!endpoint.auditorStorageService) {\n\t\t\tif (hasExistingRecords || audits?.length) {\n\t\t\t\tlogger.warn('No auditor storage service available');\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Get or create auditor\n\t\tlet auditor: Auditor<TAuditAction>;\n\n\t\tif (existingAuditor) {\n\t\t\t// Use existing auditor (preserves stored transaction and manual audits)\n\t\t\tauditor = existingAuditor;\n\t\t\tlogger.debug('Using existing auditor from handler context');\n\t\t} else {\n\t\t\t// Create new auditor (backward compatibility)\n\t\t\tconst services = await serviceDiscovery.register([\n\t\t\t\tendpoint.auditorStorageService,\n\t\t\t]);\n\t\t\tconst storage = services[\n\t\t\t\tendpoint.auditorStorageService.serviceName\n\t\t\t] as AuditStorage;\n\n\t\t\t// Extract actor if configured\n\t\t\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\t\t\tif (endpoint.actorExtractor) {\n\t\t\t\ttry {\n\t\t\t\t\tactor = await (\n\t\t\t\t\t\tendpoint.actorExtractor as ActorExtractor<\n\t\t\t\t\t\t\tTServices,\n\t\t\t\t\t\t\tTSession,\n\t\t\t\t\t\t\tTLogger\n\t\t\t\t\t\t>\n\t\t\t\t\t)({\n\t\t\t\t\t\tservices: ctx.services as any,\n\t\t\t\t\t\tsession: ctx.session,\n\t\t\t\t\t\theader: ctx.header,\n\t\t\t\t\t\tcookie: ctx.cookie,\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t\t\t\t// Continue with system actor\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tauditor = new DefaultAuditor<TAuditAction>({\n\t\t\t\tactor,\n\t\t\t\tstorage,\n\t\t\t\tmetadata: {\n\t\t\t\t\tendpoint: endpoint.route,\n\t\t\t\t\tmethod: endpoint.method,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Process each declarative audit\n\t\tif (audits?.length) {\n\t\t\tfor (const audit of audits) {\n\t\t\t\tlogger.debug({ audit: audit.type }, 'Processing declarative audit');\n\n\t\t\t\t// Check when condition\n\t\t\t\tif (audit.when && !audit.when(response as any)) {\n\t\t\t\t\tlogger.debug(\n\t\t\t\t\t\t{ audit: audit.type },\n\t\t\t\t\t\t'Audit skipped due to when condition',\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Extract payload\n\t\t\t\tconst payload = audit.payload(response as any);\n\n\t\t\t\t// Extract entityId if configured\n\t\t\t\tconst entityId = audit.entityId?.(response as any);\n\n\t\t\t\t// Record the audit\n\t\t\t\tauditor.audit(audit.type as any, payload as any, {\n\t\t\t\t\ttable: audit.table,\n\t\t\t\t\tentityId,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\t// Flush audits to storage\n\t\t// Note: If existingAuditor has a stored transaction (via setTransaction),\n\t\t// flush() will use it automatically\n\t\tconst recordCount = auditor.getRecords().length;\n\t\tif (recordCount > 0) {\n\t\t\t// Check if auditor has a stored transaction (for logging purposes)\n\t\t\tconst trx =\n\t\t\t\t'getTransaction' in auditor\n\t\t\t\t\t? (auditor as { getTransaction(): unknown }).getTransaction()\n\t\t\t\t\t: undefined;\n\t\t\tlogger.debug(\n\t\t\t\t{ auditCount: recordCount, hasTransaction: !!trx },\n\t\t\t\t'Flushing audits',\n\t\t\t);\n\t\t\tawait auditor.flush();\n\t\t}\n\t} catch (error) {\n\t\tlogger.error(error as Error, 'Failed to process audits');\n\t\t// Don't rethrow - audit failures shouldn't fail the request\n\t}\n}\n\n/**\n * Context for audit-aware handler execution.\n */\nexport interface AuditExecutionContext<\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n> {\n\t/** The auditor instance for recording audits */\n\tauditor: Auditor<TAuditAction>;\n\t/** The audit storage instance */\n\tstorage: AuditStorage;\n}\n\n/**\n * Create audit context for handler execution.\n * Returns the auditor and storage for use in the handler.\n *\n * @param endpoint - The endpoint with audit configuration\n * @param serviceDiscovery - Service discovery for getting audit storage\n * @param logger - Logger for debug/error messages\n * @param ctx - Request context for actor extraction\n * @returns Audit context with auditor and storage, or undefined if not configured\n */\nexport async function createAuditContext<\n\tTServices extends Service[] = [],\n\tTSession = unknown,\n\tTLogger extends Logger = Logger,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n\tTDatabase = undefined,\n\tTDatabaseServiceName extends string = string,\n>(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tany,\n\t\tany,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction,\n\t\tTDatabase,\n\t\tTDatabaseServiceName\n\t>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n\tlogger: TLogger,\n\tctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t},\n): Promise<AuditExecutionContext<TAuditAction> | undefined> {\n\tif (!endpoint.auditorStorageService) {\n\t\treturn undefined;\n\t}\n\n\tconst services = await serviceDiscovery.register([\n\t\tendpoint.auditorStorageService,\n\t]);\n\tconst storage = services[\n\t\tendpoint.auditorStorageService.serviceName\n\t] as AuditStorage;\n\n\t// Extract actor if configured\n\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\tif (endpoint.actorExtractor) {\n\t\ttry {\n\t\t\tactor = await (\n\t\t\t\tendpoint.actorExtractor as ActorExtractor<TServices, TSession, TLogger>\n\t\t\t)({\n\t\t\t\tservices: ctx.services as any,\n\t\t\t\tsession: ctx.session,\n\t\t\t\theader: ctx.header,\n\t\t\t\tcookie: ctx.cookie,\n\t\t\t\tlogger,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t}\n\t}\n\n\tconst auditor = new DefaultAuditor<TAuditAction>({\n\t\tactor,\n\t\tstorage,\n\t\tmetadata: {\n\t\t\tendpoint: endpoint.route,\n\t\t\tmethod: endpoint.method,\n\t\t},\n\t});\n\n\treturn { auditor, storage };\n}\n\n/**\n * Options for executeWithAuditTransaction.\n */\nexport interface ExecuteWithAuditTransactionOptions {\n\t/**\n\t * Database connection to use for the transaction.\n\t * If this is already a transaction, it will be reused instead of creating a nested one.\n\t * If not provided, the storage's internal database is used.\n\t */\n\tdb?: unknown;\n}\n\n/**\n * Execute a handler with automatic audit transaction support.\n * If the audit storage provides a withTransaction method, wraps execution\n * in a transaction so audits are atomic with handler's database operations.\n *\n * This is database-agnostic - each storage implementation provides its own\n * transaction handling based on the underlying database (Kysely, Drizzle, etc.).\n *\n * If the db parameter is provided and is already a transaction, the storage\n * will reuse it instead of creating a nested transaction (similar to\n * packages/db/src/kysely.ts#withTransaction).\n *\n * @param auditContext - The audit context from createAuditContext\n * @param handler - The handler function to execute (receives auditor)\n * @param onComplete - Called after handler with response, to process declarative audits\n * @param options - Optional configuration including database connection\n * @returns The handler result\n */\nexport async function executeWithAuditTransaction<\n\tT,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n>(\n\tauditContext: AuditExecutionContext<TAuditAction> | undefined,\n\thandler: (auditor?: Auditor<TAuditAction>) => Promise<T>,\n\tonComplete?: (response: T, auditor: Auditor<TAuditAction>) => Promise<void>,\n\toptions?: ExecuteWithAuditTransactionOptions,\n): Promise<T> {\n\t// No audit context - just run handler\n\tif (!auditContext) {\n\t\treturn handler(undefined);\n\t}\n\n\tconst { auditor, storage } = auditContext;\n\n\t// Check if storage provides a transaction wrapper\n\tif (storage.withTransaction) {\n\t\t// Wrap in transaction - audits are atomic with handler operations\n\t\t// The storage's withTransaction handles setTransaction and flush\n\t\t// Pass db so existing transactions are reused\n\t\treturn storage.withTransaction(\n\t\t\tauditor,\n\t\t\tasync () => {\n\t\t\t\tconst response = await handler(auditor);\n\n\t\t\t\t// Process declarative audits within the transaction\n\t\t\t\tif (onComplete) {\n\t\t\t\t\tawait onComplete(response, auditor);\n\t\t\t\t}\n\n\t\t\t\treturn response;\n\t\t\t},\n\t\t\toptions?.db,\n\t\t);\n\t}\n\n\t// No transaction support - run handler and flush audits after\n\tconst response = await handler(auditor);\n\n\tif (onComplete) {\n\t\tawait onComplete(response, auditor);\n\t}\n\n\t// Flush audits (no transaction)\n\tawait auditor.flush();\n\n\treturn response;\n}\n"],"mappings":";;;;;;;;;;;;;;AAkNA,eAAsB,mBAarBA,UAgBAC,kBACAC,QACAC,KAM2D;AAC3D,MAAK,SAAS,sBACb;CAGD,MAAM,WAAW,MAAM,iBAAiB,SAAS,CAChD,SAAS,qBACT,EAAC;CACF,MAAM,UAAU,SACf,SAAS,sBAAsB;CAIhC,IAAIC,QAAoB;EAAE,IAAI;EAAU,MAAM;CAAU;AACxD,KAAI,SAAS,eACZ,KAAI;AACH,UAAQ,MAAM,AACb,SAAS,eACR;GACD,UAAU,IAAI;GACd,SAAS,IAAI;GACb,QAAQ,IAAI;GACZ,QAAQ,IAAI;GACZ;EACA,EAAC;CACF,SAAQ,OAAO;AACf,SAAO,MAAM,OAAgB,qCAAqC;CAClE;CAGF,MAAM,UAAU,IAAIC,iCAA6B;EAChD;EACA;EACA,UAAU;GACT,UAAU,SAAS;GACnB,QAAQ,SAAS;EACjB;CACD;AAED,QAAO;EAAE;EAAS;CAAS;AAC3B;;;;;;;;;;;;;;;;;;;AAgCD,eAAsB,4BAOrBC,cACAC,SACAC,YACAC,SACa;AAEb,MAAK,aACJ,QAAO,eAAkB;CAG1B,MAAM,EAAE,SAAS,SAAS,GAAG;AAG7B,KAAI,QAAQ,gBAIX,QAAO,QAAQ,gBACd,SACA,YAAY;EACX,MAAMC,aAAW,MAAM,QAAQ,QAAQ;AAGvC,MAAI,WACH,OAAM,WAAWA,YAAU,QAAQ;AAGpC,SAAOA;CACP,GACD,SAAS,GACT;CAIF,MAAM,WAAW,MAAM,QAAQ,QAAQ;AAEvC,KAAI,WACH,OAAM,WAAW,UAAU,QAAQ;AAIpC,OAAM,QAAQ,OAAO;AAErB,QAAO;AACP"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
1
|
+
const require_chunk = require('../chunk-DWy1uDak.cjs');
|
|
2
|
+
const require_Construct = require('../Construct-DXfLPGwg.cjs');
|
|
3
|
+
const __geekmidas_logger_console = require_chunk.__toESM(require("@geekmidas/logger/console"));
|
|
3
4
|
|
|
4
5
|
//#region src/subscribers/Subscriber.ts
|
|
5
6
|
const DEFAULT_LOGGER$1 = new __geekmidas_logger_console.ConsoleLogger();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["DEFAULT_LOGGER","ConsoleLogger","Construct","obj: any","ConstructType","handler: SubscriberHandler<\n\t\t\tTEventPublisher,\n\t\t\tTSubscribedEvents,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tOutSchema\n\t\t>","timeout: number","_subscribedEvents?: TSubscribedEvents","outputSchema?: OutSchema","services: TServices","logger: TLogger","publisherService?: Service<\n\t\t\tTEventPublisherServiceName,\n\t\t\tTEventPublisher\n\t\t>","DEFAULT_LOGGER","timeout: number","schema: T","services: T","logger: T","publisher: Service<TName, T>","event: TEvent","fn: SubscriberHandler<\n\t\t\tTEventPublisher,\n\t\t\tTSubscribedEvents,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tOutSchema\n\t\t>"],"sources":["../../src/subscribers/Subscriber.ts","../../src/subscribers/SubscriberBuilder.ts","../../src/subscribers/index.ts"],"sourcesContent":["import type {\n\tEventPublisher,\n\tExtractPublisherMessage,\n} from '@geekmidas/events';\nimport type { Logger } from '@geekmidas/logger';\nimport { ConsoleLogger } from '@geekmidas/logger/console';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service, ServiceRecord } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { Construct, ConstructType } from '../Construct';\n\nconst DEFAULT_LOGGER = new ConsoleLogger() as any;\n\n// Helper type to extract payload types for subscribed events\ntype ExtractEventPayloads<\n\tTPublisher extends EventPublisher<any> | undefined,\n\tTEventTypes extends any[],\n> = TPublisher extends EventPublisher<any>\n\t? Extract<ExtractPublisherMessage<TPublisher>, { type: TEventTypes[number] }>\n\t: never;\n\nexport class Subscriber<\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\tTEventPublisherServiceName extends string = string,\n\tTSubscribedEvents extends\n\t\tExtractPublisherMessage<TEventPublisher>['type'][] = ExtractPublisherMessage<TEventPublisher>['type'][],\n> extends Construct<\n\tTLogger,\n\tTEventPublisherServiceName,\n\tTEventPublisher,\n\tOutSchema,\n\tTServices\n> {\n\t__IS_SUBSCRIBER__ = true;\n\n\tstatic isSubscriber(\n\t\tobj: any,\n\t): obj is Subscriber<any, any, any, any, any, any> {\n\t\treturn Boolean(\n\t\t\tobj &&\n\t\t\t\tobj.__IS_SUBSCRIBER__ === true &&\n\t\t\t\tobj.type === ConstructType.Subscriber,\n\t\t);\n\t}\n\n\tconstructor(\n\t\tpublic readonly handler: SubscriberHandler<\n\t\t\tTEventPublisher,\n\t\t\tTSubscribedEvents,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tOutSchema\n\t\t>,\n\t\tpublic override readonly timeout: number = 30000,\n\t\tprotected _subscribedEvents?: TSubscribedEvents,\n\t\tpublic override readonly outputSchema?: OutSchema,\n\t\tpublic override readonly services: TServices = [] as unknown as TServices,\n\t\tpublic override readonly logger: TLogger = DEFAULT_LOGGER as TLogger,\n\t\tpublic override readonly publisherService?: Service<\n\t\t\tTEventPublisherServiceName,\n\t\t\tTEventPublisher\n\t\t>,\n\t) {\n\t\tsuper(\n\t\t\tConstructType.Subscriber,\n\t\t\tlogger,\n\t\t\tservices,\n\t\t\t[],\n\t\t\tpublisherService,\n\t\t\toutputSchema,\n\t\t);\n\t}\n\n\tget subscribedEvents(): TSubscribedEvents | undefined {\n\t\treturn this._subscribedEvents;\n\t}\n}\n\n// Handler type for subscribers that receives an array of events\nexport type SubscriberHandler<\n\tTEventPublisher extends EventPublisher<any> | undefined,\n\tTSubscribedEvents extends ExtractPublisherMessage<TEventPublisher>['type'][],\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tOutSchema extends StandardSchemaV1 | undefined = undefined,\n> = (\n\tctx: SubscriberContext<\n\t\tTEventPublisher,\n\t\tTSubscribedEvents,\n\t\tTServices,\n\t\tTLogger\n\t>,\n) => OutSchema extends StandardSchemaV1\n\t? InferStandardSchema<OutSchema> | Promise<InferStandardSchema<OutSchema>>\n\t: any | Promise<any>;\n\n// Context type for subscriber handlers\nexport type SubscriberContext<\n\tTEventPublisher extends EventPublisher<any> | undefined,\n\tTSubscribedEvents extends ExtractPublisherMessage<TEventPublisher>['type'][],\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n> = {\n\tevents: ExtractEventPayloads<TEventPublisher, TSubscribedEvents>[];\n\tservices: ServiceRecord<TServices>;\n\tlogger: TLogger;\n};\n","import type {\n\tEventPublisher,\n\tExtractPublisherMessage,\n} from '@geekmidas/events';\nimport type { Logger } from '@geekmidas/logger';\nimport { DEFAULT_LOGGER } from '@geekmidas/logger/console';\nimport type { Service } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { Subscriber, type SubscriberHandler } from './Subscriber';\n\nexport class SubscriberBuilder<\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\tTEventPublisherServiceName extends string = string,\n\tTSubscribedEvents extends any[] = [],\n> {\n\tprivate _subscribedEvents: TSubscribedEvents = [] as any;\n\tprivate _timeout?: number;\n\tprivate outputSchema?: OutSchema;\n\tprivate _services: TServices = [] as Service[] as TServices;\n\tprivate _logger: TLogger = DEFAULT_LOGGER;\n\tprivate _publisher?: Service<TEventPublisherServiceName, TEventPublisher>;\n\n\tconstructor() {\n\t\tthis._timeout = 30000; // Default timeout\n\t}\n\n\ttimeout(timeout: number): this {\n\t\tthis._timeout = timeout;\n\t\treturn this;\n\t}\n\n\toutput<T extends StandardSchemaV1>(\n\t\tschema: T,\n\t): SubscriberBuilder<\n\t\tTServices,\n\t\tTLogger,\n\t\tT,\n\t\tTEventPublisher,\n\t\tTEventPublisherServiceName,\n\t\tTSubscribedEvents\n\t> {\n\t\tthis.outputSchema = schema as unknown as OutSchema;\n\t\treturn this as any;\n\t}\n\n\tservices<T extends Service[]>(\n\t\tservices: T,\n\t): SubscriberBuilder<\n\t\t[...TServices, ...T],\n\t\tTLogger,\n\t\tOutSchema,\n\t\tTEventPublisher,\n\t\tTEventPublisherServiceName,\n\t\tTSubscribedEvents\n\t> {\n\t\tthis._services = [...this._services, ...services] as any;\n\t\treturn this as any;\n\t}\n\n\tlogger<T extends Logger>(\n\t\tlogger: T,\n\t): SubscriberBuilder<\n\t\tTServices,\n\t\tT,\n\t\tOutSchema,\n\t\tTEventPublisher,\n\t\tTEventPublisherServiceName,\n\t\tTSubscribedEvents\n\t> {\n\t\tthis._logger = logger as unknown as TLogger;\n\t\treturn this as any;\n\t}\n\n\tpublisher<T extends EventPublisher<any>, TName extends string>(\n\t\tpublisher: Service<TName, T>,\n\t): SubscriberBuilder<\n\t\tTServices,\n\t\tTLogger,\n\t\tOutSchema,\n\t\tT,\n\t\tTName,\n\t\tTSubscribedEvents\n\t> {\n\t\tthis._publisher = publisher as any;\n\t\treturn this as any;\n\t}\n\n\tsubscribe<\n\t\tTEvent extends TEventPublisher extends EventPublisher<any>\n\t\t\t?\n\t\t\t\t\t| ExtractPublisherMessage<TEventPublisher>['type']\n\t\t\t\t\t| ExtractPublisherMessage<TEventPublisher>['type'][]\n\t\t\t: never,\n\t>(\n\t\tevent: TEvent,\n\t): SubscriberBuilder<\n\t\tTServices,\n\t\tTLogger,\n\t\tOutSchema,\n\t\tTEventPublisher,\n\t\tTEventPublisherServiceName,\n\t\tTEvent extends any[]\n\t\t\t? [...TSubscribedEvents, ...TEvent]\n\t\t\t: [...TSubscribedEvents, TEvent]\n\t> {\n\t\tconst eventsToAdd = Array.isArray(event) ? event : [event];\n\t\tthis._subscribedEvents = [...this._subscribedEvents, ...eventsToAdd] as any;\n\t\treturn this as any;\n\t}\n\n\thandle(\n\t\tfn: SubscriberHandler<\n\t\t\tTEventPublisher,\n\t\t\tTSubscribedEvents,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tOutSchema\n\t\t>,\n\t): Subscriber<\n\t\tTServices,\n\t\tTLogger,\n\t\tOutSchema,\n\t\tTEventPublisher,\n\t\tTEventPublisherServiceName,\n\t\tTSubscribedEvents\n\t> {\n\t\tconst subscriber = new Subscriber(\n\t\t\tfn,\n\t\t\tthis._timeout,\n\t\t\tthis._subscribedEvents,\n\t\t\tthis.outputSchema,\n\t\t\tthis._services,\n\t\t\tthis._logger,\n\t\t\tthis._publisher,\n\t\t);\n\n\t\t// Reset builder state after creating the subscriber to prevent pollution\n\t\tthis._services = [] as Service[] as TServices;\n\t\tthis._logger = DEFAULT_LOGGER;\n\t\tthis._publisher = undefined;\n\t\tthis._subscribedEvents = [] as any;\n\t\tthis._timeout = 30000; // Reset to default\n\t\tthis.outputSchema = undefined;\n\n\t\treturn subscriber;\n\t}\n}\n","import { SubscriberBuilder } from './SubscriberBuilder';\n\nexport { Subscriber } from './Subscriber';\nexport { SubscriberBuilder };\n\nexport const s = new SubscriberBuilder();\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["DEFAULT_LOGGER","ConsoleLogger","Construct","obj: any","ConstructType","handler: SubscriberHandler<\n\t\t\tTEventPublisher,\n\t\t\tTSubscribedEvents,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tOutSchema\n\t\t>","timeout: number","_subscribedEvents?: TSubscribedEvents","outputSchema?: OutSchema","services: TServices","logger: TLogger","publisherService?: Service<\n\t\t\tTEventPublisherServiceName,\n\t\t\tTEventPublisher\n\t\t>","DEFAULT_LOGGER","timeout: number","schema: T","services: T","logger: T","publisher: Service<TName, T>","event: TEvent","fn: SubscriberHandler<\n\t\t\tTEventPublisher,\n\t\t\tTSubscribedEvents,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tOutSchema\n\t\t>"],"sources":["../../src/subscribers/Subscriber.ts","../../src/subscribers/SubscriberBuilder.ts","../../src/subscribers/index.ts"],"sourcesContent":["import type {\n\tEventPublisher,\n\tExtractPublisherMessage,\n} from '@geekmidas/events';\nimport type { Logger } from '@geekmidas/logger';\nimport { ConsoleLogger } from '@geekmidas/logger/console';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service, ServiceRecord } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { Construct, ConstructType } from '../Construct';\n\nconst DEFAULT_LOGGER = new ConsoleLogger() as any;\n\n// Helper type to extract payload types for subscribed events\ntype ExtractEventPayloads<\n\tTPublisher extends EventPublisher<any> | undefined,\n\tTEventTypes extends any[],\n> = TPublisher extends EventPublisher<any>\n\t? Extract<ExtractPublisherMessage<TPublisher>, { type: TEventTypes[number] }>\n\t: never;\n\nexport class Subscriber<\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\tTEventPublisherServiceName extends string = string,\n\tTSubscribedEvents extends\n\t\tExtractPublisherMessage<TEventPublisher>['type'][] = ExtractPublisherMessage<TEventPublisher>['type'][],\n> extends Construct<\n\tTLogger,\n\tTEventPublisherServiceName,\n\tTEventPublisher,\n\tOutSchema,\n\tTServices\n> {\n\t__IS_SUBSCRIBER__ = true;\n\n\tstatic isSubscriber(\n\t\tobj: any,\n\t): obj is Subscriber<any, any, any, any, any, any> {\n\t\treturn Boolean(\n\t\t\tobj &&\n\t\t\t\tobj.__IS_SUBSCRIBER__ === true &&\n\t\t\t\tobj.type === ConstructType.Subscriber,\n\t\t);\n\t}\n\n\tconstructor(\n\t\tpublic readonly handler: SubscriberHandler<\n\t\t\tTEventPublisher,\n\t\t\tTSubscribedEvents,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tOutSchema\n\t\t>,\n\t\tpublic override readonly timeout: number = 30000,\n\t\tprotected _subscribedEvents?: TSubscribedEvents,\n\t\tpublic override readonly outputSchema?: OutSchema,\n\t\tpublic override readonly services: TServices = [] as unknown as TServices,\n\t\tpublic override readonly logger: TLogger = DEFAULT_LOGGER as TLogger,\n\t\tpublic override readonly publisherService?: Service<\n\t\t\tTEventPublisherServiceName,\n\t\t\tTEventPublisher\n\t\t>,\n\t) {\n\t\tsuper(\n\t\t\tConstructType.Subscriber,\n\t\t\tlogger,\n\t\t\tservices,\n\t\t\t[],\n\t\t\tpublisherService,\n\t\t\toutputSchema,\n\t\t);\n\t}\n\n\tget subscribedEvents(): TSubscribedEvents | undefined {\n\t\treturn this._subscribedEvents;\n\t}\n}\n\n// Handler type for subscribers that receives an array of events\nexport type SubscriberHandler<\n\tTEventPublisher extends EventPublisher<any> | undefined,\n\tTSubscribedEvents extends ExtractPublisherMessage<TEventPublisher>['type'][],\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tOutSchema extends StandardSchemaV1 | undefined = undefined,\n> = (\n\tctx: SubscriberContext<\n\t\tTEventPublisher,\n\t\tTSubscribedEvents,\n\t\tTServices,\n\t\tTLogger\n\t>,\n) => OutSchema extends StandardSchemaV1\n\t? InferStandardSchema<OutSchema> | Promise<InferStandardSchema<OutSchema>>\n\t: any | Promise<any>;\n\n// Context type for subscriber handlers\nexport type SubscriberContext<\n\tTEventPublisher extends EventPublisher<any> | undefined,\n\tTSubscribedEvents extends ExtractPublisherMessage<TEventPublisher>['type'][],\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n> = {\n\tevents: ExtractEventPayloads<TEventPublisher, TSubscribedEvents>[];\n\tservices: ServiceRecord<TServices>;\n\tlogger: TLogger;\n};\n","import type {\n\tEventPublisher,\n\tExtractPublisherMessage,\n} from '@geekmidas/events';\nimport type { Logger } from '@geekmidas/logger';\nimport { DEFAULT_LOGGER } from '@geekmidas/logger/console';\nimport type { Service } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { Subscriber, type SubscriberHandler } from './Subscriber';\n\nexport class SubscriberBuilder<\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTEventPublisher extends EventPublisher<any> | undefined = undefined,\n\tTEventPublisherServiceName extends string = string,\n\tTSubscribedEvents extends any[] = [],\n> {\n\tprivate _subscribedEvents: TSubscribedEvents = [] as any;\n\tprivate _timeout?: number;\n\tprivate outputSchema?: OutSchema;\n\tprivate _services: TServices = [] as Service[] as TServices;\n\tprivate _logger: TLogger = DEFAULT_LOGGER;\n\tprivate _publisher?: Service<TEventPublisherServiceName, TEventPublisher>;\n\n\tconstructor() {\n\t\tthis._timeout = 30000; // Default timeout\n\t}\n\n\ttimeout(timeout: number): this {\n\t\tthis._timeout = timeout;\n\t\treturn this;\n\t}\n\n\toutput<T extends StandardSchemaV1>(\n\t\tschema: T,\n\t): SubscriberBuilder<\n\t\tTServices,\n\t\tTLogger,\n\t\tT,\n\t\tTEventPublisher,\n\t\tTEventPublisherServiceName,\n\t\tTSubscribedEvents\n\t> {\n\t\tthis.outputSchema = schema as unknown as OutSchema;\n\t\treturn this as any;\n\t}\n\n\tservices<T extends Service[]>(\n\t\tservices: T,\n\t): SubscriberBuilder<\n\t\t[...TServices, ...T],\n\t\tTLogger,\n\t\tOutSchema,\n\t\tTEventPublisher,\n\t\tTEventPublisherServiceName,\n\t\tTSubscribedEvents\n\t> {\n\t\tthis._services = [...this._services, ...services] as any;\n\t\treturn this as any;\n\t}\n\n\tlogger<T extends Logger>(\n\t\tlogger: T,\n\t): SubscriberBuilder<\n\t\tTServices,\n\t\tT,\n\t\tOutSchema,\n\t\tTEventPublisher,\n\t\tTEventPublisherServiceName,\n\t\tTSubscribedEvents\n\t> {\n\t\tthis._logger = logger as unknown as TLogger;\n\t\treturn this as any;\n\t}\n\n\tpublisher<T extends EventPublisher<any>, TName extends string>(\n\t\tpublisher: Service<TName, T>,\n\t): SubscriberBuilder<\n\t\tTServices,\n\t\tTLogger,\n\t\tOutSchema,\n\t\tT,\n\t\tTName,\n\t\tTSubscribedEvents\n\t> {\n\t\tthis._publisher = publisher as any;\n\t\treturn this as any;\n\t}\n\n\tsubscribe<\n\t\tTEvent extends TEventPublisher extends EventPublisher<any>\n\t\t\t?\n\t\t\t\t\t| ExtractPublisherMessage<TEventPublisher>['type']\n\t\t\t\t\t| ExtractPublisherMessage<TEventPublisher>['type'][]\n\t\t\t: never,\n\t>(\n\t\tevent: TEvent,\n\t): SubscriberBuilder<\n\t\tTServices,\n\t\tTLogger,\n\t\tOutSchema,\n\t\tTEventPublisher,\n\t\tTEventPublisherServiceName,\n\t\tTEvent extends any[]\n\t\t\t? [...TSubscribedEvents, ...TEvent]\n\t\t\t: [...TSubscribedEvents, TEvent]\n\t> {\n\t\tconst eventsToAdd = Array.isArray(event) ? event : [event];\n\t\tthis._subscribedEvents = [...this._subscribedEvents, ...eventsToAdd] as any;\n\t\treturn this as any;\n\t}\n\n\thandle(\n\t\tfn: SubscriberHandler<\n\t\t\tTEventPublisher,\n\t\t\tTSubscribedEvents,\n\t\t\tTServices,\n\t\t\tTLogger,\n\t\t\tOutSchema\n\t\t>,\n\t): Subscriber<\n\t\tTServices,\n\t\tTLogger,\n\t\tOutSchema,\n\t\tTEventPublisher,\n\t\tTEventPublisherServiceName,\n\t\tTSubscribedEvents\n\t> {\n\t\tconst subscriber = new Subscriber(\n\t\t\tfn,\n\t\t\tthis._timeout,\n\t\t\tthis._subscribedEvents,\n\t\t\tthis.outputSchema,\n\t\t\tthis._services,\n\t\t\tthis._logger,\n\t\t\tthis._publisher,\n\t\t);\n\n\t\t// Reset builder state after creating the subscriber to prevent pollution\n\t\tthis._services = [] as Service[] as TServices;\n\t\tthis._logger = DEFAULT_LOGGER;\n\t\tthis._publisher = undefined;\n\t\tthis._subscribedEvents = [] as any;\n\t\tthis._timeout = 30000; // Reset to default\n\t\tthis.outputSchema = undefined;\n\n\t\treturn subscriber;\n\t}\n}\n","import { SubscriberBuilder } from './SubscriberBuilder';\n\nexport { Subscriber } from './Subscriber';\nexport { SubscriberBuilder };\n\nexport const s = new SubscriberBuilder();\n"],"mappings":";;;;;AAWA,MAAMA,mBAAiB,IAAIC;AAU3B,IAAa,aAAb,cAQUC,4BAMR;CACD,oBAAoB;CAEpB,OAAO,aACNC,KACkD;AAClD,SAAO,QACN,OACC,IAAI,sBAAsB,QAC1B,IAAI,SAASC,gCAAc,WAC5B;CACD;CAED,YACiBC,SAOSC,UAAkB,KACjCC,mBACeC,cACAC,WAAsB,CAAE,GACxBC,SAAkBV,kBAClBW,kBAIxB;AACD,QACCP,gCAAc,YACd,QACA,UACA,CAAE,GACF,kBACA,aACA;EAxBe;EAOS;EACf;EACe;EACA;EACA;EACA;CAazB;CAED,IAAI,mBAAkD;AACrD,SAAO,KAAK;CACZ;AACD;;;;ACrED,IAAa,oBAAb,MAOE;CACD,AAAQ,oBAAuC,CAAE;CACjD,AAAQ;CACR,AAAQ;CACR,AAAQ,YAAuB,CAAE;CACjC,AAAQ,UAAmBQ;CAC3B,AAAQ;CAER,cAAc;AACb,OAAK,WAAW;CAChB;CAED,QAAQC,SAAuB;AAC9B,OAAK,WAAW;AAChB,SAAO;CACP;CAED,OACCC,QAQC;AACD,OAAK,eAAe;AACpB,SAAO;CACP;CAED,SACCC,UAQC;AACD,OAAK,YAAY,CAAC,GAAG,KAAK,WAAW,GAAG,QAAS;AACjD,SAAO;CACP;CAED,OACCC,QAQC;AACD,OAAK,UAAU;AACf,SAAO;CACP;CAED,UACCC,WAQC;AACD,OAAK,aAAa;AAClB,SAAO;CACP;CAED,UAOCC,OAUC;EACD,MAAM,cAAc,MAAM,QAAQ,MAAM,GAAG,QAAQ,CAAC,KAAM;AAC1D,OAAK,oBAAoB,CAAC,GAAG,KAAK,mBAAmB,GAAG,WAAY;AACpE,SAAO;CACP;CAED,OACCC,IAcC;EACD,MAAM,aAAa,IAAI,WACtB,IACA,KAAK,UACL,KAAK,mBACL,KAAK,cACL,KAAK,WACL,KAAK,SACL,KAAK;AAIN,OAAK,YAAY,CAAE;AACnB,OAAK,UAAUP;AACf,OAAK;AACL,OAAK,oBAAoB,CAAE;AAC3B,OAAK,WAAW;AAChB,OAAK;AAEL,SAAO;CACP;AACD;;;;AChJD,MAAa,IAAI,IAAI"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/constructs",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.14",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -74,6 +74,16 @@
|
|
|
74
74
|
"default": "./dist/adaptors/hono.cjs"
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
|
+
"./trpc": {
|
|
78
|
+
"import": {
|
|
79
|
+
"types": "./dist/adaptors/trpc.d.mts",
|
|
80
|
+
"default": "./dist/adaptors/trpc.mjs"
|
|
81
|
+
},
|
|
82
|
+
"require": {
|
|
83
|
+
"types": "./dist/adaptors/trpc.d.cts",
|
|
84
|
+
"default": "./dist/adaptors/trpc.cjs"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
77
87
|
"./aws": {
|
|
78
88
|
"import": {
|
|
79
89
|
"types": "./dist/adaptors/aws.d.mts",
|
|
@@ -121,6 +131,7 @@
|
|
|
121
131
|
"@types/lodash.uniqby": "~4.7.9",
|
|
122
132
|
"@types/pg": "~8.15.6",
|
|
123
133
|
"@types/qs": "~6.15.0",
|
|
134
|
+
"@trpc/server": "~11.16.0",
|
|
124
135
|
"better-auth": "~1.4.18",
|
|
125
136
|
"kysely": "~0.28.8",
|
|
126
137
|
"pg": "~8.16.3",
|
|
@@ -131,15 +142,16 @@
|
|
|
131
142
|
"@geekmidas/db": "^1.0.2",
|
|
132
143
|
"@geekmidas/envkit": "^1.0.7",
|
|
133
144
|
"@geekmidas/errors": "^1.0.1",
|
|
134
|
-
"@geekmidas/
|
|
145
|
+
"@geekmidas/schema": "^1.0.2",
|
|
135
146
|
"@geekmidas/logger": "^1.0.2",
|
|
136
147
|
"@geekmidas/rate-limit": "^2.0.1",
|
|
137
|
-
"@geekmidas/
|
|
138
|
-
"@geekmidas/
|
|
139
|
-
"@geekmidas/
|
|
148
|
+
"@geekmidas/services": "^1.0.4",
|
|
149
|
+
"@geekmidas/testkit": "^1.0.9",
|
|
150
|
+
"@geekmidas/events": "^1.1.4"
|
|
140
151
|
},
|
|
141
152
|
"peerDependencies": {
|
|
142
153
|
"@middy/core": ">=6.3.1",
|
|
154
|
+
"@trpc/server": ">=11.0.0",
|
|
143
155
|
"@types/aws-lambda": ">=8.10.92",
|
|
144
156
|
"hono": ">=4.8.2",
|
|
145
157
|
"msw": ">=2.0.0",
|
|
@@ -148,11 +160,11 @@
|
|
|
148
160
|
"@geekmidas/db": "^1.0.2",
|
|
149
161
|
"@geekmidas/envkit": "^1.0.7",
|
|
150
162
|
"@geekmidas/errors": "^1.0.1",
|
|
151
|
-
"@geekmidas/events": "^1.1.
|
|
163
|
+
"@geekmidas/events": "^1.1.4",
|
|
152
164
|
"@geekmidas/logger": "^1.0.2",
|
|
153
|
-
"@geekmidas/rate-limit": "^2.0.1",
|
|
154
165
|
"@geekmidas/schema": "^1.0.2",
|
|
155
|
-
"@geekmidas/services": "^1.0.
|
|
166
|
+
"@geekmidas/services": "^1.0.4",
|
|
167
|
+
"@geekmidas/rate-limit": "^2.0.1"
|
|
156
168
|
},
|
|
157
169
|
"peerDependenciesMeta": {
|
|
158
170
|
"@geekmidas/audit": {
|
|
@@ -188,6 +200,9 @@
|
|
|
188
200
|
"@middy/core": {
|
|
189
201
|
"optional": true
|
|
190
202
|
},
|
|
203
|
+
"@trpc/server": {
|
|
204
|
+
"optional": true
|
|
205
|
+
},
|
|
191
206
|
"@types/aws-lambda": {
|
|
192
207
|
"optional": true
|
|
193
208
|
},
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { EnvironmentParser } from '@geekmidas/envkit';
|
|
2
|
+
import type { Logger } from '@geekmidas/logger';
|
|
3
|
+
import {
|
|
4
|
+
type Service,
|
|
5
|
+
ServiceDiscovery,
|
|
6
|
+
serviceContext,
|
|
7
|
+
} from '@geekmidas/services';
|
|
8
|
+
import { initTRPC } from '@trpc/server';
|
|
9
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import {
|
|
11
|
+
createRequestContextMiddleware,
|
|
12
|
+
createServicesMiddleware,
|
|
13
|
+
} from '../trpc';
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// ServiceDiscovery is a process-wide singleton that caches resolved
|
|
17
|
+
// instances. Reset so each test gets a fresh registry.
|
|
18
|
+
ServiceDiscovery.reset();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function makeLogger(): Logger {
|
|
22
|
+
const logger: Logger = {
|
|
23
|
+
debug: vi.fn(),
|
|
24
|
+
info: vi.fn(),
|
|
25
|
+
warn: vi.fn(),
|
|
26
|
+
error: vi.fn(),
|
|
27
|
+
fatal: vi.fn(),
|
|
28
|
+
trace: vi.fn(),
|
|
29
|
+
child: vi.fn(() => logger),
|
|
30
|
+
};
|
|
31
|
+
return logger;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('createServicesMiddleware (with envParser)', () => {
|
|
35
|
+
it('resolves services and merges them onto ctx', async () => {
|
|
36
|
+
const databaseService = {
|
|
37
|
+
serviceName: 'database' as const,
|
|
38
|
+
register: () => ({ query: (sql: string) => `result: ${sql}` }),
|
|
39
|
+
} satisfies Service<'database', { query: (sql: string) => string }>;
|
|
40
|
+
|
|
41
|
+
const t = initTRPC.context<{ logger: Logger }>().create();
|
|
42
|
+
const withServices = createServicesMiddleware(
|
|
43
|
+
t.middleware,
|
|
44
|
+
new EnvironmentParser({}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const caller = t.router({
|
|
48
|
+
run: t.procedure
|
|
49
|
+
.use(withServices([databaseService]))
|
|
50
|
+
.query(({ ctx }) => ctx.database.query('select 1')),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const result = await caller.createCaller({ logger: makeLogger() }).run();
|
|
54
|
+
|
|
55
|
+
expect(result).toBe('result: select 1');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('makes serviceContext.getLogger() readable from inside a service method', async () => {
|
|
59
|
+
let observedLogger: Logger | null = null;
|
|
60
|
+
const dbService = {
|
|
61
|
+
serviceName: 'database' as const,
|
|
62
|
+
register: () => ({
|
|
63
|
+
touch() {
|
|
64
|
+
observedLogger = serviceContext.getLogger();
|
|
65
|
+
observedLogger.info('touched');
|
|
66
|
+
return 'ok';
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
} satisfies Service<'database', { touch: () => string }>;
|
|
70
|
+
|
|
71
|
+
const t = initTRPC.context<{ logger: Logger }>().create();
|
|
72
|
+
const withServices = createServicesMiddleware(
|
|
73
|
+
t.middleware,
|
|
74
|
+
new EnvironmentParser({}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const requestLogger = makeLogger();
|
|
78
|
+
|
|
79
|
+
const caller = t.router({
|
|
80
|
+
run: t.procedure
|
|
81
|
+
.use(withServices([dbService]))
|
|
82
|
+
.query(({ ctx }) => ctx.database.touch()),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await caller.createCaller({ logger: requestLogger }).run();
|
|
86
|
+
|
|
87
|
+
// getLogger() returns a request-scoped proxy (not the raw logger), so we
|
|
88
|
+
// assert it delegates to the request logger rather than checking identity.
|
|
89
|
+
expect(observedLogger).not.toBeNull();
|
|
90
|
+
expect(requestLogger.info).toHaveBeenCalledWith('touched');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('throws cleanly when service code runs outside the procedure', () => {
|
|
94
|
+
// Sanity check that serviceContext stays scoped to the procedure call —
|
|
95
|
+
// reads outside any procedure must throw.
|
|
96
|
+
expect(() => serviceContext.getLogger()).toThrow();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('createServicesMiddleware (context-supplied discovery)', () => {
|
|
101
|
+
it('uses ctx.serviceDiscovery when no envParser is provided', async () => {
|
|
102
|
+
const cacheService = {
|
|
103
|
+
serviceName: 'cache' as const,
|
|
104
|
+
register: () => ({ get: (key: string) => `cached:${key}` }),
|
|
105
|
+
} satisfies Service<'cache', { get: (key: string) => string }>;
|
|
106
|
+
|
|
107
|
+
type Ctx = { logger: Logger; serviceDiscovery: ServiceDiscovery };
|
|
108
|
+
const t = initTRPC.context<Ctx>().create();
|
|
109
|
+
const withServices = createServicesMiddleware<Ctx, object>(t.middleware);
|
|
110
|
+
|
|
111
|
+
const caller = t.router({
|
|
112
|
+
run: t.procedure
|
|
113
|
+
.use(withServices([cacheService]))
|
|
114
|
+
.query(({ ctx }) => ctx.cache.get('k')),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const discovery = ServiceDiscovery.getInstance(new EnvironmentParser({}));
|
|
118
|
+
|
|
119
|
+
const result = await caller
|
|
120
|
+
.createCaller({ logger: makeLogger(), serviceDiscovery: discovery })
|
|
121
|
+
.run();
|
|
122
|
+
|
|
123
|
+
expect(result).toBe('cached:k');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('createRequestContextMiddleware', () => {
|
|
128
|
+
it('exposes ctx.logger via serviceContext.getLogger() for the handler', async () => {
|
|
129
|
+
const t = initTRPC.context<{ logger: Logger }>().create();
|
|
130
|
+
const withRequestContext = createRequestContextMiddleware(t.middleware);
|
|
131
|
+
|
|
132
|
+
let observed: Logger | null = null;
|
|
133
|
+
const caller = t.router({
|
|
134
|
+
run: t.procedure.use(withRequestContext).query(() => {
|
|
135
|
+
observed = serviceContext.getLogger();
|
|
136
|
+
observed.info('handled');
|
|
137
|
+
return 'done';
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const requestLogger = makeLogger();
|
|
142
|
+
const result = await caller.createCaller({ logger: requestLogger }).run();
|
|
143
|
+
|
|
144
|
+
expect(result).toBe('done');
|
|
145
|
+
// The request-scoped proxy delegates to ctx.logger.
|
|
146
|
+
expect(observed).not.toBeNull();
|
|
147
|
+
expect(requestLogger.info).toHaveBeenCalledWith('handled');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('auto-generates requestId and startTime when ctx does not provide them', async () => {
|
|
151
|
+
const t = initTRPC.context<{ logger: Logger }>().create();
|
|
152
|
+
const withRequestContext = createRequestContextMiddleware(t.middleware);
|
|
153
|
+
|
|
154
|
+
let requestId: string | null = null;
|
|
155
|
+
let startTime: number | null = null;
|
|
156
|
+
|
|
157
|
+
const caller = t.router({
|
|
158
|
+
run: t.procedure.use(withRequestContext).query(() => {
|
|
159
|
+
requestId = serviceContext.getRequestId();
|
|
160
|
+
startTime = serviceContext.getRequestStartTime();
|
|
161
|
+
return 'ok';
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await caller.createCaller({ logger: makeLogger() }).run();
|
|
166
|
+
|
|
167
|
+
expect(requestId).toBeTypeOf('string');
|
|
168
|
+
expect(requestId!.length).toBeGreaterThan(0);
|
|
169
|
+
expect(typeof startTime).toBe('number');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('honors caller-supplied requestId and startTime when present on ctx', async () => {
|
|
173
|
+
type Ctx = { logger: Logger; requestId?: string; startTime?: number };
|
|
174
|
+
const t = initTRPC.context<Ctx>().create();
|
|
175
|
+
const withRequestContext = createRequestContextMiddleware(t.middleware);
|
|
176
|
+
|
|
177
|
+
let observedId: string | null = null;
|
|
178
|
+
let observedStart: number | null = null;
|
|
179
|
+
|
|
180
|
+
const caller = t.router({
|
|
181
|
+
run: t.procedure.use(withRequestContext).query(() => {
|
|
182
|
+
observedId = serviceContext.getRequestId();
|
|
183
|
+
observedStart = serviceContext.getRequestStartTime();
|
|
184
|
+
return 'ok';
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await caller
|
|
189
|
+
.createCaller({
|
|
190
|
+
logger: makeLogger(),
|
|
191
|
+
requestId: 'req_abc',
|
|
192
|
+
startTime: 12345,
|
|
193
|
+
})
|
|
194
|
+
.run();
|
|
195
|
+
|
|
196
|
+
expect(observedId).toBe('req_abc');
|
|
197
|
+
expect(observedStart).toBe(12345);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('tagging for tooling', () => {
|
|
202
|
+
it('attaches the resolved services tuple to the inner middleware', () => {
|
|
203
|
+
const svc = {
|
|
204
|
+
serviceName: 'sample' as const,
|
|
205
|
+
register: () => ({}),
|
|
206
|
+
} satisfies Service<'sample', {}>;
|
|
207
|
+
|
|
208
|
+
const t = initTRPC.context<{ logger: Logger }>().create();
|
|
209
|
+
const withServices = createServicesMiddleware(
|
|
210
|
+
t.middleware,
|
|
211
|
+
new EnvironmentParser({}),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const builder = withServices([svc]);
|
|
215
|
+
const middlewares = (
|
|
216
|
+
builder as unknown as { _middlewares: Array<{ _services?: Service[] }> }
|
|
217
|
+
)._middlewares;
|
|
218
|
+
expect(middlewares?.at(-1)?._services).toEqual([svc]);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
2
|
+
import type { Logger } from '@geekmidas/logger';
|
|
3
|
+
import {
|
|
4
|
+
runWithRequestContext,
|
|
5
|
+
type Service,
|
|
6
|
+
ServiceDiscovery,
|
|
7
|
+
type ServiceRecord,
|
|
8
|
+
} from '@geekmidas/services';
|
|
9
|
+
import type {
|
|
10
|
+
TRPCMiddlewareBuilder,
|
|
11
|
+
TRPCMiddlewareFunction,
|
|
12
|
+
} from '@trpc/server';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Shape of `t.middleware` from `@trpc/server`. We accept this rather than the
|
|
16
|
+
* initialized `t` object so callers retain ownership of their tRPC instance
|
|
17
|
+
* (no double-initialization, no opinion on context/meta shape).
|
|
18
|
+
*/
|
|
19
|
+
type CreateMiddleware<TContext, TMeta> = <$ContextOverrides>(
|
|
20
|
+
fn: TRPCMiddlewareFunction<
|
|
21
|
+
TContext,
|
|
22
|
+
TMeta,
|
|
23
|
+
object,
|
|
24
|
+
$ContextOverrides,
|
|
25
|
+
unknown
|
|
26
|
+
>,
|
|
27
|
+
) => TRPCMiddlewareBuilder<TContext, TMeta, $ContextOverrides, unknown>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Result of `createServicesMiddleware`: a function that accepts a service
|
|
31
|
+
* tuple and returns a tRPC middleware that merges resolved services onto the
|
|
32
|
+
* context.
|
|
33
|
+
*/
|
|
34
|
+
export type ServicesMiddleware<
|
|
35
|
+
TContext extends object,
|
|
36
|
+
TMeta extends object,
|
|
37
|
+
> = <const T extends Service[]>(
|
|
38
|
+
services: [...T],
|
|
39
|
+
) => TRPCMiddlewareBuilder<TContext, TMeta, ServiceRecord<T>, unknown>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Context shape required by `createServicesMiddleware` overload 2.
|
|
43
|
+
* Procedures that pull services via context-stored discovery must expose it
|
|
44
|
+
* under `serviceDiscovery`.
|
|
45
|
+
*/
|
|
46
|
+
export interface ContextWithServiceDiscovery {
|
|
47
|
+
serviceDiscovery: ServiceDiscovery;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Minimum context required for request-context propagation. `logger` must be
|
|
52
|
+
* present so services can call `serviceContext.getLogger()`. `requestId` and
|
|
53
|
+
* `startTime` are auto-generated if missing.
|
|
54
|
+
*/
|
|
55
|
+
export interface ContextWithLogger {
|
|
56
|
+
logger: Logger;
|
|
57
|
+
requestId?: string;
|
|
58
|
+
startTime?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a tRPC middleware that:
|
|
63
|
+
*
|
|
64
|
+
* 1. Resolves the requested services via `ServiceDiscovery`.
|
|
65
|
+
* 2. Wraps the downstream call in `runWithRequestContext` so any code reached
|
|
66
|
+
* by the procedure (including service method implementations) can read the
|
|
67
|
+
* current logger/request id via `serviceContext`.
|
|
68
|
+
* 3. Merges the resolved services onto the tRPC context so handlers can access
|
|
69
|
+
* them by service name (`ctx.database`, `ctx.cache`, ...).
|
|
70
|
+
*
|
|
71
|
+
* Two overloads:
|
|
72
|
+
* - Pass an `envParser` to create a per-request `ServiceDiscovery` instance.
|
|
73
|
+
* - Omit `envParser` to read `ctx.serviceDiscovery` from the tRPC context.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* import { initTRPC } from '@trpc/server';
|
|
78
|
+
* import { createServicesMiddleware } from '@geekmidas/constructs/trpc';
|
|
79
|
+
*
|
|
80
|
+
* const t = initTRPC.context<Context>().create();
|
|
81
|
+
* const withServices = createServicesMiddleware(t.middleware, envParser);
|
|
82
|
+
*
|
|
83
|
+
* export const authedProcedure = t.procedure.use(
|
|
84
|
+
* withServices([databaseService, cacheService]),
|
|
85
|
+
* );
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function createServicesMiddleware<
|
|
89
|
+
TContext extends ContextWithLogger & object,
|
|
90
|
+
TMeta extends object,
|
|
91
|
+
>(
|
|
92
|
+
mw: CreateMiddleware<TContext, TMeta>,
|
|
93
|
+
envParser: EnvironmentParser<{}>,
|
|
94
|
+
): ServicesMiddleware<TContext, TMeta>;
|
|
95
|
+
export function createServicesMiddleware<
|
|
96
|
+
TContext extends ContextWithLogger & ContextWithServiceDiscovery & object,
|
|
97
|
+
TMeta extends object,
|
|
98
|
+
>(mw: CreateMiddleware<TContext, TMeta>): ServicesMiddleware<TContext, TMeta>;
|
|
99
|
+
export function createServicesMiddleware<
|
|
100
|
+
TContext extends ContextWithLogger & object,
|
|
101
|
+
TMeta extends object,
|
|
102
|
+
>(
|
|
103
|
+
mw: CreateMiddleware<TContext, TMeta>,
|
|
104
|
+
envParser?: EnvironmentParser<{}>,
|
|
105
|
+
): ServicesMiddleware<TContext, TMeta> {
|
|
106
|
+
return (<const T extends Service[]>(services: [...T]) => {
|
|
107
|
+
const builder = mw(async (opts) => {
|
|
108
|
+
const ctx = opts.ctx as TContext & Partial<ContextWithServiceDiscovery>;
|
|
109
|
+
|
|
110
|
+
const discovery =
|
|
111
|
+
ctx.serviceDiscovery ??
|
|
112
|
+
ServiceDiscovery.getInstance(
|
|
113
|
+
envParser ??
|
|
114
|
+
(() => {
|
|
115
|
+
// Hit only if overload 2 was selected but ctx.serviceDiscovery is
|
|
116
|
+
// missing at runtime — surface the mistake immediately rather
|
|
117
|
+
// than letting an undefined env parser fail deep inside register.
|
|
118
|
+
throw new Error(
|
|
119
|
+
'createServicesMiddleware: no `envParser` provided and ' +
|
|
120
|
+
'`ctx.serviceDiscovery` is missing. Pass an EnvironmentParser ' +
|
|
121
|
+
'to createServicesMiddleware(), or attach a ServiceDiscovery ' +
|
|
122
|
+
'instance to the tRPC context.',
|
|
123
|
+
);
|
|
124
|
+
})(),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const requestId = ctx.requestId ?? crypto.randomUUID();
|
|
128
|
+
const startTime = ctx.startTime ?? Date.now();
|
|
129
|
+
|
|
130
|
+
return runWithRequestContext(
|
|
131
|
+
{ logger: ctx.logger, requestId, startTime },
|
|
132
|
+
async () => {
|
|
133
|
+
const resolved = await discovery.register(services);
|
|
134
|
+
return opts.next({
|
|
135
|
+
ctx: { ...opts.ctx, ...resolved } as typeof opts.ctx &
|
|
136
|
+
ServiceRecord<T>,
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Tag the inner middleware function with the requested services so external
|
|
143
|
+
// tooling (e.g. detect-procedures route generators) can introspect a
|
|
144
|
+
// procedure's service dependencies without re-executing middleware.
|
|
145
|
+
const middlewares = (
|
|
146
|
+
builder as unknown as { _middlewares?: Array<{ _services?: Service[] }> }
|
|
147
|
+
)._middlewares;
|
|
148
|
+
if (middlewares?.length) {
|
|
149
|
+
const last = middlewares[middlewares.length - 1];
|
|
150
|
+
if (last) last._services = services as unknown as Service[];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return builder as TRPCMiddlewareBuilder<
|
|
154
|
+
TContext,
|
|
155
|
+
TMeta,
|
|
156
|
+
ServiceRecord<T>,
|
|
157
|
+
unknown
|
|
158
|
+
>;
|
|
159
|
+
}) as ServicesMiddleware<TContext, TMeta>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Create a tRPC middleware that establishes a request context for downstream
|
|
164
|
+
* code without resolving any services. Useful when services aren't needed on
|
|
165
|
+
* a procedure but the handler (or libraries it calls) still wants to read
|
|
166
|
+
* `serviceContext.getLogger()` / `getRequestId()` / `getRequestStartTime()`.
|
|
167
|
+
*
|
|
168
|
+
* `requestId` and `startTime` are pulled from the tRPC context when present,
|
|
169
|
+
* otherwise generated (`crypto.randomUUID()` and `Date.now()`).
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```ts
|
|
173
|
+
* const withRequestContext = createRequestContextMiddleware(t.middleware);
|
|
174
|
+
* export const baseProcedure = t.procedure.use(withRequestContext);
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
export function createRequestContextMiddleware<
|
|
178
|
+
TContext extends ContextWithLogger & object,
|
|
179
|
+
TMeta extends object,
|
|
180
|
+
>(
|
|
181
|
+
mw: CreateMiddleware<TContext, TMeta>,
|
|
182
|
+
): TRPCMiddlewareBuilder<TContext, TMeta, object, unknown> {
|
|
183
|
+
return mw(async (opts) => {
|
|
184
|
+
const ctx = opts.ctx as TContext;
|
|
185
|
+
const requestId = ctx.requestId ?? crypto.randomUUID();
|
|
186
|
+
const startTime = ctx.startTime ?? Date.now();
|
|
187
|
+
return runWithRequestContext(
|
|
188
|
+
{ logger: ctx.logger, requestId, startTime },
|
|
189
|
+
() => opts.next(),
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
}
|