@classytic/arc 2.11.4 → 2.14.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 (167) hide show
  1. package/README.md +16 -12
  2. package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
  3. package/dist/EventTransport-CT_52aWU.d.mts +34 -0
  4. package/dist/EventTransport-DLWoUMHy.mjs +103 -0
  5. package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
  6. package/dist/audit/index.d.mts +2 -2
  7. package/dist/audit/index.mjs +1 -1
  8. package/dist/auth/audit.d.mts +199 -0
  9. package/dist/auth/audit.mjs +288 -0
  10. package/dist/auth/index.d.mts +3 -3
  11. package/dist/auth/index.mjs +117 -191
  12. package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
  13. package/dist/buildHandler-olo-gt94.mjs +610 -0
  14. package/dist/cache/index.mjs +3 -3
  15. package/dist/cli/commands/describe.d.mts +89 -13
  16. package/dist/cli/commands/describe.mjs +56 -2
  17. package/dist/cli/commands/docs.mjs +2 -2
  18. package/dist/cli/commands/generate.mjs +147 -48
  19. package/dist/cli/commands/init.d.mts +13 -0
  20. package/dist/cli/commands/init.mjs +130 -87
  21. package/dist/cli/commands/introspect.mjs +8 -1
  22. package/dist/context/index.mjs +1 -1
  23. package/dist/core/index.d.mts +3 -3
  24. package/dist/core/index.mjs +5 -5
  25. package/dist/core-DECn6zaU.mjs +1399 -0
  26. package/dist/{createActionRouter-CIKOcNA7.mjs → createActionRouter-CBxLLbn3.mjs} +7 -20
  27. package/dist/createAggregationRouter-CRIBv4sC.mjs +114 -0
  28. package/dist/{createApp-C9bRrqlX.mjs → createApp-XX2-N0Yd.mjs} +28 -22
  29. package/dist/{defineEvent-D1Ky9M1D.mjs → defineEvent-D5h7EvAx.mjs} +1 -1
  30. package/dist/docs/index.d.mts +24 -11
  31. package/dist/docs/index.mjs +2 -2
  32. package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
  33. package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
  34. package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
  35. package/dist/errors-j4aJm1Wg.mjs +184 -0
  36. package/dist/{eventPlugin-Cts2-Tfj.mjs → eventPlugin-CaKTYkYM.mjs} +28 -4
  37. package/dist/{eventPlugin-DDJoNEPL.d.mts → eventPlugin-qXpqTebY.d.mts} +24 -1
  38. package/dist/events/index.d.mts +6 -6
  39. package/dist/events/index.mjs +11 -35
  40. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  41. package/dist/events/transports/redis.d.mts +1 -1
  42. package/dist/factory/index.d.mts +2 -2
  43. package/dist/factory/index.mjs +2 -2
  44. package/dist/{fields-BRjxOAFp.d.mts → fields-COhcH3fk.d.mts} +23 -2
  45. package/dist/hooks/index.d.mts +1 -1
  46. package/dist/hooks/index.mjs +1 -1
  47. package/dist/idempotency/index.d.mts +1 -1
  48. package/dist/idempotency/index.mjs +1 -20
  49. package/dist/idempotency/redis.mjs +1 -1
  50. package/dist/{index-rHjXmJar.d.mts → index-BTqLEvhu.d.mts} +163 -3
  51. package/dist/{index-CXXRbnf8.d.mts → index-BtW7qYwa.d.mts} +660 -326
  52. package/dist/{index-m8mOOlFW.d.mts → index-Ds61mrJE.d.mts} +50 -4
  53. package/dist/{index-D9t1KNaB.d.mts → index-Dz5IKsrE.d.mts} +360 -219
  54. package/dist/index.d.mts +6 -7
  55. package/dist/index.mjs +9 -10
  56. package/dist/integrations/event-gateway.d.mts +1 -1
  57. package/dist/integrations/event-gateway.mjs +1 -1
  58. package/dist/integrations/index.d.mts +1 -1
  59. package/dist/integrations/mcp/index.d.mts +2 -2
  60. package/dist/integrations/mcp/index.mjs +1 -1
  61. package/dist/integrations/mcp/testing.d.mts +1 -1
  62. package/dist/integrations/mcp/testing.mjs +1 -1
  63. package/dist/integrations/streamline.d.mts +60 -11
  64. package/dist/integrations/streamline.mjs +75 -85
  65. package/dist/integrations/websocket.mjs +2 -8
  66. package/dist/middleware/index.d.mts +1 -1
  67. package/dist/middleware/index.mjs +2 -2
  68. package/dist/migrations/index.d.mts +23 -3
  69. package/dist/migrations/index.mjs +0 -7
  70. package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
  71. package/dist/openapi-noXno2CV.mjs +968 -0
  72. package/dist/org/index.d.mts +2 -2
  73. package/dist/org/index.mjs +1 -1
  74. package/dist/permissions/index.d.mts +3 -3
  75. package/dist/permissions/index.mjs +3 -3
  76. package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
  77. package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
  78. package/dist/pipeline/index.d.mts +1 -1
  79. package/dist/pipeline/index.mjs +1 -1
  80. package/dist/plugins/index.d.mts +16 -31
  81. package/dist/plugins/index.mjs +33 -13
  82. package/dist/plugins/response-cache.mjs +1 -1
  83. package/dist/plugins/tracing-entry.mjs +1 -1
  84. package/dist/presets/filesUpload.d.mts +4 -4
  85. package/dist/presets/filesUpload.mjs +6 -9
  86. package/dist/presets/index.d.mts +1 -1
  87. package/dist/presets/index.mjs +1 -1
  88. package/dist/presets/multiTenant.d.mts +1 -1
  89. package/dist/presets/multiTenant.mjs +2 -2
  90. package/dist/presets/search.d.mts +2 -2
  91. package/dist/presets/search.mjs +6 -8
  92. package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
  93. package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
  94. package/dist/{redis-stream-xTGxB2bm.d.mts → redis-stream-D6HzR1Z_.d.mts} +1 -1
  95. package/dist/registry/index.d.mts +1 -1
  96. package/dist/registry/index.mjs +2 -2
  97. package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
  98. package/dist/{resourceToTools-CxNmI6xF.mjs → resourceToTools-DLL32us3.mjs} +224 -71
  99. package/dist/{routerShared-BqLRb5l7.mjs → routerShared-DrOa-26E.mjs} +41 -36
  100. package/dist/{schemaIR-Dy2p4MxS.mjs → schemaIR-lYhC2gE5.mjs} +1 -1
  101. package/dist/schemas/index.d.mts +100 -30
  102. package/dist/schemas/index.mjs +86 -29
  103. package/dist/scim/index.d.mts +264 -0
  104. package/dist/scim/index.mjs +963 -0
  105. package/dist/scope/index.d.mts +3 -3
  106. package/dist/scope/index.mjs +4 -4
  107. package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
  108. package/dist/{store-helpers-Cp4uKC1U.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
  109. package/dist/testing/index.d.mts +2 -8
  110. package/dist/testing/index.mjs +16 -24
  111. package/dist/types/index.d.mts +4 -4
  112. package/dist/{types-D7KpfiL1.d.mts → types-BvqwCCSx.d.mts} +73 -25
  113. package/dist/{types-DDyTPc6y.d.mts → types-CTYvcwHe.d.mts} +195 -1
  114. package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
  115. package/dist/{types-BQ9TJQNy.d.mts → types-DQHFc8PM.d.mts} +1 -1
  116. package/dist/utils/index.d.mts +2 -2
  117. package/dist/utils/index.mjs +5 -5
  118. package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
  119. package/dist/{versioning-DsglKfM_.d.mts → versioning-DTTvc80y.d.mts} +1 -1
  120. package/package.json +24 -34
  121. package/skills/arc/SKILL.md +147 -51
  122. package/skills/arc/references/agent-auth.md +238 -0
  123. package/skills/arc/references/api-reference.md +187 -0
  124. package/skills/arc/references/auth.md +354 -7
  125. package/skills/arc/references/enterprise-auth.md +94 -0
  126. package/skills/arc/references/events.md +8 -6
  127. package/skills/arc/references/mcp.md +2 -2
  128. package/skills/arc/references/multi-tenancy.md +11 -2
  129. package/skills/arc/references/production.md +10 -9
  130. package/skills/arc/references/scim.md +247 -0
  131. package/skills/arc/references/testing.md +1 -1
  132. package/skills/arc-code-review/SKILL.md +141 -0
  133. package/skills/arc-code-review/references/anti-patterns.md +911 -0
  134. package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
  135. package/skills/arc-code-review/references/migration-recipes.md +700 -0
  136. package/skills/arc-code-review/references/mongokit-migration.md +386 -0
  137. package/skills/arc-code-review/references/scaffolding.md +230 -0
  138. package/skills/arc-code-review/references/severity.md +127 -0
  139. package/dist/EventTransport-BFQjw9pB.mjs +0 -133
  140. package/dist/EventTransport-CYNUXdCJ.d.mts +0 -293
  141. package/dist/adapters/index.d.mts +0 -3
  142. package/dist/adapters/index.mjs +0 -2
  143. package/dist/adapters-DUUiiimH.mjs +0 -964
  144. package/dist/auth/mongoose.d.mts +0 -191
  145. package/dist/auth/mongoose.mjs +0 -73
  146. package/dist/core-CbcQRIch.mjs +0 -1054
  147. package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
  148. package/dist/errorHandler-DEWmGWPz.d.mts +0 -114
  149. package/dist/errors-D5c-5BJL.mjs +0 -232
  150. package/dist/index-Rg8axYPz.d.mts +0 -370
  151. package/dist/openapi-D7G1V7ex.mjs +0 -557
  152. /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
  153. /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
  154. /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
  155. /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
  156. /package/dist/{elevation-BQQXZ_VR.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
  157. /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
  158. /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
  159. /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
  160. /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
  161. /package/dist/{pluralize-CWP6MB39.mjs → pluralize-DQgqgifU.mjs} +0 -0
  162. /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
  163. /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
  164. /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
  165. /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
  166. /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
  167. /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
@@ -1,198 +1,15 @@
1
1
  import { a as QueryCacheConfig } from "./QueryCache-D41bfdBB.mjs";
2
- import { r as RequestScope } from "./types-DDyTPc6y.mjs";
3
- import { c as PermissionCheck, d as UserBase, n as FieldPermissionMap } from "./fields-BRjxOAFp.mjs";
4
- import { n as DomainEvent } from "./EventTransport-CYNUXdCJ.mjs";
5
- import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, RouteHandlerMethod, RouteHandlerMethod as RouteHandlerMethod$1 } from "fastify";
2
+ import { i as RequestScope } from "./types-CTYvcwHe.mjs";
3
+ import { c as PermissionCheck, d as UserBase, n as FieldPermissionMap } from "./fields-COhcH3fk.mjs";
4
+ import { n as DomainEvent } from "./EventTransport-CT_52aWU.mjs";
6
5
  import { KeysetPaginationResult, OffsetPaginationResult } from "@classytic/repo-core/pagination";
7
- import { MinimalRepo, QueryOptions, StandardRepo } from "@classytic/repo-core/repository";
6
+ import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, RouteHandlerMethod, RouteHandlerMethod as RouteHandlerMethod$1 } from "fastify";
7
+ import * as _$_classytic_repo_core_adapter0 from "@classytic/repo-core/adapter";
8
+ import { DataAdapter, RepositoryLike } from "@classytic/repo-core/adapter";
9
+ import { AggDateBucket, AggMeasure, AggTopN, QueryOptions, StandardRepo } from "@classytic/repo-core/repository";
10
+ import { LookupSpec } from "@classytic/repo-core/lookup";
8
11
  import { FieldRule, SchemaBuilderOptions } from "@classytic/repo-core/schema";
