@classytic/arc 2.10.8 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/{BaseController-DVNKvoX4.mjs → BaseController-JNV08qOT.mjs} +480 -442
  2. package/dist/{queryCachePlugin-Dumka73q.d.mts → QueryCache-DOBNHBE0.d.mts} +2 -32
  3. package/dist/adapters/index.d.mts +2 -2
  4. package/dist/adapters/index.mjs +1 -1
  5. package/dist/{adapters-BXY4i-hw.mjs → adapters-D0tT2Tyo.mjs} +54 -0
  6. package/dist/audit/index.d.mts +1 -1
  7. package/dist/auth/index.d.mts +1 -1
  8. package/dist/auth/index.mjs +5 -5
  9. package/dist/{betterAuthOpenApi--rdY15Ld.mjs → betterAuthOpenApi-DwxtK3uG.mjs} +1 -1
  10. package/dist/cache/index.d.mts +3 -2
  11. package/dist/cache/index.mjs +3 -3
  12. package/dist/cli/commands/docs.mjs +2 -2
  13. package/dist/cli/commands/generate.mjs +37 -27
  14. package/dist/cli/commands/init.mjs +46 -33
  15. package/dist/cli/commands/introspect.mjs +1 -1
  16. package/dist/context/index.mjs +1 -1
  17. package/dist/core/index.d.mts +3 -3
  18. package/dist/core/index.mjs +4 -3
  19. package/dist/core-DXdSSFW-.mjs +1037 -0
  20. package/dist/createActionRouter-BwaSM0No.mjs +166 -0
  21. package/dist/{createApp-BwnEAO2h.mjs → createApp-DvNYEhpb.mjs} +75 -27
  22. package/dist/docs/index.d.mts +1 -1
  23. package/dist/docs/index.mjs +2 -2
  24. package/dist/{elevation-Dci0AYLT.mjs → elevation-DOFoxoDs.mjs} +1 -1
  25. package/dist/{errorHandler-CSxe7KIM.mjs → errorHandler-BQm8ZxTK.mjs} +1 -1
  26. package/dist/{eventPlugin-ByU4Cv0e.mjs → eventPlugin--5HIkdPU.mjs} +1 -1
  27. package/dist/events/index.d.mts +3 -3
  28. package/dist/events/index.mjs +2 -2
  29. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  30. package/dist/factory/index.d.mts +1 -1
  31. package/dist/factory/index.mjs +2 -2
  32. package/dist/hooks/index.d.mts +1 -1
  33. package/dist/hooks/index.mjs +1 -1
  34. package/dist/idempotency/index.d.mts +3 -3
  35. package/dist/idempotency/index.mjs +1 -1
  36. package/dist/idempotency/redis.d.mts +1 -1
  37. package/dist/{index-C_Noptz-.d.mts → index-BYCqHCVu.d.mts} +2 -2
  38. package/dist/{index-BGbpGVyM.d.mts → index-Cm0vUrr_.d.mts} +699 -494
  39. package/dist/{index-BziRPS4H.d.mts → index-DAushRTt.d.mts} +29 -10
  40. package/dist/index-DsJ1MNfC.d.mts +1179 -0
  41. package/dist/{index-EqQN6p0W.d.mts → index-t8pLpPFW.d.mts} +11 -8
  42. package/dist/index.d.mts +6 -38
  43. package/dist/index.mjs +9 -9
  44. package/dist/integrations/event-gateway.d.mts +1 -1
  45. package/dist/integrations/event-gateway.mjs +1 -1
  46. package/dist/integrations/index.d.mts +2 -2
  47. package/dist/integrations/mcp/index.d.mts +2 -2
  48. package/dist/integrations/mcp/index.mjs +1 -1
  49. package/dist/integrations/mcp/testing.d.mts +1 -1
  50. package/dist/integrations/mcp/testing.mjs +1 -1
  51. package/dist/integrations/streamline.d.mts +46 -5
  52. package/dist/integrations/streamline.mjs +50 -21
  53. package/dist/integrations/websocket-redis.d.mts +1 -1
  54. package/dist/integrations/websocket.d.mts +2 -154
  55. package/dist/integrations/websocket.mjs +292 -224
  56. package/dist/{keys-nWQGUTu1.mjs → keys-CARyUjiR.mjs} +2 -0
  57. package/dist/{loadResources-Bksk8ydA.mjs → loadResources-YNwKHvRA.mjs} +3 -1
  58. package/dist/middleware/index.d.mts +1 -1
  59. package/dist/middleware/index.mjs +1 -1
  60. package/dist/{openapi-DpNpqBmo.mjs → openapi-C0L9ar7m.mjs} +4 -4
  61. package/dist/org/index.d.mts +1 -1
  62. package/dist/permissions/index.d.mts +1 -1
  63. package/dist/permissions/index.mjs +2 -4
  64. package/dist/{permissions-wkqRwicB.mjs → permissions-B4vU9L0Q.mjs} +221 -3
  65. package/dist/{pipe-CGJxqDGx.mjs → pipe-DVoIheVC.mjs} +1 -1
  66. package/dist/pipeline/index.d.mts +1 -1
  67. package/dist/pipeline/index.mjs +1 -1
  68. package/dist/plugins/index.d.mts +4 -4
  69. package/dist/plugins/index.mjs +10 -10
  70. package/dist/plugins/response-cache.mjs +1 -1
  71. package/dist/plugins/tracing-entry.d.mts +1 -1
  72. package/dist/plugins/tracing-entry.mjs +42 -24
  73. package/dist/presets/filesUpload.d.mts +1 -1
  74. package/dist/presets/filesUpload.mjs +3 -3
  75. package/dist/presets/index.d.mts +1 -1
  76. package/dist/presets/index.mjs +1 -1
  77. package/dist/presets/multiTenant.d.mts +1 -1
  78. package/dist/presets/multiTenant.mjs +6 -0
  79. package/dist/presets/search.d.mts +1 -1
  80. package/dist/presets/search.mjs +1 -1
  81. package/dist/{presets-CrwOvuXI.mjs → presets-k604Lj99.mjs} +1 -1
  82. package/dist/queryCachePlugin-BUXBSm4F.d.mts +34 -0
  83. package/dist/{queryCachePlugin-ChLNZvFT.mjs → queryCachePlugin-Bq6bO6vc.mjs} +3 -3
  84. package/dist/{redis-MXLp1oOf.d.mts → redis-Cm1gnRDf.d.mts} +1 -1
  85. package/dist/registry/index.d.mts +1 -1
  86. package/dist/registry/index.mjs +2 -2
  87. package/dist/{resourceToTools-BhF3JV5p.mjs → resourceToTools--okX6QBr.mjs} +534 -420
  88. package/dist/routerShared-DeESFp4a.mjs +515 -0
  89. package/dist/schemaIR-BlG9bY7v.mjs +137 -0
  90. package/dist/scope/index.mjs +2 -2
  91. package/dist/testing/index.d.mts +367 -711
  92. package/dist/testing/index.mjs +637 -1434
  93. package/dist/{tracing-xqXzWeaf.d.mts → tracing-DokiEsuz.d.mts} +9 -4
  94. package/dist/types/index.d.mts +3 -3
  95. package/dist/types/index.mjs +1 -3
  96. package/dist/{types-CVdgPXBW.d.mts → types-CgikqKAj.d.mts} +118 -19
  97. package/dist/{types-CVKBssX5.d.mts → types-D9NqiYIw.d.mts} +1 -1
  98. package/dist/utils/index.d.mts +2 -968
  99. package/dist/utils/index.mjs +5 -6
  100. package/dist/utils-D3Yxnrwr.mjs +1639 -0
  101. package/dist/websocket-CyJ1VIFI.d.mts +186 -0
  102. package/package.json +7 -5
  103. package/skills/arc/SKILL.md +123 -38
  104. package/skills/arc/references/testing.md +212 -183
  105. package/dist/applyPermissionResult-QhV1Pa-g.mjs +0 -37
  106. package/dist/core-3MWJosCH.mjs +0 -1459
  107. package/dist/createActionRouter-C8UUB3Px.mjs +0 -249
  108. package/dist/errors-BI8kEKsO.d.mts +0 -140
  109. package/dist/fields-CTMWOUDt.mjs +0 -126
  110. package/dist/queryParser-NR__Qiju.mjs +0 -419
  111. package/dist/types-CDnTEpga.mjs +0 -27
  112. package/dist/utils-LMwVidKy.mjs +0 -947
  113. /package/dist/{HookSystem-BjFu7zf1.mjs → HookSystem-CGsMd6oK.mjs} +0 -0
  114. /package/dist/{ResourceRegistry-CcN2LVrc.mjs → ResourceRegistry-DkAeAuTX.mjs} +0 -0
  115. /package/dist/{actionPermissions-TUVR3uiZ.mjs → actionPermissions-C8YYU92K.mjs} +0 -0
  116. /package/dist/{caching-3h93rkJM.mjs → caching-CheW3m-S.mjs} +0 -0
  117. /package/dist/{errorHandler-2ii4RIYr.d.mts → errorHandler-Co3lnVmJ.d.mts} +0 -0
  118. /package/dist/{errors-BqdUDja_.mjs → errors-D5c-5BJL.mjs} +0 -0
  119. /package/dist/{eventPlugin-D1ThQ1Pp.d.mts → eventPlugin-CUNjYYRY.d.mts} +0 -0
  120. /package/dist/{interface-B-pe8fhj.d.mts → interface-CkkWm5uR.d.mts} +0 -0
  121. /package/dist/{interface-yhyb_pLY.d.mts → interface-Da0r7Lna.d.mts} +0 -0
  122. /package/dist/{memory-DqI-449b.mjs → memory-DikHSvWa.mjs} +0 -0
  123. /package/dist/{metrics-TuOmguhi.mjs → metrics-Csh4nsvv.mjs} +0 -0
  124. /package/dist/{multipartBody-CUQGVlM_.mjs → multipartBody-CvTR1Un6.mjs} +0 -0
  125. /package/dist/{pluralize-CWP6MB39.mjs → pluralize-BneOJkpi.mjs} +0 -0
  126. /package/dist/{redis-stream-bkO88VHx.d.mts → redis-stream-CM8TXTix.d.mts} +0 -0
  127. /package/dist/{registry-B0Wl7uVV.mjs → registry-D63ee7fl.mjs} +0 -0
  128. /package/dist/{replyHelpers-BLojtuvR.mjs → replyHelpers-ByllIXXV.mjs} +0 -0
  129. /package/dist/{requestContext-C38GskNt.mjs → requestContext-CfRkaxwf.mjs} +0 -0
  130. /package/dist/{schemaConverter-BxFDdtXu.mjs → schemaConverter-B0oKLuqI.mjs} +0 -0
  131. /package/dist/{sse-D8UeDwis.mjs → sse-V7aXc3bW.mjs} +0 -0
  132. /package/dist/{store-helpers-DYYUQbQN.mjs → store-helpers-BhrzxvyQ.mjs} +0 -0
  133. /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
  134. /package/dist/{types-D57iXYb8.mjs → types-DV9WDfeg.mjs} +0 -0
  135. /package/dist/{versioning-B6mimogM.mjs → versioning-CGPjkqAg.mjs} +0 -0
  136. /package/dist/{versioning-CeUXHfjw.d.mts → versioning-M9lNLhO8.d.mts} +0 -0
