@classytic/arc 2.1.2 → 2.1.7
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/README.md +6 -5
- package/bin/arc.js +1 -0
- package/dist/{EventTransport-BD2U0BTc.d.mts → EventTransport-BkUDYZEb.d.mts} +1 -2
- package/dist/HookSystem-BsGV-j2l.mjs +1 -2
- package/dist/{ResourceRegistry-DsN4KJjV.mjs → ResourceRegistry-7Ic20ZMw.mjs} +1 -2
- package/dist/adapters/index.d.mts +4 -4
- package/dist/audit/index.d.mts +5 -6
- package/dist/audit/index.mjs +2 -3
- package/dist/audit/mongodb.d.mts +4 -4
- package/dist/audit/mongodb.mjs +1 -1
- package/dist/{audited-C3T5DTUx.mjs → audited-CGdLiSlE.mjs} +1 -2
- package/dist/auth/index.d.mts +6 -7
- package/dist/auth/index.mjs +10 -16
- package/dist/auth/redis-session.d.mts +2 -3
- package/dist/auth/redis-session.mjs +1 -2
- package/dist/{betterAuthOpenApi-BrHKeSAx.mjs → betterAuthOpenApi-DjWDddNc.mjs} +2 -3
- package/dist/cache/index.d.mts +3 -4
- package/dist/cache/index.mjs +4 -5
- package/dist/{caching-Bl28lYsR.mjs → caching-GSDJcA6-.mjs} +1 -2
- package/dist/{circuitBreaker-DeY4FCjs.mjs → circuitBreaker-DYhWBW_D.mjs} +1 -2
- package/dist/cli/commands/describe.d.mts +1 -2
- package/dist/cli/commands/describe.mjs +1 -2
- package/dist/cli/commands/docs.d.mts +1 -2
- package/dist/cli/commands/docs.mjs +3 -4
- package/dist/cli/commands/generate.d.mts +6 -2
- package/dist/cli/commands/generate.mjs +89 -58
- package/dist/cli/commands/init.d.mts +1 -2
- package/dist/cli/commands/init.mjs +15 -19
- package/dist/cli/commands/introspect.d.mts +1 -2
- package/dist/cli/commands/introspect.mjs +2 -3
- package/dist/cli/index.d.mts +1 -2
- package/dist/cli/index.mjs +1 -2
- package/dist/constants-DdXFXQtN.mjs +1 -2
- package/dist/core/index.d.mts +4 -4
- package/dist/core/index.mjs +1 -1
- package/dist/{createApp-CUgNqegw.mjs → createApp-D2D5XXaV.mjs} +9 -10
- package/dist/{defineResource-k0_BDn8v.mjs → defineResource-DZVbwsFb.mjs} +17 -39
- package/dist/discovery/index.d.mts +1 -2
- package/dist/discovery/index.mjs +1 -2
- package/dist/docs/index.d.mts +5 -6
- package/dist/docs/index.mjs +5 -4
- package/dist/{elevation-B_2dRLVP.d.mts → elevation-DGo5shaX.d.mts} +1 -2
- package/dist/{elevation-BRy3yFWT.mjs → elevation-DSTbVvYj.mjs} +4 -4
- package/dist/{errorHandler-C1okiriz.mjs → errorHandler-C3GY3_ow.mjs} +2 -3
- package/dist/{errorHandler-BbcgBmIH.d.mts → errorHandler-CW3OOeYq.d.mts} +2 -3
- package/dist/{errors-ChKiFz62.d.mts → errors-DAWRdiYP.d.mts} +1 -2
- package/dist/{errors-B9bZok84.mjs → errors-DBANPbGr.mjs} +1 -2
- package/dist/{eventPlugin-DGR_B2on.mjs → eventPlugin-BEOvaDqo.mjs} +2 -3
- package/dist/{eventPlugin-CTrLH3mt.d.mts → eventPlugin-H6wDDjGO.d.mts} +2 -3
- package/dist/events/index.d.mts +4 -5
- package/dist/events/index.mjs +2 -3
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +1 -2
- package/dist/events/transports/redis.d.mts +2 -3
- package/dist/events/transports/redis.mjs +1 -2
- package/dist/{externalPaths-DlINfKbP.d.mts → externalPaths-SyPF2tgK.d.mts} +1 -2
- package/dist/factory/index.d.mts +8 -9
- package/dist/factory/index.mjs +1 -1
- package/dist/{fastifyAdapter-BkrGrlFi.d.mts → fastifyAdapter-sGkvUvf5.d.mts} +4 -5
- package/dist/{fields-DyaDVX4J.d.mts → fields-Bi_AVKSo.d.mts} +2 -3
- package/dist/{fields-iagOozy0.mjs → fields-CTd_CrKr.mjs} +2 -3
- package/dist/hooks/index.d.mts +3 -3
- package/dist/idempotency/index.d.mts +4 -5
- package/dist/idempotency/index.mjs +1 -2
- package/dist/idempotency/mongodb.d.mts +1 -1
- package/dist/idempotency/mongodb.mjs +1 -2
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +1 -2
- package/dist/index.d.mts +9 -10
- package/dist/index.mjs +7 -8
- package/dist/integrations/event-gateway.d.mts +2 -3
- package/dist/integrations/event-gateway.mjs +2 -3
- package/dist/integrations/jobs.d.mts +1 -2
- package/dist/integrations/jobs.mjs +1 -2
- package/dist/integrations/streamline.d.mts +1 -2
- package/dist/integrations/streamline.mjs +1 -2
- package/dist/integrations/websocket.d.mts +1 -2
- package/dist/integrations/websocket.mjs +1 -2
- package/dist/{interface-B01JvPVc.d.mts → interface-CSNjltAc.d.mts} +1 -2
- package/dist/{interface-Ch8HU9uM.d.mts → interface-Cb2klgid.d.mts} +10 -10
- package/dist/{interface-CZe8IkMf.d.mts → interface-DTbsvIWe.d.mts} +1 -2
- package/dist/{introspectionPlugin-rFdO8ZUa.mjs → introspectionPlugin-B3JkrjwU.mjs} +1 -2
- package/dist/{keys-BqNejWup.mjs → keys-DhqDRxv3.mjs} +1 -2
- package/dist/{logger-Df2O2WsW.mjs → logger-ByrvQWZO.mjs} +1 -2
- package/dist/{memory-cQgelFOj.mjs → memory-B2v7KrCB.mjs} +1 -2
- package/dist/migrations/index.d.mts +1 -2
- package/dist/migrations/index.mjs +1 -2
- package/dist/{mongodb-CGzRbfAK.d.mts → mongodb-ClykrfGo.d.mts} +2 -3
- package/dist/{mongodb-BfJVlUJH.mjs → mongodb-DNKEExbf.mjs} +1 -2
- package/dist/{mongodb-JN-9JA7K.d.mts → mongodb-Dg8O_gvd.d.mts} +2 -3
- package/dist/{openapi-G3Cw7XuM.mjs → openapi-9nB_kiuR.mjs} +5 -4
- package/dist/org/index.d.mts +4 -5
- package/dist/org/index.mjs +1 -2
- package/dist/org/types.d.mts +1 -2
- package/dist/permissions/index.d.mts +5 -6
- package/dist/permissions/index.mjs +7 -7
- package/dist/plugins/index.d.mts +7 -8
- package/dist/plugins/index.mjs +7 -8
- package/dist/plugins/response-cache.d.mts +1 -2
- package/dist/plugins/response-cache.mjs +2 -3
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -2
- package/dist/{pluralize-CEweyOEm.mjs → pluralize-CM-jZg7p.mjs} +1 -2
- package/dist/policies/index.d.mts +4 -5
- package/dist/policies/index.mjs +1 -2
- package/dist/presets/index.d.mts +4 -5
- package/dist/presets/index.mjs +2 -3
- package/dist/presets/multiTenant.d.mts +4 -5
- package/dist/presets/multiTenant.mjs +1 -2
- package/dist/{presets-DzSMwlKj.d.mts → presets-BTeYbw7h.d.mts} +2 -3
- package/dist/{presets-BITljm96.mjs → presets-CeFtfDR8.mjs} +1 -2
- package/dist/prisma-DJbMt3yf.mjs +1 -2
- package/dist/{prisma-Dg9GoVdj.d.mts → prisma-DQBSSHAB.d.mts} +2 -3
- package/dist/{queryCachePlugin-DMBnp2Q0.mjs → queryCachePlugin-B6R0d4av.mjs} +4 -5
- package/dist/{queryCachePlugin-7THaI5mt.d.mts → queryCachePlugin-Q6SYuHZ6.d.mts} +2 -3
- package/dist/{redis-D-JAeLtm.d.mts → redis-UwjEp8Ea.d.mts} +2 -3
- package/dist/{redis-stream-Bdh_vUU8.d.mts → redis-stream-CBg0upHI.d.mts} +2 -3
- package/dist/registry/index.d.mts +4 -5
- package/dist/registry/index.mjs +2 -2
- package/dist/{requestContext-QQD6ROJc.mjs → requestContext-xi6OKBL-.mjs} +1 -2
- package/dist/{schemaConverter-BwrmWroW.mjs → schemaConverter-Dtg0Kt9T.mjs} +1 -2
- package/dist/schemas/index.d.mts +1 -2
- package/dist/schemas/index.mjs +1 -2
- package/dist/scope/index.d.mts +2 -3
- package/dist/scope/index.mjs +2 -3
- package/dist/{sessionManager-jPKLbHE0.d.mts → sessionManager-D_iEHjQl.d.mts} +1 -2
- package/dist/{sse-B3c3_yZp.mjs → sse-DkqQ1uxb.mjs} +2 -3
- package/dist/testing/index.d.mts +8 -9
- package/dist/testing/index.mjs +3 -4
- package/dist/{tracing-Cc7vVQPp.d.mts → tracing-8CEbhF0w.d.mts} +1 -2
- package/dist/{typeGuards-DhMNLuvU.mjs → typeGuards-DwxA1t_L.mjs} +1 -2
- package/dist/types/index.d.mts +7 -8
- package/dist/types/index.mjs +1 -2
- package/dist/{types-CIgB7UUl.d.mts → types-B0dhNrnd.d.mts} +9 -10
- package/dist/types-Beqn1Un7.mjs +1 -2
- package/dist/types-DelU6kln.mjs +25 -0
- package/dist/{types-aYB4V7uN.d.mts → types-RLkFVgaw.d.mts} +18 -4
- package/dist/utils/index.d.mts +5 -6
- package/dist/utils/index.mjs +4 -4
- package/package.json +1 -1
- package/dist/EventTransport-BD2U0BTc.d.mts.map +0 -1
- package/dist/HookSystem-BsGV-j2l.mjs.map +0 -1
- package/dist/ResourceRegistry-DsN4KJjV.mjs.map +0 -1
- package/dist/audit/index.d.mts.map +0 -1
- package/dist/audit/index.mjs.map +0 -1
- package/dist/audited-C3T5DTUx.mjs.map +0 -1
- package/dist/auth/index.d.mts.map +0 -1
- package/dist/auth/index.mjs.map +0 -1
- package/dist/auth/redis-session.d.mts.map +0 -1
- package/dist/auth/redis-session.mjs.map +0 -1
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +0 -1
- package/dist/cache/index.d.mts.map +0 -1
- package/dist/cache/index.mjs.map +0 -1
- package/dist/caching-Bl28lYsR.mjs.map +0 -1
- package/dist/circuitBreaker-DeY4FCjs.mjs.map +0 -1
- package/dist/cli/commands/describe.d.mts.map +0 -1
- package/dist/cli/commands/describe.mjs.map +0 -1
- package/dist/cli/commands/docs.d.mts.map +0 -1
- package/dist/cli/commands/docs.mjs.map +0 -1
- package/dist/cli/commands/generate.d.mts.map +0 -1
- package/dist/cli/commands/generate.mjs.map +0 -1
- package/dist/cli/commands/init.d.mts.map +0 -1
- package/dist/cli/commands/init.mjs.map +0 -1
- package/dist/cli/commands/introspect.d.mts.map +0 -1
- package/dist/cli/commands/introspect.mjs.map +0 -1
- package/dist/cli/index.d.mts.map +0 -1
- package/dist/cli/index.mjs.map +0 -1
- package/dist/constants-DdXFXQtN.mjs.map +0 -1
- package/dist/createApp-CUgNqegw.mjs.map +0 -1
- package/dist/defineResource-k0_BDn8v.mjs.map +0 -1
- package/dist/discovery/index.d.mts.map +0 -1
- package/dist/discovery/index.mjs.map +0 -1
- package/dist/docs/index.d.mts.map +0 -1
- package/dist/docs/index.mjs.map +0 -1
- package/dist/elevation-BRy3yFWT.mjs.map +0 -1
- package/dist/elevation-B_2dRLVP.d.mts.map +0 -1
- package/dist/errorHandler-BbcgBmIH.d.mts.map +0 -1
- package/dist/errorHandler-C1okiriz.mjs.map +0 -1
- package/dist/errors-B9bZok84.mjs.map +0 -1
- package/dist/errors-ChKiFz62.d.mts.map +0 -1
- package/dist/eventPlugin-CTrLH3mt.d.mts.map +0 -1
- package/dist/eventPlugin-DGR_B2on.mjs.map +0 -1
- package/dist/events/index.d.mts.map +0 -1
- package/dist/events/index.mjs.map +0 -1
- package/dist/events/transports/redis-stream-entry.mjs.map +0 -1
- package/dist/events/transports/redis.d.mts.map +0 -1
- package/dist/events/transports/redis.mjs.map +0 -1
- package/dist/externalPaths-DlINfKbP.d.mts.map +0 -1
- package/dist/factory/index.d.mts.map +0 -1
- package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +0 -1
- package/dist/fields-DyaDVX4J.d.mts.map +0 -1
- package/dist/fields-iagOozy0.mjs.map +0 -1
- package/dist/idempotency/index.d.mts.map +0 -1
- package/dist/idempotency/index.mjs.map +0 -1
- package/dist/idempotency/mongodb.mjs.map +0 -1
- package/dist/idempotency/redis.mjs.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/integrations/event-gateway.d.mts.map +0 -1
- package/dist/integrations/event-gateway.mjs.map +0 -1
- package/dist/integrations/jobs.d.mts.map +0 -1
- package/dist/integrations/jobs.mjs.map +0 -1
- package/dist/integrations/streamline.d.mts.map +0 -1
- package/dist/integrations/streamline.mjs.map +0 -1
- package/dist/integrations/websocket.d.mts.map +0 -1
- package/dist/integrations/websocket.mjs.map +0 -1
- package/dist/interface-B01JvPVc.d.mts.map +0 -1
- package/dist/interface-CZe8IkMf.d.mts.map +0 -1
- package/dist/interface-Ch8HU9uM.d.mts.map +0 -1
- package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +0 -1
- package/dist/keys-BqNejWup.mjs.map +0 -1
- package/dist/logger-Df2O2WsW.mjs.map +0 -1
- package/dist/memory-cQgelFOj.mjs.map +0 -1
- package/dist/migrations/index.d.mts.map +0 -1
- package/dist/migrations/index.mjs.map +0 -1
- package/dist/mongodb-BfJVlUJH.mjs.map +0 -1
- package/dist/mongodb-CGzRbfAK.d.mts.map +0 -1
- package/dist/mongodb-JN-9JA7K.d.mts.map +0 -1
- package/dist/openapi-G3Cw7XuM.mjs.map +0 -1
- package/dist/org/index.d.mts.map +0 -1
- package/dist/org/index.mjs.map +0 -1
- package/dist/org/types.d.mts.map +0 -1
- package/dist/permissions/index.d.mts.map +0 -1
- package/dist/permissions/index.mjs.map +0 -1
- package/dist/plugins/index.d.mts.map +0 -1
- package/dist/plugins/index.mjs.map +0 -1
- package/dist/plugins/response-cache.d.mts.map +0 -1
- package/dist/plugins/response-cache.mjs.map +0 -1
- package/dist/plugins/tracing-entry.mjs.map +0 -1
- package/dist/pluralize-CEweyOEm.mjs.map +0 -1
- package/dist/policies/index.d.mts.map +0 -1
- package/dist/policies/index.mjs.map +0 -1
- package/dist/presets/index.d.mts.map +0 -1
- package/dist/presets/index.mjs.map +0 -1
- package/dist/presets/multiTenant.d.mts.map +0 -1
- package/dist/presets/multiTenant.mjs.map +0 -1
- package/dist/presets-BITljm96.mjs.map +0 -1
- package/dist/presets-DzSMwlKj.d.mts.map +0 -1
- package/dist/prisma-DJbMt3yf.mjs.map +0 -1
- package/dist/prisma-Dg9GoVdj.d.mts.map +0 -1
- package/dist/queryCachePlugin-7THaI5mt.d.mts.map +0 -1
- package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +0 -1
- package/dist/redis-D-JAeLtm.d.mts.map +0 -1
- package/dist/redis-stream-Bdh_vUU8.d.mts.map +0 -1
- package/dist/registry/index.d.mts.map +0 -1
- package/dist/requestContext-QQD6ROJc.mjs.map +0 -1
- package/dist/schemaConverter-BwrmWroW.mjs.map +0 -1
- package/dist/schemas/index.d.mts.map +0 -1
- package/dist/schemas/index.mjs.map +0 -1
- package/dist/scope/index.d.mts.map +0 -1
- package/dist/scope/index.mjs.map +0 -1
- package/dist/sessionManager-jPKLbHE0.d.mts.map +0 -1
- package/dist/sse-B3c3_yZp.mjs.map +0 -1
- package/dist/testing/index.d.mts.map +0 -1
- package/dist/testing/index.mjs.map +0 -1
- package/dist/tracing-Cc7vVQPp.d.mts.map +0 -1
- package/dist/typeGuards-DhMNLuvU.mjs.map +0 -1
- package/dist/types/index.d.mts.map +0 -1
- package/dist/types/index.mjs.map +0 -1
- package/dist/types-Beqn1Un7.mjs.map +0 -1
- package/dist/types-CIgB7UUl.d.mts.map +0 -1
- package/dist/types-aYB4V7uN.d.mts.map +0 -1
- package/dist/utils/index.d.mts.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/plugins/requestId.ts","../../src/plugins/health.ts","../../src/plugins/gracefulShutdown.ts","../../src/plugins/createPlugin.ts","../../src/core/arcCorePlugin.ts"],"sourcesContent":["/**\n * Request ID Plugin\n *\n * Propagates request IDs for distributed tracing.\n * - Accepts incoming x-request-id header\n * - Generates UUID if not provided\n * - Attaches to request.id and response header\n *\n * @example\n * import { requestIdPlugin } from '@classytic/arc';\n *\n * await fastify.register(requestIdPlugin);\n *\n * // In handlers, access via request.id\n * fastify.get('/', async (request) => {\n * console.log(request.id); // UUID\n * });\n */\n\nimport fp from 'fastify-plugin';\nimport { randomUUID } from 'crypto';\nimport type { FastifyInstance, FastifyPluginAsync } from 'fastify';\n\nexport interface RequestIdOptions {\n /** Header name to read/write request ID (default: 'x-request-id') */\n header?: string;\n /** Custom ID generator (default: crypto.randomUUID) */\n generator?: () => string;\n /** Whether to set response header (default: true) */\n setResponseHeader?: boolean;\n}\n\ndeclare module 'fastify' {\n interface FastifyRequest {\n /** Unique request identifier for tracing */\n requestId: string;\n }\n}\n\nconst requestIdPlugin: FastifyPluginAsync<RequestIdOptions> = async (\n fastify: FastifyInstance,\n opts: RequestIdOptions = {}\n) => {\n const {\n header = 'x-request-id',\n generator = randomUUID,\n setResponseHeader = true,\n } = opts;\n\n // Decorate request with requestId\n if (!fastify.hasRequestDecorator('requestId')) {\n fastify.decorateRequest('requestId', '');\n }\n\n // Assign request ID on each request\n fastify.addHook('onRequest', async (request) => {\n const incomingId = request.headers[header];\n // Sanitize incoming ID: max 128 chars, alphanumeric + dashes/underscores/dots only.\n // Rejects crafted values that could pollute logs or headers.\n const sanitized = typeof incomingId === 'string' ? incomingId.trim() : '';\n const isValid = sanitized.length > 0\n && sanitized.length <= 128\n && /^[\\w.:-]+$/.test(sanitized);\n const requestId = isValid ? sanitized : generator();\n\n // Set on request object (Fastify's native id)\n (request as { id: string }).id = requestId;\n // Set on our decorated property\n request.requestId = requestId;\n });\n\n // Add to response headers\n if (setResponseHeader) {\n fastify.addHook('onSend', async (request, reply) => {\n reply.header(header, request.requestId);\n });\n }\n\n fastify.log?.debug?.('Request ID plugin registered');\n};\n\nexport default fp(requestIdPlugin, {\n name: 'arc-request-id',\n fastify: '5.x',\n});\n\nexport { requestIdPlugin };\n","/**\n * Health Check Plugin\n *\n * Kubernetes-ready health endpoints:\n * - /health/live - Liveness probe (is the process alive?)\n * - /health/ready - Readiness probe (can we serve traffic?)\n * - /health/metrics - Prometheus metrics (optional)\n *\n * @example\n * import { healthPlugin } from '@classytic/arc';\n *\n * await fastify.register(healthPlugin, {\n * prefix: '/_health',\n * checks: [\n * { name: 'mongodb', check: async () => mongoose.connection.readyState === 1 },\n * { name: 'redis', check: async () => redis.ping() === 'PONG' },\n * ],\n * });\n */\n\nimport fp from 'fastify-plugin';\nimport type { FastifyInstance, FastifyPluginAsync } from 'fastify';\n\n// Plugin-local augmentation for HTTP metrics timing\ndeclare module 'fastify' {\n interface FastifyRequest {\n _startTime?: number;\n }\n}\n\nexport interface HealthCheck {\n /** Name of the dependency */\n name: string;\n /** Function that returns true if healthy, false otherwise */\n check: () => Promise<boolean> | boolean;\n /** Optional timeout in ms (default: 5000) */\n timeout?: number;\n /** Whether this check is critical for readiness (default: true) */\n critical?: boolean;\n}\n\nexport interface HealthOptions {\n /** Route prefix (default: '/_health') */\n prefix?: string;\n /** Health check dependencies */\n checks?: HealthCheck[];\n /** Enable metrics endpoint (default: false) */\n metrics?: boolean;\n /** Custom metrics collector function */\n metricsCollector?: () => Promise<string> | string;\n /** Version info to include in responses */\n version?: string;\n /** Collect HTTP request metrics (default: true if metrics enabled) */\n collectHttpMetrics?: boolean;\n}\n\ninterface CheckResult {\n name: string;\n healthy: boolean;\n duration: number;\n error?: string;\n}\n\n// Metrics storage (instance-scoped to avoid contamination between app instances)\ninterface HttpMetrics {\n requestsTotal: Record<string, number>;\n requestDurations: number[];\n /** Write index for ring buffer — wraps modulo capacity */\n _ringIndex: number;\n startTime: number;\n}\n\nfunction createHttpMetrics(): HttpMetrics {\n return {\n requestsTotal: {},\n requestDurations: [],\n _ringIndex: 0,\n startTime: Date.now(),\n };\n}\n\nconst healthPlugin: FastifyPluginAsync<HealthOptions> = async (\n fastify: FastifyInstance,\n opts: HealthOptions = {}\n) => {\n const {\n prefix = '/_health',\n checks = [],\n metrics = false,\n metricsCollector,\n version,\n collectHttpMetrics = metrics,\n } = opts;\n\n // Instance-scoped metrics — each Fastify instance gets its own counters\n const httpMetrics = createHttpMetrics();\n\n // ========================================\n // Liveness Probe\n // ========================================\n\n fastify.get(`${prefix}/live`, {\n schema: {\n tags: ['Health'],\n summary: 'Liveness probe',\n description: 'Returns 200 if the process is alive',\n response: {\n 200: {\n type: 'object',\n properties: {\n status: { type: 'string', enum: ['ok'] },\n timestamp: { type: 'string' },\n version: { type: 'string' },\n },\n },\n },\n },\n }, async () => {\n return {\n status: 'ok',\n timestamp: new Date().toISOString(),\n ...(version ? { version } : {}),\n };\n });\n\n // ========================================\n // Readiness Probe\n // ========================================\n\n fastify.get(`${prefix}/ready`, {\n schema: {\n tags: ['Health'],\n summary: 'Readiness probe',\n description: 'Returns 200 if all dependencies are healthy',\n response: {\n 200: {\n type: 'object',\n properties: {\n status: { type: 'string', enum: ['ready', 'not_ready'] },\n timestamp: { type: 'string' },\n checks: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n name: { type: 'string' },\n healthy: { type: 'boolean' },\n duration: { type: 'number' },\n error: { type: 'string' },\n },\n },\n },\n },\n },\n 503: {\n type: 'object',\n properties: {\n status: { type: 'string', enum: ['not_ready'] },\n timestamp: { type: 'string' },\n checks: { type: 'array' },\n },\n },\n },\n },\n }, async (_, reply) => {\n const results = await runChecks(checks);\n const criticalFailed = results.some(\n (r) => !r.healthy && (checks.find((c) => c.name === r.name)?.critical ?? true)\n );\n\n const response = {\n status: criticalFailed ? 'not_ready' : 'ready',\n timestamp: new Date().toISOString(),\n checks: results,\n };\n\n if (criticalFailed) {\n reply.code(503);\n }\n\n return response;\n });\n\n // ========================================\n // Metrics Endpoint (Optional)\n // ========================================\n\n if (metrics) {\n fastify.get(`${prefix}/metrics`, async (_, reply) => {\n reply.type('text/plain; charset=utf-8');\n\n if (metricsCollector) {\n return await metricsCollector();\n }\n\n // Default Prometheus metrics\n const uptime = process.uptime();\n const memory = process.memoryUsage();\n const cpu = process.cpuUsage();\n\n const lines = [\n '# HELP process_uptime_seconds Process uptime in seconds',\n '# TYPE process_uptime_seconds gauge',\n `process_uptime_seconds ${uptime.toFixed(2)}`,\n '',\n '# HELP process_memory_heap_bytes Heap memory usage in bytes',\n '# TYPE process_memory_heap_bytes gauge',\n `process_memory_heap_bytes{type=\"used\"} ${memory.heapUsed}`,\n `process_memory_heap_bytes{type=\"total\"} ${memory.heapTotal}`,\n '',\n '# HELP process_memory_rss_bytes RSS memory in bytes',\n '# TYPE process_memory_rss_bytes gauge',\n `process_memory_rss_bytes ${memory.rss}`,\n '',\n '# HELP process_memory_external_bytes External memory in bytes',\n '# TYPE process_memory_external_bytes gauge',\n `process_memory_external_bytes ${memory.external}`,\n '',\n '# HELP process_cpu_user_microseconds User CPU time in microseconds',\n '# TYPE process_cpu_user_microseconds counter',\n `process_cpu_user_microseconds ${cpu.user}`,\n '',\n '# HELP process_cpu_system_microseconds System CPU time in microseconds',\n '# TYPE process_cpu_system_microseconds counter',\n `process_cpu_system_microseconds ${cpu.system}`,\n '',\n ];\n\n // HTTP request metrics\n if (collectHttpMetrics && Object.keys(httpMetrics.requestsTotal).length > 0) {\n lines.push(\n '# HELP http_requests_total Total HTTP requests by status code',\n '# TYPE http_requests_total counter'\n );\n for (const [status, count] of Object.entries(httpMetrics.requestsTotal)) {\n lines.push(`http_requests_total{status=\"${status}\"} ${count}`);\n }\n lines.push('');\n\n // Request duration histogram\n if (httpMetrics.requestDurations.length > 0) {\n const sorted = [...httpMetrics.requestDurations].sort((a, b) => a - b);\n const p50 = sorted[Math.floor(sorted.length * 0.5)] || 0;\n const p95 = sorted[Math.floor(sorted.length * 0.95)] || 0;\n const p99 = sorted[Math.floor(sorted.length * 0.99)] || 0;\n const sum = sorted.reduce((a, b) => a + b, 0);\n\n lines.push(\n '# HELP http_request_duration_milliseconds HTTP request duration',\n '# TYPE http_request_duration_milliseconds summary',\n `http_request_duration_milliseconds{quantile=\"0.5\"} ${p50.toFixed(2)}`,\n `http_request_duration_milliseconds{quantile=\"0.95\"} ${p95.toFixed(2)}`,\n `http_request_duration_milliseconds{quantile=\"0.99\"} ${p99.toFixed(2)}`,\n `http_request_duration_milliseconds_sum ${sum.toFixed(2)}`,\n `http_request_duration_milliseconds_count ${sorted.length}`,\n ''\n );\n }\n }\n\n return lines.join('\\n');\n });\n }\n\n // Collect HTTP metrics\n if (collectHttpMetrics) {\n fastify.addHook('onRequest', async (request) => {\n request._startTime = Date.now();\n });\n\n fastify.addHook('onResponse', async (request, reply) => {\n const duration = Date.now() - (request._startTime ?? Date.now());\n\n // Track by status code bucket (2xx, 3xx, 4xx, 5xx)\n const statusBucket = `${Math.floor(reply.statusCode / 100)}xx`;\n httpMetrics.requestsTotal[statusBucket] = (httpMetrics.requestsTotal[statusBucket] || 0) + 1;\n\n // Store duration in ring buffer (O(1) vs O(n) for Array.shift)\n if (httpMetrics.requestDurations.length < 10000) {\n httpMetrics.requestDurations.push(duration);\n } else {\n httpMetrics.requestDurations[httpMetrics._ringIndex % 10000] = duration;\n }\n httpMetrics._ringIndex = httpMetrics._ringIndex + 1;\n });\n }\n\n fastify.log?.debug?.(`Health plugin registered at ${prefix}`);\n};\n\n/**\n * Run all health checks with timeout\n */\nasync function runChecks(checks: HealthCheck[]): Promise<CheckResult[]> {\n const results: CheckResult[] = [];\n\n for (const check of checks) {\n const start = Date.now();\n const timeout = check.timeout ?? 5000;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n try {\n const checkPromise = Promise.resolve(check.check());\n const timeoutPromise = new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new Error('Health check timeout')), timeout);\n });\n\n const healthy = await Promise.race([checkPromise, timeoutPromise]);\n\n results.push({\n name: check.name,\n healthy: Boolean(healthy),\n duration: Date.now() - start,\n });\n } catch (err) {\n results.push({\n name: check.name,\n healthy: false,\n duration: Date.now() - start,\n error: (err as Error).message,\n });\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n\n return results;\n}\n\nexport default fp(healthPlugin, {\n name: 'arc-health',\n fastify: '5.x',\n});\n\nexport { healthPlugin };\n","/**\n * Graceful Shutdown Plugin\n *\n * Handles SIGTERM and SIGINT signals for clean shutdown:\n * - Stops accepting new connections\n * - Waits for in-flight requests to complete\n * - Closes database connections\n * - Exits cleanly\n *\n * Essential for Kubernetes deployments.\n *\n * @example\n * import { gracefulShutdownPlugin } from '@classytic/arc';\n *\n * // Production\n * await fastify.register(gracefulShutdownPlugin, {\n * timeout: 30000, // 30 seconds max\n * onShutdown: async () => {\n * await mongoose.disconnect();\n * await redis.quit();\n * },\n * });\n *\n * // Tests — prevent process.exit from killing the runner\n * await fastify.register(gracefulShutdownPlugin, {\n * onForceExit: () => {},\n * });\n */\n\nimport fp from 'fastify-plugin';\nimport type { FastifyInstance, FastifyPluginAsync } from 'fastify';\n\nexport interface GracefulShutdownOptions {\n /** Maximum time to wait for graceful shutdown in ms (default: 30000) */\n timeout?: number;\n /** Custom cleanup function called before exit */\n onShutdown?: () => Promise<void> | void;\n /** Signals to handle (default: ['SIGTERM', 'SIGINT']) */\n signals?: NodeJS.Signals[];\n /** Whether to log shutdown events (default: true) */\n logEvents?: boolean;\n /**\n * Called when shutdown times out or encounters an error.\n * Defaults to `process.exit(1)` — appropriate for production but dangerous in:\n * - **Tests**: kills the test runner. Pass `() => {}` or `() => { throw … }`.\n * - **Shared runtimes** (e.g., serverless): may kill unrelated handlers.\n *\n * @param reason - `'timeout'` if shutdown exceeded `timeout` ms,\n * `'error'` if `onShutdown` or `fastify.close()` threw.\n */\n onForceExit?: (reason: 'timeout' | 'error') => void;\n}\n\nconst gracefulShutdownPlugin: FastifyPluginAsync<GracefulShutdownOptions> = async (\n fastify: FastifyInstance,\n opts: GracefulShutdownOptions = {}\n) => {\n const {\n timeout = 30000,\n onShutdown,\n signals = ['SIGTERM', 'SIGINT'],\n logEvents = true,\n onForceExit = () => process.exit(1),\n } = opts;\n\n let isShuttingDown = false;\n\n // Keep references to signal handlers so we can remove them on close\n const signalHandlers = new Map<string, () => void>();\n\n const shutdown = async (signal: string): Promise<void> => {\n // Prevent multiple shutdown attempts\n if (isShuttingDown) {\n if (logEvents) {\n fastify.log?.warn?.({ signal }, 'Shutdown already in progress, ignoring signal');\n }\n return;\n }\n isShuttingDown = true;\n\n if (logEvents) {\n fastify.log?.info?.({ signal, timeout }, 'Shutdown signal received, starting graceful shutdown');\n }\n\n // Set a hard timeout — force-exit only as last resort\n const forceExitTimer = setTimeout(() => {\n if (logEvents) {\n fastify.log?.error?.('Graceful shutdown timeout exceeded, forcing exit');\n }\n onForceExit('timeout');\n }, timeout);\n\n // Don't keep the process alive just for this timer\n forceExitTimer.unref();\n\n try {\n // 1. Stop accepting new connections and wait for in-flight requests\n if (logEvents) {\n fastify.log?.info?.('Closing server to new connections');\n }\n await fastify.close();\n\n // 2. Run custom cleanup (database connections, Redis, etc.)\n if (onShutdown) {\n if (logEvents) {\n fastify.log?.info?.('Running custom shutdown handler');\n }\n await onShutdown();\n }\n\n if (logEvents) {\n fastify.log?.info?.('Graceful shutdown complete');\n }\n\n clearTimeout(forceExitTimer);\n // Let Node.js exit naturally when the event loop drains\n // instead of calling process.exit(0) which skips cleanup\n } catch (err) {\n if (logEvents) {\n fastify.log?.error?.({ error: (err as Error).message }, 'Error during shutdown');\n }\n clearTimeout(forceExitTimer);\n onForceExit('error');\n }\n };\n\n // Register signal handlers (with references for cleanup)\n for (const signal of signals) {\n const handler = () => { void shutdown(signal); };\n signalHandlers.set(signal, handler);\n process.on(signal, handler);\n }\n\n // Cleanup signal handlers on close to prevent test pollution\n fastify.addHook('onClose', async () => {\n for (const [signal, handler] of signalHandlers) {\n process.removeListener(signal, handler);\n }\n signalHandlers.clear();\n });\n\n // Decorate fastify with manual shutdown trigger\n fastify.decorate('shutdown', async () => {\n await shutdown('MANUAL');\n });\n\n if (logEvents) {\n fastify.log?.debug?.({ signals }, 'Graceful shutdown plugin registered');\n }\n};\n\n// Extend Fastify types\ndeclare module 'fastify' {\n interface FastifyInstance {\n /** Trigger graceful shutdown manually */\n shutdown: () => Promise<void>;\n }\n}\n\nexport default fp(gracefulShutdownPlugin, {\n name: 'arc-graceful-shutdown',\n fastify: '5.x',\n});\n\nexport { gracefulShutdownPlugin };\n","/**\n * createPlugin() — forRoot/forFeature Pattern\n *\n * Standard pattern for plugins that need both global setup and per-resource configuration.\n * Inspired by NestJS forRoot/forFeature but simpler — plain functions, no decorators.\n *\n * @example\n * ```typescript\n * // Define a plugin with global + per-resource config\n * const analytics = createPlugin('analytics', {\n * forRoot: async (fastify, opts) => {\n * // Global setup: connect to analytics service, add decorators\n * const client = new AnalyticsClient(opts.apiKey);\n * fastify.decorate('analytics', client);\n * },\n * forResource: (resourceConfig, opts) => {\n * // Per-resource: return hooks, middleware, or routes\n * return {\n * hooks: [{\n * operation: 'create', phase: 'after', priority: 100,\n * handler: (ctx) => client.track('created', ctx.result),\n * }],\n * };\n * },\n * });\n *\n * // Usage — register globally once\n * await app.register(analytics.forRoot({ apiKey: 'xxx' }));\n *\n * // Then apply per-resource\n * const productResource = defineResource({\n * name: 'product',\n * adapter: productAdapter,\n * ...analytics.forResource({ trackEvents: true }),\n * });\n * ```\n */\n\nimport fp from 'fastify-plugin';\nimport type { FastifyInstance, FastifyPluginAsync } from 'fastify';\nimport type { AnyRecord, AdditionalRoute, PresetHook, MiddlewareConfig, RouteSchemaOptions } from '../types/index.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface PluginResourceResult {\n /** Additional routes to add to the resource */\n additionalRoutes?: AdditionalRoute[];\n /** Middlewares per operation */\n middlewares?: MiddlewareConfig;\n /** Hooks to register */\n hooks?: PresetHook[];\n /** Schema options to merge */\n schemaOptions?: RouteSchemaOptions;\n}\n\nexport interface CreatePluginDefinition<\n TRootOpts extends AnyRecord = AnyRecord,\n TResourceOpts extends AnyRecord = AnyRecord,\n> {\n /**\n * Global setup function. Called once when the plugin is registered on the Fastify instance.\n * Use this for database connections, decorators, shared state, etc.\n */\n forRoot?: (fastify: FastifyInstance, opts: TRootOpts) => void | Promise<void>;\n\n /**\n * Per-resource configuration function. Called for each resource that uses this plugin.\n * Returns hooks, routes, middlewares, etc. to merge into the resource config.\n */\n forResource?: (resourceConfig: AnyRecord, opts: TResourceOpts) => PluginResourceResult;\n}\n\nexport interface ArcPlugin<\n TRootOpts extends AnyRecord = AnyRecord,\n TResourceOpts extends AnyRecord = AnyRecord,\n> {\n /** Plugin name */\n readonly name: string;\n\n /**\n * Register the plugin globally on a Fastify instance.\n * Returns a Fastify plugin that can be passed to `app.register()`.\n */\n forRoot(opts?: TRootOpts): FastifyPluginAsync<TRootOpts>;\n\n /**\n * Apply per-resource configuration.\n * Returns a partial resource config to spread into `defineResource()`.\n */\n forResource(opts?: TResourceOpts): PluginResourceResult;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a structured plugin with forRoot (global) and forResource (per-resource) support.\n *\n * @param name - Plugin name (used for Fastify registration and debugging)\n * @param definition - Plugin setup functions\n * @returns ArcPlugin with forRoot() and forResource() methods\n */\nexport function createPlugin<\n TRootOpts extends AnyRecord = AnyRecord,\n TResourceOpts extends AnyRecord = AnyRecord,\n>(\n name: string,\n definition: CreatePluginDefinition<TRootOpts, TResourceOpts>,\n): ArcPlugin<TRootOpts, TResourceOpts> {\n return {\n name,\n\n forRoot(opts?: TRootOpts): FastifyPluginAsync<TRootOpts> {\n const plugin: FastifyPluginAsync<TRootOpts> = async (fastify, pluginOpts) => {\n const mergedOpts = { ...opts, ...pluginOpts } as TRootOpts;\n if (definition.forRoot) {\n await definition.forRoot(fastify, mergedOpts);\n }\n };\n\n return fp(plugin, {\n name: `arc-plugin-${name}`,\n fastify: '5.x',\n });\n },\n\n forResource(opts?: TResourceOpts): PluginResourceResult {\n if (!definition.forResource) {\n return {};\n }\n return definition.forResource({} as AnyRecord, (opts ?? {}) as TResourceOpts);\n },\n };\n}\n","/**\r\n * Arc Core Plugin\r\n *\r\n * Sets up instance-scoped Arc systems:\r\n * - HookSystem: Lifecycle hooks per app instance\r\n * - ResourceRegistry: Resource tracking per app instance\r\n * - Event integration: Wires CRUD operations to fastify.events\r\n *\r\n * This solves the global singleton leak problem where multiple\r\n * app instances (e.g., in tests) would share state.\r\n *\r\n * @example\r\n * import { arcCorePlugin } from '@classytic/arc';\r\n *\r\n * const app = Fastify();\r\n * await app.register(arcCorePlugin);\r\n *\r\n * // Now use instance-scoped hooks\r\n * app.arc.hooks.before('product', 'create', async (ctx) => {\r\n * ctx.data.slug = slugify(ctx.data.name);\r\n * });\r\n */\r\n\r\nimport fp from 'fastify-plugin';\r\nimport type { FastifyInstance, FastifyPluginAsync } from 'fastify';\r\nimport { HookSystem } from '../hooks/HookSystem.js';\r\nimport { ResourceRegistry } from '../registry/ResourceRegistry.js';\r\nimport { requestContext } from '../context/requestContext.js';\r\nimport type { RequestStore } from '../context/requestContext.js';\r\nimport type { ExternalOpenApiPaths } from '../docs/externalPaths.js';\r\nimport type { RequestScope } from '../scope/types.js';\r\nimport { getOrgId } from '../scope/types.js';\r\nimport { MUTATION_OPERATIONS } from '../constants.js';\r\nimport { hasEvents } from '../utils/typeGuards.js';\r\n\r\nexport interface ArcCorePluginOptions {\r\n /** Enable event emission for CRUD operations (requires eventPlugin) */\r\n emitEvents?: boolean;\r\n /** Hook system instance (for testing/custom setup) */\r\n hookSystem?: HookSystem;\r\n /** Resource registry instance (for testing/custom setup) */\r\n registry?: ResourceRegistry;\r\n}\r\n\r\nexport interface PluginMeta {\r\n name: string;\r\n version?: string;\r\n options?: Record<string, unknown>;\r\n registeredAt: string;\r\n}\r\n\r\nexport interface ArcCore {\r\n /** Instance-scoped hook system */\r\n hooks: HookSystem;\r\n /** Instance-scoped resource registry */\r\n registry: ResourceRegistry;\r\n /** Whether event emission is enabled */\r\n emitEvents: boolean;\r\n /** External OpenAPI paths contributed by auth adapters or third-party integrations */\r\n externalOpenApiPaths: ExternalOpenApiPaths[];\r\n /** Registered plugins for introspection */\r\n plugins: Map<string, PluginMeta>;\r\n}\r\n\r\ndeclare module 'fastify' {\r\n interface FastifyInstance {\r\n arc: ArcCore;\r\n }\r\n}\r\n\r\nconst arcCorePlugin: FastifyPluginAsync<ArcCorePluginOptions> = async (\r\n fastify: FastifyInstance,\r\n opts: ArcCorePluginOptions = {}\r\n) => {\r\n const {\r\n emitEvents = true,\r\n hookSystem,\r\n registry,\r\n } = opts;\r\n\r\n // Always use instance-scoped systems — no global singletons\r\n const actualHookSystem = hookSystem ?? new HookSystem();\r\n const actualRegistry = registry ?? new ResourceRegistry();\r\n\r\n // Decorate with instance-scoped Arc core\r\n fastify.decorate('arc', {\r\n hooks: actualHookSystem,\r\n registry: actualRegistry,\r\n emitEvents,\r\n externalOpenApiPaths: [],\r\n plugins: new Map<string, PluginMeta>(),\r\n });\r\n\r\n // Request context via AsyncLocalStorage — zero-cost per request.\r\n // storage.run(store, done) wraps the ENTIRE remaining request lifecycle\r\n // so any code in the call stack can access user/org/requestId.\r\n fastify.addHook('onRequest', (request, _reply, done) => {\r\n const store: RequestStore = {\r\n requestId: request.id,\r\n startTime: performance.now(),\r\n };\r\n requestContext.storage.run(store, done);\r\n });\r\n\r\n // Populate user/org after auth middleware runs (user isn't set during onRequest)\r\n fastify.addHook('preHandler', (request, _reply, done) => {\r\n const store = requestContext.get();\r\n if (store) {\r\n const req = request as unknown as Record<string, unknown>;\r\n store.user = req.user as RequestStore['user'] ?? null;\r\n store.organizationId = request.scope?.kind === 'member' ? request.scope.organizationId\r\n : request.scope?.kind === 'elevated' ? request.scope.organizationId\r\n : undefined;\r\n }\r\n done();\r\n });\r\n\r\n // Wire events into hooks if event plugin is available and events enabled\r\n if (emitEvents) {\r\n // Register after hooks that emit events\r\n const eventOperations = MUTATION_OPERATIONS;\r\n\r\n for (const operation of eventOperations) {\r\n actualHookSystem.after('*', operation, async (ctx) => {\r\n // Check if events plugin is registered using type guard\r\n if (!hasEvents(fastify)) return;\r\n\r\n const store = requestContext.get();\r\n const eventType = `${ctx.resource}.${operation}d`; // e.g., 'product.created'\r\n const userId = ctx.user?.id ?? ctx.user?._id;\r\n const organizationId = ctx.context?._scope\r\n ? getOrgId(ctx.context._scope as RequestScope)\r\n : undefined;\r\n const payload = {\r\n resource: ctx.resource,\r\n operation: ctx.operation,\r\n data: ctx.result,\r\n userId,\r\n organizationId,\r\n timestamp: new Date().toISOString(),\r\n };\r\n\r\n try {\r\n await fastify.events.publish(eventType, payload, {\r\n correlationId: store?.requestId,\r\n resource: ctx.resource,\r\n resourceId: extractId(ctx.result),\r\n userId: userId ? String(userId) : undefined,\r\n organizationId,\r\n });\r\n } catch (error) {\r\n // Log but don't fail the request\r\n fastify.log?.warn?.(\r\n { eventType, error },\r\n 'Failed to emit event'\r\n );\r\n }\r\n });\r\n }\r\n }\r\n\r\n // Emit arc.ready lifecycle event when all resources are registered\r\n fastify.addHook('onReady', async () => {\r\n if (!hasEvents(fastify)) return;\r\n try {\r\n await fastify.events.publish('arc.ready', {\r\n resources: actualRegistry.getAll().length,\r\n hooks: actualHookSystem.getAll().length,\r\n timestamp: new Date().toISOString(),\r\n });\r\n } catch {\r\n // Lifecycle events are best-effort\r\n }\r\n });\r\n\r\n // Cleanup on close\r\n fastify.addHook('onClose', async () => {\r\n actualHookSystem.clear();\r\n actualRegistry._clear();\r\n });\r\n\r\n fastify.log?.debug?.('Arc core plugin enabled (instance-scoped hooks & registry)');\r\n};\r\n\r\n/** Extract document ID from a result (handles Mongoose docs and plain objects) */\r\nfunction extractId(doc: unknown): string | undefined {\r\n if (!doc || typeof doc !== 'object') return undefined;\r\n const d = doc as Record<string, unknown>;\r\n const rawId = d._id ?? d.id;\r\n return rawId ? String(rawId) : undefined;\r\n}\r\n\r\nexport default fp(arcCorePlugin, {\r\n name: 'arc-core',\r\n fastify: '5.x',\r\n});\r\n\r\nexport { arcCorePlugin };\r\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"response-cache.d.mts","names":[],"sources":["../../src/plugins/response-cache.ts"],"mappings":";;;UA6EiB,iBAAA;EAqON;EAnOT,KAAA;EAwOE;EAtOF,GAAA;AAAA;AAAA,UAGe,oBAAA;EAoeJ;EAleX,UAAA;;EAEA,UAAA;EAgeuE;EA9dvE,KAAA,GAAQ,iBAAA;;EAER,OAAA;;EAEA,YAAA;;EAEA,YAAA;;EAEA,SAAA;;EAEA,KAAA,IAAS,OAAA,EAAS,cAAA;;;;;;;;;;;;;;;;;;;EAmBlB,iBAAA;IAGM,QAAA,GAAW,MAAA;EAAA;AAAA;AAAA,UAYF,kBAAA;EACf,OAAA;EACA,UAAA;EACA,IAAA;EACA,MAAA;EACA,OAAA;EACA,SAAA;AAAA;AAAA;EAAA,UA6IU,eAAA;IACR,aAAA;mEAEE,UAAA,GAAa,UAAA;MAEb,aAAA;MAEA,KAAA,QAAa,kBAAA;;;;;;;;;;;;;;;MAeb,UAAA,GACE,OAAA,EAAS,cAAA,EACT,KAAA,EAAO,YAAA,KACJ,OAAA;IAAA;EAAA;EAAA,UAGC,cAAA;;IAER,aAAA;EAAA;AAAA;AAAA,cAiQS,mBAAA,EAAqB,kBAAA,CAAmB,oBAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"response-cache.mjs","names":[],"sources":["../../src/plugins/response-cache.ts"],"sourcesContent":["/**\n * Response Cache Plugin for Arc\n *\n * In-memory LRU/TTL response cache that sits in front of your database.\n * Caches serialized responses for GET requests, dramatically reducing DB load\n * for frequently accessed resources.\n *\n * Features:\n * - LRU eviction with configurable max entries\n * - Per-route TTL configuration\n * - Automatic invalidation on mutations (POST/PUT/PATCH/DELETE)\n * - Manual invalidation via `fastify.responseCache.invalidate()`\n * - Cache stats endpoint for monitoring\n * - Zero external deps — pure in-memory, serverless-safe\n *\n * NOTE: This cache is per-instance (in-memory). In multi-instance deployments,\n * each instance maintains its own cache. For cross-instance invalidation,\n * wire `fastify.responseCache.invalidate()` to your event bus manually.\n *\n * ## Auth Safety\n *\n * The cache check runs as a **route-level middleware** (`responseCache.middleware`)\n * that must be wired AFTER authentication in the preHandler chain. Arc's\n * `createCrudRouter` does this automatically. For custom routes, wire it\n * manually:\n *\n * ```typescript\n * fastify.get('/data', {\n * preHandler: [fastify.authenticate, fastify.responseCache.middleware],\n * }, handler);\n * ```\n *\n * This ensures cached responses are never served before auth validates the\n * caller's identity. The default cache key includes `userId` and `orgId`\n * to prevent cross-caller data leaks.\n *\n * This is a SEPARATE subpath import — only loaded when explicitly used:\n * import { responseCachePlugin } from '@classytic/arc/plugins/response-cache';\n *\n * @example\n * ```typescript\n * import { responseCachePlugin } from '@classytic/arc/plugins/response-cache';\n *\n * await fastify.register(responseCachePlugin, {\n * maxEntries: 1000,\n * defaultTTL: 30, // 30 seconds\n * rules: [\n * { match: '/api/products', ttl: 120 }, // 2 min for products\n * { match: '/api/categories', ttl: 300 }, // 5 min for categories\n * { match: '/api/users', ttl: 0 }, // never cache users\n * ],\n * invalidateOn: ['POST', 'PUT', 'PATCH', 'DELETE'],\n * });\n *\n * // Manual invalidation\n * fastify.responseCache.invalidate('/api/products');\n * fastify.responseCache.invalidateAll();\n *\n * // Stats\n * const stats = fastify.responseCache.stats();\n * // { entries: 42, hits: 1250, misses: 180, hitRate: 0.87, evictions: 5 }\n * ```\n */\n\nimport fp from \"fastify-plugin\";\nimport type {\n FastifyInstance,\n FastifyPluginAsync,\n FastifyReply,\n FastifyRequest,\n} from \"fastify\";\nimport { hasEvents } from \"../utils/typeGuards.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ResponseCacheRule {\n /** Path prefix to match (e.g., '/api/products') */\n match: string;\n /** TTL in seconds for this path (0 = don't cache) */\n ttl: number;\n}\n\nexport interface ResponseCacheOptions {\n /** Maximum number of cached entries (default: 500). LRU eviction when exceeded. */\n maxEntries?: number;\n /** Default TTL in seconds (default: 30). Set to 0 to require explicit rules. */\n defaultTTL?: number;\n /** Per-path cache rules */\n rules?: ResponseCacheRule[];\n /** Paths to exclude from caching (prefix match) */\n exclude?: string[];\n /** HTTP methods that trigger cache invalidation (default: POST, PUT, PATCH, DELETE) */\n invalidateOn?: string[];\n /** Whether to add X-Cache header (HIT/MISS) to responses (default: true) */\n xCacheHeader?: boolean;\n /** Enable stats endpoint at this path (default: null = disabled) */\n statsPath?: string | null;\n /** Custom cache key function (default: method + url + userId + orgId) */\n keyFn?: (request: FastifyRequest) => string | null;\n /**\n * Auto-invalidate cache entries when CRUD domain events fire (requires eventPlugin).\n *\n * - `true`: Invalidate resource prefix on its own CRUD events\n * - `{ patterns: { 'order.*': ['/api/products'] } }`: Cross-resource invalidation rules\n * - `false` / omitted: Disabled (default)\n *\n * @example\n * ```typescript\n * await fastify.register(responseCachePlugin, {\n * eventInvalidation: {\n * patterns: {\n * 'order.*': ['/api/products', '/api/inventory'],\n * },\n * },\n * });\n * ```\n */\n eventInvalidation?:\n | boolean\n | {\n patterns?: Record<string, string[]>;\n };\n}\n\ninterface CacheEntry {\n body: string;\n statusCode: number;\n headers: Record<string, string>;\n createdAt: number;\n ttl: number; // ms\n}\n\nexport interface ResponseCacheStats {\n entries: number;\n maxEntries: number;\n hits: number;\n misses: number;\n hitRate: number;\n evictions: number;\n}\n\n// ============================================================================\n// LRU Cache (minimal, zero-dep)\n// ============================================================================\n\n/**\n * Simple LRU cache using Map iteration order.\n * Map in JS preserves insertion order — we re-insert on access to make it LRU.\n */\nclass LRUCache {\n private cache = new Map<string, CacheEntry>();\n private maxEntries: number;\n\n // Locks to prevent caching stale replica data immediately after mutation\n private invalidatedPrefixes = new Map<string, number>();\n\n // Stats\n hits = 0;\n misses = 0;\n evictions = 0;\n\n constructor(maxEntries: number) {\n this.maxEntries = maxEntries;\n }\n\n get(key: string): CacheEntry | null {\n const entry = this.cache.get(key);\n if (!entry) {\n this.misses++;\n return null;\n }\n\n // Check TTL\n if (Date.now() - entry.createdAt > entry.ttl) {\n this.cache.delete(key);\n this.misses++;\n return null;\n }\n\n // Move to end (most recently used)\n this.cache.delete(key);\n this.cache.set(key, entry);\n this.hits++;\n return entry;\n }\n\n set(key: string, entry: CacheEntry): void {\n // If prefix is locked due to recent invalidation, do not cache (prevents stale replica reads)\n if (this.isPrefixLocked(key)) {\n return;\n }\n\n // Delete first to reset position\n if (this.cache.has(key)) {\n this.cache.delete(key);\n }\n\n // Evict oldest (first in Map) if at capacity\n while (this.cache.size >= this.maxEntries) {\n const firstKey = this.cache.keys().next().value;\n if (firstKey !== undefined) {\n this.cache.delete(firstKey);\n this.evictions++;\n }\n }\n\n this.cache.set(key, entry);\n }\n\n /** Invalidate entries matching a path prefix and lock it from caching to allow DB replicas to catch up */\n invalidatePrefix(prefix: string, jitterMs = 1500): number {\n let count = 0;\n for (const key of this.cache.keys()) {\n // Keys are formatted as \"GET:/api/products?page=1:u=...:o=...\"\n // Extract path after method\n const colonIdx = key.indexOf(\":\");\n const path = colonIdx >= 0 ? key.slice(colonIdx + 1) : key;\n // Strip query string and user/org suffix for prefix matching\n const pathOnly = path.split(\"?\")[0]!;\n if (pathOnly.startsWith(prefix)) {\n this.cache.delete(key);\n count++;\n }\n }\n\n // Lock this prefix from being cached for `jitterMs` milliseconds\n if (jitterMs > 0) {\n this.invalidatedPrefixes.set(prefix, Date.now() + jitterMs);\n }\n\n return count;\n }\n\n /** Check if a key falls under a recently invalidated prefix */\n private isPrefixLocked(key: string): boolean {\n if (this.invalidatedPrefixes.size === 0) return false;\n\n const colonIdx = key.indexOf(\":\");\n const path = colonIdx >= 0 ? key.slice(colonIdx + 1) : key;\n const pathOnly = path.split(\"?\")[0]!;\n\n const now = Date.now();\n for (const [prefix, expiresAt] of this.invalidatedPrefixes.entries()) {\n if (now > expiresAt) {\n this.invalidatedPrefixes.delete(prefix);\n } else if (pathOnly.startsWith(prefix)) {\n return true;\n }\n }\n return false;\n }\n\n /** Clear all entries */\n clear(): void {\n this.cache.clear();\n }\n\n get size(): number {\n return this.cache.size;\n }\n\n getStats(maxEntries: number): ResponseCacheStats {\n const total = this.hits + this.misses;\n return {\n entries: this.cache.size,\n maxEntries,\n hits: this.hits,\n misses: this.misses,\n hitRate: total > 0 ? Math.round((this.hits / total) * 100) / 100 : 0,\n evictions: this.evictions,\n };\n }\n}\n\n// ============================================================================\n// Fastify Type Extensions\n// ============================================================================\n\ndeclare module \"fastify\" {\n interface FastifyInstance {\n responseCache: {\n /** Invalidate all cached responses matching a path prefix */\n invalidate: (pathPrefix: string) => number;\n /** Clear the entire cache */\n invalidateAll: () => void;\n /** Get cache statistics */\n stats: () => ResponseCacheStats;\n /**\n * Route-level preHandler for cache lookup.\n * Wire AFTER authenticate in the preHandler chain so that\n * `request.user` / `request.scope` are populated before the\n * cache key is computed.\n *\n * `createCrudRouter` injects this automatically for GET routes.\n * For custom routes, add it manually:\n * ```typescript\n * fastify.get('/data', {\n * preHandler: [fastify.authenticate, fastify.responseCache.middleware],\n * }, handler);\n * ```\n */\n middleware: (\n request: FastifyRequest,\n reply: FastifyReply,\n ) => Promise<void>;\n };\n }\n interface FastifyRequest {\n /** @internal Cache TTL in seconds — set by onRequest, consumed by middleware + onSend */\n __arcCacheTTL?: number;\n }\n}\n\n// ============================================================================\n// Plugin Implementation\n// ============================================================================\n\nconst responseCachePluginImpl: FastifyPluginAsync<\n ResponseCacheOptions\n> = async (fastify: FastifyInstance, opts: ResponseCacheOptions = {}) => {\n const {\n maxEntries = 500,\n defaultTTL = 30,\n rules = [],\n exclude = [],\n invalidateOn = [\"POST\", \"PUT\", \"PATCH\", \"DELETE\"],\n xCacheHeader = true,\n statsPath = null,\n keyFn,\n } = opts;\n\n const cache = new LRUCache(maxEntries);\n const invalidateMethods = new Set(invalidateOn.map((m) => m.toUpperCase()));\n\n /** Find TTL for a given URL path (seconds) */\n function getTTL(url: string): number {\n const path = url.split(\"?\")[0]!;\n for (const rule of rules) {\n if (path.startsWith(rule.match)) {\n return rule.ttl;\n }\n }\n return defaultTTL;\n }\n\n /** Check if a URL should be excluded */\n function isExcluded(url: string): boolean {\n return exclude.some((p) => url.startsWith(p));\n }\n\n /** Build cache key — includes user/org scope by default to prevent cross-caller leaks */\n function buildKey(request: FastifyRequest): string | null {\n if (keyFn) return keyFn(request);\n // Scope the cache key to the user and org to prevent serving\n // User A's response to User B, or Org A's data to Org B\n const user = request.user as { id?: string; _id?: string } | undefined;\n const userId = user?.id ?? user?._id ?? \"anon\";\n const scope = request.scope as\n | { kind: string; organizationId?: string }\n | undefined;\n const orgId = scope?.organizationId ?? \"no-org\";\n return `${request.method}:${request.url}:u=${userId}:o=${orgId}`;\n }\n\n // ---- onRequest hook: mark cacheable GET/HEAD requests ----\n fastify.addHook(\"onRequest\", async (request: FastifyRequest) => {\n // Only mark GET/HEAD requests as cacheable (TTL computed early, key deferred to after auth)\n if (request.method !== \"GET\" && request.method !== \"HEAD\") return;\n if (isExcluded(request.url)) return;\n\n const ttl = getTTL(request.url);\n if (ttl <= 0) return;\n\n // Store TTL for downstream middleware + onSend\n request.__arcCacheTTL = ttl;\n });\n\n // ---- onResponse hook: invalidate cache only on successful (2xx) mutations ----\n // This runs AFTER the request completes, so failed/unauthorized mutations\n // do NOT purge the cache (prevents cache-purge DoS attacks).\n fastify.addHook(\n \"onResponse\",\n async (request: FastifyRequest, reply: FastifyReply) => {\n if (!invalidateMethods.has(request.method.toUpperCase())) return;\n\n // Only invalidate on successful responses\n const statusCode = reply.statusCode;\n if (statusCode < 200 || statusCode >= 300) return;\n\n const path = request.url.split(\"?\")[0]!;\n const segments = path.split(\"/\").filter(Boolean);\n\n // Detect item-scoped paths by checking if the last segment looks like\n // a resource ID (not a collection name). This handles both prefixed\n // routes like /api/products/123 (3 segments) and non-prefixed routes\n // like /products/123 (2 segments).\n const lastSegment = segments[segments.length - 1];\n const isItemScoped =\n segments.length >= 2 &&\n lastSegment != null &&\n /^[0-9a-f]{8,}$|^\\d+$/.test(lastSegment);\n\n if (isItemScoped) {\n // Item-level mutation — invalidate both the item and its collection\n const resourceRoot = \"/\" + segments.slice(0, -1).join(\"/\");\n cache.invalidatePrefix(resourceRoot);\n cache.invalidatePrefix(path);\n } else {\n // Collection-level mutation (e.g., POST /api/products)\n cache.invalidatePrefix(path);\n }\n },\n );\n\n // ---- Route-level middleware: serve from cache (AFTER auth) ----\n const cacheMiddleware = async (\n request: FastifyRequest,\n reply: FastifyReply,\n ): Promise<void> => {\n // Only check cache for cacheable requests\n const ttl = request.__arcCacheTTL;\n if (!ttl || ttl <= 0) return;\n if (request.method !== \"GET\" && request.method !== \"HEAD\") return;\n\n const key = buildKey(request);\n if (!key) return;\n\n const entry = cache.get(key);\n if (!entry) return; // Cache MISS — let handler run, onSend will store\n\n // Cache HIT — serve directly (auth has already validated the caller)\n if (xCacheHeader) {\n reply.header(\"x-cache\", \"HIT\");\n }\n\n for (const [name, value] of Object.entries(entry.headers)) {\n reply.header(name, value);\n }\n\n // Clear TTL so the onSend hook doesn't overwrite x-cache to MISS\n request.__arcCacheTTL = 0;\n reply.code(entry.statusCode).send(entry.body);\n };\n\n // ---- onSend hook: store in cache (recompute key — user is now populated) ----\n fastify.addHook(\n \"onSend\",\n async (request: FastifyRequest, reply: FastifyReply, payload) => {\n const ttl = request.__arcCacheTTL;\n if (!ttl || ttl <= 0) return payload;\n\n if (request.method !== \"GET\" && request.method !== \"HEAD\") return payload;\n\n // Only cache 2xx responses\n const statusCode = reply.statusCode;\n if (statusCode < 200 || statusCode >= 300) return payload;\n\n // Recompute key with now-populated user identity (auth has run by this point)\n const key = buildKey(request);\n if (!key) return payload;\n\n if (xCacheHeader) {\n reply.header(\"x-cache\", \"MISS\");\n }\n\n // Store in cache — handle Buffer correctly (String(buffer) produces '[object Buffer]')\n let body: string;\n if (typeof payload === \"string\") {\n body = payload;\n } else if (Buffer.isBuffer(payload)) {\n body = payload.toString(\"utf-8\");\n } else if (payload != null) {\n body = JSON.stringify(payload);\n } else {\n body = \"\";\n }\n\n // Capture cacheable headers\n const headers: Record<string, string> = {};\n const contentType = reply.getHeader(\"content-type\");\n if (contentType) headers[\"content-type\"] = String(contentType);\n const etag = reply.getHeader(\"etag\");\n if (etag) headers[\"etag\"] = String(etag);\n\n cache.set(key, {\n body,\n statusCode,\n headers,\n createdAt: Date.now(),\n ttl: ttl * 1000, // Convert to ms\n });\n\n return payload;\n },\n );\n\n // ---- Decorator ----\n fastify.decorate(\"responseCache\", {\n invalidate: (pathPrefix: string) => cache.invalidatePrefix(pathPrefix),\n invalidateAll: () => cache.clear(),\n stats: () => cache.getStats(maxEntries),\n middleware: cacheMiddleware,\n });\n\n // ---- Optional stats endpoint ----\n if (statsPath) {\n fastify.get(statsPath, async () => {\n return cache.getStats(maxEntries);\n });\n }\n\n // ---- Event-driven invalidation (requires eventPlugin) ----\n const evtInv = opts.eventInvalidation;\n if (evtInv && hasEvents(fastify)) {\n const crossResourcePatterns =\n typeof evtInv === \"object\" ? (evtInv.patterns ?? {}) : {};\n\n fastify.events\n .subscribe(\"*\", async (event) => {\n const parts = event.type.split(\".\");\n if (parts.length !== 2) return;\n const [resource, action] = parts;\n if (!resource || ![\"created\", \"updated\", \"deleted\"].includes(action!))\n return;\n\n // Invalidate the resource's own cache prefix (both singular and plural)\n cache.invalidatePrefix(`/${resource}s`);\n cache.invalidatePrefix(`/${resource}`);\n\n // Apply cross-resource invalidation rules\n for (const [pattern, prefixes] of Object.entries(\n crossResourcePatterns,\n )) {\n if (eventMatchesPattern(event.type, pattern)) {\n for (const prefix of prefixes) {\n cache.invalidatePrefix(prefix);\n }\n }\n }\n })\n .catch((err) => {\n fastify.log?.warn?.(\n { err },\n \"Response cache: failed to subscribe to events for invalidation\",\n );\n });\n\n fastify.log?.debug?.(\"Response cache: event-driven invalidation enabled\");\n } else if (evtInv) {\n fastify.log?.warn?.(\n \"Response cache: eventInvalidation enabled but eventPlugin not registered.\",\n );\n }\n\n fastify.log?.debug?.(\n `Response cache: registered (max=${maxEntries}, defaultTTL=${defaultTTL}s, rules=${rules.length})`,\n );\n};\n\n/** Check if an event type matches a pattern (supports wildcards) */\nfunction eventMatchesPattern(type: string, pattern: string): boolean {\n if (pattern === \"*\") return true;\n if (pattern.endsWith(\".*\")) return type.startsWith(pattern.slice(0, -1));\n return type === pattern;\n}\n\nexport const responseCachePlugin: FastifyPluginAsync<ResponseCacheOptions> = fp(\n responseCachePluginImpl,\n {\n name: \"arc-response-cache\",\n fastify: \"5.x\",\n },\n) as unknown as FastifyPluginAsync<ResponseCacheOptions>;\n\nexport default responseCachePlugin;\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tracing-entry.mjs","names":[],"sources":["../../src/plugins/tracing.ts"],"sourcesContent":["/**\n * OpenTelemetry Distributed Tracing Plugin\n *\n * Traces HTTP requests, repository operations, and MongoDB queries\n * across the entire application lifecycle.\n *\n * @example\n * import { tracingPlugin } from '@classytic/arc/plugins';\n *\n * await fastify.register(tracingPlugin, {\n * serviceName: 'my-api',\n * exporterUrl: 'http://localhost:4318/v1/traces', // OTLP endpoint\n * });\n */\n\nimport type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';\nimport fp from 'fastify-plugin';\nimport { createRequire } from 'node:module';\n\nconst require = createRequire(import.meta.url);\n\n// OpenTelemetry imports (peer dependencies)\nlet trace: any;\nlet context: any;\nlet SpanStatusCode: any;\nlet NodeTracerProvider: any;\nlet BatchSpanProcessor: any;\nlet OTLPTraceExporter: any;\nlet HttpInstrumentation: any;\nlet MongoDBInstrumentation: any;\nlet getNodeAutoInstrumentations: any;\n\n// Try to load OpenTelemetry (optional peer dependency)\nlet isAvailable = false;\ntry {\n const api = require('@opentelemetry/api');\n trace = api.trace;\n context = api.context;\n SpanStatusCode = api.SpanStatusCode;\n\n const sdkNode = require('@opentelemetry/sdk-node');\n NodeTracerProvider = sdkNode.NodeTracerProvider;\n BatchSpanProcessor = sdkNode.BatchSpanProcessor;\n\n const exporterTraceOtlp = require('@opentelemetry/exporter-trace-otlp-http');\n OTLPTraceExporter = exporterTraceOtlp.OTLPTraceExporter;\n\n const instrHttp = require('@opentelemetry/instrumentation-http');\n HttpInstrumentation = instrHttp.HttpInstrumentation;\n\n const instrMongo = require('@opentelemetry/instrumentation-mongodb');\n MongoDBInstrumentation = instrMongo.MongoDBInstrumentation;\n\n const autoInstr = require('@opentelemetry/auto-instrumentations-node');\n getNodeAutoInstrumentations = autoInstr.getNodeAutoInstrumentations;\n\n isAvailable = true;\n} catch (e) {\n // OpenTelemetry not installed - plugin will be no-op\n}\n\nexport interface TracingOptions {\n /**\n * Service name for traces\n */\n serviceName?: string;\n\n /**\n * OTLP exporter endpoint URL\n * @default 'http://localhost:4318/v1/traces'\n */\n exporterUrl?: string;\n\n /**\n * Enable auto-instrumentation for HTTP, MongoDB, etc.\n * @default true\n */\n autoInstrumentation?: boolean;\n\n /**\n * Sample rate (0.0 to 1.0)\n * @default 1.0 (trace everything)\n */\n sampleRate?: number;\n}\n\ninterface TracerContext {\n tracer: any;\n currentSpan: any;\n}\n\ndeclare module 'fastify' {\n interface FastifyRequest {\n tracer?: TracerContext;\n }\n}\n\n/**\n * Create a tracer provider\n */\nfunction createTracerProvider(options: TracingOptions) {\n if (!isAvailable) {\n return null;\n }\n\n const { serviceName = '@classytic/arc', exporterUrl = 'http://localhost:4318/v1/traces' } =\n options;\n\n const exporter = new OTLPTraceExporter({\n url: exporterUrl,\n });\n\n const provider = new NodeTracerProvider({\n resource: {\n attributes: {\n 'service.name': serviceName,\n 'service.version': '1.0.0',\n },\n },\n });\n\n provider.addSpanProcessor(new BatchSpanProcessor(exporter));\n provider.register();\n\n return provider;\n}\n\n/**\n * OpenTelemetry Distributed Tracing Plugin\n */\nasync function tracingPlugin(fastify: FastifyInstance, options: TracingOptions = {}) {\n const {\n serviceName = '@classytic/arc',\n autoInstrumentation = true,\n sampleRate = 1.0,\n } = options;\n\n // Skip if OpenTelemetry is not available\n if (!isAvailable) {\n fastify.log.warn('OpenTelemetry not installed. Tracing disabled.');\n fastify.log.warn('Install: npm install @opentelemetry/api @opentelemetry/sdk-node');\n return;\n }\n\n // Initialize tracer provider\n const provider = createTracerProvider(options);\n if (!provider) {\n return;\n }\n\n // Auto-instrumentation — enable HTTP + MongoDB tracing\n if (autoInstrumentation && getNodeAutoInstrumentations) {\n const instrumentations = getNodeAutoInstrumentations({\n '@opentelemetry/instrumentation-http': {\n enabled: true,\n },\n '@opentelemetry/instrumentation-mongodb': {\n enabled: true,\n },\n });\n for (const instrumentation of instrumentations) {\n instrumentation.enable();\n }\n fastify.log.debug('OpenTelemetry auto-instrumentation enabled');\n }\n\n const tracer = trace.getTracer(serviceName);\n\n // Add tracer to request\n fastify.decorateRequest('tracer', undefined);\n\n // Create span for each HTTP request\n fastify.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => {\n // Sampling\n if (Math.random() > sampleRate) {\n return;\n }\n\n const span = tracer.startSpan(`HTTP ${request.method} ${request.url}`, {\n kind: 1, // SpanKind.SERVER\n attributes: {\n 'http.method': request.method,\n 'http.url': request.url,\n 'http.target': request.routeOptions?.url ?? request.url,\n 'http.host': request.hostname,\n 'http.scheme': request.protocol,\n 'http.user_agent': request.headers['user-agent'],\n },\n });\n\n // Store span in request for child spans\n request.tracer = {\n tracer,\n currentSpan: span,\n };\n\n // Set active context\n context.with(trace.setSpan(context.active(), span), () => {\n // Context is now active for this request\n });\n });\n\n // End span after response\n fastify.addHook('onResponse', async (request: FastifyRequest, reply: FastifyReply) => {\n if (!request.tracer?.currentSpan) {\n return;\n }\n\n const span = request.tracer.currentSpan;\n\n // Add response attributes\n span.setAttributes({\n 'http.status_code': reply.statusCode,\n 'http.response_content_length': reply.getHeader('content-length'),\n });\n\n // Set span status\n if (reply.statusCode >= 500) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: `HTTP ${reply.statusCode}`,\n });\n } else {\n span.setStatus({ code: SpanStatusCode.OK });\n }\n\n span.end();\n });\n\n // Error tracking\n fastify.addHook('onError', async (request: FastifyRequest, reply: FastifyReply, error: Error) => {\n if (!request.tracer?.currentSpan) {\n return;\n }\n\n const span = request.tracer.currentSpan;\n span.recordException(error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error.message,\n });\n });\n\n fastify.log.debug({ serviceName }, 'OpenTelemetry tracing enabled');\n}\n\n/**\n * Utility to create custom spans in your code\n *\n * @example\n * import { createSpan } from '@classytic/arc/plugins';\n *\n * async function expensiveOperation(req) {\n * return createSpan(req, 'expensiveOperation', async (span) => {\n * span.setAttribute('custom.attribute', 'value');\n * return await doWork();\n * });\n * }\n */\nexport function createSpan<T>(\n request: FastifyRequest,\n name: string,\n fn: (span: any) => Promise<T>,\n attributes?: Record<string, any>\n): Promise<T> {\n if (!isAvailable || !request.tracer) {\n // No tracing available, just execute function\n return fn(null);\n }\n\n const { tracer, currentSpan } = request.tracer;\n\n const span = tracer.startSpan(\n name,\n {\n parent: currentSpan,\n attributes: attributes || {},\n },\n trace.setSpan(context.active(), currentSpan)\n );\n\n return context.with(trace.setSpan(context.active(), span), async () => {\n try {\n const result = await fn(span);\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error: any) {\n span.recordException(error);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error.message,\n });\n throw error;\n } finally {\n span.end();\n }\n });\n}\n\n/**\n * Decorator to automatically trace repository methods\n *\n * @example\n * class ProductRepository extends Repository {\n * @traced()\n * async findActive() {\n * return this.findAll({ filter: { isActive: true } });\n * }\n * }\n */\nexport function traced(spanName?: string) {\n return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n\n descriptor.value = async function (this: any, ...args: any[]) {\n // Extract request from args if available\n const request = args.find((arg) => arg && arg.tracer);\n\n if (!request?.tracer) {\n // No tracing context, just execute\n return originalMethod.apply(this, args);\n }\n\n const name = spanName || `${target.constructor.name}.${propertyKey}`;\n return createSpan(request, name, async (span) => {\n if (span) {\n span.setAttribute('db.operation', propertyKey);\n span.setAttribute('db.system', 'mongodb');\n }\n return originalMethod.apply(this, args);\n });\n };\n\n return descriptor;\n };\n}\n\n/**\n * Check if OpenTelemetry is available\n */\nexport function isTracingAvailable(): boolean {\n return isAvailable;\n}\n\nexport default fp(tracingPlugin, {\n name: 'arc-tracing',\n fastify: '5.x',\n});\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"pluralize-CEweyOEm.mjs","names":[],"sources":["../src/cli/utils/pluralize.ts"],"sourcesContent":["/**\n * Lightweight English pluralization for the Arc CLI.\n *\n * Covers the common cases developers hit when naming resources:\n * company → companies\n * category → categories\n * status → statuses\n * address → addresses\n * person → people\n * child → children\n * bus → buses\n * box → boxes\n * quiz → quizzes\n * leaf → leaves\n * wolf → wolves\n *\n * No external dependencies — designed to keep the CLI install-free.\n */\n\n// Irregular nouns that can't be handled by suffix rules\nconst IRREGULARS: Record<string, string> = {\n person: 'people',\n child: 'children',\n man: 'men',\n woman: 'women',\n mouse: 'mice',\n goose: 'geese',\n tooth: 'teeth',\n foot: 'feet',\n ox: 'oxen',\n datum: 'data',\n medium: 'media',\n index: 'indices',\n matrix: 'matrices',\n vertex: 'vertices',\n criterion: 'criteria',\n};\n\n// Words that are the same singular and plural\nconst UNCOUNTABLES = new Set([\n 'sheep', 'fish', 'deer', 'series', 'species', 'money',\n 'rice', 'information', 'equipment', 'media', 'data',\n]);\n\n/**\n * Pluralize an English word.\n *\n * @param word - Singular noun (e.g. \"company\", \"product\", \"person\")\n * @returns Plural form (e.g. \"companies\", \"products\", \"people\")\n */\nexport function pluralize(word: string): string {\n const lower = word.toLowerCase();\n\n // Uncountable — return as-is\n if (UNCOUNTABLES.has(lower)) return word;\n\n // Irregular — preserve original casing of first char\n if (IRREGULARS[lower]) {\n const plural = IRREGULARS[lower];\n return word[0]! === word[0]!.toUpperCase()\n ? plural.charAt(0).toUpperCase() + plural.slice(1)\n : plural;\n }\n\n // Suffix rules (order matters — most specific first)\n\n // -fe / -f → -ves (leaf → leaves, wolf → wolves, knife → knives)\n if (lower.endsWith('fe')) return word.slice(0, -2) + 'ves';\n if (lower.endsWith('f') && !lower.endsWith('ff') && !lower.endsWith('roof') && !lower.endsWith('chief') && !lower.endsWith('belief')) {\n return word.slice(0, -1) + 'ves';\n }\n\n // consonant + y → -ies (company → companies, category → categories)\n if (lower.endsWith('y') && !/[aeiou]y$/i.test(lower)) {\n return word.slice(0, -1) + 'ies';\n }\n\n // -is → -es (analysis → analyses, crisis → crises)\n if (lower.endsWith('is')) return word.slice(0, -2) + 'es';\n\n // -us → -i (only Latin-origin words, not status/bus/campus/virus)\n const LATIN_US_TO_I = new Set([\n 'cactus', 'stimulus', 'focus', 'fungus', 'nucleus',\n 'syllabus', 'radius', 'alumnus', 'terminus', 'bacillus',\n ]);\n if (LATIN_US_TO_I.has(lower)) return word.slice(0, -2) + 'i';\n\n // -z at end → double z + -es (quiz → quizzes, fez → fezzes)\n if (lower.endsWith('z') && !lower.endsWith('zz')) return word + 'zes';\n\n // sibilant endings: -s, -ss, -sh, -ch, -x, -zz → -es\n if (/(?:s|sh|ch|x|zz)$/i.test(lower)) return word + 'es';\n\n // -o → -es for common cases (hero → heroes, tomato → tomatoes)\n // but not for words ending in a vowel + o (radio → radios)\n if (lower.endsWith('o') && !/[aeiou]o$/i.test(lower)) return word + 'es';\n\n // Default: just add -s\n return word + 's';\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAoBA,MAAM,aAAqC;CACzC,QAAQ;CACR,OAAO;CACP,KAAK;CACL,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,MAAM;CACN,IAAI;CACJ,OAAO;CACP,QAAQ;CACR,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,WAAW;CACZ;AAGD,MAAM,eAAe,IAAI,IAAI;CAC3B;CAAS;CAAQ;CAAQ;CAAU;CAAW;CAC9C;CAAQ;CAAe;CAAa;CAAS;CAC9C,CAAC;;;;;;;AAQF,SAAgB,UAAU,MAAsB;CAC9C,MAAM,QAAQ,KAAK,aAAa;AAGhC,KAAI,aAAa,IAAI,MAAM,CAAE,QAAO;AAGpC,KAAI,WAAW,QAAQ;EACrB,MAAM,SAAS,WAAW;AAC1B,SAAO,KAAK,OAAQ,KAAK,GAAI,aAAa,GACtC,OAAO,OAAO,EAAE,CAAC,aAAa,GAAG,OAAO,MAAM,EAAE,GAChD;;AAMN,KAAI,MAAM,SAAS,KAAK,CAAE,QAAO,KAAK,MAAM,GAAG,GAAG,GAAG;AACrD,KAAI,MAAM,SAAS,IAAI,IAAI,CAAC,MAAM,SAAS,KAAK,IAAI,CAAC,MAAM,SAAS,OAAO,IAAI,CAAC,MAAM,SAAS,QAAQ,IAAI,CAAC,MAAM,SAAS,SAAS,CAClI,QAAO,KAAK,MAAM,GAAG,GAAG,GAAG;AAI7B,KAAI,MAAM,SAAS,IAAI,IAAI,CAAC,aAAa,KAAK,MAAM,CAClD,QAAO,KAAK,MAAM,GAAG,GAAG,GAAG;AAI7B,KAAI,MAAM,SAAS,KAAK,CAAE,QAAO,KAAK,MAAM,GAAG,GAAG,GAAG;AAOrD,KAJsB,IAAI,IAAI;EAC5B;EAAU;EAAY;EAAS;EAAU;EACzC;EAAY;EAAU;EAAW;EAAY;EAC9C,CAAC,CACgB,IAAI,MAAM,CAAE,QAAO,KAAK,MAAM,GAAG,GAAG,GAAG;AAGzD,KAAI,MAAM,SAAS,IAAI,IAAI,CAAC,MAAM,SAAS,KAAK,CAAE,QAAO,OAAO;AAGhE,KAAI,qBAAqB,KAAK,MAAM,CAAE,QAAO,OAAO;AAIpD,KAAI,MAAM,SAAS,IAAI,IAAI,CAAC,aAAa,KAAK,MAAM,CAAE,QAAO,OAAO;AAGpE,QAAO,OAAO"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/policies/PolicyInterface.ts","../../src/policies/helpers.ts"],"mappings":";;;;;;;UA+CiB,YAAA;EAyMb;;;EArMF,OAAA;EA4NA;;;;EAtNA,MAAA;EAwN6B;;;;AAsB/B;;;;;;;;;;AAWA;EAxOE,OAAA,GAAU,MAAA;;;;AAkPZ;;;;;;;;;EApOE,SAAA;IACE,OAAA;IACA,OAAA;EAAA;EAiSY;;;;;;;;;AAmDf;;EAtUC,QAAA,GAAW,MAAA;AAAA;;;;UAMI,aAAA;EAoUc;;;;EA/T7B,QAAA;ECnFc;;;EDwFd,IAAA;ECrFW;;;ED0FX,MAAA;EC1F0D;;;ED+F1D,KAAA;EC/FW;;;;EAAA,CDqGV,GAAA;AAAA;;AC5BH;;;;;;;;;AA4HA;;;;;;;;;AA2FA;;;;;AA6BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UDhIiB,YAAA;;;;;;;;;;;;;;;;;;;;;EAqBf,GAAA,CACE,IAAA,OACA,SAAA,UACA,OAAA,GAAU,aAAA,GACT,YAAA,GAAe,OAAA,CAAQ,YAAA;;;;;;;;;;;;;;;;;;;;;EAsB1B,YAAA,CACE,SAAA,YACE,OAAA,EAAS,cAAA,EAAgB,KAAA,EAAO,YAAA,KAAiB,OAAA;AAAA;;;;;;;;;;;;;;;;;;;;KAsB3C,aAAA,mBAAgC,MAAA,EAAQ,OAAA,KAAY,YAAA;;;;;;;;;;UAW/C,sBAAA;;EAEf,QAAA;;EAEA,MAAA;AAAA;;;;UAMe,0BAAA;;EAEf,UAAA,EAAY,sBAAA;;;;;;;;;;;EAWZ,eAAA,IAAmB,MAAA,UAAgB,QAAA,UAAkB,MAAA,aAAmB,OAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkD1D,yBAAA,CACd,OAAA,EAAS,0BAAA,GACR,eAAA;AAAA;EAAA,UAoDS,cAAA;IACR,YAAA,GAAe,YAAA;EAAA;AAAA;;;;;;;;;;;;;;;AApUnB;;;;;;;;;;;iBC9EgB,sBAAA,CACd,MAAA,EAAQ,YAAA,EACR,SAAA,YACE,OAAA,EAAS,cAAA,EAAgB,KAAA,EAAO,YAAA,KAAiB,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ADoQrD;iBC3LgB,eAAA,CAAA,GAAmB,QAAA,EAAU,YAAA,KAAiB,YAAA;;;;;;;;;ADsM9D;;;;;AAUA;;;;;;;iBCpFgB,SAAA,CAAA,GAAa,QAAA,EAAU,YAAA,KAAiB,YAAA;;;;;;ADmJxD;;;;;;;iBCxDgB,QAAA,CAAA,GAAY,YAAA;;AD2G3B;;;;;;;;;;;;;AC9YD;iBAgUgB,OAAA,CAAQ,MAAA,YAAmC,YAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/policies/PolicyInterface.ts","../../src/policies/helpers.ts"],"sourcesContent":["/**\n * Policy Interface\n *\n * Pluggable authorization interface for Arc.\n * Apps implement this interface to define custom authorization strategies.\n *\n * @example RBAC Policy\n * ```typescript\n * class RBACPolicy implements PolicyEngine {\n * can(user, operation, context) {\n * return {\n * allowed: user.roles.includes('admin'),\n * reason: 'Admin role required',\n * };\n * }\n * toMiddleware(operation) {\n * return async (request, reply) => {\n * const result = await this.can(request.user, operation);\n * if (!result.allowed) {\n * reply.code(403).send({ error: result.reason });\n * }\n * };\n * }\n * }\n * ```\n *\n * @example ABAC (Attribute-Based) Policy\n * ```typescript\n * class ABACPolicy implements PolicyEngine {\n * can(user, operation, context) {\n * return {\n * allowed: this.evaluateAttributes(user, operation, context),\n * filters: { department: user.department },\n * fieldMask: { exclude: ['salary', 'ssn'] },\n * };\n * }\n * // ...\n * }\n * ```\n */\n\nimport type { FastifyRequest, FastifyReply } from 'fastify';\nimport type { PermissionCheck } from '../permissions/types.js';\n\n/**\n * Policy result returned by can() method\n */\nexport interface PolicyResult {\n /**\n * Whether the operation is allowed\n */\n allowed: boolean;\n\n /**\n * Human-readable reason if denied\n * Returned in 403 error responses\n */\n reason?: string;\n\n /**\n * Query filters to apply (for list operations)\n *\n * @example\n * ```typescript\n * // Multi-tenant filter\n * { organizationId: user.organizationId }\n *\n * // Ownership filter\n * { userId: user.id }\n *\n * // Complex filter\n * { $or: [{ public: true }, { createdBy: user.id }] }\n * ```\n */\n filters?: Record<string, any>;\n\n /**\n * Fields to include/exclude in response\n *\n * @example\n * ```typescript\n * // Hide sensitive fields from non-admins\n * { exclude: ['password', 'ssn', 'salary'] }\n *\n * // Only show specific fields\n * { include: ['name', 'email', 'role'] }\n * ```\n */\n fieldMask?: {\n include?: string[];\n exclude?: string[];\n };\n\n /**\n * Additional context for downstream middleware\n *\n * @example\n * ```typescript\n * {\n * auditLog: { action: 'read', resource: 'patient', userId: user.id },\n * rateLimit: { tier: user.subscriptionTier },\n * }\n * ```\n */\n metadata?: Record<string, any>;\n}\n\n/**\n * Policy context provided to can() method\n */\nexport interface PolicyContext {\n /**\n * The document being accessed (for update/delete/get)\n * Populated by fetchDocument middleware\n */\n document?: any;\n\n /**\n * Request body (for create/update)\n */\n body?: any;\n\n /**\n * Request params (e.g., :id from route)\n */\n params?: any;\n\n /**\n * Request query parameters\n */\n query?: any;\n\n /**\n * Additional app-specific context\n * Can include anything your policy needs to make decisions\n */\n [key: string]: any;\n}\n\n/**\n * Policy Engine Interface\n *\n * Implement this interface to create your own authorization strategy.\n *\n * Arc provides the interface, apps provide the implementation.\n * This follows the same pattern as:\n * - Database drivers (interface: query(), implementation: PostgreSQL, MySQL)\n * - Storage providers (interface: upload(), implementation: S3, Azure)\n * - Authentication strategies (interface: verify(), implementation: JWT, OAuth)\n *\n * @example E-commerce RBAC + Ownership\n * ```typescript\n * class EcommercePolicyEngine implements PolicyEngine {\n * constructor(private config: { roles: Record<string, string[]> }) {}\n *\n * can(user, operation, context) {\n * // Check RBAC\n * const allowedRoles = this.config.roles[operation] || [];\n * if (!user.roles.some(r => allowedRoles.includes(r))) {\n * return { allowed: false, reason: 'Insufficient permissions' };\n * }\n *\n * // Check ownership for update/delete\n * if (['update', 'delete'].includes(operation)) {\n * if (context.document.userId !== user.id) {\n * return { allowed: false, reason: 'Not the owner' };\n * }\n * }\n *\n * // Multi-tenant filter for list\n * if (operation === 'list') {\n * return {\n * allowed: true,\n * filters: { organizationId: user.organizationId },\n * };\n * }\n *\n * return { allowed: true };\n * }\n *\n * toMiddleware(operation) {\n * return async (request, reply) => {\n * const result = await this.can(request.user, operation, {\n * document: request.document,\n * body: request.body,\n * params: request.params,\n * query: request.query,\n * });\n *\n * if (!result.allowed) {\n * reply.code(403).send({ error: result.reason });\n * }\n *\n * // Attach filters/fieldMask to request\n * request.policyResult = result;\n * };\n * }\n * }\n * ```\n *\n * @example HIPAA Compliance\n * ```typescript\n * class HIPAAPolicyEngine implements PolicyEngine {\n * can(user, operation, context) {\n * // Check patient consent\n * // Verify user certifications\n * // Check data sensitivity level\n * // Create audit log entry\n *\n * return {\n * allowed: this.checkHIPAACompliance(user, operation, context),\n * reason: 'HIPAA compliance check failed',\n * metadata: {\n * auditLog: this.createAuditEntry(user, operation),\n * },\n * };\n * }\n *\n * toMiddleware(operation) {\n * // HIPAA-specific middleware with audit logging\n * }\n * }\n * ```\n */\nexport interface PolicyEngine {\n /**\n * Check if user can perform operation\n *\n * @param user - User object from request (request.user)\n * @param operation - Operation name (list, get, create, update, delete, custom)\n * @param context - Additional context (document, body, params, query, etc.)\n * @returns Policy result with allowed/denied and optional filters/fieldMask\n *\n * @example\n * ```typescript\n * const result = await policy.can(request.user, 'update', {\n * document: existingDocument,\n * body: request.body,\n * });\n *\n * if (!result.allowed) {\n * throw new Error(result.reason);\n * }\n * ```\n */\n can(\n user: any,\n operation: string,\n context?: PolicyContext\n ): PolicyResult | Promise<PolicyResult>;\n\n /**\n * Generate Fastify middleware for this policy\n *\n * Called during route registration to create preHandler middleware.\n * Middleware should:\n * 1. Call can() with request context\n * 2. Return 403 if denied\n * 3. Attach result to request for downstream use\n *\n * @param operation - Operation name (list, get, create, update, delete)\n * @returns Fastify preHandler middleware\n *\n * @example\n * ```typescript\n * const middleware = policy.toMiddleware('update');\n * fastify.put('/products/:id', {\n * preHandler: [authenticate, middleware],\n * }, handler);\n * ```\n */\n toMiddleware(\n operation: string\n ): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;\n}\n\n/**\n * Policy factory function signature\n *\n * Policies are typically created via factory functions that accept configuration.\n *\n * @example\n * ```typescript\n * export function definePolicy(config: PolicyConfig): PolicyEngine {\n * return new MyPolicyEngine(config);\n * }\n *\n * // Usage\n * const productPolicy = definePolicy({\n * resource: 'product',\n * roles: { list: ['user'], create: ['admin'] },\n * ownership: { field: 'userId', operations: ['update', 'delete'] },\n * });\n * ```\n */\nexport type PolicyFactory<TConfig = any> = (config: TConfig) => PolicyEngine;\n\n/**\n * Extended Fastify request with policy result\n */\n/**\n * Access control statement\n *\n * Maps to Better Auth's organization permission model\n * where permissions are defined as resource + action pairs.\n */\nexport interface AccessControlStatement {\n /** Resource name (e.g., 'product', 'order') */\n resource: string;\n /** Allowed actions on this resource */\n action: string[];\n}\n\n/**\n * Options for createAccessControlPolicy\n */\nexport interface AccessControlPolicyOptions {\n /** Permission statements defining resource-action pairs */\n statements: AccessControlStatement[];\n /**\n * Optional async permission check against external source (e.g., org role permissions).\n * Called when the static statements allow the action — use this for dynamic checks\n * like verifying the user's org role actually grants the permission.\n *\n * @param userId - ID of the user\n * @param resource - Resource being accessed\n * @param action - Action being performed\n * @returns Whether the user has the permission\n */\n checkPermission?: (userId: string, resource: string, action: string) => Promise<boolean>;\n}\n\n/**\n * Create a PermissionCheck from access control statements.\n *\n * Maps Better Auth's statement-based access control model to Arc's\n * PermissionCheck function, which can be used directly in resource permissions.\n *\n * The returned PermissionCheck:\n * 1. Looks up the resource + action in the statements list\n * 2. If no matching statement exists, denies access\n * 3. If a matching statement exists and `checkPermission` is provided,\n * calls it for dynamic verification (e.g., check org role)\n * 4. If `checkPermission` is not provided, allows access based on static statements\n *\n * @example Static statements only\n * ```typescript\n * import { createAccessControlPolicy } from '@classytic/arc/policies';\n *\n * const editorPermissions = createAccessControlPolicy({\n * statements: [\n * { resource: 'product', action: ['create', 'update'] },\n * { resource: 'order', action: ['read'] },\n * ],\n * });\n *\n * // Use in resource config\n * defineResource({\n * name: 'product',\n * permissions: {\n * create: editorPermissions,\n * update: editorPermissions,\n * },\n * });\n * ```\n *\n * @example With dynamic permission check (Better Auth org roles)\n * ```typescript\n * const policy = createAccessControlPolicy({\n * statements: [\n * { resource: 'product', action: ['create', 'update'] },\n * { resource: 'order', action: ['read'] },\n * ],\n * checkPermission: async (userId, resource, action) => {\n * return hasOrgPermission(userId, resource, action);\n * },\n * });\n * ```\n */\nexport function createAccessControlPolicy(\n options: AccessControlPolicyOptions\n): PermissionCheck {\n // Pre-compute a lookup map: resource -> Set<action> for O(1) checks\n const statementMap = new Map<string, Set<string>>();\n for (const statement of options.statements) {\n const existing = statementMap.get(statement.resource);\n if (existing) {\n for (const action of statement.action) {\n existing.add(action);\n }\n } else {\n statementMap.set(statement.resource, new Set(statement.action));\n }\n }\n\n const permissionCheck: PermissionCheck = async (context) => {\n const { user, resource, action } = context;\n\n // Check if the action is allowed by any statement\n const allowedActions = statementMap.get(resource);\n if (!allowedActions || !allowedActions.has(action)) {\n return {\n granted: false,\n reason: `Action '${action}' is not permitted on resource '${resource}'`,\n };\n }\n\n // If a dynamic permission check is provided, verify against it\n if (options.checkPermission) {\n const userId = user?.id ?? user?._id;\n if (!userId) {\n return {\n granted: false,\n reason: 'Authentication required',\n };\n }\n\n const hasPermission = await options.checkPermission(String(userId), resource, action);\n if (!hasPermission) {\n return {\n granted: false,\n reason: `User does not have '${action}' permission on '${resource}'`,\n };\n }\n }\n\n return { granted: true };\n };\n\n return permissionCheck;\n}\n\ndeclare module 'fastify' {\n interface FastifyRequest {\n policyResult?: PolicyResult;\n }\n}\n","/**\n * Policy Helper Utilities\n *\n * Common operations for working with PolicyEngine implementations.\n */\n\nimport type { FastifyRequest, FastifyReply } from 'fastify';\nimport type { PolicyEngine, PolicyResult } from './PolicyInterface.js';\n\n/**\n * Helper to create Fastify middleware from any PolicyEngine implementation\n *\n * This is a convenience function that provides a standard middleware pattern.\n * Most policies can use this instead of implementing toMiddleware() manually.\n *\n * @param policy - Policy engine instance\n * @param operation - Operation name (list, get, create, update, delete)\n * @returns Fastify preHandler middleware\n *\n * @example\n * ```typescript\n * class SimplePolicy implements PolicyEngine {\n * can(user, operation) {\n * return { allowed: user.isActive };\n * }\n *\n * toMiddleware(operation) {\n * return createPolicyMiddleware(this, operation);\n * }\n * }\n * ```\n */\nexport function createPolicyMiddleware(\n policy: PolicyEngine,\n operation: string\n): (request: FastifyRequest, reply: FastifyReply) => Promise<void> {\n return async function policyMiddleware(\n request: FastifyRequest,\n reply: FastifyReply\n ): Promise<void> {\n // Build context from request\n const context = {\n document: request.document,\n body: request.body,\n params: request.params,\n query: request.query,\n };\n\n // Check policy\n const result = await policy.can(request.user, operation, context);\n\n if (!result.allowed) {\n return reply.code(403).send({\n success: false,\n error: 'Access denied',\n message: result.reason || 'You do not have permission to perform this action',\n });\n }\n\n // Attach result to request for downstream use\n request.policyResult = result;\n\n // Apply policy filters on trusted location (for list operations)\n if (result.filters && Object.keys(result.filters).length > 0) {\n request._policyFilters = result.filters;\n }\n\n // Apply field mask (for response serialization)\n if (result.fieldMask) {\n request.fieldMask = result.fieldMask;\n }\n\n // Attach metadata (for downstream middleware/logging)\n if (result.metadata) {\n request.policyMetadata = result.metadata;\n }\n };\n}\n\n/**\n * Combine multiple policies with AND logic\n *\n * All policies must allow the operation for it to succeed.\n * First denial stops evaluation and returns the denial reason.\n *\n * @param policies - Array of policy engines to combine\n * @returns Combined policy engine\n *\n * @example\n * ```typescript\n * const combinedPolicy = combinePolicies(\n * rbacPolicy, // Must have correct role\n * ownershipPolicy, // Must own the resource\n * auditPolicy, // Logs access for compliance\n * );\n *\n * // All three policies must pass for the operation to succeed\n * const result = await combinedPolicy.can(user, 'update', context);\n * ```\n *\n * @example Multi-tenant + RBAC\n * ```typescript\n * const policy = combinePolicies(\n * definePolicy({ tenant: { field: 'organizationId' } }),\n * definePolicy({ roles: { update: ['admin', 'editor'] } }),\n * );\n * ```\n */\nexport function combinePolicies(...policies: PolicyEngine[]): PolicyEngine {\n if (policies.length === 0) {\n throw new Error('combinePolicies requires at least one policy');\n }\n\n if (policies.length === 1) {\n return policies[0]!;\n }\n\n return {\n async can(user: any, operation: string, context?: any): Promise<PolicyResult> {\n const results: PolicyResult[] = [];\n\n for (const policy of policies) {\n const result = await policy.can(user, operation, context);\n\n if (!result.allowed) {\n // First denial stops evaluation\n return result;\n }\n\n results.push(result);\n }\n\n // Merge all results\n const mergedResult: PolicyResult = {\n allowed: true,\n filters: {},\n metadata: {},\n };\n\n // Merge filters (AND logic - all filters must match)\n for (const result of results) {\n if (result.filters) {\n Object.assign(mergedResult.filters!, result.filters);\n }\n }\n\n // Merge field masks (union of excludes, intersection of includes)\n const allExcludes = new Set<string>();\n const allIncludes: Set<string>[] = [];\n\n for (const result of results) {\n if (result.fieldMask?.exclude) {\n result.fieldMask.exclude.forEach((field) => allExcludes.add(field));\n }\n if (result.fieldMask?.include) {\n allIncludes.push(new Set(result.fieldMask.include));\n }\n }\n\n if (allExcludes.size > 0 || allIncludes.length > 0) {\n mergedResult.fieldMask = {};\n\n if (allExcludes.size > 0) {\n mergedResult.fieldMask.exclude = Array.from(allExcludes);\n }\n\n if (allIncludes.length > 0) {\n // Intersection of all includes\n const intersection = allIncludes.reduce((acc, set) => {\n return new Set([...acc].filter((x) => set.has(x)));\n });\n if (intersection.size > 0) {\n mergedResult.fieldMask.include = Array.from(intersection);\n }\n }\n }\n\n // Merge metadata\n for (const result of results) {\n if (result.metadata) {\n Object.assign(mergedResult.metadata!, result.metadata);\n }\n }\n\n // Clean up empty objects\n if (Object.keys(mergedResult.filters!).length === 0) {\n delete mergedResult.filters;\n }\n if (Object.keys(mergedResult.metadata!).length === 0) {\n delete mergedResult.metadata;\n }\n\n return mergedResult;\n },\n\n toMiddleware(operation: string) {\n const middlewares = policies.map((p) => p.toMiddleware(operation));\n\n return async (request: FastifyRequest, reply: FastifyReply) => {\n for (const middleware of middlewares) {\n await middleware(request, reply);\n\n // Stop if response was sent (denial)\n if (reply.sent) {\n return;\n }\n }\n };\n },\n };\n}\n\n/**\n * Combine multiple policies with OR logic\n *\n * At least one policy must allow the operation for it to succeed.\n * If all policies deny, returns the first denial reason.\n *\n * @param policies - Array of policy engines to combine\n * @returns Combined policy engine\n *\n * @example\n * ```typescript\n * const policy = anyPolicy(\n * ownerPolicy, // User owns the resource\n * adminPolicy, // OR user is admin\n * publicPolicy, // OR resource is public\n * );\n *\n * // Any one of these policies passing allows the operation\n * ```\n */\nexport function anyPolicy(...policies: PolicyEngine[]): PolicyEngine {\n if (policies.length === 0) {\n throw new Error('anyPolicy requires at least one policy');\n }\n\n if (policies.length === 1) {\n return policies[0]!;\n }\n\n return {\n async can(user: any, operation: string, context?: any): Promise<PolicyResult> {\n let firstDenial: PolicyResult | null = null;\n\n for (const policy of policies) {\n const result = await policy.can(user, operation, context);\n\n if (result.allowed) {\n // First success stops evaluation\n return result;\n }\n\n if (!firstDenial) {\n firstDenial = result;\n }\n }\n\n // All policies denied - return first denial\n return firstDenial!;\n },\n\n toMiddleware(operation: string) {\n return async (request: FastifyRequest, reply: FastifyReply) => {\n const results: PolicyResult[] = [];\n\n for (const policy of policies) {\n const result = await policy.can(\n request.user,\n operation,\n {\n document: request.document,\n body: request.body,\n params: request.params,\n query: request.query,\n }\n );\n\n if (result.allowed) {\n // First success - attach result and continue\n request.policyResult = result;\n\n if (result.filters) {\n request._policyFilters = result.filters;\n }\n\n if (result.fieldMask) {\n request.fieldMask = result.fieldMask;\n }\n\n if (result.metadata) {\n request.policyMetadata = result.metadata;\n }\n\n return;\n }\n\n results.push(result);\n }\n\n // All policies denied\n return reply.code(403).send({\n success: false,\n error: 'Access denied',\n message: results[0]?.reason || 'You do not have permission to perform this action',\n });\n };\n },\n };\n}\n\n/**\n * Create a pass-through policy that always allows\n *\n * Useful for testing or for routes that don't need authorization.\n *\n * @example\n * ```typescript\n * const policy = allowAll();\n * const result = await policy.can(user, 'any-operation');\n * // result.allowed === true\n * ```\n */\nexport function allowAll(): PolicyEngine {\n return {\n can() {\n return { allowed: true };\n },\n\n toMiddleware() {\n return async () => {\n // No-op - always allow\n };\n },\n };\n}\n\n/**\n * Create a policy that always denies\n *\n * Useful for explicitly blocking operations or for testing.\n *\n * @param reason - Denial reason\n *\n * @example\n * ```typescript\n * const policy = denyAll('This resource is deprecated');\n * const result = await policy.can(user, 'any-operation');\n * // result.allowed === false\n * // result.reason === 'This resource is deprecated'\n * ```\n */\nexport function denyAll(reason = 'Operation not allowed'): PolicyEngine {\n return {\n can() {\n return { allowed: false, reason };\n },\n\n toMiddleware() {\n return async (request: FastifyRequest, reply: FastifyReply) => {\n return reply.code(403).send({\n success: false,\n error: 'Access denied',\n message: reason,\n });\n };\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2XA,SAAgB,0BACd,SACiB;CAEjB,MAAM,+BAAe,IAAI,KAA0B;AACnD,MAAK,MAAM,aAAa,QAAQ,YAAY;EAC1C,MAAM,WAAW,aAAa,IAAI,UAAU,SAAS;AACrD,MAAI,SACF,MAAK,MAAM,UAAU,UAAU,OAC7B,UAAS,IAAI,OAAO;MAGtB,cAAa,IAAI,UAAU,UAAU,IAAI,IAAI,UAAU,OAAO,CAAC;;CAInE,MAAM,kBAAmC,OAAO,YAAY;EAC1D,MAAM,EAAE,MAAM,UAAU,WAAW;EAGnC,MAAM,iBAAiB,aAAa,IAAI,SAAS;AACjD,MAAI,CAAC,kBAAkB,CAAC,eAAe,IAAI,OAAO,CAChD,QAAO;GACL,SAAS;GACT,QAAQ,WAAW,OAAO,kCAAkC,SAAS;GACtE;AAIH,MAAI,QAAQ,iBAAiB;GAC3B,MAAM,SAAS,MAAM,MAAM,MAAM;AACjC,OAAI,CAAC,OACH,QAAO;IACL,SAAS;IACT,QAAQ;IACT;AAIH,OAAI,CADkB,MAAM,QAAQ,gBAAgB,OAAO,OAAO,EAAE,UAAU,OAAO,CAEnF,QAAO;IACL,SAAS;IACT,QAAQ,uBAAuB,OAAO,mBAAmB,SAAS;IACnE;;AAIL,SAAO,EAAE,SAAS,MAAM;;AAG1B,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC7YT,SAAgB,uBACd,QACA,WACiE;AACjE,QAAO,eAAe,iBACpB,SACA,OACe;EAEf,MAAM,UAAU;GACd,UAAU,QAAQ;GAClB,MAAM,QAAQ;GACd,QAAQ,QAAQ;GAChB,OAAO,QAAQ;GAChB;EAGD,MAAM,SAAS,MAAM,OAAO,IAAI,QAAQ,MAAM,WAAW,QAAQ;AAEjE,MAAI,CAAC,OAAO,QACV,QAAO,MAAM,KAAK,IAAI,CAAC,KAAK;GAC1B,SAAS;GACT,OAAO;GACP,SAAS,OAAO,UAAU;GAC3B,CAAC;AAIJ,UAAQ,eAAe;AAGvB,MAAI,OAAO,WAAW,OAAO,KAAK,OAAO,QAAQ,CAAC,SAAS,EACzD,SAAQ,iBAAiB,OAAO;AAIlC,MAAI,OAAO,UACT,SAAQ,YAAY,OAAO;AAI7B,MAAI,OAAO,SACT,SAAQ,iBAAiB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCtC,SAAgB,gBAAgB,GAAG,UAAwC;AACzE,KAAI,SAAS,WAAW,EACtB,OAAM,IAAI,MAAM,+CAA+C;AAGjE,KAAI,SAAS,WAAW,EACtB,QAAO,SAAS;AAGlB,QAAO;EACL,MAAM,IAAI,MAAW,WAAmB,SAAsC;GAC5E,MAAM,UAA0B,EAAE;AAElC,QAAK,MAAM,UAAU,UAAU;IAC7B,MAAM,SAAS,MAAM,OAAO,IAAI,MAAM,WAAW,QAAQ;AAEzD,QAAI,CAAC,OAAO,QAEV,QAAO;AAGT,YAAQ,KAAK,OAAO;;GAItB,MAAM,eAA6B;IACjC,SAAS;IACT,SAAS,EAAE;IACX,UAAU,EAAE;IACb;AAGD,QAAK,MAAM,UAAU,QACnB,KAAI,OAAO,QACT,QAAO,OAAO,aAAa,SAAU,OAAO,QAAQ;GAKxD,MAAM,8BAAc,IAAI,KAAa;GACrC,MAAM,cAA6B,EAAE;AAErC,QAAK,MAAM,UAAU,SAAS;AAC5B,QAAI,OAAO,WAAW,QACpB,QAAO,UAAU,QAAQ,SAAS,UAAU,YAAY,IAAI,MAAM,CAAC;AAErE,QAAI,OAAO,WAAW,QACpB,aAAY,KAAK,IAAI,IAAI,OAAO,UAAU,QAAQ,CAAC;;AAIvD,OAAI,YAAY,OAAO,KAAK,YAAY,SAAS,GAAG;AAClD,iBAAa,YAAY,EAAE;AAE3B,QAAI,YAAY,OAAO,EACrB,cAAa,UAAU,UAAU,MAAM,KAAK,YAAY;AAG1D,QAAI,YAAY,SAAS,GAAG;KAE1B,MAAM,eAAe,YAAY,QAAQ,KAAK,QAAQ;AACpD,aAAO,IAAI,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,MAAM,IAAI,IAAI,EAAE,CAAC,CAAC;OAClD;AACF,SAAI,aAAa,OAAO,EACtB,cAAa,UAAU,UAAU,MAAM,KAAK,aAAa;;;AAM/D,QAAK,MAAM,UAAU,QACnB,KAAI,OAAO,SACT,QAAO,OAAO,aAAa,UAAW,OAAO,SAAS;AAK1D,OAAI,OAAO,KAAK,aAAa,QAAS,CAAC,WAAW,EAChD,QAAO,aAAa;AAEtB,OAAI,OAAO,KAAK,aAAa,SAAU,CAAC,WAAW,EACjD,QAAO,aAAa;AAGtB,UAAO;;EAGT,aAAa,WAAmB;GAC9B,MAAM,cAAc,SAAS,KAAK,MAAM,EAAE,aAAa,UAAU,CAAC;AAElE,UAAO,OAAO,SAAyB,UAAwB;AAC7D,SAAK,MAAM,cAAc,aAAa;AACpC,WAAM,WAAW,SAAS,MAAM;AAGhC,SAAI,MAAM,KACR;;;;EAKT;;;;;;;;;;;;;;;;;;;;;;AAuBH,SAAgB,UAAU,GAAG,UAAwC;AACnE,KAAI,SAAS,WAAW,EACtB,OAAM,IAAI,MAAM,yCAAyC;AAG3D,KAAI,SAAS,WAAW,EACtB,QAAO,SAAS;AAGlB,QAAO;EACL,MAAM,IAAI,MAAW,WAAmB,SAAsC;GAC5E,IAAI,cAAmC;AAEvC,QAAK,MAAM,UAAU,UAAU;IAC7B,MAAM,SAAS,MAAM,OAAO,IAAI,MAAM,WAAW,QAAQ;AAEzD,QAAI,OAAO,QAET,QAAO;AAGT,QAAI,CAAC,YACH,eAAc;;AAKlB,UAAO;;EAGT,aAAa,WAAmB;AAC9B,UAAO,OAAO,SAAyB,UAAwB;IAC7D,MAAM,UAA0B,EAAE;AAElC,SAAK,MAAM,UAAU,UAAU;KAC7B,MAAM,SAAS,MAAM,OAAO,IAC1B,QAAQ,MACR,WACA;MACE,UAAU,QAAQ;MAClB,MAAM,QAAQ;MACd,QAAQ,QAAQ;MAChB,OAAO,QAAQ;MAChB,CACF;AAED,SAAI,OAAO,SAAS;AAElB,cAAQ,eAAe;AAEvB,UAAI,OAAO,QACT,SAAQ,iBAAiB,OAAO;AAGlC,UAAI,OAAO,UACT,SAAQ,YAAY,OAAO;AAG7B,UAAI,OAAO,SACT,SAAQ,iBAAiB,OAAO;AAGlC;;AAGF,aAAQ,KAAK,OAAO;;AAItB,WAAO,MAAM,KAAK,IAAI,CAAC,KAAK;KAC1B,SAAS;KACT,OAAO;KACP,SAAS,QAAQ,IAAI,UAAU;KAChC,CAAC;;;EAGP;;;;;;;;;;;;;;AAeH,SAAgB,WAAyB;AACvC,QAAO;EACL,MAAM;AACJ,UAAO,EAAE,SAAS,MAAM;;EAG1B,eAAe;AACb,UAAO,YAAY;;EAItB;;;;;;;;;;;;;;;;;AAkBH,SAAgB,QAAQ,SAAS,yBAAuC;AACtE,QAAO;EACL,MAAM;AACJ,UAAO;IAAE,SAAS;IAAO;IAAQ;;EAGnC,eAAe;AACb,UAAO,OAAO,SAAyB,UAAwB;AAC7D,WAAO,MAAM,KAAK,IAAI,CAAC,KAAK;KAC1B,SAAS;KACT,OAAO;KACP,SAAS;KACV,CAAC;;;EAGP"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/presets/softDelete.ts","../../src/presets/slugLookup.ts","../../src/presets/ownedByUser.ts","../../src/presets/tree.ts","../../src/presets/audited.ts","../../src/presets/types.ts","../../src/presets/index.ts"],"mappings":";;;;;;;iBAWgB,gBAAA,CAAA,GAAoB,YAAA;;;UCFnB,iBAAA;EACf,SAAA;AAAA;AAAA,iBAGc,gBAAA,CAAiB,OAAA,GAAS,iBAAA,GAAyB,YAAA;;;UCqBlD,kBAAA;EACf,UAAA;AAAA;AAAA,iBA4Bc,iBAAA,CAAkB,OAAA,GAAS,kBAAA,GAA0B,YAAA;;;UCtDpD,WAAA;EACf,WAAA;AAAA;AAAA,iBAGc,UAAA,CAAW,OAAA,GAAS,WAAA,GAAmB,YAAA;;;UCItC,oBAAA;EHRA;EGUf,cAAA;;EAEA,cAAA;AAAA;AHRF;;;AAAA,iBGcgB,aAAA,CAAc,OAAA,GAAS,oBAAA,GAA4B,YAAA;;;;;;AHdnE;;;;;;;;;;;;ACqBA;;;;;AA6BA;;;;;;;UGjBiB,qBAAA;EHiBgE;;;;EGZ/E,UAAA,CAAW,GAAA,EAAK,eAAA,GAAkB,OAAA,CAAQ,mBAAA,CAAoB,eAAA,CAAgB,IAAA;EF1CpD;;;;EEgD1B,OAAA,CAAQ,GAAA,EAAK,eAAA,GAAkB,OAAA,CAAQ,mBAAA,CAAoB,IAAA;AAAA;;;;;;;;;;;ADxC7D;;;;;AAUA;;;;;;;;;;UC0DiB,qBAAA;;AAvCjB;;;EA4CE,SAAA,CAAU,GAAA,EAAK,eAAA,GAAkB,OAAA,CAAQ,mBAAA,CAAoB,IAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;AAL/D;;UAmCiB,eAAA;EA9BA;;;;EAmCf,OAAA,CAAQ,GAAA,EAAK,eAAA,GAAkB,OAAA,CAAQ,mBAAA,CAAoB,IAAA;EAnCnB;;;;EAyCxC,WAAA,CAAY,GAAA,EAAK,eAAA,GAAkB,OAAA,CAAQ,mBAAA,CAAoB,IAAA;AAAA;;;;;AAXjE;;;;;;;;;;;;;;;;KAkCY,kBAAA;;;;;;;;;;;;AAAZ;;;;;AAuBA;;;;;KAAY,kBAAA;;;;;AAwCZ;;;;;;;;;;;;;;;;;KAjBY,cAAA;;;;;;;;;;;;;ACtJZ;;;KDuKY,iBAAA,0FAGR,QAAA,wBACA,qBAAA,CAAsB,IAAA,IACtB,QAAA,wBACE,qBAAA,CAAsB,IAAA,IACtB,QAAA,kBACE,eAAA,CAAgB,IAAA;;;;AH/LxB;;;cIgBa,yBAAA,GACX,OAAA,GAAS,IAAA,CACyD,kBAAA,qBADc,YAAA;AAAA,KAgC7E,aAAA,IAAiB,OAAA,GAAU,SAAA,KAAc,YAAA;;;;iBAc9B,SAAA,CAAU,YAAA;EAAyB,IAAA;EAAA,CAAe,GAAA;AAAA,IAA0B,YAAA;AHxF5F;;;AAAA,iBGsHgB,cAAA,CACd,IAAA,UACA,OAAA,EAAS,aAAA,EACT,OAAA;EAAY,QAAA;AAAA;;;;iBAaE,mBAAA,CAAA;AAAA,KAQX,WAAA,YAAuB,YAAA;EAAiB,IAAA;EAAA,CAAe,GAAA;AAAA;;;;AFtI5D;iBEqLgB,YAAA,QAAoB,SAAA,CAAA,CAClC,MAAA,EAAQ,cAAA,CAAe,IAAA,GACvB,OAAA,GAAS,WAAA,KACR,cAAA,CAAe,IAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/presets/index.ts"],"sourcesContent":["/**\n * Presets Module\n *\n * Reusable resource configurations that add routes, middlewares, and schema options.\n *\n * @example\n * import { defineResource } from '@classytic/arc';\n *\n * // Using preset strings (resolved internally)\n * defineResource({\n * presets: ['softDelete', 'slugLookup'],\n * });\n *\n * // Using preset functions with options\n * import { softDeletePreset, treePreset } from '@classytic/arc/presets';\n *\n * defineResource({\n * presets: [\n * softDeletePreset(),\n * treePreset({ parentField: 'parentSlug' }),\n * ],\n * });\n */\n\nimport type {\n AdditionalRoute,\n AnyRecord,\n MiddlewareConfig,\n PresetResult,\n ResourceConfig,\n RouteSchemaOptions,\n} from '../types/index.js';\n\n// Export preset functions\nexport { softDeletePreset } from './softDelete.js';\n\n\nexport { slugLookupPreset } from './slugLookup.js';\nexport type { SlugLookupOptions } from './slugLookup.js';\n\nexport { ownedByUserPreset } from './ownedByUser.js';\nexport type { OwnedByUserOptions } from './ownedByUser.js';\n\nexport { multiTenantPreset } from './multiTenant.js';\nexport type { MultiTenantOptions } from './multiTenant.js';\n\n/**\n * Convenience alias for multiTenantPreset with public list/get routes\n * Equivalent to: multiTenantPreset({ allowPublic: ['list', 'get'] })\n */\nexport const flexibleMultiTenantPreset = (\n options: Omit<import('./multiTenant.js').MultiTenantOptions, 'allowPublic'> = {}\n) => multiTenantPreset({ ...options, allowPublic: ['list', 'get'] });\n\nexport { treePreset } from './tree.js';\nexport type { TreeOptions } from './tree.js';\n\nexport { auditedPreset } from './audited.js';\nexport type { AuditedPresetOptions } from './audited.js';\n\n// Export preset type interfaces for type safety\nexport type {\n ISoftDeleteController,\n ISlugLookupController,\n ITreeController,\n IOwnedByUserPreset,\n IMultiTenantPreset,\n IAuditedPreset,\n IPresetController,\n} from './types.js';\n\n// Import preset implementations for sync resolution\nimport { softDeletePreset } from './softDelete.js';\nimport { slugLookupPreset } from './slugLookup.js';\nimport { ownedByUserPreset } from './ownedByUser.js';\nimport { multiTenantPreset } from './multiTenant.js';\nimport { treePreset } from './tree.js';\nimport { auditedPreset } from './audited.js';\n\n// ============================================================================\n// Preset Registry\n// ============================================================================\n\ntype PresetFactory = (options?: AnyRecord) => PresetResult;\n\nconst presetRegistry: Record<string, PresetFactory> = {\n softDelete: softDeletePreset,\n slugLookup: slugLookupPreset,\n ownedByUser: ownedByUserPreset,\n multiTenant: multiTenantPreset,\n tree: treePreset,\n audited: auditedPreset,\n};\n\n/**\n * Get preset by name with options\n */\nexport function getPreset(nameOrConfig: string | { name: string; [key: string]: unknown }): PresetResult {\n if (typeof nameOrConfig === 'object' && nameOrConfig.name) {\n const { name, ...options } = nameOrConfig;\n return resolvePreset(name, options);\n }\n\n return resolvePreset(nameOrConfig as string);\n}\n\n/**\n * Resolve preset by name\n */\nfunction resolvePreset(name: string, options: AnyRecord = {}): PresetResult {\n const factory = presetRegistry[name];\n\n if (!factory) {\n const available = Object.keys(presetRegistry).join(', ');\n throw new Error(\n `Unknown preset: '${name}'\\n` +\n `Available presets: ${available}\\n` +\n `Docs: https://github.com/classytic/arc#presets`\n );\n }\n\n return factory(options);\n}\n\n/**\n * Register a custom preset\n */\nexport function registerPreset(\n name: string,\n factory: PresetFactory,\n options?: { override?: boolean },\n): void {\n if (presetRegistry[name] && !options?.override) {\n throw new Error(\n `Preset '${name}' already exists. Pass { override: true } to replace.`,\n );\n }\n presetRegistry[name] = factory;\n}\n\n/**\n * Get all available preset names\n */\nexport function getAvailablePresets(): string[] {\n return Object.keys(presetRegistry);\n}\n\n// ============================================================================\n// Apply Presets\n// ============================================================================\n\ntype PresetInput = string | PresetResult | { name: string; [key: string]: unknown };\n\n// ============================================================================\n// Preset Conflict Detection\n// ============================================================================\n\ninterface PresetConflict {\n presets: [string, string];\n message: string;\n severity: 'error' | 'warning';\n}\n\n/**\n * Validate that preset combinations don't conflict.\n * Detects route collisions (same method + path from different presets).\n */\nfunction validatePresetCombination(presets: PresetResult[]): PresetConflict[] {\n const conflicts: PresetConflict[] = [];\n const routeMap = new Map<string, string>(); // \"METHOD /path\" -> preset name\n\n for (const preset of presets) {\n const name = preset.name ?? 'unknown';\n const routes: AdditionalRoute[] = typeof preset.additionalRoutes === 'function'\n ? preset.additionalRoutes({})\n : (preset.additionalRoutes ?? []);\n\n for (const route of routes) {\n const key = `${route.method} ${route.path}`;\n const existing = routeMap.get(key);\n if (existing) {\n conflicts.push({\n presets: [existing, name],\n message: `Both '${existing}' and '${name}' define route ${key}`,\n severity: 'error',\n });\n }\n routeMap.set(key, name);\n }\n }\n\n return conflicts;\n}\n\n/**\n * Apply presets to resource config.\n * Validates preset combinations for conflicts before merging.\n */\nexport function applyPresets<TDoc = AnyRecord>(\n config: ResourceConfig<TDoc>,\n presets: PresetInput[] = []\n): ResourceConfig<TDoc> {\n let result = { ...config };\n\n // Resolve all presets first for validation\n const resolved = presets.map(resolvePresetInput);\n\n // Validate combinations — fail-fast on route collisions\n const conflicts = validatePresetCombination(resolved);\n const errors = conflicts.filter((c) => c.severity === 'error');\n if (errors.length > 0) {\n throw new Error(\n `[Arc] Resource '${config.name}' preset conflicts:\\n` +\n errors.map((c) => ` - ${c.message}`).join('\\n'),\n );\n }\n\n for (const preset of resolved) {\n result = mergePreset(result, preset) as ResourceConfig<TDoc>;\n }\n\n return result;\n}\n\n/**\n * Resolve preset input to PresetResult\n */\nfunction resolvePresetInput(preset: PresetInput): PresetResult {\n // Check if already a fully-resolved PresetResult (has middlewares or additionalRoutes)\n // This allows custom presets to be passed directly without registry lookup\n if (typeof preset === 'object' && ('middlewares' in preset || 'additionalRoutes' in preset)) {\n return preset as PresetResult;\n }\n\n // Object with name and options (for registry lookup)\n if (typeof preset === 'object' && 'name' in preset) {\n const { name, ...options } = preset as { name: string; [key: string]: unknown };\n return resolvePreset(name, options);\n }\n\n // String preset name\n return resolvePreset(preset);\n}\n\n/** Extended config with internal fields */\ninterface ExtendedResourceConfig<TDoc = AnyRecord> extends ResourceConfig<TDoc> {\n _controllerOptions?: {\n slugField?: string;\n parentField?: string;\n [key: string]: unknown;\n };\n _hooks?: Array<{\n presetName: string;\n operation: 'create' | 'update' | 'delete' | 'read' | 'list';\n phase: 'before' | 'after';\n handler: (ctx: {\n resource: string;\n operation: string;\n phase: string;\n data?: AnyRecord;\n result?: AnyRecord | AnyRecord[];\n user?: { id: string; email: string; [key: string]: unknown };\n context?: { organizationId?: string | null; [key: string]: unknown };\n meta?: AnyRecord;\n }) => void | Promise<void> | AnyRecord | Promise<AnyRecord>;\n priority?: number;\n }>;\n}\n\n/**\n * Merge preset into config\n */\nfunction mergePreset<TDoc = AnyRecord>(\n config: ResourceConfig<TDoc>,\n preset: PresetResult\n): ResourceConfig<TDoc> {\n const result = { ...config } as ExtendedResourceConfig<TDoc>;\n\n // Merge additional routes\n if (preset.additionalRoutes) {\n const routes: AdditionalRoute[] = typeof preset.additionalRoutes === 'function'\n ? preset.additionalRoutes(config.permissions ?? {})\n : preset.additionalRoutes;\n\n result.additionalRoutes = [\n ...(result.additionalRoutes ?? []),\n ...routes,\n ];\n }\n\n // Merge middlewares\n if (preset.middlewares) {\n result.middlewares = result.middlewares ?? {};\n for (const [op, mws] of Object.entries(preset.middlewares)) {\n const key = op as keyof MiddlewareConfig;\n result.middlewares[key] = [\n ...(result.middlewares[key] ?? []),\n ...(mws ?? []),\n ];\n }\n }\n\n // Merge schema options (deep-merge fieldRules so presets accumulate rules)\n if (preset.schemaOptions) {\n result.schemaOptions = {\n ...result.schemaOptions,\n ...preset.schemaOptions,\n fieldRules: {\n ...result.schemaOptions?.fieldRules,\n ...preset.schemaOptions?.fieldRules,\n },\n } as RouteSchemaOptions;\n }\n\n // Merge controller options (slugField, parentField, etc.)\n if (preset.controllerOptions) {\n result._controllerOptions = {\n ...result._controllerOptions,\n ...preset.controllerOptions,\n };\n }\n\n // Collect hooks from preset\n if (preset.hooks && preset.hooks.length > 0) {\n result._hooks = result._hooks ?? [];\n for (const hook of preset.hooks) {\n result._hooks.push({\n presetName: preset.name,\n operation: hook.operation,\n phase: hook.phase,\n handler: hook.handler,\n priority: hook.priority,\n });\n }\n }\n\n return result;\n}\n"],"mappings":";;;;;;;;AAkDA,MAAa,6BACX,UAA8E,EAAE,KAC7E,kBAAkB;CAAE,GAAG;CAAS,aAAa,CAAC,QAAQ,MAAM;CAAE,CAAC;AAiCpE,MAAM,iBAAgD;CACpD,YAAY;CACZ,YAAY;CACZ,aAAa;CACb,aAAa;CACb,MAAM;CACN,SAAS;CACV;;;;AAKD,SAAgB,UAAU,cAA+E;AACvG,KAAI,OAAO,iBAAiB,YAAY,aAAa,MAAM;EACzD,MAAM,EAAE,MAAM,GAAG,YAAY;AAC7B,SAAO,cAAc,MAAM,QAAQ;;AAGrC,QAAO,cAAc,aAAuB;;;;;AAM9C,SAAS,cAAc,MAAc,UAAqB,EAAE,EAAgB;CAC1E,MAAM,UAAU,eAAe;AAE/B,KAAI,CAAC,SAAS;EACZ,MAAM,YAAY,OAAO,KAAK,eAAe,CAAC,KAAK,KAAK;AACxD,QAAM,IAAI,MACR,oBAAoB,KAAK,wBACH,UAAU,kDAEjC;;AAGH,QAAO,QAAQ,QAAQ;;;;;AAMzB,SAAgB,eACd,MACA,SACA,SACM;AACN,KAAI,eAAe,SAAS,CAAC,SAAS,SACpC,OAAM,IAAI,MACR,WAAW,KAAK,uDACjB;AAEH,gBAAe,QAAQ;;;;;AAMzB,SAAgB,sBAAgC;AAC9C,QAAO,OAAO,KAAK,eAAe;;;;;;AAuBpC,SAAS,0BAA0B,SAA2C;CAC5E,MAAM,YAA8B,EAAE;CACtC,MAAM,2BAAW,IAAI,KAAqB;AAE1C,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,OAAO,OAAO,QAAQ;EAC5B,MAAM,SAA4B,OAAO,OAAO,qBAAqB,aACjE,OAAO,iBAAiB,EAAE,CAAC,GAC1B,OAAO,oBAAoB,EAAE;AAElC,OAAK,MAAM,SAAS,QAAQ;GAC1B,MAAM,MAAM,GAAG,MAAM,OAAO,GAAG,MAAM;GACrC,MAAM,WAAW,SAAS,IAAI,IAAI;AAClC,OAAI,SACF,WAAU,KAAK;IACb,SAAS,CAAC,UAAU,KAAK;IACzB,SAAS,SAAS,SAAS,SAAS,KAAK,iBAAiB;IAC1D,UAAU;IACX,CAAC;AAEJ,YAAS,IAAI,KAAK,KAAK;;;AAI3B,QAAO;;;;;;AAOT,SAAgB,aACd,QACA,UAAyB,EAAE,EACL;CACtB,IAAI,SAAS,EAAE,GAAG,QAAQ;CAG1B,MAAM,WAAW,QAAQ,IAAI,mBAAmB;CAIhD,MAAM,SADY,0BAA0B,SAAS,CAC5B,QAAQ,MAAM,EAAE,aAAa,QAAQ;AAC9D,KAAI,OAAO,SAAS,EAClB,OAAM,IAAI,MACR,mBAAmB,OAAO,KAAK,yBAC/B,OAAO,KAAK,MAAM,OAAO,EAAE,UAAU,CAAC,KAAK,KAAK,CACjD;AAGH,MAAK,MAAM,UAAU,SACnB,UAAS,YAAY,QAAQ,OAAO;AAGtC,QAAO;;;;;AAMT,SAAS,mBAAmB,QAAmC;AAG7D,KAAI,OAAO,WAAW,aAAa,iBAAiB,UAAU,sBAAsB,QAClF,QAAO;AAIT,KAAI,OAAO,WAAW,YAAY,UAAU,QAAQ;EAClD,MAAM,EAAE,MAAM,GAAG,YAAY;AAC7B,SAAO,cAAc,MAAM,QAAQ;;AAIrC,QAAO,cAAc,OAAO;;;;;AA+B9B,SAAS,YACP,QACA,QACsB;CACtB,MAAM,SAAS,EAAE,GAAG,QAAQ;AAG5B,KAAI,OAAO,kBAAkB;EAC3B,MAAM,SAA4B,OAAO,OAAO,qBAAqB,aACjE,OAAO,iBAAiB,OAAO,eAAe,EAAE,CAAC,GACjD,OAAO;AAEX,SAAO,mBAAmB,CACxB,GAAI,OAAO,oBAAoB,EAAE,EACjC,GAAG,OACJ;;AAIH,KAAI,OAAO,aAAa;AACtB,SAAO,cAAc,OAAO,eAAe,EAAE;AAC7C,OAAK,MAAM,CAAC,IAAI,QAAQ,OAAO,QAAQ,OAAO,YAAY,EAAE;GAC1D,MAAM,MAAM;AACZ,UAAO,YAAY,OAAO,CACxB,GAAI,OAAO,YAAY,QAAQ,EAAE,EACjC,GAAI,OAAO,EAAE,CACd;;;AAKL,KAAI,OAAO,cACT,QAAO,gBAAgB;EACrB,GAAG,OAAO;EACV,GAAG,OAAO;EACV,YAAY;GACV,GAAG,OAAO,eAAe;GACzB,GAAG,OAAO,eAAe;GAC1B;EACF;AAIH,KAAI,OAAO,kBACT,QAAO,qBAAqB;EAC1B,GAAG,OAAO;EACV,GAAG,OAAO;EACX;AAIH,KAAI,OAAO,SAAS,OAAO,MAAM,SAAS,GAAG;AAC3C,SAAO,SAAS,OAAO,UAAU,EAAE;AACnC,OAAK,MAAM,QAAQ,OAAO,MACxB,QAAO,OAAO,KAAK;GACjB,YAAY,OAAO;GACnB,WAAW,KAAK;GAChB,OAAO,KAAK;GACZ,SAAS,KAAK;GACd,UAAU,KAAK;GAChB,CAAC;;AAIN,QAAO"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"multiTenant.d.mts","names":[],"sources":["../../src/presets/multiTenant.ts"],"mappings":";;;;;;UAmBiB,kBAAA;EAAkB;EAEjC,WAAA;EAY0B;;;;;;AA6H5B;;;;EA7HE,WAAA,GAAc,YAAA;AAAA;AAAA,iBA6HA,iBAAA,CAAkB,OAAA,GAAS,kBAAA,GAA0B,YAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"multiTenant.mjs","names":[],"sources":["../../src/presets/multiTenant.ts"],"sourcesContent":["/**\n * Multi-Tenant Preset\n *\n * Adds tenant (organization) filtering and injection middlewares.\n */\n\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport type {\n AnyRecord,\n CrudRouteKey,\n MiddlewareConfig,\n PresetResult,\n RequestWithExtras,\n RouteHandler,\n} from '../types/index.js';\nimport { DEFAULT_TENANT_FIELD } from '../constants.js';\nimport type { RequestScope } from '../scope/types.js';\nimport { isMember, isElevated, getOrgId, PUBLIC_SCOPE } from '../scope/types.js';\n\nexport interface MultiTenantOptions {\n /** Field name in database (default: 'organizationId') */\n tenantField?: string;\n\n /**\n * Routes that allow public access (no auth required)\n * When a route is in this array:\n * - If no org context: allow through without filtering (public data)\n * - If org context present: require auth and apply filter\n *\n * @default [] (strict mode - all routes require auth)\n * @example\n * multiTenantPreset({ allowPublic: ['list', 'get'] })\n */\n allowPublic?: CrudRouteKey[];\n}\n\n/** Read request.scope safely */\nfunction getScope(request: FastifyRequest): RequestScope {\n return request.scope ?? PUBLIC_SCOPE;\n}\n\n/**\n * Create tenant filter middleware\n * Adds tenant filter to query for list/get operations.\n * Reads `request.scope` for org context and elevation bypass.\n */\nfunction createTenantFilter(tenantField: string): RouteHandler {\n return async (request: RequestWithExtras, reply: FastifyReply): Promise<void> => {\n const scope = getScope(request);\n\n // Elevated without org → no filter (admin viewing all)\n // Elevated with org → filter by that org\n if (isElevated(scope)) {\n const orgId = getOrgId(scope);\n if (orgId) {\n request._policyFilters = {\n ...(request._policyFilters ?? {}),\n [tenantField]: orgId,\n };\n }\n return;\n }\n\n // Member → filter by scope.organizationId\n if (isMember(scope)) {\n request._policyFilters = {\n ...(request._policyFilters ?? {}),\n [tenantField]: scope.organizationId,\n };\n return;\n }\n\n // authenticated / public → 403 (multi-tenant requires org context)\n if (scope.kind === 'public') {\n reply.code(401).send({\n success: false,\n error: 'Unauthorized',\n message: 'Authentication required for multi-tenant resources',\n });\n return;\n }\n\n // authenticated but no org → 403\n reply.code(403).send({\n success: false,\n error: 'Forbidden',\n message: 'Organization context required for this operation',\n });\n };\n}\n\n/**\n * Create flexible tenant filter middleware\n * For routes in allowPublic: only filter when org context is present\n * No org context = allow through (public data)\n * Org context present = require auth and apply filter\n */\nfunction createFlexibleTenantFilter(tenantField: string): RouteHandler {\n return async (request: RequestWithExtras, reply: FastifyReply): Promise<void> => {\n const scope = getScope(request);\n\n // Elevated without org → no filter (admin viewing all)\n if (isElevated(scope)) {\n const orgId = getOrgId(scope);\n if (orgId) {\n request._policyFilters = {\n ...(request._policyFilters ?? {}),\n [tenantField]: orgId,\n };\n }\n return;\n }\n\n // Member → filter by scope.organizationId\n if (isMember(scope)) {\n request._policyFilters = {\n ...(request._policyFilters ?? {}),\n [tenantField]: scope.organizationId,\n };\n return;\n }\n\n // authenticated / public with no org context → allow through (public data)\n return;\n };\n}\n\n/**\n * Create tenant injection middleware\n * Injects tenant ID into request body on create.\n * Reads `request.scope` for org context.\n */\nfunction createTenantInjection(tenantField: string): RouteHandler {\n return async (request: RequestWithExtras, reply: FastifyReply): Promise<void> => {\n const scope = getScope(request);\n const orgId = getOrgId(scope);\n\n // Elevated without org → skip injection (admin cross-org operation)\n if (isElevated(scope) && !orgId) {\n return;\n }\n\n // Fail-closed: Require orgId to prevent orphaned data\n if (!orgId) {\n reply.code(403).send({\n success: false,\n error: 'Forbidden',\n message: 'Organization context required to create resources',\n });\n return;\n }\n\n if (request.body) {\n (request.body as AnyRecord)[tenantField] = orgId;\n }\n };\n}\n\nexport function multiTenantPreset(options: MultiTenantOptions = {}): PresetResult {\n const {\n tenantField = DEFAULT_TENANT_FIELD,\n allowPublic = [],\n } = options;\n\n // Create middleware variants\n const strictTenantFilter = createTenantFilter(tenantField);\n const flexibleTenantFilter = createFlexibleTenantFilter(tenantField);\n const tenantInjection = createTenantInjection(tenantField);\n\n // Helper to select appropriate filter based on allowPublic\n const getFilter = (route: CrudRouteKey): RouteHandler =>\n allowPublic.includes(route) ? flexibleTenantFilter : strictTenantFilter;\n\n return {\n name: 'multiTenant',\n middlewares: {\n list: [getFilter('list')],\n get: [getFilter('get')],\n create: [tenantInjection],\n update: [getFilter('update')],\n delete: [getFilter('delete')],\n } as MiddlewareConfig,\n };\n}\n\nexport default multiTenantPreset;\n"],"mappings":";;;;;AAqCA,SAAS,SAAS,SAAuC;AACvD,QAAO,QAAQ,SAAS;;;;;;;AAQ1B,SAAS,mBAAmB,aAAmC;AAC7D,QAAO,OAAO,SAA4B,UAAuC;EAC/E,MAAM,QAAQ,SAAS,QAAQ;AAI/B,MAAI,WAAW,MAAM,EAAE;GACrB,MAAM,QAAQ,SAAS,MAAM;AAC7B,OAAI,MACF,SAAQ,iBAAiB;IACvB,GAAI,QAAQ,kBAAkB,EAAE;KAC/B,cAAc;IAChB;AAEH;;AAIF,MAAI,SAAS,MAAM,EAAE;AACnB,WAAQ,iBAAiB;IACvB,GAAI,QAAQ,kBAAkB,EAAE;KAC/B,cAAc,MAAM;IACtB;AACD;;AAIF,MAAI,MAAM,SAAS,UAAU;AAC3B,SAAM,KAAK,IAAI,CAAC,KAAK;IACnB,SAAS;IACT,OAAO;IACP,SAAS;IACV,CAAC;AACF;;AAIF,QAAM,KAAK,IAAI,CAAC,KAAK;GACnB,SAAS;GACT,OAAO;GACP,SAAS;GACV,CAAC;;;;;;;;;AAUN,SAAS,2BAA2B,aAAmC;AACrE,QAAO,OAAO,SAA4B,UAAuC;EAC/E,MAAM,QAAQ,SAAS,QAAQ;AAG/B,MAAI,WAAW,MAAM,EAAE;GACrB,MAAM,QAAQ,SAAS,MAAM;AAC7B,OAAI,MACF,SAAQ,iBAAiB;IACvB,GAAI,QAAQ,kBAAkB,EAAE;KAC/B,cAAc;IAChB;AAEH;;AAIF,MAAI,SAAS,MAAM,EAAE;AACnB,WAAQ,iBAAiB;IACvB,GAAI,QAAQ,kBAAkB,EAAE;KAC/B,cAAc,MAAM;IACtB;AACD;;;;;;;;;AAaN,SAAS,sBAAsB,aAAmC;AAChE,QAAO,OAAO,SAA4B,UAAuC;EAC/E,MAAM,QAAQ,SAAS,QAAQ;EAC/B,MAAM,QAAQ,SAAS,MAAM;AAG7B,MAAI,WAAW,MAAM,IAAI,CAAC,MACxB;AAIF,MAAI,CAAC,OAAO;AACV,SAAM,KAAK,IAAI,CAAC,KAAK;IACnB,SAAS;IACT,OAAO;IACP,SAAS;IACV,CAAC;AACF;;AAGF,MAAI,QAAQ,KACV,CAAC,QAAQ,KAAmB,eAAe;;;AAKjD,SAAgB,kBAAkB,UAA8B,EAAE,EAAgB;CAChF,MAAM,EACJ,cAAc,sBACd,cAAc,EAAE,KACd;CAGJ,MAAM,qBAAqB,mBAAmB,YAAY;CAC1D,MAAM,uBAAuB,2BAA2B,YAAY;CACpE,MAAM,kBAAkB,sBAAsB,YAAY;CAG1D,MAAM,aAAa,UACjB,YAAY,SAAS,MAAM,GAAG,uBAAuB;AAEvD,QAAO;EACL,MAAM;EACN,aAAa;GACX,MAAM,CAAC,UAAU,OAAO,CAAC;GACzB,KAAK,CAAC,UAAU,MAAM,CAAC;GACvB,QAAQ,CAAC,gBAAgB;GACzB,QAAQ,CAAC,UAAU,SAAS,CAAC;GAC7B,QAAQ,CAAC,UAAU,SAAS,CAAC;GAC9B;EACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"presets-BITljm96.mjs","names":[],"sources":["../src/permissions/presets.ts"],"sourcesContent":["/**\n * Permission Presets — Common permission patterns in one call.\n *\n * Reduces 5 lines of permission declarations to 1.\n * Each preset returns a ResourcePermissions object that can be\n * spread or overridden per-operation.\n *\n * @example\n * ```typescript\n * import { permissions } from '@classytic/arc';\n *\n * // Public read, authenticated write\n * defineResource({ name: 'product', permissions: permissions.publicRead() });\n *\n * // Override specific operations\n * defineResource({\n * name: 'product',\n * permissions: permissions.publicRead({ delete: requireRoles(['superadmin']) }),\n * });\n * ```\n */\n\nimport type { PermissionCheck } from \"./types.js\";\nimport {\n allowPublic,\n requireAuth,\n requireRoles,\n requireOwnership,\n anyOf,\n} from \"./index.js\";\n\n/**\n * ResourcePermissions shape — matches the type in types/index.ts\n */\ninterface ResourcePermissions<TDoc = any> {\n list?: PermissionCheck<TDoc>;\n get?: PermissionCheck<TDoc>;\n create?: PermissionCheck<TDoc>;\n update?: PermissionCheck<TDoc>;\n delete?: PermissionCheck<TDoc>;\n}\n\ntype PermissionOverrides<TDoc = any> = Partial<ResourcePermissions<TDoc>>;\n\n/**\n * Merge a base preset with user overrides.\n * Overrides replace individual operations — undefined values don't clear them.\n */\nfunction withOverrides<TDoc = any>(\n base: ResourcePermissions<TDoc>,\n overrides?: PermissionOverrides<TDoc>,\n): ResourcePermissions<TDoc> {\n if (!overrides) return base;\n const filtered = Object.fromEntries(\n Object.entries(overrides).filter(([, v]) => v !== undefined),\n );\n return { ...base, ...filtered };\n}\n\n/**\n * Public read, authenticated write.\n * list + get = allowPublic(), create + update + delete = requireAuth()\n */\nexport function publicRead<TDoc = any>(\n overrides?: PermissionOverrides<TDoc>,\n): ResourcePermissions<TDoc> {\n return withOverrides(\n {\n list: allowPublic(),\n get: allowPublic(),\n create: requireAuth(),\n update: requireAuth(),\n delete: requireAuth(),\n },\n overrides,\n );\n}\n\n/**\n * Public read, admin write.\n * list + get = allowPublic(), create + update + delete = requireRoles(['admin'])\n */\nexport function publicReadAdminWrite<TDoc = any>(\n roles: readonly string[] = [\"admin\"],\n overrides?: PermissionOverrides<TDoc>,\n): ResourcePermissions<TDoc> {\n return withOverrides(\n {\n list: allowPublic(),\n get: allowPublic(),\n create: requireRoles(roles),\n update: requireRoles(roles),\n delete: requireRoles(roles),\n },\n overrides,\n );\n}\n\n/**\n * All operations require authentication.\n */\nexport function authenticated<TDoc = any>(\n overrides?: PermissionOverrides<TDoc>,\n): ResourcePermissions<TDoc> {\n return withOverrides(\n {\n list: requireAuth(),\n get: requireAuth(),\n create: requireAuth(),\n update: requireAuth(),\n delete: requireAuth(),\n },\n overrides,\n );\n}\n\n/**\n * All operations require specific roles.\n * @param roles - Required roles (user needs at least one). Default: ['admin']\n */\nexport function adminOnly<TDoc = any>(\n roles: readonly string[] = [\"admin\"],\n overrides?: PermissionOverrides<TDoc>,\n): ResourcePermissions<TDoc> {\n return withOverrides(\n {\n list: requireRoles(roles),\n get: requireRoles(roles),\n create: requireRoles(roles),\n update: requireRoles(roles),\n delete: requireRoles(roles),\n },\n overrides,\n );\n}\n\n/**\n * Owner-scoped with admin bypass.\n * list = auth (scoped to owner), get = auth, create = auth,\n * update + delete = ownership check with admin bypass.\n *\n * @param ownerField - Field containing owner ID (default: 'userId')\n * @param bypassRoles - Roles that bypass ownership check (default: ['admin'])\n */\nexport function ownerWithAdminBypass<TDoc = any>(\n ownerField: Extract<keyof TDoc, string> | string = \"userId\",\n bypassRoles: readonly string[] = [\"admin\"],\n overrides?: PermissionOverrides<TDoc>,\n): ResourcePermissions<TDoc> {\n return withOverrides(\n {\n list: requireAuth(),\n get: requireAuth(),\n create: requireAuth(),\n update: anyOf(requireRoles(bypassRoles), requireOwnership(ownerField)),\n delete: anyOf(requireRoles(bypassRoles), requireOwnership(ownerField)),\n },\n overrides,\n );\n}\n\n/**\n * Full public access — no auth required for any operation.\n * Use sparingly (dev/testing, truly public APIs).\n */\nexport function fullPublic<TDoc = any>(\n overrides?: PermissionOverrides<TDoc>,\n): ResourcePermissions<TDoc> {\n return withOverrides(\n {\n list: allowPublic(),\n get: allowPublic(),\n create: allowPublic(),\n update: allowPublic(),\n delete: allowPublic(),\n },\n overrides,\n );\n}\n\n/**\n * Read-only: list + get authenticated, write operations denied.\n * Useful for computed/derived resources.\n */\nexport function readOnly<TDoc = any>(\n overrides?: PermissionOverrides<TDoc>,\n): ResourcePermissions<TDoc> {\n return withOverrides(\n {\n list: requireAuth(),\n get: requireAuth(),\n },\n overrides,\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgDA,SAAS,cACP,MACA,WAC2B;AAC3B,KAAI,CAAC,UAAW,QAAO;CACvB,MAAM,WAAW,OAAO,YACtB,OAAO,QAAQ,UAAU,CAAC,QAAQ,GAAG,OAAO,MAAM,OAAU,CAC7D;AACD,QAAO;EAAE,GAAG;EAAM,GAAG;EAAU;;;;;;AAOjC,SAAgB,WACd,WAC2B;AAC3B,QAAO,cACL;EACE,MAAM,aAAa;EACnB,KAAK,aAAa;EAClB,QAAQ,aAAa;EACrB,QAAQ,aAAa;EACrB,QAAQ,aAAa;EACtB,EACD,UACD;;;;;;AAOH,SAAgB,qBACd,QAA2B,CAAC,QAAQ,EACpC,WAC2B;AAC3B,QAAO,cACL;EACE,MAAM,aAAa;EACnB,KAAK,aAAa;EAClB,QAAQ,aAAa,MAAM;EAC3B,QAAQ,aAAa,MAAM;EAC3B,QAAQ,aAAa,MAAM;EAC5B,EACD,UACD;;;;;AAMH,SAAgB,cACd,WAC2B;AAC3B,QAAO,cACL;EACE,MAAM,aAAa;EACnB,KAAK,aAAa;EAClB,QAAQ,aAAa;EACrB,QAAQ,aAAa;EACrB,QAAQ,aAAa;EACtB,EACD,UACD;;;;;;AAOH,SAAgB,UACd,QAA2B,CAAC,QAAQ,EACpC,WAC2B;AAC3B,QAAO,cACL;EACE,MAAM,aAAa,MAAM;EACzB,KAAK,aAAa,MAAM;EACxB,QAAQ,aAAa,MAAM;EAC3B,QAAQ,aAAa,MAAM;EAC3B,QAAQ,aAAa,MAAM;EAC5B,EACD,UACD;;;;;;;;;;AAWH,SAAgB,qBACd,aAAmD,UACnD,cAAiC,CAAC,QAAQ,EAC1C,WAC2B;AAC3B,QAAO,cACL;EACE,MAAM,aAAa;EACnB,KAAK,aAAa;EAClB,QAAQ,aAAa;EACrB,QAAQ,MAAM,aAAa,YAAY,EAAE,iBAAiB,WAAW,CAAC;EACtE,QAAQ,MAAM,aAAa,YAAY,EAAE,iBAAiB,WAAW,CAAC;EACvE,EACD,UACD;;;;;;AAOH,SAAgB,WACd,WAC2B;AAC3B,QAAO,cACL;EACE,MAAM,aAAa;EACnB,KAAK,aAAa;EAClB,QAAQ,aAAa;EACrB,QAAQ,aAAa;EACrB,QAAQ,aAAa;EACtB,EACD,UACD;;;;;;AAOH,SAAgB,SACd,WAC2B;AAC3B,QAAO,cACL;EACE,MAAM,aAAa;EACnB,KAAK,aAAa;EACnB,EACD,UACD"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"presets-DzSMwlKj.d.mts","names":[],"sources":["../src/permissions/presets.ts"],"mappings":";;;;;;;;;UAkCU,mBAAA;EACR,IAAA,GAAO,eAAA,CAAgB,IAAA;EACvB,GAAA,GAAM,eAAA,CAAgB,IAAA;EACtB,MAAA,GAAS,eAAA,CAAgB,IAAA;EACzB,MAAA,GAAS,eAAA,CAAgB,IAAA;EACzB,MAAA,GAAS,eAAA,CAAgB,IAAA;AAAA;AAAA,KAGtB,mBAAA,eAAkC,OAAA,CAAQ,mBAAA,CAAoB,IAAA;;;;;iBAqBnD,UAAA,YAAA,CACd,SAAA,GAAY,mBAAA,CAAoB,IAAA,IAC/B,mBAAA,CAAoB,IAAA;;;;;iBAiBP,oBAAA,YAAA,CACd,KAAA,sBACA,SAAA,GAAY,mBAAA,CAAoB,IAAA,IAC/B,mBAAA,CAAoB,IAAA;;;;iBAgBP,aAAA,YAAA,CACd,SAAA,GAAY,mBAAA,CAAoB,IAAA,IAC/B,mBAAA,CAAoB,IAAA;;;;;iBAiBP,SAAA,YAAA,CACd,KAAA,sBACA,SAAA,GAAY,mBAAA,CAAoB,IAAA,IAC/B,mBAAA,CAAoB,IAAA;;;;;;;;;iBAqBP,oBAAA,YAAA,CACd,UAAA,GAAY,OAAA,OAAc,IAAA,oBAC1B,WAAA,sBACA,SAAA,GAAY,mBAAA,CAAoB,IAAA,IAC/B,mBAAA,CAAoB,IAAA;;;;;iBAiBP,UAAA,YAAA,CACd,SAAA,GAAY,mBAAA,CAAoB,IAAA,IAC/B,mBAAA,CAAoB,IAAA;;;;;iBAiBP,QAAA,YAAA,CACd,SAAA,GAAY,mBAAA,CAAoB,IAAA,IAC/B,mBAAA,CAAoB,IAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"prisma-DJbMt3yf.mjs","names":[],"sources":["../src/adapters/types.ts","../src/adapters/mongoose.ts","../src/adapters/prisma.ts"],"sourcesContent":["/**\r\n * Type Utilities for Adapters\r\n *\r\n * Type-safe helpers for working with database adapters.\r\n * Eliminates the need for 'as any' casts in application code.\r\n */\r\n\r\nimport type { Model, Document } from 'mongoose';\r\nimport type { CrudRepository } from '../types/index.js';\r\n\r\n// ============================================================================\r\n// Type Inference Helpers\r\n// ============================================================================\r\n\r\n/**\r\n * Infer document type from Mongoose model\r\n *\r\n * @example\r\n * const ProductModel = mongoose.model('Product', productSchema);\r\n * type ProductDoc = InferMongooseDoc<typeof ProductModel>;\r\n * // Result: ProductDocument with all fields typed\r\n */\r\nexport type InferMongooseDoc<M> = M extends Model<infer D> ? D : never;\r\n\r\n/**\r\n * Infer document type from repository\r\n *\r\n * @example\r\n * const productRepo = new ProductRepository();\r\n * type ProductDoc = InferRepoDoc<typeof productRepo>;\r\n */\r\nexport type InferRepoDoc<R> = R extends CrudRepository<infer D> ? D : never;\r\n\r\n/**\r\n * Infer document type from data adapter\r\n *\r\n * @example\r\n * const adapter = createMongooseAdapter({ model, repository });\r\n * type Doc = InferAdapterDoc<typeof adapter>;\r\n */\r\nexport type InferAdapterDoc<A> = A extends { repository: CrudRepository<infer D> } ? D : never;\r\n\r\n/**\r\n * Extract clean document type (removes Mongoose-specific fields)\r\n *\r\n * @example\r\n * type CleanProduct = CleanDoc<ProductDocument>;\r\n * // Result: Product without _id, __v, save(), etc.\r\n */\r\nexport type CleanDoc<T> = T extends Document\r\n ? Omit<T, keyof Document | '_id' | '__v' | '$__' | '$isNew' | 'save' | 'remove'>\r\n : T;\r\n\r\n// ============================================================================\r\n// Adapter Constraint Types\r\n// ============================================================================\r\n\r\n/**\r\n * Ensures type is a valid Mongoose document\r\n */\r\nexport type MongooseDocument = Document & Record<string, unknown>;\r\n\r\n/**\r\n * Ensures type is a valid repository\r\n */\r\nexport type ValidRepository<TDoc> = CrudRepository<TDoc> & {\r\n getAll: CrudRepository<TDoc>['getAll'];\r\n getById: CrudRepository<TDoc>['getById'];\r\n create: CrudRepository<TDoc>['create'];\r\n update: CrudRepository<TDoc>['update'];\r\n delete: CrudRepository<TDoc>['delete'];\r\n};\r\n\r\n/**\r\n * Ensures model matches repository document type\r\n */\r\nexport type MatchingModel<TDoc> = Model<TDoc & Document>;\r\n\r\n// ============================================================================\r\n// Type Guards\r\n// ============================================================================\r\n\r\n/**\r\n * Check if value is a Mongoose model\r\n */\r\nexport function isMongooseModel(value: unknown): value is Model<Document> {\r\n return (\r\n typeof value === 'function' &&\r\n value.prototype &&\r\n 'modelName' in value &&\r\n 'schema' in value\r\n );\r\n}\r\n\r\n/**\r\n * Check if value is a repository\r\n */\r\nexport function isRepository(value: unknown): value is CrudRepository<unknown> {\r\n return (\r\n typeof value === 'object' &&\r\n value !== null &&\r\n 'getAll' in value &&\r\n 'getById' in value &&\r\n 'create' in value &&\r\n 'update' in value &&\r\n 'delete' in value\r\n );\r\n}\r\n\r\n// Types are already exported at declaration\r\n// Functions (isMongooseModel, isRepository) are already exported at declaration\r\n","/**\r\n * Mongoose Adapter - Type-Safe Database Adapter\r\n *\r\n * Bridges Mongoose models with Arc's resource system.\r\n * Proper generics eliminate the need for 'as any' casts.\r\n */\r\n\r\nimport type { Model } from 'mongoose';\r\nimport type { DataAdapter, SchemaMetadata, RepositoryLike } from './interface.js';\r\nimport type { CrudRepository, RouteSchemaOptions, OpenApiSchemas, AnyRecord } from '../types/index.js';\r\nimport { SYSTEM_FIELDS } from '../constants.js';\r\nimport { isMongooseModel, isRepository } from './types.js';\r\n\r\n/**\r\n * Mongoose SchemaType internal shape (not fully exposed by @types/mongoose)\r\n * Used to extract field metadata from schema paths\r\n */\r\ninterface MongooseSchemaType {\r\n instance: string;\r\n isRequired?: boolean;\r\n options?: {\r\n ref?: string;\r\n enum?: Array<string | number>;\r\n minlength?: number;\r\n maxlength?: number;\r\n min?: number;\r\n max?: number;\r\n [key: string]: unknown;\r\n };\r\n [key: string]: unknown;\r\n}\r\n\r\n// ============================================================================\r\n// Mongoose Adapter Options\r\n// ============================================================================\r\n\r\n/**\r\n * Options for creating a Mongoose adapter\r\n *\r\n * @typeParam TDoc - The document type (inferred or explicit)\r\n */\r\n/**\r\n * Options for creating a Mongoose adapter\r\n *\r\n * @typeParam TDoc - The document type (inferred or explicit)\r\n */\r\nexport interface MongooseAdapterOptions<TDoc = unknown> {\r\n /** Mongoose model instance — preserves document type for type safety */\r\n model: Model<TDoc>;\r\n /** Repository implementing CRUD operations - accepts any repository-like object */\r\n repository: CrudRepository<TDoc> | RepositoryLike;\r\n /**\r\n * External schema generator plugin for OpenAPI docs.\r\n * When provided, replaces the built-in basic type conversion.\r\n * Receives the Mongoose model and schema options, must return OpenApiSchemas.\r\n *\r\n * @example MongoKit integration\r\n * ```typescript\r\n * import { buildCrudSchemasFromModel } from '@classytic/mongokit';\r\n *\r\n * createMongooseAdapter({\r\n * model: JobModel,\r\n * repository: jobRepository,\r\n * schemaGenerator: (model, options) => buildCrudSchemasFromModel(model, options),\r\n * });\r\n * ```\r\n */\r\n schemaGenerator?: (model: Model<TDoc>, options?: RouteSchemaOptions) => OpenApiSchemas;\r\n}\r\n\r\n// ============================================================================\r\n// Mongoose Adapter\r\n// ============================================================================\r\n\r\n/**\r\n * Mongoose data adapter with proper type safety\r\n *\r\n * @typeParam TDoc - The document type\r\n */\r\nexport class MongooseAdapter<TDoc = unknown> implements DataAdapter<TDoc> {\r\n readonly type = 'mongoose' as const;\r\n readonly name: string;\r\n readonly model: Model<TDoc>;\r\n readonly repository: CrudRepository<TDoc> | RepositoryLike;\r\n private readonly schemaGenerator?: (model: Model<TDoc>, options?: RouteSchemaOptions) => OpenApiSchemas;\r\n\r\n constructor(options: MongooseAdapterOptions<TDoc>) {\r\n // Runtime validation\r\n if (!isMongooseModel(options.model)) {\r\n throw new TypeError(\r\n 'MongooseAdapter: Invalid model. Expected Mongoose Model instance.\\n' +\r\n 'Usage: createMongooseAdapter({ model: YourModel, repository: yourRepo })'\r\n );\r\n }\r\n\r\n if (!isRepository(options.repository)) {\r\n throw new TypeError(\r\n 'MongooseAdapter: Invalid repository. Expected CrudRepository instance.\\n' +\r\n 'Usage: createMongooseAdapter({ model: YourModel, repository: yourRepo })'\r\n );\r\n }\r\n\r\n this.model = options.model;\r\n this.repository = options.repository;\r\n this.schemaGenerator = options.schemaGenerator;\r\n this.name = `MongooseAdapter<${options.model.modelName}>`;\r\n }\r\n\r\n /**\r\n * Get schema metadata from Mongoose model\r\n */\r\n getSchemaMetadata(): SchemaMetadata {\r\n const schema = this.model.schema;\r\n const paths = schema.paths;\r\n const fields: SchemaMetadata['fields'] = {};\r\n\r\n for (const [fieldName, schemaType] of Object.entries(paths)) {\r\n // Skip internal fields\r\n if (fieldName.startsWith('_') && fieldName !== '_id') continue;\r\n\r\n const typeInfo = schemaType as MongooseSchemaType;\r\n const mongooseType = typeInfo.instance || 'Mixed';\r\n\r\n // Map Mongoose types to our FieldMetadata types\r\n const typeMap: Record<string, 'string' | 'number' | 'boolean' | 'date' | 'object' | 'array' | 'objectId' | 'enum'> = {\r\n String: 'string',\r\n Number: 'number',\r\n Boolean: 'boolean',\r\n Date: 'date',\r\n ObjectID: 'objectId',\r\n ObjectId: 'objectId',\r\n Array: 'array',\r\n Mixed: 'object',\r\n Buffer: 'object',\r\n Embedded: 'object',\r\n };\r\n\r\n fields[fieldName] = {\r\n type: typeMap[mongooseType] ?? 'object',\r\n required: !!typeInfo.isRequired,\r\n ref: typeInfo.options?.ref,\r\n };\r\n }\r\n\r\n return {\r\n name: this.model.modelName,\r\n fields,\r\n relations: this.extractRelations(paths),\r\n };\r\n }\r\n\r\n /**\r\n * Generate OpenAPI schemas from Mongoose model.\r\n *\r\n * If a `schemaGenerator` plugin was provided (e.g. MongoKit's buildCrudSchemasFromModel),\r\n * it is used instead of the built-in basic conversion.\r\n */\r\n generateSchemas(schemaOptions?: RouteSchemaOptions): OpenApiSchemas | null {\r\n try {\r\n // Delegate to external schema generator plugin when available\r\n if (this.schemaGenerator) {\r\n return this.schemaGenerator(this.model, schemaOptions);\r\n }\r\n\r\n // Built-in basic conversion (fallback)\r\n const schema = this.model.schema;\r\n const paths = schema.paths;\r\n const properties: AnyRecord = {};\r\n const required: string[] = [];\r\n\r\n // Extract field rules from schema options\r\n const fieldRules = schemaOptions?.fieldRules || {};\r\n const blockedFields = new Set<string>(\r\n Object.entries(fieldRules)\r\n .filter(([, rules]) => rules.systemManaged || rules.hidden)\r\n .map(([field]) => field)\r\n );\r\n\r\n for (const [fieldName, schemaType] of Object.entries(paths)) {\r\n // Skip internal and blocked fields\r\n if (fieldName.startsWith('__')) continue;\r\n if (blockedFields.has(fieldName)) continue;\r\n\r\n const typeInfo = schemaType as MongooseSchemaType;\r\n properties[fieldName] = this.mongooseTypeToOpenApi(typeInfo);\r\n\r\n if (typeInfo.isRequired) {\r\n required.push(fieldName);\r\n }\r\n }\r\n\r\n // Filter out system-managed fields for input schemas.\r\n // Uses SYSTEM_FIELDS from constants (excludes __v which is Mongoose-internal\r\n // and already absent from input schemas).\r\n const systemFieldSet = new Set<string>(SYSTEM_FIELDS);\r\n const inputProperties = Object.fromEntries(\r\n Object.entries(properties).filter(\r\n ([field]) => !systemFieldSet.has(field)\r\n )\r\n );\r\n\r\n const inputRequired = required.filter(\r\n (field) => !systemFieldSet.has(field)\r\n );\r\n\r\n return {\r\n createBody: {\r\n type: 'object',\r\n properties: inputProperties,\r\n required: inputRequired.length > 0 ? inputRequired : undefined,\r\n },\r\n updateBody: {\r\n type: 'object',\r\n properties: inputProperties,\r\n // All fields optional for PATCH\r\n },\r\n response: {\r\n type: 'object',\r\n properties,\r\n },\r\n };\r\n } catch {\r\n // Schema generation is optional - fail silently\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Extract relation metadata\r\n */\r\n private extractRelations(paths: Record<string, unknown>): SchemaMetadata['relations'] {\r\n const relations: Record<string, { type: 'one-to-one' | 'one-to-many' | 'many-to-many'; target: string; foreignKey?: string }> = {};\r\n\r\n for (const [fieldName, schemaType] of Object.entries(paths)) {\r\n const ref = (schemaType as MongooseSchemaType).options?.ref;\r\n if (ref) {\r\n relations[fieldName] = {\r\n type: 'one-to-one', // Mongoose refs are typically one-to-one\r\n target: ref,\r\n foreignKey: fieldName,\r\n };\r\n }\r\n }\r\n\r\n return Object.keys(relations).length > 0 ? relations : undefined;\r\n }\r\n\r\n /**\r\n * Convert Mongoose type to OpenAPI type\r\n */\r\n private mongooseTypeToOpenApi(typeInfo: MongooseSchemaType): AnyRecord {\r\n const instance = typeInfo.instance;\r\n const options = typeInfo.options || {};\r\n\r\n const baseType: AnyRecord = {};\r\n\r\n switch (instance) {\r\n case 'String':\r\n baseType.type = 'string';\r\n if (options.enum) baseType.enum = options.enum;\r\n if (options.minlength) baseType.minLength = options.minlength;\r\n if (options.maxlength) baseType.maxLength = options.maxlength;\r\n break;\r\n case 'Number':\r\n baseType.type = 'number';\r\n if (options.min !== undefined) baseType.minimum = options.min;\r\n if (options.max !== undefined) baseType.maximum = options.max;\r\n break;\r\n case 'Boolean':\r\n baseType.type = 'boolean';\r\n break;\r\n case 'Date':\r\n baseType.type = 'string';\r\n baseType.format = 'date-time';\r\n break;\r\n case 'ObjectID':\r\n case 'ObjectId':\r\n baseType.type = 'string';\r\n baseType.pattern = '^[a-f\\\\d]{24}$';\r\n break;\r\n case 'Array':\r\n baseType.type = 'array';\r\n baseType.items = { type: 'string' }; // Default, can be improved\r\n break;\r\n default:\r\n baseType.type = 'object';\r\n }\r\n\r\n return baseType;\r\n }\r\n}\r\n\r\n// ============================================================================\r\n// Factory Function with Type Inference\r\n// ============================================================================\r\n\r\n/**\r\n * Create Mongoose adapter with flexible type acceptance.\r\n * Accepts any repository with CRUD methods — no `as any` needed.\r\n *\r\n * @example\r\n * ```typescript\r\n * // Object form (explicit)\r\n * const adapter = createMongooseAdapter({\r\n * model: ProductModel,\r\n * repository: productRepository,\r\n * });\r\n *\r\n * // Shorthand form (2-arg) — most common path\r\n * const adapter = createMongooseAdapter(ProductModel, productRepository);\r\n * ```\r\n */\r\nexport function createMongooseAdapter<TDoc = unknown>(\r\n model: Model<TDoc>,\r\n repository: CrudRepository<TDoc> | RepositoryLike,\r\n): DataAdapter<TDoc>;\r\nexport function createMongooseAdapter<TDoc = unknown>(\r\n options: MongooseAdapterOptions<TDoc>,\r\n): DataAdapter<TDoc>;\r\nexport function createMongooseAdapter<TDoc = unknown>(\r\n modelOrOptions: Model<TDoc> | MongooseAdapterOptions<TDoc>,\r\n repository?: CrudRepository<TDoc> | RepositoryLike,\r\n): DataAdapter<TDoc> {\r\n if (isMongooseModel(modelOrOptions)) {\r\n if (!repository) {\r\n throw new TypeError(\r\n 'createMongooseAdapter: repository is required when using 2-arg form.\\n' +\r\n 'Usage: createMongooseAdapter(Model, repository)'\r\n );\r\n }\r\n return new MongooseAdapter<TDoc>({ model: modelOrOptions as Model<TDoc>, repository });\r\n }\r\n return new MongooseAdapter<TDoc>(modelOrOptions as MongooseAdapterOptions<TDoc>);\r\n}\r\n\r\n// ============================================================================\r\n// Exports\r\n// ============================================================================\r\n\r\nexport default createMongooseAdapter;\r\n","/**\n * Prisma Adapter - PostgreSQL/MySQL/SQLite Implementation\n *\n * @experimental This adapter is implemented but has no integration tests yet.\n * Use in production at your own risk. The Mongoose adapter is the recommended\n * and battle-tested path.\n *\n * Bridges Prisma Client with Arc's DataAdapter interface.\n * Supports Prisma 5+ with all database providers.\n *\n * Implemented features:\n * - Schema generation (OpenAPI docs from DMMF)\n * - Health checks (database connectivity)\n * - Query parsing (URL params → Prisma where/orderBy)\n * - Policy filter translation\n * - Soft delete preset support\n *\n * Known gaps:\n * - No integration test coverage\n * - Multi-tenant isolation relies on caller-provided policyFilters (no auto-enforcement)\n *\n * @example\n * ```typescript\n * import { PrismaClient, Prisma } from '@prisma/client';\n * import { createPrismaAdapter, PrismaQueryParser } from '@classytic/arc/adapters';\n *\n * const prisma = new PrismaClient();\n *\n * const userAdapter = createPrismaAdapter({\n * client: prisma,\n * modelName: 'user',\n * repository: new UserRepository(prisma),\n * dmmf: Prisma.dmmf, // For schema generation\n * queryParser: new PrismaQueryParser(), // Optional: custom parser\n * });\n * ```\n */\n\nimport type { DataAdapter, SchemaMetadata, FieldMetadata, ValidationResult } from './interface.js';\nimport type { CrudRepository, OpenApiSchemas, RouteSchemaOptions, ParsedQuery, QueryParserInterface, AnyRecord } from '../types/index.js';\nimport { DEFAULT_MAX_LIMIT, DEFAULT_LIMIT, RESERVED_QUERY_PARAMS } from '../constants.js';\n\n// ============================================================================\n// Prisma DMMF Types (runtime shapes from @prisma/client)\n// ============================================================================\n\n/** Prisma DMMF field shape */\ninterface DmmfField {\n name: string;\n type: string;\n kind: string;\n isList: boolean;\n isRequired: boolean;\n isUnique?: boolean;\n isId?: boolean;\n isGenerated?: boolean;\n hasDefaultValue?: boolean;\n default?: unknown;\n documentation?: string;\n relationName?: string;\n}\n\n/** Prisma DMMF enum value */\ninterface DmmfEnumValue {\n name: string;\n}\n\n/** Prisma DMMF enum */\ninterface DmmfEnum {\n name: string;\n values: DmmfEnumValue[];\n}\n\n/** Prisma DMMF model shape */\ninterface DmmfModel {\n name: string;\n fields: DmmfField[];\n uniqueIndexes?: Array<{ fields: string[] }>;\n}\n\n/** Prisma DMMF datamodel */\ninterface DmmfDatamodel {\n models: DmmfModel[];\n enums?: DmmfEnum[];\n}\n\n/** Prisma DMMF root shape */\ninterface PrismaDmmf {\n datamodel?: DmmfDatamodel;\n}\n\n/** Prisma client delegate (model accessor) */\ninterface PrismaDelegate {\n findMany(args?: unknown): Promise<unknown[]>;\n}\n\n/** Prisma client shape */\ninterface PrismaClientLike {\n $disconnect(): Promise<void>;\n [key: string]: unknown;\n}\n\n// ============================================================================\n// Prisma Query Parser\n// ============================================================================\n\n/**\n * Options for PrismaQueryParser\n */\nexport interface PrismaQueryParserOptions {\n /** Maximum allowed limit value (default: 1000) */\n maxLimit?: number;\n /** Default limit for pagination (default: 20) */\n defaultLimit?: number;\n /** Enable soft delete filtering by default (default: true) */\n softDeleteEnabled?: boolean;\n /** Field name for soft delete (default: 'deletedAt') */\n softDeleteField?: string;\n}\n\n/**\n * Prisma Query Parser - Converts URL parameters to Prisma query format\n *\n * Translates Arc's query format to Prisma's where/orderBy/take/skip structure.\n *\n * @example\n * ```typescript\n * const parser = new PrismaQueryParser();\n *\n * // URL: ?status=active&price[gte]=100&sort=-createdAt&page=2&limit=10\n * const prismaQuery = parser.toPrismaQuery(parsedQuery);\n * // Returns:\n * // {\n * // where: { status: 'active', price: { gte: 100 }, deletedAt: null },\n * // orderBy: { createdAt: 'desc' },\n * // take: 10,\n * // skip: 10,\n * // }\n * ```\n */\nexport class PrismaQueryParser implements QueryParserInterface {\n private readonly maxLimit: number;\n private readonly defaultLimit: number;\n private readonly softDeleteEnabled: boolean;\n private readonly softDeleteField: string;\n\n /** Map Arc operators to Prisma operators */\n private readonly operatorMap: Record<string, string> = {\n $eq: 'equals',\n $ne: 'not',\n $gt: 'gt',\n $gte: 'gte',\n $lt: 'lt',\n $lte: 'lte',\n $in: 'in',\n $nin: 'notIn',\n $regex: 'contains',\n $exists: undefined as unknown as string, // Handled specially in translateFilters\n };\n\n constructor(options: PrismaQueryParserOptions = {}) {\n this.maxLimit = options.maxLimit ?? DEFAULT_MAX_LIMIT;\n this.defaultLimit = options.defaultLimit ?? DEFAULT_LIMIT;\n this.softDeleteEnabled = options.softDeleteEnabled ?? true;\n this.softDeleteField = options.softDeleteField ?? 'deletedAt';\n }\n\n /**\n * Parse URL query parameters (delegates to ArcQueryParser format)\n */\n parse(query: Record<string, unknown> | null | undefined): ParsedQuery {\n const q = query ?? {};\n\n const page = this.parseNumber(q.page, 1);\n const limit = Math.min(this.parseNumber(q.limit, this.defaultLimit), this.maxLimit);\n\n return {\n filters: this.parseFilters(q),\n limit,\n page,\n sort: this.parseSort(q.sort),\n search: q.search as string | undefined,\n select: this.parseSelect(q.select),\n };\n }\n\n /**\n * Convert ParsedQuery to Prisma query options\n */\n toPrismaQuery(parsed: ParsedQuery, policyFilters?: Record<string, unknown>): PrismaQueryOptions {\n const where: Record<string, unknown> = {};\n\n // Apply filters\n if (parsed.filters) {\n Object.assign(where, this.translateFilters(parsed.filters));\n }\n\n // Apply policy filters (multi-tenant, ownership, etc.)\n if (policyFilters) {\n Object.assign(where, this.translateFilters(policyFilters));\n }\n\n // Apply soft delete filter\n if (this.softDeleteEnabled) {\n where[this.softDeleteField] = null;\n }\n\n // Build orderBy\n const orderBy: Array<Record<string, 'asc' | 'desc'>> | undefined = parsed.sort\n ? Object.entries(parsed.sort).map(([field, dir]) => ({\n [field]: (dir === 1 ? 'asc' : 'desc') as 'asc' | 'desc',\n }))\n : undefined;\n\n // Build pagination\n const take = parsed.limit ?? this.defaultLimit;\n const skip = parsed.page ? (parsed.page - 1) * take : 0;\n\n // Build select\n const select = parsed.select\n ? Object.fromEntries(\n Object.entries(parsed.select)\n .filter(([, v]) => v === 1)\n .map(([k]) => [k, true])\n )\n : undefined;\n\n return {\n where: Object.keys(where).length > 0 ? where : undefined,\n orderBy: orderBy && orderBy.length > 0 ? orderBy : undefined,\n take,\n skip,\n select: select && Object.keys(select).length > 0 ? select : undefined,\n };\n }\n\n /**\n * Translate Arc/MongoDB-style filters to Prisma where clause\n */\n private translateFilters(filters: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n\n for (const [field, value] of Object.entries(filters)) {\n if (value === null || value === undefined) continue;\n\n // Handle nested operator objects: { status: { $ne: 'deleted' } }\n if (typeof value === 'object' && !Array.isArray(value)) {\n const prismaCondition: Record<string, unknown> = {};\n\n for (const [op, opValue] of Object.entries(value as Record<string, unknown>)) {\n if (op === '$exists') {\n // $exists: true → { not: null }, $exists: false → null\n result[field] = opValue ? { not: null } : null;\n continue;\n }\n\n const prismaOp = this.operatorMap[op];\n if (prismaOp) {\n prismaCondition[prismaOp] = opValue;\n }\n }\n\n if (Object.keys(prismaCondition).length > 0) {\n result[field] = prismaCondition;\n }\n } else {\n // Direct equality\n result[field] = value;\n }\n }\n\n return result;\n }\n\n private parseNumber(value: unknown, defaultValue: number): number {\n if (value === undefined || value === null) return defaultValue;\n const num = parseInt(String(value), 10);\n return Number.isNaN(num) ? defaultValue : Math.max(1, num);\n }\n\n private parseSort(value: unknown): Record<string, 1 | -1> | undefined {\n if (!value) return undefined;\n\n const sortStr = String(value);\n const result: Record<string, 1 | -1> = {};\n\n for (const field of sortStr.split(',')) {\n const trimmed = field.trim();\n if (!trimmed || !/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) continue;\n\n if (trimmed.startsWith('-')) {\n result[trimmed.slice(1)] = -1;\n } else {\n result[trimmed] = 1;\n }\n }\n\n return Object.keys(result).length > 0 ? result : undefined;\n }\n\n private parseSelect(value: unknown): Record<string, 0 | 1> | undefined {\n if (!value) return undefined;\n\n const result: Record<string, 0 | 1> = {};\n\n for (const field of String(value).split(',')) {\n const trimmed = field.trim();\n if (!trimmed || !/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) continue;\n\n result[trimmed.startsWith('-') ? trimmed.slice(1) : trimmed] = trimmed.startsWith('-') ? 0 : 1;\n }\n\n return Object.keys(result).length > 0 ? result : undefined;\n }\n\n private parseFilters(query: Record<string, unknown>): Record<string, unknown> {\n const filters: Record<string, unknown> = {};\n\n const operators: Record<string, string> = {\n eq: '$eq', ne: '$ne', gt: '$gt', gte: '$gte',\n lt: '$lt', lte: '$lte', in: '$in', nin: '$nin',\n like: '$regex', contains: '$regex', exists: '$exists',\n };\n\n for (const [key, value] of Object.entries(query)) {\n if (RESERVED_QUERY_PARAMS.has(key) || value === undefined || value === null) continue;\n\n const match = key.match(/^([a-zA-Z_][a-zA-Z0-9_.]*)(?:\\[([a-z]+)\\])?$/);\n if (!match) continue;\n\n const [, fieldName, operator] = match;\n if (!fieldName) continue;\n\n if (operator && operators[operator]) {\n if (!filters[fieldName]) filters[fieldName] = {};\n (filters[fieldName] as Record<string, unknown>)[operators[operator]] = this.coerceValue(value, operator);\n } else if (!operator) {\n filters[fieldName] = this.coerceValue(value);\n }\n }\n\n return filters;\n }\n\n private coerceValue(value: unknown, operator?: string): unknown {\n if (operator === 'in' || operator === 'nin') {\n if (Array.isArray(value)) return value.map(v => this.coerceValue(v));\n if (typeof value === 'string' && value.includes(',')) {\n return value.split(',').map(v => this.coerceValue(v.trim()));\n }\n return [this.coerceValue(value)];\n }\n\n if (operator === 'exists') {\n return String(value).toLowerCase() === 'true' || value === '1';\n }\n\n if (value === 'true') return true;\n if (value === 'false') return false;\n if (value === 'null') return null;\n\n if (typeof value === 'string') {\n const num = Number(value);\n if (!Number.isNaN(num) && value.trim() !== '') return num;\n }\n\n return value;\n }\n}\n\n/**\n * Prisma query options returned by toPrismaQuery\n */\nexport interface PrismaQueryOptions {\n where?: Record<string, unknown>;\n orderBy?: Array<Record<string, 'asc' | 'desc'>>;\n take?: number;\n skip?: number;\n select?: Record<string, boolean>;\n include?: Record<string, boolean>;\n}\n\n// ============================================================================\n// Prisma Adapter Options\n// ============================================================================\n\nexport interface PrismaAdapterOptions<TModel> {\n /** Prisma client instance */\n client: PrismaClientLike;\n /** Model name (e.g., 'user', 'product') */\n modelName: string;\n /** Repository instance implementing CRUD operations */\n repository: CrudRepository<TModel>;\n /** Optional: Prisma DMMF (Data Model Meta Format) for schema extraction */\n dmmf?: PrismaDmmf;\n /** Optional: Custom query parser (default: PrismaQueryParser) */\n queryParser?: PrismaQueryParser;\n /** Enable soft delete filtering (default: true) */\n softDeleteEnabled?: boolean;\n /** Field name for soft delete (default: 'deletedAt') */\n softDeleteField?: string;\n}\n\nexport class PrismaAdapter<TModel = unknown> implements DataAdapter<TModel> {\n readonly type = 'prisma' as const;\n readonly name: string;\n readonly repository: CrudRepository<TModel>;\n readonly queryParser: PrismaQueryParser;\n\n private client: PrismaClientLike;\n private modelName: string;\n private dmmf?: PrismaDmmf;\n private softDeleteEnabled: boolean;\n private softDeleteField: string;\n\n constructor(options: PrismaAdapterOptions<TModel>) {\n this.client = options.client;\n this.modelName = options.modelName;\n this.repository = options.repository;\n this.dmmf = options.dmmf;\n this.name = `prisma:${options.modelName}`;\n this.softDeleteEnabled = options.softDeleteEnabled ?? true;\n this.softDeleteField = options.softDeleteField ?? 'deletedAt';\n\n // Initialize query parser\n this.queryParser = options.queryParser ?? new PrismaQueryParser({\n softDeleteEnabled: this.softDeleteEnabled,\n softDeleteField: this.softDeleteField,\n });\n }\n\n /**\n * Parse URL query parameters and convert to Prisma query options\n */\n parseQuery(query: Record<string, unknown>, policyFilters?: Record<string, unknown>): PrismaQueryOptions {\n const parsed = this.queryParser.parse(query);\n return this.queryParser.toPrismaQuery(parsed, policyFilters);\n }\n\n /**\n * Apply policy filters to existing Prisma where clause\n * Used for multi-tenant, ownership, and other security filters\n */\n applyPolicyFilters(\n where: Record<string, unknown>,\n policyFilters: Record<string, unknown>\n ): Record<string, unknown> {\n return { ...where, ...policyFilters };\n }\n\n generateSchemas(options?: RouteSchemaOptions): OpenApiSchemas | null {\n // Extract schema from Prisma DMMF if available\n if (!this.dmmf) return null;\n\n try {\n const model = this.dmmf.datamodel?.models?.find(\n (m: DmmfModel) => m.name.toLowerCase() === this.modelName.toLowerCase()\n );\n\n if (!model) return null;\n\n const entitySchema = this.buildEntitySchema(model, options);\n const createBodySchema = this.buildCreateSchema(model, options);\n const updateBodySchema = this.buildUpdateSchema(model, options);\n\n return {\n entity: entitySchema,\n createBody: createBodySchema,\n updateBody: updateBodySchema,\n params: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n },\n required: ['id'],\n },\n listQuery: {\n type: 'object',\n properties: {\n page: { type: 'number', minimum: 1, description: 'Page number for pagination' },\n limit: { type: 'number', minimum: 1, maximum: 100, description: 'Items per page' },\n sort: { type: 'string', description: 'Sort field (e.g., \"name\", \"-createdAt\")' },\n // Note: Actual filtering requires custom query parser implementation\n // This is placeholder documentation only\n },\n },\n };\n } catch {\n // Schema generation is optional - fail silently\n return null;\n }\n }\n\n getSchemaMetadata(): SchemaMetadata | null {\n if (!this.dmmf) return null;\n\n try {\n const model = this.dmmf.datamodel?.models?.find(\n (m: DmmfModel) => m.name.toLowerCase() === this.modelName.toLowerCase()\n );\n\n if (!model) return null;\n\n const fields: Record<string, FieldMetadata> = {};\n\n for (const field of model.fields) {\n fields[field.name] = this.convertPrismaFieldToMetadata(field);\n }\n\n return {\n name: model.name,\n fields,\n indexes: model.uniqueIndexes?.map((idx: { fields: string[] }) => ({\n fields: idx.fields,\n unique: true,\n })),\n };\n } catch (err) {\n return null;\n }\n }\n\n async validate(data: unknown): Promise<ValidationResult> {\n // Prisma validates on write, so we do basic type checking here\n if (!data || typeof data !== 'object') {\n return {\n valid: false,\n errors: [{ field: 'root', message: 'Data must be an object' }],\n };\n }\n\n // Get required fields from DMMF\n if (this.dmmf) {\n try {\n const model = this.dmmf.datamodel?.models?.find(\n (m: DmmfModel) => m.name.toLowerCase() === this.modelName.toLowerCase()\n );\n\n if (model) {\n const requiredFields = model.fields.filter(\n (f: DmmfField) => f.isRequired && !f.hasDefaultValue && !f.isGenerated\n );\n\n const errors: Array<{ field: string; message: string }> = [];\n\n for (const field of requiredFields) {\n if (!(field.name in (data as Record<string, unknown>))) {\n errors.push({\n field: field.name,\n message: `${field.name} is required`,\n });\n }\n }\n\n if (errors.length > 0) {\n return { valid: false, errors };\n }\n }\n } catch (err) {\n // Validation failed, but we'll let Prisma handle it on write\n }\n }\n\n return { valid: true };\n }\n\n async healthCheck(): Promise<boolean> {\n try {\n // Use findMany with take: 1 for database-agnostic health check\n // This works across all Prisma providers (SQL, MongoDB, etc.)\n // Prisma client delegates use camelCase (e.g., prisma.userProfile, not prisma.UserProfile)\n const delegateName = this.modelName.charAt(0).toLowerCase() + this.modelName.slice(1);\n const delegate = this.client[delegateName] as PrismaDelegate | undefined;\n if (!delegate) {\n return false;\n }\n await delegate.findMany({ take: 1 });\n return true;\n } catch (err) {\n return false;\n }\n }\n\n async close(): Promise<void> {\n try {\n await this.client.$disconnect();\n } catch (err) {\n // Already disconnected or error - ignore\n }\n }\n\n // ============================================================================\n // Private Helper Methods\n // ============================================================================\n\n private buildEntitySchema(model: DmmfModel, options?: RouteSchemaOptions): AnyRecord {\n const properties: Record<string, AnyRecord> = {};\n const required: string[] = [];\n\n for (const field of model.fields) {\n // Skip internal fields unless explicitly included\n if (this.shouldSkipField(field, options)) continue;\n\n properties[field.name] = this.convertPrismaFieldToJsonSchema(field);\n\n if (field.isRequired && !field.hasDefaultValue) {\n required.push(field.name);\n }\n }\n\n return {\n type: 'object',\n properties,\n ...(required.length > 0 && { required }),\n };\n }\n\n private buildCreateSchema(model: DmmfModel, options?: RouteSchemaOptions): AnyRecord {\n const properties: Record<string, AnyRecord> = {};\n const required: string[] = [];\n\n for (const field of model.fields) {\n // Skip auto-generated and relation fields for create\n if (field.isGenerated || field.relationName) continue;\n if (this.shouldSkipField(field, options)) continue;\n\n properties[field.name] = this.convertPrismaFieldToJsonSchema(field);\n\n if (field.isRequired && !field.hasDefaultValue) {\n required.push(field.name);\n }\n }\n\n return {\n type: 'object',\n properties,\n ...(required.length > 0 && { required }),\n };\n }\n\n private buildUpdateSchema(model: DmmfModel, options?: RouteSchemaOptions): AnyRecord {\n const properties: Record<string, AnyRecord> = {};\n\n for (const field of model.fields) {\n // Skip auto-generated, ID, and relation fields for update\n if (field.isGenerated || field.isId || field.relationName) continue;\n if (this.shouldSkipField(field, options)) continue;\n\n properties[field.name] = this.convertPrismaFieldToJsonSchema(field);\n }\n\n return {\n type: 'object',\n properties,\n };\n }\n\n private shouldSkipField(field: DmmfField, options?: RouteSchemaOptions): boolean {\n // Check if field is in excludeFields\n if (options?.excludeFields?.includes(field.name)) {\n return true;\n }\n\n // Skip internal Prisma fields\n if (field.name.startsWith('_')) {\n return true;\n }\n\n return false;\n }\n\n private convertPrismaFieldToJsonSchema(field: DmmfField): AnyRecord {\n const schema: AnyRecord = {};\n\n // Map Prisma types to JSON Schema types\n switch (field.type) {\n case 'String':\n schema.type = 'string';\n break;\n case 'Int':\n case 'BigInt':\n schema.type = 'integer';\n break;\n case 'Float':\n case 'Decimal':\n schema.type = 'number';\n break;\n case 'Boolean':\n schema.type = 'boolean';\n break;\n case 'DateTime':\n schema.type = 'string';\n schema.format = 'date-time';\n break;\n case 'Json':\n schema.type = 'object';\n break;\n default:\n // Enums and other types\n if (field.kind === 'enum') {\n schema.type = 'string';\n // Extract enum values from DMMF if available\n if (this.dmmf?.datamodel?.enums) {\n const enumDef = this.dmmf.datamodel.enums.find((e: DmmfEnum) => e.name === field.type);\n if (enumDef) {\n schema.enum = enumDef.values.map((v: DmmfEnumValue) => v.name);\n }\n }\n } else {\n schema.type = 'string';\n }\n }\n\n // Handle arrays\n if (field.isList) {\n return {\n type: 'array',\n items: schema,\n };\n }\n\n // Add description if available\n if (field.documentation) {\n schema.description = field.documentation;\n }\n\n return schema;\n }\n\n private convertPrismaFieldToMetadata(field: DmmfField): FieldMetadata {\n const metadata: FieldMetadata = {\n type: this.mapPrismaTypeToMetadataType(field.type, field.kind),\n required: field.isRequired,\n array: field.isList,\n };\n\n if (field.isUnique) {\n metadata.unique = true;\n }\n\n if (field.hasDefaultValue) {\n metadata.default = field.default;\n }\n\n if (field.documentation) {\n metadata.description = field.documentation;\n }\n\n if (field.relationName) {\n metadata.ref = field.type;\n }\n\n return metadata;\n }\n\n private mapPrismaTypeToMetadataType(\n type: string,\n kind: string\n ): FieldMetadata['type'] {\n if (kind === 'enum') return 'enum';\n\n switch (type) {\n case 'String':\n return 'string';\n case 'Int':\n case 'BigInt':\n case 'Float':\n case 'Decimal':\n return 'number';\n case 'Boolean':\n return 'boolean';\n case 'DateTime':\n return 'date';\n case 'Json':\n return 'object';\n default:\n return 'string';\n }\n }\n}\n\n/**\n * Factory function to create Prisma adapter\n *\n * @example\n * import { PrismaClient } from '@prisma/client';\n * import { createPrismaAdapter } from '@classytic/arc';\n *\n * const prisma = new PrismaClient();\n *\n * const userAdapter = createPrismaAdapter({\n * client: prisma,\n * modelName: 'user',\n * repository: userRepository,\n * dmmf: Prisma.dmmf, // Optional: for schema generation\n * });\n */\nexport function createPrismaAdapter<TModel>(\n options: PrismaAdapterOptions<TModel>\n): PrismaAdapter<TModel> {\n return new PrismaAdapter(options);\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"prisma-Dg9GoVdj.d.mts","names":[],"sources":["../src/adapters/mongoose.ts","../src/adapters/prisma.ts"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"queryCachePlugin-7THaI5mt.d.mts","names":[],"sources":["../src/cache/QueryCache.ts","../src/cache/queryCachePlugin.ts"],"mappings":";;;;;UAciB,aAAA;EACf,IAAA,EAAM,CAAA;EACN,SAAA;EACA,UAAA;EACA,SAAA;EACA,IAAA;AAAA;AAAA,UAGe,gBAAA;;EAEf,SAAA;EAAA;EAEA,MAAA;EAEA;EAAA,IAAA;AAAA;AAAA,KAGU,WAAA;AAAA,UAEK,WAAA;EACf,IAAA,EAAM,CAAA;EACN,MAAA,EAAQ,WAAA;AAAA;AAAA,cAGG,UAAA;EAAA,iBACM,KAAA;cAEL,KAAA,EAAO,UAAA;EAIb,GAAA,GAAA,CAAO,GAAA,WAAc,OAAA,CAAQ,WAAA,CAAY,CAAA;EAqBzC,GAAA,GAAA,CAAO,GAAA,UAAa,IAAA,EAAM,CAAA,EAAG,MAAA,EAAQ,gBAAA,GAAmB,OAAA;EAiBxD,UAAA,CAAW,GAAA,WAAc,OAAA;EAjDzB;EAsDA,kBAAA,CAAmB,QAAA,WAAmB,OAAA;EArDpC;EA2DF,mBAAA,CAAoB,QAAA,WAAmB,OAAA;EA3D1B;EAmEb,aAAA,CAAc,GAAA,WAAc,OAAA;EAhEb;EAsEf,cAAA,CAAe,GAAA,WAAc,OAAA;AAAA;;;UClFpB,uBAAA;EDAf;ECEA,KAAA,GAAQ,UAAA;EDAJ;ECEJ,QAAA;IACE,SAAA;IACA,MAAA;EAAA;AAAA;AAAA,UAIa,kBAAA;EACf,SAAA;EACA,MAAA;AAAA;;UAIe,iBAAA;EACf,OAAA;EACA,IAAA;AAAA;AAAA;EAAA,UAIU,eAAA;IACR,UAAA,EAAY,UAAA;IACZ,gBAAA,EAAkB,kBAAA;IDZC;ICcnB,6BAAA,EAA+B,IAAA,EAAM,iBAAA;EAAA;AAAA;AAAA,cA8D5B,gBAAA,EAAgB,kBAAA,CAAA,uBAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"queryCachePlugin-DMBnp2Q0.mjs","names":[],"sources":["../src/cache/QueryCache.ts","../src/cache/queryCachePlugin.ts"],"sourcesContent":["/**\r\n * QueryCache — TanStack Query-inspired server cache\r\n *\r\n * Wraps any CacheStore with:\r\n * - Freshness metadata (staleTime / gcTime envelope)\r\n * - Stale-while-revalidate status detection\r\n * - Version-based O(1) invalidation (no key scanning)\r\n * - Tag-based cross-resource invalidation\r\n */\r\n\r\nimport type { CacheStore } from './interface.js';\r\nimport { tagVersionKey, versionKey } from './keys.js';\r\n\r\n/** Metadata wrapper stored in CacheStore */\r\nexport interface CacheEnvelope<T = unknown> {\r\n data: T;\r\n createdAt: number;\r\n staleAfter: number;\r\n expiresAt: number;\r\n tags: string[];\r\n}\r\n\r\nexport interface QueryCacheConfig {\r\n /** Seconds data is \"fresh\" (no revalidation). Default: 0 */\r\n staleTime?: number;\r\n /** Seconds stale data stays cached (SWR window). Default: 60 */\r\n gcTime?: number;\r\n /** Tags for group invalidation */\r\n tags?: string[];\r\n}\r\n\r\nexport type CacheStatus = 'fresh' | 'stale' | 'miss';\r\n\r\nexport interface CacheResult<T> {\r\n data: T;\r\n status: CacheStatus;\r\n}\r\n\r\nexport class QueryCache {\r\n private readonly store: CacheStore;\r\n\r\n constructor(store: CacheStore) {\r\n this.store = store;\r\n }\r\n\r\n async get<T>(key: string): Promise<CacheResult<T>> {\r\n const envelope = await this.store.get(key) as CacheEnvelope<T> | undefined;\r\n\r\n if (!envelope || !envelope.createdAt) {\r\n return { data: undefined as T, status: 'miss' };\r\n }\r\n\r\n const now = Date.now();\r\n\r\n if (now >= envelope.expiresAt) {\r\n await this.store.delete(key);\r\n return { data: undefined as T, status: 'miss' };\r\n }\r\n\r\n if (now < envelope.staleAfter) {\r\n return { data: envelope.data, status: 'fresh' };\r\n }\r\n\r\n return { data: envelope.data, status: 'stale' };\r\n }\r\n\r\n async set<T>(key: string, data: T, config: QueryCacheConfig): Promise<void> {\r\n const staleTimeMs = (config.staleTime ?? 0) * 1000;\r\n const gcTimeMs = (config.gcTime ?? 60) * 1000;\r\n const totalTtl = staleTimeMs + gcTimeMs;\r\n const now = Date.now();\r\n\r\n const envelope: CacheEnvelope<T> = {\r\n data,\r\n createdAt: now,\r\n staleAfter: now + staleTimeMs,\r\n expiresAt: now + totalTtl,\r\n tags: config.tags ?? [],\r\n };\r\n\r\n await this.store.set(key, envelope, { ttlMs: totalTtl });\r\n }\r\n\r\n async invalidate(key: string): Promise<void> {\r\n await this.store.delete(key);\r\n }\r\n\r\n /** Get current version for a resource (defaults to 0 if not set) */\r\n async getResourceVersion(resource: string): Promise<number> {\r\n const ver = await this.store.get(versionKey(resource)) as number | undefined;\r\n return ver ?? 0;\r\n }\r\n\r\n /** Bump resource version — orphans all cached queries for this resource */\r\n async bumpResourceVersion(resource: string): Promise<void> {\r\n const key = versionKey(resource);\r\n const newVersion = Date.now();\r\n // Store version with a very long TTL (24h) — it's tiny data\r\n await this.store.set(key, newVersion, { ttlMs: 24 * 60 * 60 * 1000 });\r\n }\r\n\r\n /** Get current version for a tag */\r\n async getTagVersion(tag: string): Promise<number> {\r\n const ver = await this.store.get(tagVersionKey(tag)) as number | undefined;\r\n return ver ?? 0;\r\n }\r\n\r\n /** Bump tag version — orphans all cached queries tagged with this tag */\r\n async bumpTagVersion(tag: string): Promise<void> {\r\n const key = tagVersionKey(tag);\r\n const newVersion = Date.now();\r\n await this.store.set(key, newVersion, { ttlMs: 24 * 60 * 60 * 1000 });\r\n }\r\n}\r\n","/**\r\n * QueryCache Fastify Plugin\r\n *\r\n * Registers QueryCache on `fastify.queryCache` and wires automatic\r\n * cache invalidation via CRUD events. Zero config for memory mode.\r\n *\r\n * @example\r\n * ```typescript\r\n * // Memory mode (default)\r\n * await fastify.register(queryCachePlugin);\r\n *\r\n * // With Redis store\r\n * await fastify.register(queryCachePlugin, {\r\n * store: new RedisCacheStore({ client: redis, prefix: 'arc:qc:' }),\r\n * defaults: { staleTime: 30, gcTime: 300 },\r\n * });\r\n * ```\r\n */\r\n\r\nimport fp from 'fastify-plugin';\r\nimport type { FastifyInstance, FastifyPluginAsync } from 'fastify';\r\nimport { QueryCache, type QueryCacheConfig } from './QueryCache.js';\r\nimport type { CacheStore } from './interface.js';\r\nimport { MemoryCacheStore } from './memory.js';\r\nimport { hasEvents } from '../utils/typeGuards.js';\r\n\r\nexport interface QueryCachePluginOptions {\r\n /** CacheStore instance. Default: MemoryCacheStore with default options. */\r\n store?: CacheStore;\r\n /** Global defaults for staleTime/gcTime (seconds) */\r\n defaults?: {\r\n staleTime?: number;\r\n gcTime?: number;\r\n };\r\n}\r\n\r\nexport interface QueryCacheDefaults {\r\n staleTime: number;\r\n gcTime: number;\r\n}\r\n\r\n/** Cross-resource invalidation rules collected from resource configs */\r\nexport interface CrossResourceRule {\r\n pattern: string;\r\n tags: string[];\r\n}\r\n\r\ndeclare module 'fastify' {\r\n interface FastifyInstance {\r\n queryCache: QueryCache;\r\n queryCacheConfig: QueryCacheDefaults;\r\n /** Register cross-resource invalidation rules (called by defineResource) */\r\n registerCacheInvalidationRule?(rule: CrossResourceRule): void;\r\n }\r\n}\r\n\r\nconst CRUD_SUFFIXES = new Set(['created', 'updated', 'deleted']);\r\n\r\nconst queryCachePluginImpl: FastifyPluginAsync<QueryCachePluginOptions> = async (\r\n fastify: FastifyInstance,\r\n opts: QueryCachePluginOptions = {},\r\n) => {\r\n const store = opts.store ?? new MemoryCacheStore();\r\n const queryCache = new QueryCache(store);\r\n\r\n const defaults: QueryCacheDefaults = {\r\n staleTime: opts.defaults?.staleTime ?? 0,\r\n gcTime: opts.defaults?.gcTime ?? 60,\r\n };\r\n\r\n fastify.decorate('queryCache', queryCache);\r\n fastify.decorate('queryCacheConfig', defaults);\r\n\r\n // Collect cross-resource rules from defineResource calls\r\n const crossResourceRules: CrossResourceRule[] = [];\r\n fastify.decorate('registerCacheInvalidationRule', (rule: CrossResourceRule) => {\r\n crossResourceRules.push(rule);\r\n });\r\n\r\n // Wire event-driven invalidation after all resources are registered\r\n fastify.addHook('onReady', async () => {\r\n if (!hasEvents(fastify)) return;\r\n\r\n // Auto-invalidate on CRUD events (product.created → bump product version)\r\n await fastify.events.subscribe('*', async (event) => {\r\n const type = (event as { type: string }).type;\r\n const dotIdx = type.lastIndexOf('.');\r\n if (dotIdx === -1) return;\r\n\r\n const suffix = type.slice(dotIdx + 1);\r\n if (!CRUD_SUFFIXES.has(suffix)) return;\r\n\r\n const resource = type.slice(0, dotIdx);\r\n await queryCache.bumpResourceVersion(resource);\r\n });\r\n\r\n // Wire cross-resource tag invalidation\r\n for (const rule of crossResourceRules) {\r\n await fastify.events.subscribe(rule.pattern, async () => {\r\n for (const tag of rule.tags) {\r\n await queryCache.bumpTagVersion(tag);\r\n }\r\n });\r\n }\r\n });\r\n\r\n // Cleanup on close\r\n fastify.addHook('onClose', async () => {\r\n if ('close' in store && typeof store.close === 'function') {\r\n await store.close();\r\n }\r\n });\r\n};\r\n\r\nexport const queryCachePlugin = fp(queryCachePluginImpl, {\r\n name: 'arc-query-cache',\r\n fastify: '5.x',\r\n});\r\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"redis-D-JAeLtm.d.mts","names":[],"sources":["../src/idempotency/stores/redis.ts"],"mappings":";;;UAoBiB,WAAA;EACf,GAAA,CAAI,GAAA,WAAc,OAAA;EAClB,GAAA,CAAI,GAAA,UAAa,KAAA,UAAe,OAAA;IAAY,EAAA;IAAa,EAAA;EAAA,IAAiB,OAAA;EAC1E,GAAA,CAAI,GAAA,sBAAyB,OAAA;EAC7B,MAAA,CAAO,GAAA,sBAAyB,OAAA;EAD5B;EAGJ,IAAA,EAAM,MAAA,sBAA4B,IAAA,wBAA4B,OAAA;EAC9D,IAAA,KAAS,OAAA;EACT,UAAA,KAAe,OAAA;AAAA;AAAA,UAGA,4BAAA;EALT;EAON,MAAA,EAAQ,WAAA;EAPsD;EAS9D,MAAA;EARS;EAUT,UAAA;EATe;EAWf,KAAA;AAAA;AAAA,cAGW,qBAAA,YAAiC,gBAAA;EAAA,SACnC,IAAA;EAAA,QACD,MAAA;EAAA,QACA,MAAA;EAAA,QACA,UAAA;EAAA,QACA,KAAA;cAEI,OAAA,EAAS,4BAAA;EAAA,QAOb,SAAA;EAAA,QAIA,OAAA;EAIF,GAAA,CAAI,GAAA,WAAc,OAAA,CAAQ,iBAAA;EAqB1B,GAAA,CAAI,GAAA,UAAa,MAAA,EAAQ,IAAA,CAAK,iBAAA,WAA4B,OAAA;EAa1D,OAAA,CAAQ,GAAA,UAAa,SAAA,UAAmB,KAAA,WAAgB,OAAA;EAUxD,MAAA,CAAO,GAAA,UAAa,SAAA,WAAoB,OAAA;EAQxC,QAAA,CAAS,GAAA,WAAc,OAAA;EAKvB,MAAA,CAAO,GAAA,WAAc,OAAA;EAIrB,cAAA,CAAe,MAAA,WAAiB,OAAA;EAQhC,YAAA,CAAa,MAAA,WAAiB,OAAA,CAAQ,iBAAA;EAhDR;EAAA,QAqEtB,YAAA;EAcR,KAAA,CAAA,GAAS,OAAA;AAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"redis-stream-Bdh_vUU8.d.mts","names":[],"sources":["../src/events/transports/redis-stream.ts"],"mappings":";;;UAkCiB,eAAA;EACf,IAAA,CAAK,GAAA,UAAa,EAAA,aAAe,WAAA,aAAwB,OAAA;EACzD,UAAA,CACE,OAAA,WACA,KAAA,UACA,QAAA,aACG,IAAA,wBACF,OAAA,CAAQ,KAAA,UAAe,KAAA;EAC1B,IAAA,CAAK,GAAA,UAAa,KAAA,aAAkB,GAAA,aAAgB,OAAA;EACpD,MAAA,CAAO,OAAA,UAAiB,GAAA,UAAa,KAAA,aAAkB,IAAA,aAAiB,OAAA;EACxE,QAAA,CACE,GAAA,UACA,KAAA,aACG,IAAA,wBACF,OAAA,CAAQ,KAAA;EACX,MAAA,CACE,GAAA,UACA,KAAA,UACA,QAAA,UACA,WAAA,aACG,GAAA,aACF,OAAA,CAAQ,KAAA;EACX,IAAA,CAAK,GAAA,WAAc,OAAA;EACnB,IAAA,IAAQ,OAAA;AAAA;AAAA,UAOO,2BAAA;EArBR;;;;EA0BP,MAAA;EAzBA;;;;;EAgCA,KAAA;EA3BA;;;;EAiCA,QAAA;EA5BK;;;;EAkCL,WAAA;EAhCmB;;;;EAsCnB,SAAA;EA9Be;;;;EAoCf,UAAA;EAxBA;;;;;;EAgCA,cAAA;EAcA;;;;;EAPA,gBAAA;EAoBgC;;;;;EAbhC,MAAA;EAqE0C;;;;EA/D1C,MAAA,GAAS,WAAA;AAAA;AAAA,cAOE,oBAAA,YAAgC,cAAA;EAAA,SAClC,IAAA;EAAA,QAED,KAAA;EAAA,QACA,MAAA;EAAA,QACA,KAAA;EAAA,QACA,QAAA;EAAA,QACA,WAAA;EAAA,QACA,SAAA;EAAA,QACA,UAAA;EAAA,QACA,cAAA;EAAA,QACA,gBAAA;EAAA,QACA,MAAA;EAAA,QAEA,MAAA;EAAA,QAEA,QAAA;EAAA,QACA,OAAA;EAAA,QACA,WAAA;EAAA,QACA,YAAA;cAEI,KAAA,EAAO,eAAA,EAAiB,OAAA,GAAS,2BAAA;EAkBvC,OAAA,CAAQ,KAAA,EAAO,WAAA,GAAc,OAAA;EAiB7B,SAAA,CAAU,OAAA,UAAiB,OAAA,EAAS,YAAA,GAAe,OAAA;EA8BnD,KAAA,CAAA,GAAS,OAAA;EAAA,QAeD,WAAA;EAAA,QAqBA,QAAA;EAAA,QAkBA,eAAA;EAAA,QAkBA,YAAA;EAAA,QAiDA,YAAA;EAAA,QA8CN,mBAAA;EAAA,QAcA,cAAA;EAAA,QAcM,SAAA;EAAA,QA+BN,KAAA;AAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/registry/introspectionPlugin.ts"],"mappings":";;;;;;;cAUM,mBAAA,EAAqB,kBAAA,CAAmB,0BAAA;AAAA,cAA0B,QAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"requestContext-QQD6ROJc.mjs","names":[],"sources":["../src/context/requestContext.ts"],"sourcesContent":["/**\n * Request Context via AsyncLocalStorage\n *\n * Provides request-scoped context accessible anywhere in the call stack\n * without threading parameters through every function call.\n *\n * Uses Node.js native AsyncLocalStorage — zero-cost per request, no allocation\n * beyond the store object, and fully supported since Node 16.\n *\n * @example\n * ```typescript\n * import { requestContext } from '@classytic/arc';\n *\n * // Anywhere in the call stack — no parameter passing needed\n * async function auditAction(action: string) {\n * const ctx = requestContext.get();\n * await auditLog.write({\n * action,\n * userId: ctx?.user?.id,\n * orgId: ctx?.organizationId,\n * requestId: ctx?.requestId,\n * });\n * }\n *\n * // Type-safe access to specific fields\n * const userId = requestContext.get()?.user?.id;\n * const orgId = requestContext.get()?.organizationId;\n * ```\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\n\n/**\n * Shape of the request-scoped context store.\n * Populated by Arc's onRequest hook in arcCorePlugin.\n */\nexport interface RequestStore {\n /** Unique request identifier */\n requestId?: string;\n /** Authenticated user (if any) */\n user?: { id?: string; _id?: string; roles?: string[]; [key: string]: unknown } | null;\n /** Active organization ID (multi-tenant) */\n organizationId?: string;\n /** Active team ID (team-scoped resources) */\n teamId?: string;\n /** Current resource name (set by arcDecorator in CRUD routes) */\n resourceName?: string;\n /** Request start time (for timing) */\n startTime: number;\n /** Additional context — extensible by app */\n [key: string]: unknown;\n}\n\nconst storage = new AsyncLocalStorage<RequestStore>();\n\n/**\n * Request context API.\n *\n * - `get()` — returns current store or undefined if outside request scope\n * - `run(store, fn)` — run a function with a specific store (used by Arc internals)\n * - `getStore()` — alias for get() (matches Node.js API naming)\n */\nexport const requestContext = {\n /**\n * Get the current request context.\n * Returns undefined if called outside a request lifecycle.\n */\n get(): RequestStore | undefined {\n return storage.getStore();\n },\n\n /**\n * Alias for get() — matches Node.js AsyncLocalStorage API naming.\n */\n getStore(): RequestStore | undefined {\n return storage.getStore();\n },\n\n /**\n * Run a function within a specific request context.\n * Used internally by Arc's onRequest hook.\n */\n run<T>(store: RequestStore, fn: () => T): T {\n return storage.run(store, fn);\n },\n\n /**\n * The underlying AsyncLocalStorage instance.\n * Exposed for advanced use cases (testing, custom integrations).\n */\n storage,\n};\n\nexport default requestContext;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,MAAM,UAAU,IAAI,mBAAiC;;;;;;;;AASrD,MAAa,iBAAiB;CAK5B,MAAgC;AAC9B,SAAO,QAAQ,UAAU;;CAM3B,WAAqC;AACnC,SAAO,QAAQ,UAAU;;CAO3B,IAAO,OAAqB,IAAgB;AAC1C,SAAO,QAAQ,IAAI,OAAO,GAAG;;CAO/B;CACD"}
|