9
12
 
10
- //#region src/adapters/interface.d.ts
11
- /**
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.
47
- *
48
- * ```ts
49
- * const adapter: DataAdapter<Product> = {
50
- * repository: myRepo, // any MinimalRepo<Product> — kit-agnostic
51
- * type: 'drizzle', // or 'mongoose' | 'prisma' | 'custom'
52
- * name: 'products',
53
- * };
54
- * defineResource({ adapter, ... });
55
- * ```
56
- */
57
- type RepositoryLike<TDoc = unknown> = MinimalRepo<TDoc> & Partial<StandardRepo<TDoc>>;
58
- /**
59
- * Permissive structural input accepted at every adapter factory boundary.
60
- *
61
- * Wider than `RepositoryLike<TDoc>` on `getAll`'s `params`/`options` —
62
- * uses method-shorthand syntax with `unknown` so kit-native repositories
63
- * (mongokit's `Repository<TDoc>`, sqlitekit, prismakit) plug in directly,
64
- * without `as RepositoryLike<TDoc>` casts on the host.
65
- *
66
- * **Why this exists.** repo-core 0.2 widened `MinimalRepo['getAll']`'s
67
- * `params.filters` to a `Filter | Record<string, unknown>` IR union, but
68
- * concrete kit `Repository` classes still type `filters` as the narrower
69
- * `Record<string, unknown>`. Under `strictFunctionTypes` the kit's narrower
70
- * function-property `getAll` is no longer assignable to the IR-aware one,
71
- * which forced every host adapter glue file to write
72
- * `repository as unknown as RepositoryLike<TDoc>`.
73
- *
74
- * Adapter factories accept this permissive shape, then call
75
- * `asRepositoryLike()` once to widen for arc internals (audit, outbox,
76
- * idempotency stores still see the strict `RepositoryLike` view). The
77
- * documented escape hatch lives in arc, not at every host call site.
78
- */
79
- interface AdapterRepositoryInput<TDoc = unknown> {
80
- readonly idField?: string;
81
- getAll(params?: unknown, options?: unknown): Promise<unknown>;
82
- getById(id: string, options?: unknown): Promise<TDoc | null>;
83
- create(data: Partial<TDoc>, options?: unknown): Promise<TDoc>;
84
- update(id: string, data: Partial<TDoc>, options?: unknown): Promise<TDoc | null>;
85
- delete(id: string, options?: unknown): Promise<{
86
- success: boolean;
87
- message: string;
88
- id?: string;
89
- soft?: boolean;
90
- count?: number;
91
- }>;
92
- }
93
- /**
94
- * Widen a permissive `AdapterRepositoryInput<TDoc>` to arc's strict
95
- * `RepositoryLike<TDoc>` view. Single-source escape hatch for the
96
- * filter-IR drift documented on `AdapterRepositoryInput`.
97
- *
98
- * Arc internals (audit / outbox / idempotency, BaseController) still see
99
- * the IR-aware `RepositoryLike`; only the call paths arc exercises are
100
- * shared between the two views, and those use the narrower
101
- * `Record<string, unknown>` filter shape both sides agree on.
102
- */
103
- declare function asRepositoryLike<TDoc = unknown>(input: AdapterRepositoryInput<TDoc>): RepositoryLike<TDoc>;
104
- interface DataAdapter<TDoc = unknown> {
105
- /**
106
- * Repository implementing CRUD operations. Any value that satisfies
107
- * `RepositoryLike<TDoc>` — which includes `StandardRepo<TDoc>` (all
108
- * methods implemented), `MinimalRepo<TDoc>` (5-method floor), or
109
- * anything in between a kit declares. Arc feature-detects optional
110
- * methods at runtime — kits only declare what they support.
111
- */
112
- repository: RepositoryLike<TDoc>;
113
- /** Adapter identifier for introspection */
114
- readonly type: "mongoose" | "prisma" | "drizzle" | "typeorm" | "custom";
115
- /** Human-readable name */
116
- readonly name: string;
117
- /**
118
- * Generate OpenAPI schemas for CRUD operations. Each adapter produces
119
- * schemas appropriate to its ORM/database (mongoose kits use mongokit's
120
- * `buildCrudSchemasFromModel`; SQL kits introspect columns).
121
- *
122
- * @param options - Schema generation options (field rules, populate hints)
123
- * @param context - Resource-level context (idField for params schema, name for logs).
124
- * Adapters should honor `context.idField` when producing the params
125
- * schema (e.g., skip the ObjectId pattern when idField is a custom
126
- * string field).
127
- */
128
- generateSchemas?(options?: RouteSchemaOptions, context?: AdapterSchemaContext): OpenApiSchemas | Record<string, unknown> | null;
129
- /** Extract schema metadata for OpenAPI/introspection. */
130
- getSchemaMetadata?(): SchemaMetadata | null;
131
- /** Validate data against schema before persistence. */
132
- validate?(data: unknown): Promise<ValidationResult$1> | ValidationResult$1;
133
- /** Health check for database connection. */
134
- healthCheck?(): Promise<boolean>;
135
- /**
136
- * Custom filter matching for in-memory policy enforcement. Falls back
137
- * to arc's built-in shallow matcher when omitted. Override for SQL
138
- * adapters, non-Mongo operators, or kits that compile Filter IR.
139
- */
140
- matchesFilter?: (item: unknown, filters: Record<string, unknown>) => boolean;
141
- /** Close / cleanup resources. */
142
- close?(): Promise<void>;
143
- }
144
- /**
145
- * Context passed to `adapter.generateSchemas()` so adapters shape output
146
- * to match resource-level configuration. All fields optional — adapters
147
- * that ignore this still work; arc applies safety-net normalization.
148
- */
149
- interface AdapterSchemaContext {
150
- /** The idField configured on the resource. Defaults to "_id". */
151
- idField?: string;
152
- /** Resource name (for error messages / logging). */
153
- resourceName?: string;
154
- }
155
- interface SchemaMetadata {
156
- name: string;
157
- fields: Record<string, FieldMetadata>;
158
- indexes?: Array<{
159
- fields: string[];
160
- unique?: boolean;
161
- sparse?: boolean;
162
- }>;
163
- relations?: Record<string, RelationMetadata>;
164
- }
165
- interface FieldMetadata {
166
- type: "string" | "number" | "boolean" | "date" | "object" | "array" | "objectId" | "enum";
167
- required?: boolean;
168
- unique?: boolean;
169
- default?: unknown;
170
- enum?: Array<string | number>;
171
- min?: number;
172
- max?: number;
173
- minLength?: number;
174
- maxLength?: number;
175
- pattern?: string;
176
- description?: string;
177
- ref?: string;
178
- array?: boolean;
179
- }
180
- interface RelationMetadata {
181
- type: "one-to-one" | "one-to-many" | "many-to-many";
182
- target: string;
183
- foreignKey?: string;
184
- through?: string;
185
- }
186
- interface ValidationResult$1 {
187
- valid: boolean;
188
- errors?: Array<{
189
- field: string;
190
- message: string;
191
- code?: string;
192
- }>;
193
- }
194
- type AdapterFactory<TDoc> = (config: unknown) => DataAdapter<TDoc>;
195
- //#endregion
196
13
  //#region src/hooks/HookSystem.d.ts
197
14
  type HookPhase = "before" | "around" | "after";
198
15
  type HookOperation = "create" | "update" | "delete" | "restore" | "read" | "list";
@@ -605,6 +422,67 @@ declare class BodySanitizer {
605
422
  sanitize(body: AnyRecord, _operation: "create" | "update", req?: IRequestContext, meta?: ArcInternalMetadata): AnyRecord;
606
423
  }
607
424
  //#endregion
