@classytic/arc 2.2.5 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +187 -18
  2. package/bin/arc.js +11 -3
  3. package/dist/BaseController-CkM5dUh_.mjs +1031 -0
  4. package/dist/{EventTransport-BkUDYZEb.d.mts → EventTransport-wc5hSLik.d.mts} +1 -1
  5. package/dist/{HookSystem-BsGV-j2l.mjs → HookSystem-COkyWztM.mjs} +2 -3
  6. package/dist/{ResourceRegistry-7Ic20ZMw.mjs → ResourceRegistry-DeCIFlix.mjs} +8 -5
  7. package/dist/adapters/index.d.mts +3 -5
  8. package/dist/adapters/index.mjs +2 -3
  9. package/dist/{prisma-DJbMt3yf.mjs → adapters-DTC4Ug66.mjs} +45 -12
  10. package/dist/audit/index.d.mts +4 -7
  11. package/dist/audit/index.mjs +2 -29
  12. package/dist/audit/mongodb.d.mts +1 -4
  13. package/dist/audit/mongodb.mjs +2 -3
  14. package/dist/auth/index.d.mts +7 -9
  15. package/dist/auth/index.mjs +65 -63
  16. package/dist/auth/redis-session.d.mts +1 -1
  17. package/dist/auth/redis-session.mjs +1 -2
  18. package/dist/{betterAuthOpenApi-DjWDddNc.mjs → betterAuthOpenApi-lz0IRbXJ.mjs} +4 -6
  19. package/dist/cache/index.d.mts +23 -23
  20. package/dist/cache/index.mjs +4 -6
  21. package/dist/{caching-GSDJcA6-.mjs → caching-BSXB-Xr7.mjs} +2 -24
  22. package/dist/chunk-BpYLSNr0.mjs +14 -0
  23. package/dist/circuitBreaker-BOBOpN2w.mjs +284 -0
  24. package/dist/circuitBreaker-JP2GdJ4b.d.mts +206 -0
  25. package/dist/cli/commands/describe.mjs +24 -7
  26. package/dist/cli/commands/docs.mjs +6 -7
  27. package/dist/cli/commands/doctor.d.mts +10 -0
  28. package/dist/cli/commands/doctor.mjs +156 -0
  29. package/dist/cli/commands/generate.mjs +66 -17
  30. package/dist/cli/commands/init.mjs +315 -45
  31. package/dist/cli/commands/introspect.mjs +2 -4
  32. package/dist/cli/index.d.mts +1 -10
  33. package/dist/cli/index.mjs +4 -153
  34. package/dist/{constants-DdXFXQtN.mjs → constants-Cxde4rpC.mjs} +1 -2
  35. package/dist/core/index.d.mts +3 -5
  36. package/dist/core/index.mjs +5 -4
  37. package/dist/core-C1XCMtqM.mjs +185 -0
  38. package/dist/{createApp-BKHSl2nT.mjs → createApp-ByWNRsZj.mjs} +65 -36
  39. package/dist/{defineResource-DO9ONe_D.mjs → defineResource-D9aY5Cy6.mjs} +154 -1165
  40. package/dist/discovery/index.mjs +37 -5
  41. package/dist/docs/index.d.mts +6 -9
  42. package/dist/docs/index.mjs +3 -21
  43. package/dist/dynamic/index.d.mts +93 -0
  44. package/dist/dynamic/index.mjs +122 -0
  45. package/dist/{elevation-DSTbVvYj.mjs → elevation-BEdACOLB.mjs} +5 -36
  46. package/dist/{elevation-DGo5shaX.d.mts → elevation-Ca_yveIO.d.mts} +41 -7
  47. package/dist/{errorHandler-C3GY3_ow.mjs → errorHandler--zp54tGc.mjs} +3 -5
  48. package/dist/errorHandler-Do4vVQ1f.d.mts +139 -0
  49. package/dist/{errors-DBANPbGr.mjs → errors-rxhfP7Hf.mjs} +1 -2
  50. package/dist/{eventPlugin-BEOvaDqo.mjs → eventPlugin-Ba00swHF.mjs} +25 -27
  51. package/dist/{eventPlugin-H6wDDjGO.d.mts → eventPlugin-iGrSEmwJ.d.mts} +105 -5
  52. package/dist/events/index.d.mts +72 -7
  53. package/dist/events/index.mjs +216 -4
  54. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  55. package/dist/events/transports/redis-stream-entry.mjs +19 -7
  56. package/dist/events/transports/redis.d.mts +1 -1
  57. package/dist/events/transports/redis.mjs +3 -4
  58. package/dist/factory/index.d.mts +23 -9
  59. package/dist/factory/index.mjs +48 -3
  60. package/dist/{fields-Bi_AVKSo.d.mts → fields-DFwdaWCq.d.mts} +1 -1
  61. package/dist/{fields-CTd_CrKr.mjs → fields-ipsbIRPK.mjs} +1 -2
  62. package/dist/hooks/index.d.mts +1 -3
  63. package/dist/hooks/index.mjs +2 -3
  64. package/dist/idempotency/index.d.mts +5 -5
  65. package/dist/idempotency/index.mjs +3 -7
  66. package/dist/idempotency/mongodb.d.mts +1 -1
  67. package/dist/idempotency/mongodb.mjs +4 -5
  68. package/dist/idempotency/redis.d.mts +1 -1
  69. package/dist/idempotency/redis.mjs +2 -5
  70. package/dist/{fastifyAdapter-CyAA2zlB.d.mts → index-BL8CaQih.d.mts} +56 -57
  71. package/dist/index-Diqcm14c.d.mts +369 -0
  72. package/dist/{prisma-xjhMEq_S.d.mts → index-yhxyjqNb.d.mts} +4 -5
  73. package/dist/index.d.mts +100 -105
  74. package/dist/index.mjs +85 -58
  75. package/dist/integrations/event-gateway.d.mts +1 -1
  76. package/dist/integrations/event-gateway.mjs +8 -4
  77. package/dist/integrations/index.d.mts +4 -2
  78. package/dist/integrations/index.mjs +1 -1
  79. package/dist/integrations/jobs.d.mts +2 -2
  80. package/dist/integrations/jobs.mjs +63 -14
  81. package/dist/integrations/mcp/index.d.mts +219 -0
  82. package/dist/integrations/mcp/index.mjs +572 -0
  83. package/dist/integrations/mcp/testing.d.mts +53 -0
  84. package/dist/integrations/mcp/testing.mjs +104 -0
  85. package/dist/integrations/streamline.mjs +39 -19
  86. package/dist/integrations/webhooks.d.mts +56 -0
  87. package/dist/integrations/webhooks.mjs +139 -0
  88. package/dist/integrations/websocket-redis.d.mts +46 -0
  89. package/dist/integrations/websocket-redis.mjs +50 -0
  90. package/dist/integrations/websocket.d.mts +68 -2
  91. package/dist/integrations/websocket.mjs +96 -13
  92. package/dist/{interface-CSNjltAc.d.mts → interface-B4awm1RJ.d.mts} +2 -2
  93. package/dist/interface-DGmPxakH.d.mts +2213 -0
  94. package/dist/{keys-DhqDRxv3.mjs → keys-qcD-TVJl.mjs} +3 -4
  95. package/dist/{logger-ByrvQWZO.mjs → logger-Dz3j1ItV.mjs} +2 -4
  96. package/dist/{memory-B2v7KrCB.mjs → memory-Cb_7iy9e.mjs} +2 -4
  97. package/dist/metrics-Csh4nsvv.mjs +224 -0
  98. package/dist/migrations/index.mjs +3 -7
  99. package/dist/{mongodb-DNKEExbf.mjs → mongodb-BuQ7fNTg.mjs} +1 -4
  100. package/dist/{mongodb-ClykrfGo.d.mts → mongodb-CUpYfxfD.d.mts} +2 -3
  101. package/dist/{mongodb-Dg8O_gvd.d.mts → mongodb-bga9AbkD.d.mts} +2 -2
  102. package/dist/{openapi-9nB_kiuR.mjs → openapi-CBmZ6EQN.mjs} +4 -21
  103. package/dist/org/index.d.mts +12 -14
  104. package/dist/org/index.mjs +92 -119
  105. package/dist/org/types.d.mts +2 -2
  106. package/dist/org/types.mjs +1 -1
  107. package/dist/permissions/index.d.mts +4 -278
  108. package/dist/permissions/index.mjs +4 -579
  109. package/dist/permissions-CA5zg0yK.mjs +751 -0
  110. package/dist/plugins/index.d.mts +104 -107
  111. package/dist/plugins/index.mjs +203 -313
  112. package/dist/plugins/response-cache.mjs +4 -69
  113. package/dist/plugins/tracing-entry.d.mts +1 -1
  114. package/dist/plugins/tracing-entry.mjs +24 -11
  115. package/dist/{pluralize-CM-jZg7p.mjs → pluralize-CcT6qF0a.mjs} +12 -13
  116. package/dist/policies/index.d.mts +2 -2
  117. package/dist/policies/index.mjs +80 -83
  118. package/dist/presets/index.d.mts +26 -19
  119. package/dist/presets/index.mjs +2 -142
  120. package/dist/presets/multiTenant.d.mts +1 -4
  121. package/dist/presets/multiTenant.mjs +4 -6
  122. package/dist/presets-C9QXJV1u.mjs +422 -0
  123. package/dist/{queryCachePlugin-B6R0d4av.mjs → queryCachePlugin-ClosZdNS.mjs} +6 -27
  124. package/dist/{queryCachePlugin-Q6SYuHZ6.d.mts → queryCachePlugin-DcmETvcB.d.mts} +3 -3
  125. package/dist/queryParser-CgCtsjti.mjs +352 -0
  126. package/dist/{redis-UwjEp8Ea.d.mts → redis-CQ5YxMC5.d.mts} +2 -2
  127. package/dist/{redis-stream-CBg0upHI.d.mts → redis-stream-BW9UKLZM.d.mts} +9 -2
  128. package/dist/registry/index.d.mts +1 -4
  129. package/dist/registry/index.mjs +3 -4
  130. package/dist/{introspectionPlugin-B3JkrjwU.mjs → registry-I-ogLgL9.mjs} +1 -8
  131. package/dist/{requestContext-xi6OKBL-.mjs → requestContext-DYtmNpm5.mjs} +1 -3
  132. package/dist/resourceToTools-B6ZN9Ing.mjs +489 -0
  133. package/dist/rpc/index.d.mts +90 -0
  134. package/dist/rpc/index.mjs +248 -0
  135. package/dist/{schemaConverter-Dtg0Kt9T.mjs → schemaConverter-DjzHpFam.mjs} +1 -2
  136. package/dist/schemas/index.d.mts +30 -30
  137. package/dist/schemas/index.mjs +4 -6
  138. package/dist/scope/index.d.mts +13 -2
  139. package/dist/scope/index.mjs +18 -5
  140. package/dist/{sessionManager-D_iEHjQl.d.mts → sessionManager-wbkYj2HL.d.mts} +2 -2
  141. package/dist/{sse-DkqQ1uxb.mjs → sse-BkViJPlT.mjs} +4 -25
  142. package/dist/testing/index.d.mts +551 -567
  143. package/dist/testing/index.mjs +1744 -1799
  144. package/dist/{tracing-8CEbhF0w.d.mts → tracing-bz_U4EM1.d.mts} +6 -1
  145. package/dist/{typeGuards-DwxA1t_L.mjs → typeGuards-Cj5Rgvlg.mjs} +1 -2
  146. package/dist/types/index.d.mts +4 -946
  147. package/dist/types/index.mjs +2 -4
  148. package/dist/types-BJmgxNbF.d.mts +275 -0
  149. package/dist/{types-RLkFVgaw.d.mts → types-BNUccdcf.d.mts} +2 -2
  150. package/dist/{types-Beqn1Un7.mjs → types-C6TQjtdi.mjs} +30 -2
  151. package/dist/{types-DMSBMkaZ.d.mts → types-Dt0-AI6E.d.mts} +85 -27
  152. package/dist/{types-DelU6kln.mjs → types-ZUu_h0jp.mjs} +1 -2
  153. package/dist/utils/index.d.mts +255 -352
  154. package/dist/utils/index.mjs +7 -6
  155. package/dist/utils-Dc0WhlIl.mjs +594 -0
  156. package/dist/versioning-BzfeHmhj.mjs +37 -0
  157. package/package.json +46 -12
  158. package/skills/arc/SKILL.md +506 -0
  159. package/skills/arc/references/auth.md +250 -0
  160. package/skills/arc/references/events.md +272 -0
  161. package/skills/arc/references/integrations.md +385 -0
  162. package/skills/arc/references/mcp.md +386 -0
  163. package/skills/arc/references/production.md +610 -0
  164. package/skills/arc/references/testing.md +183 -0
  165. package/dist/audited-CGdLiSlE.mjs +0 -140
  166. package/dist/chunk-C7Uep-_p.mjs +0 -20
  167. package/dist/circuitBreaker-DYhWBW_D.mjs +0 -1096
  168. package/dist/errorHandler-CW3OOeYq.d.mts +0 -72
  169. package/dist/interface-DZYNK9bb.d.mts +0 -1112
  170. package/dist/presets-BTeYbw7h.d.mts +0 -57
  171. package/dist/presets-CeFtfDR8.mjs +0 -119
  172. /package/dist/{errors-DAWRdiYP.d.mts → errors-CPpvPHT0.d.mts} +0 -0
  173. /package/dist/{externalPaths-SyPF2tgK.d.mts → externalPaths-DpO-s7r8.d.mts} +0 -0
  174. /package/dist/{interface-DTbsvIWe.d.mts → interface-D_BWALyZ.d.mts} +0 -0