@@ -1,17 +1,49 @@
1
+ import { a as QueryCacheConfig } from "./QueryCache-DOBNHBE0.mjs";
1
2
  import { r as RequestScope } from "./types-tgR4Pt8F.mjs";
2
3
  import { c as PermissionCheck, d as UserBase, n as FieldPermissionMap } from "./fields-C8Y0XLAu.mjs";
3
4
  import { n as DomainEvent } from "./EventTransport-CfVEGaEl.mjs";
4
5
  import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, RouteHandlerMethod, RouteHandlerMethod as RouteHandlerMethod$1 } from "fastify";
5
- import * as _$_classytic_repo_core_repository0 from "@classytic/repo-core/repository";
6
- import { MinimalRepo, QueryOptions, StandardRepo } from "@classytic/repo-core/repository";
7
6
  import { KeysetPaginationResult, OffsetPaginationResult } from "@classytic/repo-core/pagination";
7
+ import { MinimalRepo, QueryOptions, StandardRepo } from "@classytic/repo-core/repository";
8
+ import { FieldRule, SchemaBuilderOptions } from "@classytic/repo-core/schema";
8
9
 
9
10
  //#region src/adapters/interface.d.ts
10
11
  /**
11
- * Arc's structural repository contract: the repo-core minimum plus any
12
- * standard-repo methods a given kit implements. All optional methods are
13
- * feature-detected at call sitesarc never assumes capabilities it
14
- * hasn't probed.
12
+ * Arc's structural repository contract.
13
+ *
14
+ * Defined as `MinimalRepo<TDoc> & Partial<StandardRepo<TDoc>>` the
15
+ * repo-core 5-method floor (required) plus every other `StandardRepo`
16
+ * method (optional). This compound is the single shape arc accepts
17
+ * across its entire API surface:
18
+ *
19
+ * - `defineResource({ adapter: { repository, ... } })`
20
+ * - `auditPlugin({ repository })`, `idempotencyPlugin({ repository })`
21
+ * - `new EventOutbox({ repository, transport })`
22
+ *
23
+ * **Why compound and not `StandardRepo` alone:** forcing every kit to
24
+ * implement the full `StandardRepo` surface would break kits with
25
+ * partial capabilities (sqlitekit has no aggregation, prismakit has no
26
+ * native atomic CAS the same way). Feature-detection at the arc layer
27
+ * lets each kit declare only what it implements; arc's audit / outbox /
28
+ * idempotency plugins check `typeof repo.method === 'function'` at
29
+ * construction and throw with the list of missing primitives if a
30
+ * required subset isn't covered. See the store-backing contract matrix
31
+ * in the file header.
32
+ *
33
+ * **Why compound and not `MinimalRepo` alone:** arc's internal plugins
34
+ * still need the `StandardRepo` type info at call sites where they use
35
+ * optionals like `findOneAndUpdate` / `deleteMany`. Without the
36
+ * `Partial<StandardRepo>` half, every access would require
37
+ * `as StandardRepo` casts and the feature-detect pattern would be
38
+ * runtime-only with no type-level backing.
39
+ *
40
+ * **Hosts importing repo-core directly.** `MinimalRepo` and
41
+ * `StandardRepo` are repo-core's contract — hosts that want to reference
42
+ * them by name should `import type { MinimalRepo, StandardRepo } from
43
+ * '@classytic/repo-core/repository'` rather than go through arc. Arc
44
+ * doesn't re-export them from its root barrel on purpose: creating a
45
+ * second source of truth would force arc to either drift from repo-core
46
+ * or force-sync every time the contract iterates.
15
47
  *
16
48
  * ```ts
17
49
  * const adapter: DataAdapter<Product> = {
@@ -25,12 +57,13 @@ import { KeysetPaginationResult, OffsetPaginationResult } from "@classytic/repo-
25
57
  type RepositoryLike<TDoc = unknown> = MinimalRepo<TDoc> & Partial<StandardRepo<TDoc>>;
26
58
  interface DataAdapter<TDoc = unknown> {
27
59
  /**
28
- * Repository implementing CRUD operations. Accepts the typed
29
- * `StandardRepo<TDoc>` (repo-core's standard contract) or the structural
30
- * `RepositoryLike` (minimum + optionals). Arc feature-detects optional
60
+ * Repository implementing CRUD operations. Any value that satisfies
61
+ * `RepositoryLike<TDoc>` which includes `StandardRepo<TDoc>` (all
62
+ * methods implemented), `MinimalRepo<TDoc>` (5-method floor), or
63
+ * anything in between a kit declares. Arc feature-detects optional
31
64
  * methods at runtime — kits only declare what they support.
32
65
  */
33
- repository: StandardRepo<TDoc> | RepositoryLike<TDoc>;
66
+ repository: RepositoryLike<TDoc>;
34
67
  /** Adapter identifier for introspection */
35
68
  readonly type: "mongoose" | "prisma" | "drizzle" | "typeorm" | "custom";
36
69
  /** Human-readable name */
@@ -114,6 +147,257 @@ interface ValidationResult$1 {
114
147
  }
115
148
  type AdapterFactory<TDoc> = (config: unknown) => DataAdapter<TDoc>;
116
149
  //#endregion
150
+ //#region src/hooks/HookSystem.d.ts
151
+ type HookPhase = "before" | "around" | "after";
152
+ type HookOperation = "create" | "update" | "delete" | "restore" | "read" | "list";
153
+ interface HookContext<T = AnyRecord> {
154
+ resource: string;
155
+ operation: HookOperation;
156
+ phase: HookPhase;
157
+ data?: T;
158
+ result?: T | T[];
159
+ user?: UserBase;
160
+ context?: RequestContext;
161
+ meta?: AnyRecord;
162
+ }
163
+ type HookHandler<T = AnyRecord> = (ctx: HookContext<T>) => void | Promise<void> | T | Promise<T>;
164
+ /**
165
+ * Around hook handler — wraps the core operation.
166
+ * Call `next()` to proceed to the next around hook or the actual operation.
167
+ */
168
+ type AroundHookHandler<T = AnyRecord> = (ctx: HookContext<T>, next: () => Promise<T | undefined>) => T | undefined | Promise<T | undefined>;
169
+ interface HookRegistration {
170
+ /** Hook name for dependency resolution and debugging */
171
+ name?: string;
172
+ resource: string;
173
+ operation: HookOperation;
174
+ phase: HookPhase;
175
+ handler: HookHandler;
176
+ priority: number;
177
+ /** Names of hooks that must execute before this one */
178
+ dependsOn?: string[];
179
+ }
180
+ interface HookSystemOptions {
181
+ /** Custom logger for error/warning reporting. Defaults to console */
182
+ logger?: {
183
+ error: (message: string, ...args: unknown[]) => void;
184
+ warn?: (message: string, ...args: unknown[]) => void;
185
+ };
186
+ }
187
+ declare class HookSystem {
188
+ private hooks;
189
+ private logger;
190
+ private warn;
191
+ constructor(options?: HookSystemOptions);
192
+ /**
193
+ * Generate hook key
194
+ */
195
+ private getKey;
196
+ /**
197
+ * Register a hook
198
+ * Supports both object parameter and positional arguments
199
+ */
200
+ register<T = AnyRecord>(resourceOrOptions: string | {
201
+ name?: string;
202
+ resource: string;
203
+ operation: HookOperation;
204
+ phase: HookPhase;
205
+ handler: HookHandler<T>;
206
+ priority?: number;
207
+ dependsOn?: string[];
208
+ }, operation?: HookOperation, phase?: HookPhase, handler?: HookHandler<T>, priority?: number): () => void;
209
+ /**
210
+ * Register before hook
211
+ */
212
+ before<T = AnyRecord>(resource: string, operation: HookOperation, handler: HookHandler<T>, priority?: number): () => void;
213
+ /**
214
+ * Register after hook
215
+ */
216
+ after<T = AnyRecord>(resource: string, operation: HookOperation, handler: HookHandler<T>, priority?: number): () => void;
217
+ /**
218
+ * Register around hook — wraps the core operation.
219
+ * Call `next()` inside the handler to proceed.
220
+ */
221
+ around<T = AnyRecord>(resource: string, operation: HookOperation, handler: AroundHookHandler<T>, priority?: number): () => void;
222
+ /**
223
+ * Execute around hooks as a nested middleware chain.
224
+ * Each around hook receives `next()` to call the next hook or the core operation.
225
+ */
226
+ executeAround<T = AnyRecord>(resource: string, operation: HookOperation, data: T, execute: () => Promise<T | undefined>, options?: {
227
+ user?: UserBase;
228
+ context?: RequestContext;
229
+ meta?: AnyRecord;
230
+ }): Promise<T | undefined>;
231
+ /**
232
+ * Execute hooks for a given context
233
+ */
234
+ execute<T = AnyRecord>(ctx: HookContext<T>): Promise<T | undefined>;
235
+ /**
236
+ * Execute before hooks
237
+ */
238
+ executeBefore<T = AnyRecord>(resource: string, operation: HookOperation, data: T, options?: {
239
+ user?: UserBase;
240
+ context?: RequestContext;
241
+ meta?: AnyRecord;
242
+ }): Promise<T>;
243
+ /**
244
+ * Execute after hooks
245
+ * Errors in after hooks are logged but don't fail the request
246
+ */
247
+ executeAfter<T = AnyRecord>(resource: string, operation: HookOperation, result: T | T[], options?: {
248
+ user?: UserBase;
249
+ context?: RequestContext;
250
+ meta?: AnyRecord;
251
+ }): Promise<void>;
252
+ /**
253
+ * Topological sort with Kahn's algorithm.
254
+ * Hooks with `dependsOn` are ordered after their dependencies.
255
+ * Within the same dependency level, priority ordering is preserved.
256
+ * Hooks without names or dependencies pass through in their original order.
257
+ */
258
+ private topologicalSort;
259
+ /**
260
+ * Get all registered hooks
261
+ */
262
+ getAll(): HookRegistration[];
263
+ /**
264
+ * Get hooks for a specific resource
265
+ */
266
+ getForResource(resource: string): HookRegistration[];
267
+ /**
268
+ * Get hooks matching filter criteria.
269
+ * Useful for debugging and testing specific hook combinations.
270
+ *
271
+ * @example
272
+ * ```typescript
273
+ * // Find all before-create hooks for products (including wildcards)
274
+ * const hooks = hookSystem.getRegistered({
275
+ * resource: 'product',
276
+ * operation: 'create',
277
+ * phase: 'before',
278
+ * });
279
+ * ```
280
+ */
281
+ getRegistered(filter?: {
282
+ resource?: string;
283
+ operation?: HookOperation;
284
+ phase?: HookPhase;
285
+ }): HookRegistration[];
286
+ /**
287
+ * Get a structured summary of all registered hooks for debugging.
288
+ *
289
+ * @example
290
+ * ```typescript
291
+ * const info = hookSystem.inspect();
292
+ * // { total: 12, resources: { product: [...], '*': [...] }, summary: [...] }
293
+ * ```
294
+ */
295
+ inspect(): {
296
+ total: number;
297
+ resources: Record<string, HookRegistration[]>;
298
+ summary: Array<{
299
+ name?: string;
300
+ key: string;
301
+ priority: number;
302
+ dependsOn?: string[];
303
+ }>;
304
+ };
305
+ /**
306
+ * Check if any hooks exist for a specific resource/operation/phase combination.
307
+ */
308
+ has(resource: string, operation: HookOperation, phase: HookPhase): boolean;
309
+ /**
310
+ * Clear all hooks
311
+ */
312
+ clear(): void;
313
+ /**
314
+ * Clear hooks for a specific resource
315
+ */
316
+ clearResource(resource: string): void;
317
+ }
318
+ /**
319
+ * Create a new isolated HookSystem instance
320
+ *
321
+ * Use this for:
322
+ * - Test isolation (parallel test suites)
323
+ * - Multiple app instances with independent hooks
324
+ *
325
+ * @example
326
+ * const hooks = createHookSystem();
327
+ * await app.register(arcCorePlugin, { hookSystem: hooks });
328
+ *
329
+ * @example With custom logger
330
+ * const hooks = createHookSystem({ logger: fastify.log });
331
+ */
332
+ declare function createHookSystem(options?: HookSystemOptions): HookSystem;
333
+ interface DefineHookOptions<T = AnyRecord> {
334
+ /** Unique hook name (required for dependency resolution) */
335
+ name: string;
336
+ /** Target resource */
337
+ resource: string;
338
+ /** CRUD operation */
339
+ operation: HookOperation;
340
+ /** before or after */
341
+ phase: HookPhase;
342
+ /** Hook handler */
343
+ handler: HookHandler<T>;
344
+ /** Priority (lower = earlier, default: 10) */
345
+ priority?: number;
346
+ /** Names of hooks that must execute before this one */
347
+ dependsOn?: string[];
348
+ }
349
+ /**
350
+ * Define a named hook with optional dependencies.
351
+ * Returns a registration object — call `register(hookSystem)` to activate.
352
+ *
353
+ * @example
354
+ * ```typescript
355
+ * const generateSlug = defineHook({
356
+ * name: 'generateSlug',
357
+ * resource: 'product', operation: 'create', phase: 'before',
358
+ * handler: (ctx) => ({ ...ctx.data, slug: slugify(ctx.data.name) }),
359
+ * });
360
+ *
361
+ * const validateUniqueSlug = defineHook({
362
+ * name: 'validateUniqueSlug',
363
+ * resource: 'product', operation: 'create', phase: 'before',
364
+ * dependsOn: ['generateSlug'],
365
+ * handler: async (ctx) => { // check uniqueness },
366
+ * });
367
+ *
368
+ * // Register on a hook system
369
+ * generateSlug.register(hooks);
370
+ * validateUniqueSlug.register(hooks);
371
+ * ```
372
+ */
373
+ declare function defineHook<T = AnyRecord>(options: DefineHookOptions<T>): DefineHookOptions<T> & {
374
+ register: (hooks: HookSystem) => () => void;
375
+ };
376
+ /**
377
+ * Create a before-create hook registration for a given hook system
378
+ */
379
+ declare function beforeCreate<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
380
+ /**
381
+ * Create an after-create hook registration for a given hook system
382
+ */
383
+ declare function afterCreate<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
384
+ /**
385
+ * Create a before-update hook registration for a given hook system
386
+ */
387
+ declare function beforeUpdate<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
388
+ /**
389
+ * Create an after-update hook registration for a given hook system
390
+ */
391
+ declare function afterUpdate<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
392
+ /**
393
+ * Create a before-delete hook registration for a given hook system
394
+ */
395
+ declare function beforeDelete<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
396
+ /**
397
+ * Create an after-delete hook registration for a given hook system
398
+ */
399
+ declare function afterDelete<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
400
+ //#endregion
117
401
  //#region src/core/AccessControl.d.ts