425
+ //#region src/core/controllerTypes.d.ts
426
+ /**
427
+ * Union of every return shape repo-core's `MinimalRepo.getAll()` is
428
+ * contractually allowed to produce. See repo-core's `MinimalRepo.getAll`
429
+ * docstring for the three-way split:
430
+ *
431
+ * - `OffsetPaginationResult<TDoc>` — `page` param drives pagination.
432
+ * - `KeysetPaginationResult<TDoc>` — `sort` + optional `after` drives pagination.
433
+ * - `TDoc[]` — raw array when neither drives pagination.
434
+ *
435
+ * Arc passes the kit's response verbatim; consumers narrow on shape.
436
+ */
437
+ type ListResult<TDoc> = OffsetPaginationResult<TDoc> | KeysetPaginationResult<TDoc> | TDoc[];
438
+ /**
439
+ * Discrete cache states reported via the `x-cache` response header.
440
+ *
441
+ * - `'HIT'` — fresh cache entry served, no upstream call made.
442
+ * - `'STALE'` — stale entry served, upstream refresh scheduled in the background.
443
+ * - `'MISS'` — no cache entry; upstream call ran and the result was cached.
444
+ *
445
+ * Exported as a literal union so test code and downstream clients can
446
+ * import + narrow without restating the literal triple.
447
+ */
448
+ type CacheStatus = "HIT" | "STALE" | "MISS";
449
+ /**
450
+ * Controller-shape surface that the `Arc*Result` utilities read return
451
+ * types from. Internal — exported so the utility types can reference
452
+ * the minimal shape without a circular dependency on the full
453
+ * `BaseCrudController` / `BaseController` declarations.
454
+ */
455
+ type ArcControllerLike = {
456
+ list: (...args: any[]) => unknown;
457
+ get: (...args: any[]) => unknown;
458
+ create: (...args: any[]) => unknown;
459
+ update: (...args: any[]) => unknown;
460
+ delete: (...args: any[]) => unknown;
461
+ };
462
+ /**
463
+ * Return type of the controller's `list` method.
464
+ *
465
+ * @example
466
+ * ```ts
467
+ * class ProductController extends BaseController<Product> {
468
+ * async list(ctx: IRequestContext): ArcListResult<this> {
469
+ * // return shape inferred from BaseController.list — no need to
470
+ * // restate `Promise<IControllerResponse<ListResult<Product>>>`
471
+ * return super.list(ctx);
472
+ * }
473
+ * }
474
+ * ```
475
+ */
476
+ type ArcListResult<TCtrl extends ArcControllerLike> = ReturnType<TCtrl["list"]>;
477
+ /** Return type of the controller's `get` method. See {@link ArcListResult}. */
478
+ type ArcGetResult<TCtrl extends ArcControllerLike> = ReturnType<TCtrl["get"]>;
479
+ /** Return type of the controller's `create` method. See {@link ArcListResult}. */
480
+ type ArcCreateResult<TCtrl extends ArcControllerLike> = ReturnType<TCtrl["create"]>;
481
+ /** Return type of the controller's `update` method. See {@link ArcListResult}. */
482
+ type ArcUpdateResult<TCtrl extends ArcControllerLike> = ReturnType<TCtrl["update"]>;
483
+ /** Return type of the controller's `delete` method. See {@link ArcListResult}. */
484
+ type ArcDeleteResult<TCtrl extends ArcControllerLike> = ReturnType<TCtrl["delete"]>;
485
+ //#endregion
608
486
  //#region src/core/QueryResolver.d.ts
