@classytic/arc 2.10.8 → 2.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BaseController-DVNKvoX4.mjs → BaseController-JNV08qOT.mjs} +480 -442
- package/dist/{queryCachePlugin-Dumka73q.d.mts → QueryCache-DOBNHBE0.d.mts} +2 -32
- package/dist/adapters/index.d.mts +2 -2
- package/dist/adapters/index.mjs +1 -1
- package/dist/{adapters-BXY4i-hw.mjs → adapters-D0tT2Tyo.mjs} +54 -0
- package/dist/audit/index.d.mts +1 -1
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.mjs +5 -5
- package/dist/{betterAuthOpenApi--rdY15Ld.mjs → betterAuthOpenApi-DwxtK3uG.mjs} +1 -1
- package/dist/cache/index.d.mts +3 -2
- package/dist/cache/index.mjs +3 -3
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +37 -27
- package/dist/cli/commands/init.mjs +46 -33
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/context/index.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -3
- package/dist/core-DXdSSFW-.mjs +1037 -0
- package/dist/createActionRouter-BwaSM0No.mjs +166 -0
- package/dist/{createApp-BwnEAO2h.mjs → createApp-P1d6rjPy.mjs} +75 -27
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-Dci0AYLT.mjs → elevation-DOFoxoDs.mjs} +1 -1
- package/dist/{errorHandler-CSxe7KIM.mjs → errorHandler-BQm8ZxTK.mjs} +1 -1
- package/dist/{eventPlugin-ByU4Cv0e.mjs → eventPlugin--5HIkdPU.mjs} +1 -1
- package/dist/events/index.d.mts +3 -3
- package/dist/events/index.mjs +2 -2
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/factory/index.d.mts +2 -2
- package/dist/factory/index.mjs +2 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/index.mjs +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-C_Noptz-.d.mts → index-BYCqHCVu.d.mts} +2 -2
- package/dist/{index-BGbpGVyM.d.mts → index-C_bgx9o4.d.mts} +712 -500
- package/dist/{index-BziRPS4H.d.mts → index-CvM1e09j.d.mts} +29 -10
- package/dist/{index-EqQN6p0W.d.mts → index-pUczGjO0.d.mts} +11 -8
- package/dist/index-smCAoA5W.d.mts +1179 -0
- package/dist/index.d.mts +6 -38
- package/dist/index.mjs +9 -9
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +46 -5
- package/dist/integrations/streamline.mjs +50 -21
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +2 -154
- package/dist/integrations/websocket.mjs +292 -224
- package/dist/{keys-nWQGUTu1.mjs → keys-CARyUjiR.mjs} +2 -0
- package/dist/{loadResources-Bksk8ydA.mjs → loadResources-CPpkyKfM.mjs} +32 -8
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/{openapi-DpNpqBmo.mjs → openapi-C0L9ar7m.mjs} +4 -4
- package/dist/org/index.d.mts +1 -1
- package/dist/permissions/index.d.mts +1 -1
- package/dist/permissions/index.mjs +2 -4
- package/dist/{permissions-wkqRwicB.mjs → permissions-B4vU9L0Q.mjs} +221 -3
- package/dist/{pipe-CGJxqDGx.mjs → pipe-DVoIheVC.mjs} +1 -1
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +4 -4
- package/dist/plugins/index.mjs +10 -10
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +42 -24
- package/dist/presets/filesUpload.d.mts +1 -1
- package/dist/presets/filesUpload.mjs +3 -3
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +6 -0
- package/dist/presets/search.d.mts +1 -1
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-CrwOvuXI.mjs → presets-k604Lj99.mjs} +1 -1
- package/dist/queryCachePlugin-BUXBSm4F.d.mts +34 -0
- package/dist/{queryCachePlugin-ChLNZvFT.mjs → queryCachePlugin-Bq6bO6vc.mjs} +3 -3
- package/dist/{redis-MXLp1oOf.d.mts → redis-Cm1gnRDf.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{resourceToTools-BhF3JV5p.mjs → resourceToTools--okX6QBr.mjs} +534 -420
- package/dist/routerShared-DeESFp4a.mjs +515 -0
- package/dist/schemaIR-BlG9bY7v.mjs +137 -0
- package/dist/scope/index.mjs +2 -2
- package/dist/testing/index.d.mts +367 -711
- package/dist/testing/index.mjs +637 -1434
- package/dist/{tracing-xqXzWeaf.d.mts → tracing-DokiEsuz.d.mts} +9 -4
- package/dist/types/index.d.mts +3 -3
- package/dist/types/index.mjs +1 -3
- package/dist/{types-CVdgPXBW.d.mts → types-BdA4uMBV.d.mts} +191 -28
- package/dist/{types-CVKBssX5.d.mts → types-Bh_gEJBi.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -968
- package/dist/utils/index.mjs +5 -6
- package/dist/utils-D3Yxnrwr.mjs +1639 -0
- package/dist/websocket-CyJ1VIFI.d.mts +186 -0
- package/package.json +7 -5
- package/skills/arc/SKILL.md +124 -39
- package/skills/arc/references/testing.md +212 -183
- package/dist/applyPermissionResult-QhV1Pa-g.mjs +0 -37
- package/dist/core-3MWJosCH.mjs +0 -1459
- package/dist/createActionRouter-C8UUB3Px.mjs +0 -249
- package/dist/errors-BI8kEKsO.d.mts +0 -140
- package/dist/fields-CTMWOUDt.mjs +0 -126
- package/dist/queryParser-NR__Qiju.mjs +0 -419
- package/dist/types-CDnTEpga.mjs +0 -27
- package/dist/utils-LMwVidKy.mjs +0 -947
- /package/dist/{HookSystem-BjFu7zf1.mjs → HookSystem-CGsMd6oK.mjs} +0 -0
- /package/dist/{ResourceRegistry-CcN2LVrc.mjs → ResourceRegistry-DkAeAuTX.mjs} +0 -0
- /package/dist/{actionPermissions-TUVR3uiZ.mjs → actionPermissions-C8YYU92K.mjs} +0 -0
- /package/dist/{caching-3h93rkJM.mjs → caching-CheW3m-S.mjs} +0 -0
- /package/dist/{errorHandler-2ii4RIYr.d.mts → errorHandler-Co3lnVmJ.d.mts} +0 -0
- /package/dist/{errors-BqdUDja_.mjs → errors-D5c-5BJL.mjs} +0 -0
- /package/dist/{eventPlugin-D1ThQ1Pp.d.mts → eventPlugin-CUNjYYRY.d.mts} +0 -0
- /package/dist/{interface-B-pe8fhj.d.mts → interface-CkkWm5uR.d.mts} +0 -0
- /package/dist/{interface-yhyb_pLY.d.mts → interface-Da0r7Lna.d.mts} +0 -0
- /package/dist/{memory-DqI-449b.mjs → memory-DikHSvWa.mjs} +0 -0
- /package/dist/{metrics-TuOmguhi.mjs → metrics-Csh4nsvv.mjs} +0 -0
- /package/dist/{multipartBody-CUQGVlM_.mjs → multipartBody-CvTR1Un6.mjs} +0 -0
- /package/dist/{pluralize-CWP6MB39.mjs → pluralize-BneOJkpi.mjs} +0 -0
- /package/dist/{redis-stream-bkO88VHx.d.mts → redis-stream-CM8TXTix.d.mts} +0 -0
- /package/dist/{registry-B0Wl7uVV.mjs → registry-D63ee7fl.mjs} +0 -0
- /package/dist/{replyHelpers-BLojtuvR.mjs → replyHelpers-ByllIXXV.mjs} +0 -0
- /package/dist/{requestContext-C38GskNt.mjs → requestContext-CfRkaxwf.mjs} +0 -0
- /package/dist/{schemaConverter-BxFDdtXu.mjs → schemaConverter-B0oKLuqI.mjs} +0 -0
- /package/dist/{sse-D8UeDwis.mjs → sse-V7aXc3bW.mjs} +0 -0
- /package/dist/{store-helpers-DYYUQbQN.mjs → store-helpers-BhrzxvyQ.mjs} +0 -0
- /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
- /package/dist/{types-D57iXYb8.mjs → types-DV9WDfeg.mjs} +0 -0
- /package/dist/{versioning-B6mimogM.mjs → versioning-CGPjkqAg.mjs} +0 -0
- /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
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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.
|
|
29
|
-
* `
|
|
30
|
-
* `
|
|
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:
|
|
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/
|
|
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.
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
* - `
|
|
342
|
-
*
|
|
343
|
-
*
|
|
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
|
-
* - `
|
|
370
|
-
*
|
|
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
|
-
* - `'
|
|
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
|
|
719
|
+
* Framework-agnostic CRUD controller implementing IController.
|
|
430
720
|
*
|
|
431
|
-
* Composes AccessControl, BodySanitizer, and QueryResolver
|
|
432
|
-
*
|
|
433
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
*
|
|
461
|
-
*
|
|
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
|
-
*
|
|
464
|
-
*
|
|
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
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
* though arc already injected the tenant into the request body.
|
|
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.
|
|
476
787
|
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
* elevated admin impersonating an org).
|
|
481
|
-
* - `{}` otherwise — platform-universal resources (`tenantField: false`),
|
|
482
|
-
* public/anonymous reads, elevated admins without an org target.
|
|
483
|
-
*
|
|
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
|
-
|
|
792
|
+
protected tenantRepoOptions(req: IRequestContext): AnyRecord;
|
|
497
793
|
/** Extract typed Arc internal metadata from request */
|
|
498
|
-
|
|
794
|
+
protected meta(req: IRequestContext): ArcInternalMetadata | undefined;
|
|
499
795
|
/** Get hook system from request context (instance-scoped) */
|
|
500
|
-
|
|
796
|
+
protected getHooks(req: IRequestContext): HookSystem | null;
|
|
501
797
|
/**
|
|
502
|
-
* Resolve the repository primary key for mutation calls
|
|
798
|
+
* Resolve the repository primary key for mutation calls.
|
|
503
799
|
*
|
|
504
|
-
* When the resource declares a custom `idField` (
|
|
505
|
-
*
|
|
506
|
-
* because most Mongo repositories key
|
|
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`.
|
|
507
803
|
*
|
|
508
|
-
* Exception: if the
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
* route id through unchanged and skip the translation.
|
|
512
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
522
|
-
*
|
|
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
|
-
|
|
815
|
+
protected notFoundResponse(reason?: FetchDenialReason | null): IControllerResponse<never>;
|
|
528
816
|
/** Resolve cache config for a specific operation, merging per-op overrides */
|
|
529
|
-
|
|
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
|
|
533
|
-
* Universal resources (tenantField: false) get shared cache keys
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,7 +1646,110 @@ interface RateLimitConfig {
|
|
|
1285
1646
|
/** Time window for rate limiting (e.g., '1 minute', '15 seconds') */
|
|
1286
1647
|
timeWindow: string;
|
|
1287
1648
|
}
|
|
1288
|
-
|
|
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[];
|
|
@@ -1294,51 +1758,26 @@ interface RouteSchemaOptions {
|
|
|
1294
1758
|
/**
|
|
1295
1759
|
* Fields allowed for filtering in list operations. MCP auto-derives
|
|
1296
1760
|
* from `QueryParser.allowedFilterFields` when not set explicitly.
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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>;
|
|
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
|
-
*
|
|
1333
|
-
*
|
|
1334
|
-
*
|
|
1335
|
-
* `
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
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
|
-
|
|
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;
|
|
@@ -1503,10 +1942,13 @@ interface ActionDefinition {
|
|
|
1503
1942
|
readonly permissions?: PermissionCheck;
|
|
1504
1943
|
/**
|
|
1505
1944
|
* JSON Schema or Zod v4 schema for action-specific body fields.
|
|
1506
|
-
*
|
|
1507
|
-
*
|
|
1945
|
+
*
|
|
1946
|
+
* Typed `unknown` (not `Record<string, unknown>`) so Zod class instances
|
|
1947
|
+
* — `ZodObject<...>` carries no string index signature — assign without
|
|
1948
|
+
* a cast. Same convention as `RouteDefinition.schema.body` / `customSchemas`.
|
|
1949
|
+
* Runtime feature-detects via `convertRouteSchema` / `toJsonSchema`.
|
|
1508
1950
|
*/
|
|
1509
|
-
readonly schema?:
|
|
1951
|
+
readonly schema?: unknown;
|
|
1510
1952
|
/** Description for OpenAPI docs and MCP tool */
|
|
1511
1953
|
readonly description?: string;
|
|
1512
1954
|
/**
|
|
@@ -1621,8 +2063,12 @@ interface EventDefinition {
|
|
|
1621
2063
|
name: string;
|
|
1622
2064
|
/** Optional handler — events are published via `fastify.events.publish()`. */
|
|
1623
2065
|
handler?: (data: unknown) => Promise<void> | void;
|
|
1624
|
-
/**
|
|
1625
|
-
|
|
2066
|
+
/**
|
|
2067
|
+
* JSON Schema or Zod v4 schema for event payload. Typed `unknown` so Zod
|
|
2068
|
+
* class instances assign without a cast (same convention as
|
|
2069
|
+
* `ActionDefinition.schema` and `RouteDefinition.schema`).
|
|
2070
|
+
*/
|
|
2071
|
+
schema?: unknown;
|
|
1626
2072
|
description?: string;
|
|
1627
2073
|
}
|
|
1628
2074
|
/** Resource-level permissions — only `PermissionCheck` functions allowed. */
|
|
@@ -1834,263 +2280,29 @@ interface ResourceConfig<TDoc = AnyRecord> {
|
|
|
1834
2280
|
};
|
|
1835
2281
|
}
|
|
1836
2282
|
//#endregion
|
|
1837
|
-
//#region src/
|
|
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
|
-
}
|
|
2283
|
+
//#region src/core/defineResource.d.ts
|
|
2036
2284
|
/**
|
|
2037
|
-
* Define a
|
|
2038
|
-
* Returns a registration object — call `register(hookSystem)` to activate.
|
|
2285
|
+
* Define a resource with database adapter.
|
|
2039
2286
|
*
|
|
2040
|
-
*
|
|
2041
|
-
*
|
|
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
|
-
* });
|
|
2287
|
+
* This is the MAIN entry point for creating Arc resources — the adapter
|
|
2288
|
+
* provides both repository and schema metadata.
|
|
2047
2289
|
*
|
|
2048
|
-
*
|
|
2049
|
-
*
|
|
2050
|
-
* resource: 'product', operation: 'create', phase: 'before',
|
|
2051
|
-
* dependsOn: ['generateSlug'],
|
|
2052
|
-
* handler: async (ctx) => { // check uniqueness },
|
|
2053
|
-
* });
|
|
2290
|
+
* Staged into seven named phases so future refactors touch one phase at a
|
|
2291
|
+
* time instead of threading changes through a 450-line function:
|
|
2054
2292
|
*
|
|
2055
|
-
*
|
|
2056
|
-
*
|
|
2057
|
-
*
|
|
2058
|
-
*
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
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
|
|
2293
|
+
* 1. validate — fail-fast structural checks
|
|
2294
|
+
* 2. resolveIdField — auto-derive `idField` from repository
|
|
2295
|
+
* 3. applyPresetsAndAutoInject — clone + apply presets + tenant-field rules
|
|
2296
|
+
* 4. resolveController — reuse user controller or auto-create BaseController
|
|
2297
|
+
* 5. buildResource — construct ResourceDefinition + validate methods
|
|
2298
|
+
* 6. wireHooks — push preset + inline `config.hooks` onto _pendingHooks
|
|
2299
|
+
* 7. resolveOpenApiSchemas — adapter schemas → parser listQuery → user override
|
|
2091
2300
|
*
|
|
2092
|
-
*
|
|
2093
|
-
*
|
|
2301
|
+
* Each phase has a single responsibility; `resolvedConfig` is the canonical
|
|
2302
|
+
* post-preset, post-auto-inject config that every later phase reads. Raw
|
|
2303
|
+
* `config` is only consulted for things presets don't touch (adapter,
|
|
2304
|
+
* skipRegistry, skipValidation, hooks — which are wired separately from
|
|
2305
|
+
* preset hooks).
|
|
2094
2306
|
*/
|
|
2095
2307
|
declare function defineResource<TDoc = AnyRecord>(config: ResourceConfig<TDoc>): ResourceDefinition<TDoc>;
|
|
2096
2308
|
interface ResolvedResourceConfig<TDoc = AnyRecord> extends ResourceConfig<TDoc> {
|
|
@@ -2147,7 +2359,7 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
|
|
|
2147
2359
|
_registryMeta?: RegisterOptions;
|
|
2148
2360
|
constructor(config: ResolvedResourceConfig<TDoc>);
|
|
2149
2361
|
/** Get repository from adapter (if available) */
|
|
2150
|
-
get repository():
|
|
2362
|
+
get repository(): RepositoryLike<TDoc> | undefined;
|
|
2151
2363
|
_validateControllerMethods(): void;
|
|
2152
2364
|
toPlugin(): FastifyPluginAsync;
|
|
2153
2365
|
/**
|
|
@@ -2156,7 +2368,7 @@ declare class ResourceDefinition<TDoc = AnyRecord> {
|
|
|
2156
2368
|
getEvents(): Array<{
|
|
2157
2369
|
name: string;
|
|
2158
2370
|
module: string;
|
|
2159
|
-
schema?:
|
|
2371
|
+
schema?: unknown;
|
|
2160
2372
|
description?: string;
|
|
2161
2373
|
}>;
|
|
2162
2374
|
/**
|
|
@@ -2557,7 +2769,7 @@ interface ResourceMetadata {
|
|
|
2557
2769
|
description?: string;
|
|
2558
2770
|
permissions?: PermissionCheck;
|
|
2559
2771
|
raw?: boolean;
|
|
2560
|
-
schema?:
|
|
2772
|
+
schema?: unknown;
|
|
2561
2773
|
}>;
|
|
2562
2774
|
routes: Array<{
|
|
2563
2775
|
method: string;
|
|
@@ -2609,7 +2821,7 @@ interface RegistryEntry extends ResourceMetadata {
|
|
|
2609
2821
|
actions?: Array<{
|
|
2610
2822
|
readonly name: string;
|
|
2611
2823
|
readonly description?: string; /** Raw per-action schema (JSON Schema, Zod v4, or legacy field map) */
|
|
2612
|
-
readonly schema?:
|
|
2824
|
+
readonly schema?: unknown; /** Per-action permission check (if different from resource-level `actionPermissions`) */
|
|
2613
2825
|
readonly permissions?: PermissionCheck; /** MCP tool generation flag — `false` to skip, object for overrides */
|
|
2614
2826
|
readonly mcp?: boolean | {
|
|
2615
2827
|
readonly description?: string;
|
|
@@ -2705,4 +2917,4 @@ interface ValidateOptions {
|
|
|
2705
2917
|
strict?: boolean;
|
|
2706
2918
|
}
|
|
2707
2919
|
//#endregion
|
|
2708
|
-
export {
|
|
2920
|
+
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 };
|