118
402
  /** Denial reason codes returned by `fetchDetailed()`. */
119
403
  type FetchDenialReason = "NOT_FOUND" | "POLICY_FILTERED" | "ORG_SCOPE_DENIED";
@@ -330,26 +614,55 @@ declare class QueryResolver {
330
614
  private getBlockedFields;
331
615
  }
332
616
  //#endregion
333
- //#region src/core/BaseController.d.ts
617
+ //#region src/core/BaseCrudController.d.ts
334
618
  /**
335
619
  * Union of every return shape repo-core's `MinimalRepo.getAll()` is
336
- * contractually allowed to produce. Keeping arc's list surface in sync
337
- * with that contract (v2.10.6) — previously arc narrowed to
338
- * `OffsetPaginationResult<TDoc>` and treated bare arrays as
339
- * "non-conforming," which drifted from repo-core's published shape.
340
- *
341
- * - `OffsetPaginationResult<TDoc>`when the query uses offset pagination
342
- * (`page` parameter).
343
- * - `KeysetPaginationResult<TDoc>` when the query uses keyset/cursor
344
- * pagination (`sort` + optional `after`).
345
- * - `TDoc[]` — raw array when neither drives pagination; valid per
346
- * repo-core's `MinimalRepo.getAll` docstring.
347
- *
348
- * Arc passes the kit's response verbatim; callers / consumers should
349
- * narrow on shape (`Array.isArray(result)` → bare array, presence of
350
- * `page`/`total` → offset, presence of `nextCursor` → keyset).
620
+ * contractually allowed to produce. See repo-core's `MinimalRepo.getAll`
621
+ * docstring for the three-way split:
622
+ *
623
+ * - `OffsetPaginationResult<TDoc>` `page` param drives pagination.
624
+ * - `KeysetPaginationResult<TDoc>` — `sort` + optional `after` drives pagination.
625
+ * - `TDoc[]`raw array when neither drives pagination.
626
+ *
627
+ * Arc passes the kit's response verbatim; consumers narrow on shape.
351
628
  */
352
629
  type ListResult<TDoc> = OffsetPaginationResult<TDoc> | KeysetPaginationResult<TDoc> | TDoc[];