@@ -0,0 +1,2213 @@
1
+ import { s as RequestScope } from "./elevation-Ca_yveIO.mjs";
2
+ import { n as FieldPermissionMap } from "./fields-DFwdaWCq.mjs";
3
+ import { i as UserBase, t as PermissionCheck } from "./types-BNUccdcf.mjs";
4
+ import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, RouteHandlerMethod, RouteHandlerMethod as RouteHandlerMethod$1 } from "fastify";
5
+
6
+ //#region src/hooks/HookSystem.d.ts
7
+ type HookPhase = "before" | "around" | "after";
8
+ type HookOperation = "create" | "update" | "delete" | "read" | "list";
9
+ interface HookContext<T = AnyRecord> {
10
+ resource: string;
11
+ operation: HookOperation;
12
+ phase: HookPhase;
13
+ data?: T;
14
+ result?: T | T[];
15
+ user?: UserBase;
16
+ context?: RequestContext;
17
+ meta?: AnyRecord;
18
+ }
19
+ type HookHandler<T = AnyRecord> = (ctx: HookContext<T>) => void | Promise<void> | T | Promise<T>;
20
+ /**
21
+ * Around hook handler — wraps the core operation.
22
+ * Call `next()` to proceed to the next around hook or the actual operation.
23
+ */
24
+ type AroundHookHandler<T = AnyRecord> = (ctx: HookContext<T>, next: () => Promise<T | undefined>) => T | undefined | Promise<T | undefined>;
25
+ interface HookRegistration {
26
+ /** Hook name for dependency resolution and debugging */
27
+ name?: string;
28
+ resource: string;
29
+ operation: HookOperation;
30
+ phase: HookPhase;
31
+ handler: HookHandler;
32
+ priority: number;
33
+ /** Names of hooks that must execute before this one */
34
+ dependsOn?: string[];
35
+ }
36
+ interface HookSystemOptions {
37
+ /** Custom logger for error/warning reporting. Defaults to console */
38
+ logger?: {
39
+ error: (message: string, ...args: unknown[]) => void;
40
+ warn?: (message: string, ...args: unknown[]) => void;
41
+ };
42
+ }
43
+ declare class HookSystem {
44
+ private hooks;
45
+ private logger;
46
+ private warn;
47
+ constructor(options?: HookSystemOptions);
48
+ /**
49
+ * Generate hook key
50
+ */
51
+ private getKey;
52
+ /**
53
+ * Register a hook
54
+ * Supports both object parameter and positional arguments
55
+ */
56
+ register<T = AnyRecord>(resourceOrOptions: string | {
57
+ name?: string;
58
+ resource: string;
59
+ operation: HookOperation;
60
+ phase: HookPhase;
61
+ handler: HookHandler<T>;
62
+ priority?: number;
63
+ dependsOn?: string[];
64
+ }, operation?: HookOperation, phase?: HookPhase, handler?: HookHandler<T>, priority?: number): () => void;
65
+ /**
66
+ * Register before hook
67
+ */
68
+ before<T = AnyRecord>(resource: string, operation: HookOperation, handler: HookHandler<T>, priority?: number): () => void;
69
+ /**
70
+ * Register after hook
71
+ */
72
+ after<T = AnyRecord>(resource: string, operation: HookOperation, handler: HookHandler<T>, priority?: number): () => void;
73
+ /**
74
+ * Register around hook — wraps the core operation.
75
+ * Call `next()` inside the handler to proceed.
76
+ */
77
+ around<T = AnyRecord>(resource: string, operation: HookOperation, handler: AroundHookHandler<T>, priority?: number): () => void;
78
+ /**
79
+ * Execute around hooks as a nested middleware chain.
80
+ * Each around hook receives `next()` to call the next hook or the core operation.
81
+ */
82
+ executeAround<T = AnyRecord>(resource: string, operation: HookOperation, data: T, execute: () => Promise<T | undefined>, options?: {
83
+ user?: UserBase;
84
+ context?: RequestContext;
85
+ meta?: AnyRecord;
86
+ }): Promise<T | undefined>;
87
+ /**
88
+ * Execute hooks for a given context
89
+ */
90
+ execute<T = AnyRecord>(ctx: HookContext<T>): Promise<T | undefined>;
91
+ /**
92
+ * Execute before hooks
93
+ */
94
+ executeBefore<T = AnyRecord>(resource: string, operation: HookOperation, data: T, options?: {
95
+ user?: UserBase;
96
+ context?: RequestContext;
97
+ meta?: AnyRecord;
98
+ }): Promise<T>;
99
+ /**
100
+ * Execute after hooks
101
+ * Errors in after hooks are logged but don't fail the request
102
+ */
103
+ executeAfter<T = AnyRecord>(resource: string, operation: HookOperation, result: T | T[], options?: {
104
+ user?: UserBase;
105
+ context?: RequestContext;
106
+ meta?: AnyRecord;
107
+ }): Promise<void>;
108
+ /**
109
+ * Topological sort with Kahn's algorithm.
110
+ * Hooks with `dependsOn` are ordered after their dependencies.
111
+ * Within the same dependency level, priority ordering is preserved.
112
+ * Hooks without names or dependencies pass through in their original order.
113
+ */
114
+ private topologicalSort;
115
+ /**
116
+ * Get all registered hooks
117
+ */
118
+ getAll(): HookRegistration[];
119
+ /**
120
+ * Get hooks for a specific resource
121
+ */
122
+ getForResource(resource: string): HookRegistration[];
123
+ /**
124
+ * Get hooks matching filter criteria.
125
+ * Useful for debugging and testing specific hook combinations.
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * // Find all before-create hooks for products (including wildcards)
130
+ * const hooks = hookSystem.getRegistered({
131
+ * resource: 'product',
132
+ * operation: 'create',
133
+ * phase: 'before',
134
+ * });
135
+ * ```
136
+ */
137
+ getRegistered(filter?: {
138
+ resource?: string;
139
+ operation?: HookOperation;
140
+ phase?: HookPhase;
141
+ }): HookRegistration[];
142
+ /**
143
+ * Get a structured summary of all registered hooks for debugging.
144
+ *
145
+ * @example
146
+ * ```typescript
147
+ * const info = hookSystem.inspect();
148
+ * // { total: 12, resources: { product: [...], '*': [...] }, summary: [...] }
149
+ * ```
150
+ */
151
+ inspect(): {
152
+ total: number;
153
+ resources: Record<string, HookRegistration[]>;
154
+ summary: Array<{
155
+ name?: string;
156
+ key: string;
157
+ priority: number;
158
+ dependsOn?: string[];
159
+ }>;
160
+ };
161
+ /**
162
+ * Check if any hooks exist for a specific resource/operation/phase combination.
163
+ */
164
+ has(resource: string, operation: HookOperation, phase: HookPhase): boolean;
165
+ /**
166
+ * Clear all hooks
167
+ */
168
+ clear(): void;
169
+ /**
170
+ * Clear hooks for a specific resource
171
+ */
172
+ clearResource(resource: string): void;
173
+ }
174
+ /**
175
+ * Create a new isolated HookSystem instance
176
+ *
177
+ * Use this for:
178
+ * - Test isolation (parallel test suites)
179
+ * - Multiple app instances with independent hooks
180
+ *
181
+ * @example
182
+ * const hooks = createHookSystem();
183
+ * await app.register(arcCorePlugin, { hookSystem: hooks });
184
+ *
185
+ * @example With custom logger
186
+ * const hooks = createHookSystem({ logger: fastify.log });
187
+ */
188
+ declare function createHookSystem(options?: HookSystemOptions): HookSystem;
189
+ interface DefineHookOptions<T = AnyRecord> {
190
+ /** Unique hook name (required for dependency resolution) */
191
+ name: string;
192
+ /** Target resource */
193
+ resource: string;
194
+ /** CRUD operation */
195
+ operation: HookOperation;
196
+ /** before or after */
197
+ phase: HookPhase;
198
+ /** Hook handler */
199
+ handler: HookHandler<T>;
200
+ /** Priority (lower = earlier, default: 10) */
201
+ priority?: number;
202
+ /** Names of hooks that must execute before this one */
203
+ dependsOn?: string[];
204
+ }
205
+ /**
206
+ * Define a named hook with optional dependencies.
207
+ * Returns a registration object — call `register(hookSystem)` to activate.
208
+ *
209
+ * @example
210
+ * ```typescript
211
+ * const generateSlug = defineHook({
212
+ * name: 'generateSlug',
213
+ * resource: 'product', operation: 'create', phase: 'before',
214
+ * handler: (ctx) => ({ ...ctx.data, slug: slugify(ctx.data.name) }),
215
+ * });
216
+ *
217
+ * const validateUniqueSlug = defineHook({
218
+ * name: 'validateUniqueSlug',
219
+ * resource: 'product', operation: 'create', phase: 'before',
220
+ * dependsOn: ['generateSlug'],
221
+ * handler: async (ctx) => { // check uniqueness },
222
+ * });
223
+ *
224
+ * // Register on a hook system
225
+ * generateSlug.register(hooks);
226
+ * validateUniqueSlug.register(hooks);
227
+ * ```
228
+ */
229
+ declare function defineHook<T = AnyRecord>(options: DefineHookOptions<T>): DefineHookOptions<T> & {
230
+ register: (hooks: HookSystem) => () => void;
231
+ };
232
+ /**
233
+ * Create a before-create hook registration for a given hook system
234
+ */
235
+ declare function beforeCreate<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
236
+ /**
237
+ * Create an after-create hook registration for a given hook system
238
+ */
239
+ declare function afterCreate<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
240
+ /**
241
+ * Create a before-update hook registration for a given hook system
242
+ */
243
+ declare function beforeUpdate<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
244
+ /**
245
+ * Create an after-update hook registration for a given hook system
246
+ */
247
+ declare function afterUpdate<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
248
+ /**
249
+ * Create a before-delete hook registration for a given hook system
250
+ */
251
+ declare function beforeDelete<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
252
+ /**
253
+ * Create an after-delete hook registration for a given hook system
254
+ */
255
+ declare function afterDelete<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
256
+ //#endregion
257
+ //#region src/pipeline/types.d.ts
258
+ /**
259
+ * Pipeline context passed to guards, transforms, and interceptors.
260
+ * Extends IRequestContext with pipeline-specific metadata.
261
+ */
262
+ interface PipelineContext extends IRequestContext {
263
+ /** Resource name being accessed */
264
+ resource: string;
265
+ /** CRUD operation being performed */
266
+ operation: "list" | "get" | "create" | "update" | "delete" | string;
267
+ }
268
+ /**
269
+ * Which operations a pipeline step applies to.
270
+ * If omitted, applies to ALL operations.
271
+ */
272
+ type OperationFilter = Array<"list" | "get" | "create" | "update" | "delete" | string>;
273
+ /**
274
+ * Guard — boolean check that short-circuits on failure.
275
+ * Return true to proceed, throw to deny.
276
+ */
277
+ interface Guard {
278
+ readonly _type: "guard";
279
+ readonly name: string;
280
+ readonly operations?: OperationFilter;
281
+ handler(ctx: PipelineContext): boolean | Promise<boolean>;
282
+ }
283
+ /**
284
+ * Transform — modifies request data before the handler.
285
+ * Returns modified context (or mutates in place).
286
+ */
287
+ interface Transform {
288
+ readonly _type: "transform";
289
+ readonly name: string;
290
+ readonly operations?: OperationFilter;
291
+ handler(ctx: PipelineContext): PipelineContext | undefined | Promise<PipelineContext | undefined>;
292
+ }
293
+ /**
294
+ * Next function passed to interceptors — calls the handler (or next interceptor).
295
+ */
296
+ type NextFunction = () => Promise<IControllerResponse<unknown>>;
297
+ /**
298
+ * Intercept — wraps handler execution (before + after pattern).
299
+ */
300
+ interface Interceptor {
301
+ readonly _type: "interceptor";
302
+ readonly name: string;
303
+ readonly operations?: OperationFilter;
304
+ handler(ctx: PipelineContext, next: NextFunction): Promise<IControllerResponse<unknown>>;
305
+ }
306
+ type PipelineStep = Guard | Transform | Interceptor;
307
+ /**
308
+ * Pipeline configuration — can be a flat array or per-operation map.
309
+ */
310
+ type PipelineConfig = PipelineStep[] | {
311
+ list?: PipelineStep[];
312
+ get?: PipelineStep[];
313
+ create?: PipelineStep[];
314
+ update?: PipelineStep[];
315
+ delete?: PipelineStep[];
316
+ [operation: string]: PipelineStep[] | undefined;
317
+ };
318
+ //#endregion
319
+ //#region src/types/repository.d.ts
320
+ /**
321
+ * Repository Interface - Database-Agnostic CRUD Operations
322
+ *
323
+ * This is the standard interface that all repositories must implement.
324
+ * MongoKit Repository already implements this interface.
325
+ *
326
+ * @example
327
+ * ```typescript
328
+ * import type { CrudRepository } from '@classytic/arc';
329
+ *
330
+ * // Your repository automatically satisfies this interface
331
+ * const userRepo: CrudRepository<UserDocument> = new Repository(UserModel);
332
+ * ```
333
+ */
334
+ /**
335
+ * Query options for read operations
336
+ */
337
+ interface QueryOptions {
338
+ /** Transaction session — adapters handle the actual type (e.g., Mongoose ClientSession) */
339
+ session?: unknown;
340
+ /** Field selection - include or exclude fields */
341
+ select?: string | string[] | Record<string, 0 | 1>;
342
+ /** Relations to populate - string, array, or Mongoose populate options */
343
+ populate?: string | string[] | Record<string, unknown>;
344
+ /** Return plain JS objects instead of Mongoose documents */
345
+ lean?: boolean;
346
+ /** Allow additional adapter-specific options */
347
+ [key: string]: unknown;
348
+ }
349
+ /**
350
+ * Pagination parameters for list operations
351
+ */
352
+ interface PaginationParams<TDoc = unknown> {
353
+ /** Filter criteria */
354
+ filters?: Partial<TDoc> & Record<string, unknown>;
355
+ /** Sort specification - string ("-createdAt") or object ({ createdAt: -1 }) */
356
+ sort?: string | Record<string, 1 | -1>;
357
+ /** Page number (1-indexed) */
358
+ page?: number;
359
+ /** Items per page */
360
+ limit?: number;
361
+ /** Allow additional options (select, populate, etc.) */
362
+ [key: string]: unknown;
363
+ }
364
+ /**
365
+ * Paginated result from list operations
366
+ */
367
+ interface PaginatedResult<TDoc> {
368
+ /** Documents for current page */
369
+ docs: TDoc[];
370
+ /** Current page number */
371
+ page: number;
372
+ /** Items per page */
373
+ limit: number;
374
+ /** Total document count */
375
+ total: number;
376
+ /** Total page count */
377
+ pages: number;
378
+ /** Has next page */
379
+ hasNext: boolean;
380
+ /** Has previous page */
381
+ hasPrev: boolean;
382
+ }
383
+ /**
384
+ * Standard CRUD Repository Interface
385
+ *
386
+ * Defines the contract for data access operations.
387
+ * All database adapters (MongoKit, Prisma, etc.) implement this interface.
388
+ *
389
+ * @typeParam TDoc - The document/entity type
390
+ */
391
+ interface CrudRepository<TDoc> {
392
+ /**
393
+ * Get paginated list of documents
394
+ */
395
+ getAll(params?: PaginationParams<TDoc>, options?: QueryOptions): Promise<PaginatedResult<TDoc>>;
396
+ /**
397
+ * Get single document by ID
398
+ */
399
+ getById(id: string, options?: QueryOptions): Promise<TDoc | null>;
400
+ /**
401
+ * Create new document
402
+ */
403
+ create(data: Partial<TDoc>, options?: {
404
+ session?: unknown;
405
+ [key: string]: unknown;
406
+ }): Promise<TDoc>;
407
+ /**
408
+ * Update document by ID
409
+ */
410
+ update(id: string, data: Partial<TDoc>, options?: QueryOptions): Promise<TDoc | null>;
411
+ /**
412
+ * Delete document by ID
413
+ */
414
+ delete(id: string, options?: {
415
+ session?: unknown;
416
+ [key: string]: unknown;
417
+ }): Promise<{
418
+ success: boolean;
419
+ message: string;
420
+ }>;
421
+ /** Allow custom methods (getBySlug, getTree, restore, etc.) */
422
+ [key: string]: unknown;
423
+ }
424
+ /**
425
+ * Extract document type from a repository
426
+ *
427
+ * @example
428
+ * ```typescript
429
+ * type UserDoc = InferDoc<typeof userRepository>;
430
+ * // UserDoc is now the document type of userRepository
431
+ * ```
432
+ */
433
+ type InferDoc<R> = R extends CrudRepository<infer T> ? T : never;
434
+ //#endregion
435
+ //#region src/core/defineResource.d.ts
436
+ /**
437
+ * Define a resource with database adapter
438
+ *
439
+ * This is the MAIN entry point for creating Arc resources.
440
+ * The adapter provides both repository and schema metadata.
441
+ */
442
+ declare function defineResource<TDoc = AnyRecord>(config: ResourceConfig<TDoc>): ResourceDefinition<TDoc>;
443
+ interface ResolvedResourceConfig<TDoc = AnyRecord> extends ResourceConfig<TDoc> {
444
+ _appliedPresets?: string[];
445
+ _controllerOptions?: {
446
+ slugField?: string;
447
+ parentField?: string;
448
+ [key: string]: unknown;
449
+ };
450
+ _pendingHooks?: Array<{
451
+ operation: "create" | "update" | "delete" | "read" | "list";
452
+ phase: "before" | "after";
453
+ handler: (ctx: AnyRecord) => unknown;
454
+ priority: number;
455
+ }>;
456
+ }
457
+ declare class ResourceDefinition<TDoc = AnyRecord> {
458
+ readonly name: string;
459
+ readonly displayName: string;
460
+ readonly tag: string;
461
+ readonly prefix: string;
462
+ readonly adapter?: DataAdapter<TDoc>;
463
+ readonly controller?: IController<TDoc>;
464
+ readonly schemaOptions: RouteSchemaOptions;
465
+ readonly customSchemas: CrudSchemas;
466
+ readonly permissions: ResourcePermissions;
467
+ readonly additionalRoutes: AdditionalRoute[];
468
+ readonly middlewares: MiddlewareConfig;
469
+ readonly disableDefaultRoutes: boolean;
470
+ readonly disabledRoutes: CrudRouteKey[];
471
+ readonly events: Record<string, EventDefinition>;
472
+ readonly rateLimit?: RateLimitConfig | false;
473
+ readonly updateMethod?: "PUT" | "PATCH" | "both";
474
+ readonly pipe?: PipelineConfig;
475
+ readonly fields?: FieldPermissionMap;
476
+ readonly cache?: ResourceCacheConfig;
477
+ readonly tenantField?: string | false;
478
+ readonly idField?: string;
479
+ readonly queryParser?: QueryParserInterface;
480
+ readonly _appliedPresets: string[];
481
+ _pendingHooks: Array<{
482
+ operation: "create" | "update" | "delete" | "read" | "list";
483
+ phase: "before" | "after";
484
+ handler: (ctx: AnyRecord) => unknown;
485
+ priority: number;
486
+ }>;
487
+ _registryMeta?: RegisterOptions;
488
+ constructor(config: ResolvedResourceConfig<TDoc>);
489
+ /** Get repository from adapter (if available) */
490
+ get repository(): RepositoryLike | CrudRepository<TDoc> | undefined;
491
+ _validateControllerMethods(): void;
492
+ toPlugin(): FastifyPluginAsync;
493
+ /**
494
+ * Get event definitions for registry
495
+ */
496
+ getEvents(): Array<{
497
+ name: string;
498
+ module: string;
499
+ schema?: AnyRecord;
500
+ description?: string;
501
+ }>;
502
+ /**
503
+ * Get resource metadata
504
+ */
505
+ getMetadata(): ResourceMetadata;
506
+ }
507
+ //#endregion
508
+ //#region src/registry/ResourceRegistry.d.ts
509
+ interface RegisterOptions {
510
+ module?: string;
511
+ /** Pre-generated OpenAPI schemas */
512
+ openApiSchemas?: OpenApiSchemas;
513
+ }
514
+ declare class ResourceRegistry {
515
+ private _resources;
516
+ private _frozen;
517
+ constructor();
518
+ /**
519
+ * Register a resource
520
+ */
521
+ register(resource: ResourceDefinition<unknown>, options?: RegisterOptions): this;
522
+ /**
523
+ * Get resource by name
524
+ */
525
+ get(name: string): RegistryEntry | undefined;
526
+ /**
527
+ * Get all resources
528
+ */
529
+ getAll(): RegistryEntry[];
530
+ /**
531
+ * Get resources by module
532
+ */
533
+ getByModule(moduleName: string): RegistryEntry[];
534
+ /**
535
+ * Get resources by preset
536
+ */
537
+ getByPreset(presetName: string): RegistryEntry[];
538
+ /**
539
+ * Check if resource exists
540
+ */
541
+ has(name: string): boolean;
542
+ /**
543
+ * Get registry statistics
544
+ */
545
+ getStats(): RegistryStats;
546
+ /**
547
+ * Get full introspection data
548
+ */
549
+ getIntrospection(): IntrospectionData;
550
+ /**
551
+ * Freeze registry (prevent further registrations)
552
+ */
553
+ freeze(): void;
554
+ /**
555
+ * Check if frozen
556
+ */
557
+ isFrozen(): boolean;
558
+ /**
559
+ * Unfreeze registry (allow new registrations)
560
+ */
561
+ unfreeze(): void;
562
+ /**
563
+ * Reset registry — clear all resources and unfreeze
564
+ */
565
+ reset(): void;
566
+ /** @internal Alias for unfreeze() */
567
+ _unfreeze(): void;
568
+ /** @internal Alias for reset() */
569
+ _clear(): void;
570
+ /**
571
+ * Group by key
572
+ */
573
+ private _groupBy;
574
+ }
575
+ //#endregion
576
+ //#region src/types/handlers.d.ts
577
+ /**
578
+ * Minimal server accessor — exposes safe, read-only server decorators.
579
+ * Allows controller handlers to publish events, log, and audit
580
+ * without switching to `wrapHandler: false`.
581
+ */
582
+ interface ServerAccessor {
583
+ /** Event bus — publish domain events from any handler */
584
+ events?: {
585
+ publish: <T>(type: string, payload: T, meta?: Partial<Record<string, unknown>>) => Promise<void>;
586
+ };
587
+ /** Audit logger — log custom audit entries */
588
+ audit?: {
589
+ create: (resource: string, documentId: string, data: Record<string, unknown>, context?: Record<string, unknown>) => Promise<void>;
590
+ update: (resource: string, documentId: string, before: Record<string, unknown>, after: Record<string, unknown>, context?: Record<string, unknown>) => Promise<void>;
591
+ delete: (resource: string, documentId: string, data: Record<string, unknown>, context?: Record<string, unknown>) => Promise<void>;
592
+ custom: (resource: string, documentId: string, action: string, data?: Record<string, unknown>, context?: Record<string, unknown>) => Promise<void>;
593
+ };
594
+ /** Logger — structured logging */
595
+ log?: {
596
+ info: (...args: unknown[]) => void;
597
+ warn: (...args: unknown[]) => void;
598
+ error: (...args: unknown[]) => void;
599
+ debug: (...args: unknown[]) => void;
600
+ };
601
+ /** QueryCache — stale-while-revalidate data cache */
602
+ queryCache?: {
603
+ get: <T>(key: string) => Promise<{
604
+ data: T;
605
+ status: 'fresh' | 'stale' | 'miss';
606
+ }>;
607
+ set: <T>(key: string, data: T, config: {
608
+ staleTime?: number;
609
+ gcTime?: number;
610
+ tags?: string[];
611
+ }) => Promise<void>;
612
+ getResourceVersion: (resource: string) => Promise<number>;
613
+ bumpResourceVersion: (resource: string) => Promise<void>;
614
+ };
615
+ }
616
+ /**
617
+ * Request context passed to controller handlers
618
+ */
619
+ interface IRequestContext {
620
+ /** Route parameters (e.g., { id: '123' }) */
621
+ params: Record<string, string>;
622
+ /** Query string parameters */
623
+ query: Record<string, unknown>;
624
+ /** Request body */
625
+ body: unknown;
626
+ /** Authenticated user or null */
627
+ user: UserBase | null;
628
+ /** Request headers */
629
+ headers: Record<string, string | undefined>;
630
+ /** Organization ID (for multi-tenant apps) */
631
+ organizationId?: string;
632
+ /** Team ID (for team-scoped resources) */
633
+ teamId?: string;
634
+ /**
635
+ * Organization/auth context from middleware.
636
+ * Contains orgRoles, orgScope, organizationId, and any custom fields
637
+ * set by the auth adapter or org-scope plugin.
638
+ *
639
+ * @example
640
+ * ```typescript
641
+ * async create(req: IRequestContext) {
642
+ * const roles = req.context?.orgRoles ?? [];
643
+ * if (roles.includes('manager')) { ... }
644
+ * }
645
+ * ```
646
+ */
647
+ context?: RequestContext;
648
+ /** Internal metadata (includes context + Arc internals like _policyFilters, log) */
649
+ metadata?: Record<string, unknown>;
650
+ /**
651
+ * Fastify server accessor — publish events, log, and audit
652
+ * from any handler without switching to `wrapHandler: false`.
653
+ *
654
+ * @example
655
+ * ```typescript
656
+ * async reschedule(req: IRequestContext) {
657
+ * const result = await repo.reschedule(req.params.id, req.body);
658
+ * await req.server?.events?.publish('interview.rescheduled', { data: result });
659
+ * return { success: true, data: result };
660
+ * }
661
+ * ```
662
+ */
663
+ server?: ServerAccessor;
664
+ }
665
+ /**
666
+ * Standard response from controller handlers
667
+ */
668
+ interface IControllerResponse<T = unknown> {
669
+ /** Operation success status */
670
+ success: boolean;
671
+ /** Response data */
672
+ data?: T;
673
+ /** Error message (when success is false) */
674
+ error?: string;
675
+ /** HTTP status code (default: 200 for success, 400 for error) */
676
+ status?: number;
677
+ /** Additional metadata */
678
+ meta?: Record<string, unknown>;
679
+ /** Error details (for debugging) */
680
+ details?: Record<string, unknown>;
681
+ /** Custom response headers (e.g., X-Total-Count, Link, ETag) */
682
+ headers?: Record<string, string>;
683
+ }
684
+ /**
685
+ * Controller handler - Arc's standard pattern
686
+ *
687
+ * Receives a request context object, returns IControllerResponse.
688
+ * Use with `wrapHandler: true` in additionalRoutes.
689
+ *
690
+ * @example
691
+ * ```typescript
692
+ * const createProduct: ControllerHandler<Product> = async (req) => {
693
+ * const product = await productRepo.create(req.body);
694
+ * return { success: true, data: product, status: 201 };
695
+ * };
696
+ *
697
+ * additionalRoutes: [{
698
+ * method: 'POST',
699
+ * path: '/products',
700
+ * handler: createProduct,
701
+ * permissions: requireAuth(),
702
+ * wrapHandler: true, // Arc wraps this into Fastify handler
703
+ * }]
704
+ * ```
705
+ */
706
+ type ControllerHandler<T = unknown> = (req: IRequestContext) => Promise<IControllerResponse<T>>;
707
+ /**
708
+ * Fastify native handler
709
+ *
710
+ * Standard Fastify request/reply pattern.
711
+ * Use with `wrapHandler: false` in additionalRoutes.
712
+ *
713
+ * @example
714
+ * ```typescript
715
+ * const downloadFile: FastifyHandler = async (request, reply) => {
716
+ * const file = await getFile(request.params.id);
717
+ * reply.header('Content-Type', file.mimeType);
718
+ * return reply.send(file.buffer);
719
+ * };
720
+ *
721
+ * additionalRoutes: [{
722
+ * method: 'GET',
723
+ * path: '/files/:id/download',
724
+ * handler: downloadFile,
725
+ * permissions: requireAuth(),
726
+ * wrapHandler: false, // Use as-is, no wrapping
727
+ * }]
728
+ * ```
729
+ */
730
+ type FastifyHandler = (request: FastifyRequest, reply: FastifyReply) => Promise<void> | void;
731
+ /**
732
+ * Union type for route handlers
733
+ */
734
+ type RouteHandler = ControllerHandler | FastifyHandler;
735
+ /**
736
+ * Controller interface for CRUD operations (strict)
737
+ */
738
+ interface IController<TDoc = unknown> {
739
+ list(req: IRequestContext): Promise<IControllerResponse<{
740
+ docs: TDoc[];
741
+ total: number;
742
+ }>>;
743
+ get(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
744
+ create(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
745
+ update(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
746
+ delete(req: IRequestContext): Promise<IControllerResponse<{
747
+ message: string;
748
+ }>>;
749
+ }
750
+ /**
751
+ * Flexible controller interface - accepts controllers with any handler style
752
+ * Use this when your controller uses Fastify native handlers
753
+ */
754
+ interface ControllerLike {
755
+ list?: unknown;
756
+ get?: unknown;
757
+ create?: unknown;
758
+ update?: unknown;
759
+ delete?: unknown;
760
+ [key: string]: unknown;
761
+ }
762
+ //#endregion
763
+ //#region src/core/AccessControl.d.ts
764
+ interface AccessControlConfig {
765
+ /** Field name used for multi-tenant scoping (default: 'organizationId'). Set to `false` to disable org filtering. */
766
+ tenantField: string | false;
767
+ /** Primary key field name (default: '_id') */
768
+ idField: string;
769
+ /**
770
+ * Custom filter matching for policy enforcement.
771
+ * Provided by the DataAdapter for non-MongoDB databases (SQL, etc.).
772
+ * Falls back to built-in MongoDB-style matching if not provided.
773
+ */
774
+ matchesFilter?: (item: unknown, filters: Record<string, unknown>) => boolean;
775
+ }
776
+ /** Minimal repository interface for access-controlled fetch operations */
777
+ interface AccessControlRepository {
778
+ getById(id: string, options?: unknown): Promise<unknown>;
779
+ getOne?: (filter: AnyRecord, options?: unknown) => Promise<unknown>;
780
+ }
781
+ declare class AccessControl {
782
+ private readonly tenantField;
783
+ private readonly idField;
784
+ private readonly _adapterMatchesFilter?;
785
+ /** Patterns that indicate dangerous regex (nested quantifiers, excessive backtracking).
786
+ * Uses [^...] character classes instead of .+ to avoid backtracking in the detector itself. */
787
+ private static readonly DANGEROUS_REGEX;
788
+ /** Forbidden paths that could lead to prototype pollution */
789
+ private static readonly FORBIDDEN_PATHS;
790
+ constructor(config: AccessControlConfig);
791
+ /**
792
+ * Build filter for single-item operations (get/update/delete)
793
+ * Combines ID filter with policy/org filters for proper security enforcement
794
+ */
795
+ buildIdFilter(id: string, req: IRequestContext): AnyRecord;
796
+ /**
797
+ * Check if item matches policy filters (for get/update/delete operations)
798
+ * Validates that fetched item satisfies all policy constraints
799
+ *
800
+ * Delegates to adapter-provided matchesFilter if available (for SQL, etc.),
801
+ * otherwise falls back to built-in MongoDB-style matching.
802
+ */
803
+ checkPolicyFilters(item: AnyRecord, req: IRequestContext): boolean;
804
+ /**
805
+ * Check org/tenant scope for a document — uses configurable tenantField.
806
+ *
807
+ * SECURITY: When org scope is active (orgId present), documents that are
808
+ * missing the tenant field are DENIED by default. This prevents legacy or
809
+ * unscoped records from leaking across tenants.
810
+ */
811
+ checkOrgScope(item: AnyRecord | null, arcContext: ArcInternalMetadata | RequestContext | undefined): boolean;
812
+ /** Check ownership for update/delete (ownedByUser preset) */
813
+ checkOwnership(item: AnyRecord | null, req: IRequestContext): boolean;
814
+ /**
815
+ * Fetch a single document with full access control enforcement.
816
+ * Combines compound DB filter (ID + org + policy) with post-hoc fallback.
817
+ *
818
+ * Takes repository as a parameter to avoid coupling.
819
+ *
820
+ * Replaces the duplicated pattern in get/update/delete:
821
+ * buildIdFilter -> getOne (or getById + checkOrgScope + checkPolicyFilters)
822
+ */
823
+ fetchWithAccessControl<TDoc>(id: string, req: IRequestContext, repository: AccessControlRepository, queryOptions?: unknown): Promise<TDoc | null>;
824
+ /**
825
+ * Post-fetch access control validation for items fetched by non-ID queries
826
+ * (e.g., getBySlug, restore). Applies org scope, policy filters, and
827
+ * ownership checks — the same guarantees as fetchWithAccessControl.
828
+ */
829
+ validateItemAccess(item: AnyRecord | null, req: IRequestContext): boolean;
830
+ /** Extract typed Arc internal metadata from request */
831
+ private _meta;
832
+ /**
833
+ * Check if a value matches a MongoDB query operator
834
+ */
835
+ private matchesOperator;
836
+ /**
837
+ * Check if item matches a single filter condition
838
+ * Supports nested paths (e.g., "owner.id", "metadata.status")
839
+ */
840
+ private matchesFilter;
841
+ /**
842
+ * Built-in MongoDB-style policy filter matching.
843
+ * Supports: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $regex, $and, $or
844
+ */
845
+ private defaultMatchesPolicyFilters;
846
+ /**
847
+ * Get nested value from object using dot notation (e.g., "owner.id")
848
+ * Security: Validates path against forbidden patterns to prevent prototype pollution
849
+ */
850
+ private getNestedValue;
851
+ /**
852
+ * Create a safe RegExp from a string, guarding against ReDoS.
853
+ * Returns null if the pattern is invalid or dangerous.
854
+ */
855
+ private static safeRegex;
856
+ }
857
+ //#endregion
858
+ //#region src/core/BodySanitizer.d.ts
859
+ interface BodySanitizerConfig {
860
+ /** Schema options for field sanitization */
861
+ schemaOptions: RouteSchemaOptions;
862
+ }
863
+ declare class BodySanitizer {
864
+ private schemaOptions;
865
+ constructor(config: BodySanitizerConfig);
866
+ /**
867
+ * Strip readonly and system-managed fields from request body.
868
+ * Prevents clients from overwriting _id, timestamps, __v, etc.
869
+ *
870
+ * Also applies field-level write permissions when the request has
871
+ * field permission metadata.
872
+ */
873
+ sanitize(body: AnyRecord, _operation: "create" | "update", req?: IRequestContext, meta?: ArcInternalMetadata): AnyRecord;
874
+ }
875
+ //#endregion
876
+ //#region src/core/QueryResolver.d.ts
877
+ interface QueryResolverConfig {
878
+ /** Query parser instance (default: Arc built-in parser) */
879
+ queryParser?: QueryParserInterface;
880
+ /** Maximum limit for pagination (default: 100) */
881
+ maxLimit?: number;
882
+ /** Default limit for pagination (default: 20) */
883
+ defaultLimit?: number;
884
+ /** Default sort field (default: '-createdAt') */
885
+ defaultSort?: string;
886
+ /** Schema options for field sanitization */
887
+ schemaOptions?: RouteSchemaOptions;
888
+ /** Field name used for multi-tenant scoping (default: 'organizationId'). Set to `false` to disable. */
889
+ tenantField?: string | false;
890
+ }
891
+ declare class QueryResolver {
892
+ private queryParser;
893
+ private maxLimit;
894
+ private defaultLimit;
895
+ private defaultSort;
896
+ private schemaOptions;
897
+ private tenantField;
898
+ constructor(config?: QueryResolverConfig);
899
+ /**
900
+ * Resolve a request into parsed query options -- ONE parse per request.
901
+ * Combines what was previously _buildContext + _parseQueryOptions + _applyFilters.
902
+ */
903
+ resolve(req: IRequestContext, meta?: ArcInternalMetadata): ControllerQueryOptions;
904
+ /**
905
+ * Sanitize select — preserves the input format (string, array, or object).
906
+ * This is critical for db-agnostic support: MongoKit returns object projections,
907
+ * Mongoose uses space-separated strings, SQL adapters may use arrays.
908
+ */
909
+ private sanitizeSelectAny;
910
+ /** Sanitize populate fields */
911
+ private sanitizePopulate;
912
+ /** Sanitize advanced populate options against allowedPopulate */
913
+ private sanitizePopulateOptions;
914
+ /**
915
+ * Sanitize lookup/join options.
916
+ * If schemaOptions.query.allowedLookups is set, only those collections are allowed.
917
+ * Validates lookup structure to prevent injection.
918
+ */
919
+ private sanitizeLookups;
920
+ /** Get blocked fields from schema options */
921
+ private getBlockedFields;
922
+ }
923
+ //#endregion
924
+ //#region src/core/BaseController.d.ts
925
+ interface BaseControllerOptions {
926
+ /** Schema options for field sanitization */
927
+ schemaOptions?: RouteSchemaOptions;
928
+ /**
929
+ * Query parser instance.
930
+ * Default: Arc built-in query parser (adapter-agnostic).
931
+ * Swap in MongoKit QueryParser, pgkit parser, etc.
932
+ */
933
+ queryParser?: QueryParserInterface;
934
+ /** Maximum limit for pagination (default: 100) */
935
+ maxLimit?: number;
936
+ /** Default limit for pagination (default: 20) */
937
+ defaultLimit?: number;
938
+ /** Default sort field (default: '-createdAt') */
939
+ defaultSort?: string;
940
+ /** Resource name for hook execution (e.g., 'product' -> 'product.created') */
941
+ resourceName?: string;
942
+ /**
943
+ * Field name used for multi-tenant scoping (default: 'organizationId').
944
+ * Override to match your schema: 'workspaceId', 'tenantId', 'teamId', etc.
945
+ * Set to `false` to disable org filtering for platform-universal resources.
946
+ */
947
+ tenantField?: string | false;
948
+ /**
949
+ * Primary key field name (default: '_id').
950
+ * Override for non-MongoDB adapters (e.g., 'id' for SQL databases).
951
+ */
952
+ idField?: string;
953
+ /**
954
+ * Custom filter matching for policy enforcement.
955
+ * Provided by the DataAdapter for non-MongoDB databases (SQL, etc.).
956
+ * Falls back to built-in MongoDB-style matching if not provided.
957
+ */
958
+ matchesFilter?: (item: unknown, filters: Record<string, unknown>) => boolean;
959
+ /** Cache configuration for the resource */
960
+ cache?: ResourceCacheConfig;
961
+ /** Internal preset fields map (slug, tree, etc.) */
962
+ presetFields?: {
963
+ slugField?: string;
964
+ parentField?: string;
965
+ };
966
+ }
967
+ /**
968
+ * Framework-agnostic base controller implementing IController.
969
+ *
970
+ * Composes AccessControl, BodySanitizer, and QueryResolver for clean
971
+ * separation of concerns. CRUD methods delegate directly to these
972
+ * composed classes — no intermediate wrapper methods.
973
+ *
974
+ * @template TDoc - The document type
975
+ * @template TRepository - The repository type (defaults to RepositoryLike)
976
+ */
977
+ declare class BaseController<TDoc = AnyRecord, TRepository extends RepositoryLike = RepositoryLike> implements IController<TDoc> {
978
+ protected repository: TRepository;
979
+ protected schemaOptions: RouteSchemaOptions;
980
+ protected queryParser: QueryParserInterface;
981
+ protected maxLimit: number;
982
+ protected defaultLimit: number;
983
+ protected defaultSort: string;
984
+ protected resourceName?: string;
985
+ protected tenantField: string | false;
986
+ protected idField: string;
987
+ /** Composable access control (ID filtering, policy checks, org scope, ownership) */
988
+ readonly accessControl: AccessControl;
989
+ /** Composable body sanitization (field permissions, system fields) */
990
+ readonly bodySanitizer: BodySanitizer;
991
+ /** Composable query resolution (parsing, pagination, sort, select/populate) */
992
+ readonly queryResolver: QueryResolver;
993
+ private _matchesFilter?;
994
+ private _presetFields;
995
+ private _cacheConfig?;
996
+ constructor(repository: TRepository, options?: BaseControllerOptions);
997
+ /**
998
+ * Get the tenant field name if multi-tenant scoping is enabled.
999
+ * Returns `undefined` when `tenantField` is `false` (platform-universal mode).
1000
+ *
1001
+ * Use this in subclass overrides instead of accessing `this.tenantField` directly
1002
+ * to avoid TypeScript indexing errors with `string | false`.
1003
+ */
1004
+ protected getTenantField(): string | undefined;
1005
+ /** Extract typed Arc internal metadata from request */
1006
+ private meta;
1007
+ /** Get hook system from request context (instance-scoped) */
1008
+ private getHooks;
1009
+ /** Resolve cache config for a specific operation, merging per-op overrides */
1010
+ private resolveCacheConfig;
1011
+ /**
1012
+ * Extract user/org IDs from request for cache key scoping.
1013
+ * Only includes orgId when this resource uses tenant-scoped data (tenantField is set).
1014
+ * Universal resources (tenantField: false) get shared cache keys to avoid fragmentation.
1015
+ */
1016
+ private cacheScope;
1017
+ list(req: IRequestContext): Promise<IControllerResponse<PaginatedResult<TDoc>>>;
1018
+ /** Execute list query through hooks (extracted for cache revalidation) */
1019
+ private executeListQuery;
1020
+ get(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
1021
+ /** Execute get query through hooks (extracted for cache revalidation) */
1022
+ private executeGetQuery;
1023
+ create(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
1024
+ update(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
1025
+ delete(req: IRequestContext): Promise<IControllerResponse<{
1026
+ message: string;
1027
+ id?: string;
1028
+ soft?: boolean;
1029
+ }>>;
1030
+ getBySlug(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
1031
+ getDeleted(req: IRequestContext): Promise<IControllerResponse<PaginatedResult<TDoc>>>;
1032
+ restore(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
1033
+ getTree(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
1034
+ getChildren(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
1035
+ bulkCreate(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
1036
+ bulkUpdate(req: IRequestContext): Promise<IControllerResponse<{
1037
+ matchedCount: number;
1038
+ modifiedCount: number;
1039
+ }>>;
1040
+ bulkDelete(req: IRequestContext): Promise<IControllerResponse<{
1041
+ deletedCount: number;
1042
+ }>>;
1043
+ }
1044
+ //#endregion
1045
+ //#region src/types/index.d.ts
1046
+ declare module 'fastify' {
1047
+ interface FastifyRequest {
1048
+ /** Request scope — set by auth adapter, read by permissions/presets/guards */
1049
+ scope: RequestScope;
1050
+ /**
1051
+ * Current user — set by auth adapter (Better Auth, JWT, custom).
1052
+ * `undefined` on public routes (`auth: false`) or unauthenticated requests.
1053
+ * Guard with `if (request.user)` on routes that allow anonymous access.
1054
+ *
1055
+ * Note: kept as required (not `user?`) because `@fastify/jwt` declares it
1056
+ * as required — declaration merges must have identical modifiers.
1057
+ * The `| undefined` in the type achieves the same DX: TypeScript will
1058
+ * flag unguarded access like `request.user.id` as possibly undefined.
1059
+ */
1060
+ user: Record<string, unknown> | undefined;
1061
+ /** Policy-injected query filters (e.g. ownership, org-scoping) */
1062
+ _policyFilters?: Record<string, unknown>;
1063
+ /** Field mask — fields to include/exclude in responses */
1064
+ fieldMask?: {
1065
+ include?: string[];
1066
+ exclude?: string[];
1067
+ };
1068
+ /** Arbitrary policy metadata for downstream consumers */
1069
+ policyMetadata?: Record<string, unknown>;
1070
+ /** Document loaded by policy middleware for ownership checks */
1071
+ document?: unknown;
1072
+ /** Ownership check context (field name + user field) */
1073
+ _ownershipCheck?: Record<string, unknown>;
1074
+ }
1075
+ }
1076
+ type AnyRecord = Record<string, unknown>;
1077
+ /** MongoDB ObjectId — accepts string or any object with a `toString()` (e.g. mongoose ObjectId). */
1078
+ type ObjectId = string | {
1079
+ toString(): string;
1080
+ };
1081
+ /**
1082
+ * Flexible user type that accepts any object with id/ID properties.
1083
+ * Use this instead of `any` when dealing with user objects.
1084
+ * Re-exports UserBase from permissions module for convenience.
1085
+ * The actual user structure is defined by your app's auth system.
1086
+ */
1087
+ type UserLike = UserBase & {
1088
+ /** User email (optional) */email?: string;
1089
+ };
1090
+ /**
1091
+ * Extract user ID from a user object (supports both id and _id)
1092
+ */
1093
+ declare function getUserId(user: UserLike | null | undefined): string | undefined;
1094
+ type CrudController<TDoc> = IController<TDoc>;
1095
+ interface ApiResponse<T = unknown> {
1096
+ success: boolean;
1097
+ data?: T;
1098
+ error?: string;
1099
+ message?: string;
1100
+ meta?: Record<string, unknown>;
1101
+ }
1102
+ interface UserOrganization {
1103
+ userId: string;
1104
+ organizationId: string;
1105
+ [key: string]: unknown;
1106
+ }
1107
+ interface JWTPayload {
1108
+ sub: string;
1109
+ [key: string]: unknown;
1110
+ }
1111
+ interface RequestContext {
1112
+ operation?: string;
1113
+ user?: unknown;
1114
+ filters?: Record<string, unknown>;
1115
+ [key: string]: unknown;
1116
+ }
1117
+ /**
1118
+ * Internal metadata shape injected by Arc's Fastify adapter.
1119
+ * Extends RequestContext with known internal fields so controllers
1120
+ * can access them without `as AnyRecord` casts.
1121
+ */
1122
+ interface ArcInternalMetadata extends RequestContext {
1123
+ /** Policy filters from permission middleware */
1124
+ _policyFilters?: Record<string, unknown>;
1125
+ /** Request scope from scope resolution */
1126
+ _scope?: RequestScope;
1127
+ /** Ownership check config from ownedByUser preset */
1128
+ _ownershipCheck?: {
1129
+ field: string;
1130
+ userId: string;
1131
+ };
1132
+ /** Arc instance references (hooks, field permissions, etc.) */
1133
+ arc?: {
1134
+ hooks?: HookSystem;
1135
+ fields?: FieldPermissionMap;
1136
+ [key: string]: unknown;
1137
+ };
1138
+ }
1139
+ /**
1140
+ * Controller-level query options - parsed from request query string
1141
+ * Includes pagination, filtering, and context data
1142
+ */
1143
+ interface ControllerQueryOptions {
1144
+ page?: number;
1145
+ limit?: number;
1146
+ sort?: string | Record<string, 1 | -1>;
1147
+ /** Simple populate (comma-separated string or array) */
1148
+ populate?: string | string[] | Record<string, unknown>;
1149
+ /**
1150
+ * Advanced populate options (Mongoose-compatible)
1151
+ * When set, takes precedence over simple `populate`
1152
+ */
1153
+ populateOptions?: PopulateOption[];
1154
+ /**
1155
+ * Lookup/join options (database-agnostic).
1156
+ * MongoKit maps these to $lookup aggregation pipeline stages.
1157
+ * Future adapters (PrismaKit, PgKit) would map to SQL JOINs.
1158
+ *
1159
+ * @example
1160
+ * URL: ?lookup[category][from]=categories&lookup[category][localField]=categorySlug&lookup[category][foreignField]=slug
1161
+ */
1162
+ lookups?: LookupOption[];
1163
+ select?: string | string[] | Record<string, 0 | 1>;
1164
+ filters?: Record<string, unknown>;
1165
+ search?: string;
1166
+ lean?: boolean;
1167
+ after?: string;
1168
+ user?: unknown;
1169
+ context?: Record<string, unknown>;
1170
+ /** Allow additional options */
1171
+ [key: string]: unknown;
1172
+ }
1173
+ /**
1174
+ * Database-agnostic lookup/join option.
1175
+ * Parsed from URL: ?lookup[alias][from]=collection&lookup[alias][localField]=field&lookup[alias][foreignField]=field
1176
+ *
1177
+ * MongoKit maps this to MongoDB $lookup aggregation.
1178
+ * Future adapters would map to SQL JOINs or Prisma includes.
1179
+ */
1180
+ interface LookupOption {
1181
+ /** Source collection/table to join from */
1182
+ from: string;
1183
+ /** Local field to match on */
1184
+ localField: string;
1185
+ /** Foreign field to match on */
1186
+ foreignField: string;
1187
+ /** Alias for the joined data (defaults to the lookup key) */
1188
+ as?: string;
1189
+ /** Return a single object instead of array (default: false) */
1190
+ single?: boolean;
1191
+ /** Field selection on the joined collection (comma-separated) */
1192
+ select?: string;
1193
+ }
1194
+ /**
1195
+ * Mongoose-compatible populate option for advanced field selection
1196
+ * Used when you need to select specific fields from populated documents
1197
+ *
1198
+ * @example
1199
+ * ```typescript
1200
+ * // URL: ?populate[author][select]=name,email
1201
+ * // Generates: { path: 'author', select: 'name email' }
1202
+ * ```
1203
+ */
1204
+ interface PopulateOption {
1205
+ /** Field path to populate */
1206
+ path: string;
1207
+ /** Fields to select (space-separated) */
1208
+ select?: string;
1209
+ /** Filter conditions for populated documents */
1210
+ match?: Record<string, unknown>;
1211
+ /** Query options (limit, sort, skip) */
1212
+ options?: {
1213
+ limit?: number;
1214
+ sort?: Record<string, 1 | -1>;
1215
+ skip?: number;
1216
+ };
1217
+ /** Nested populate configuration */
1218
+ populate?: PopulateOption;
1219
+ }
1220
+ /**
1221
+ * Parsed query result from QueryParser
1222
+ * Includes pagination, sorting, filtering, etc.
1223
+ *
1224
+ * The index signature allows custom query parsers (like MongoKit's QueryParser)
1225
+ * to add additional fields without breaking Arc's type system.
1226
+ */
1227
+ interface ParsedQuery {
1228
+ filters?: Record<string, unknown>;
1229
+ limit?: number;
1230
+ sort?: string | Record<string, 1 | -1>;
1231
+ /** Simple populate (comma-separated string or array) */
1232
+ populate?: string | string[] | Record<string, unknown>;
1233
+ /**
1234
+ * Advanced populate options (Mongoose-compatible)
1235
+ * When set, takes precedence over simple `populate`
1236
+ * @example [{ path: 'author', select: 'name email' }]
1237
+ */
1238
+ populateOptions?: PopulateOption[];
1239
+ /**
1240
+ * Lookup/join options from MongoKit QueryParser or custom parsers.
1241
+ * Maps to $lookup in MongoDB, JOINs in SQL adapters.
1242
+ */
1243
+ lookups?: LookupOption[];
1244
+ search?: string;
1245
+ page?: number;
1246
+ after?: string;
1247
+ select?: string | string[] | Record<string, 0 | 1>;
1248
+ /** Allow additional fields from custom query parsers */
1249
+ [key: string]: unknown;
1250
+ }
1251
+ /**
1252
+ * Query Parser Interface
1253
+ * Implement this to create custom query parsers
1254
+ *
1255
+ * @example MongoKit QueryParser
1256
+ * ```typescript
1257
+ * import { QueryParser } from '@classytic/mongokit';
1258
+ * const queryParser = new QueryParser();
1259
+ * ```
1260
+ */
1261
+ interface QueryParserInterface {
1262
+ parse(query: Record<string, unknown> | null | undefined): ParsedQuery;
1263
+ /**
1264
+ * Optional: Export OpenAPI schema for query parameters
1265
+ * Use this to document query parameters in OpenAPI/Swagger
1266
+ */
1267
+ getQuerySchema?(): {
1268
+ type: 'object';
1269
+ properties: Record<string, unknown>;
1270
+ required?: string[];
1271
+ };
1272
+ /**
1273
+ * Optional: Allowed filter fields whitelist.
1274
+ * When set, MCP auto-derives `filterableFields` from this
1275
+ * if `schemaOptions.filterableFields` is not explicitly configured.
1276
+ */
1277
+ allowedFilterFields?: readonly string[];
1278
+ /**
1279
+ * Optional: Allowed filter operators whitelist.
1280
+ * Used by MCP to enrich list tool descriptions with available operators.
1281
+ * Values are human-readable keys: 'eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', etc.
1282
+ */
1283
+ allowedOperators?: readonly string[];
1284
+ /**
1285
+ * Optional: Allowed sort fields whitelist.
1286
+ * Used by MCP to describe available sort options in list tool descriptions.
1287
+ */
1288
+ allowedSortFields?: readonly string[];
1289
+ }
1290
+ interface FastifyRequestExtras {
1291
+ user?: Record<string, unknown>;
1292
+ }
1293
+ interface RequestWithExtras extends FastifyRequest {
1294
+ /**
1295
+ * Arc metadata - set by createCrudRouter
1296
+ * Contains resource configuration and schema options
1297
+ */
1298
+ arc?: {
1299
+ resourceName?: string;
1300
+ schemaOptions?: RouteSchemaOptions;
1301
+ permissions?: ResourcePermissions;
1302
+ };
1303
+ context?: Record<string, unknown>;
1304
+ _policyFilters?: Record<string, unknown>;
1305
+ fieldMask?: {
1306
+ include?: string[];
1307
+ exclude?: string[];
1308
+ };
1309
+ _ownershipCheck?: Record<string, unknown>;
1310
+ }
1311
+ type FastifyWithAuth = FastifyInstance & {
1312
+ authenticate: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
1313
+ };
1314
+ /**
1315
+ * Arc core decorator interface
1316
+ * Added by arcCorePlugin to provide instance-scoped hooks and registry
1317
+ */
1318
+ interface ArcDecorator {
1319
+ /** Instance-scoped hook system */
1320
+ hooks: HookSystem;
1321
+ /** Instance-scoped resource registry */
1322
+ registry: ResourceRegistry;
1323
+ /** Whether event emission is enabled */
1324
+ emitEvents: boolean;
1325
+ }
1326
+ /**
1327
+ * Events decorator interface
1328
+ * Added by eventPlugin to provide event pub/sub
1329
+ */
1330
+ interface EventsDecorator {
1331
+ /** Publish an event */
1332
+ publish: <T>(type: string, payload: T, meta?: Partial<{
1333
+ id: string;
1334
+ timestamp: Date;
1335
+ }>) => Promise<void>;
1336
+ /** Subscribe to events */
1337
+ subscribe: (pattern: string, handler: (event: unknown) => void | Promise<void>) => Promise<() => void>;
1338
+ /** Get transport name */
1339
+ transportName: string;
1340
+ }
1341
+ /**
1342
+ * Fastify instance with Arc decorators
1343
+ * Arc adds these decorators via plugins/presets
1344
+ */
1345
+ type FastifyWithDecorators = FastifyInstance & {
1346
+ arc?: ArcDecorator;
1347
+ events?: EventsDecorator;
1348
+ authenticate?: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
1349
+ optionalAuthenticate?: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
1350
+ organizationScoped?: (options?: {
1351
+ required?: boolean;
1352
+ }) => RouteHandlerMethod;
1353
+ [key: string]: unknown;
1354
+ };
1355
+ interface OwnershipCheck {
1356
+ field: string;
1357
+ userField?: string;
1358
+ }
1359
+ /**
1360
+ * Per-resource rate limit configuration.
1361
+ *
1362
+ * Applied to all routes of the resource when `@fastify/rate-limit` is registered
1363
+ * on the Fastify instance. Set to `false` to explicitly disable rate limiting
1364
+ * for a resource even when a global rate limit is configured.
1365
+ *
1366
+ * @example
1367
+ * ```typescript
1368
+ * defineResource({
1369
+ * name: 'product',
1370
+ * rateLimit: { max: 100, timeWindow: '1 minute' },
1371
+ * });
1372
+ * ```
1373
+ */
1374
+ /**
1375
+ * Per-resource cache configuration for QueryCache.
1376
+ * Enables stale-while-revalidate, auto-invalidation on mutations,
1377
+ * and cross-resource tag-based invalidation.
1378
+ */
1379
+ interface ResourceCacheConfig {
1380
+ /** Seconds data is "fresh" (no revalidation). Default: 0 */
1381
+ staleTime?: number;
1382
+ /** Seconds stale data stays cached (SWR window). Default: 60 */
1383
+ gcTime?: number;
1384
+ /** Per-operation overrides */
1385
+ list?: {
1386
+ staleTime?: number;
1387
+ gcTime?: number;
1388
+ };
1389
+ byId?: {
1390
+ staleTime?: number;
1391
+ gcTime?: number;
1392
+ };
1393
+ /** Tags for cross-resource invalidation grouping */
1394
+ tags?: string[];
1395
+ /**
1396
+ * Cross-resource invalidation: event pattern → tag targets.
1397
+ * When matched event fires, all caches with those tags are invalidated.
1398
+ * @example { 'category.*': ['catalog'] }
1399
+ */
1400
+ invalidateOn?: Record<string, string[]>;
1401
+ /** Disable caching for this resource */
1402
+ disabled?: boolean;
1403
+ }
1404
+ interface RateLimitConfig {
1405
+ /** Maximum number of requests allowed within the time window */
1406
+ max: number;
1407
+ /** Time window for rate limiting (e.g., '1 minute', '15 seconds', '1 hour') */
1408
+ timeWindow: string;
1409
+ }
1410
+ interface ResourceConfig<TDoc = AnyRecord> {
1411
+ name: string;
1412
+ displayName?: string;
1413
+ tag?: string;
1414
+ prefix?: string;
1415
+ adapter?: DataAdapter<TDoc>;
1416
+ /** Controller instance - accepts any object with CRUD methods */
1417
+ controller?: IController<TDoc> | ControllerLike;
1418
+ queryParser?: unknown;
1419
+ permissions?: ResourcePermissions;
1420
+ schemaOptions?: RouteSchemaOptions;
1421
+ openApiSchemas?: OpenApiSchemas;
1422
+ customSchemas?: Partial<CrudSchemas>;
1423
+ presets?: Array<string | PresetResult | {
1424
+ name: string;
1425
+ [key: string]: unknown;
1426
+ }>;
1427
+ hooks?: ResourceHooks;
1428
+ /**
1429
+ * Functional pipeline — guards, transforms, and interceptors.
1430
+ * Can be a flat array (all operations) or per-operation map.
1431
+ *
1432
+ * @example
1433
+ * ```typescript
1434
+ * import { pipe, guard, transform, intercept } from '@classytic/arc';
1435
+ *
1436
+ * resource('product', {
1437
+ * pipe: pipe(isActive, slugify, timing),
1438
+ * // OR per-operation:
1439
+ * pipe: { create: pipe(isActive, slugify), list: pipe(timing) },
1440
+ * });
1441
+ * ```
1442
+ */
1443
+ pipe?: PipelineConfig;
1444
+ /**
1445
+ * Field-level permissions — control visibility and writability per role.
1446
+ *
1447
+ * @example
1448
+ * ```typescript
1449
+ * import { fields } from '@classytic/arc';
1450
+ * fields: {
1451
+ * salary: fields.visibleTo(['admin', 'hr']),
1452
+ * password: fields.hidden(),
1453
+ * }
1454
+ * ```
1455
+ */
1456
+ fields?: FieldPermissionMap;
1457
+ middlewares?: MiddlewareConfig;
1458
+ additionalRoutes?: AdditionalRoute[];
1459
+ disableCrud?: boolean;
1460
+ disableDefaultRoutes?: boolean;
1461
+ disabledRoutes?: CrudRouteKey[];
1462
+ /**
1463
+ * Field name used for multi-tenant scoping (default: 'organizationId').
1464
+ * Override to match your schema: 'workspaceId', 'tenantId', 'teamId', etc.
1465
+ * Takes effect when org context is present (via multiTenant preset).
1466
+ */
1467
+ tenantField?: string | false;
1468
+ /**
1469
+ * Primary key field name (default: '_id').
1470
+ * Override for non-MongoDB adapters (e.g., 'id' for SQL databases).
1471
+ */
1472
+ idField?: string;
1473
+ module?: string;
1474
+ events?: Record<string, EventDefinition>;
1475
+ skipValidation?: boolean;
1476
+ skipRegistry?: boolean;
1477
+ _appliedPresets?: string[];
1478
+ /** HTTP method for update routes. Default: 'PATCH' */
1479
+ updateMethod?: 'PUT' | 'PATCH' | 'both';
1480
+ /**
1481
+ * Per-resource rate limiting.
1482
+ * Requires `@fastify/rate-limit` to be registered on the Fastify instance.
1483
+ * Set to `false` to disable rate limiting for this resource.
1484
+ */
1485
+ rateLimit?: RateLimitConfig | false;
1486
+ /**
1487
+ * QueryCache configuration for this resource.
1488
+ * Enables stale-while-revalidate and auto-invalidation.
1489
+ * Requires `queryCachePlugin` to be registered.
1490
+ */
1491
+ cache?: ResourceCacheConfig;
1492
+ }
1493
+ /**
1494
+ * Resource-level permissions
1495
+ * ONLY PermissionCheck functions allowed - no string arrays
1496
+ */
1497
+ interface ResourcePermissions {
1498
+ list?: PermissionCheck;
1499
+ get?: PermissionCheck;
1500
+ create?: PermissionCheck;
1501
+ update?: PermissionCheck;
1502
+ delete?: PermissionCheck;
1503
+ }
1504
+ interface ResourceHooks {
1505
+ beforeCreate?: (data: AnyRecord) => Promise<AnyRecord> | AnyRecord;
1506
+ afterCreate?: (doc: AnyRecord) => Promise<void> | void;
1507
+ beforeUpdate?: (id: string, data: AnyRecord) => Promise<AnyRecord> | AnyRecord;
1508
+ afterUpdate?: (doc: AnyRecord) => Promise<void> | void;
1509
+ beforeDelete?: (id: string) => Promise<void> | void;
1510
+ afterDelete?: (id: string) => Promise<void> | void;
1511
+ }
1512
+ /**
1513
+ * Additional route definition for custom endpoints
1514
+ */
1515
+ interface AdditionalRoute {
1516
+ /** HTTP method */
1517
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
1518
+ /** Route path (relative to resource prefix) */
1519
+ path: string;
1520
+ /**
1521
+ * Handler - string (controller method name) or function
1522
+ * Function can be Fastify handler or (request, reply) => Promise<unknown>
1523
+ */
1524
+ handler: string | RouteHandlerMethod | ((request: FastifyRequest<any>, reply: FastifyReply) => unknown);
1525
+ /** Permission check - REQUIRED */
1526
+ permissions: PermissionCheck;
1527
+ /**
1528
+ * Handler type - REQUIRED, no auto-detection
1529
+ * true = ControllerHandler (receives context object)
1530
+ * false = FastifyHandler (receives request, reply)
1531
+ */
1532
+ wrapHandler: boolean;
1533
+ /**
1534
+ * Logical operation name for pipeline keys and permission actions.
1535
+ * Defaults to handler name (string handlers) or method+path slug.
1536
+ * Prevents collisions when multiple routes share the same HTTP method.
1537
+ *
1538
+ * @example
1539
+ * operation: 'listDeleted' // Used as pipeline key and permission action
1540
+ * operation: 'restore'
1541
+ */
1542
+ operation?: string;
1543
+ /** OpenAPI summary */
1544
+ summary?: string;
1545
+ /** OpenAPI description */
1546
+ description?: string;
1547
+ /** OpenAPI tags */
1548
+ tags?: string[];
1549
+ /**
1550
+ * Custom route-level middleware
1551
+ * Can be an array of handlers or a function that receives fastify and returns handlers
1552
+ * @example
1553
+ * // Direct array
1554
+ * preHandler: [myMiddleware]
1555
+ * // Function that receives fastify (for accessing decorators)
1556
+ * preHandler: (fastify) => [fastify.customerContext({ required: true })]
1557
+ */
1558
+ preHandler?: RouteHandlerMethod[] | ((fastify: FastifyInstance) => RouteHandlerMethod[]);
1559
+ /** Fastify route schema */
1560
+ schema?: Record<string, unknown>;
1561
+ /**
1562
+ * MCP handler for routes with `wrapHandler: false`.
1563
+ * When set, this route becomes an MCP tool without needing `wrapHandler: true`.
1564
+ * The HTTP handler stays a plain Fastify handler; MCP gets a parallel entry point.
1565
+ *
1566
+ * @example
1567
+ * ```typescript
1568
+ * additionalRoutes: [{
1569
+ * method: 'GET',
1570
+ * path: '/stats',
1571
+ * handler: (req, reply) => reply.send(getStats()),
1572
+ * wrapHandler: false,
1573
+ * permissions: isAuthenticated,
1574
+ * mcpHandler: async (input) => ({
1575
+ * content: [{ type: 'text', text: JSON.stringify(await getStats()) }],
1576
+ * }),
1577
+ * }]
1578
+ * ```
1579
+ */
1580
+ mcpHandler?: (input: Record<string, unknown>) => Promise<{
1581
+ content: Array<{
1582
+ type: string;
1583
+ text: string;
1584
+ }>;
1585
+ isError?: boolean;
1586
+ }>;
1587
+ }
1588
+ interface RouteSchemaOptions {
1589
+ hiddenFields?: string[];
1590
+ readonlyFields?: string[];
1591
+ requiredFields?: string[];
1592
+ optionalFields?: string[];
1593
+ excludeFields?: string[];
1594
+ /**
1595
+ * Fields allowed for filtering in list operations.
1596
+ * Used by MCP tool generation to build the list tool's input schema.
1597
+ * If not set and using a QueryParser with `allowedFilterFields`, MCP auto-derives from it.
1598
+ */
1599
+ filterableFields?: string[];
1600
+ fieldRules?: Record<string, {
1601
+ systemManaged?: boolean;
1602
+ immutable?: boolean;
1603
+ immutableAfterCreate?: boolean;
1604
+ optional?: boolean;
1605
+ [key: string]: unknown;
1606
+ }>;
1607
+ query?: Record<string, unknown>;
1608
+ }
1609
+ interface FieldRule {
1610
+ field: string;
1611
+ required?: boolean;
1612
+ readonly?: boolean;
1613
+ hidden?: boolean;
1614
+ }
1615
+ /**
1616
+ * CRUD Route Schemas (Fastify Native Format)
1617
+ *
1618
+ * @example
1619
+ * {
1620
+ * list: {
1621
+ * querystring: { type: 'object', properties: { page: { type: 'number' } } },
1622
+ * response: { 200: { type: 'object', properties: { docs: { type: 'array' } } } }
1623
+ * },
1624
+ * create: {
1625
+ * body: { type: 'object', properties: { name: { type: 'string' } } },
1626
+ * response: { 201: { type: 'object' } }
1627
+ * }
1628
+ * }
1629
+ */
1630
+ interface CrudSchemas {
1631
+ /** GET / - List all resources */
1632
+ list?: {
1633
+ querystring?: Record<string, unknown>;
1634
+ response?: Record<number, unknown>;
1635
+ [key: string]: unknown;
1636
+ };
1637
+ /** GET /:id - Get single resource */
1638
+ get?: {
1639
+ params?: Record<string, unknown>;
1640
+ response?: Record<number, unknown>;
1641
+ [key: string]: unknown;
1642
+ };
1643
+ /** POST / - Create resource */
1644
+ create?: {
1645
+ body?: Record<string, unknown>;
1646
+ response?: Record<number, unknown>;
1647
+ [key: string]: unknown;
1648
+ };
1649
+ /** PATCH /:id - Update resource */
1650
+ update?: {
1651
+ params?: Record<string, unknown>;
1652
+ body?: Record<string, unknown>;
1653
+ response?: Record<number, unknown>;
1654
+ [key: string]: unknown;
1655
+ };
1656
+ /** DELETE /:id - Delete resource */
1657
+ delete?: {
1658
+ params?: Record<string, unknown>;
1659
+ response?: Record<number, unknown>;
1660
+ [key: string]: unknown;
1661
+ };
1662
+ [key: string]: unknown;
1663
+ }
1664
+ interface OpenApiSchemas {
1665
+ entity?: unknown;
1666
+ createBody?: unknown;
1667
+ updateBody?: unknown;
1668
+ params?: unknown;
1669
+ listQuery?: unknown;
1670
+ /**
1671
+ * Explicit response schema for OpenAPI documentation.
1672
+ * If provided, this will be used as-is for the response schema.
1673
+ * If not provided, response schema is auto-generated from createBody.
1674
+ *
1675
+ * Note: This is for OpenAPI docs only - does NOT affect Fastify serialization.
1676
+ *
1677
+ * @example
1678
+ * response: {
1679
+ * type: 'object',
1680
+ * properties: {
1681
+ * _id: { type: 'string' },
1682
+ * name: { type: 'string' },
1683
+ * email: { type: 'string' },
1684
+ * // Exclude password, include virtuals
1685
+ * fullName: { type: 'string' },
1686
+ * }
1687
+ * }
1688
+ */
1689
+ response?: unknown;
1690
+ [key: string]: unknown;
1691
+ }
1692
+ /** Handler for middleware functions */
1693
+ type MiddlewareHandler = (request: RequestWithExtras, reply: FastifyReply) => Promise<unknown>;
1694
+ type CrudRouteKey = 'list' | 'get' | 'create' | 'update' | 'delete';
1695
+ interface MiddlewareConfig {
1696
+ list?: MiddlewareHandler[];
1697
+ get?: MiddlewareHandler[];
1698
+ create?: MiddlewareHandler[];
1699
+ update?: MiddlewareHandler[];
1700
+ delete?: MiddlewareHandler[];
1701
+ [key: string]: MiddlewareHandler[] | undefined;
1702
+ }
1703
+ /**
1704
+ * JWT utilities provided to authenticator
1705
+ * Arc provides these helpers, app uses them as needed
1706
+ */
1707
+ interface JwtContext {
1708
+ /** Verify a JWT token and return decoded payload */
1709
+ verify: <T = Record<string, unknown>>(token: string) => T;
1710
+ /** Sign a payload and return JWT token */
1711
+ sign: (payload: Record<string, unknown>, options?: {
1712
+ expiresIn?: string;
1713
+ }) => string;
1714
+ /** Decode without verification (for inspection) */
1715
+ decode: <T = Record<string, unknown>>(token: string) => T | null;
1716
+ }
1717
+ /**
1718
+ * Context passed to app's authenticator function
1719
+ */
1720
+ interface AuthenticatorContext {
1721
+ /** JWT utilities (available if jwt.secret provided) */
1722
+ jwt: JwtContext | null;
1723
+ /** Fastify instance for advanced use cases */
1724
+ fastify: FastifyInstance;
1725
+ }
1726
+ /**
1727
+ * App-provided authenticator function
1728
+ *
1729
+ * Arc calls this for every non-public route.
1730
+ * App has FULL control over authentication logic.
1731
+ *
1732
+ * @example
1733
+ * ```typescript
1734
+ * // Simple JWT auth
1735
+ * authenticate: async (request, { jwt }) => {
1736
+ * const token = request.headers.authorization?.split(' ')[1];
1737
+ * if (!token || !jwt) return null;
1738
+ * const decoded = jwt.verify(token);
1739
+ * return userRepo.findById(decoded.id);
1740
+ * }
1741
+ *
1742
+ * // Multi-strategy (JWT + API Key)
1743
+ * authenticate: async (request, { jwt }) => {
1744
+ * const apiKey = request.headers['x-api-key'];
1745
+ * if (apiKey) {
1746
+ * const result = await apiKeyService.verify(apiKey);
1747
+ * if (result) return { _id: result.userId, isApiKey: true };
1748
+ * }
1749
+ * const token = request.headers.authorization?.split(' ')[1];
1750
+ * if (token && jwt) {
1751
+ * const decoded = jwt.verify(token);
1752
+ * return userRepo.findById(decoded.id);
1753
+ * }
1754
+ * return null;
1755
+ * }
1756
+ * ```
1757
+ */
1758
+ type Authenticator = (request: FastifyRequest, context: AuthenticatorContext) => Promise<unknown | null> | unknown | null;
1759
+ /**
1760
+ * Token pair returned by issueTokens helper
1761
+ */
1762
+ interface TokenPair {
1763
+ /** Access token (JWT) */
1764
+ accessToken: string;
1765
+ /** Refresh token (JWT with longer expiry) */
1766
+ refreshToken?: string;
1767
+ /** Access token expiry in seconds */
1768
+ expiresIn: number;
1769
+ /** Refresh token expiry in seconds */
1770
+ refreshExpiresIn?: number;
1771
+ /** Token type (always 'Bearer') */
1772
+ tokenType: 'Bearer';
1773
+ }
1774
+ /**
1775
+ * Auth helpers available on fastify.auth
1776
+ *
1777
+ * @example
1778
+ * ```typescript
1779
+ * // In login handler
1780
+ * const user = await userRepo.findByEmail(email);
1781
+ * if (!user || !await bcrypt.compare(password, user.password)) {
1782
+ * return reply.code(401).send({ error: 'Invalid credentials' });
1783
+ * }
1784
+ *
1785
+ * const tokens = fastify.auth.issueTokens({
1786
+ * id: user._id,
1787
+ * email: user.email,
1788
+ * role: user.role,
1789
+ * });
1790
+ *
1791
+ * return { success: true, ...tokens, user };
1792
+ * ```
1793
+ */
1794
+ interface AuthHelpers {
1795
+ /** JWT utilities (if configured) */
1796
+ jwt: JwtContext | null;
1797
+ /**
1798
+ * Issue access + refresh tokens for a user
1799
+ * App calls this after validating credentials
1800
+ */
1801
+ issueTokens: (payload: Record<string, unknown>, options?: {
1802
+ expiresIn?: string;
1803
+ refreshExpiresIn?: string;
1804
+ }) => TokenPair;
1805
+ /**
1806
+ * Verify a refresh token and return decoded payload
1807
+ */
1808
+ verifyRefreshToken: <T = Record<string, unknown>>(token: string) => T;
1809
+ }
1810
+ interface ServiceContext {
1811
+ user?: unknown;
1812
+ requestId?: string;
1813
+ select?: string[] | Record<string, 0 | 1>;
1814
+ populate?: string | string[];
1815
+ lean?: boolean;
1816
+ }
1817
+ interface PresetHook {
1818
+ operation: 'create' | 'update' | 'delete' | 'read' | 'list';
1819
+ phase: 'before' | 'after';
1820
+ handler: (ctx: AnyRecord) => void | Promise<void> | AnyRecord | Promise<AnyRecord>;
1821
+ priority?: number;
1822
+ }
1823
+ interface PresetResult {
1824
+ name: string;
1825
+ additionalRoutes?: AdditionalRoute[] | ((permissions: ResourcePermissions) => AdditionalRoute[]);
1826
+ middlewares?: MiddlewareConfig;
1827
+ schemaOptions?: RouteSchemaOptions;
1828
+ controllerOptions?: Record<string, unknown>;
1829
+ hooks?: PresetHook[];
1830
+ }
1831
+ type PresetFunction = (config: ResourceConfig) => PresetResult;
1832
+ interface GracefulShutdownOptions {
1833
+ timeout?: number;
1834
+ onShutdown?: () => Promise<void> | void;
1835
+ signals?: NodeJS.Signals[];
1836
+ logEvents?: boolean;
1837
+ }
1838
+ interface RequestIdOptions {
1839
+ headerName?: string;
1840
+ generator?: () => string;
1841
+ }
1842
+ interface HealthOptions {
1843
+ path?: string;
1844
+ check?: () => Promise<unknown>;
1845
+ }
1846
+ interface HealthCheck {
1847
+ healthy: boolean;
1848
+ timestamp: string;
1849
+ [key: string]: unknown;
1850
+ }
1851
+ /**
1852
+ * Auth Plugin Options - Clean, Minimal Configuration
1853
+ *
1854
+ * Arc provides JWT infrastructure and calls your authenticator.
1855
+ * You control ALL authentication logic.
1856
+ *
1857
+ * @example
1858
+ * ```typescript
1859
+ * // Minimal: just JWT (uses default jwtVerify)
1860
+ * auth: {
1861
+ * jwt: { secret: process.env.JWT_SECRET },
1862
+ * }
1863
+ *
1864
+ * // With custom authenticator (recommended)
1865
+ * auth: {
1866
+ * jwt: { secret: process.env.JWT_SECRET },
1867
+ * authenticate: async (request, { jwt }) => {
1868
+ * const token = request.headers.authorization?.split(' ')[1];
1869
+ * if (!token) return null;
1870
+ * const decoded = jwt.verify(token);
1871
+ * return userRepo.findById(decoded.id);
1872
+ * },
1873
+ * }
1874
+ *
1875
+ * // Multi-strategy (JWT + API Key)
1876
+ * auth: {
1877
+ * jwt: { secret: process.env.JWT_SECRET },
1878
+ * authenticate: async (request, { jwt }) => {
1879
+ * // Try API key first (faster)
1880
+ * const apiKey = request.headers['x-api-key'];
1881
+ * if (apiKey) {
1882
+ * const result = await apiKeyService.verify(apiKey);
1883
+ * if (result) return { _id: result.userId, isApiKey: true };
1884
+ * }
1885
+ * // Try JWT
1886
+ * const token = request.headers.authorization?.split(' ')[1];
1887
+ * if (token) {
1888
+ * const decoded = jwt.verify(token);
1889
+ * return userRepo.findById(decoded.id);
1890
+ * }
1891
+ * return null;
1892
+ * },
1893
+ * onFailure: (request, reply) => {
1894
+ * reply.code(401).send({
1895
+ * success: false,
1896
+ * error: 'Authentication required',
1897
+ * message: 'Use Bearer token or X-API-Key header',
1898
+ * });
1899
+ * },
1900
+ * }
1901
+ * ```
1902
+ */
1903
+ interface AuthPluginOptions {
1904
+ /**
1905
+ * JWT configuration (optional but recommended)
1906
+ * If provided, jwt utilities are available in authenticator context
1907
+ */
1908
+ jwt?: {
1909
+ /** JWT secret (required for JWT features) */secret: string; /** Access token expiry (default: '15m') */
1910
+ expiresIn?: string; /** Refresh token secret (defaults to main secret) */
1911
+ refreshSecret?: string; /** Refresh token expiry (default: '7d') */
1912
+ refreshExpiresIn?: string; /** Additional @fastify/jwt sign options */
1913
+ sign?: Record<string, unknown>; /** Additional @fastify/jwt verify options */
1914
+ verify?: Record<string, unknown>;
1915
+ };
1916
+ /**
1917
+ * Custom authenticator function (recommended)
1918
+ *
1919
+ * Arc calls this for non-public routes.
1920
+ * Return user object to authenticate, null/undefined to reject.
1921
+ *
1922
+ * If not provided and jwt.secret is set, uses default jwtVerify.
1923
+ */
1924
+ authenticate?: Authenticator;
1925
+ /**
1926
+ * Custom auth failure handler
1927
+ * Customize the 401 response when authentication fails
1928
+ */
1929
+ onFailure?: (request: FastifyRequest, reply: FastifyReply, error?: Error) => void | Promise<void>;
1930
+ /**
1931
+ * Expose detailed auth error messages in 401 responses.
1932
+ * When false (default), returns generic "Authentication required".
1933
+ * When true, includes the actual error message for debugging.
1934
+ * Decoupled from log level — set explicitly per environment.
1935
+ */
1936
+ exposeAuthErrors?: boolean;
1937
+ /**
1938
+ * Property name to store user on request (default: 'user')
1939
+ */
1940
+ userProperty?: string;
1941
+ /**
1942
+ * Custom token extractor for the built-in JWT auth path.
1943
+ * When not provided, defaults to extracting Bearer token from Authorization header.
1944
+ * Use this when tokens are in HttpOnly cookies, custom headers, or query params.
1945
+ *
1946
+ * @example
1947
+ * ```typescript
1948
+ * // Extract from HttpOnly cookie
1949
+ * tokenExtractor: (request) => request.cookies?.['auth-token'] ?? null,
1950
+ *
1951
+ * // Extract from custom header
1952
+ * tokenExtractor: (request) => request.headers['x-api-token'] as string ?? null,
1953
+ * ```
1954
+ */
1955
+ tokenExtractor?: (request: FastifyRequest) => string | null;
1956
+ /**
1957
+ * Token revocation check — called after JWT verification succeeds.
1958
+ * Return `true` to reject the token (revoked), `false` to allow.
1959
+ *
1960
+ * Arc provides this primitive — implement your own store (Redis set,
1961
+ * DB lookup, Better Auth session check, etc.)
1962
+ *
1963
+ * **Fail-closed**: if the check throws, the token is treated as revoked.
1964
+ *
1965
+ * @example
1966
+ * ```typescript
1967
+ * // Redis-backed revocation
1968
+ * isRevoked: async (decoded) => {
1969
+ * return await redis.sismember('revoked-tokens', decoded.jti ?? decoded.id);
1970
+ * },
1971
+ *
1972
+ * // DB-backed revocation
1973
+ * isRevoked: async (decoded) => {
1974
+ * const user = await db.user.findById(decoded.id);
1975
+ * return !user || user.bannedAt != null;
1976
+ * },
1977
+ * ```
1978
+ */
1979
+ isRevoked?: (decoded: Record<string, unknown>) => boolean | Promise<boolean>;
1980
+ }
1981
+ interface IntrospectionPluginOptions {
1982
+ path?: string;
1983
+ prefix?: string;
1984
+ enabled?: boolean;
1985
+ authRoles?: string[];
1986
+ }
1987
+ interface CrudRouterOptions {
1988
+ /** Route prefix */
1989
+ prefix?: string;
1990
+ /** Permission checks for CRUD operations */
1991
+ permissions?: ResourcePermissions;
1992
+ /** OpenAPI tag for grouping routes */
1993
+ tag?: string;
1994
+ /** JSON schemas for CRUD operations */
1995
+ schemas?: Partial<CrudSchemas>;
1996
+ /** Middlewares for each CRUD operation */
1997
+ middlewares?: MiddlewareConfig;
1998
+ /** Additional custom routes (from presets or user-defined) */
1999
+ additionalRoutes?: AdditionalRoute[];
2000
+ /** Disable all default CRUD routes */
2001
+ disableDefaultRoutes?: boolean;
2002
+ /** Disable specific CRUD routes */
2003
+ disabledRoutes?: CrudRouteKey[];
2004
+ /** Functional pipeline (guard/transform/intercept) */
2005
+ pipe?: PipelineConfig;
2006
+ /** Resource name for lifecycle hooks */
2007
+ resourceName?: string;
2008
+ /** Schema generation options */
2009
+ schemaOptions?: RouteSchemaOptions;
2010
+ /** Field-level permissions (visibility, writability per role) */
2011
+ fields?: FieldPermissionMap;
2012
+ /** HTTP method for update routes. Default: 'PATCH' */
2013
+ updateMethod?: 'PUT' | 'PATCH' | 'both';
2014
+ /**
2015
+ * Per-resource rate limiting.
2016
+ * Requires `@fastify/rate-limit` to be registered on the Fastify instance.
2017
+ * Set to `false` to disable rate limiting for this resource.
2018
+ */
2019
+ rateLimit?: RateLimitConfig | false;
2020
+ }
2021
+ interface ResourceMetadata {
2022
+ name: string;
2023
+ displayName?: string;
2024
+ tag?: string;
2025
+ prefix: string;
2026
+ module?: string;
2027
+ permissions?: ResourcePermissions;
2028
+ presets: string[];
2029
+ additionalRoutes?: AdditionalRoute[];
2030
+ routes: Array<{
2031
+ method: string;
2032
+ path: string;
2033
+ handler?: string;
2034
+ operation?: string;
2035
+ summary?: string;
2036
+ }>;
2037
+ events?: string[];
2038
+ }
2039
+ interface RegistryEntry extends ResourceMetadata {
2040
+ plugin: unknown;
2041
+ adapter?: {
2042
+ type: string;
2043
+ name: string;
2044
+ } | null;
2045
+ events?: string[];
2046
+ disableDefaultRoutes?: boolean;
2047
+ openApiSchemas?: OpenApiSchemas;
2048
+ registeredAt?: string;
2049
+ /** Field-level permissions metadata (for OpenAPI docs) */
2050
+ fieldPermissions?: Record<string, {
2051
+ type: string;
2052
+ roles?: readonly string[];
2053
+ redactValue?: unknown;
2054
+ }>;
2055
+ /** Pipeline step names (for OpenAPI docs) */
2056
+ pipelineSteps?: Array<{
2057
+ type: string;
2058
+ name: string;
2059
+ operations?: string[];
2060
+ }>;
2061
+ /** Update HTTP method(s) used for this resource */
2062
+ updateMethod?: 'PUT' | 'PATCH' | 'both';
2063
+ /** Routes disabled for this resource */
2064
+ disabledRoutes?: string[];
2065
+ /** Rate limit config */
2066
+ rateLimit?: RateLimitConfig | false;
2067
+ }
2068
+ interface RegistryStats {
2069
+ total?: number;
2070
+ totalResources: number;
2071
+ byTag?: Record<string, number>;
2072
+ byModule?: Record<string, number>;
2073
+ presetUsage?: Record<string, number>;
2074
+ totalRoutes?: number;
2075
+ totalEvents?: number;
2076
+ }
2077
+ interface IntrospectionData {
2078
+ resources: ResourceMetadata[];
2079
+ stats: RegistryStats;
2080
+ generatedAt?: string;
2081
+ }
2082
+ interface EventDefinition {
2083
+ name: string;
2084
+ /** Optional handler — events are published via fastify.events.publish(), not invoked through resource definitions */
2085
+ handler?: (data: unknown) => Promise<void> | void;
2086
+ schema?: Record<string, unknown>;
2087
+ description?: string;
2088
+ }
2089
+ interface ConfigError {
2090
+ field: string;
2091
+ message: string;
2092
+ code?: string;
2093
+ }
2094
+ interface ValidationResult$1 {
2095
+ valid: boolean;
2096
+ errors: ConfigError[];
2097
+ }
2098
+ interface ValidateOptions {
2099
+ strict?: boolean;
2100
+ }
2101
+ /**
2102
+ * Infer document type from DataAdapter or ResourceConfig
2103
+ */
2104
+ type InferDocType<T> = T extends DataAdapter<infer D> ? D : T extends ResourceConfig<infer D> ? D : never;
2105
+ /**
2106
+ * Infer document type from a DataAdapter.
2107
+ * Falls back to `unknown` (not `never`) — safe for generic constraints.
2108
+ *
2109
+ * @example
2110
+ * ```typescript
2111
+ * const adapter = createMongooseAdapter({ model: ProductModel, repository: productRepo });
2112
+ * type ProductDoc = InferAdapterDoc<typeof adapter>;
2113
+ * // ProductDoc = the document type inferred from the adapter
2114
+ * ```
2115
+ */
2116
+ type InferAdapterDoc<A> = A extends DataAdapter<infer D> ? D : unknown;
2117
+ type InferResourceDoc<T> = T extends ResourceConfig<infer D> ? D : never;
2118
+ type TypedResourceConfig<TDoc> = ResourceConfig<TDoc>;
2119
+ type TypedController<TDoc> = IController<TDoc>;
2120
+ type TypedRepository<TDoc> = CrudRepository<TDoc>;
2121
+ //#endregion
2122
+ //#region src/adapters/interface.d.ts
2123
+ /**
2124
+ * Minimal repository interface for flexible adapter compatibility.
2125
+ * Any repository with these method signatures is accepted — no `as any` needed.
2126
+ *
2127
+ * CrudRepository<TDoc> and MongoKit Repository both satisfy this interface.
2128
+ */
2129
+ interface RepositoryLike {
2130
+ getAll(params?: unknown): Promise<unknown>;
2131
+ getById(id: string, options?: unknown): Promise<unknown>;
2132
+ create(data: unknown, options?: unknown): Promise<unknown>;
2133
+ update(id: string, data: unknown, options?: unknown): Promise<unknown>;
2134
+ delete(id: string, options?: unknown): Promise<unknown>;
2135
+ [key: string]: unknown;
2136
+ }
2137
+ interface DataAdapter<TDoc = unknown> {
2138
+ /**
2139
+ * Repository implementing CRUD operations
2140
+ * Accepts CrudRepository, MongoKit Repository, or any compatible object
2141
+ */
2142
+ repository: CrudRepository<TDoc> | RepositoryLike;
2143
+ /** Adapter identifier for introspection */
2144
+ readonly type: "mongoose" | "prisma" | "drizzle" | "typeorm" | "custom";
2145
+ /** Human-readable name */
2146
+ readonly name: string;
2147
+ /**
2148
+ * Generate OpenAPI schemas for CRUD operations
2149
+ *
2150
+ * This method allows each adapter to generate schemas specific to its ORM/database.
2151
+ * For example, Mongoose adapter can use mongokit to generate schemas from Mongoose models.
2152
+ *
2153
+ * @param options - Schema generation options (field rules, populate settings, etc.)
2154
+ * @returns OpenAPI schemas for CRUD operations or null if not supported
2155
+ */
2156
+ generateSchemas?(options?: RouteSchemaOptions): OpenApiSchemas | Record<string, unknown> | null;
2157
+ /** Extract schema metadata for OpenAPI/introspection */
2158
+ getSchemaMetadata?(): SchemaMetadata | null;
2159
+ /** Validate data against schema before persistence */
2160
+ validate?(data: unknown): Promise<ValidationResult> | ValidationResult;
2161
+ /** Health check for database connection */
2162
+ healthCheck?(): Promise<boolean>;
2163
+ /**
2164
+ * Custom filter matching for policy enforcement.
2165
+ * Falls back to built-in MongoDB-style matching if not provided.
2166
+ * Override this for SQL adapters or non-MongoDB query operators.
2167
+ */
2168
+ matchesFilter?: (item: unknown, filters: Record<string, unknown>) => boolean;
2169
+ /** Close/cleanup resources */
2170
+ close?(): Promise<void>;
2171
+ }
2172
+ interface SchemaMetadata {
2173
+ name: string;
2174
+ fields: Record<string, FieldMetadata>;
2175
+ indexes?: Array<{
2176
+ fields: string[];
2177
+ unique?: boolean;
2178
+ sparse?: boolean;
2179
+ }>;
2180
+ relations?: Record<string, RelationMetadata>;
2181
+ }
2182
+ interface FieldMetadata {
2183
+ type: "string" | "number" | "boolean" | "date" | "object" | "array" | "objectId" | "enum";
2184
+ required?: boolean;
2185
+ unique?: boolean;
2186
+ default?: unknown;
2187
+ enum?: Array<string | number>;
2188
+ min?: number;
2189
+ max?: number;
2190
+ minLength?: number;
2191
+ maxLength?: number;
2192
+ pattern?: string;
2193
+ description?: string;
2194
+ ref?: string;
2195
+ array?: boolean;
2196
+ }
2197
+ interface RelationMetadata {
2198
+ type: "one-to-one" | "one-to-many" | "many-to-many";
2199
+ target: string;
2200
+ foreignKey?: string;
2201
+ through?: string;
2202
+ }
2203
+ interface ValidationResult {
2204
+ valid: boolean;
2205
+ errors?: Array<{
2206
+ field: string;
2207
+ message: string;
2208
+ code?: string;
2209
+ }>;
2210
+ }
2211
+ type AdapterFactory<TDoc> = (config: unknown) => DataAdapter<TDoc>;
2212
+ //#endregion
2213
+ export { RegistryStats as $, HookContext as $t, HealthCheck as A, FastifyHandler as At, MiddlewareConfig as B, InferDoc as Bt, EventDefinition as C, QueryResolverConfig as Ct, FastifyWithDecorators as D, AccessControlConfig as Dt, FastifyWithAuth as E, AccessControl as Et, IntrospectionData as F, RegisterOptions as Ft, ParsedQuery as G, Interceptor as Gt, ObjectId as H, PaginationParams as Ht, IntrospectionPluginOptions as I, ResourceRegistry as It, PresetHook as J, PipelineConfig as Jt, PopulateOption as K, NextFunction as Kt, JWTPayload as L, ResourceDefinition as Lt, InferAdapterDoc as M, IControllerResponse as Mt, InferDocType as N, IRequestContext as Nt, FieldRule as O, ControllerHandler as Ot, InferResourceDoc as P, RouteHandler as Pt, RegistryEntry as Q, DefineHookOptions as Qt, JwtContext as R, defineResource as Rt, CrudSchemas as S, QueryResolver as St, FastifyRequestExtras as T, BodySanitizerConfig as Tt, OpenApiSchemas as U, QueryOptions as Ut, MiddlewareHandler as V, PaginatedResult as Vt, OwnershipCheck as W, Guard as Wt, QueryParserInterface as X, PipelineStep as Xt, PresetResult as Y, PipelineContext as Yt, RateLimitConfig as Z, Transform as Zt, ConfigError as _, ValidateOptions as _t, RepositoryLike as a, HookSystemOptions as an, ResourceHooks as at, CrudRouteKey as b, BaseController as bt, AdditionalRoute as c, afterUpdate as cn, RouteHandlerMethod$1 as ct, ArcDecorator as d, beforeUpdate as dn, TokenPair as dt, HookHandler as en, RequestContext as et, ArcInternalMetadata as f, createHookSystem as fn, TypedController as ft, AuthenticatorContext as g, UserOrganization as gt, Authenticator as h, UserLike as ht, RelationMetadata as i, HookSystem as in, ResourceConfig as it, HealthOptions as j, IController as jt, GracefulShutdownOptions as k, ControllerLike as kt, AnyRecord as l, beforeCreate as ln, RouteSchemaOptions as lt, AuthPluginOptions as m, TypedResourceConfig as mt, DataAdapter as n, HookPhase as nn, RequestWithExtras as nt, SchemaMetadata as o, afterCreate as on, ResourceMetadata as ot, AuthHelpers as p, defineHook as pn, TypedRepository as pt, PresetFunction as q, OperationFilter as qt, FieldMetadata as r, HookRegistration as rn, ResourceCacheConfig as rt, ValidationResult as s, afterDelete as sn, ResourcePermissions as st, AdapterFactory as t, HookOperation as tn, RequestIdOptions as tt, ApiResponse as u, beforeDelete as un, ServiceContext as ut, ControllerQueryOptions as v, ValidationResult$1 as vt, EventsDecorator as w, BodySanitizer as wt, CrudRouterOptions as x, BaseControllerOptions as xt, CrudController as y, getUserId as yt, LookupOption as z, CrudRepository as zt };