609
487
  interface QueryResolverConfig {
610
488
  /** Query parser instance (default: Arc built-in parser) */
@@ -635,6 +513,13 @@ declare class QueryResolver {
635
513
  private schemaOptions;
636
514
  private tenantField;
637
515
  constructor(config?: QueryResolverConfig);
516
+ /**
517
+ * Swap the underlying parser. Mutates in place so the resolver instance
518
+ * stays referentially stable (hosts capturing a `queryResolver` ref via
519
+ * `defineResource({ controller })` keep that ref valid). Single source of
520
+ * truth — pairs with `BaseCrudController.setQueryParser()`.
521
+ */
522
+ setParser(parser: QueryParserInterface): void;
638
523
  /**
639
524
  * Resolve a request into parsed query options -- ONE parse per request.
640
525
  * Combines what was previously _buildContext + _parseQueryOptions + _applyFilters.
@@ -661,54 +546,6 @@ declare class QueryResolver {
661
546
  }
662
547
  //#endregion
663
548
  //#region src/core/BaseCrudController.d.ts
664
- /**
665
- * Union of every return shape repo-core's `MinimalRepo.getAll()` is
666
- * contractually allowed to produce. See repo-core's `MinimalRepo.getAll`
667
- * docstring for the three-way split:
668
- *
669
- * - `OffsetPaginationResult<TDoc>` — `page` param drives pagination.
670
- * - `KeysetPaginationResult<TDoc>` — `sort` + optional `after` drives pagination.
671
- * - `TDoc[]` — raw array when neither drives pagination.
672
- *
673
- * Arc passes the kit's response verbatim; consumers narrow on shape.
674
- */
675
- type ListResult<TDoc> = OffsetPaginationResult<TDoc> | KeysetPaginationResult<TDoc> | TDoc[];
676
- /**
677
- * Controller-shape surface that the `Arc*Result` utilities read return
678
- * types from. Internal — exported so the utility types can reference
679
- * the minimal shape without a circular dependency on the full
680
- * `BaseCrudController` / `BaseController` declarations.
681
- */
682
- type ArcControllerLike = {
683
- list: (...args: any[]) => unknown;
684
- get: (...args: any[]) => unknown;
685
- create: (...args: any[]) => unknown;
686
- update: (...args: any[]) => unknown;
687
- delete: (...args: any[]) => unknown;
688
- };
689
- /**
690
- * Return type of the controller's `list` method.
691
- *
692
- * @example
693
- * ```ts
694
- * class ProductController extends BaseController<Product> {
695
- * async list(ctx: IRequestContext): ArcListResult<this> {
696
- * // return shape inferred from BaseController.list — no need to
697
- * // restate `Promise<IControllerResponse<ListResult<Product>>>`
698
- * return super.list(ctx);
699
- * }
700
- * }
701
- * ```
702
- */
703
- type ArcListResult<TCtrl extends ArcControllerLike> = ReturnType<TCtrl["list"]>;
704
- /** Return type of the controller's `get` method. See {@link ArcListResult}. */
705
- type ArcGetResult<TCtrl extends ArcControllerLike> = ReturnType<TCtrl["get"]>;
706
- /** Return type of the controller's `create` method. See {@link ArcListResult}. */
707
- type ArcCreateResult<TCtrl extends ArcControllerLike> = ReturnType<TCtrl["create"]>;
708
- /** Return type of the controller's `update` method. See {@link ArcListResult}. */
709
- type ArcUpdateResult<TCtrl extends ArcControllerLike> = ReturnType<TCtrl["update"]>;
710
- /** Return type of the controller's `delete` method. See {@link ArcListResult}. */
711
- type ArcDeleteResult<TCtrl extends ArcControllerLike> = ReturnType<TCtrl["delete"]>;
712
549
  interface BaseControllerOptions {
713
550
  /** Schema options for field sanitization */
714
551
  schemaOptions?: RouteSchemaOptions;
@@ -724,8 +561,8 @@ interface BaseControllerOptions {
724
561
  defaultLimit?: number;
725
562
  /**
726
563
  * Default sort applied when the request doesn't specify one.
727
- * - `string` (default: `'-createdAt'`) Mongo `-field` DESC convention.
728
- * - `false` disable the default sort entirely (SQL/Drizzle resources
564
+ * - `string` (default: `'-createdAt'`) — Mongo `-field` DESC convention.
565
+ * - `false` — disable the default sort entirely (SQL/Drizzle resources
729
566
  * without a `createdAt` column).
730
567
  */
731
568
  defaultSort?: string | false;
@@ -777,8 +614,6 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
777
614
  protected queryParser: QueryParserInterface;
778
615
  protected maxLimit: number;
779
616
  protected defaultLimit: number;
780
- /** `undefined` means "no default sort" (caller passed `false`). */
781
- protected defaultSort: string | undefined;
782
617
  protected resourceName?: string;
783
618
  protected tenantField: string | false;
784
619
  protected idField: string;
@@ -789,7 +624,7 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
789
624
  /**
790
625
  * Composable query resolution (parsing, pagination, sort, select/populate).
791
626
  *
792
- * Not `readonly` `setQueryParser()` rebuilds this resolver to swap in a
627
+ * Not `readonly` — `setQueryParser()` rebuilds this resolver to swap in a
793
628
  * different parser (e.g. mongokit's `QueryParser`). `defineResource` calls
794
629
  * it automatically when a resource supplies both `controller` and
795
630
  * `queryParser`.
@@ -803,18 +638,15 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
803
638
  protected _cacheConfig?: ResourceCacheConfig;
804
639
  constructor(repository: TRepository, options?: BaseControllerOptions);
805
640
  /**
806
- * Swap the controller's query parser. Rebuilds the internal `QueryResolver`
807
- * with the new parser while preserving every other config.
641
+ * Swap the controller's query parser. Mutates the existing `QueryResolver`
642
+ * in place via `QueryResolver.setParser()` the resolver instance stays
643
+ * referentially stable, and there is no second copy of `defaultSort` /
644
+ * `tenantField` / `schemaOptions` for the swap to drift away from.
808
645
  *
809
646
  * Closes the v2.10.9 gap where `defineResource({ controller, queryParser })`
810
- * forwarded the parser only to auto-constructed controllers; user-supplied
811
- * controllers kept their default `ArcQueryParser`. `defineResource` calls
812
- * this via duck-typing when both `controller` and `queryParser` are
813
- * supplied — controllers that don't implement `setQueryParser` are left
814
- * untouched.
815
- *
816
- * Idempotent + safe to call repeatedly. Does NOT touch `maxLimit` or
817
- * `defaultLimit` — those are construction-time decisions.
647
+ * forwarded the parser only to auto-constructed controllers. `defineResource`
648
+ * calls this via duck-typing when both `controller` and `queryParser` are
649
+ * supplied; controllers that don't implement it are left untouched.
818
650
  */
819
651
  setQueryParser(queryParser: QueryParserInterface): void;
820
652
  /**
@@ -823,17 +655,39 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
823
655
  */
824
656
  protected getTenantField(): string | undefined;
825
657
  /**
826
- * Build top-level tenant options to thread into the repository call.
658
+ * Build the canonical repo-options bag from the Fastify request.
659
+ *
660
+ * Forwards the cross-kit canonical set (see repo-core's
661
+ * `STANDARD_REPO_OPTION_KEYS`) into every CRUD repo call so kit
662
+ * plugins (multi-tenant, audit, audit-trail, observability) get
663
+ * what they need without per-resource wiring:
664
+ *
665
+ * - **Tenant scope** — `[tenantField]: orgId` from `RequestScope`.
666
+ * Plugin-scoped repos (mongokit's `multiTenantPlugin`) read tenant
667
+ * scope from the TOP of the options bag, not `data.organizationId`.
668
+ * Without this stamping, a tenant-scoped repo throws "Missing
669
+ * 'organizationId' in context" even when arc has injected the
670
+ * tenant into the request body.
671
+ * Multi-field tenancy from `_tenantFields` (populated by
672
+ * `multiTenantPreset`) is merged in.
827
673
  *
828
- * Plugin-scoped repos (mongokit's `multiTenantPlugin`) read tenant scope
829
- * from the TOP of the operation context — `context.organizationId`, not
830
- * `context.data.organizationId`. Without this stamping, a tenant-scoped
831
- * repo throws "Missing 'organizationId' in context" even when arc has
832
- * injected the tenant into the request body.
674
+ * - **Audit attribution** — `userId` + `user` from the authenticated
675
+ * actor. Mongokit's audit-log / audit-trail plugins read these
676
+ * into the `who` column; sqlitekit's audit plugin reads the same
677
+ * names. No host-side forwarding needed.
833
678
  *
834
- * Returns `{ [tenantField]: orgId }` for tenant-scoped + org-carrying
835
- * requests, `{}` otherwise. Merges multi-field tenancy from
836
- * `_tenantFields` (populated by `multiTenantPreset`).
679
+ * - **Trace correlation** — `requestId` from Fastify's request id
680
+ * for stitching logs / events / downstream calls.
681
+ *
682
+ * - **`session` is intentionally NOT auto-set.** Sessions are tied
683
+ * to explicit transaction scopes the controller doesn't manage;
684
+ * pass `session` inline at the call site when running inside a
685
+ * `withTransaction` helper.
686
+ *
687
+ * Method kept named `tenantRepoOptions` for back-compat with hosts
688
+ * that spread `...this.tenantRepoOptions(req)` (10+ call sites in
689
+ * arc, plus host overrides). The bag has always grown over time —
690
+ * hosts that don't want audit forwarding never read those keys.
837
691
  */
838
692
  protected tenantRepoOptions(req: IRequestContext): AnyRecord;
839
693
  /** Extract typed Arc internal metadata from request */
@@ -844,21 +698,40 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
844
698
  * Resolve the repository primary key for mutation calls.
845
699
  *
846
700
  * When the resource declares a custom `idField` (slug, jobId, UUID), the
847
- * default behavior is to translate the route id the fetched doc's `_id`
701
+ * default behavior is to translate the route id → the fetched doc's `_id`
848
702
  * because most Mongo repositories key mutation methods off `_id`.
849
703
  *
850
704
  * Exception: if the repo exposes a matching `idField` property (e.g.
851
705
  * MongoKit's `new Repository(Model, [], {}, { idField: 'id' })`), the
852
- * repo handles lookup itself pass the route id through unchanged.
706
+ * repo handles lookup itself — pass the route id through unchanged.
853
707
  */
854
708
  protected resolveRepoId(id: string, existing: AnyRecord | null): string;
855
709
  /**
856
- * Centralized 404 response builder. Maps the denial reason from
857
- * `fetchDetailed()` into a structured `details.code` so consumers can
858
- * distinguish "doc doesn't exist" from "doc filtered by policy/org scope"
859
- * without parsing error strings.
710
+ * Read-side preflight for mutable-target operations (`update`, `delete`).
711
+ *
712
+ * Bundles the four steps that every mutation must do before touching the
713
+ * repo: (1) extract `:id`, (2) fetch under access control + tenant scope,
714
+ * (3) verify ownership, (4) translate the route id to the repo's primary
715
+ * key. Returning `{id, existing, repoId}` keeps the call sites a single
716
+ * line and makes drift between `update` and `delete` structurally
717
+ * impossible — there is one preflight, one denial-reason mapping, one
718
+ * ownership check.
719
+ *
720
+ * Pass `extraFetchOptions` for callers (e.g. soft-delete restore) that
721
+ * need to widen the fetch (`{ includeDeleted: true }`).
860
722
  */
861
- protected notFoundResponse(reason?: FetchDenialReason | null): IControllerResponse<never>;
723
+ protected loadMutableTarget(req: IRequestContext, extraFetchOptions?: AnyRecord): Promise<{
724
+ id: string;
725
+ existing: TDoc;
726
+ repoId: string;
727
+ }>;
728
+ /**
729
+ * Centralized 404 thrower. Maps the denial reason from `fetchDetailed()`
730
+ * into a `NotFoundError` so consumers can distinguish "doc doesn't
731
+ * exist" from "doc filtered by policy/org scope" via the error
732
+ * `details.code` set by the global error handler.
733
+ */
734
+ protected throwNotFound(reason?: FetchDenialReason | null): never;
862
735
  /** Resolve cache config for a specific operation, merging per-op overrides */
863
736
  protected resolveCacheConfig(operation: "list" | "byId"): QueryCacheConfig | null;
864
737
  /**
@@ -870,7 +743,86 @@ declare class BaseCrudController<TDoc = AnyRecord, TRepository extends Repositor
870
743
  userId?: string;
871
744
  orgId?: string;
872
745
  };
746
+ /** Shared `x-cache` response envelope builder. */
747
+ protected cacheResponse<T>(data: T, cacheStatus: CacheStatus): IControllerResponse<T>;
748
+ /** Required route-id helper shared by get/update/delete. Throws on missing id. */
749
+ protected requireIdParam(req: IRequestContext): string;
750
+ /**
751
+ * Normalizes `repo.exists()` return shapes across adapters. Per
752
+ * StandardRepo's contract, `exists` may return `boolean`, `{ _id }`,
753
+ * or `null` — every truthy non-null shape collapses to `true`.
754
+ */
755
+ protected isExistsTruthy(result: unknown): boolean;
756
+ /**
757
+ * Run `executeBefore` then `executeAround` (or just the executor if no
758
+ * hooks are wired). Returns the around-phase result directly. Throws an
759
+ * `ArcError` (status 400, code `BEFORE_<OP>_HOOK_ERROR`) when the
760
+ * before-hook fails — the global error handler emits the canonical
761
+ * `ErrorContract` shape.
762
+ *
763
+ * The caller runs `executeAfter` separately via `runAfterHook` — typically
764
+ * after success-checking the result (delete checks `isDeleteSuccess`,
765
+ * update checks `if (!item)`).
766
+ *
767
+ * **Knobs:**
768
+ * - `meta` — passed verbatim into `executeBefore` / `executeAround` opts.
769
+ * - `pipeProcessedData` (default `true`) — whether `executeBefore`'s
770
+ * return value flows into `executeAround` as the data parameter.
771
+ * Set `false` for delete (current behaviour: discards before's
772
+ * return, passes original input to around).
773
+ */
774
+ protected runHookedOpUntilResult<TInput, TResult>(req: IRequestContext, args: {
775
+ op: "create" | "update" | "delete";
776
+ input: TInput;
777
+ meta?: Record<string, unknown>;
778
+ pipeProcessedData?: boolean;
779
+ }, executor: (processed: TInput) => Promise<TResult>): Promise<TResult>;
780
+ /**
781
+ * Run `executeAfter` for the given op + data. No-op when hooks aren't
782
+ * wired or `resourceName` isn't set. Caller passes the data shape it
783
+ * wants downstream after-handlers to receive — typically the result for
784
+ * create/update, the original input (`existing`) for delete.
785
+ */
786
+ protected runAfterHook(req: IRequestContext, op: "create" | "update" | "delete", data: AnyRecord, meta?: Record<string, unknown>): Promise<void>;
787
+ /** Cached `list()` flow with SWR semantics. Returns null when cache is disabled. */
788
+ protected withListCache(req: IRequestContext, options: ParsedQuery): Promise<IControllerResponse<ListResult<TDoc>> | null>;
789
+ /** Cached `get()` flow with SWR semantics. Returns null when cache is disabled. */
790
+ protected withGetCache(req: IRequestContext, id: string, options: ParsedQuery): Promise<IControllerResponse<TDoc> | null>;
873
791
  list(req: IRequestContext): Promise<IControllerResponse<ListResult<TDoc>>>;
792
+ /**
793
+ * Resource-dispatch verbs router. Returns `null` when the request is
794
+ * a regular list query, otherwise returns the dispatch promise.
795
+ *
796
+ * Verbs (mutually exclusive — first match wins):
797
+ * - `?_count=true` → `{ count: number }` via `repo.count()`
798
+ * - `?_distinct=field` → `unknown[]` via `repo.distinct(field)`
799
+ * - `?_exists=true` → `{ exists: boolean }` via `repo.exists()`
800
+ *
801
+ * All verbs share the resolved filter (parsed query + policy filters
802
+ * + tenant scope). Adapters that don't ship the underlying repo
803
+ * method get a `501` so failures surface loudly instead of falling
804
+ * back to a full table scan.
805
+ */
806
+ protected dispatchResourceVerb(req: IRequestContext): Promise<IControllerResponse<unknown>> | null;
807
+ /** Resolve filter + tenant/audit options for a dispatch verb. */
808
+ private resolveDispatchScope;
809
+ /** `?_count=true` → `repo.count(filter)` */
810
+ protected dispatchCount(req: IRequestContext): Promise<IControllerResponse<{
811
+ count: number;
812
+ }>>;
813
+ /** `?_distinct=field` → `repo.distinct(field, filter)` */
814
+ protected dispatchDistinct(req: IRequestContext, field: string): Promise<IControllerResponse<unknown[]>>;
815
+ /** `?_exists=true` → `repo.exists(filter)` */
816
+ protected dispatchExists(req: IRequestContext): Promise<IControllerResponse<{
817
+ exists: boolean;
818
+ }>>;
819
+ /**
820
+ * True when `field` is safe to expose via `_distinct`. Mirrors the
821
+ * `select` allowlist — fields marked `hidden` or `systemManaged` in
822
+ * `schemaOptions.fieldRules` are NOT exposed (would leak password
823
+ * hashes, internal flags, etc).
824
+ */
825
+ protected isFieldExposedForRead(field: string): boolean;
874
826
  /** Execute list query through hooks (extracted for cache revalidation) */
875
827
  protected executeListQuery(options: ParsedQuery, req: IRequestContext): Promise<ListResult<TDoc>>;
876
828
  get(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
@@ -999,7 +951,7 @@ declare function SoftDeleteMixin<TBase extends Constructor<BaseCrudController>>(
999
951
  *
1000
952
  * Spec reference: https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-classes-with-other-types
1001
953
  */
1002
- interface BaseController<TDoc extends AnyRecord = AnyRecord, TRepository extends RepositoryLike = RepositoryLike<TDoc>> {
954
+ interface BaseController<TDoc extends AnyRecord = AnyRecord, _TRepository extends RepositoryLike = RepositoryLike<TDoc>> {
1003
955
  readonly accessControl: AccessControl;
1004
956
  readonly bodySanitizer: BodySanitizer;
1005
957
  queryResolver: QueryResolver;
@@ -1034,8 +986,8 @@ declare const BaseController_base: (new (repository: any, options?: BaseControll
1034
986
  * companion interface above gives every method full generic precision
1035
987
  * on `TDoc` via declaration merging.
1036
988
  */
1037
- declare class BaseController<TDoc extends AnyRecord = AnyRecord, TRepository extends RepositoryLike = RepositoryLike<TDoc>> extends BaseController_base {
1038
- readonly _phantom?: [TDoc, TRepository];
989
+ declare class BaseController<TDoc extends AnyRecord = AnyRecord, _TRepository extends RepositoryLike = RepositoryLike<TDoc>> extends BaseController_base {
990
+ readonly _phantom?: [TDoc, _TRepository];
1039
991
  }
1040
992
  //#endregion
1041
993
  //#region src/types/base.d.ts
@@ -1355,6 +1307,283 @@ type PipelineConfig = PipelineStep[] | {
1355
1307
  [operation: string]: PipelineStep[] | undefined;
1356
1308
  };
1357
1309
  //#endregion
1310
+ //#region src/core/aggregation/types.d.ts
1311
+ /**
1312
+ * Sugar for measures: `'count'` / `'count:field'` / `'sum:price'` /
1313
+ * `'avg:rating'` / `'min:created'` / `'max:updated'` /
1314
+ * `'countDistinct:userId'` / `'percentile:latency:0.95'`.
1315
+ *
1316
+ * Compiles to the canonical `AggMeasure` IR at boot. Hosts who want
1317
+ * the full IR can pass an `AggMeasure` object directly.
1318
+ *
1319
+ * **Percentile.** `'percentile:<field>:<p>'` where `p` is a numeric
1320
+ * literal in `[0, 1]` (e.g. `'percentile:latency:0.95'` for P95).
1321
+ * Mongokit (≥3.13) compiles to `$percentile`; sqlitekit throws
1322
+ * `UnsupportedOperationError` (no native percentile in SQLite).
1323
+ */
1324
+ type AggMeasureShorthand = "count" | `count:${string}` | `countDistinct:${string}` | `sum:${string}` | `avg:${string}` | `min:${string}` | `max:${string}` | `percentile:${string}:${number}`;
1325
+ /** Either canonical IR or shorthand string — both compile to `AggMeasure`. */
1326
+ type AggMeasureInput = AggMeasure | AggMeasureShorthand;
1327
+ /**
1328
+ * Cache config for an aggregation. Translates directly to the kit-side
1329
+ * `CacheOptions` (TanStack-shaped) which the unified
1330
+ * `@classytic/repo-core/cache` plugin reads from `req.cache`.
1331
+ *
1332
+ * Caching only fires when the kit's repo has the `cachePlugin` wired —
1333
+ * arc declares the policy; the kit handles SWR + tag invalidation +
1334
+ * version-bump on writes. Hosts without the plugin installed silently
1335
+ * fall through to a non-cached call.
1336
+ */
1337
+ interface AggregationCacheConfig {
1338
+ /** Seconds the entry is fresh — no revalidation while inside this window. */
1339
+ staleTime?: number;
1340
+ /** Seconds the entry stays in cache past stale before eviction. Default: 60. */
1341
+ gcTime?: number;
1342
+ /**
1343
+ * Stale-while-revalidate. When stale entries serve immediately and a
1344
+ * background refresh updates the cache. Default: `true` for
1345
+ * aggregations (dashboards almost always benefit from stale-serve).
1346
+ */
1347
+ swr?: boolean;
1348
+ /**
1349
+ * Group invalidation tags. Pass to `repo.cache?.invalidateByTags(tags)`
1350
+ * after a write to clear matching entries. The model name is
1351
+ * auto-tagged by the plugin — you only declare cross-cutting tags
1352
+ * (e.g. `'pricing'` to invalidate every aggregation that depends on
1353
+ * pricing across multiple resources).
1354
+ */
1355
+ tags?: readonly string[];
1356
+ }
1357
+ /**
1358
+ * Per-aggregation rate limit. Layers on top of any global rate limit
1359
+ * via `@fastify/rate-limit` route-level config.
1360
+ */
1361
+ interface AggregationRateLimit {
1362
+ /** Max requests per window. */
1363
+ max: number;
1364
+ /** Window in milliseconds. */
1365
+ windowMs: number;
1366
+ }
1367
+ /**
1368
+ * Required date-range narrowing. Caller MUST send a bounded range on
1369
+ * this field, and the range MUST NOT exceed `maxRangeDays`.
1370
+ *
1371
+ * Prevents "all-time" scans on billion-row collections — the single
1372
+ * biggest performance footgun for live aggregation endpoints.
1373
+ */
1374
+ interface AggregationDateRangeRequirement {
1375
+ /** Field whose range the caller must narrow (e.g. `'createdAt'`). */
1376
+ field: string;
1377
+ /**
1378
+ * Cap on the queryable range. A request asking for >N days is
1379
+ * rejected 400. Omit for "any range, but bounded" (lower + upper
1380
+ * required, no cap).
1381
+ */
1382
+ maxRangeDays?: number;
1383
+ }
1384
+ /**
1385
+ * Boot-time index hint. Arc warns when the kit's schema doesn't have
1386
+ * an index whose leading keys match — flags the misconfig before
1387
+ * traffic hits the DB.
1388
+ *
1389
+ * Documented intent, NOT runtime-enforced. Kits with their own index
1390
+ * introspection (mongokit reads Mongoose schema, sqlitekit reads
1391
+ * Drizzle indexes) can act on the hint; kits without introspection
1392
+ * silently accept it.
1393
+ */
1394
+ interface AggregationIndexHint {
1395
+ /** Leading-key columns the host expects the planner to use. */
1396
+ leadingKeys: readonly string[];
1397
+ }
1398
+ /**
1399
+ * Runtime context passed to the `materialized` hook.
1400
+ *
1401
+ * The hook returns pre-computed data instead of running the live
1402
+ * aggregation. Hosts use this for ultra-frequent dashboards backed by
1403
+ * rollup tables maintained out-of-band (cron / CDC).
1404
+ */
1405
+ interface AggregationMaterializedContext {
1406
+ /** Compiled filter (host base + tenant + caller). */
1407
+ filter: AnyRecord;
1408
+ /** Tenant id when the resource is tenant-scoped. */
1409
+ orgId?: string;
1410
+ /** Authenticated user id, when present. */
1411
+ userId?: string;
1412
+ /** Fastify request id for tracing. */
1413
+ requestId?: string;
1414
+ /** Raw URL query params (post-validation). */
1415
+ query: Record<string, unknown>;
1416
+ }
1417
+ /** Materialized hook return shape — same envelope as `AggResult`. */
1418
+ interface AggregationMaterializedResult<TRow = AnyRecord> {
1419
+ rows: readonly TRow[];
1420
+ }
1421
+ /**
1422
+ * Single named aggregation declaration. Composes into `AggRequest` at
1423
+ * request time, with safety knobs layered on at the arc-handler level.
1424
+ */
1425
+ interface AggregationConfig {
1426
+ /**
1427
+ * Pre-aggregate filter on the BASE rows (before lookups). Always
1428
+ * ANDed with auto-injected tenant scope + caller URL-narrowing
1429
+ * filters. Use for host-defined invariants (e.g. `archived: false`).
1430
+ */
1431
+ filter?: AnyRecord;
1432
+ /**
1433
+ * Cross-table joins. Each `LookupSpec` reuses the IR
1434
+ * `@classytic/repo-core/lookup` defines for `lookupPopulate()`.
1435
+ * Same compile path the kit already ships.
1436
+ *
1437
+ * **Kit support is incremental.** A kit's `aggregate()` may not yet
1438
+ * compile lookups — boot validation against the adapter version
1439
+ * surfaces this loud, so hosts pin the kit major they need.
1440
+ */
1441
+ lookups?: readonly LookupSpec[];
1442
+ /**
1443
+ * Group key(s). Dotted paths into joined aliases supported when
1444
+ * `lookups` is set: `'category.parent'` groups by the joined
1445
+ * `category` row's `parent` field.
1446
+ */
1447
+ groupBy?: string | readonly string[];
1448
+ /**
1449
+ * Time-bucket group keys for time-series aggregations. Each entry
1450
+ * promotes a date column into a synthetic group key bucketed at
1451
+ * the chosen interval. The map key becomes a column on the output
1452
+ * row holding the canonical ISO-shaped bucket label
1453
+ * (`'2026-04'` for month, `'2026-W15'` for ISO week, etc.).
1454
+ *
1455
+ * Bucketed keys participate in grouping the same way `groupBy`
1456
+ * columns do — `sort: { month: 1 }`, `having`, pagination, and
1457
+ * `topN.partitionBy` all treat them as first-class.
1458
+ *
1459
+ * Aliases must NOT collide with a `groupBy` field name or measure
1460
+ * alias — boot validation throws on collision.
1461
+ *
1462
+ * @example "Daily revenue for the last quarter"
1463
+ * ```ts
1464
+ * dailyRevenue: defineAggregation({
1465
+ * dateBuckets: { day: { field: 'createdAt', interval: 'day' } },
1466
+ * measures: { revenue: 'sum:totalPrice' },
1467
+ * sort: { day: 1 },
1468
+ * permissions: requireRoles(['admin']),
1469
+ * }),
1470
+ * ```
1471
+ *
1472
+ * @example "15-minute traffic buckets (custom-bin form)"
1473
+ * ```ts
1474
+ * traffic: defineAggregation({
1475
+ * dateBuckets: {
1476
+ * slot: { field: 'ts', interval: { every: 15, unit: 'minute' } },
1477
+ * },
1478
+ * measures: { hits: 'count' },
1479
+ * permissions: allowPublic(),
1480
+ * }),
1481
+ * ```
1482
+ */
1483
+ dateBuckets?: Record<string, AggDateBucket>;
1484
+ /** Named aggregations. At least one entry required. */
1485
+ measures: Record<string, AggMeasureInput>;
1486
+ /**
1487
+ * Post-aggregate filter referencing measure aliases.
1488
+ * Example: `{ revenue: { gt: 1000 } }` → `HAVING revenue > 1000`.
1489
+ */
1490
+ having?: AnyRecord;
1491
+ /** Order grouped rows by groupBy field, measure alias, or joined-alias path. */
1492
+ sort?: Record<string, 1 | -1>;
1493
+ /** Hard cap on result rows. Applied at the IR level (LIMIT / `$limit`). */
1494
+ limit?: number;
1495
+ /**
1496
+ * Top-N-per-group filter. Keeps only the top `limit` rows per
1497
+ * partition, ranked by `sortBy`. The classic "top 3 products per
1498
+ * category" / "top 5 customers per region" dashboard primitive.
1499
+ *
1500
+ * Composes with `having` / `sort` — applies AFTER group + measures +
1501
+ * having, so `partitionBy` and `sortBy` may reference groupBy fields,
1502
+ * dateBucket aliases, or measure aliases. The top-level `sort` orders
1503
+ * the final row set across partitions.
1504
+ *
1505
+ * **Per-kit support.** Mongokit compiles to `$setWindowFields` (Mongo 5+,
1506
+ * runs in-engine — scales). Sqlitekit post-processes in JS (fine for
1507
+ * typical dashboards; prefer mongokit for >100k groups). See
1508
+ * `AggTopN` for full semantics.
1509
+ */
1510
+ topN?: AggTopN;
1511
+ /**
1512
+ * Permission check. **REQUIRED.** Aggregations are read-shape but
1513
+ * different from list (different threat model — measures may expose
1514
+ * cardinality info even when individual rows are hidden). Boot error
1515
+ * if missing.
1516
+ */
1517
+ permissions: PermissionCheck;
1518
+ /**
1519
+ * DB-level execution cap (ms). Mongokit threads to `maxTimeMS`;
1520
+ * sqlitekit threads to per-statement timeout where supported.
1521
+ * Default: kit's default (typically none).
1522
+ */
1523
+ timeout?: number;
1524
+ /**
1525
+ * Reject 422 if the result row count exceeds this cap. Better than
1526
+ * silent truncation — caller knows the dashboard is incomplete.
1527
+ * Default: no cap (use `limit` for truncation semantics).
1528
+ */
1529
+ maxGroups?: number;
1530
+ /**
1531
+ * Caller MUST provide filters on these fields (else 400 at request).
1532
+ * Use to require a tenant-side narrowing the host can't infer
1533
+ * (segment id, customer id, etc.).
1534
+ */
1535
+ requireFilters?: readonly string[];
1536
+ /**
1537
+ * Caller MUST send a bounded date range on this field. Prevents
1538
+ * all-time scans on billion-row collections.
1539
+ */
1540
+ requireDateRange?: AggregationDateRangeRequirement;
1541
+ /**
1542
+ * Documented index expectation. Arc warns at boot when the kit
1543
+ * exposes index introspection and no matching index exists. NOT
1544
+ * runtime-enforced — purely a misconfig signal.
1545
+ */
1546
+ indexHint?: AggregationIndexHint;
1547
+ /**
1548
+ * Per-aggregation cache. Tenant-scoped keys; invalidates with the
1549
+ * resource's cache namespace + any explicit `tags`.
1550
+ */
1551
+ cache?: AggregationCacheConfig;
1552
+ /**
1553
+ * Per-route rate limit. Wired to `@fastify/rate-limit` when the
1554
+ * plugin is registered.
1555
+ */
1556
+ rateLimit?: AggregationRateLimit;
1557
+ /**
1558
+ * Pre-computed read replacement. When set, arc skips
1559
+ * `repo.aggregate()` and calls this function instead. Same wire
1560
+ * shape, same permissions, different data source.
1561
+ *
1562
+ * Use for homepage-counter dashboards backed by host-managed rollup
1563
+ * tables (cron / CDC). The hook receives the compiled context
1564
+ * (filter + scope) so the host can route the lookup to the right
1565
+ * pre-aggregated bucket.
1566
+ */
1567
+ materialized?: (ctx: AggregationMaterializedContext) => Promise<AggregationMaterializedResult>;
1568
+ /** Optional summary rendered in OpenAPI + MCP tool description. */
1569
+ summary?: string;
1570
+ /** Optional longer description for OpenAPI / MCP. */
1571
+ description?: string;
1572
+ /**
1573
+ * MCP tool generation control. Mirrors `actions[name].mcp` semantics.
1574
+ *
1575
+ * - `undefined` (default) — generate an MCP tool for this aggregation
1576
+ * - `false` — skip MCP tool generation (REST route still works)
1577
+ * - `{ description?, annotations? }` — generate with overrides
1578
+ */
1579
+ mcp?: boolean | {
1580
+ readonly description?: string;
1581
+ readonly annotations?: Record<string, unknown>;
1582
+ };
1583
+ }
1584
+ /** Map of name → declaration. Keys become URL segments under `/aggregations/<name>`. */
1585
+ type AggregationsMap = Record<string, AggregationConfig>;
1586
+ //#endregion
1358
1587
  //#region src/types/handlers.d.ts
1359
1588
  /**
1360
1589
  * Minimal server accessor — exposes safe, read-only server decorators.
@@ -1514,30 +1743,29 @@ interface IRequestContext<TBody = unknown, TParams extends Record<string, string
1514
1743
  * async reschedule(req: IRequestContext) {
1515
1744
  * const result = await repo.reschedule(req.params.id, req.body);
1516
1745
  * await req.server?.events?.publish('interview.rescheduled', { data: result });
1517
- * return { success: true, data: result };
1746
+ * return { data: result };
1518
1747
  * }
1519
1748
  * ```
1520
1749
  */
1521
1750
  server?: ServerAccessor;
1522
1751
  }
1523
1752
  /**
1524
- * Standard response from controller handlers
1753
+ * Controller response shape — the success-path return from any handler.
1754
+ *
1755
+ * Errors throw `ArcError` (or any `HttpError`-shaped class); the global
1756
+ * error handler catches them and emits an `ErrorContract`. There is no
1757
+ * `success` discriminator on the response — HTTP status is the wire
1758
+ * discriminator (2xx = data, 4xx/5xx = ErrorContract).
1525
1759
  */
1526
1760
  interface IControllerResponse<T = unknown> {
1527
- /** Operation success status */
1528
- success: boolean;
1529
- /** Response data */
1530
- data?: T;
1531
- /** Error message (when success is false) */
1532
- error?: string;
1533
- /** HTTP status code (default: 200 for success, 400 for error) */
1761
+ /** Response payload emitted directly to the wire (no envelope wrap). */
1762
+ data: T;
1763
+ /** HTTP status code. Defaults to 200. */
1534
1764
  status?: number;
1535
- /** Additional metadata */
1536
- meta?: Record<string, unknown>;
1537
- /** Error details (for debugging) */
1538
- details?: Record<string, unknown>;
1539
- /** Custom response headers (e.g., X-Total-Count, Link, ETag) */
1765
+ /** Custom response headers (e.g. X-Total-Count, Link, ETag). */
1540
1766
  headers?: Record<string, string>;
1767
+ /** Top-level metadata merged into list-shaped responses (e.g. `{ took }`). */
1768
+ meta?: Record<string, unknown>;
1541
1769
  }
1542
1770
  /**
1543
1771
  * Controller handler — Arc's standard pattern.
@@ -1560,7 +1788,7 @@ interface IControllerResponse<T = unknown> {
1560
1788
  * // Untyped req — body is unknown, must be narrowed
1561
1789
  * const createProduct: ControllerHandler<Product> = async (req) => {
1562
1790
  * const product = await productRepo.create(req.body as Partial<Product>);
1563
- * return { success: true, data: product, status: 201 };
1791
+ * return { data: product, status: 201 };
1564
1792
  * };
1565
1793
  *
1566
1794
  * // Fully typed — body, params, query, and response all inferred
@@ -1572,7 +1800,7 @@ interface IControllerResponse<T = unknown> {
1572
1800
  * > = async (req) => {
1573
1801
  * const upsert = req.query.upsert === "true";
1574
1802
  * const product = await productRepo.update(req.params.id, req.body, { upsert });
1575
- * return { success: true, data: product };
1803
+ * return { data: product };
1576
1804
  * };
1577
1805
  *
1578
1806
  * routes: [{
@@ -2116,9 +2344,9 @@ interface ResourceHookContext {
2116
2344
  * ```
2117
2345
  */
2118
2346
  interface ResourceHooks {
2119
- beforeCreate?: (ctx: ResourceHookContext) => Promise<AnyRecord | void> | AnyRecord | void;
2347
+ beforeCreate?: (ctx: ResourceHookContext) => Promise<AnyRecord | undefined> | AnyRecord | undefined;
2120
2348
  afterCreate?: (ctx: ResourceHookContext) => Promise<void> | void;
2121
- beforeUpdate?: (ctx: ResourceHookContext) => Promise<AnyRecord | void> | AnyRecord | void;
2349
+ beforeUpdate?: (ctx: ResourceHookContext) => Promise<AnyRecord | undefined> | AnyRecord | undefined;
2122
2350
  afterUpdate?: (ctx: ResourceHookContext) => Promise<void> | void;
2123
2351
  beforeDelete?: (ctx: ResourceHookContext) => Promise<void> | void;
2124
2352
  afterDelete?: (ctx: ResourceHookContext) => Promise<void> | void;
@@ -2270,6 +2498,31 @@ interface ResourceConfig<TDoc = AnyRecord> {
2270
2498
  * Only applies when `actions` is defined.
2271
2499
  */
2272
2500
  actionPermissions?: PermissionCheck;
2501
+ /**
2502
+ * Declarative aggregations (v2.13) — generate `GET /:resource/aggregations/:name`
2503
+ * routes from the portable `AggRequest` IR. Each entry pins permissions,
2504
+ * filters, lookups, measures, sort, limit, plus big-data safety knobs
2505
+ * (timeout, maxGroups, requireDateRange, indexHint, materialized).
2506
+ *
2507
+ * @example
2508
+ * ```ts
2509
+ * defineResource({
2510
+ * name: 'order',
2511
+ * aggregations: {
2512
+ * revenueByStatus: defineAggregation({
2513
+ * groupBy: 'status',
2514
+ * measures: { count: 'count', revenue: 'sum:totalPrice' },
2515
+ * permissions: requireRoles(['admin']),
2516
+ * requireDateRange: { field: 'createdAt', maxRangeDays: 90 },
2517
+ * timeout: 5000,
2518
+ * maxGroups: 1000,
2519
+ * cache: { staleTime: 60 },
2520
+ * }),
2521
+ * },
2522
+ * });
2523
+ * ```
2524
+ */
2525
+ aggregations?: AggregationsMap;
2273
2526
  disableCrud?: boolean;
2274
2527
  disableDefaultRoutes?: boolean;
2275
2528
  /** Specific routes to disable */
@@ -2360,31 +2613,13 @@ interface ResourceConfig<TDoc = AnyRecord> {
2360
2613
  };
2361
2614
  }
2362
2615
  //#endregion
2363
- //#region src/core/defineResource.d.ts
2616
+ //#region src/core/defineResource/ResourceDefinition.d.ts
2364
2617
  /**
2365
- * Define a resource with database adapter.
2366
- *
2367
- * This is the MAIN entry point for creating Arc resources — the adapter
2368
- * provides both repository and schema metadata.
2369
- *
2370
- * Staged into seven named phases so future refactors touch one phase at a
2371
- * time instead of threading changes through a 450-line function:
2372
- *
2373
- * 1. validate — fail-fast structural checks
2374
- * 2. resolveIdField — auto-derive `idField` from repository
2375
- * 3. applyPresetsAndAutoInject — clone + apply presets + tenant-field rules
2376
- * 4. resolveController — reuse user controller or auto-create BaseController
2377
- * 5. buildResource — construct ResourceDefinition + validate methods
2378
- * 6. wireHooks — push preset + inline `config.hooks` onto _pendingHooks
2379
- * 7. resolveOpenApiSchemas — adapter schemas → parser listQuery → user override
2380
- *
2381
- * Each phase has a single responsibility; `resolvedConfig` is the canonical
2382
- * post-preset, post-auto-inject config that every later phase reads. Raw
2383
- * `config` is only consulted for things presets don't touch (adapter,
2384
- * skipRegistry, skipValidation, hooks — which are wired separately from
2385
- * preset hooks).
2618
+ * Constructor input shape `ResourceConfig` plus the metadata
2619
+ * Phases 3-6 stamp on it. Defined locally because the class
2620
+ * constructor is the only consumer; no other code path needs this
2621
+ * type.
2386
2622
  */
2387
- declare function defineResource<TDoc = AnyRecord>(config: ResourceConfig<TDoc>): ResourceDefinition<TDoc>;
2388
2623
  interface ResolvedResourceConfig<TDoc = AnyRecord> extends ResourceConfig<TDoc> {
2389
2624
  _appliedPresets?: string[];
2390
2625
  _controllerOptions?: {
@@ -2404,6 +2639,7 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
2404
2639
  readonly displayName: string;
2405
2640
  readonly tag: string;
2406
2641
  readonly prefix: string;
2642
+ readonly skipGlobalPrefix: boolean;
2407
2643
  readonly adapter?: DataAdapter<TDoc>;
2408
2644
  readonly controller?: IController<TDoc>;
2409
2645
  readonly schemaOptions: RouteSchemaOptions;
@@ -2413,9 +2649,10 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
2413
2649
  readonly middlewares: MiddlewareConfig;
2414
2650
  readonly routeGuards?: RouteHandlerMethod$1[];
2415
2651
  readonly disableDefaultRoutes: boolean;
2416
- readonly disabledRoutes: CrudRouteKey[];
2652
+ readonly disabledRoutes: readonly CrudRouteKey[];
2417
2653
  readonly actions?: ActionsMap;
2418
2654
  readonly actionPermissions?: PermissionCheck;
2655
+ readonly aggregations?: AggregationsMap;
2419
2656
  readonly events: Record<string, EventDefinition>;
2420
2657
  readonly rateLimit?: RateLimitConfig | false;
2421
2658
  readonly audit?: boolean | {
@@ -2425,7 +2662,6 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
2425
2662
  readonly pipe?: PipelineConfig;
2426
2663
  readonly fields?: FieldPermissionMap;
2427
2664
  readonly cache?: ResourceCacheConfig;
2428
- readonly skipGlobalPrefix: boolean;
2429
2665
  readonly tenantField?: string | false;
2430
2666
  readonly idField?: string;
2431
2667
  readonly queryParser?: QueryParserInterface;
@@ -2437,32 +2673,78 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
2437
2673
  priority: number;
2438
2674
  }>;
2439
2675
  _registryMeta?: RegisterOptions;
2676
+ /**
2677
+ * Per-host idempotency guard used by `buildResourcePlugin` to
2678
+ * skip duplicate shared-state writes when the same resource is
2679
+ * mounted at multiple prefixes (`/v1`, `/v2`). See the plugin
2680
+ * file for the full rationale; surfaced here as `readonly` so
2681
+ * the helper can consult it without a class-method indirection.
2682
+ */
2683
+ readonly _sharedStateRegisteredOn: WeakSet<object>;
2440
2684
  constructor(config: ResolvedResourceConfig<TDoc>);
2441
- /** Get repository from adapter (if available) */
2442
- get repository(): RepositoryLike<TDoc> | undefined;
2685
+ /** Repository accessor pulled off the adapter when one is wired. */
2686
+ get repository(): _$_classytic_repo_core_adapter0.RepositoryLike<TDoc> | undefined;
2687
+ /**
2688
+ * Validate that the wired controller implements every method
2689
+ * needed by enabled CRUD routes + every string-handler custom
2690
+ * route. Runs at the end of `defineResource()` (skippable via
2691
+ * `skipValidation: true`) so misconfigured resources fail at
2692
+ * boot, not on first request.
2693
+ */
2443
2694
  _validateControllerMethods(): void;
2444
- toPlugin(): FastifyPluginAsync;
2445
2695
  /**
2446
- * Get event definitions for registry
2696
+ * Build the Fastify plugin that materialises this resource into
2697
+ * routes, hooks, registry entries, and cache invalidation rules.
2698
+ * One-line delegate — the implementation lives in `./plugin.ts`.
2447
2699
  */
2700
+ toPlugin(): FastifyPluginAsync;
2701
+ /** Event definitions for registry consumption. */
2448
2702
  getEvents(): Array<{
2449
2703
  name: string;
2450
2704
  module: string;
2451
2705
  schema?: unknown;
2452
2706
  description?: string;
2453
2707
  }>;
2454
- /**
2455
- * Get resource metadata
2456
- */
2708
+ /** Resource metadata — shape consumed by registry / introspection. */
2457
2709
  getMetadata(): ResourceMetadata;
2458
2710
  }
2459
2711
  //#endregion
2712
+ //#region src/core/defineResource.d.ts
2713
+ /**
2714
+ * `TDoc` is **unconstrained** at this layer. The previous `TDoc
2715
+ * extends AnyRecord` bound leaked out of `BaseController`'s
2716
+ * mixin-composition requirement into every host's adapter boundary:
2717
+ * Mongoose's `HydratedDocument<T>`, Prisma's generated row types,
2718
+ * and any domain interface without an explicit index signature all
2719
+ * failed to satisfy `Record<string, unknown>` even though at runtime
2720
+ * they ARE string-keyed objects. Hosts were forced to cast at every
2721
+ * adapter (`as RepositoryLike<Record<string, unknown>>`) — a type
2722
+ * escape with no runtime purpose, since arc's pipeline only reads
2723
+ * known envelope fields.
2724
+ *
2725
+ * The cast moved inside `resolveOrAutoCreateController` where
2726
+ * `BaseController<TDoc extends AnyRecord>` actually requires it.
2727
+ * One internal boundary cast replaces N host-side casts.
2728
+ */
2729
+ declare function defineResource<TDoc = AnyRecord>(config: ResourceConfig<TDoc>): ResourceDefinition<TDoc>;
2730
+ //#endregion
2460
2731
  //#region src/registry/ResourceRegistry.d.ts
2461
2732
  interface RegisterOptions {
2462
2733
  module?: string;
2463
2734
  /** Pre-generated OpenAPI schemas */
2464
2735
  openApiSchemas?: OpenApiSchemas;
2465
2736
  }
2737
+ /**
2738
+ * One enumerated wire route. Matches `ResourceMetadata.routes[]`'s shape so
2739
+ * it slots straight into `IntrospectionData` without re-mapping.
2740
+ */
2741
+ interface RouteRow {
2742
+ method: string;
2743
+ path: string;
2744
+ operation?: string;
2745
+ handler?: string;
2746
+ summary?: string;
2747
+ }
2466
2748
  declare class ResourceRegistry {
2467
2749
  private _resources;
2468
2750
  private _frozen;
@@ -2493,12 +2775,36 @@ declare class ResourceRegistry {
2493
2775
  has(name: string): boolean;
2494
2776
  /**
2495
2777
  * Get registry statistics
2778
+ *
2779
+ * `totalRoutes` is derived from `enumerateRoutes()` — single source of
2780
+ * truth shared with `getIntrospection()` and consistent with what
2781
+ * OpenAPI / Fastify actually mount. New route sources (e.g. v2.13
2782
+ * aggregations) light up here automatically.
2496
2783
  */
2497
2784
  getStats(): RegistryStats;
2498
2785
  /**
2499
2786
  * Get full introspection data
2787
+ *
2788
+ * Routes come from `enumerateRoutes()` so consumers see the complete
2789
+ * surface — CRUD + custom + actions + aggregations — and match what
2790
+ * `getStats()` counts.
2500
2791
  */
2501
2792
  getIntrospection(): IntrospectionData;
2793
+ /**
2794
+ * Single source of truth for "what routes does this resource expose?".
2795
+ *
2796
+ * Enumerates every wire route the resource will mount on Fastify:
2797
+ * - default CRUD (respecting `disabledRoutes` + `updateMethod`)
2798
+ * - host-declared `customRoutes` (alias: `routes`)
2799
+ * - the unified `POST /:id/action` endpoint when `actions` is set
2800
+ * - one `GET /:resource/aggregations/:name` per declared aggregation
2801
+ *
2802
+ * Both `getStats()` and `getIntrospection()` consume this list, so a
2803
+ * new route source (e.g. future webhook routes) only has to be added
2804
+ * here — the count and the introspection contract update together.
2805
+ * Mirrors the same set of paths emitted by `docs/openapi.ts`.
2806
+ */
2807
+ enumerateRoutes(r: RegistryEntry): RouteRow[];
2502
2808
  /**
2503
2809
  * Freeze registry (prevent further registrations)
2504
2810
  */
@@ -2870,13 +3176,13 @@ interface RegistryEntry extends ResourceMetadata {
2870
3176
  disableDefaultRoutes?: boolean;
2871
3177
  openApiSchemas?: OpenApiSchemas;
2872
3178
  registeredAt?: string;
2873
- /** Field-level permissions metadata (for OpenAPI docs) */
3179
+ /** Field-level permissions metadata (for OpenAPI data) */
2874
3180
  fieldPermissions?: Record<string, {
2875
3181
  type: string;
2876
3182
  roles?: readonly string[];
2877
3183
  redactValue?: unknown;
2878
3184
  }>;
2879
- /** Pipeline step names (for OpenAPI docs) */
3185
+ /** Pipeline step names (for OpenAPI data) */
2880
3186
  pipelineSteps?: Array<{
2881
3187
  type: string;
2882
3188
  name: string;
@@ -2914,6 +3220,34 @@ interface RegistryEntry extends ResourceMetadata {
2914
3220
  * MCP as the fallback in `createActionToolHandler`. Added in 2.8.1.
2915
3221
  */
2916
3222
  actionPermissions?: PermissionCheck;
3223
+ /**
3224
+ * Aggregation route metadata (v2.13). Mirrors the runtime config in
3225
+ * a doc-friendly shape so OpenAPI emission and MCP tool generation
3226
+ * read from one source.
3227
+ *
3228
+ * Each entry corresponds to a `GET /:resource/aggregations/:name`
3229
+ * route. Response shape (rows array of objects keyed by groupBy +
3230
+ * measure aliases) is derived at OpenAPI emission time from
3231
+ * `groupBy` + `measures` + `lookups`.
3232
+ */
3233
+ aggregations?: Array<{
3234
+ readonly name: string;
3235
+ readonly summary?: string;
3236
+ readonly description?: string;
3237
+ readonly permissions: PermissionCheck;
3238
+ readonly groupBy?: string | readonly string[]; /** Measure aliases keyed to their op-tag (e.g. `'count'`, `'sum:price'`). */
3239
+ readonly measures: Readonly<Record<string, string>>; /** Lookup alias names (`as` or `from`) — used by OpenAPI to know which dotted-path output keys nest. */
3240
+ readonly lookupAliases: readonly string[]; /** Whether the aggregation requires a date range — surfaced in docs. */
3241
+ readonly requireDateRange?: {
3242
+ field: string;
3243
+ maxRangeDays?: number;
3244
+ }; /** Whether the aggregation requires named filters — surfaced in docs. */
3245
+ readonly requireFilters?: readonly string[]; /** MCP tool generation flag — `false` to skip, object for overrides. */
3246
+ readonly mcp?: boolean | {
3247
+ readonly description?: string;
3248
+ readonly annotations?: Record<string, unknown>;
3249
+ };
3250
+ }>;
2917
3251
  }
2918
3252
  interface RegistryStats {
2919
3253
  total?: number;
@@ -2997,4 +3331,4 @@ interface ValidateOptions {
2997
3331
  strict?: boolean;
2998
3332
  }
2999
3333
  //#endregion
3000
- export { OpenApiSchemas as $, ArcListResult as $t, RequestIdOptions as A, FieldMetadata as An, Authenticator as At, ResourceDefinition as B, UserOrganization as Bt, RequestContext as C, beforeUpdate as Cn, OperationFilter as Ct, HealthCheck as D, AdapterRepositoryInput as Dn, Transform as Dt, GracefulShutdownOptions as E, AdapterFactory as En, PipelineStep as Et, FastifyWithDecorators as F, asRepositoryLike as Fn, 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, RepositoryLike as Mn, JwtContext as Mt, FastifyRequestExtras as N, SchemaMetadata as Nn, TokenPair as Nt, HealthOptions as O, AdapterSchemaContext as On, AuthHelpers as Ot, FastifyWithAuth as P, ValidationResult$1 as Pn, 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, RelationMetadata as jn, AuthenticatorContext as jt, IntrospectionPluginOptions as k, DataAdapter 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 };
3334
+ export { OpenApiSchemas as $, SoftDeleteMixin as $t, RequestIdOptions as A, afterUpdate as An, Guard as At, defineResource as B, Authenticator as Bt, RequestContext as C, HookOperation as Cn, AggregationConfig as Ct, HealthCheck as D, HookSystemOptions as Dn, AggregationMaterializedResult as Dt, GracefulShutdownOptions as E, HookSystem as En, AggregationMaterializedContext as Et, FastifyWithDecorators as F, defineHook as Fn, PipelineContext as Ft, ActionsMap as G, ApiResponse as Gt, ActionDefinition as H, JwtContext as Ht, MiddlewareHandler as I, PipelineStep as It, CrudRouteKey as J, ObjectId as Jt, ArcFieldRule as K, ArcRequest as Kt, RequestWithExtras as L, Transform as Lt, EventsDecorator as M, beforeDelete as Mn, NextFunction as Mt, FastifyRequestExtras as N, beforeUpdate as Nn, OperationFilter as Nt, HealthOptions as O, afterCreate as On, AggregationRateLimit as Ot, FastifyWithAuth as P, createHookSystem as Pn, PipelineConfig as Pt, MiddlewareConfig as Q, SoftDeleteExt as Qt, RegisterOptions as R, AuthHelpers as Rt, QueryParserInterface as S, HookHandler as Sn, AggregationCacheConfig as St, CrudRouterOptions as T, HookRegistration as Tn, AggregationIndexHint as Tt, ActionEntry as U, TokenPair as Ut, ResourceDefinition as V, AuthenticatorContext as Vt, ActionHandlerFn as W, AnyRecord as Wt, EventDefinition as X, UserOrganization as Xt, CrudSchemas as Y, UserLike as Yt, FieldRule$1 as Z, BaseController as Zt, ControllerQueryOptions as _, BodySanitizerConfig as _n, IControllerResponse as _t, InferAdapterDoc as a, BulkMixin as an, ResourceConfig as at, ParsedQuery as b, DefineHookOptions as bn, AggMeasureInput as bt, TypedController as c, QueryResolver as cn, ResourcePermissions as ct, PaginationResult as d, ArcDeleteResult as dn, RouteMethod as dt, TreeExt as en, PresetFunction as et, IntrospectionData as f, ArcGetResult as fn, RouteSchemaOptions as ft, ArcInternalMetadata as g, BodySanitizer as gn, IController as gt, ResourceMetadata as h, ListResult as hn, FastifyHandler as ht, ValidationResult as i, BulkExt as in, ResourceCacheConfig as it, ArcDecorator as j, beforeCreate as jn, Interceptor as jt, IntrospectionPluginOptions as k, afterDelete as kn, AggregationsMap as kt, TypedRepository as l, QueryResolverConfig as ln, RouteDefinition as lt, RegistryStats as m, ArcUpdateResult as mn, ControllerLike as mt, ConfigError as n, SlugExt as nn, PresetResult as nt, InferDocType as o, BaseControllerOptions as on, ResourceHookContext as ot, RegistryEntry as p, ArcListResult as pn, ControllerHandler as pt, CrudController as q, JWTPayload as qt, ValidateOptions as r, SlugMixin as rn, RateLimitConfig as rt, InferResourceDoc as s, BaseCrudController as sn, ResourceHooks as st, RouteHandlerMethod$1 as t, TreeMixin as tn, PresetHook as tt, TypedResourceConfig as u, ArcCreateResult as un, RouteMcpConfig as ut, LookupOption as v, AccessControl as vn, IRequestContext as vt, ServiceContext as w, HookPhase as wn, AggregationDateRangeRequirement as wt, PopulateOption as x, HookContext as xn, AggMeasureShorthand as xt, OwnershipCheck as y, AccessControlConfig as yn, RouteHandler as yt, ResourceRegistry as z, AuthPluginOptions as zt };