630
+ /**
631
+ * Controller-shape surface that the `Arc*Result` utilities read return
632
+ * types from. Internal — exported so the utility types can reference
633
+ * the minimal shape without a circular dependency on the full
634
+ * `BaseCrudController` / `BaseController` declarations.
635
+ */
636
+ type ArcControllerLike = {
637
+ list: (...args: any[]) => unknown;
638
+ get: (...args: any[]) => unknown;
639
+ create: (...args: any[]) => unknown;
640
+ update: (...args: any[]) => unknown;
641
+ delete: (...args: any[]) => unknown;
642
+ };
643
+ /**
644
+ * Return type of the controller's `list` method.
645
+ *
646
+ * @example
647
+ * ```ts
648
+ * class ProductController extends BaseController<Product> {
649
+ * async list(ctx: IRequestContext): ArcListResult<this> {
650
+ * // return shape inferred from BaseController.list — no need to
651
+ * // restate `Promise<IControllerResponse<ListResult<Product>>>`
652
+ * return super.list(ctx);
653
+ * }
654
+ * }
655
+ * ```
656
+ */
657
+ type ArcListResult<TCtrl extends ArcControllerLike> = ReturnType<TCtrl["list"]>;
658
+ /** Return type of the controller's `get` method. See {@link ArcListResult}. */
659
+ type ArcGetResult<TCtrl extends ArcControllerLike> = ReturnType<TCtrl["get"]>;
660
+ /** Return type of the controller's `create` method. See {@link ArcListResult}. */
661
+ type ArcCreateResult<TCtrl extends ArcControllerLike> = ReturnType<TCtrl["create"]>;
662
+ /** Return type of the controller's `update` method. See {@link ArcListResult}. */
663
+ type ArcUpdateResult<TCtrl extends ArcControllerLike> = ReturnType<TCtrl["update"]>;
664
+ /** Return type of the controller's `delete` method. See {@link ArcListResult}. */
665
+ type ArcDeleteResult<TCtrl extends ArcControllerLike> = ReturnType<TCtrl["delete"]>;
353
666
  interface BaseControllerOptions {
354
667
  /** Schema options for field sanitization */
355
668
  schemaOptions?: RouteSchemaOptions;
@@ -365,19 +678,9 @@ interface BaseControllerOptions {
365
678
  defaultLimit?: number;
366
679
  /**
367
680
  * Default sort applied when the request doesn't specify one.
368
- *
369
- * - `string` (default: `'-createdAt'`) sort descending on the field.
370
- * Uses the Mongo convention `-fieldName` for DESC; matches the
371
- * mongokit / mongokit-sort parser. Applied only if the resource's
372
- * schema actually has the column.
373
- * - `false` — disable the default sort. Requests that pass no `sort`
374
- * get whatever order the adapter returns (PK order on most kits).
375
- * **Use this for SQL/Drizzle resources that don't declare a
376
- * `createdAt` column** — the default would otherwise compile to
377
- * `ORDER BY "createdAt" DESC` against a missing column.
378
- *
379
- * The `-createdAt` default is kept for back-compat with existing
380
- * mongokit users; going forward, set this explicitly per resource.
681
+ * - `string` (default: `'-createdAt'`) — Mongo `-field` DESC convention.
682
+ * - `false` disable the default sort entirely (SQL/Drizzle resources
683
+ * without a `createdAt` column).
381
684
  */
382
685
  defaultSort?: string | false;
383
686
  /** Resource name for hook execution (e.g., 'product' -> 'product.created') */
@@ -389,22 +692,13 @@ interface BaseControllerOptions {
389
692
  */
390
693
  tenantField?: string | false;
391
694
  /**
392
- * Primary key field name (default: '_id').
393
- *
394
- * If not set, the controller auto-derives it from the repository's own
395
- * `idField` property (e.g. MongoKit's `Repository({ idField: 'id' })`),
396
- * so you only need to configure it in one place.
397
- *
398
- * Set explicitly to override the repo's setting (e.g. `'_id'` to opt out
399
- * of native pass-through and force the slug-translation path).
400
- *
401
- * Override for non-MongoDB adapters (e.g., 'id' for SQL databases).
695
+ * Primary key field name (default: '_id'). Auto-derives from the repo's
696
+ * own `idField` when unset.
402
697
  */
403
698
  idField?: string;
404
699
  /**
405
700
  * Custom filter matching for policy enforcement.
406
701
  * Provided by the DataAdapter for non-MongoDB databases (SQL, etc.).
407
- * Falls back to built-in MongoDB-style matching if not provided.
408
702
  */
409
703
  matchesFilter?: (item: unknown, filters: Record<string, unknown>) => boolean;
410
704
  /** Cache configuration for the resource */
@@ -416,26 +710,22 @@ interface BaseControllerOptions {
416
710
  };
417
711
  /**
418
712
  * Policy for requests that include fields the caller can't write.
419
- *
420
- * - `'reject'` (default): 403 with the denied field names. Surfaces
421
- * misconfigurations and attempts to set protected fields instead of
422
- * silently dropping them.
423
- * - `'strip'`: legacy silent-drop behaviour. Only opt in when migrating
424
- * code that relied on the pre-2.9 permissive default.
713
+ * - `'reject'` (default): 403 with denied field names.
714
+ * - `'strip'`: legacy silent-drop.
425
715
  */
426
716
  onFieldWriteDenied?: FieldWriteDenialPolicy;
427
717
  }
428
718
  /**
429
- * Framework-agnostic base controller implementing IController.
719
+ * Framework-agnostic CRUD controller implementing IController.
430
720
  *
431
- * Composes AccessControl, BodySanitizer, and QueryResolver for clean
432
- * separation of concerns. CRUD methods delegate directly to these
433
- * composed classes no intermediate wrapper methods.
721
+ * Composes AccessControl, BodySanitizer, and QueryResolver. All shared
722
+ * state and helpers are `protected` so the preset mixins (SoftDelete,
723
+ * Tree, Slug, Bulk) can extend cleanly.
434
724
  *
435
- * @template TDoc - The document type
436
- * @template TRepository - The repository type (defaults to RepositoryLike)
725
+ * @template TDoc - The document type.
726
+ * @template TRepository - The repository type (defaults to RepositoryLike).
437
727
  */
438
- declare class BaseController<TDoc = AnyRecord, TRepository extends RepositoryLike = RepositoryLike> implements IController<TDoc> {
728
+ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends RepositoryLike = RepositoryLike> implements IController<TDoc> {
439
729
  protected repository: TRepository;
440
730
  protected schemaOptions: RouteSchemaOptions;
441
731
  protected queryParser: QueryParserInterface;
@@ -450,95 +740,226 @@ declare class BaseController<TDoc = AnyRecord, TRepository extends RepositoryLik
450
740
  readonly accessControl: AccessControl;
451
741
  /** Composable body sanitization (field permissions, system fields) */
452
742
  readonly bodySanitizer: BodySanitizer;
453
- /** Composable query resolution (parsing, pagination, sort, select/populate) */
454
- readonly queryResolver: QueryResolver;
455
- private _matchesFilter?;
456
- private _presetFields;
457
- private _cacheConfig?;
743
+ /**
744
+ * Composable query resolution (parsing, pagination, sort, select/populate).
745
+ *
746
+ * Not `readonly` — `setQueryParser()` rebuilds this resolver to swap in a
747
+ * different parser (e.g. mongokit's `QueryParser`). `defineResource` calls
748
+ * it automatically when a resource supplies both `controller` and
749
+ * `queryParser`.
750
+ */
751
+ queryResolver: QueryResolver;
752
+ protected _matchesFilter?: (item: unknown, filters: Record<string, unknown>) => boolean;
753
+ protected _presetFields: {
754
+ slugField?: string;
755
+ parentField?: string;
756
+ };
757
+ protected _cacheConfig?: ResourceCacheConfig;
458
758
  constructor(repository: TRepository, options?: BaseControllerOptions);
459
759
  /**
460
- * Get the tenant field name if multi-tenant scoping is enabled.
461
- * Returns `undefined` when `tenantField` is `false` (platform-universal mode).
760
+ * Swap the controller's query parser. Rebuilds the internal `QueryResolver`
761
+ * with the new parser while preserving every other config.
762
+ *
763
+ * Closes the v2.10.9 gap where `defineResource({ controller, queryParser })`
764
+ * forwarded the parser only to auto-constructed controllers; user-supplied
765
+ * controllers kept their default `ArcQueryParser`. `defineResource` calls
766
+ * this via duck-typing when both `controller` and `queryParser` are
767
+ * supplied — controllers that don't implement `setQueryParser` are left
768
+ * untouched.
462
769
  *
463
- * Use this in subclass overrides instead of accessing `this.tenantField` directly
464
- * to avoid TypeScript indexing errors with `string | false`.
770
+ * Idempotent + safe to call repeatedly. Does NOT touch `maxLimit` or
771
+ * `defaultLimit` those are construction-time decisions.
772
+ */
773
+ setQueryParser(queryParser: QueryParserInterface): void;
774
+ /**
775
+ * Get the tenant field name if multi-tenant scoping is enabled.
776
+ * Returns `undefined` when `tenantField` is `false`.
465
777
  */
466
778
  protected getTenantField(): string | undefined;
467
779
  /**
468
780
  * Build top-level tenant options to thread into the repository call.
469
781
  *
470
- * **Why this exists:** repo plugins (e.g. `@classytic/mongokit`'s
471
- * `multiTenantPlugin`) read tenant scope from the TOP of the repository
472
- * operation context — `context.organizationId`, not `context.data.organizationId`
473
- * or `context.context.organizationId`. Without this stamping, a tenant-scoped
474
- * repository throws `Missing 'organizationId' in context for '<op>'` even
475
- * though arc already injected the tenant into the request body.
476
- *
477
- * **What this returns:**
478
- * - `{ [tenantField]: orgId }` when the resource is tenant-scoped and the
479
- * caller's scope carries an org ID (member, service key bound to an org,
480
- * elevated admin impersonating an org).
481
- * - `{}` otherwise — platform-universal resources (`tenantField: false`),
482
- * public/anonymous reads, elevated admins without an org target.
782
+ * Plugin-scoped repos (mongokit's `multiTenantPlugin`) read tenant scope
783
+ * from the TOP of the operation context — `context.organizationId`, not
784
+ * `context.data.organizationId`. Without this stamping, a tenant-scoped
785
+ * repo throws "Missing 'organizationId' in context" even when arc has
786
+ * injected the tenant into the request body.
483
787
  *
484
- * **Call sites:** every `this.repository.*` CRUD entry `create`, `update`,
485
- * `delete`, `getAll` (via list), plus merged into `QueryOptions` for the
486
- * access-controlled read path (`accessControl.fetchDetailed` `getById`/`getOne`).
487
- *
488
- * **Name of the field:** uses the instance's own `tenantField` configuration
489
- * (default `organizationId`). Matches mongokit's `multiTenantPlugin` default
490
- * `contextKey` so host apps don't need to override either side.
491
- *
492
- * Multi-field tenancy (via `multiTenantPreset({ tenantFields: [...] })`)
493
- * resolves additional fields at middleware time and stashes them on
494
- * `_tenantFields` — {@link tenantRepoOptions} merges those in too.
788
+ * Returns `{ [tenantField]: orgId }` for tenant-scoped + org-carrying
789
+ * requests, `{}` otherwise. Merges multi-field tenancy from
790
+ * `_tenantFields` (populated by `multiTenantPreset`).
495
791
  */
496
- private tenantRepoOptions;
792
+ protected tenantRepoOptions(req: IRequestContext): AnyRecord;
497
793
  /** Extract typed Arc internal metadata from request */
498
- private meta;
794
+ protected meta(req: IRequestContext): ArcInternalMetadata | undefined;
499
795
  /** Get hook system from request context (instance-scoped) */
500
- private getHooks;
796
+ protected getHooks(req: IRequestContext): HookSystem | null;
501
797
  /**
502
- * Resolve the repository primary key for mutation calls (update/delete/restore).
503
- *
504
- * When the resource declares a custom `idField` (e.g. `slug`, `jobId`, UUID),
505
- * the default behavior is to translate the route id → the fetched doc's `_id`
506
- * because most Mongo repositories key their mutation methods off `_id`.
798
+ * Resolve the repository primary key for mutation calls.
507
799
  *
508
- * Exception: if the repository itself exposes a matching `idField` property
509
- * (e.g. MongoKit's `new Repository(Model, [], {}, { idField: 'id' })`), the
510
- * repository already knows how to look up by that field — so we pass the
511
- * route id through unchanged and skip the translation.
800
+ * When the resource declares a custom `idField` (slug, jobId, UUID), the
801
+ * default behavior is to translate the route id → the fetched doc's `_id`
802
+ * because most Mongo repositories key mutation methods off `_id`.
512
803
  *
513
- * This makes `defineResource({ idField: 'id' })` work end-to-end with repos
514
- * that natively support custom primary keys, without breaking the slug-style
515
- * aliasing that Arc 2.6.3 introduced for repos keyed on `_id`.
804
+ * Exception: if the repo exposes a matching `idField` property (e.g.
805
+ * MongoKit's `new Repository(Model, [], {}, { idField: 'id' })`), the
806
+ * repo handles lookup itself pass the route id through unchanged.
516
807
  */
517
- private resolveRepoId;
808
+ protected resolveRepoId(id: string, existing: AnyRecord | null): string;
518
809
  /**
519
810
  * Centralized 404 response builder. Maps the denial reason from
520
811
  * `fetchDetailed()` into a structured `details.code` so consumers can
521
- * programmatically distinguish "doc doesn't exist" from "doc filtered
522
- * by policy/org scope" without parsing error strings.
523
- *
524
- * Error messages are intentionally vague in the `error` field (don't
525
- * leak whether the doc exists) — the detail is in `details.code` only.
812
+ * distinguish "doc doesn't exist" from "doc filtered by policy/org scope"
813
+ * without parsing error strings.
526
814
  */
527
- private notFoundResponse;
815
+ protected notFoundResponse(reason?: FetchDenialReason | null): IControllerResponse<never>;
528
816
  /** Resolve cache config for a specific operation, merging per-op overrides */
529
- private resolveCacheConfig;
817
+ protected resolveCacheConfig(operation: "list" | "byId"): QueryCacheConfig | null;
530
818
  /**
531
819
  * Extract user/org IDs from request for cache key scoping.
532
- * Only includes orgId when this resource uses tenant-scoped data (tenantField is set).
533
- * Universal resources (tenantField: false) get shared cache keys to avoid fragmentation.
820
+ * Only includes orgId when the resource uses tenant-scoped data (tenantField is set).
821
+ * Universal resources (tenantField: false) get shared cache keys.
534
822
  */
535
- private cacheScope;
823
+ protected cacheScope(req: IRequestContext): {
824
+ userId?: string;
825
+ orgId?: string;
826
+ };
536
827
  list(req: IRequestContext): Promise<IControllerResponse<ListResult<TDoc>>>;
537
828
  /** Execute list query through hooks (extracted for cache revalidation) */
538
- private executeListQuery;
829
+ protected executeListQuery(options: ParsedQuery, req: IRequestContext): Promise<ListResult<TDoc>>;
539
830
  get(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
540
831
  /** Execute get query through hooks (extracted for cache revalidation) */
541
- private executeGetQuery;
832
+ protected executeGetQuery(id: string, options: ParsedQuery, req: IRequestContext): Promise<{
833
+ doc: TDoc | null;
834
+ reason: FetchDenialReason | null;
835
+ }>;
836
+ create(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
837
+ update(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
838
+ delete(req: IRequestContext): Promise<IControllerResponse<{
839
+ message: string;
840
+ id?: string;
841
+ soft?: boolean;
842
+ }>>;
843
+ }
844
+ //#endregion
845
+ //#region src/core/mixins/bulk.d.ts
846
+ type Constructor$3<T> = new (...args: any[]) => T;
847
+ /** Public surface contributed by BulkMixin. */
848
+ interface BulkExt {
849
+ bulkCreate(req: IRequestContext): Promise<IControllerResponse<AnyRecord[]>>;
850
+ bulkUpdate(req: IRequestContext): Promise<IControllerResponse<{
851
+ matchedCount: number;
852
+ modifiedCount: number;
853
+ }>>;
854
+ bulkDelete(req: IRequestContext): Promise<IControllerResponse<{
855
+ deletedCount: number;
856
+ }>>;
857
+ }
858
+ declare function BulkMixin<TBase extends Constructor$3<BaseCrudController>>(Base: TBase): TBase & Constructor$3<BulkExt>;
859
+ //#endregion
860
+ //#region src/core/mixins/slug.d.ts
861
+ type Constructor$2<T> = new (...args: any[]) => T;
862
+ /** Public surface contributed by SlugMixin. */
863
+ interface SlugExt {
864
+ getBySlug(req: IRequestContext): Promise<IControllerResponse<AnyRecord>>;
865
+ }
866
+ declare function SlugMixin<TBase extends Constructor$2<BaseCrudController>>(Base: TBase): TBase & Constructor$2<SlugExt>;
867
+ //#endregion
868
+ //#region src/core/mixins/tree.d.ts
869
+ type Constructor$1<T> = new (...args: any[]) => T;
870
+ /** Public surface contributed by TreeMixin. */
871
+ interface TreeExt {
872
+ getTree(req: IRequestContext): Promise<IControllerResponse<AnyRecord[]>>;
873
+ getChildren(req: IRequestContext): Promise<IControllerResponse<AnyRecord[]>>;
874
+ }
875
+ declare function TreeMixin<TBase extends Constructor$1<BaseCrudController>>(Base: TBase): TBase & Constructor$1<TreeExt>;
876
+ //#endregion
877
+ //#region src/core/mixins/softDelete.d.ts
878
+ type Constructor<T> = new (...args: any[]) => T;
879
+ /** Public surface contributed by SoftDeleteMixin. */
880
+ interface SoftDeleteExt {
881
+ getDeleted(req: IRequestContext): Promise<IControllerResponse<PaginationResult<AnyRecord>>>;
882
+ restore(req: IRequestContext): Promise<IControllerResponse<AnyRecord>>;
883
+ }
884
+ declare function SoftDeleteMixin<TBase extends Constructor<BaseCrudController>>(Base: TBase): TBase & Constructor<SoftDeleteExt>;
885
+ //#endregion
886
+ //#region src/core/BaseController.d.ts
887
+ /**
888
+ * Fully-composed controller shape: all CRUD methods + every preset method
889
+ * (SoftDelete / Tree / Slug / Bulk) typed over the caller-supplied `TDoc`.
890
+ *
891
+ * **Inheritance summary** (what hover-docs should show when you reach for
892
+ * a method):
893
+ *
894
+ * `BaseController<TDoc>`
895
+ * └─ composes: `BaseCrudController` → `BulkMixin` → `SlugMixin` → `TreeMixin` → `SoftDeleteMixin`
896
+ *
897
+ * - From `BaseCrudController`: `list`, `get`, `create`, `update`, `delete`
898
+ * - From `BulkMixin`: `bulkCreate`, `bulkUpdate`, `bulkDelete`
899
+ * - From `SlugMixin`: `getBySlug`
900
+ * - From `TreeMixin`: `getTree`, `getChildren`
901
+ * - From `SoftDeleteMixin`: `getDeleted`, `restore`
902
+ *
903
+ * Hosts that only need CRUD extend `BaseCrudController` directly for a
904
+ * smaller surface. Hosts that want specific mixins compose them by hand
905
+ * (e.g. `class X extends SoftDeleteMixin(BaseCrudController<Doc>)`).
906
+ *
907
+ * ──────────────────────────────────────────────────────────────────────────
908
+ * ADR — why declaration merging + why `TDoc` carries `extends AnyRecord`
909
+ * ──────────────────────────────────────────────────────────────────────────
910
+ * The natural reflex is:
911
+ *
912
+ * class BaseController<TDoc> extends SoftDelete(Tree(Slug(Bulk(BaseCrud<TDoc>)))) {}
913
+ *
914
+ * TypeScript rejects this. Mixin factories can't receive a generic type
915
+ * parameter from the derived class (TS4.0 added limited support, but
916
+ * chained mixins over 4 levels deep still break because each factory
917
+ * would need its own TDoc binding — TS can't infer a shared `TDoc` across
918
+ * the chain without losing the base `extends Constructor<Base>` constraint).
919
+ *
920
+ * The composed runtime pins `BaseCrudController` to `AnyRecord` at the
921
+ * mixin-chain base; the TYPE surface hosts interact with threads `TDoc` so
922
+ * `new BaseController<Product>().list(req)` returns
923
+ * `Promise<IControllerResponse<ListResult<Product>>>` and not
924
+ * `ListResult<AnyRecord>`. Declaration merging bridges the two.
925
+ *
926
+ * **Why `TDoc extends AnyRecord` IS load-bearing:** the derived class
927
+ * inherits the mixin-composed base which is pinned to `AnyRecord`.
928
+ * Inherited methods return `ListResult<AnyRecord>` and the derived
929
+ * interface returns `ListResult<TDoc>`. For TS's "derived is assignable
930
+ * to base" check to pass, `TDoc` must be assignable to `AnyRecord` —
931
+ * which requires the `extends AnyRecord` bound. Dropping the bound fails
932
+ * with `Type 'TDoc[]' is not assignable to type 'AnyRecord[]'` at the
933
+ * class declaration. Hosts therefore need one of:
934
+ * (a) `class X extends BaseController { ... }` — drop the generic,
935
+ * lose return-type narrowing for `list` / `get` / etc.
936
+ * (b) `interface IUser extends AnyRecord { ... }` — add an index
937
+ * signature to the domain interface (preferred when you own it).
938
+ * (c) Use the utility types (`ArcListResult<typeof this>`,
939
+ * `ArcCreateResult<typeof this>`) when overriding methods — they
940
+ * read the return type from the class, sidestepping the bound
941
+ * for method bodies even when `TDoc` is unbound elsewhere.
942
+ *
943
+ * **Why `TRepository` defaults to `RepositoryLike<TDoc>`:** keeps
944
+ * diagnostics symmetric. With the older `RepositoryLike = RepositoryLike<unknown>`
945
+ * default, error messages mixed `AnyRecord` and `unknown` in the same
946
+ * signature, which confused the reader about where the mismatch was.
947
+ *
948
+ * **Cost this pays:** every method that participates in the `TDoc`
949
+ * narrowing is redeclared on the interface. Adding a new method to a
950
+ * mixin means updating this interface too. The alternative (losing
951
+ * host-side generics OR rewriting mixins as a non-generic union) is
952
+ * strictly worse.
953
+ *
954
+ * Spec reference: https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-classes-with-other-types
955
+ */
956
+ interface BaseController<TDoc extends AnyRecord = AnyRecord, TRepository extends RepositoryLike = RepositoryLike<TDoc>> {
957
+ readonly accessControl: AccessControl;
958
+ readonly bodySanitizer: BodySanitizer;
959
+ queryResolver: QueryResolver;
960
+ setQueryParser(queryParser: QueryParserInterface): void;
961
+ list(req: IRequestContext): Promise<IControllerResponse<ListResult<TDoc>>>;
962
+ get(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
542
963
  create(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
543
964
  update(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
544
965
  delete(req: IRequestContext): Promise<IControllerResponse<{
@@ -546,72 +967,30 @@ declare class BaseController<TDoc = AnyRecord, TRepository extends RepositoryLik
546
967
  id?: string;
547
968
  soft?: boolean;
548
969
  }>>;
549
- getBySlug(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
550
970
  getDeleted(req: IRequestContext): Promise<IControllerResponse<PaginationResult<TDoc>>>;
551
971
  restore(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
552
972
  getTree(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
553
973
  getChildren(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
974
+ getBySlug(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
554
975
  bulkCreate(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
555
- /**
556
- * Build a tenant-scoped filter for bulk update/delete.
557
- *
558
- * Mirrors `AccessControl.buildIdFilter` semantics for single-doc ops:
559
- * - Always merge `_policyFilters` (from permission middleware)
560
- * - When `tenantField` is set AND a `member` scope is present, add the
561
- * org filter so cross-tenant data can't be touched.
562
- * - When the scope is `elevated` (platform admin), no org filter is
563
- * applied — admins can bulk-update across orgs intentionally.
564
- * - When the scope is `public` on a tenant-scoped resource, deny.
565
- * - When NO scope is present at all (e.g., direct controller calls in
566
- * unit tests, or app routes without auth middleware), the controller
567
- * stays lenient — it's the middleware layer's job to fail-close.
568
- * Apps that want fail-close on bulk routes should run the multi-tenant
569
- * preset middleware (or equivalent) ahead of these handlers.
570
- *
571
- * Returns the merged filter, or `null` when access must be denied.
572
- */
573
- private buildBulkFilter;
574
- /**
575
- * Sanitize a bulk update data payload through the same write-permission
576
- * pipeline as single-doc update(). Handles both shapes:
577
- *
578
- * - Flat: `{ name: 'x', status: 'y' }`
579
- * - Mongo operator: `{ $set: { name: 'x' }, $inc: { views: 1 }, $unset: { tag: '' } }`
580
- *
581
- * For each operand, runs `bodySanitizer.sanitize('update', ...)` so that
582
- * system fields, systemManaged/readonly/immutable rules, AND field-level
583
- * write permissions are enforced. Without this, a tenant-scoped user could
584
- * pass `{ $set: { organizationId: 'org-b' } }` to move records across orgs.
585
- *
586
- * Returns the sanitized payload along with the list of stripped fields for
587
- * audit/error reporting.
588
- */
589
- private sanitizeBulkUpdateData;
590
976
  bulkUpdate(req: IRequestContext): Promise<IControllerResponse<{
591
977
  matchedCount: number;
592
978
  modifiedCount: number;
593
979
  }>>;
594
- /**
595
- * Bulk delete by `filter` or `ids`.
596
- *
597
- * Body shape (one of):
598
- * - `{ filter: { status: 'archived' } }` — delete by query filter
599
- * - `{ ids: ['id1', 'id2', 'id3'] }` — delete specific docs by id
600
- *
601
- * The `ids` form translates to `{ [idField]: { $in: ids } }` using the
602
- * resource's `idField` (so it works with custom PKs like `slug`, `jobId`,
603
- * UUID, etc.). Tenant scope and policy filters are merged in either way,
604
- * so cross-tenant deletes are blocked at the controller layer.
605
- *
606
- * Both forms perform a single `repo.deleteMany()` DB call — no per-doc
607
- * fetch loop. Per-doc lifecycle hooks (`before:delete`/`after:delete`) do
608
- * NOT fire for bulk operations; use the single-doc `delete()` if you need
609
- * them, or subscribe to the bulk lifecycle event from the events plugin.
610
- */
611
980
  bulkDelete(req: IRequestContext): Promise<IControllerResponse<{
612
981
  deletedCount: number;
613
982
  }>>;
614
983
  }
984
+ declare const BaseController_base: (new (repository: any, options?: BaseControllerOptions) => BaseCrudController<AnyRecord, any>) & (new (...args: any[]) => BulkExt) & (new (...args: any[]) => SlugExt) & (new (...args: any[]) => TreeExt) & (new (...args: any[]) => SoftDeleteExt);
985
+ /**
986
+ * Fully-composed controller: `BaseCrudController` + SoftDelete + Tree +
987
+ * Slug + Bulk. Drop-in replacement for the pre-2.11 god class. The
988
+ * companion interface above gives every method full generic precision
989
+ * on `TDoc` via declaration merging.
990
+ */
991
+ declare class BaseController<TDoc extends AnyRecord = AnyRecord, TRepository extends RepositoryLike = RepositoryLike<TDoc>> extends BaseController_base {
992
+ readonly _phantom?: [TDoc, TRepository];
993
+ }
615
994
  //#endregion
616
995
  //#region src/types/base.d.ts
617
996
  declare module "fastify" {
@@ -655,8 +1034,6 @@ type ObjectId = string | {
655
1034
  type UserLike = UserBase & {
656
1035
  /** User email (optional) */email?: string;
657
1036
  };
658
- /** Extract user ID from a user object (supports both id and _id). */
659
- declare function getUserId(user: UserLike | null | undefined): string | undefined;
660
1037
  interface UserOrganization {
661
1038
  userId: string;
662
1039
  organizationId: string;
@@ -697,22 +1074,6 @@ type ArcRequest = FastifyRequest & {
697
1074
  user: Record<string, unknown> | undefined;
698
1075
  signal: AbortSignal;
699
1076
  };
700
- /**
701
- * Wrap data in Arc's standard `{ success: true, data }` envelope.
702
- *
703
- * @example
704
- * ```typescript
705
- * handler: async (req, reply) => {
706
- * const data = await getResults();
707
- * return envelope(data); // → { success: true, data }
708
- * }
709
- * ```
710
- */
711
- declare function envelope<T>(data: T, meta?: Record<string, unknown>): {
712
- success: true;
713
- data: T;
714
- [key: string]: unknown;
715
- };
716
1077
  //#endregion
717
1078
  //#region src/types/auth.d.ts
718
1079
  /**
@@ -1285,60 +1646,138 @@ interface RateLimitConfig {
1285
1646
  /** Time window for rate limiting (e.g., '1 minute', '15 seconds') */
1286
1647
  timeWindow: string;
1287
1648
  }
1288
- interface RouteSchemaOptions {
1649
+ /**
1650
+ * Per-field rule — arc's extension of repo-core's 4-field `FieldRule` floor
1651
+ * (`immutable`, `immutableAfterCreate`, `systemManaged`, `optional`) with
1652
+ * the constraint / UI / security bits arc layers on top.
1653
+ *
1654
+ * Kept structurally compatible with `@classytic/repo-core/schema`'s
1655
+ * `FieldRule` so arc's `fieldRules: Record<string, ArcFieldRule>` flows
1656
+ * into mongokit's / sqlitekit's `buildCrudSchemasFromModel(..., options)`
1657
+ * without a cast. See `RouteSchemaOptions` JSDoc for the full rationale.
1658
+ */
1659
+ interface ArcFieldRule extends FieldRule {
1660
+ /**
1661
+ * When `true`, bypass the `systemManaged` / `readonly` / `immutable`
1662
+ * strip in `BodySanitizer` for callers whose request scope is
1663
+ * `elevated`. Lets platform admins stamp the value from the request
1664
+ * body — needed for cross-tenant admin writes where the tenant field
1665
+ * is the only way to pick a target org.
1666
+ *
1667
+ * Auto-set by `defineResource` on the configured `tenantField`. Hosts
1668
+ * can set it manually on other fields (e.g. `createdBy`) if they want
1669
+ * elevation-only override semantics for those too.
1670
+ *
1671
+ * Has no effect when `isElevated(scope)` is false — member and
1672
+ * service callers continue to have the field stripped.
1673
+ */
1674
+ preserveForElevated?: boolean;
1675
+ hidden?: boolean;
1676
+ /** String minimum length — auto-maps to OpenAPI `minLength` and MCP tool schema */
1677
+ minLength?: number;
1678
+ /** String maximum length — auto-maps to OpenAPI `maxLength` and MCP tool schema */
1679
+ maxLength?: number;
1680
+ /** Number minimum — auto-maps to OpenAPI `minimum` and MCP tool schema */
1681
+ min?: number;
1682
+ /** Number maximum — auto-maps to OpenAPI `maximum` and MCP tool schema */
1683
+ max?: number;
1684
+ /** Regex pattern — auto-maps to OpenAPI `pattern` and MCP tool schema */
1685
+ pattern?: string;
1686
+ /** Allowed values — auto-maps to OpenAPI `enum` and MCP tool schema */
1687
+ enum?: ReadonlyArray<string | number>;
1688
+ /**
1689
+ * When `true`, widen the JSON Schema `type` of this field to also
1690
+ * accept `null`. Mirrors Zod's `.nullable()` at the arc config layer
1691
+ * for kit-generated schemas that don't carry the flag end-to-end
1692
+ * (e.g. Zod → Mongoose → mongokit drops `.nullable()` because
1693
+ * Mongoose has no first-class nullable marker unless `default: null`
1694
+ * is also set).
1695
+ *
1696
+ * Applied post-kit by `mergeFieldRuleConstraints`: if the adapter
1697
+ * emitted `{ type: 'string', enum: [...] }` for a field arc should
1698
+ * accept null for, the merge widens it to
1699
+ * `{ type: ['string', 'null'], enum: [...] }` — draft-7 tuple form
1700
+ * AJV 8 validates natively.
1701
+ *
1702
+ * No-op when the property already declares `type: [...,'null']` or
1703
+ * an `anyOf: [..., { type: 'null' }]` branch — arc never fights the
1704
+ * kit's own output.
1705
+ */
1706
+ nullable?: boolean;
1707
+ /** Human-readable description — auto-maps to OpenAPI `description` */
1708
+ description?: string;
1709
+ [key: string]: unknown;
1710
+ }
1711
+ /**
1712
+ * Schema-shaping options for a resource.
1713
+ *
1714
+ * Extends `@classytic/repo-core/schema`'s `SchemaBuilderOptions` so every
1715
+ * kit-generator callback typed against the repo-core contract
1716
+ * (mongokit's `buildCrudSchemasFromModel`, sqlitekit's
1717
+ * `buildCrudSchemasFromTable`, prismakit's equivalent) accepts arc's
1718
+ * options bag directly — no `as SchemaBuilderOptions` / `Parameters<...>[1]`
1719
+ * cast at the host wiring site.
1720
+ *
1721
+ * Inherited from `SchemaBuilderOptions`:
1722
+ * - `strictAdditionalProperties` — emit `additionalProperties: false`
1723
+ * - `dateAs` — `'date'` vs `'datetime'` ISO rendering
1724
+ * - `softRequiredFields` — stay in `properties`, drop from `required[]`
1725
+ * - `create: { omitFields, requiredOverrides, optionalOverrides, schemaOverrides }`
1726
+ * - `update: { omitFields, requireAtLeastOne }`
1727
+ * - `query: { filterableFields }` (kit-native filter declaration)
1728
+ * - `openApiExtensions` — emit `x-*` vendor keywords for docgen
1729
+ *
1730
+ * Arc adds:
1731
+ * - `fieldRules` with the richer `ArcFieldRule` per-entry shape
1732
+ * (preserveForElevated, minLength/maxLength/min/max/pattern, enum,
1733
+ * nullable, description) — arc's extensions are applied post-kit by
1734
+ * `mergeFieldRuleConstraints`; the kit only sees the repo-core floor.
1735
+ * - `hiddenFields` / `readonlyFields` / `requiredFields` / `optionalFields`
1736
+ * / `excludeFields` — arc-only convenience lists that predate fieldRules.
1737
+ * Keep using them if they're already in place; new code should prefer
1738
+ * `fieldRules` for per-field control.
1739
+ * - `filterableFields: string[]` — top-level list arc's MCP layer auto-
1740
+ * derives from `QueryParser.allowedFilterFields`. Distinct from the
1741
+ * inherited `query.filterableFields: Record<...>` which feeds the kit's
1742
+ * list-query schema; nothing stops a resource from using both.
1743
+ *
1744
+ * **Why extend rather than duplicate**: mongokit's
1745
+ * `buildCrudSchemasFromModel(model, options: SchemaBuilderOptions)` is the
1746
+ * canonical callback shape. Before the extension, hosts wrote
1747
+ * `Parameters<typeof buildCrudSchemasFromModel>[1]` or
1748
+ * `as SchemaBuilderOptions` at every wiring site — a defensive cast with
1749
+ * no runtime effect. Extension locks the structural relationship at the
1750
+ * type layer so the cast is compile-verified gone.
1751
+ */
1752
+ interface RouteSchemaOptions extends SchemaBuilderOptions {
1289
1753
  hiddenFields?: string[];
1290
1754
  readonlyFields?: string[];
1291
1755
  requiredFields?: string[];
1292
1756
  optionalFields?: string[];
1293
1757
  excludeFields?: string[];
1294
1758
  /**
1295
- * Fields allowed for filtering in list operations. MCP auto-derives
1296
- * from `QueryParser.allowedFilterFields` when not set explicitly.
1297
- */
1298
- filterableFields?: string[];
1299
- fieldRules?: Record<string, {
1300
- systemManaged?: boolean;
1301
- /**
1302
- * When `true`, bypass the `systemManaged` / `readonly` / `immutable`
1303
- * strip in `BodySanitizer` for callers whose request scope is
1304
- * `elevated`. Lets platform admins stamp the value from the request
1305
- * body — needed for cross-tenant admin writes where the tenant field
1306
- * is the only way to pick a target org.
1307
- *
1308
- * Auto-set by `defineResource` on the configured `tenantField`. Hosts
1309
- * can set it manually on other fields (e.g. `createdBy`) if they want
1310
- * elevation-only override semantics for those too.
1311
- *
1312
- * Has no effect when `isElevated(scope)` is false — member and
1313
- * service callers continue to have the field stripped.
1314
- */
1315
- preserveForElevated?: boolean;
1316
- hidden?: boolean;
1317
- immutable?: boolean;
1318
- immutableAfterCreate?: boolean;
1319
- optional?: boolean; /** String minimum length — auto-maps to OpenAPI `minLength` and MCP tool schema */
1320
- minLength?: number; /** String maximum length — auto-maps to OpenAPI `maxLength` and MCP tool schema */
1321
- maxLength?: number; /** Number minimum — auto-maps to OpenAPI `minimum` and MCP tool schema */
1322
- min?: number; /** Number maximum — auto-maps to OpenAPI `maximum` and MCP tool schema */
1323
- max?: number; /** Regex pattern — auto-maps to OpenAPI `pattern` and MCP tool schema */
1324
- pattern?: string; /** Allowed values — auto-maps to OpenAPI `enum` and MCP tool schema */
1325
- enum?: ReadonlyArray<string | number>; /** Human-readable description — auto-maps to OpenAPI `description` */
1326
- description?: string;
1327
- [key: string]: unknown;
1328
- }>;
1329
- /** Query parameter schema for OpenAPI */
1330
- query?: Record<string, unknown>;
1759
+ * Fields allowed for filtering in list operations. MCP auto-derives
1760
+ * from `QueryParser.allowedFilterFields` when not set explicitly.
1761
+ *
1762
+ * Distinct from the inherited `query.filterableFields: Record<...>`
1763
+ * from `SchemaBuilderOptions` — that entry feeds the kit's list-query
1764
+ * JSON Schema; this one is arc's MCP-auto-derivation list.
1765
+ */
1766
+ filterableFields?: string[];
1331
1767
  /**
1332
- * When `true`, emitted CRUD body schemas set `additionalProperties: false`
1333
- * so AJV rejects unknown fields on create / update. Honored by kit schema
1334
- * generators that receive this options bag (sqlitekit's
1335
- * `buildCrudSchemasFromTable`, pgkit's equivalent). Mongoose-based
1336
- * generators may ignore it — Mongoose schemas are inherently strict at
1337
- * the model level.
1768
+ * Per-field rules. Richer than repo-core's `FieldRules` arc adds
1769
+ * `preserveForElevated`, constraint hints (`minLength`, `enum`,
1770
+ * `nullable`, etc.), and `description` on top of the four-flag floor
1771
+ * (`immutable`, `immutableAfterCreate`, `systemManaged`, `optional`).
1772
+ *
1773
+ * Structurally compatible: `Record<string, ArcFieldRule>` is assignable
1774
+ * to repo-core's `Record<string, FieldRule>` since `ArcFieldRule extends
1775
+ * FieldRule`. Kits see only the floor; arc's extensions are applied
1776
+ * post-kit by `mergeFieldRuleConstraints`.
1338
1777
  */
1339
- strictAdditionalProperties?: boolean;
1778
+ fieldRules?: Record<string, ArcFieldRule>;
1340
1779
  }
1341
- interface FieldRule {
1780
+ interface FieldRule$1 {
1342
1781
  field: string;
1343
1782
  required?: boolean;
1344
1783
  readonly?: boolean;
@@ -1834,263 +2273,29 @@ interface ResourceConfig<TDoc = AnyRecord> {
1834
2273
  };
1835
2274
  }
1836
2275
  //#endregion
1837
- //#region src/hooks/HookSystem.d.ts
1838
- type HookPhase = "before" | "around" | "after";
1839
- type HookOperation = "create" | "update" | "delete" | "restore" | "read" | "list";
1840
- interface HookContext<T = AnyRecord> {
1841
- resource: string;
1842
- operation: HookOperation;
1843
- phase: HookPhase;
1844
- data?: T;
1845
- result?: T | T[];
1846
- user?: UserBase;
1847
- context?: RequestContext;
1848
- meta?: AnyRecord;
1849
- }
1850
- type HookHandler<T = AnyRecord> = (ctx: HookContext<T>) => void | Promise<void> | T | Promise<T>;
1851
- /**
1852
- * Around hook handler — wraps the core operation.
1853
- * Call `next()` to proceed to the next around hook or the actual operation.
1854
- */
1855
- type AroundHookHandler<T = AnyRecord> = (ctx: HookContext<T>, next: () => Promise<T | undefined>) => T | undefined | Promise<T | undefined>;
1856
- interface HookRegistration {
1857
- /** Hook name for dependency resolution and debugging */
1858
- name?: string;
1859
- resource: string;
1860
- operation: HookOperation;
1861
- phase: HookPhase;
1862
- handler: HookHandler;
1863
- priority: number;
1864
- /** Names of hooks that must execute before this one */
1865
- dependsOn?: string[];
1866
- }
1867
- interface HookSystemOptions {
1868
- /** Custom logger for error/warning reporting. Defaults to console */
1869
- logger?: {
1870
- error: (message: string, ...args: unknown[]) => void;
1871
- warn?: (message: string, ...args: unknown[]) => void;
1872
- };
1873
- }
1874
- declare class HookSystem {
1875
- private hooks;
1876
- private logger;
1877
- private warn;
1878
- constructor(options?: HookSystemOptions);
1879
- /**
1880
- * Generate hook key
1881
- */
1882
- private getKey;
1883
- /**
1884
- * Register a hook
1885
- * Supports both object parameter and positional arguments
1886
- */
1887
- register<T = AnyRecord>(resourceOrOptions: string | {
1888
- name?: string;
1889
- resource: string;
1890
- operation: HookOperation;
1891
- phase: HookPhase;
1892
- handler: HookHandler<T>;
1893
- priority?: number;
1894
- dependsOn?: string[];
1895
- }, operation?: HookOperation, phase?: HookPhase, handler?: HookHandler<T>, priority?: number): () => void;
1896
- /**
1897
- * Register before hook
1898
- */
1899
- before<T = AnyRecord>(resource: string, operation: HookOperation, handler: HookHandler<T>, priority?: number): () => void;
1900
- /**
1901
- * Register after hook
1902
- */
1903
- after<T = AnyRecord>(resource: string, operation: HookOperation, handler: HookHandler<T>, priority?: number): () => void;
1904
- /**
1905
- * Register around hook — wraps the core operation.
1906
- * Call `next()` inside the handler to proceed.
1907
- */
1908
- around<T = AnyRecord>(resource: string, operation: HookOperation, handler: AroundHookHandler<T>, priority?: number): () => void;
1909
- /**
1910
- * Execute around hooks as a nested middleware chain.
1911
- * Each around hook receives `next()` to call the next hook or the core operation.
1912
- */
1913
- executeAround<T = AnyRecord>(resource: string, operation: HookOperation, data: T, execute: () => Promise<T | undefined>, options?: {
1914
- user?: UserBase;
1915
- context?: RequestContext;
1916
- meta?: AnyRecord;
1917
- }): Promise<T | undefined>;
1918
- /**
1919
- * Execute hooks for a given context
1920
- */
1921
- execute<T = AnyRecord>(ctx: HookContext<T>): Promise<T | undefined>;
1922
- /**
1923
- * Execute before hooks
1924
- */
1925
- executeBefore<T = AnyRecord>(resource: string, operation: HookOperation, data: T, options?: {
1926
- user?: UserBase;
1927
- context?: RequestContext;
1928
- meta?: AnyRecord;
1929
- }): Promise<T>;
1930
- /**
1931
- * Execute after hooks
1932
- * Errors in after hooks are logged but don't fail the request
1933
- */
1934
- executeAfter<T = AnyRecord>(resource: string, operation: HookOperation, result: T | T[], options?: {
1935
- user?: UserBase;
1936
- context?: RequestContext;
1937
- meta?: AnyRecord;
1938
- }): Promise<void>;
1939
- /**
1940
- * Topological sort with Kahn's algorithm.
1941
- * Hooks with `dependsOn` are ordered after their dependencies.
1942
- * Within the same dependency level, priority ordering is preserved.
1943
- * Hooks without names or dependencies pass through in their original order.
1944
- */
1945
- private topologicalSort;
1946
- /**
1947
- * Get all registered hooks
1948
- */
1949
- getAll(): HookRegistration[];
1950
- /**
1951
- * Get hooks for a specific resource
1952
- */
1953
- getForResource(resource: string): HookRegistration[];
1954
- /**
1955
- * Get hooks matching filter criteria.
1956
- * Useful for debugging and testing specific hook combinations.
1957
- *
1958
- * @example
1959
- * ```typescript
1960
- * // Find all before-create hooks for products (including wildcards)
1961
- * const hooks = hookSystem.getRegistered({
1962
- * resource: 'product',
1963
- * operation: 'create',
1964
- * phase: 'before',
1965
- * });
1966
- * ```
1967
- */
1968
- getRegistered(filter?: {
1969
- resource?: string;
1970
- operation?: HookOperation;
1971
- phase?: HookPhase;
1972
- }): HookRegistration[];
1973
- /**
1974
- * Get a structured summary of all registered hooks for debugging.
1975
- *
1976
- * @example
1977
- * ```typescript
1978
- * const info = hookSystem.inspect();
1979
- * // { total: 12, resources: { product: [...], '*': [...] }, summary: [...] }
1980
- * ```
1981
- */
1982
- inspect(): {
1983
- total: number;
1984
- resources: Record<string, HookRegistration[]>;
1985
- summary: Array<{
1986
- name?: string;
1987
- key: string;
1988
- priority: number;
1989
- dependsOn?: string[];
1990
- }>;
1991
- };
1992
- /**
1993
- * Check if any hooks exist for a specific resource/operation/phase combination.
1994
- */
1995
- has(resource: string, operation: HookOperation, phase: HookPhase): boolean;
1996
- /**
1997
- * Clear all hooks
1998
- */
1999
- clear(): void;
2000
- /**
2001
- * Clear hooks for a specific resource
2002
- */
2003
- clearResource(resource: string): void;
2004
- }
2005
- /**
2006
- * Create a new isolated HookSystem instance
2007
- *
2008
- * Use this for:
2009
- * - Test isolation (parallel test suites)
2010
- * - Multiple app instances with independent hooks
2011
- *
2012
- * @example
2013
- * const hooks = createHookSystem();
2014
- * await app.register(arcCorePlugin, { hookSystem: hooks });
2015
- *
2016
- * @example With custom logger
2017
- * const hooks = createHookSystem({ logger: fastify.log });
2018
- */
2019
- declare function createHookSystem(options?: HookSystemOptions): HookSystem;
2020
- interface DefineHookOptions<T = AnyRecord> {
2021
- /** Unique hook name (required for dependency resolution) */
2022
- name: string;
2023
- /** Target resource */
2024
- resource: string;
2025
- /** CRUD operation */
2026
- operation: HookOperation;
2027
- /** before or after */
2028
- phase: HookPhase;
2029
- /** Hook handler */
2030
- handler: HookHandler<T>;
2031
- /** Priority (lower = earlier, default: 10) */
2032
- priority?: number;
2033
- /** Names of hooks that must execute before this one */
2034
- dependsOn?: string[];
2035
- }
2276
+ //#region src/core/defineResource.d.ts
2036
2277
  /**
2037
- * Define a named hook with optional dependencies.
2038
- * Returns a registration object — call `register(hookSystem)` to activate.
2278
+ * Define a resource with database adapter.
2039
2279
  *
2040
- * @example
2041
- * ```typescript
2042
- * const generateSlug = defineHook({
2043
- * name: 'generateSlug',
2044
- * resource: 'product', operation: 'create', phase: 'before',
2045
- * handler: (ctx) => ({ ...ctx.data, slug: slugify(ctx.data.name) }),
2046
- * });
2280
+ * This is the MAIN entry point for creating Arc resources — the adapter
2281
+ * provides both repository and schema metadata.
2047
2282
  *
2048
- * const validateUniqueSlug = defineHook({
2049
- * name: 'validateUniqueSlug',
2050
- * resource: 'product', operation: 'create', phase: 'before',
2051
- * dependsOn: ['generateSlug'],
2052
- * handler: async (ctx) => { // check uniqueness },
2053
- * });
2283
+ * Staged into seven named phases so future refactors touch one phase at a
2284
+ * time instead of threading changes through a 450-line function:
2054
2285
  *
2055
- * // Register on a hook system
2056
- * generateSlug.register(hooks);
2057
- * validateUniqueSlug.register(hooks);
2058
- * ```
2059
- */
2060
- declare function defineHook<T = AnyRecord>(options: DefineHookOptions<T>): DefineHookOptions<T> & {
2061
- register: (hooks: HookSystem) => () => void;
2062
- };
2063
- /**
2064
- * Create a before-create hook registration for a given hook system
2065
- */
2066
- declare function beforeCreate<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
2067
- /**
2068
- * Create an after-create hook registration for a given hook system
2069
- */
2070
- declare function afterCreate<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
2071
- /**
2072
- * Create a before-update hook registration for a given hook system
2073
- */
2074
- declare function beforeUpdate<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
2075
- /**
2076
- * Create an after-update hook registration for a given hook system
2077
- */
2078
- declare function afterUpdate<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
2079
- /**
2080
- * Create a before-delete hook registration for a given hook system
2081
- */
2082
- declare function beforeDelete<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
2083
- /**
2084
- * Create an after-delete hook registration for a given hook system
2085
- */
2086
- declare function afterDelete<T = AnyRecord>(hooks: HookSystem, resource: string, handler: HookHandler<T>, priority?: number): () => void;
2087
- //#endregion
2088
- //#region src/core/defineResource.d.ts
2089
- /**
2090
- * Define a resource with database adapter
2286
+ * 1. validate — fail-fast structural checks
2287
+ * 2. resolveIdField — auto-derive `idField` from repository
2288
+ * 3. applyPresetsAndAutoInject — clone + apply presets + tenant-field rules
2289
+ * 4. resolveController — reuse user controller or auto-create BaseController
2290
+ * 5. buildResource — construct ResourceDefinition + validate methods
2291
+ * 6. wireHooks — push preset + inline `config.hooks` onto _pendingHooks
2292
+ * 7. resolveOpenApiSchemas — adapter schemas parser listQuery → user override
2091
2293
  *
2092
- * This is the MAIN entry point for creating Arc resources.
2093
- * The adapter provides both repository and schema metadata.
2294
+ * Each phase has a single responsibility; `resolvedConfig` is the canonical
2295
+ * post-preset, post-auto-inject config that every later phase reads. Raw
2296
+ * `config` is only consulted for things presets don't touch (adapter,
2297
+ * skipRegistry, skipValidation, hooks — which are wired separately from
2298
+ * preset hooks).
2094
2299
  */
2095
2300
  declare function defineResource<TDoc = AnyRecord>(config: ResourceConfig<TDoc>): ResourceDefinition<TDoc>;
2096
2301
  interface ResolvedResourceConfig<TDoc = AnyRecord> extends ResourceConfig<TDoc> {
@@ -2147,7 +2352,7 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
2147
2352
  _registryMeta?: RegisterOptions;
2148
2353
  constructor(config: ResolvedResourceConfig<TDoc>);
2149
2354
  /** Get repository from adapter (if available) */
2150
- get repository(): _$_classytic_repo_core_repository0.StandardRepo<TDoc> | RepositoryLike<TDoc> | undefined;
2355
+ get repository(): RepositoryLike<TDoc> | undefined;
2151
2356
  _validateControllerMethods(): void;
2152
2357
  toPlugin(): FastifyPluginAsync;
2153
2358
  /**
@@ -2705,4 +2910,4 @@ interface ValidateOptions {
2705
2910
  strict?: boolean;
2706
2911
  }
2707
2912
  //#endregion
2708
- export { beforeCreate as $, ObjectId as $t, RequestIdOptions as A, FastifyHandler as At, ResourceDefinition as B, PipelineContext as Bt, RequestContext as C, ResourcePermissions as Ct, HealthCheck as D, RouteSchemaOptions as Dt, GracefulShutdownOptions as E, RouteMethod as Et, FastifyWithDecorators as F, Guard as Ft, HookOperation as G, Authenticator as Gt, DefineHookOptions as H, Transform as Ht, MiddlewareHandler as I, Interceptor as It, HookSystem as J, TokenPair as Jt, HookPhase as K, AuthenticatorContext as Kt, RequestWithExtras as L, NextFunction as Lt, EventsDecorator as M, IControllerResponse as Mt, FastifyRequestExtras as N, IRequestContext as Nt, HealthOptions as O, ControllerHandler as Ot, FastifyWithAuth as P, RouteHandler as Pt, afterUpdate as Q, JWTPayload as Qt, RegisterOptions as R, OperationFilter as Rt, QueryParserInterface as S, ResourceHooks as St, CrudRouterOptions as T, RouteMcpConfig as Tt, HookContext as U, AuthHelpers as Ut, defineResource as V, PipelineStep as Vt, HookHandler as W, AuthPluginOptions as Wt, afterCreate as X, ApiResponse as Xt, HookSystemOptions as Y, AnyRecord as Yt, afterDelete as Z, ArcRequest as Zt, ControllerQueryOptions as _, RepositoryLike as _n, PresetResult as _t, InferAdapterDoc as a, BaseControllerOptions as an, ActionEntry as at, ParsedQuery as b, ResourceConfig as bt, TypedController as c, BodySanitizer as cn, CrudController as ct, PaginationResult as d, AccessControlConfig as dn, EventDefinition as dt, UserLike as en, beforeDelete as et, IntrospectionData as f, AdapterFactory as fn, FieldRule as ft, ArcInternalMetadata as g, RelationMetadata as gn, PresetHook as gt, ResourceMetadata as h, FieldMetadata as hn, PresetFunction as ht, ValidationResult as i, BaseController as in, ActionDefinition as it, ArcDecorator as j, IController as jt, IntrospectionPluginOptions as k, ControllerLike as kt, TypedRepository as l, BodySanitizerConfig as ln, CrudRouteKey as lt, RegistryStats as m, DataAdapter as mn, OpenApiSchemas as mt, ConfigError as n, envelope as nn, createHookSystem as nt, InferDocType as o, QueryResolver as on, ActionHandlerFn as ot, RegistryEntry as p, AdapterSchemaContext as pn, MiddlewareConfig as pt, HookRegistration as q, JwtContext as qt, ValidateOptions as r, getUserId as rn, defineHook as rt, InferResourceDoc as s, QueryResolverConfig as sn, ActionsMap as st, RouteHandlerMethod$1 as t, UserOrganization as tn, beforeUpdate as tt, TypedResourceConfig as u, AccessControl as un, CrudSchemas as ut, LookupOption as v, SchemaMetadata as vn, RateLimitConfig as vt, ServiceContext as w, RouteDefinition as wt, PopulateOption as x, ResourceHookContext as xt, OwnershipCheck as y, ValidationResult$1 as yn, ResourceCacheConfig as yt, ResourceRegistry as z, PipelineConfig as zt };
2913
+ export { OpenApiSchemas as $, ArcListResult as $t, RequestIdOptions as A, RelationMetadata as An, Authenticator as At, ResourceDefinition as B, UserOrganization as Bt, RequestContext as C, beforeUpdate as Cn, OperationFilter as Ct, HealthCheck as D, AdapterSchemaContext as Dn, Transform as Dt, GracefulShutdownOptions as E, AdapterFactory as En, PipelineStep as Et, FastifyWithDecorators as F, ApiResponse as Ft, ActionsMap as G, TreeMixin as Gt, ActionDefinition as H, SoftDeleteExt as Ht, MiddlewareHandler as I, ArcRequest as It, CrudRouteKey as J, BulkExt as Jt, ArcFieldRule as K, SlugExt as Kt, RequestWithExtras as L, JWTPayload as Lt, EventsDecorator as M, SchemaMetadata as Mn, JwtContext as Mt, FastifyRequestExtras as N, ValidationResult$1 as Nn, TokenPair as Nt, HealthOptions as O, DataAdapter as On, AuthHelpers as Ot, FastifyWithAuth as P, AnyRecord as Pt, MiddlewareConfig as Q, ArcGetResult as Qt, RegisterOptions as R, ObjectId as Rt, QueryParserInterface as S, beforeDelete as Sn, NextFunction as St, CrudRouterOptions as T, defineHook as Tn, PipelineContext as Tt, ActionEntry as U, SoftDeleteMixin as Ut, defineResource as V, BaseController as Vt, ActionHandlerFn as W, TreeExt as Wt, EventDefinition as X, ArcCreateResult as Xt, CrudSchemas as Y, BulkMixin as Yt, FieldRule$1 as Z, ArcDeleteResult as Zt, ControllerQueryOptions as _, HookSystemOptions as _n, IControllerResponse as _t, InferAdapterDoc as a, QueryResolverConfig as an, ResourceConfig as at, ParsedQuery as b, afterUpdate as bn, Guard as bt, TypedController as c, AccessControl as cn, ResourcePermissions as ct, PaginationResult as d, HookContext as dn, RouteMethod as dt, ArcUpdateResult as en, PresetFunction as et, IntrospectionData as f, HookHandler as fn, RouteSchemaOptions as ft, ArcInternalMetadata as g, HookSystem as gn, IController as gt, ResourceMetadata as h, HookRegistration as hn, FastifyHandler as ht, ValidationResult as i, QueryResolver as in, ResourceCacheConfig as it, ArcDecorator as j, RepositoryLike as jn, AuthenticatorContext as jt, IntrospectionPluginOptions as k, FieldMetadata as kn, AuthPluginOptions as kt, TypedRepository as l, AccessControlConfig as ln, RouteDefinition as lt, RegistryStats as m, HookPhase as mn, ControllerLike as mt, ConfigError as n, BaseCrudController as nn, PresetResult as nt, InferDocType as o, BodySanitizer as on, ResourceHookContext as ot, RegistryEntry as p, HookOperation as pn, ControllerHandler as pt, CrudController as q, SlugMixin as qt, ValidateOptions as r, ListResult as rn, RateLimitConfig as rt, InferResourceDoc as s, BodySanitizerConfig as sn, ResourceHooks as st, RouteHandlerMethod$1 as t, BaseControllerOptions as tn, PresetHook as tt, TypedResourceConfig as u, DefineHookOptions as un, RouteMcpConfig as ut, LookupOption as v, afterCreate as vn, IRequestContext as vt, ServiceContext as w, createHookSystem as wn, PipelineConfig as wt, PopulateOption as x, beforeCreate as xn, Interceptor as xt, OwnershipCheck as y, afterDelete as yn, RouteHandler as yt, ResourceRegistry as z, UserLike as zt };