@classytic/arc 2.21.0 → 2.23.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.
- package/dist/{BaseController-BnLYgE9A.mjs → BaseController-BQ8KNvWC.mjs} +3 -2
- package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
- package/dist/{actionPermissions-ze70xXJi.mjs → actionPermissions-lszoJle1.mjs} +1 -1
- package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
- package/dist/audit/index.d.mts +2 -2
- package/dist/auth/index.d.mts +30 -2
- package/dist/auth/index.mjs +6 -3
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +1 -1
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -3
- package/dist/{core-DZ6Xe1jR.mjs → core-DQ6rDTyH.mjs} +126 -68
- package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-BuOv-m4Y.mjs} +10 -6
- package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-CgJv_BU_.mjs} +3 -3
- package/dist/{createApp-Bi61XxqU.mjs → createApp-CjOeWJ9_.mjs} +252 -57
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/encryption/index.mjs +9 -5
- package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-Bq2OeEQL.mjs} +34 -10
- package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
- package/dist/events/index.mjs +5 -2
- package/dist/factory/index.d.mts +46 -9
- package/dist/factory/index.mjs +64 -13
- package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-g_f9omSc.d.mts} +2 -2
- package/dist/fields-D3lhgzhD.d.mts +121 -0
- package/dist/health-ScyjIVo5.mjs +202 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +7 -0
- package/dist/idempotency/index.mjs +7 -17
- package/dist/{index-Dtrj6XZk.d.mts → index-BBsvQt7U.d.mts} +1310 -1226
- package/dist/{types-DpxoNX5v.d.mts → index-Ba10fdKp.d.mts} +196 -66
- package/dist/{index-B5N-NaHs.d.mts → index-CC5ruDGh.d.mts} +3 -3
- package/dist/{index-B5PoNbfu.d.mts → index-wMoWBLkn.d.mts} +68 -2
- package/dist/index.d.mts +6 -5
- package/dist/index.mjs +6 -5
- package/dist/{init-0YhHkXGk.mjs → init-CjO4p_q1.mjs} +123 -4
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +67 -4
- 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/webhooks.d.mts +131 -7
- package/dist/integrations/webhooks.mjs +169 -10
- package/dist/interface-n5hpKg1d.mjs +16 -0
- package/dist/jsonBody-BlzAoGCe.mjs +43 -0
- package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
- package/dist/logger/index.d.mts +26 -1
- package/dist/logger/index.mjs +39 -1
- package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-BtzIL_kM.mjs} +312 -39
- package/dist/middleware/index.d.mts +1 -1
- package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
- package/dist/{openapi-Kyod_di6.mjs → openapi-jSDx-VDv.mjs} +3 -3
- package/dist/permissions/index.d.mts +4 -3
- package/dist/permissions/index.mjs +3 -2
- package/dist/{permissions-BJMuKMl7.mjs → permissions-tbxFHxjZ.mjs} +77 -264
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +24 -7
- package/dist/plugins/index.mjs +14 -282
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.mjs +7 -4
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +1 -1
- 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/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-gw0ciIUD.mjs → presets-BOe4Ugkl.mjs} +1 -1
- package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
- package/dist/realtime-I8OrfnMR.mjs +375 -0
- package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-CWO6mdYe.mjs} +38 -18
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/requestId-5BhTVzBv.mjs +113 -0
- package/dist/schemaConverter-DJvodiHj.mjs +171 -0
- package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-Ca7ySNPx.mjs} +2 -2
- package/dist/scim/index.mjs +6 -7
- package/dist/scope/index.mjs +2 -76
- package/dist/scope-LONQ3mfZ.mjs +37 -0
- package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
- package/dist/{sse-CfUjwjDk.mjs → sse-0u4P4-OI.mjs} +7 -30
- package/dist/sseStream-BQ7Qaynp.mjs +57 -0
- package/dist/testing/index.d.mts +83 -3
- package/dist/testing/index.mjs +70 -3
- package/dist/types/index.d.mts +2 -2
- package/dist/{types-DRj-Ut4n.d.mts → types-5OuqeTDX.d.mts} +37 -2
- package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
- package/dist/usage/index.d.mts +142 -0
- package/dist/usage/index.mjs +0 -0
- package/dist/utils/index.d.mts +101 -19
- package/dist/utils/index.mjs +5 -3
- package/dist/{utils-CO8a3B_1.mjs → utils-CZyeRXTh.mjs} +80 -82
- package/dist/{versioning-DQ4mCvg0.d.mts → versioning-D6PvCwpq.d.mts} +43 -9
- package/llms.txt +39 -0
- package/package.json +513 -447
- package/skills/arc/SKILL.md +14 -8
- package/skills/arc/references/api-reference.md +34 -0
- package/skills/arc/references/auth.md +21 -4
- package/skills/arc/references/mcp.md +1 -1
- package/skills/arc/references/production.md +734 -654
- package/skills/arc-code-review/SKILL.md +1 -1
- package/skills/arc-code-review/references/anti-patterns.md +71 -18
- package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
- package/skills/arc-code-review/references/migration-recipes.md +7 -7
- package/skills/arc-code-review/references/mongokit-migration.md +4 -3
- package/skills/arc-module-publishing/SKILL.md +2 -1
- package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
- package/skills/arc-module-publishing/references/package-manifest.md +4 -2
- package/dist/preloadResources-CHw3_oA_.mjs +0 -45
- package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { a as QueryCacheConfig } from "./QueryCache-Z1B38koh.mjs";
|
|
2
2
|
import { i as RequestScope } from "./types-DH0GupI7.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { i as UserBase, t as PermissionCheck } from "./types-Csxga7d3.mjs";
|
|
4
4
|
import "./elevation-C9s5hFP_.mjs";
|
|
5
|
+
import { n as FieldPermissionMap } from "./fields-D3lhgzhD.mjs";
|
|
5
6
|
import { n as EncryptionDirective } from "./types-Cc91nXuA.mjs";
|
|
6
7
|
import { n as DomainEvent } from "./EventTransport-QmY7A258.mjs";
|
|
7
8
|
import { KeysetPaginationResult, OffsetPaginationResult } from "@classytic/repo-core/pagination";
|
|
@@ -1096,8 +1097,14 @@ interface JWTPayload {
|
|
|
1096
1097
|
[key: string]: unknown;
|
|
1097
1098
|
}
|
|
1098
1099
|
/**
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1100
|
+
* @deprecated LEGACY envelope — arc's actual wire contract has not used
|
|
1101
|
+
* `{ success, data, error, message }` for a long time. Successes return
|
|
1102
|
+
* DIRECT data (see `IControllerResponse` — `{ data, meta?, status?, headers? }`
|
|
1103
|
+
* flattened onto the wire), and errors return the canonical `ErrorContract`
|
|
1104
|
+
* (`{ code, message, status, details? }` from `@classytic/repo-core/errors`)
|
|
1105
|
+
* emitted by the global error handler. Keeping this type visible teaches new
|
|
1106
|
+
* applications and AI agents to generate an obsolete shape. Scheduled for
|
|
1107
|
+
* removal in v3. Do NOT build new response handling against it.
|
|
1101
1108
|
*/
|
|
1102
1109
|
interface ApiResponse<T = unknown> {
|
|
1103
1110
|
success: boolean;
|
|
@@ -1305,1287 +1312,1078 @@ interface AuthPluginOptions {
|
|
|
1305
1312
|
strictTokenType?: boolean;
|
|
1306
1313
|
}
|
|
1307
1314
|
//#endregion
|
|
1308
|
-
//#region src/
|
|
1309
|
-
/**
|
|
1310
|
-
* Pipeline context passed to guards, transforms, and interceptors.
|
|
1311
|
-
* Extends IRequestContext with pipeline-specific metadata.
|
|
1312
|
-
*/
|
|
1313
|
-
interface PipelineContext extends IRequestContext {
|
|
1314
|
-
/** Resource name being accessed */
|
|
1315
|
-
resource: string;
|
|
1316
|
-
/** CRUD operation being performed */
|
|
1317
|
-
operation: "list" | "get" | "create" | "update" | "delete" | string;
|
|
1318
|
-
}
|
|
1319
|
-
/**
|
|
1320
|
-
* Which operations a pipeline step applies to.
|
|
1321
|
-
* If omitted, applies to ALL operations.
|
|
1322
|
-
*/
|
|
1323
|
-
type OperationFilter = Array<"list" | "get" | "create" | "update" | "delete" | string>;
|
|
1324
|
-
/**
|
|
1325
|
-
* Guard — boolean check that short-circuits on failure.
|
|
1326
|
-
* Return true to proceed, throw to deny.
|
|
1327
|
-
*/
|
|
1328
|
-
interface Guard {
|
|
1329
|
-
readonly _type: "guard";
|
|
1330
|
-
readonly name: string;
|
|
1331
|
-
readonly operations?: OperationFilter;
|
|
1332
|
-
handler(ctx: PipelineContext): boolean | Promise<boolean>;
|
|
1333
|
-
}
|
|
1334
|
-
/**
|
|
1335
|
-
* Transform — modifies request data before the handler.
|
|
1336
|
-
* Returns modified context (or mutates in place).
|
|
1337
|
-
*/
|
|
1338
|
-
interface Transform {
|
|
1339
|
-
readonly _type: "transform";
|
|
1340
|
-
readonly name: string;
|
|
1341
|
-
readonly operations?: OperationFilter;
|
|
1342
|
-
handler(ctx: PipelineContext): PipelineContext | undefined | Promise<PipelineContext | undefined>;
|
|
1343
|
-
}
|
|
1344
|
-
/**
|
|
1345
|
-
* Next function passed to interceptors — calls the handler (or next interceptor).
|
|
1346
|
-
*/
|
|
1347
|
-
type NextFunction = () => Promise<IControllerResponse<unknown>>;
|
|
1315
|
+
//#region src/types/handlers.d.ts
|
|
1348
1316
|
/**
|
|
1349
|
-
*
|
|
1317
|
+
* Minimal server accessor — exposes safe, read-only server decorators.
|
|
1318
|
+
* Allows controller handlers to publish events, log, and audit
|
|
1319
|
+
* without switching to `raw: true`.
|
|
1350
1320
|
*/
|
|
1351
|
-
interface
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1321
|
+
interface ServerAccessor {
|
|
1322
|
+
/** Event bus — publish domain events from any handler */
|
|
1323
|
+
events?: {
|
|
1324
|
+
publish: <T>(type: string, payload: T, meta?: Partial<Record<string, unknown>>) => Promise<void>;
|
|
1325
|
+
};
|
|
1326
|
+
/** Audit logger — log custom audit entries */
|
|
1327
|
+
audit?: {
|
|
1328
|
+
create: (resource: string, documentId: string, data: Record<string, unknown>, context?: Record<string, unknown>) => Promise<void>;
|
|
1329
|
+
update: (resource: string, documentId: string, before: Record<string, unknown>, after: Record<string, unknown>, context?: Record<string, unknown>) => Promise<void>;
|
|
1330
|
+
delete: (resource: string, documentId: string, data: Record<string, unknown>, context?: Record<string, unknown>) => Promise<void>;
|
|
1331
|
+
custom: (resource: string, documentId: string, action: string, data?: Record<string, unknown>, context?: Record<string, unknown>) => Promise<void>;
|
|
1332
|
+
};
|
|
1333
|
+
/** Logger — structured logging */
|
|
1334
|
+
log?: {
|
|
1335
|
+
info: (...args: unknown[]) => void;
|
|
1336
|
+
warn: (...args: unknown[]) => void;
|
|
1337
|
+
error: (...args: unknown[]) => void;
|
|
1338
|
+
debug: (...args: unknown[]) => void;
|
|
1339
|
+
};
|
|
1340
|
+
/** QueryCache — stale-while-revalidate data cache */
|
|
1341
|
+
queryCache?: {
|
|
1342
|
+
get: <T>(key: string) => Promise<{
|
|
1343
|
+
data: T;
|
|
1344
|
+
status: "fresh" | "stale" | "miss";
|
|
1345
|
+
}>;
|
|
1346
|
+
set: <T>(key: string, data: T, config: {
|
|
1347
|
+
staleTime?: number;
|
|
1348
|
+
gcTime?: number;
|
|
1349
|
+
tags?: string[];
|
|
1350
|
+
}) => Promise<void>;
|
|
1351
|
+
getResourceVersion: (resource: string) => Promise<number>;
|
|
1352
|
+
bumpResourceVersion: (resource: string) => Promise<void>;
|
|
1353
|
+
};
|
|
1356
1354
|
}
|
|
1357
|
-
type PipelineStep = Guard | Transform | Interceptor;
|
|
1358
|
-
/**
|
|
1359
|
-
* Pipeline configuration — can be a flat array or per-operation map.
|
|
1360
|
-
*/
|
|
1361
|
-
type PipelineConfig = PipelineStep[] | {
|
|
1362
|
-
list?: PipelineStep[];
|
|
1363
|
-
get?: PipelineStep[];
|
|
1364
|
-
create?: PipelineStep[];
|
|
1365
|
-
update?: PipelineStep[];
|
|
1366
|
-
delete?: PipelineStep[];
|
|
1367
|
-
[operation: string]: PipelineStep[] | undefined;
|
|
1368
|
-
};
|
|
1369
|
-
//#endregion
|
|
1370
|
-
//#region src/core/aggregation/types.d.ts
|
|
1371
1355
|
/**
|
|
1372
|
-
*
|
|
1373
|
-
* `'avg:rating'` / `'min:created'` / `'max:updated'` /
|
|
1374
|
-
* `'countDistinct:userId'` / `'percentile:latency:0.95'`.
|
|
1356
|
+
* Request context passed to controller handlers.
|
|
1375
1357
|
*
|
|
1376
|
-
*
|
|
1377
|
-
*
|
|
1358
|
+
* **Generic parameters** (all default to safe permissive types so existing code keeps working):
|
|
1359
|
+
* - `TBody` — request body shape (default: `unknown`)
|
|
1360
|
+
* - `TParams` — route param shape (default: `Record<string, string>`)
|
|
1361
|
+
* - `TQuery` — query string shape (default: `Record<string, unknown>`)
|
|
1362
|
+
* - `TUser` — authenticated user shape (default: `UserBase`)
|
|
1363
|
+
* - `TMetadata` — internal metadata shape (default: `Record<string, unknown>`;
|
|
1364
|
+
* override with `ArcInternalMetadata` or your own augmentation when you
|
|
1365
|
+
* need typed access to `_scope`, `_policyFilters`, custom hook context, etc.)
|
|
1378
1366
|
*
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
1381
|
-
*
|
|
1382
|
-
*
|
|
1367
|
+
* @example
|
|
1368
|
+
* ```typescript
|
|
1369
|
+
* // Untyped (default) — req.body is `unknown`, must be narrowed
|
|
1370
|
+
* async create(req: IRequestContext) {
|
|
1371
|
+
* const data = req.body as Partial<Product>;
|
|
1372
|
+
* return { success: true, data: await productRepo.create(data) };
|
|
1373
|
+
* }
|
|
1374
|
+
*
|
|
1375
|
+
* // Typed body — req.body is `CreateProductInput`, narrowing not needed
|
|
1376
|
+
* async create(req: IRequestContext<CreateProductInput>) {
|
|
1377
|
+
* return { success: true, data: await productRepo.create(req.body) };
|
|
1378
|
+
* }
|
|
1379
|
+
*
|
|
1380
|
+
* // Fully typed — body, route params, query, and metadata
|
|
1381
|
+
* async update(
|
|
1382
|
+
* req: IRequestContext<
|
|
1383
|
+
* Partial<Product>,
|
|
1384
|
+
* { id: string },
|
|
1385
|
+
* { fields?: string },
|
|
1386
|
+
* ArcInternalMetadata
|
|
1387
|
+
* >,
|
|
1388
|
+
* ) {
|
|
1389
|
+
* const fields = req.query.fields?.split(',');
|
|
1390
|
+
* const orgId = req.metadata?._scope ? getOrgId(req.metadata._scope) : undefined;
|
|
1391
|
+
* return { success: true, data: await productRepo.update(req.params.id, req.body) };
|
|
1392
|
+
* }
|
|
1393
|
+
* ```
|
|
1383
1394
|
*/
|
|
1384
|
-
type AggMeasureShorthand = "count" | `count:${string}` | `countDistinct:${string}` | `sum:${string}` | `avg:${string}` | `min:${string}` | `max:${string}` | `percentile:${string}:${number}`;
|
|
1385
|
-
/** Either canonical IR or shorthand string — both compile to `AggMeasure`. */
|
|
1386
|
-
type AggMeasureInput = AggMeasure | AggMeasureShorthand;
|
|
1387
1395
|
/**
|
|
1388
|
-
*
|
|
1389
|
-
* `CacheOptions` (TanStack-shaped) which the unified
|
|
1390
|
-
* `@classytic/repo-core/cache` plugin reads from `req.cache`.
|
|
1396
|
+
* First-class projection of `request._scope` for controller handlers.
|
|
1391
1397
|
*
|
|
1392
|
-
*
|
|
1393
|
-
*
|
|
1394
|
-
*
|
|
1395
|
-
*
|
|
1398
|
+
* **v2.10.6:** previously, pulling tenant/user/role info from inside a
|
|
1399
|
+
* controller override meant digging through `req.metadata._scope` and
|
|
1400
|
+
* calling `getOrgId(scope)` / `getUserId(user)` manually. This projection
|
|
1401
|
+
* lifts the two fields most hosts need directly onto `req` so cross-kit
|
|
1402
|
+
* controller code reads:
|
|
1403
|
+
*
|
|
1404
|
+
* ```ts
|
|
1405
|
+
* async create(req: IRequestContext) {
|
|
1406
|
+
* const flowCtx = { organizationId: req.scope?.organizationId, actorId: req.scope?.userId };
|
|
1407
|
+
* }
|
|
1408
|
+
* ```
|
|
1409
|
+
*
|
|
1410
|
+
* Full scope shape (discriminated union of `member` / `service` / `elevated` / `public`)
|
|
1411
|
+
* still lives on `req.metadata._scope` for code that needs to branch on
|
|
1412
|
+
* `scope.kind` — this projection just surfaces the two keys every
|
|
1413
|
+
* tenant-scoped resource reaches for.
|
|
1396
1414
|
*/
|
|
1397
|
-
interface
|
|
1398
|
-
/**
|
|
1399
|
-
|
|
1400
|
-
/**
|
|
1401
|
-
|
|
1415
|
+
interface RequestScopeProjection {
|
|
1416
|
+
/** Tenant the caller is scoped to (org member, service key bound to an org, or elevated admin's target org). */
|
|
1417
|
+
organizationId?: string;
|
|
1418
|
+
/** Caller's user id when authenticated — undefined for public / service-only scopes. */
|
|
1419
|
+
userId?: string;
|
|
1420
|
+
/** Org-level roles (e.g. `['admin', 'warehouse-manager']`) — separate from global `user.roles`. */
|
|
1421
|
+
orgRoles?: string[];
|
|
1422
|
+
}
|
|
1423
|
+
interface IRequestContext<TBody = unknown, TParams extends Record<string, string> = Record<string, string>, TQuery extends Record<string, unknown> = Record<string, unknown>, TUser extends UserBase = UserBase, TMetadata extends Record<string, unknown> = Record<string, unknown>> {
|
|
1424
|
+
/** Route parameters (e.g., { id: '123' }) */
|
|
1425
|
+
params: TParams;
|
|
1426
|
+
/** Query string parameters */
|
|
1427
|
+
query: TQuery;
|
|
1428
|
+
/** Request body */
|
|
1429
|
+
body: TBody;
|
|
1430
|
+
/** Authenticated user or null */
|
|
1431
|
+
user: TUser | null;
|
|
1402
1432
|
/**
|
|
1403
|
-
*
|
|
1404
|
-
*
|
|
1405
|
-
*
|
|
1433
|
+
* Request headers.
|
|
1434
|
+
*
|
|
1435
|
+
* ⚠ Declared narrow for ergonomics, but on the HTTP path this is
|
|
1436
|
+
* Fastify's headers object passed through — repeated headers arrive as
|
|
1437
|
+
* `string[]` at RUNTIME behind this type. Read via `getHeader(ctx.headers,
|
|
1438
|
+
* name)` from `@classytic/arc/utils` for array-safe, duplicate-deterministic
|
|
1439
|
+
* access. The declared type widens to `ArcHeaders` in v3.
|
|
1406
1440
|
*/
|
|
1407
|
-
|
|
1441
|
+
headers: Record<string, string | undefined>;
|
|
1442
|
+
/** Organization ID (for multi-tenant apps) */
|
|
1443
|
+
organizationId?: string;
|
|
1444
|
+
/** Team ID (for team-scoped resources) */
|
|
1445
|
+
teamId?: string;
|
|
1408
1446
|
/**
|
|
1409
|
-
*
|
|
1410
|
-
*
|
|
1411
|
-
*
|
|
1412
|
-
*
|
|
1413
|
-
* pricing across multiple resources).
|
|
1447
|
+
* First-class tenant/user scope projection — lifted from `metadata._scope`
|
|
1448
|
+
* so controller overrides don't have to dig through Arc internals.
|
|
1449
|
+
* See {@link RequestScopeProjection}. `undefined` for routes that run
|
|
1450
|
+
* without auth / scope resolution.
|
|
1414
1451
|
*/
|
|
1415
|
-
|
|
1452
|
+
scope?: RequestScopeProjection;
|
|
1453
|
+
/**
|
|
1454
|
+
* Organization/auth context from middleware.
|
|
1455
|
+
* Contains orgRoles, orgScope, organizationId, and any custom fields
|
|
1456
|
+
* set by the auth adapter or org-scope plugin.
|
|
1457
|
+
*
|
|
1458
|
+
* @example
|
|
1459
|
+
* ```typescript
|
|
1460
|
+
* async create(req: IRequestContext) {
|
|
1461
|
+
* const roles = req.context?.orgRoles ?? [];
|
|
1462
|
+
* if (roles.includes('manager')) { ... }
|
|
1463
|
+
* }
|
|
1464
|
+
* ```
|
|
1465
|
+
*/
|
|
1466
|
+
context?: RequestContext;
|
|
1467
|
+
/**
|
|
1468
|
+
* Internal metadata (includes context + Arc internals like `_policyFilters`,
|
|
1469
|
+
* `_scope`, `log`). Type as `ArcInternalMetadata` for typed access to Arc's
|
|
1470
|
+
* built-in fields, or supply your own interface to layer custom fields.
|
|
1471
|
+
*/
|
|
1472
|
+
metadata?: TMetadata;
|
|
1473
|
+
/**
|
|
1474
|
+
* Fastify server accessor — publish events, log, and audit
|
|
1475
|
+
* from any handler without switching to `raw: true`.
|
|
1476
|
+
*
|
|
1477
|
+
* @example
|
|
1478
|
+
* ```typescript
|
|
1479
|
+
* async reschedule(req: IRequestContext) {
|
|
1480
|
+
* const result = await repo.reschedule(req.params.id, req.body);
|
|
1481
|
+
* await req.server?.events?.publish('interview.rescheduled', { data: result });
|
|
1482
|
+
* return { data: result };
|
|
1483
|
+
* }
|
|
1484
|
+
* ```
|
|
1485
|
+
*/
|
|
1486
|
+
server?: ServerAccessor;
|
|
1416
1487
|
}
|
|
1417
1488
|
/**
|
|
1418
|
-
*
|
|
1419
|
-
*
|
|
1489
|
+
* Controller response shape — the success-path return from any handler.
|
|
1490
|
+
*
|
|
1491
|
+
* Errors throw `ArcError` (or any `HttpError`-shaped class); the global
|
|
1492
|
+
* error handler catches them and emits an `ErrorContract`. There is no
|
|
1493
|
+
* `success` discriminator on the response — HTTP status is the wire
|
|
1494
|
+
* discriminator (2xx = data, 4xx/5xx = ErrorContract).
|
|
1420
1495
|
*/
|
|
1421
|
-
interface
|
|
1422
|
-
/**
|
|
1423
|
-
|
|
1424
|
-
/**
|
|
1425
|
-
|
|
1496
|
+
interface IControllerResponse<T = unknown> {
|
|
1497
|
+
/** Response payload — emitted directly to the wire (no envelope wrap). */
|
|
1498
|
+
data: T;
|
|
1499
|
+
/** HTTP status code. Defaults to 200. */
|
|
1500
|
+
status?: number;
|
|
1501
|
+
/** Custom response headers (e.g. X-Total-Count, Link, ETag). */
|
|
1502
|
+
headers?: Record<string, string>;
|
|
1503
|
+
/** Top-level metadata merged into list-shaped responses (e.g. `{ took }`). */
|
|
1504
|
+
meta?: Record<string, unknown>;
|
|
1426
1505
|
}
|
|
1427
1506
|
/**
|
|
1428
|
-
*
|
|
1429
|
-
* this field, and the range MUST NOT exceed `maxRangeDays`.
|
|
1507
|
+
* Controller handler — Arc's standard pattern.
|
|
1430
1508
|
*
|
|
1431
|
-
*
|
|
1432
|
-
*
|
|
1509
|
+
* Receives a request context object, returns IControllerResponse.
|
|
1510
|
+
* Use with `raw: false` in routes.
|
|
1511
|
+
*
|
|
1512
|
+
* **Generic parameters:**
|
|
1513
|
+
* - `TResponse` — shape of `IControllerResponse.data` (default: `unknown`)
|
|
1514
|
+
* - `TBody` — shape of `req.body` (default: `unknown`)
|
|
1515
|
+
* - `TParams` — shape of `req.params` (default: `Record<string, string>`)
|
|
1516
|
+
* - `TQuery` — shape of `req.query` (default: `Record<string, unknown>`)
|
|
1517
|
+
*
|
|
1518
|
+
* Backward-compatible: `ControllerHandler<Product>` still works (only the
|
|
1519
|
+
* response data is typed); add more generics as needed when you want
|
|
1520
|
+
* type-safe access to the request body, params, or query string.
|
|
1521
|
+
*
|
|
1522
|
+
* @example
|
|
1523
|
+
* ```typescript
|
|
1524
|
+
* // Untyped req — body is unknown, must be narrowed
|
|
1525
|
+
* const createProduct: ControllerHandler<Product> = async (req) => {
|
|
1526
|
+
* const product = await productRepo.create(req.body as Partial<Product>);
|
|
1527
|
+
* return { data: product, status: 201 };
|
|
1528
|
+
* };
|
|
1529
|
+
*
|
|
1530
|
+
* // Fully typed — body, params, query, and response all inferred
|
|
1531
|
+
* const updateProduct: ControllerHandler<
|
|
1532
|
+
* Product,
|
|
1533
|
+
* Partial<Product>,
|
|
1534
|
+
* { id: string },
|
|
1535
|
+
* { upsert?: string }
|
|
1536
|
+
* > = async (req) => {
|
|
1537
|
+
* const upsert = req.query.upsert === "true";
|
|
1538
|
+
* const product = await productRepo.update(req.params.id, req.body, { upsert });
|
|
1539
|
+
* return { data: product };
|
|
1540
|
+
* };
|
|
1541
|
+
*
|
|
1542
|
+
* routes: [{
|
|
1543
|
+
* method: 'POST',
|
|
1544
|
+
* path: '/products',
|
|
1545
|
+
* handler: createProduct,
|
|
1546
|
+
* permissions: requireAuth(),
|
|
1547
|
+
* raw: false, // Arc wraps this into Fastify handler
|
|
1548
|
+
* }]
|
|
1549
|
+
* ```
|
|
1433
1550
|
*/
|
|
1434
|
-
|
|
1435
|
-
/** Field whose range the caller must narrow (e.g. `'createdAt'`). */
|
|
1436
|
-
field: string;
|
|
1437
|
-
/**
|
|
1438
|
-
* Cap on the queryable range. A request asking for >N days is
|
|
1439
|
-
* rejected 400. Omit for "any range, but bounded" (lower + upper
|
|
1440
|
-
* required, no cap).
|
|
1441
|
-
*/
|
|
1442
|
-
maxRangeDays?: number;
|
|
1443
|
-
}
|
|
1551
|
+
type ControllerHandler<TResponse = unknown, TBody = unknown, TParams extends Record<string, string> = Record<string, string>, TQuery extends Record<string, unknown> = Record<string, unknown>> = (req: IRequestContext<TBody, TParams, TQuery>) => Promise<IControllerResponse<TResponse>>;
|
|
1444
1552
|
/**
|
|
1445
|
-
*
|
|
1446
|
-
* an index whose leading keys match — flags the misconfig before
|
|
1447
|
-
* traffic hits the DB.
|
|
1553
|
+
* Fastify native handler
|
|
1448
1554
|
*
|
|
1449
|
-
*
|
|
1450
|
-
*
|
|
1451
|
-
*
|
|
1452
|
-
*
|
|
1555
|
+
* Standard Fastify request/reply pattern.
|
|
1556
|
+
* Use with `raw: true` in routes.
|
|
1557
|
+
*
|
|
1558
|
+
* @example
|
|
1559
|
+
* ```typescript
|
|
1560
|
+
* const downloadFile: FastifyHandler = async (request, reply) => {
|
|
1561
|
+
* const file = await getFile(request.params.id);
|
|
1562
|
+
* reply.header('Content-Type', file.mimeType);
|
|
1563
|
+
* return reply.send(file.buffer);
|
|
1564
|
+
* };
|
|
1565
|
+
*
|
|
1566
|
+
* routes: [{
|
|
1567
|
+
* method: 'GET',
|
|
1568
|
+
* path: '/files/:id/download',
|
|
1569
|
+
* handler: downloadFile,
|
|
1570
|
+
* permissions: requireAuth(),
|
|
1571
|
+
* raw: true, // Use as-is, no wrapping
|
|
1572
|
+
* }]
|
|
1573
|
+
* ```
|
|
1453
1574
|
*/
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1575
|
+
type FastifyHandler<RouteGeneric extends Record<string, unknown> = Record<string, unknown>> = (request: FastifyRequest<RouteGeneric>, reply: FastifyReply) => Promise<unknown> | unknown;
|
|
1576
|
+
/**
|
|
1577
|
+
* Union type for route handlers
|
|
1578
|
+
*/
|
|
1579
|
+
type RouteHandler = ControllerHandler | FastifyHandler;
|
|
1580
|
+
/**
|
|
1581
|
+
* Controller interface for CRUD operations (strict).
|
|
1582
|
+
*
|
|
1583
|
+
* `list`'s return type aligns with repo-core's `MinimalRepo.getAll()`
|
|
1584
|
+
* contract — the kit MAY return an offset envelope, a keyset envelope,
|
|
1585
|
+
* or a raw array. Arc's `BaseController` forwards the kit's response
|
|
1586
|
+
* verbatim; consumers narrow on shape
|
|
1587
|
+
* (`Array.isArray(data)` → bare array, presence of `total` → offset,
|
|
1588
|
+
* presence of `nextCursor` → keyset).
|
|
1589
|
+
*/
|
|
1590
|
+
interface IController<TDoc = unknown> {
|
|
1591
|
+
list(req: IRequestContext): Promise<IControllerResponse<unknown>>;
|
|
1592
|
+
get(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
1593
|
+
create(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
1594
|
+
update(req: IRequestContext): Promise<IControllerResponse<TDoc>>;
|
|
1595
|
+
delete(req: IRequestContext): Promise<IControllerResponse<{
|
|
1596
|
+
message: string;
|
|
1597
|
+
}>>;
|
|
1457
1598
|
}
|
|
1458
1599
|
/**
|
|
1459
|
-
*
|
|
1600
|
+
* Flexible controller interface — accepts controllers with any handler style,
|
|
1601
|
+
* including class instances with extra methods / private fields.
|
|
1460
1602
|
*
|
|
1461
|
-
*
|
|
1462
|
-
*
|
|
1463
|
-
*
|
|
1603
|
+
* **v2.10.6:** the previous `[key: string]: unknown` index signature made
|
|
1604
|
+
* real class instances fail structural assignment (`new ScrapController()`
|
|
1605
|
+
* needed a `as unknown as ControllerLike` cast, because class instances
|
|
1606
|
+
* don't have an index signature). Dropped — arc only invokes the CRUD
|
|
1607
|
+
* methods at runtime, so the rest of the shape is the caller's concern.
|
|
1608
|
+
*
|
|
1609
|
+
* The five CRUD slots stay optional so partial controllers (e.g. read-only)
|
|
1610
|
+
* assign too. Additional domain methods on the controller are allowed by
|
|
1611
|
+
* construction (they're just not part of this contract).
|
|
1464
1612
|
*/
|
|
1465
|
-
interface
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
userId?: string;
|
|
1472
|
-
/** Fastify request id for tracing. */
|
|
1473
|
-
requestId?: string;
|
|
1474
|
-
/** Raw URL query params (post-validation). */
|
|
1475
|
-
query: Record<string, unknown>;
|
|
1613
|
+
interface ControllerLike {
|
|
1614
|
+
list?: unknown;
|
|
1615
|
+
get?: unknown;
|
|
1616
|
+
create?: unknown;
|
|
1617
|
+
update?: unknown;
|
|
1618
|
+
delete?: unknown;
|
|
1476
1619
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1620
|
+
//#endregion
|
|
1621
|
+
//#region src/types/resource/rate-limit.d.ts
|
|
1622
|
+
/**
|
|
1623
|
+
* Resource-level rate-limit shape — shared by `ResourceConfig.rateLimit`
|
|
1624
|
+
* and per-route `RouteDefinition.rateLimit` overrides.
|
|
1625
|
+
*/
|
|
1626
|
+
interface RateLimitConfig {
|
|
1627
|
+
/** Maximum number of requests allowed within the time window */
|
|
1628
|
+
max: number;
|
|
1629
|
+
/** Time window for rate limiting (e.g., '1 minute', '15 seconds') */
|
|
1630
|
+
timeWindow: string;
|
|
1631
|
+
}
|
|
1632
|
+
//#endregion
|
|
1633
|
+
//#region src/types/resource/routes.d.ts
|
|
1634
|
+
/** HTTP methods for custom routes. */
|
|
1635
|
+
type RouteMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
1636
|
+
/** MCP tool configuration for a route or action. */
|
|
1637
|
+
interface RouteMcpConfig {
|
|
1638
|
+
/** Override auto-generated tool description */
|
|
1639
|
+
readonly description?: string;
|
|
1640
|
+
/** MCP tool annotations */
|
|
1641
|
+
readonly annotations?: {
|
|
1642
|
+
readonly readOnlyHint?: boolean;
|
|
1643
|
+
readonly destructiveHint?: boolean;
|
|
1644
|
+
readonly idempotentHint?: boolean;
|
|
1645
|
+
readonly openWorldHint?: boolean;
|
|
1646
|
+
};
|
|
1480
1647
|
}
|
|
1481
1648
|
/**
|
|
1482
|
-
*
|
|
1483
|
-
*
|
|
1649
|
+
* Route definition — single custom-route shape (user-facing + internal).
|
|
1650
|
+
*
|
|
1651
|
+
* - `handler: 'string'` → controller method → full Arc pipeline + MCP tool
|
|
1652
|
+
* - `handler: function` → inline handler → full Arc pipeline + MCP tool
|
|
1653
|
+
* - `raw: true` → raw Fastify handler → no pipeline, no MCP by default
|
|
1484
1654
|
*/
|
|
1485
|
-
interface
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
* filters. Use for host-defined invariants (e.g. `archived: false`).
|
|
1490
|
-
*/
|
|
1491
|
-
filter?: AnyRecord;
|
|
1655
|
+
interface RouteDefinition {
|
|
1656
|
+
readonly method: RouteMethod;
|
|
1657
|
+
/** Path relative to resource prefix */
|
|
1658
|
+
readonly path: string;
|
|
1492
1659
|
/**
|
|
1493
|
-
*
|
|
1494
|
-
*
|
|
1495
|
-
*
|
|
1660
|
+
* Route handler.
|
|
1661
|
+
* - String: controller method name (Arc pipeline) — runtime lookup, no TS coverage on typos.
|
|
1662
|
+
* - Function without `raw: true`: receives IRequestContext, returns IControllerResponse (Arc pipeline)
|
|
1663
|
+
* - Function with `raw: true`: raw Fastify handler `(request, reply)`
|
|
1496
1664
|
*
|
|
1497
|
-
*
|
|
1498
|
-
*
|
|
1499
|
-
*
|
|
1500
|
-
*/
|
|
1501
|
-
lookups?: readonly LookupSpec[];
|
|
1502
|
-
/**
|
|
1503
|
-
* Group key(s). Dotted paths into joined aliases supported when
|
|
1504
|
-
* `lookups` is set: `'category.parent'` groups by the joined
|
|
1505
|
-
* `category` row's `parent` field.
|
|
1665
|
+
* Prefer `controllerMethod` (2.16) over the string form when you want the
|
|
1666
|
+
* compiler to catch typos. `handler` stays optional when `controllerMethod`
|
|
1667
|
+
* is set — exactly one of the two must be declared.
|
|
1506
1668
|
*/
|
|
1507
|
-
|
|
1669
|
+
readonly handler?: string | ControllerHandler | RouteHandlerMethod | ((request: FastifyRequest<Record<string, unknown>>, reply: FastifyReply) => unknown);
|
|
1508
1670
|
/**
|
|
1509
|
-
*
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
1513
|
-
*
|
|
1671
|
+
* Typed function-reference handler (2.16). Receives the live controller
|
|
1672
|
+
* instance and returns the method to invoke — TypeScript catches typos
|
|
1673
|
+
* and surfaces autocomplete on the controller's method names. Prefer
|
|
1674
|
+
* this over the string `handler` form for any host that uses a typed
|
|
1675
|
+
* controller subclass.
|
|
1514
1676
|
*
|
|
1515
|
-
*
|
|
1516
|
-
*
|
|
1517
|
-
*
|
|
1677
|
+
* Resolved ONCE at route-registration time (the same path string
|
|
1678
|
+
* handlers go through), so there's no per-request lookup overhead.
|
|
1679
|
+
* The returned method is bound to the controller before dispatch.
|
|
1518
1680
|
*
|
|
1519
|
-
*
|
|
1520
|
-
*
|
|
1681
|
+
* Mutually exclusive with `handler` — passing both throws at boot
|
|
1682
|
+
* with a clear "pick one" message. The runtime contract on what the
|
|
1683
|
+
* returned function accepts mirrors the `handler` rules: a plain
|
|
1684
|
+
* `ControllerHandler` for pipeline routes, a raw Fastify handler when
|
|
1685
|
+
* the route is `raw: true`.
|
|
1521
1686
|
*
|
|
1522
|
-
*
|
|
1523
|
-
*
|
|
1524
|
-
*
|
|
1525
|
-
* dateBuckets: { day: { field: 'createdAt', interval: 'day' } },
|
|
1526
|
-
* measures: { revenue: 'sum:totalPrice' },
|
|
1527
|
-
* sort: { day: 1 },
|
|
1528
|
-
* permissions: requireRoles(['admin']),
|
|
1529
|
-
* }),
|
|
1530
|
-
* ```
|
|
1687
|
+
* Typed loosely (`(controller: unknown) => …`) at the type-system
|
|
1688
|
+
* boundary because `RouteDefinition` is not generic over the
|
|
1689
|
+
* controller shape — host code annotates the parameter to opt in:
|
|
1531
1690
|
*
|
|
1532
|
-
* @example
|
|
1691
|
+
* @example
|
|
1533
1692
|
* ```ts
|
|
1534
|
-
*
|
|
1535
|
-
*
|
|
1536
|
-
*
|
|
1537
|
-
*
|
|
1538
|
-
*
|
|
1539
|
-
*
|
|
1540
|
-
*
|
|
1693
|
+
* class PostController extends BaseController<Post> {
|
|
1694
|
+
* async getStats(ctx: IRequestContext) { … }
|
|
1695
|
+
* }
|
|
1696
|
+
*
|
|
1697
|
+
* defineResource({
|
|
1698
|
+
* controller: new PostController(repo),
|
|
1699
|
+
* routes: [{
|
|
1700
|
+
* method: 'GET',
|
|
1701
|
+
* path: '/stats',
|
|
1702
|
+
* controllerMethod: (c: PostController) => c.getStats,
|
|
1703
|
+
* // ^^^^^^^^^^^^^^ TS catches typos here
|
|
1704
|
+
* permissions: requireAuth(),
|
|
1705
|
+
* }],
|
|
1706
|
+
* });
|
|
1541
1707
|
* ```
|
|
1542
1708
|
*/
|
|
1543
|
-
|
|
1544
|
-
/**
|
|
1545
|
-
|
|
1546
|
-
/**
|
|
1547
|
-
* Post-aggregate filter referencing measure aliases.
|
|
1548
|
-
* Example: `{ revenue: { gt: 1000 } }` → `HAVING revenue > 1000`.
|
|
1549
|
-
*/
|
|
1550
|
-
having?: AnyRecord;
|
|
1551
|
-
/** Order grouped rows by groupBy field, measure alias, or joined-alias path. */
|
|
1552
|
-
sort?: Record<string, 1 | -1>;
|
|
1709
|
+
readonly controllerMethod?: (controller: unknown) => ControllerHandler | RouteHandlerMethod;
|
|
1710
|
+
/** Permission check — REQUIRED */
|
|
1711
|
+
readonly permissions: PermissionCheck;
|
|
1553
1712
|
/**
|
|
1554
|
-
*
|
|
1555
|
-
*
|
|
1556
|
-
* aggregation has a fixed-size shape ("top 10 categories"). Mutually
|
|
1557
|
-
* exclusive with `defaultLimit` / `maxLimit` — pick the static form
|
|
1558
|
-
* OR the URL-driven form, not both.
|
|
1713
|
+
* Raw mode — bypasses Arc pipeline. Handler receives raw Fastify
|
|
1714
|
+
* request/reply. Default: false.
|
|
1559
1715
|
*/
|
|
1560
|
-
|
|
1716
|
+
readonly raw?: boolean;
|
|
1717
|
+
/** Logical operation name (pipeline keys, MCP tool naming). */
|
|
1718
|
+
readonly operation?: string;
|
|
1719
|
+
/** OpenAPI summary */
|
|
1720
|
+
readonly summary?: string;
|
|
1721
|
+
/** OpenAPI description */
|
|
1722
|
+
readonly description?: string;
|
|
1723
|
+
/** OpenAPI tags */
|
|
1724
|
+
readonly tags?: string[];
|
|
1725
|
+
/** Route-level middleware */
|
|
1726
|
+
readonly preHandler?: RouteHandlerMethod[] | ((fastify: FastifyInstance) => RouteHandlerMethod[]);
|
|
1727
|
+
/** Pre-auth handlers (run before authentication) */
|
|
1728
|
+
readonly preAuth?: RouteHandlerMethod[];
|
|
1561
1729
|
/**
|
|
1562
|
-
*
|
|
1563
|
-
* When set, the auto-route reads `query.limit`, parses it as a
|
|
1564
|
-
* positive integer, caps it at `maxLimit` (or the framework default
|
|
1565
|
-
* if `maxLimit` is unset), and writes the result into `AggRequest.limit`.
|
|
1566
|
-
* When the caller omits `?limit`, this default applies.
|
|
1730
|
+
* Opt this custom route into the multiTenant preset's tenant scoping.
|
|
1567
1731
|
*
|
|
1568
|
-
*
|
|
1569
|
-
*
|
|
1570
|
-
*
|
|
1571
|
-
* `
|
|
1572
|
-
*
|
|
1732
|
+
* When `true` AND the resource declares `multiTenantPreset` (or
|
|
1733
|
+
* `flexibleMultiTenantPreset`), arc prepends the preset's tenant
|
|
1734
|
+
* filter + injection middleware to this route's chain — the same
|
|
1735
|
+
* pair `update` gets. The handler can then read the resolved tenant
|
|
1736
|
+
* via `getOrgId(req.scope)` / `req._tenantFields` (or, for write
|
|
1737
|
+
* routes, off `req.body` after injection) without re-implementing
|
|
1738
|
+
* the scope read + header fallback + 400 boilerplate.
|
|
1573
1739
|
*
|
|
1574
|
-
*
|
|
1575
|
-
*
|
|
1576
|
-
*
|
|
1740
|
+
* Throws at boot if set when no multiTenant preset is wired — the
|
|
1741
|
+
* misconfiguration would otherwise be silently insecure (read routes
|
|
1742
|
+
* returning every tenant's rows).
|
|
1743
|
+
*
|
|
1744
|
+
* @default false
|
|
1577
1745
|
*/
|
|
1578
|
-
|
|
1746
|
+
readonly tenantScope?: boolean;
|
|
1579
1747
|
/**
|
|
1580
|
-
*
|
|
1581
|
-
*
|
|
1582
|
-
* `
|
|
1583
|
-
*
|
|
1748
|
+
* SSE streaming mode. Two handler shapes work:
|
|
1749
|
+
*
|
|
1750
|
+
* - Write to `reply.raw` directly (NDJSON, custom SSE — historical contract).
|
|
1751
|
+
* - Return a Web `ReadableStream` (Vercel AI SDK's `result.toUIMessageStream()`,
|
|
1752
|
+
* `fetch().body`, etc.). 2.17.1+ auto-pipes these through
|
|
1753
|
+
* `pipeUIMessageStreamToReply()` so each chunk is JSON-encoded into
|
|
1754
|
+
* an SSE `data:` frame and client-disconnect cancels the source.
|
|
1755
|
+
* Pre-2.17.1 this crashed with `chunk must be a string or Buffer`.
|
|
1584
1756
|
*/
|
|
1585
|
-
|
|
1757
|
+
readonly streamResponse?: boolean;
|
|
1586
1758
|
/**
|
|
1587
|
-
*
|
|
1588
|
-
*
|
|
1589
|
-
*
|
|
1590
|
-
*
|
|
1591
|
-
*
|
|
1592
|
-
*
|
|
1593
|
-
* dateBucket aliases, or measure aliases. The top-level `sort` orders
|
|
1594
|
-
* the final row set across partitions.
|
|
1759
|
+
* INTERNAL — marker stamped by built-in presets (softDelete, tree, …)
|
|
1760
|
+
* onto routes they emit. Read by the MCP collision detector
|
|
1761
|
+
* (`createMcpServer.resolveToolCollisions`) to attribute the route
|
|
1762
|
+
* source ("preset:softDelete vs user `actions.restore`") and to
|
|
1763
|
+
* auto-namespace the preset side when a user-authored tool legitimately
|
|
1764
|
+
* shadows it. Not part of the public surface — hosts must not set this.
|
|
1595
1765
|
*
|
|
1596
|
-
*
|
|
1597
|
-
* runs in-engine — scales). Sqlitekit post-processes in JS (fine for
|
|
1598
|
-
* typical dashboards; prefer mongokit for >100k groups). See
|
|
1599
|
-
* `AggTopN` for full semantics.
|
|
1766
|
+
* @internal
|
|
1600
1767
|
*/
|
|
1601
|
-
|
|
1768
|
+
readonly _presetSource?: string;
|
|
1602
1769
|
/**
|
|
1603
|
-
*
|
|
1604
|
-
*
|
|
1605
|
-
*
|
|
1606
|
-
*
|
|
1770
|
+
* Apply the resource's `fields` (field-level write permissions) to this
|
|
1771
|
+
* custom route's request body.
|
|
1772
|
+
*
|
|
1773
|
+
* Default: `true` for body-bearing methods (POST/PUT/PATCH), `false`
|
|
1774
|
+
* otherwise. Set to `false` to opt out — handler accepts the body as
|
|
1775
|
+
* supplied with no field-write filtering. The resource-level
|
|
1776
|
+
* `onFieldWriteDenied: 'reject' | 'strip'` setting governs how denials
|
|
1777
|
+
* are surfaced.
|
|
1778
|
+
*
|
|
1779
|
+
* `raw: true` routes ALWAYS bypass this — they've opted out of the
|
|
1780
|
+
* pipeline entirely.
|
|
1607
1781
|
*/
|
|
1608
|
-
|
|
1782
|
+
readonly fieldWrite?: boolean;
|
|
1609
1783
|
/**
|
|
1610
|
-
*
|
|
1611
|
-
*
|
|
1612
|
-
*
|
|
1784
|
+
* Per-route rate limit. A custom route is its own Fastify route, so this
|
|
1785
|
+
* overrides the resource-level `rateLimit` for THIS endpoint only:
|
|
1786
|
+
*
|
|
1787
|
+
* - omitted → inherit the resource-level limit (or app default)
|
|
1788
|
+
* - `false` → disable rate limiting on this route
|
|
1789
|
+
* - `{ max, timeWindow }` → apply this specific limit
|
|
1790
|
+
*
|
|
1791
|
+
* Requires `@fastify/rate-limit` registered (arc's factory wires it by
|
|
1792
|
+
* default). Use for an expensive `/export` or an auth-sensitive `/reset`
|
|
1793
|
+
* that needs a tighter cap than the resource's read-heavy CRUD, or `false`
|
|
1794
|
+
* on an internal health/webhook route that must not be throttled.
|
|
1795
|
+
*
|
|
1796
|
+
* Note: this is per-route because each custom route is a distinct endpoint.
|
|
1797
|
+
* `actions` share ONE `POST /:id/action` mount by design, so a per-ACTION
|
|
1798
|
+
* limit can't be expressed here — throttle a specific action by promoting
|
|
1799
|
+
* it to a dedicated `routes:` entry, or rely on the resource-level limit.
|
|
1613
1800
|
*/
|
|
1614
|
-
|
|
1801
|
+
readonly rateLimit?: RateLimitConfig | false;
|
|
1615
1802
|
/**
|
|
1616
|
-
*
|
|
1617
|
-
*
|
|
1618
|
-
*
|
|
1803
|
+
* Fastify route schema. Each slot (`body`, `querystring`, `params`,
|
|
1804
|
+
* `headers`, `response[status]`) accepts a plain JSON Schema object
|
|
1805
|
+
* **or** a Zod v4 schema — Arc auto-converts via `convertRouteSchema`.
|
|
1619
1806
|
*/
|
|
1620
|
-
|
|
1807
|
+
readonly schema?: {
|
|
1808
|
+
body?: unknown;
|
|
1809
|
+
querystring?: unknown;
|
|
1810
|
+
params?: unknown;
|
|
1811
|
+
headers?: unknown;
|
|
1812
|
+
response?: Record<number | string, unknown>;
|
|
1813
|
+
[key: string]: unknown;
|
|
1814
|
+
};
|
|
1621
1815
|
/**
|
|
1622
|
-
*
|
|
1623
|
-
*
|
|
1624
|
-
*
|
|
1816
|
+
* MCP tool generation:
|
|
1817
|
+
* - omitted/true: auto-generate (non-raw routes only)
|
|
1818
|
+
* - false: skip MCP
|
|
1819
|
+
* - object: explicit config
|
|
1625
1820
|
*/
|
|
1626
|
-
|
|
1821
|
+
readonly mcp?: boolean | RouteMcpConfig;
|
|
1627
1822
|
/**
|
|
1628
|
-
*
|
|
1629
|
-
*
|
|
1823
|
+
* MCP handler for raw routes — parallel entry point for MCP without
|
|
1824
|
+
* changing the HTTP handler.
|
|
1630
1825
|
*/
|
|
1631
|
-
|
|
1826
|
+
readonly mcpHandler?: (input: Record<string, unknown>) => Promise<{
|
|
1827
|
+
content: Array<{
|
|
1828
|
+
type: string;
|
|
1829
|
+
text: string;
|
|
1830
|
+
}>;
|
|
1831
|
+
isError?: boolean;
|
|
1832
|
+
}>;
|
|
1833
|
+
}
|
|
1834
|
+
//#endregion
|
|
1835
|
+
//#region src/types/resource/actions.d.ts
|
|
1836
|
+
/**
|
|
1837
|
+
* Action handler function for state transitions. Receives the resource
|
|
1838
|
+
* ID, action-specific data, and the request.
|
|
1839
|
+
*/
|
|
1840
|
+
type ActionHandlerFn = (id: string, data: Record<string, unknown>, req: RequestWithExtras) => Promise<unknown>;
|
|
1841
|
+
/**
|
|
1842
|
+
* Full action configuration with handler, permissions, and schema.
|
|
1843
|
+
*
|
|
1844
|
+
* Rate limiting is intentionally NOT per-action: every action shares one
|
|
1845
|
+
* `POST /:id/action` (or `POST /action`) mount, so Fastify's per-route limit
|
|
1846
|
+
* can't distinguish them. Actions inherit the resource-level `rateLimit`; to
|
|
1847
|
+
* throttle a specific operation, promote it to a `routes:` entry (which
|
|
1848
|
+
* supports `RouteDefinition.rateLimit`).
|
|
1849
|
+
*/
|
|
1850
|
+
interface ActionDefinition {
|
|
1851
|
+
readonly handler: ActionHandlerFn;
|
|
1852
|
+
/** Per-action permission (overrides resource-level `actionPermissions`) */
|
|
1853
|
+
readonly permissions?: PermissionCheck;
|
|
1632
1854
|
/**
|
|
1633
|
-
*
|
|
1634
|
-
*
|
|
1635
|
-
*
|
|
1855
|
+
* JSON Schema or Zod v4 schema for action-specific body fields.
|
|
1856
|
+
*
|
|
1857
|
+
* Typed `unknown` (not `Record<string, unknown>`) so Zod class instances
|
|
1858
|
+
* — `ZodObject<...>` carries no string index signature — assign without
|
|
1859
|
+
* a cast. Same convention as `RouteDefinition.schema.body` / `customSchemas`.
|
|
1860
|
+
* Runtime feature-detects via `convertRouteSchema` / `toJsonSchema`.
|
|
1636
1861
|
*/
|
|
1637
|
-
|
|
1862
|
+
readonly schema?: unknown;
|
|
1863
|
+
/** Description for OpenAPI docs and MCP tool */
|
|
1864
|
+
readonly description?: string;
|
|
1638
1865
|
/**
|
|
1639
|
-
*
|
|
1640
|
-
*
|
|
1866
|
+
* Whether this action needs an entity id from the URL.
|
|
1867
|
+
*
|
|
1868
|
+
* - `true` (default) — mounts under `POST /<prefix>/:id/action`. The `id`
|
|
1869
|
+
* path param is required; handler receives it as the first argument.
|
|
1870
|
+
* Use for actions that operate on an existing entity: `approve`,
|
|
1871
|
+
* `dispatch`, `cancel`, `archive`.
|
|
1872
|
+
* - `false` — mounts under `POST /<prefix>/action` (no `:id` segment).
|
|
1873
|
+
* Handler receives an empty-string first argument. Use for
|
|
1874
|
+
* collection-level actions that create / search / bulk-mutate:
|
|
1875
|
+
* `propose` (creates a new entity), `search` (returns rows), `bulk`.
|
|
1876
|
+
*
|
|
1877
|
+
* 2.15.5: previously every action had to live under `:id/action`, forcing
|
|
1878
|
+
* `propose`-style actions to swallow a meaningless URL parameter and
|
|
1879
|
+
* making the auto-generated MCP tool advertise an `id` field agents
|
|
1880
|
+
* had no value for. Setting `id: false` mounts the action at the
|
|
1881
|
+
* resource root and drops `id` from the MCP tool's input shape.
|
|
1641
1882
|
*/
|
|
1642
|
-
|
|
1883
|
+
readonly id?: boolean;
|
|
1643
1884
|
/**
|
|
1644
|
-
*
|
|
1645
|
-
*
|
|
1885
|
+
* MCP tool generation:
|
|
1886
|
+
* - omitted/true: auto-generate
|
|
1887
|
+
* - false: skip
|
|
1888
|
+
* - object: explicit config
|
|
1646
1889
|
*/
|
|
1647
|
-
|
|
1648
|
-
/**
|
|
1649
|
-
* Pre-computed read replacement. When set, arc skips
|
|
1650
|
-
* `repo.aggregate()` and calls this function instead. Same wire
|
|
1651
|
-
* shape, same permissions, different data source.
|
|
1652
|
-
*
|
|
1653
|
-
* Use for homepage-counter dashboards backed by host-managed rollup
|
|
1654
|
-
* tables (cron / CDC). The hook receives the compiled context
|
|
1655
|
-
* (filter + scope) so the host can route the lookup to the right
|
|
1656
|
-
* pre-aggregated bucket.
|
|
1657
|
-
*/
|
|
1658
|
-
materialized?: (ctx: AggregationMaterializedContext) => Promise<AggregationMaterializedResult>;
|
|
1659
|
-
/** Optional summary rendered in OpenAPI + MCP tool description. */
|
|
1660
|
-
summary?: string;
|
|
1661
|
-
/** Optional longer description for OpenAPI / MCP. */
|
|
1662
|
-
description?: string;
|
|
1663
|
-
/**
|
|
1664
|
-
* MCP tool generation control. Mirrors `actions[name].mcp` semantics.
|
|
1665
|
-
*
|
|
1666
|
-
* - `undefined` (default) — generate an MCP tool for this aggregation
|
|
1667
|
-
* - `false` — skip MCP tool generation (REST route still works)
|
|
1668
|
-
* - `{ description?, annotations? }` — generate with overrides
|
|
1669
|
-
*/
|
|
1670
|
-
mcp?: boolean | {
|
|
1671
|
-
readonly description?: string;
|
|
1672
|
-
readonly annotations?: Record<string, unknown>;
|
|
1673
|
-
};
|
|
1890
|
+
readonly mcp?: boolean | RouteMcpConfig;
|
|
1674
1891
|
}
|
|
1675
|
-
/**
|
|
1676
|
-
type
|
|
1892
|
+
/** Action config: bare handler function OR full ActionDefinition. */
|
|
1893
|
+
type ActionEntry = ActionHandlerFn | ActionDefinition;
|
|
1894
|
+
/** Actions configuration map. */
|
|
1895
|
+
type ActionsMap = Record<string, ActionEntry>;
|
|
1677
1896
|
//#endregion
|
|
1678
|
-
//#region src/types/
|
|
1897
|
+
//#region src/types/resource/cache.d.ts
|
|
1679
1898
|
/**
|
|
1680
|
-
*
|
|
1681
|
-
*
|
|
1682
|
-
*
|
|
1899
|
+
* Per-resource QueryCache configuration — stale-while-revalidate
|
|
1900
|
+
* windows, per-operation overrides, and tag-based cross-resource
|
|
1901
|
+
* invalidation.
|
|
1683
1902
|
*/
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
info: (...args: unknown[]) => void;
|
|
1699
|
-
warn: (...args: unknown[]) => void;
|
|
1700
|
-
error: (...args: unknown[]) => void;
|
|
1701
|
-
debug: (...args: unknown[]) => void;
|
|
1903
|
+
/**
|
|
1904
|
+
* Per-resource cache configuration for QueryCache. Enables
|
|
1905
|
+
* stale-while-revalidate, auto-invalidation on mutations, and
|
|
1906
|
+
* cross-resource tag-based invalidation.
|
|
1907
|
+
*/
|
|
1908
|
+
interface ResourceCacheConfig {
|
|
1909
|
+
/** Seconds data is "fresh" (no revalidation). Default: 0 */
|
|
1910
|
+
staleTime?: number;
|
|
1911
|
+
/** Seconds stale data stays cached (SWR window). Default: 60 */
|
|
1912
|
+
gcTime?: number;
|
|
1913
|
+
/** Per-operation overrides */
|
|
1914
|
+
list?: {
|
|
1915
|
+
staleTime?: number;
|
|
1916
|
+
gcTime?: number;
|
|
1702
1917
|
};
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
data: T;
|
|
1707
|
-
status: "fresh" | "stale" | "miss";
|
|
1708
|
-
}>;
|
|
1709
|
-
set: <T>(key: string, data: T, config: {
|
|
1710
|
-
staleTime?: number;
|
|
1711
|
-
gcTime?: number;
|
|
1712
|
-
tags?: string[];
|
|
1713
|
-
}) => Promise<void>;
|
|
1714
|
-
getResourceVersion: (resource: string) => Promise<number>;
|
|
1715
|
-
bumpResourceVersion: (resource: string) => Promise<void>;
|
|
1918
|
+
byId?: {
|
|
1919
|
+
staleTime?: number;
|
|
1920
|
+
gcTime?: number;
|
|
1716
1921
|
};
|
|
1922
|
+
/** Tags for cross-resource invalidation grouping */
|
|
1923
|
+
tags?: string[];
|
|
1924
|
+
/**
|
|
1925
|
+
* Cross-resource invalidation: event pattern → tag targets.
|
|
1926
|
+
* @example { 'category.*': ['catalog'] }
|
|
1927
|
+
*/
|
|
1928
|
+
invalidateOn?: Record<string, string[]>;
|
|
1929
|
+
/** Disable caching for this resource */
|
|
1930
|
+
disabled?: boolean;
|
|
1717
1931
|
}
|
|
1932
|
+
//#endregion
|
|
1933
|
+
//#region src/pipeline/types.d.ts
|
|
1718
1934
|
/**
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1721
|
-
* **Generic parameters** (all default to safe permissive types so existing code keeps working):
|
|
1722
|
-
* - `TBody` — request body shape (default: `unknown`)
|
|
1723
|
-
* - `TParams` — route param shape (default: `Record<string, string>`)
|
|
1724
|
-
* - `TQuery` — query string shape (default: `Record<string, unknown>`)
|
|
1725
|
-
* - `TUser` — authenticated user shape (default: `UserBase`)
|
|
1726
|
-
* - `TMetadata` — internal metadata shape (default: `Record<string, unknown>`;
|
|
1727
|
-
* override with `ArcInternalMetadata` or your own augmentation when you
|
|
1728
|
-
* need typed access to `_scope`, `_policyFilters`, custom hook context, etc.)
|
|
1729
|
-
*
|
|
1730
|
-
* @example
|
|
1731
|
-
* ```typescript
|
|
1732
|
-
* // Untyped (default) — req.body is `unknown`, must be narrowed
|
|
1733
|
-
* async create(req: IRequestContext) {
|
|
1734
|
-
* const data = req.body as Partial<Product>;
|
|
1735
|
-
* return { success: true, data: await productRepo.create(data) };
|
|
1736
|
-
* }
|
|
1737
|
-
*
|
|
1738
|
-
* // Typed body — req.body is `CreateProductInput`, narrowing not needed
|
|
1739
|
-
* async create(req: IRequestContext<CreateProductInput>) {
|
|
1740
|
-
* return { success: true, data: await productRepo.create(req.body) };
|
|
1741
|
-
* }
|
|
1742
|
-
*
|
|
1743
|
-
* // Fully typed — body, route params, query, and metadata
|
|
1744
|
-
* async update(
|
|
1745
|
-
* req: IRequestContext<
|
|
1746
|
-
* Partial<Product>,
|
|
1747
|
-
* { id: string },
|
|
1748
|
-
* { fields?: string },
|
|
1749
|
-
* ArcInternalMetadata
|
|
1750
|
-
* >,
|
|
1751
|
-
* ) {
|
|
1752
|
-
* const fields = req.query.fields?.split(',');
|
|
1753
|
-
* const orgId = req.metadata?._scope ? getOrgId(req.metadata._scope) : undefined;
|
|
1754
|
-
* return { success: true, data: await productRepo.update(req.params.id, req.body) };
|
|
1755
|
-
* }
|
|
1756
|
-
* ```
|
|
1935
|
+
* Pipeline context passed to guards, transforms, and interceptors.
|
|
1936
|
+
* Extends IRequestContext with pipeline-specific metadata.
|
|
1757
1937
|
*/
|
|
1938
|
+
interface PipelineContext extends IRequestContext {
|
|
1939
|
+
/** Resource name being accessed */
|
|
1940
|
+
resource: string;
|
|
1941
|
+
/** CRUD operation being performed */
|
|
1942
|
+
operation: "list" | "get" | "create" | "update" | "delete" | string;
|
|
1943
|
+
}
|
|
1758
1944
|
/**
|
|
1759
|
-
*
|
|
1760
|
-
*
|
|
1761
|
-
* **v2.10.6:** previously, pulling tenant/user/role info from inside a
|
|
1762
|
-
* controller override meant digging through `req.metadata._scope` and
|
|
1763
|
-
* calling `getOrgId(scope)` / `getUserId(user)` manually. This projection
|
|
1764
|
-
* lifts the two fields most hosts need directly onto `req` so cross-kit
|
|
1765
|
-
* controller code reads:
|
|
1766
|
-
*
|
|
1767
|
-
* ```ts
|
|
1768
|
-
* async create(req: IRequestContext) {
|
|
1769
|
-
* const flowCtx = { organizationId: req.scope?.organizationId, actorId: req.scope?.userId };
|
|
1770
|
-
* }
|
|
1771
|
-
* ```
|
|
1772
|
-
*
|
|
1773
|
-
* Full scope shape (discriminated union of `member` / `service` / `elevated` / `public`)
|
|
1774
|
-
* still lives on `req.metadata._scope` for code that needs to branch on
|
|
1775
|
-
* `scope.kind` — this projection just surfaces the two keys every
|
|
1776
|
-
* tenant-scoped resource reaches for.
|
|
1945
|
+
* Which operations a pipeline step applies to.
|
|
1946
|
+
* If omitted, applies to ALL operations.
|
|
1777
1947
|
*/
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
params: TParams;
|
|
1789
|
-
/** Query string parameters */
|
|
1790
|
-
query: TQuery;
|
|
1791
|
-
/** Request body */
|
|
1792
|
-
body: TBody;
|
|
1793
|
-
/** Authenticated user or null */
|
|
1794
|
-
user: TUser | null;
|
|
1795
|
-
/** Request headers */
|
|
1796
|
-
headers: Record<string, string | undefined>;
|
|
1797
|
-
/** Organization ID (for multi-tenant apps) */
|
|
1798
|
-
organizationId?: string;
|
|
1799
|
-
/** Team ID (for team-scoped resources) */
|
|
1800
|
-
teamId?: string;
|
|
1801
|
-
/**
|
|
1802
|
-
* First-class tenant/user scope projection — lifted from `metadata._scope`
|
|
1803
|
-
* so controller overrides don't have to dig through Arc internals.
|
|
1804
|
-
* See {@link RequestScopeProjection}. `undefined` for routes that run
|
|
1805
|
-
* without auth / scope resolution.
|
|
1806
|
-
*/
|
|
1807
|
-
scope?: RequestScopeProjection;
|
|
1808
|
-
/**
|
|
1809
|
-
* Organization/auth context from middleware.
|
|
1810
|
-
* Contains orgRoles, orgScope, organizationId, and any custom fields
|
|
1811
|
-
* set by the auth adapter or org-scope plugin.
|
|
1812
|
-
*
|
|
1813
|
-
* @example
|
|
1814
|
-
* ```typescript
|
|
1815
|
-
* async create(req: IRequestContext) {
|
|
1816
|
-
* const roles = req.context?.orgRoles ?? [];
|
|
1817
|
-
* if (roles.includes('manager')) { ... }
|
|
1818
|
-
* }
|
|
1819
|
-
* ```
|
|
1820
|
-
*/
|
|
1821
|
-
context?: RequestContext;
|
|
1822
|
-
/**
|
|
1823
|
-
* Internal metadata (includes context + Arc internals like `_policyFilters`,
|
|
1824
|
-
* `_scope`, `log`). Type as `ArcInternalMetadata` for typed access to Arc's
|
|
1825
|
-
* built-in fields, or supply your own interface to layer custom fields.
|
|
1826
|
-
*/
|
|
1827
|
-
metadata?: TMetadata;
|
|
1828
|
-
/**
|
|
1829
|
-
* Fastify server accessor — publish events, log, and audit
|
|
1830
|
-
* from any handler without switching to `raw: true`.
|
|
1831
|
-
*
|
|
1832
|
-
* @example
|
|
1833
|
-
* ```typescript
|
|
1834
|
-
* async reschedule(req: IRequestContext) {
|
|
1835
|
-
* const result = await repo.reschedule(req.params.id, req.body);
|
|
1836
|
-
* await req.server?.events?.publish('interview.rescheduled', { data: result });
|
|
1837
|
-
* return { data: result };
|
|
1838
|
-
* }
|
|
1839
|
-
* ```
|
|
1840
|
-
*/
|
|
1841
|
-
server?: ServerAccessor;
|
|
1948
|
+
type OperationFilter = Array<"list" | "get" | "create" | "update" | "delete" | string>;
|
|
1949
|
+
/**
|
|
1950
|
+
* Guard — boolean check that short-circuits on failure.
|
|
1951
|
+
* Return true to proceed, throw to deny.
|
|
1952
|
+
*/
|
|
1953
|
+
interface Guard {
|
|
1954
|
+
readonly _type: "guard";
|
|
1955
|
+
readonly name: string;
|
|
1956
|
+
readonly operations?: OperationFilter;
|
|
1957
|
+
handler(ctx: PipelineContext): boolean | Promise<boolean>;
|
|
1842
1958
|
}
|
|
1843
1959
|
/**
|
|
1844
|
-
*
|
|
1845
|
-
*
|
|
1846
|
-
* Errors throw `ArcError` (or any `HttpError`-shaped class); the global
|
|
1847
|
-
* error handler catches them and emits an `ErrorContract`. There is no
|
|
1848
|
-
* `success` discriminator on the response — HTTP status is the wire
|
|
1849
|
-
* discriminator (2xx = data, 4xx/5xx = ErrorContract).
|
|
1960
|
+
* Transform — modifies request data before the handler.
|
|
1961
|
+
* Returns modified context (or mutates in place).
|
|
1850
1962
|
*/
|
|
1851
|
-
interface
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
/** Custom response headers (e.g. X-Total-Count, Link, ETag). */
|
|
1857
|
-
headers?: Record<string, string>;
|
|
1858
|
-
/** Top-level metadata merged into list-shaped responses (e.g. `{ took }`). */
|
|
1859
|
-
meta?: Record<string, unknown>;
|
|
1963
|
+
interface Transform {
|
|
1964
|
+
readonly _type: "transform";
|
|
1965
|
+
readonly name: string;
|
|
1966
|
+
readonly operations?: OperationFilter;
|
|
1967
|
+
handler(ctx: PipelineContext): PipelineContext | undefined | Promise<PipelineContext | undefined>;
|
|
1860
1968
|
}
|
|
1861
1969
|
/**
|
|
1862
|
-
*
|
|
1863
|
-
*
|
|
1864
|
-
* Receives a request context object, returns IControllerResponse.
|
|
1865
|
-
* Use with `raw: false` in routes.
|
|
1866
|
-
*
|
|
1867
|
-
* **Generic parameters:**
|
|
1868
|
-
* - `TResponse` — shape of `IControllerResponse.data` (default: `unknown`)
|
|
1869
|
-
* - `TBody` — shape of `req.body` (default: `unknown`)
|
|
1870
|
-
* - `TParams` — shape of `req.params` (default: `Record<string, string>`)
|
|
1871
|
-
* - `TQuery` — shape of `req.query` (default: `Record<string, unknown>`)
|
|
1872
|
-
*
|
|
1873
|
-
* Backward-compatible: `ControllerHandler<Product>` still works (only the
|
|
1874
|
-
* response data is typed); add more generics as needed when you want
|
|
1875
|
-
* type-safe access to the request body, params, or query string.
|
|
1876
|
-
*
|
|
1877
|
-
* @example
|
|
1878
|
-
* ```typescript
|
|
1879
|
-
* // Untyped req — body is unknown, must be narrowed
|
|
1880
|
-
* const createProduct: ControllerHandler<Product> = async (req) => {
|
|
1881
|
-
* const product = await productRepo.create(req.body as Partial<Product>);
|
|
1882
|
-
* return { data: product, status: 201 };
|
|
1883
|
-
* };
|
|
1884
|
-
*
|
|
1885
|
-
* // Fully typed — body, params, query, and response all inferred
|
|
1886
|
-
* const updateProduct: ControllerHandler<
|
|
1887
|
-
* Product,
|
|
1888
|
-
* Partial<Product>,
|
|
1889
|
-
* { id: string },
|
|
1890
|
-
* { upsert?: string }
|
|
1891
|
-
* > = async (req) => {
|
|
1892
|
-
* const upsert = req.query.upsert === "true";
|
|
1893
|
-
* const product = await productRepo.update(req.params.id, req.body, { upsert });
|
|
1894
|
-
* return { data: product };
|
|
1895
|
-
* };
|
|
1896
|
-
*
|
|
1897
|
-
* routes: [{
|
|
1898
|
-
* method: 'POST',
|
|
1899
|
-
* path: '/products',
|
|
1900
|
-
* handler: createProduct,
|
|
1901
|
-
* permissions: requireAuth(),
|
|
1902
|
-
* raw: false, // Arc wraps this into Fastify handler
|
|
1903
|
-
* }]
|
|
1904
|
-
* ```
|
|
1905
|
-
*/
|
|
1906
|
-
type ControllerHandler<TResponse = unknown, TBody = unknown, TParams extends Record<string, string> = Record<string, string>, TQuery extends Record<string, unknown> = Record<string, unknown>> = (req: IRequestContext<TBody, TParams, TQuery>) => Promise<IControllerResponse<TResponse>>;
|
|
1907
|
-
/**
|
|
1908
|
-
* Fastify native handler
|
|
1909
|
-
*
|
|
1910
|
-
* Standard Fastify request/reply pattern.
|
|
1911
|
-
* Use with `raw: true` in routes.
|
|
1912
|
-
*
|
|
1913
|
-
* @example
|
|
1914
|
-
* ```typescript
|
|
1915
|
-
* const downloadFile: FastifyHandler = async (request, reply) => {
|
|
1916
|
-
* const file = await getFile(request.params.id);
|
|
1917
|
-
* reply.header('Content-Type', file.mimeType);
|
|
1918
|
-
* return reply.send(file.buffer);
|
|
1919
|
-
* };
|
|
1920
|
-
*
|
|
1921
|
-
* routes: [{
|
|
1922
|
-
* method: 'GET',
|
|
1923
|
-
* path: '/files/:id/download',
|
|
1924
|
-
* handler: downloadFile,
|
|
1925
|
-
* permissions: requireAuth(),
|
|
1926
|
-
* raw: true, // Use as-is, no wrapping
|
|
1927
|
-
* }]
|
|
1928
|
-
* ```
|
|
1929
|
-
*/
|
|
1930
|
-
type FastifyHandler<RouteGeneric extends Record<string, unknown> = Record<string, unknown>> = (request: FastifyRequest<RouteGeneric>, reply: FastifyReply) => Promise<unknown> | unknown;
|
|
1931
|
-
/**
|
|
1932
|
-
* Union type for route handlers
|
|
1970
|
+
* Next function passed to interceptors — calls the handler (or next interceptor).
|
|
1933
1971
|
*/
|
|
1934
|
-
type
|
|
1972
|
+
type NextFunction = () => Promise<IControllerResponse<unknown>>;
|
|
1935
1973
|
/**
|
|
1936
|
-
*
|
|
1937
|
-
*
|
|
1938
|
-
* `list`'s return type aligns with repo-core's `MinimalRepo.getAll()`
|
|
1939
|
-
* contract — the kit MAY return an offset envelope, a keyset envelope,
|
|
1940
|
-
* or a raw array. Arc's `BaseController` forwards the kit's response
|
|
1941
|
-
* verbatim; consumers narrow on shape
|
|
1942
|
-
* (`Array.isArray(data)` → bare array, presence of `total` → offset,
|
|
1943
|
-
* presence of `nextCursor` → keyset).
|
|
1974
|
+
* Intercept — wraps handler execution (before + after pattern).
|
|
1944
1975
|
*/
|
|
1945
|
-
interface
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
delete(req: IRequestContext): Promise<IControllerResponse<{
|
|
1951
|
-
message: string;
|
|
1952
|
-
}>>;
|
|
1976
|
+
interface Interceptor {
|
|
1977
|
+
readonly _type: "interceptor";
|
|
1978
|
+
readonly name: string;
|
|
1979
|
+
readonly operations?: OperationFilter;
|
|
1980
|
+
handler(ctx: PipelineContext, next: NextFunction): Promise<IControllerResponse<unknown>>;
|
|
1953
1981
|
}
|
|
1982
|
+
type PipelineStep = Guard | Transform | Interceptor;
|
|
1954
1983
|
/**
|
|
1955
|
-
*
|
|
1956
|
-
* including class instances with extra methods / private fields.
|
|
1957
|
-
*
|
|
1958
|
-
* **v2.10.6:** the previous `[key: string]: unknown` index signature made
|
|
1959
|
-
* real class instances fail structural assignment (`new ScrapController()`
|
|
1960
|
-
* needed a `as unknown as ControllerLike` cast, because class instances
|
|
1961
|
-
* don't have an index signature). Dropped — arc only invokes the CRUD
|
|
1962
|
-
* methods at runtime, so the rest of the shape is the caller's concern.
|
|
1963
|
-
*
|
|
1964
|
-
* The five CRUD slots stay optional so partial controllers (e.g. read-only)
|
|
1965
|
-
* assign too. Additional domain methods on the controller are allowed by
|
|
1966
|
-
* construction (they're just not part of this contract).
|
|
1984
|
+
* Pipeline configuration — can be a flat array or per-operation map.
|
|
1967
1985
|
*/
|
|
1968
|
-
|
|
1969
|
-
list?:
|
|
1970
|
-
get?:
|
|
1971
|
-
create?:
|
|
1972
|
-
update?:
|
|
1973
|
-
delete?:
|
|
1974
|
-
|
|
1986
|
+
type PipelineConfig = PipelineStep[] | {
|
|
1987
|
+
list?: PipelineStep[];
|
|
1988
|
+
get?: PipelineStep[];
|
|
1989
|
+
create?: PipelineStep[];
|
|
1990
|
+
update?: PipelineStep[];
|
|
1991
|
+
delete?: PipelineStep[];
|
|
1992
|
+
[operation: string]: PipelineStep[] | undefined;
|
|
1993
|
+
};
|
|
1975
1994
|
//#endregion
|
|
1976
|
-
//#region src/types
|
|
1977
|
-
/** Standard controller type alias for CRUD operations. */
|
|
1978
|
-
type CrudController<TDoc> = IController<TDoc>;
|
|
1995
|
+
//#region src/core/aggregation/types.d.ts
|
|
1979
1996
|
/**
|
|
1980
|
-
*
|
|
1981
|
-
*
|
|
1982
|
-
*
|
|
1983
|
-
* registry introspection (audit scripts ask "what happens to this
|
|
1984
|
-
* resource on org delete?").
|
|
1997
|
+
* Sugar for measures: `'count'` / `'count:field'` / `'sum:price'` /
|
|
1998
|
+
* `'avg:rating'` / `'min:created'` / `'max:updated'` /
|
|
1999
|
+
* `'countDistinct:userId'` / `'percentile:latency:0.95'`.
|
|
1985
2000
|
*
|
|
1986
|
-
*
|
|
1987
|
-
*
|
|
1988
|
-
* - `soft` — recoverable; pair with TTL for eventual hard-purge.
|
|
1989
|
-
* - `anonymize` — keep the row (legal retention) but clear PII
|
|
1990
|
-
* (HIPAA / PCI / SOX-compatible).
|
|
1991
|
-
* - `skip` — explicit opt-out with mandatory `reason`.
|
|
2001
|
+
* Compiles to the canonical `AggMeasure` IR at boot. Hosts who want
|
|
2002
|
+
* the full IR can pass an `AggMeasure` object directly.
|
|
1992
2003
|
*
|
|
1993
|
-
*
|
|
2004
|
+
* **Percentile.** `'percentile:<field>:<p>'` where `p` is a numeric
|
|
2005
|
+
* literal in `[0, 1]` (e.g. `'percentile:latency:0.95'` for P95).
|
|
2006
|
+
* Mongokit (≥3.13) compiles to `$percentile`; sqlitekit throws
|
|
2007
|
+
* `UnsupportedOperationError` (no native percentile in SQLite).
|
|
1994
2008
|
*/
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
* Discriminated union from `@classytic/repo-core/repository` — every
|
|
1999
|
-
* kit's `purgeByField` consumes the same shape.
|
|
2000
|
-
*/
|
|
2001
|
-
strategy: TenantPurgeStrategy;
|
|
2002
|
-
/**
|
|
2003
|
-
* Resources are processed in ascending `priority` order. Use to land
|
|
2004
|
-
* leaf data (logs, events) before aggregate references. Default `100`.
|
|
2005
|
-
*/
|
|
2006
|
-
priority?: number;
|
|
2007
|
-
/**
|
|
2008
|
-
* Rows per chunk for the underlying `purgeByField` call — bounds
|
|
2009
|
-
* lock contention + replication-log pressure on very large tenants.
|
|
2010
|
-
* Default kit-specific (~1000).
|
|
2011
|
-
*/
|
|
2012
|
-
batchSize?: number;
|
|
2013
|
-
}
|
|
2009
|
+
type AggMeasureShorthand = "count" | `count:${string}` | `countDistinct:${string}` | `sum:${string}` | `avg:${string}` | `min:${string}` | `max:${string}` | `percentile:${string}:${number}`;
|
|
2010
|
+
/** Either canonical IR or shorthand string — both compile to `AggMeasure`. */
|
|
2011
|
+
type AggMeasureInput = AggMeasure | AggMeasureShorthand;
|
|
2014
2012
|
/**
|
|
2015
|
-
*
|
|
2016
|
-
* `
|
|
2017
|
-
*
|
|
2013
|
+
* Cache config for an aggregation. Translates directly to the kit-side
|
|
2014
|
+
* `CacheOptions` (TanStack-shaped) which the unified
|
|
2015
|
+
* `@classytic/repo-core/cache` plugin reads from `req.cache`.
|
|
2018
2016
|
*
|
|
2019
|
-
*
|
|
2020
|
-
*
|
|
2021
|
-
*
|
|
2022
|
-
*
|
|
2023
|
-
*/
|
|
2024
|
-
interface ResolvedTenantPurge {
|
|
2025
|
-
readonly strategy: TenantPurgeStrategy;
|
|
2026
|
-
readonly priority: number;
|
|
2027
|
-
readonly batchSize?: number;
|
|
2028
|
-
/**
|
|
2029
|
-
* Where the strategy came from — `'declared'` (host wrote
|
|
2030
|
-
* `onTenantDelete` explicitly) or `'disabled'` (no declaration —
|
|
2031
|
-
* runner skips this resource).
|
|
2032
|
-
*/
|
|
2033
|
-
readonly source: "declared" | "disabled";
|
|
2034
|
-
}
|
|
2035
|
-
/**
|
|
2036
|
-
* Per-resource cache configuration for QueryCache. Enables
|
|
2037
|
-
* stale-while-revalidate, auto-invalidation on mutations, and
|
|
2038
|
-
* cross-resource tag-based invalidation.
|
|
2017
|
+
* Caching only fires when the kit's repo has the `cachePlugin` wired —
|
|
2018
|
+
* arc declares the policy; the kit handles SWR + tag invalidation +
|
|
2019
|
+
* version-bump on writes. Hosts without the plugin installed silently
|
|
2020
|
+
* fall through to a non-cached call.
|
|
2039
2021
|
*/
|
|
2040
|
-
interface
|
|
2041
|
-
/** Seconds
|
|
2022
|
+
interface AggregationCacheConfig {
|
|
2023
|
+
/** Seconds the entry is fresh — no revalidation while inside this window. */
|
|
2042
2024
|
staleTime?: number;
|
|
2043
|
-
/** Seconds
|
|
2025
|
+
/** Seconds the entry stays in cache past stale before eviction. Default: 60. */
|
|
2044
2026
|
gcTime?: number;
|
|
2045
|
-
/** Per-operation overrides */
|
|
2046
|
-
list?: {
|
|
2047
|
-
staleTime?: number;
|
|
2048
|
-
gcTime?: number;
|
|
2049
|
-
};
|
|
2050
|
-
byId?: {
|
|
2051
|
-
staleTime?: number;
|
|
2052
|
-
gcTime?: number;
|
|
2053
|
-
};
|
|
2054
|
-
/** Tags for cross-resource invalidation grouping */
|
|
2055
|
-
tags?: string[];
|
|
2056
|
-
/**
|
|
2057
|
-
* Cross-resource invalidation: event pattern → tag targets.
|
|
2058
|
-
* @example { 'category.*': ['catalog'] }
|
|
2059
|
-
*/
|
|
2060
|
-
invalidateOn?: Record<string, string[]>;
|
|
2061
|
-
/** Disable caching for this resource */
|
|
2062
|
-
disabled?: boolean;
|
|
2063
|
-
}
|
|
2064
|
-
interface RateLimitConfig {
|
|
2065
|
-
/** Maximum number of requests allowed within the time window */
|
|
2066
|
-
max: number;
|
|
2067
|
-
/** Time window for rate limiting (e.g., '1 minute', '15 seconds') */
|
|
2068
|
-
timeWindow: string;
|
|
2069
|
-
}
|
|
2070
|
-
/**
|
|
2071
|
-
* Per-field rule — arc's extension of repo-core's 4-field `FieldRule` floor
|
|
2072
|
-
* (`immutable`, `immutableAfterCreate`, `systemManaged`, `optional`) with
|
|
2073
|
-
* the constraint / UI / security bits arc layers on top.
|
|
2074
|
-
*
|
|
2075
|
-
* Kept structurally compatible with `@classytic/repo-core/schema`'s
|
|
2076
|
-
* `FieldRule` so arc's `fieldRules: Record<string, ArcFieldRule>` flows
|
|
2077
|
-
* into mongokit's / sqlitekit's `buildCrudSchemasFromModel(..., options)`
|
|
2078
|
-
* without a cast. See `RouteSchemaOptions` JSDoc for the full rationale.
|
|
2079
|
-
*/
|
|
2080
|
-
interface ArcFieldRule extends FieldRule {
|
|
2081
2027
|
/**
|
|
2082
|
-
* When
|
|
2083
|
-
*
|
|
2084
|
-
*
|
|
2085
|
-
* body — needed for cross-tenant admin writes where the tenant field
|
|
2086
|
-
* is the only way to pick a target org.
|
|
2087
|
-
*
|
|
2088
|
-
* Auto-set by `defineResource` on the configured `tenantField`. Hosts
|
|
2089
|
-
* can set it manually on other fields (e.g. `createdBy`) if they want
|
|
2090
|
-
* elevation-only override semantics for those too.
|
|
2091
|
-
*
|
|
2092
|
-
* Has no effect when `isElevated(scope)` is false — member and
|
|
2093
|
-
* service callers continue to have the field stripped.
|
|
2094
|
-
*/
|
|
2095
|
-
preserveForElevated?: boolean;
|
|
2096
|
-
hidden?: boolean;
|
|
2097
|
-
/**
|
|
2098
|
-
* Aggregation visibility override. By default, only `hidden: true`
|
|
2099
|
-
* blocks a field from `groupBy` / `measures.field` / `sort` / `dateBuckets`
|
|
2100
|
-
* — that's the genuine cardinality-leak guard (the value is omitted from
|
|
2101
|
-
* list/get responses, so exposing it via aggregation would reveal data
|
|
2102
|
-
* the client can't otherwise see).
|
|
2103
|
-
*
|
|
2104
|
-
* `systemManaged: true` does **not** block aggregation — it's a write
|
|
2105
|
-
* rule, not a visibility rule. Server-stamped fields like `createdAt`,
|
|
2106
|
-
* `status`, or plugin-generated handles are visible in every list
|
|
2107
|
-
* response and should aggregate freely.
|
|
2108
|
-
*
|
|
2109
|
-
* Use this flag to override the default:
|
|
2110
|
-
*
|
|
2111
|
-
* - `aggregable: false` — explicit deny, even on visible fields. Useful
|
|
2112
|
-
* when a value is exposed per-row but the cardinality across rows is
|
|
2113
|
-
* itself sensitive (e.g. `email` is visible in `get/:id` to admins
|
|
2114
|
-
* but you don't want a public-readable agg of email distributions).
|
|
2115
|
-
* - `aggregable: true` — escape hatch on `hidden` fields. Lets you
|
|
2116
|
-
* aggregate a hidden column when you're sure cardinality leakage
|
|
2117
|
-
* isn't a concern (e.g. `internalScore` hidden from list, but a
|
|
2118
|
-
* committee-only `byScore` agg is fine).
|
|
2119
|
-
*
|
|
2120
|
-
* Defaults to `undefined` (use the `hidden`-only rule).
|
|
2121
|
-
*/
|
|
2122
|
-
aggregable?: boolean;
|
|
2123
|
-
/** String minimum length — auto-maps to OpenAPI `minLength` and MCP tool schema */
|
|
2124
|
-
minLength?: number;
|
|
2125
|
-
/** String maximum length — auto-maps to OpenAPI `maxLength` and MCP tool schema */
|
|
2126
|
-
maxLength?: number;
|
|
2127
|
-
/** Number minimum — auto-maps to OpenAPI `minimum` and MCP tool schema */
|
|
2128
|
-
min?: number;
|
|
2129
|
-
/** Number maximum — auto-maps to OpenAPI `maximum` and MCP tool schema */
|
|
2130
|
-
max?: number;
|
|
2131
|
-
/** Regex pattern — auto-maps to OpenAPI `pattern` and MCP tool schema */
|
|
2132
|
-
pattern?: string;
|
|
2133
|
-
/** Allowed values — auto-maps to OpenAPI `enum` and MCP tool schema */
|
|
2134
|
-
enum?: ReadonlyArray<string | number>;
|
|
2135
|
-
/**
|
|
2136
|
-
* When `true`, widen the JSON Schema `type` of this field to also
|
|
2137
|
-
* accept `null`. Mirrors Zod's `.nullable()` at the arc config layer
|
|
2138
|
-
* for kit-generated schemas that don't carry the flag end-to-end
|
|
2139
|
-
* (e.g. Zod → Mongoose → mongokit drops `.nullable()` because
|
|
2140
|
-
* Mongoose has no first-class nullable marker unless `default: null`
|
|
2141
|
-
* is also set).
|
|
2142
|
-
*
|
|
2143
|
-
* Applied post-kit by `mergeFieldRuleConstraints`: if the adapter
|
|
2144
|
-
* emitted `{ type: 'string', enum: [...] }` for a field arc should
|
|
2145
|
-
* accept null for, the merge widens it to
|
|
2146
|
-
* `{ type: ['string', 'null'], enum: [...] }` — draft-7 tuple form
|
|
2147
|
-
* AJV 8 validates natively.
|
|
2148
|
-
*
|
|
2149
|
-
* No-op when the property already declares `type: [...,'null']` or
|
|
2150
|
-
* an `anyOf: [..., { type: 'null' }]` branch — arc never fights the
|
|
2151
|
-
* kit's own output.
|
|
2152
|
-
*/
|
|
2153
|
-
nullable?: boolean;
|
|
2154
|
-
/** Human-readable description — auto-maps to OpenAPI `description` */
|
|
2155
|
-
description?: string;
|
|
2156
|
-
[key: string]: unknown;
|
|
2157
|
-
}
|
|
2158
|
-
/**
|
|
2159
|
-
* Schema-shaping options for a resource.
|
|
2160
|
-
*
|
|
2161
|
-
* Extends `@classytic/repo-core/schema`'s `SchemaBuilderOptions` so every
|
|
2162
|
-
* kit-generator callback typed against the repo-core contract
|
|
2163
|
-
* (mongokit's `buildCrudSchemasFromModel`, sqlitekit's
|
|
2164
|
-
* `buildCrudSchemasFromTable`, prismakit's equivalent) accepts arc's
|
|
2165
|
-
* options bag directly — no `as SchemaBuilderOptions` / `Parameters<...>[1]`
|
|
2166
|
-
* cast at the host wiring site.
|
|
2167
|
-
*
|
|
2168
|
-
* Inherited from `SchemaBuilderOptions`:
|
|
2169
|
-
* - `strictAdditionalProperties` — emit `additionalProperties: false`
|
|
2170
|
-
* - `dateAs` — `'date'` vs `'datetime'` ISO rendering
|
|
2171
|
-
* - `softRequiredFields` — stay in `properties`, drop from `required[]`
|
|
2172
|
-
* - `create: { omitFields, requiredOverrides, optionalOverrides, schemaOverrides }`
|
|
2173
|
-
* - `update: { omitFields, requireAtLeastOne }`
|
|
2174
|
-
* - `query: { filterableFields }` (kit-native filter declaration)
|
|
2175
|
-
* - `openApiExtensions` — emit `x-*` vendor keywords for docgen
|
|
2176
|
-
*
|
|
2177
|
-
* Arc adds:
|
|
2178
|
-
* - `fieldRules` with the richer `ArcFieldRule` per-entry shape
|
|
2179
|
-
* (preserveForElevated, minLength/maxLength/min/max/pattern, enum,
|
|
2180
|
-
* nullable, description) — arc's extensions are applied post-kit by
|
|
2181
|
-
* `mergeFieldRuleConstraints`; the kit only sees the repo-core floor.
|
|
2182
|
-
* - `hiddenFields` / `readonlyFields` / `requiredFields` / `optionalFields`
|
|
2183
|
-
* / `excludeFields` — arc-only convenience lists that predate fieldRules.
|
|
2184
|
-
* Keep using them if they're already in place; new code should prefer
|
|
2185
|
-
* `fieldRules` for per-field control.
|
|
2186
|
-
* - `filterableFields: string[]` — top-level list arc's MCP layer auto-
|
|
2187
|
-
* derives from `QueryParser.allowedFilterFields`. Distinct from the
|
|
2188
|
-
* inherited `query.filterableFields: Record<...>` which feeds the kit's
|
|
2189
|
-
* list-query schema; nothing stops a resource from using both.
|
|
2190
|
-
*
|
|
2191
|
-
* **Why extend rather than duplicate**: mongokit's
|
|
2192
|
-
* `buildCrudSchemasFromModel(model, options: SchemaBuilderOptions)` is the
|
|
2193
|
-
* canonical callback shape. Before the extension, hosts wrote
|
|
2194
|
-
* `Parameters<typeof buildCrudSchemasFromModel>[1]` or
|
|
2195
|
-
* `as SchemaBuilderOptions` at every wiring site — a defensive cast with
|
|
2196
|
-
* no runtime effect. Extension locks the structural relationship at the
|
|
2197
|
-
* type layer so the cast is compile-verified gone.
|
|
2198
|
-
*/
|
|
2199
|
-
interface RouteSchemaOptions extends SchemaBuilderOptions {
|
|
2200
|
-
hiddenFields?: string[];
|
|
2201
|
-
readonlyFields?: string[];
|
|
2202
|
-
requiredFields?: string[];
|
|
2203
|
-
optionalFields?: string[];
|
|
2204
|
-
excludeFields?: string[];
|
|
2205
|
-
/**
|
|
2206
|
-
* Fields allowed for filtering in list operations. MCP auto-derives
|
|
2207
|
-
* from `QueryParser.allowedFilterFields` when not set explicitly.
|
|
2208
|
-
*
|
|
2209
|
-
* Distinct from the inherited `query.filterableFields: Record<...>`
|
|
2210
|
-
* from `SchemaBuilderOptions` — that entry feeds the kit's list-query
|
|
2211
|
-
* JSON Schema; this one is arc's MCP-auto-derivation list.
|
|
2212
|
-
*/
|
|
2213
|
-
filterableFields?: string[];
|
|
2214
|
-
/**
|
|
2215
|
-
* Per-field rules. Richer than repo-core's `FieldRules` — arc adds
|
|
2216
|
-
* `preserveForElevated`, constraint hints (`minLength`, `enum`,
|
|
2217
|
-
* `nullable`, etc.), and `description` on top of the four-flag floor
|
|
2218
|
-
* (`immutable`, `immutableAfterCreate`, `systemManaged`, `optional`).
|
|
2219
|
-
*
|
|
2220
|
-
* Structurally compatible: `Record<string, ArcFieldRule>` is assignable
|
|
2221
|
-
* to repo-core's `Record<string, FieldRule>` since `ArcFieldRule extends
|
|
2222
|
-
* FieldRule`. Kits see only the floor; arc's extensions are applied
|
|
2223
|
-
* post-kit by `mergeFieldRuleConstraints`.
|
|
2224
|
-
*/
|
|
2225
|
-
fieldRules?: Record<string, ArcFieldRule>;
|
|
2226
|
-
/**
|
|
2227
|
-
* Query-time security whitelists + the kit's `filterableFields`.
|
|
2228
|
-
*
|
|
2229
|
-
* Extends repo-core's `SchemaBuilderOptions['query']` with arc-specific
|
|
2230
|
-
* runtime features that `QueryResolver` reads at request time:
|
|
2231
|
-
*
|
|
2232
|
-
* - **`allowedPopulate`** — populate-path whitelist consumed by
|
|
2233
|
-
* `QueryResolver.sanitizePopulate` / `sanitizeAdvancedPopulate`.
|
|
2234
|
-
* When set, only paths in the list pass through; everything else is
|
|
2235
|
-
* stripped silently. Shrinks the auto-wired `?populate=` attack surface.
|
|
2236
|
-
*
|
|
2237
|
-
* - **`allowedLookups`** — lookup-collection whitelist consumed by
|
|
2238
|
-
* `QueryResolver.sanitizeLookups`. When set, only the listed
|
|
2239
|
-
* collections may be `$lookup`'d into the pipeline.
|
|
2240
|
-
*
|
|
2241
|
-
* Both are pre-2.11.2 runtime features — the type was missing them, so
|
|
2242
|
-
* hosts wrote `as Record<string, unknown>` at every call site. Arc's own
|
|
2243
|
-
* `QueryResolver` had to cast its own input via `as AnyRecord` for the
|
|
2244
|
-
* same reason. Adding the type dropped both casts.
|
|
2028
|
+
* Stale-while-revalidate. When stale entries serve immediately and a
|
|
2029
|
+
* background refresh updates the cache. Default: `true` for
|
|
2030
|
+
* aggregations (dashboards almost always benefit from stale-serve).
|
|
2245
2031
|
*/
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
* When set, only the listed collections may be `$lookup`'d. Omit to
|
|
2256
|
-
* disable the whitelist (kit-level rules still apply).
|
|
2257
|
-
*/
|
|
2258
|
-
allowedLookups?: string[];
|
|
2259
|
-
};
|
|
2260
|
-
}
|
|
2261
|
-
interface FieldRule$1 {
|
|
2262
|
-
field: string;
|
|
2263
|
-
required?: boolean;
|
|
2264
|
-
readonly?: boolean;
|
|
2265
|
-
hidden?: boolean;
|
|
2032
|
+
swr?: boolean;
|
|
2033
|
+
/**
|
|
2034
|
+
* Group invalidation tags. Pass to `repo.cache?.invalidateByTags(tags)`
|
|
2035
|
+
* after a write to clear matching entries. The model name is
|
|
2036
|
+
* auto-tagged by the plugin — you only declare cross-cutting tags
|
|
2037
|
+
* (e.g. `'pricing'` to invalidate every aggregation that depends on
|
|
2038
|
+
* pricing across multiple resources).
|
|
2039
|
+
*/
|
|
2040
|
+
tags?: readonly string[];
|
|
2266
2041
|
}
|
|
2267
2042
|
/**
|
|
2268
|
-
*
|
|
2269
|
-
*
|
|
2270
|
-
* feature-detects at runtime. Slot values are typed `unknown` so
|
|
2271
|
-
* class-based Zod schemas assign without casts.
|
|
2043
|
+
* Per-aggregation rate limit. Layers on top of any global rate limit
|
|
2044
|
+
* via `@fastify/rate-limit` route-level config.
|
|
2272
2045
|
*/
|
|
2273
|
-
interface
|
|
2274
|
-
/**
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
[key: string]: unknown;
|
|
2279
|
-
};
|
|
2280
|
-
/** GET /:id — get one */
|
|
2281
|
-
get?: {
|
|
2282
|
-
params?: unknown;
|
|
2283
|
-
response?: Record<number, unknown>;
|
|
2284
|
-
[key: string]: unknown;
|
|
2285
|
-
};
|
|
2286
|
-
/** POST / — create */
|
|
2287
|
-
create?: {
|
|
2288
|
-
body?: unknown;
|
|
2289
|
-
response?: Record<number, unknown>;
|
|
2290
|
-
[key: string]: unknown;
|
|
2291
|
-
};
|
|
2292
|
-
/** PATCH /:id — update */
|
|
2293
|
-
update?: {
|
|
2294
|
-
params?: unknown;
|
|
2295
|
-
body?: unknown;
|
|
2296
|
-
response?: Record<number, unknown>;
|
|
2297
|
-
[key: string]: unknown;
|
|
2298
|
-
};
|
|
2299
|
-
/** DELETE /:id — delete */
|
|
2300
|
-
delete?: {
|
|
2301
|
-
params?: unknown;
|
|
2302
|
-
response?: Record<number, unknown>;
|
|
2303
|
-
[key: string]: unknown;
|
|
2304
|
-
};
|
|
2305
|
-
[key: string]: unknown;
|
|
2046
|
+
interface AggregationRateLimit {
|
|
2047
|
+
/** Max requests per window. */
|
|
2048
|
+
max: number;
|
|
2049
|
+
/** Window in milliseconds. */
|
|
2050
|
+
windowMs: number;
|
|
2306
2051
|
}
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2052
|
+
/**
|
|
2053
|
+
* Required date-range narrowing. Caller MUST send a bounded range on
|
|
2054
|
+
* this field, and the range MUST NOT exceed `maxRangeDays`.
|
|
2055
|
+
*
|
|
2056
|
+
* Prevents "all-time" scans on billion-row collections — the single
|
|
2057
|
+
* biggest performance footgun for live aggregation endpoints.
|
|
2058
|
+
*/
|
|
2059
|
+
interface AggregationDateRangeRequirement {
|
|
2060
|
+
/** Field whose range the caller must narrow (e.g. `'createdAt'`). */
|
|
2061
|
+
field: string;
|
|
2313
2062
|
/**
|
|
2314
|
-
*
|
|
2315
|
-
*
|
|
2063
|
+
* Cap on the queryable range. A request asking for >N days is
|
|
2064
|
+
* rejected 400. Omit for "any range, but bounded" (lower + upper
|
|
2065
|
+
* required, no cap).
|
|
2316
2066
|
*/
|
|
2317
|
-
|
|
2318
|
-
[key: string]: unknown;
|
|
2319
|
-
}
|
|
2320
|
-
type CrudRouteKey = "list" | "get" | "create" | "update" | "delete";
|
|
2321
|
-
interface MiddlewareConfig {
|
|
2322
|
-
list?: MiddlewareHandler[];
|
|
2323
|
-
get?: MiddlewareHandler[];
|
|
2324
|
-
create?: MiddlewareHandler[];
|
|
2325
|
-
update?: MiddlewareHandler[];
|
|
2326
|
-
delete?: MiddlewareHandler[];
|
|
2327
|
-
[key: string]: MiddlewareHandler[] | undefined;
|
|
2067
|
+
maxRangeDays?: number;
|
|
2328
2068
|
}
|
|
2329
|
-
/**
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2069
|
+
/**
|
|
2070
|
+
* Boot-time index hint. Arc warns when the kit's schema doesn't have
|
|
2071
|
+
* an index whose leading keys match — flags the misconfig before
|
|
2072
|
+
* traffic hits the DB.
|
|
2073
|
+
*
|
|
2074
|
+
* Documented intent, NOT runtime-enforced. Kits with their own index
|
|
2075
|
+
* introspection (mongokit reads Mongoose schema, sqlitekit reads
|
|
2076
|
+
* Drizzle indexes) can act on the hint; kits without introspection
|
|
2077
|
+
* silently accept it.
|
|
2078
|
+
*/
|
|
2079
|
+
interface AggregationIndexHint {
|
|
2080
|
+
/** Leading-key columns the host expects the planner to use. */
|
|
2081
|
+
leadingKeys: readonly string[];
|
|
2342
2082
|
}
|
|
2343
2083
|
/**
|
|
2344
|
-
*
|
|
2084
|
+
* Runtime context passed to the `materialized` hook.
|
|
2345
2085
|
*
|
|
2346
|
-
*
|
|
2347
|
-
*
|
|
2348
|
-
*
|
|
2086
|
+
* The hook returns pre-computed data instead of running the live
|
|
2087
|
+
* aggregation. Hosts use this for ultra-frequent dashboards backed by
|
|
2088
|
+
* rollup tables maintained out-of-band (cron / CDC).
|
|
2349
2089
|
*/
|
|
2350
|
-
interface
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2090
|
+
interface AggregationMaterializedContext {
|
|
2091
|
+
/** Compiled filter (host base + tenant + caller). */
|
|
2092
|
+
filter: AnyRecord;
|
|
2093
|
+
/** Tenant id when the resource is tenant-scoped. */
|
|
2094
|
+
orgId?: string;
|
|
2095
|
+
/** Authenticated user id, when present. */
|
|
2096
|
+
userId?: string;
|
|
2097
|
+
/** Fastify request id for tracing. */
|
|
2098
|
+
requestId?: string;
|
|
2099
|
+
/** Raw URL query params (post-validation). */
|
|
2100
|
+
query: Record<string, unknown>;
|
|
2101
|
+
}
|
|
2102
|
+
/** Materialized hook return shape — same envelope as `AggResult`. */
|
|
2103
|
+
interface AggregationMaterializedResult<TRow = AnyRecord> {
|
|
2104
|
+
rows: readonly TRow[];
|
|
2105
|
+
}
|
|
2106
|
+
/**
|
|
2107
|
+
* Single named aggregation declaration. Composes into `AggRequest` at
|
|
2108
|
+
* request time, with safety knobs layered on at the arc-handler level.
|
|
2109
|
+
*/
|
|
2110
|
+
interface AggregationConfig {
|
|
2354
2111
|
/**
|
|
2355
|
-
*
|
|
2356
|
-
*
|
|
2357
|
-
* -
|
|
2358
|
-
* - Function with `raw: true`: raw Fastify handler `(request, reply)`
|
|
2359
|
-
*
|
|
2360
|
-
* Prefer `controllerMethod` (2.16) over the string form when you want the
|
|
2361
|
-
* compiler to catch typos. `handler` stays optional when `controllerMethod`
|
|
2362
|
-
* is set — exactly one of the two must be declared.
|
|
2112
|
+
* Pre-aggregate filter on the BASE rows (before lookups). Always
|
|
2113
|
+
* ANDed with auto-injected tenant scope + caller URL-narrowing
|
|
2114
|
+
* filters. Use for host-defined invariants (e.g. `archived: false`).
|
|
2363
2115
|
*/
|
|
2364
|
-
|
|
2116
|
+
filter?: AnyRecord;
|
|
2365
2117
|
/**
|
|
2366
|
-
*
|
|
2367
|
-
*
|
|
2368
|
-
*
|
|
2369
|
-
* this over the string `handler` form for any host that uses a typed
|
|
2370
|
-
* controller subclass.
|
|
2118
|
+
* Cross-table joins. Each `LookupSpec` reuses the IR
|
|
2119
|
+
* `@classytic/repo-core/lookup` defines for `lookupPopulate()`.
|
|
2120
|
+
* Same compile path the kit already ships.
|
|
2371
2121
|
*
|
|
2372
|
-
*
|
|
2373
|
-
*
|
|
2374
|
-
*
|
|
2122
|
+
* **Kit support is incremental.** A kit's `aggregate()` may not yet
|
|
2123
|
+
* compile lookups — boot validation against the adapter version
|
|
2124
|
+
* surfaces this loud, so hosts pin the kit major they need.
|
|
2125
|
+
*/
|
|
2126
|
+
lookups?: readonly LookupSpec[];
|
|
2127
|
+
/**
|
|
2128
|
+
* Group key(s). Dotted paths into joined aliases supported when
|
|
2129
|
+
* `lookups` is set: `'category.parent'` groups by the joined
|
|
2130
|
+
* `category` row's `parent` field.
|
|
2131
|
+
*/
|
|
2132
|
+
groupBy?: string | readonly string[];
|
|
2133
|
+
/**
|
|
2134
|
+
* Time-bucket group keys for time-series aggregations. Each entry
|
|
2135
|
+
* promotes a date column into a synthetic group key bucketed at
|
|
2136
|
+
* the chosen interval. The map key becomes a column on the output
|
|
2137
|
+
* row holding the canonical ISO-shaped bucket label
|
|
2138
|
+
* (`'2026-04'` for month, `'2026-W15'` for ISO week, etc.).
|
|
2375
2139
|
*
|
|
2376
|
-
*
|
|
2377
|
-
*
|
|
2378
|
-
*
|
|
2379
|
-
* `ControllerHandler` for pipeline routes, a raw Fastify handler when
|
|
2380
|
-
* the route is `raw: true`.
|
|
2140
|
+
* Bucketed keys participate in grouping the same way `groupBy`
|
|
2141
|
+
* columns do — `sort: { month: 1 }`, `having`, pagination, and
|
|
2142
|
+
* `topN.partitionBy` all treat them as first-class.
|
|
2381
2143
|
*
|
|
2382
|
-
*
|
|
2383
|
-
*
|
|
2384
|
-
* controller shape — host code annotates the parameter to opt in:
|
|
2144
|
+
* Aliases must NOT collide with a `groupBy` field name or measure
|
|
2145
|
+
* alias — boot validation throws on collision.
|
|
2385
2146
|
*
|
|
2386
|
-
* @example
|
|
2147
|
+
* @example "Daily revenue for the last quarter"
|
|
2387
2148
|
* ```ts
|
|
2388
|
-
*
|
|
2389
|
-
*
|
|
2390
|
-
* }
|
|
2149
|
+
* dailyRevenue: defineAggregation({
|
|
2150
|
+
* dateBuckets: { day: { field: 'createdAt', interval: 'day' } },
|
|
2151
|
+
* measures: { revenue: 'sum:totalPrice' },
|
|
2152
|
+
* sort: { day: 1 },
|
|
2153
|
+
* permissions: requireRoles(['admin']),
|
|
2154
|
+
* }),
|
|
2155
|
+
* ```
|
|
2391
2156
|
*
|
|
2392
|
-
*
|
|
2393
|
-
*
|
|
2394
|
-
*
|
|
2395
|
-
*
|
|
2396
|
-
*
|
|
2397
|
-
*
|
|
2398
|
-
*
|
|
2399
|
-
*
|
|
2400
|
-
*
|
|
2401
|
-
* });
|
|
2157
|
+
* @example "15-minute traffic buckets (custom-bin form)"
|
|
2158
|
+
* ```ts
|
|
2159
|
+
* traffic: defineAggregation({
|
|
2160
|
+
* dateBuckets: {
|
|
2161
|
+
* slot: { field: 'ts', interval: { every: 15, unit: 'minute' } },
|
|
2162
|
+
* },
|
|
2163
|
+
* measures: { hits: 'count' },
|
|
2164
|
+
* permissions: allowPublic(),
|
|
2165
|
+
* }),
|
|
2402
2166
|
* ```
|
|
2403
2167
|
*/
|
|
2404
|
-
|
|
2405
|
-
/**
|
|
2406
|
-
|
|
2168
|
+
dateBuckets?: Record<string, AggDateBucket>;
|
|
2169
|
+
/** Named aggregations. At least one entry required. */
|
|
2170
|
+
measures: Record<string, AggMeasureInput>;
|
|
2171
|
+
/**
|
|
2172
|
+
* Post-aggregate filter referencing measure aliases.
|
|
2173
|
+
* Example: `{ revenue: { gt: 1000 } }` → `HAVING revenue > 1000`.
|
|
2174
|
+
*/
|
|
2175
|
+
having?: AnyRecord;
|
|
2176
|
+
/** Order grouped rows by groupBy field, measure alias, or joined-alias path. */
|
|
2177
|
+
sort?: Record<string, 1 | -1>;
|
|
2178
|
+
/**
|
|
2179
|
+
* Static hard cap on result rows. Always applied at the IR level
|
|
2180
|
+
* (LIMIT / `$limit`), regardless of URL params. Use when the
|
|
2181
|
+
* aggregation has a fixed-size shape ("top 10 categories"). Mutually
|
|
2182
|
+
* exclusive with `defaultLimit` / `maxLimit` — pick the static form
|
|
2183
|
+
* OR the URL-driven form, not both.
|
|
2184
|
+
*/
|
|
2185
|
+
limit?: number;
|
|
2186
|
+
/**
|
|
2187
|
+
* URL-driven row limit — applied when the caller passes `?limit=N`.
|
|
2188
|
+
* When set, the auto-route reads `query.limit`, parses it as a
|
|
2189
|
+
* positive integer, caps it at `maxLimit` (or the framework default
|
|
2190
|
+
* if `maxLimit` is unset), and writes the result into `AggRequest.limit`.
|
|
2191
|
+
* When the caller omits `?limit`, this default applies.
|
|
2192
|
+
*
|
|
2193
|
+
* **DoS defense.** Auto-generated `/aggregations/:name` routes today
|
|
2194
|
+
* have no upper bound on response size. A `groupBy: 'userId'` on a
|
|
2195
|
+
* 10M-user table without `defaultLimit` returns a 10M-row JSON. Setting
|
|
2196
|
+
* `defaultLimit: 100, maxLimit: 1000` bounds the response shape while
|
|
2197
|
+
* letting power users request up to 1000 rows when they need them.
|
|
2198
|
+
*
|
|
2199
|
+
* Mutually exclusive with the static `limit` — boot error if both set.
|
|
2200
|
+
* Setting `defaultLimit` without `maxLimit` uses the framework
|
|
2201
|
+
* default cap (`AGG_FRAMEWORK_MAX_LIMIT = 1000`) — explicit is better.
|
|
2202
|
+
*/
|
|
2203
|
+
defaultLimit?: number;
|
|
2407
2204
|
/**
|
|
2408
|
-
*
|
|
2409
|
-
*
|
|
2205
|
+
* Hard ceiling for URL-driven `?limit=N`. Only meaningful when
|
|
2206
|
+
* `defaultLimit` is set; ignored otherwise. Defaults to
|
|
2207
|
+
* `AGG_FRAMEWORK_MAX_LIMIT` (1000) when omitted but `defaultLimit` is
|
|
2208
|
+
* present. Boot validation: must be ≥ `defaultLimit`.
|
|
2410
2209
|
*/
|
|
2411
|
-
|
|
2412
|
-
/** Logical operation name (pipeline keys, MCP tool naming). */
|
|
2413
|
-
readonly operation?: string;
|
|
2414
|
-
/** OpenAPI summary */
|
|
2415
|
-
readonly summary?: string;
|
|
2416
|
-
/** OpenAPI description */
|
|
2417
|
-
readonly description?: string;
|
|
2418
|
-
/** OpenAPI tags */
|
|
2419
|
-
readonly tags?: string[];
|
|
2420
|
-
/** Route-level middleware */
|
|
2421
|
-
readonly preHandler?: RouteHandlerMethod[] | ((fastify: FastifyInstance) => RouteHandlerMethod[]);
|
|
2422
|
-
/** Pre-auth handlers (run before authentication) */
|
|
2423
|
-
readonly preAuth?: RouteHandlerMethod[];
|
|
2210
|
+
maxLimit?: number;
|
|
2424
2211
|
/**
|
|
2425
|
-
*
|
|
2426
|
-
*
|
|
2427
|
-
*
|
|
2428
|
-
* `flexibleMultiTenantPreset`), arc prepends the preset's tenant
|
|
2429
|
-
* filter + injection middleware to this route's chain — the same
|
|
2430
|
-
* pair `update` gets. The handler can then read the resolved tenant
|
|
2431
|
-
* via `getOrgId(req.scope)` / `req._tenantFields` (or, for write
|
|
2432
|
-
* routes, off `req.body` after injection) without re-implementing
|
|
2433
|
-
* the scope read + header fallback + 400 boilerplate.
|
|
2212
|
+
* Top-N-per-group filter. Keeps only the top `limit` rows per
|
|
2213
|
+
* partition, ranked by `sortBy`. The classic "top 3 products per
|
|
2214
|
+
* category" / "top 5 customers per region" dashboard primitive.
|
|
2434
2215
|
*
|
|
2435
|
-
*
|
|
2436
|
-
*
|
|
2437
|
-
*
|
|
2216
|
+
* Composes with `having` / `sort` — applies AFTER group + measures +
|
|
2217
|
+
* having, so `partitionBy` and `sortBy` may reference groupBy fields,
|
|
2218
|
+
* dateBucket aliases, or measure aliases. The top-level `sort` orders
|
|
2219
|
+
* the final row set across partitions.
|
|
2438
2220
|
*
|
|
2439
|
-
*
|
|
2221
|
+
* **Per-kit support.** Mongokit compiles to `$setWindowFields` (Mongo 5+,
|
|
2222
|
+
* runs in-engine — scales). Sqlitekit post-processes in JS (fine for
|
|
2223
|
+
* typical dashboards; prefer mongokit for >100k groups). See
|
|
2224
|
+
* `AggTopN` for full semantics.
|
|
2440
2225
|
*/
|
|
2441
|
-
|
|
2226
|
+
topN?: AggTopN;
|
|
2442
2227
|
/**
|
|
2443
|
-
*
|
|
2444
|
-
*
|
|
2445
|
-
*
|
|
2446
|
-
*
|
|
2447
|
-
* `fetch().body`, etc.). 2.17.1+ auto-pipes these through
|
|
2448
|
-
* `pipeUIMessageStreamToReply()` so each chunk is JSON-encoded into
|
|
2449
|
-
* an SSE `data:` frame and client-disconnect cancels the source.
|
|
2450
|
-
* Pre-2.17.1 this crashed with `chunk must be a string or Buffer`.
|
|
2228
|
+
* Permission check. **REQUIRED.** Aggregations are read-shape but
|
|
2229
|
+
* different from list (different threat model — measures may expose
|
|
2230
|
+
* cardinality info even when individual rows are hidden). Boot error
|
|
2231
|
+
* if missing.
|
|
2451
2232
|
*/
|
|
2452
|
-
|
|
2233
|
+
permissions: PermissionCheck;
|
|
2453
2234
|
/**
|
|
2454
|
-
*
|
|
2455
|
-
*
|
|
2456
|
-
*
|
|
2457
|
-
* source ("preset:softDelete vs user `actions.restore`") and to
|
|
2458
|
-
* auto-namespace the preset side when a user-authored tool legitimately
|
|
2459
|
-
* shadows it. Not part of the public surface — hosts must not set this.
|
|
2460
|
-
*
|
|
2461
|
-
* @internal
|
|
2235
|
+
* DB-level execution cap (ms). Mongokit threads to `maxTimeMS`;
|
|
2236
|
+
* sqlitekit threads to per-statement timeout where supported.
|
|
2237
|
+
* Default: kit's default (typically none).
|
|
2462
2238
|
*/
|
|
2463
|
-
|
|
2239
|
+
timeout?: number;
|
|
2464
2240
|
/**
|
|
2465
|
-
*
|
|
2466
|
-
*
|
|
2467
|
-
*
|
|
2468
|
-
* Default: `true` for body-bearing methods (POST/PUT/PATCH), `false`
|
|
2469
|
-
* otherwise. Set to `false` to opt out — handler accepts the body as
|
|
2470
|
-
* supplied with no field-write filtering. The resource-level
|
|
2471
|
-
* `onFieldWriteDenied: 'reject' | 'strip'` setting governs how denials
|
|
2472
|
-
* are surfaced.
|
|
2473
|
-
*
|
|
2474
|
-
* `raw: true` routes ALWAYS bypass this — they've opted out of the
|
|
2475
|
-
* pipeline entirely.
|
|
2241
|
+
* Reject 422 if the result row count exceeds this cap. Better than
|
|
2242
|
+
* silent truncation — caller knows the dashboard is incomplete.
|
|
2243
|
+
* Default: no cap (use `limit` for truncation semantics).
|
|
2476
2244
|
*/
|
|
2477
|
-
|
|
2245
|
+
maxGroups?: number;
|
|
2478
2246
|
/**
|
|
2479
|
-
*
|
|
2480
|
-
*
|
|
2481
|
-
*
|
|
2482
|
-
* - omitted → inherit the resource-level limit (or app default)
|
|
2483
|
-
* - `false` → disable rate limiting on this route
|
|
2484
|
-
* - `{ max, timeWindow }` → apply this specific limit
|
|
2485
|
-
*
|
|
2486
|
-
* Requires `@fastify/rate-limit` registered (arc's factory wires it by
|
|
2487
|
-
* default). Use for an expensive `/export` or an auth-sensitive `/reset`
|
|
2488
|
-
* that needs a tighter cap than the resource's read-heavy CRUD, or `false`
|
|
2489
|
-
* on an internal health/webhook route that must not be throttled.
|
|
2490
|
-
*
|
|
2491
|
-
* Note: this is per-route because each custom route is a distinct endpoint.
|
|
2492
|
-
* `actions` share ONE `POST /:id/action` mount by design, so a per-ACTION
|
|
2493
|
-
* limit can't be expressed here — throttle a specific action by promoting
|
|
2494
|
-
* it to a dedicated `routes:` entry, or rely on the resource-level limit.
|
|
2247
|
+
* Caller MUST provide filters on these fields (else 400 at request).
|
|
2248
|
+
* Use to require a tenant-side narrowing the host can't infer
|
|
2249
|
+
* (segment id, customer id, etc.).
|
|
2495
2250
|
*/
|
|
2496
|
-
|
|
2251
|
+
requireFilters?: readonly string[];
|
|
2497
2252
|
/**
|
|
2498
|
-
*
|
|
2499
|
-
*
|
|
2500
|
-
* **or** a Zod v4 schema — Arc auto-converts via `convertRouteSchema`.
|
|
2253
|
+
* Caller MUST send a bounded date range on this field. Prevents
|
|
2254
|
+
* all-time scans on billion-row collections.
|
|
2501
2255
|
*/
|
|
2502
|
-
|
|
2503
|
-
body?: unknown;
|
|
2504
|
-
querystring?: unknown;
|
|
2505
|
-
params?: unknown;
|
|
2506
|
-
headers?: unknown;
|
|
2507
|
-
response?: Record<number | string, unknown>;
|
|
2508
|
-
[key: string]: unknown;
|
|
2509
|
-
};
|
|
2256
|
+
requireDateRange?: AggregationDateRangeRequirement;
|
|
2510
2257
|
/**
|
|
2511
|
-
*
|
|
2512
|
-
*
|
|
2513
|
-
* -
|
|
2514
|
-
* - object: explicit config
|
|
2258
|
+
* Documented index expectation. Arc warns at boot when the kit
|
|
2259
|
+
* exposes index introspection and no matching index exists. NOT
|
|
2260
|
+
* runtime-enforced — purely a misconfig signal.
|
|
2515
2261
|
*/
|
|
2516
|
-
|
|
2262
|
+
indexHint?: AggregationIndexHint;
|
|
2517
2263
|
/**
|
|
2518
|
-
*
|
|
2519
|
-
*
|
|
2264
|
+
* Per-aggregation cache. Tenant-scoped keys; invalidates with the
|
|
2265
|
+
* resource's cache namespace + any explicit `tags`.
|
|
2520
2266
|
*/
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2267
|
+
cache?: AggregationCacheConfig;
|
|
2268
|
+
/**
|
|
2269
|
+
* Per-route rate limit. Wired to `@fastify/rate-limit` when the
|
|
2270
|
+
* plugin is registered.
|
|
2271
|
+
*/
|
|
2272
|
+
rateLimit?: AggregationRateLimit;
|
|
2273
|
+
/**
|
|
2274
|
+
* Pre-computed read replacement. When set, arc skips
|
|
2275
|
+
* `repo.aggregate()` and calls this function instead. Same wire
|
|
2276
|
+
* shape, same permissions, different data source.
|
|
2277
|
+
*
|
|
2278
|
+
* Use for homepage-counter dashboards backed by host-managed rollup
|
|
2279
|
+
* tables (cron / CDC). The hook receives the compiled context
|
|
2280
|
+
* (filter + scope) so the host can route the lookup to the right
|
|
2281
|
+
* pre-aggregated bucket.
|
|
2282
|
+
*/
|
|
2283
|
+
materialized?: (ctx: AggregationMaterializedContext) => Promise<AggregationMaterializedResult>;
|
|
2284
|
+
/** Optional summary rendered in OpenAPI + MCP tool description. */
|
|
2285
|
+
summary?: string;
|
|
2286
|
+
/** Optional longer description for OpenAPI / MCP. */
|
|
2287
|
+
description?: string;
|
|
2288
|
+
/**
|
|
2289
|
+
* MCP tool generation control. Mirrors `actions[name].mcp` semantics.
|
|
2290
|
+
*
|
|
2291
|
+
* - `undefined` (default) — generate an MCP tool for this aggregation
|
|
2292
|
+
* - `false` — skip MCP tool generation (REST route still works)
|
|
2293
|
+
* - `{ description?, annotations? }` — generate with overrides
|
|
2294
|
+
*/
|
|
2295
|
+
mcp?: boolean | {
|
|
2296
|
+
readonly description?: string;
|
|
2297
|
+
readonly annotations?: Record<string, unknown>;
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
/** Map of name → declaration. Keys become URL segments under `/aggregations/<name>`. */
|
|
2301
|
+
type AggregationsMap = Record<string, AggregationConfig>;
|
|
2302
|
+
//#endregion
|
|
2303
|
+
//#region src/types/resource/events.d.ts
|
|
2304
|
+
/**
|
|
2305
|
+
* Domain-event declaration on a resource — name, optional handler, and
|
|
2306
|
+
* payload schema for the `events:` block of a resource definition.
|
|
2307
|
+
*/
|
|
2308
|
+
interface EventDefinition {
|
|
2309
|
+
name: string;
|
|
2310
|
+
/** Optional handler — events are published via `fastify.events.publish()`. */
|
|
2311
|
+
handler?: (data: unknown) => Promise<void> | void;
|
|
2312
|
+
/**
|
|
2313
|
+
* JSON Schema or Zod v4 schema for event payload. Typed `unknown` so Zod
|
|
2314
|
+
* class instances assign without a cast (same convention as
|
|
2315
|
+
* `ActionDefinition.schema` and `RouteDefinition.schema`).
|
|
2316
|
+
*/
|
|
2317
|
+
schema?: unknown;
|
|
2318
|
+
description?: string;
|
|
2528
2319
|
}
|
|
2320
|
+
//#endregion
|
|
2321
|
+
//#region src/types/resource/extensions.d.ts
|
|
2529
2322
|
/**
|
|
2530
|
-
*
|
|
2531
|
-
*
|
|
2323
|
+
* `ResourceExtensions` — the typed plugin-extension namespace plugins
|
|
2324
|
+
* augment via declaration merging (`declare module "@classytic/arc/types"`).
|
|
2532
2325
|
*/
|
|
2533
|
-
type ActionHandlerFn = (id: string, data: Record<string, unknown>, req: RequestWithExtras) => Promise<unknown>;
|
|
2534
2326
|
/**
|
|
2535
|
-
*
|
|
2327
|
+
* Plugin extension namespace — arc's typed escape hatch for attaching
|
|
2328
|
+
* declarative, per-resource config that **plugins** read at request time.
|
|
2536
2329
|
*
|
|
2537
|
-
*
|
|
2538
|
-
* `
|
|
2539
|
-
*
|
|
2540
|
-
*
|
|
2541
|
-
*
|
|
2330
|
+
* **External** plugins augment it via TypeScript declaration merging
|
|
2331
|
+
* (`declare module "@classytic/arc/types"`) to register their own typed
|
|
2332
|
+
* slice — exactly the model Fastify uses for `FastifyInstance` decorators.
|
|
2333
|
+
* The host gets full autocomplete + compile-time checking on the matching
|
|
2334
|
+
* `extensions` block, and a typo (`extensions: { encyrption: … }`) is a
|
|
2335
|
+
* type error rather than a silent no-op.
|
|
2336
|
+
*
|
|
2337
|
+
* **First-party** slices (arc's own subpath plugins) are declared inline
|
|
2338
|
+
* below with type-only imports instead of `declare module`. Not a style
|
|
2339
|
+
* choice: arc bundles its declarations, and a relative module augmentation
|
|
2340
|
+
* (`declare module "../types/resource.js"`) survives bundling as an
|
|
2341
|
+
* unresolvable specifier — the merge silently never happens for consumers,
|
|
2342
|
+
* so the key would be a compile ERROR downstream while working inside this
|
|
2343
|
+
* repo. Inline declaration is bundler-proof; the type-only import is erased
|
|
2344
|
+
* at runtime, so the plugin stays fully opt-in.
|
|
2345
|
+
*
|
|
2346
|
+
* **The "React for backend" composition model.** `defineResource()` is the
|
|
2347
|
+
* declarative component, `extensions` is its typed props bag, and plugins
|
|
2348
|
+
* are the providers that consume those props at the framework boundary.
|
|
2349
|
+
* Adding a cross-cutting capability becomes *additive* — a plugin ships a
|
|
2350
|
+
* subpath + a one-line `declare module` augmentation, with no new
|
|
2351
|
+
* first-class `ResourceConfig` field and no core release per feature.
|
|
2352
|
+
*
|
|
2353
|
+
* Arc threads the declared `extensions` onto every generated route's
|
|
2354
|
+
* Fastify `config` (see `createCrudRouter`), so a plugin reads its slice
|
|
2355
|
+
* at request time via `request.routeOptions.config.arcExtensions` without
|
|
2356
|
+
* re-deriving which resource a route belongs to.
|
|
2357
|
+
*
|
|
2358
|
+
* @example An external plugin package registers its slice
|
|
2359
|
+
* ```ts
|
|
2360
|
+
* declare module "@classytic/arc/types" {
|
|
2361
|
+
* interface ResourceExtensions {
|
|
2362
|
+
* myPlugin?: import("my-arc-plugin").MyPluginDirective;
|
|
2363
|
+
* }
|
|
2364
|
+
* }
|
|
2365
|
+
* ```
|
|
2366
|
+
*
|
|
2367
|
+
* @example A host declares it on a resource
|
|
2368
|
+
* ```ts
|
|
2369
|
+
* defineResource({
|
|
2370
|
+
* name: "payment",
|
|
2371
|
+
* extensions: { encryption: { mode: "fields", fields: ["cardNumber", "cvv"] } },
|
|
2372
|
+
* });
|
|
2373
|
+
* ```
|
|
2542
2374
|
*/
|
|
2543
|
-
interface
|
|
2544
|
-
readonly handler: ActionHandlerFn;
|
|
2545
|
-
/** Per-action permission (overrides resource-level `actionPermissions`) */
|
|
2546
|
-
readonly permissions?: PermissionCheck;
|
|
2547
|
-
/**
|
|
2548
|
-
* JSON Schema or Zod v4 schema for action-specific body fields.
|
|
2549
|
-
*
|
|
2550
|
-
* Typed `unknown` (not `Record<string, unknown>`) so Zod class instances
|
|
2551
|
-
* — `ZodObject<...>` carries no string index signature — assign without
|
|
2552
|
-
* a cast. Same convention as `RouteDefinition.schema.body` / `customSchemas`.
|
|
2553
|
-
* Runtime feature-detects via `convertRouteSchema` / `toJsonSchema`.
|
|
2554
|
-
*/
|
|
2555
|
-
readonly schema?: unknown;
|
|
2556
|
-
/** Description for OpenAPI docs and MCP tool */
|
|
2557
|
-
readonly description?: string;
|
|
2558
|
-
/**
|
|
2559
|
-
* Whether this action needs an entity id from the URL.
|
|
2560
|
-
*
|
|
2561
|
-
* - `true` (default) — mounts under `POST /<prefix>/:id/action`. The `id`
|
|
2562
|
-
* path param is required; handler receives it as the first argument.
|
|
2563
|
-
* Use for actions that operate on an existing entity: `approve`,
|
|
2564
|
-
* `dispatch`, `cancel`, `archive`.
|
|
2565
|
-
* - `false` — mounts under `POST /<prefix>/action` (no `:id` segment).
|
|
2566
|
-
* Handler receives an empty-string first argument. Use for
|
|
2567
|
-
* collection-level actions that create / search / bulk-mutate:
|
|
2568
|
-
* `propose` (creates a new entity), `search` (returns rows), `bulk`.
|
|
2569
|
-
*
|
|
2570
|
-
* 2.15.5: previously every action had to live under `:id/action`, forcing
|
|
2571
|
-
* `propose`-style actions to swallow a meaningless URL parameter and
|
|
2572
|
-
* making the auto-generated MCP tool advertise an `id` field agents
|
|
2573
|
-
* had no value for. Setting `id: false` mounts the action at the
|
|
2574
|
-
* resource root and drops `id` from the MCP tool's input shape.
|
|
2575
|
-
*/
|
|
2576
|
-
readonly id?: boolean;
|
|
2375
|
+
interface ResourceExtensions {
|
|
2577
2376
|
/**
|
|
2578
|
-
*
|
|
2579
|
-
*
|
|
2580
|
-
*
|
|
2581
|
-
* -
|
|
2377
|
+
* Encrypt this resource's responses — full-body JWE or specific fields.
|
|
2378
|
+
* Read at request time by `@classytic/arc/encryption`'s `encryptionPlugin`
|
|
2379
|
+
* from `request.routeOptions.config.arcExtensions.encryption`; inert (and
|
|
2380
|
+
* dependency-free — the import is type-only) unless that plugin is
|
|
2381
|
+
* registered.
|
|
2582
2382
|
*/
|
|
2583
|
-
|
|
2383
|
+
encryption?: EncryptionDirective;
|
|
2584
2384
|
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
/** Actions configuration map. */
|
|
2588
|
-
type ActionsMap = Record<string, ActionEntry>;
|
|
2385
|
+
//#endregion
|
|
2386
|
+
//#region src/types/resource/hooks.d.ts
|
|
2589
2387
|
/**
|
|
2590
2388
|
* Hook context passed to resource-level hook handlers. Mirrors
|
|
2591
2389
|
* HookSystem's HookContext but with a simpler API for inline use.
|
|
@@ -2633,39 +2431,304 @@ interface ResourceHookContext {
|
|
|
2633
2431
|
* identically-named field on `IRequestContext` (v2.10.6) so the same
|
|
2634
2432
|
* read pattern works in controllers and hooks.
|
|
2635
2433
|
*/
|
|
2636
|
-
scope?: {
|
|
2637
|
-
organizationId?: string;
|
|
2638
|
-
userId?: string;
|
|
2639
|
-
orgRoles?: string[];
|
|
2434
|
+
scope?: {
|
|
2435
|
+
organizationId?: string;
|
|
2436
|
+
userId?: string;
|
|
2437
|
+
orgRoles?: string[];
|
|
2438
|
+
};
|
|
2439
|
+
/** Additional metadata (e.g. `{ id, existing }` for update/delete) */
|
|
2440
|
+
meta?: AnyRecord;
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Inline lifecycle hooks on a resource definition. Wired into the
|
|
2444
|
+
* HookSystem automatically — same pipeline as presets and app-level hooks.
|
|
2445
|
+
*
|
|
2446
|
+
* @example
|
|
2447
|
+
* ```typescript
|
|
2448
|
+
* defineResource({
|
|
2449
|
+
* name: 'chat',
|
|
2450
|
+
* hooks: {
|
|
2451
|
+
* afterCreate: async (ctx) => { analytics.track('chat.created', { id: ctx.data._id }); },
|
|
2452
|
+
* beforeDelete: async (ctx) => {
|
|
2453
|
+
* if (ctx.data.isProtected) throw new Error('Cannot delete protected chat');
|
|
2454
|
+
* },
|
|
2455
|
+
* },
|
|
2456
|
+
* });
|
|
2457
|
+
* ```
|
|
2458
|
+
*/
|
|
2459
|
+
interface ResourceHooks {
|
|
2460
|
+
beforeCreate?: (ctx: ResourceHookContext) => Promise<AnyRecord | undefined> | AnyRecord | undefined;
|
|
2461
|
+
afterCreate?: (ctx: ResourceHookContext) => Promise<void> | void;
|
|
2462
|
+
beforeUpdate?: (ctx: ResourceHookContext) => Promise<AnyRecord | undefined> | AnyRecord | undefined;
|
|
2463
|
+
afterUpdate?: (ctx: ResourceHookContext) => Promise<void> | void;
|
|
2464
|
+
beforeDelete?: (ctx: ResourceHookContext) => Promise<void> | void;
|
|
2465
|
+
afterDelete?: (ctx: ResourceHookContext) => Promise<void> | void;
|
|
2466
|
+
}
|
|
2467
|
+
//#endregion
|
|
2468
|
+
//#region src/types/resource/fields.d.ts
|
|
2469
|
+
/**
|
|
2470
|
+
* Per-field rule — arc's extension of repo-core's 4-field `FieldRule` floor
|
|
2471
|
+
* (`immutable`, `immutableAfterCreate`, `systemManaged`, `optional`) with
|
|
2472
|
+
* the constraint / UI / security bits arc layers on top.
|
|
2473
|
+
*
|
|
2474
|
+
* Kept structurally compatible with `@classytic/repo-core/schema`'s
|
|
2475
|
+
* `FieldRule` so arc's `fieldRules: Record<string, ArcFieldRule>` flows
|
|
2476
|
+
* into mongokit's / sqlitekit's `buildCrudSchemasFromModel(..., options)`
|
|
2477
|
+
* without a cast. See `RouteSchemaOptions` JSDoc for the full rationale.
|
|
2478
|
+
*/
|
|
2479
|
+
interface ArcFieldRule extends FieldRule {
|
|
2480
|
+
/**
|
|
2481
|
+
* When `true`, bypass the `systemManaged` / `readonly` / `immutable`
|
|
2482
|
+
* strip in `BodySanitizer` for callers whose request scope is
|
|
2483
|
+
* `elevated`. Lets platform admins stamp the value from the request
|
|
2484
|
+
* body — needed for cross-tenant admin writes where the tenant field
|
|
2485
|
+
* is the only way to pick a target org.
|
|
2486
|
+
*
|
|
2487
|
+
* Auto-set by `defineResource` on the configured `tenantField`. Hosts
|
|
2488
|
+
* can set it manually on other fields (e.g. `createdBy`) if they want
|
|
2489
|
+
* elevation-only override semantics for those too.
|
|
2490
|
+
*
|
|
2491
|
+
* Has no effect when `isElevated(scope)` is false — member and
|
|
2492
|
+
* service callers continue to have the field stripped.
|
|
2493
|
+
*/
|
|
2494
|
+
preserveForElevated?: boolean;
|
|
2495
|
+
hidden?: boolean;
|
|
2496
|
+
/**
|
|
2497
|
+
* Aggregation visibility override. By default, only `hidden: true`
|
|
2498
|
+
* blocks a field from `groupBy` / `measures.field` / `sort` / `dateBuckets`
|
|
2499
|
+
* — that's the genuine cardinality-leak guard (the value is omitted from
|
|
2500
|
+
* list/get responses, so exposing it via aggregation would reveal data
|
|
2501
|
+
* the client can't otherwise see).
|
|
2502
|
+
*
|
|
2503
|
+
* `systemManaged: true` does **not** block aggregation — it's a write
|
|
2504
|
+
* rule, not a visibility rule. Server-stamped fields like `createdAt`,
|
|
2505
|
+
* `status`, or plugin-generated handles are visible in every list
|
|
2506
|
+
* response and should aggregate freely.
|
|
2507
|
+
*
|
|
2508
|
+
* Use this flag to override the default:
|
|
2509
|
+
*
|
|
2510
|
+
* - `aggregable: false` — explicit deny, even on visible fields. Useful
|
|
2511
|
+
* when a value is exposed per-row but the cardinality across rows is
|
|
2512
|
+
* itself sensitive (e.g. `email` is visible in `get/:id` to admins
|
|
2513
|
+
* but you don't want a public-readable agg of email distributions).
|
|
2514
|
+
* - `aggregable: true` — escape hatch on `hidden` fields. Lets you
|
|
2515
|
+
* aggregate a hidden column when you're sure cardinality leakage
|
|
2516
|
+
* isn't a concern (e.g. `internalScore` hidden from list, but a
|
|
2517
|
+
* committee-only `byScore` agg is fine).
|
|
2518
|
+
*
|
|
2519
|
+
* Defaults to `undefined` (use the `hidden`-only rule).
|
|
2520
|
+
*/
|
|
2521
|
+
aggregable?: boolean;
|
|
2522
|
+
/** String minimum length — auto-maps to OpenAPI `minLength` and MCP tool schema */
|
|
2523
|
+
minLength?: number;
|
|
2524
|
+
/** String maximum length — auto-maps to OpenAPI `maxLength` and MCP tool schema */
|
|
2525
|
+
maxLength?: number;
|
|
2526
|
+
/** Number minimum — auto-maps to OpenAPI `minimum` and MCP tool schema */
|
|
2527
|
+
min?: number;
|
|
2528
|
+
/** Number maximum — auto-maps to OpenAPI `maximum` and MCP tool schema */
|
|
2529
|
+
max?: number;
|
|
2530
|
+
/** Regex pattern — auto-maps to OpenAPI `pattern` and MCP tool schema */
|
|
2531
|
+
pattern?: string;
|
|
2532
|
+
/** Allowed values — auto-maps to OpenAPI `enum` and MCP tool schema */
|
|
2533
|
+
enum?: ReadonlyArray<string | number>;
|
|
2534
|
+
/**
|
|
2535
|
+
* When `true`, widen the JSON Schema `type` of this field to also
|
|
2536
|
+
* accept `null`. Mirrors Zod's `.nullable()` at the arc config layer
|
|
2537
|
+
* for kit-generated schemas that don't carry the flag end-to-end
|
|
2538
|
+
* (e.g. Zod → Mongoose → mongokit drops `.nullable()` because
|
|
2539
|
+
* Mongoose has no first-class nullable marker unless `default: null`
|
|
2540
|
+
* is also set).
|
|
2541
|
+
*
|
|
2542
|
+
* Applied post-kit by `mergeFieldRuleConstraints`: if the adapter
|
|
2543
|
+
* emitted `{ type: 'string', enum: [...] }` for a field arc should
|
|
2544
|
+
* accept null for, the merge widens it to
|
|
2545
|
+
* `{ type: ['string', 'null'], enum: [...] }` — draft-7 tuple form
|
|
2546
|
+
* AJV 8 validates natively.
|
|
2547
|
+
*
|
|
2548
|
+
* No-op when the property already declares `type: [...,'null']` or
|
|
2549
|
+
* an `anyOf: [..., { type: 'null' }]` branch — arc never fights the
|
|
2550
|
+
* kit's own output.
|
|
2551
|
+
*/
|
|
2552
|
+
nullable?: boolean;
|
|
2553
|
+
/** Human-readable description — auto-maps to OpenAPI `description` */
|
|
2554
|
+
description?: string;
|
|
2555
|
+
[key: string]: unknown;
|
|
2556
|
+
}
|
|
2557
|
+
interface FieldRule$1 {
|
|
2558
|
+
field: string;
|
|
2559
|
+
required?: boolean;
|
|
2560
|
+
readonly?: boolean;
|
|
2561
|
+
hidden?: boolean;
|
|
2562
|
+
}
|
|
2563
|
+
//#endregion
|
|
2564
|
+
//#region src/types/resource/schemas.d.ts
|
|
2565
|
+
/**
|
|
2566
|
+
* Schema-shaping options for a resource.
|
|
2567
|
+
*
|
|
2568
|
+
* Extends `@classytic/repo-core/schema`'s `SchemaBuilderOptions` so every
|
|
2569
|
+
* kit-generator callback typed against the repo-core contract
|
|
2570
|
+
* (mongokit's `buildCrudSchemasFromModel`, sqlitekit's
|
|
2571
|
+
* `buildCrudSchemasFromTable`, prismakit's equivalent) accepts arc's
|
|
2572
|
+
* options bag directly — no `as SchemaBuilderOptions` / `Parameters<...>[1]`
|
|
2573
|
+
* cast at the host wiring site.
|
|
2574
|
+
*
|
|
2575
|
+
* Inherited from `SchemaBuilderOptions`:
|
|
2576
|
+
* - `strictAdditionalProperties` — emit `additionalProperties: false`
|
|
2577
|
+
* - `dateAs` — `'date'` vs `'datetime'` ISO rendering
|
|
2578
|
+
* - `softRequiredFields` — stay in `properties`, drop from `required[]`
|
|
2579
|
+
* - `create: { omitFields, requiredOverrides, optionalOverrides, schemaOverrides }`
|
|
2580
|
+
* - `update: { omitFields, requireAtLeastOne }`
|
|
2581
|
+
* - `query: { filterableFields }` (kit-native filter declaration)
|
|
2582
|
+
* - `openApiExtensions` — emit `x-*` vendor keywords for docgen
|
|
2583
|
+
*
|
|
2584
|
+
* Arc adds:
|
|
2585
|
+
* - `fieldRules` with the richer `ArcFieldRule` per-entry shape
|
|
2586
|
+
* (preserveForElevated, minLength/maxLength/min/max/pattern, enum,
|
|
2587
|
+
* nullable, description) — arc's extensions are applied post-kit by
|
|
2588
|
+
* `mergeFieldRuleConstraints`; the kit only sees the repo-core floor.
|
|
2589
|
+
* - `hiddenFields` / `readonlyFields` / `requiredFields` / `optionalFields`
|
|
2590
|
+
* / `excludeFields` — arc-only convenience lists that predate fieldRules.
|
|
2591
|
+
* Keep using them if they're already in place; new code should prefer
|
|
2592
|
+
* `fieldRules` for per-field control.
|
|
2593
|
+
* - `filterableFields: string[]` — top-level list arc's MCP layer auto-
|
|
2594
|
+
* derives from `QueryParser.allowedFilterFields`. Distinct from the
|
|
2595
|
+
* inherited `query.filterableFields: Record<...>` which feeds the kit's
|
|
2596
|
+
* list-query schema; nothing stops a resource from using both.
|
|
2597
|
+
*
|
|
2598
|
+
* **Why extend rather than duplicate**: mongokit's
|
|
2599
|
+
* `buildCrudSchemasFromModel(model, options: SchemaBuilderOptions)` is the
|
|
2600
|
+
* canonical callback shape. Before the extension, hosts wrote
|
|
2601
|
+
* `Parameters<typeof buildCrudSchemasFromModel>[1]` or
|
|
2602
|
+
* `as SchemaBuilderOptions` at every wiring site — a defensive cast with
|
|
2603
|
+
* no runtime effect. Extension locks the structural relationship at the
|
|
2604
|
+
* type layer so the cast is compile-verified gone.
|
|
2605
|
+
*/
|
|
2606
|
+
interface RouteSchemaOptions extends SchemaBuilderOptions {
|
|
2607
|
+
hiddenFields?: string[];
|
|
2608
|
+
readonlyFields?: string[];
|
|
2609
|
+
requiredFields?: string[];
|
|
2610
|
+
optionalFields?: string[];
|
|
2611
|
+
excludeFields?: string[];
|
|
2612
|
+
/**
|
|
2613
|
+
* Fields allowed for filtering in list operations. MCP auto-derives
|
|
2614
|
+
* from `QueryParser.allowedFilterFields` when not set explicitly.
|
|
2615
|
+
*
|
|
2616
|
+
* Distinct from the inherited `query.filterableFields: Record<...>`
|
|
2617
|
+
* from `SchemaBuilderOptions` — that entry feeds the kit's list-query
|
|
2618
|
+
* JSON Schema; this one is arc's MCP-auto-derivation list.
|
|
2619
|
+
*/
|
|
2620
|
+
filterableFields?: string[];
|
|
2621
|
+
/**
|
|
2622
|
+
* Per-field rules. Richer than repo-core's `FieldRules` — arc adds
|
|
2623
|
+
* `preserveForElevated`, constraint hints (`minLength`, `enum`,
|
|
2624
|
+
* `nullable`, etc.), and `description` on top of the four-flag floor
|
|
2625
|
+
* (`immutable`, `immutableAfterCreate`, `systemManaged`, `optional`).
|
|
2626
|
+
*
|
|
2627
|
+
* Structurally compatible: `Record<string, ArcFieldRule>` is assignable
|
|
2628
|
+
* to repo-core's `Record<string, FieldRule>` since `ArcFieldRule extends
|
|
2629
|
+
* FieldRule`. Kits see only the floor; arc's extensions are applied
|
|
2630
|
+
* post-kit by `mergeFieldRuleConstraints`.
|
|
2631
|
+
*/
|
|
2632
|
+
fieldRules?: Record<string, ArcFieldRule>;
|
|
2633
|
+
/**
|
|
2634
|
+
* Query-time security whitelists + the kit's `filterableFields`.
|
|
2635
|
+
*
|
|
2636
|
+
* Extends repo-core's `SchemaBuilderOptions['query']` with arc-specific
|
|
2637
|
+
* runtime features that `QueryResolver` reads at request time:
|
|
2638
|
+
*
|
|
2639
|
+
* - **`allowedPopulate`** — populate-path whitelist consumed by
|
|
2640
|
+
* `QueryResolver.sanitizePopulate` / `sanitizeAdvancedPopulate`.
|
|
2641
|
+
* When set, only paths in the list pass through; everything else is
|
|
2642
|
+
* stripped silently. Shrinks the auto-wired `?populate=` attack surface.
|
|
2643
|
+
*
|
|
2644
|
+
* - **`allowedLookups`** — lookup-collection whitelist consumed by
|
|
2645
|
+
* `QueryResolver.sanitizeLookups`. When set, only the listed
|
|
2646
|
+
* collections may be `$lookup`'d into the pipeline.
|
|
2647
|
+
*
|
|
2648
|
+
* Both are pre-2.11.2 runtime features — the type was missing them, so
|
|
2649
|
+
* hosts wrote `as Record<string, unknown>` at every call site. Arc's own
|
|
2650
|
+
* `QueryResolver` had to cast its own input via `as AnyRecord` for the
|
|
2651
|
+
* same reason. Adding the type dropped both casts.
|
|
2652
|
+
*/
|
|
2653
|
+
query?: SchemaBuilderOptions["query"] & {
|
|
2654
|
+
/**
|
|
2655
|
+
* Populate-path whitelist. When set, `QueryResolver.sanitizePopulate`
|
|
2656
|
+
* strips any `?populate=<path>` not in this list. Omit to allow all
|
|
2657
|
+
* paths the kit recognizes.
|
|
2658
|
+
*/
|
|
2659
|
+
allowedPopulate?: string[];
|
|
2660
|
+
/**
|
|
2661
|
+
* Lookup-collection whitelist for `QueryResolver.sanitizeLookups`.
|
|
2662
|
+
* When set, only the listed collections may be `$lookup`'d. Omit to
|
|
2663
|
+
* disable the whitelist (kit-level rules still apply).
|
|
2664
|
+
*/
|
|
2665
|
+
allowedLookups?: string[];
|
|
2640
2666
|
};
|
|
2641
|
-
/** Additional metadata (e.g. `{ id, existing }` for update/delete) */
|
|
2642
|
-
meta?: AnyRecord;
|
|
2643
2667
|
}
|
|
2644
2668
|
/**
|
|
2645
|
-
*
|
|
2646
|
-
*
|
|
2647
|
-
*
|
|
2648
|
-
*
|
|
2649
|
-
* ```typescript
|
|
2650
|
-
* defineResource({
|
|
2651
|
-
* name: 'chat',
|
|
2652
|
-
* hooks: {
|
|
2653
|
-
* afterCreate: async (ctx) => { analytics.track('chat.created', { id: ctx.data._id }); },
|
|
2654
|
-
* beforeDelete: async (ctx) => {
|
|
2655
|
-
* if (ctx.data.isProtected) throw new Error('Cannot delete protected chat');
|
|
2656
|
-
* },
|
|
2657
|
-
* },
|
|
2658
|
-
* });
|
|
2659
|
-
* ```
|
|
2669
|
+
* CRUD route schemas (Fastify native format). Each slot accepts a plain
|
|
2670
|
+
* JSON Schema object **or** a Zod v4 schema — Arc's `convertRouteSchema`
|
|
2671
|
+
* feature-detects at runtime. Slot values are typed `unknown` so
|
|
2672
|
+
* class-based Zod schemas assign without casts.
|
|
2660
2673
|
*/
|
|
2661
|
-
interface
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2674
|
+
interface CrudSchemas {
|
|
2675
|
+
/** GET / — list */
|
|
2676
|
+
list?: {
|
|
2677
|
+
querystring?: unknown;
|
|
2678
|
+
response?: Record<number, unknown>;
|
|
2679
|
+
[key: string]: unknown;
|
|
2680
|
+
};
|
|
2681
|
+
/** GET /:id — get one */
|
|
2682
|
+
get?: {
|
|
2683
|
+
params?: unknown;
|
|
2684
|
+
response?: Record<number, unknown>;
|
|
2685
|
+
[key: string]: unknown;
|
|
2686
|
+
};
|
|
2687
|
+
/** POST / — create */
|
|
2688
|
+
create?: {
|
|
2689
|
+
body?: unknown;
|
|
2690
|
+
response?: Record<number, unknown>;
|
|
2691
|
+
[key: string]: unknown;
|
|
2692
|
+
};
|
|
2693
|
+
/** PATCH /:id — update */
|
|
2694
|
+
update?: {
|
|
2695
|
+
params?: unknown;
|
|
2696
|
+
body?: unknown;
|
|
2697
|
+
response?: Record<number, unknown>;
|
|
2698
|
+
[key: string]: unknown;
|
|
2699
|
+
};
|
|
2700
|
+
/** DELETE /:id — delete */
|
|
2701
|
+
delete?: {
|
|
2702
|
+
params?: unknown;
|
|
2703
|
+
response?: Record<number, unknown>;
|
|
2704
|
+
[key: string]: unknown;
|
|
2705
|
+
};
|
|
2706
|
+
[key: string]: unknown;
|
|
2668
2707
|
}
|
|
2708
|
+
interface OpenApiSchemas {
|
|
2709
|
+
entity?: unknown;
|
|
2710
|
+
createBody?: unknown;
|
|
2711
|
+
updateBody?: unknown;
|
|
2712
|
+
params?: unknown;
|
|
2713
|
+
listQuery?: unknown;
|
|
2714
|
+
/**
|
|
2715
|
+
* Explicit response schema for OpenAPI documentation. Auto-generated
|
|
2716
|
+
* from `createBody` if omitted. Does NOT affect Fastify serialization.
|
|
2717
|
+
*/
|
|
2718
|
+
response?: unknown;
|
|
2719
|
+
[key: string]: unknown;
|
|
2720
|
+
}
|
|
2721
|
+
type CrudRouteKey = "list" | "get" | "create" | "update" | "delete";
|
|
2722
|
+
interface MiddlewareConfig {
|
|
2723
|
+
list?: MiddlewareHandler[];
|
|
2724
|
+
get?: MiddlewareHandler[];
|
|
2725
|
+
create?: MiddlewareHandler[];
|
|
2726
|
+
update?: MiddlewareHandler[];
|
|
2727
|
+
delete?: MiddlewareHandler[];
|
|
2728
|
+
[key: string]: MiddlewareHandler[] | undefined;
|
|
2729
|
+
}
|
|
2730
|
+
//#endregion
|
|
2731
|
+
//#region src/types/resource/presets.d.ts
|
|
2669
2732
|
interface PresetHook {
|
|
2670
2733
|
operation: "create" | "update" | "delete" | "read" | "list";
|
|
2671
2734
|
phase: "before" | "after";
|
|
@@ -2682,18 +2745,68 @@ interface PresetResult {
|
|
|
2682
2745
|
hooks?: PresetHook[];
|
|
2683
2746
|
}
|
|
2684
2747
|
type PresetFunction = (config: ResourceConfig) => PresetResult;
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2748
|
+
//#endregion
|
|
2749
|
+
//#region src/types/resource/tenant.d.ts
|
|
2750
|
+
/**
|
|
2751
|
+
* Tenant-cleanup declaration on a resource — compliance-grade strategy
|
|
2752
|
+
* for what happens to this resource's rows when the owning organization
|
|
2753
|
+
* is deleted. Surfaces in arc's org-delete cascade runner and in
|
|
2754
|
+
* registry introspection (audit scripts ask "what happens to this
|
|
2755
|
+
* resource on org delete?").
|
|
2756
|
+
*
|
|
2757
|
+
* Strategy variants:
|
|
2758
|
+
* - `hard` — permanent removal (GDPR right-to-be-forgotten).
|
|
2759
|
+
* - `soft` — recoverable; pair with TTL for eventual hard-purge.
|
|
2760
|
+
* - `anonymize` — keep the row (legal retention) but clear PII
|
|
2761
|
+
* (HIPAA / PCI / SOX-compatible).
|
|
2762
|
+
* - `skip` — explicit opt-out with mandatory `reason`.
|
|
2763
|
+
*
|
|
2764
|
+
* See {@link ResourceConfig.onTenantDelete} for the full decision tree.
|
|
2765
|
+
*/
|
|
2766
|
+
interface OnTenantDeleteConfig {
|
|
2689
2767
|
/**
|
|
2690
|
-
*
|
|
2691
|
-
*
|
|
2692
|
-
* `
|
|
2768
|
+
* What to do with rows whose `tenantField` matches the deleted org.
|
|
2769
|
+
* Discriminated union from `@classytic/repo-core/repository` — every
|
|
2770
|
+
* kit's `purgeByField` consumes the same shape.
|
|
2693
2771
|
*/
|
|
2694
|
-
|
|
2695
|
-
|
|
2772
|
+
strategy: TenantPurgeStrategy;
|
|
2773
|
+
/**
|
|
2774
|
+
* Resources are processed in ascending `priority` order. Use to land
|
|
2775
|
+
* leaf data (logs, events) before aggregate references. Default `100`.
|
|
2776
|
+
*/
|
|
2777
|
+
priority?: number;
|
|
2778
|
+
/**
|
|
2779
|
+
* Rows per chunk for the underlying `purgeByField` call — bounds
|
|
2780
|
+
* lock contention + replication-log pressure on very large tenants.
|
|
2781
|
+
* Default kit-specific (~1000).
|
|
2782
|
+
*/
|
|
2783
|
+
batchSize?: number;
|
|
2784
|
+
}
|
|
2785
|
+
/**
|
|
2786
|
+
* Resolved tenant-purge declaration — what arc actually runs when
|
|
2787
|
+
* `cascadeDeleteForOrganization` fires. Computed once at boot from the
|
|
2788
|
+
* resource's `onTenantDelete` declaration.
|
|
2789
|
+
*
|
|
2790
|
+
* Exposed via `ResourceDefinition.resolvedTenantPurge` and
|
|
2791
|
+
* `getCascadingResourcesWithMetadata(registry)` so audit tooling can
|
|
2792
|
+
* answer "is this resource really going to hard-delete?" without
|
|
2793
|
+
* reading the source.
|
|
2794
|
+
*/
|
|
2795
|
+
interface ResolvedTenantPurge {
|
|
2796
|
+
readonly strategy: TenantPurgeStrategy;
|
|
2797
|
+
readonly priority: number;
|
|
2798
|
+
readonly batchSize?: number;
|
|
2799
|
+
/**
|
|
2800
|
+
* Where the strategy came from — `'declared'` (host wrote
|
|
2801
|
+
* `onTenantDelete` explicitly) or `'disabled'` (no declaration —
|
|
2802
|
+
* runner skips this resource).
|
|
2803
|
+
*/
|
|
2804
|
+
readonly source: "declared" | "disabled";
|
|
2696
2805
|
}
|
|
2806
|
+
//#endregion
|
|
2807
|
+
//#region src/types/resource/config.d.ts
|
|
2808
|
+
/** Standard controller type alias for CRUD operations. */
|
|
2809
|
+
type CrudController<TDoc> = IController<TDoc>;
|
|
2697
2810
|
/** Resource-level permissions — only `PermissionCheck` functions allowed. */
|
|
2698
2811
|
interface ResourcePermissions {
|
|
2699
2812
|
list?: PermissionCheck;
|
|
@@ -2702,65 +2815,6 @@ interface ResourcePermissions {
|
|
|
2702
2815
|
update?: PermissionCheck;
|
|
2703
2816
|
delete?: PermissionCheck;
|
|
2704
2817
|
}
|
|
2705
|
-
/**
|
|
2706
|
-
* Plugin extension namespace — arc's typed escape hatch for attaching
|
|
2707
|
-
* declarative, per-resource config that **plugins** read at request time.
|
|
2708
|
-
*
|
|
2709
|
-
* **External** plugins augment it via TypeScript declaration merging
|
|
2710
|
-
* (`declare module "@classytic/arc/types"`) to register their own typed
|
|
2711
|
-
* slice — exactly the model Fastify uses for `FastifyInstance` decorators.
|
|
2712
|
-
* The host gets full autocomplete + compile-time checking on the matching
|
|
2713
|
-
* `extensions` block, and a typo (`extensions: { encyrption: … }`) is a
|
|
2714
|
-
* type error rather than a silent no-op.
|
|
2715
|
-
*
|
|
2716
|
-
* **First-party** slices (arc's own subpath plugins) are declared inline
|
|
2717
|
-
* below with type-only imports instead of `declare module`. Not a style
|
|
2718
|
-
* choice: arc bundles its declarations, and a relative module augmentation
|
|
2719
|
-
* (`declare module "../types/resource.js"`) survives bundling as an
|
|
2720
|
-
* unresolvable specifier — the merge silently never happens for consumers,
|
|
2721
|
-
* so the key would be a compile ERROR downstream while working inside this
|
|
2722
|
-
* repo. Inline declaration is bundler-proof; the type-only import is erased
|
|
2723
|
-
* at runtime, so the plugin stays fully opt-in.
|
|
2724
|
-
*
|
|
2725
|
-
* **The "React for backend" composition model.** `defineResource()` is the
|
|
2726
|
-
* declarative component, `extensions` is its typed props bag, and plugins
|
|
2727
|
-
* are the providers that consume those props at the framework boundary.
|
|
2728
|
-
* Adding a cross-cutting capability becomes *additive* — a plugin ships a
|
|
2729
|
-
* subpath + a one-line `declare module` augmentation, with no new
|
|
2730
|
-
* first-class `ResourceConfig` field and no core release per feature.
|
|
2731
|
-
*
|
|
2732
|
-
* Arc threads the declared `extensions` onto every generated route's
|
|
2733
|
-
* Fastify `config` (see `createCrudRouter`), so a plugin reads its slice
|
|
2734
|
-
* at request time via `request.routeOptions.config.arcExtensions` without
|
|
2735
|
-
* re-deriving which resource a route belongs to.
|
|
2736
|
-
*
|
|
2737
|
-
* @example An external plugin package registers its slice
|
|
2738
|
-
* ```ts
|
|
2739
|
-
* declare module "@classytic/arc/types" {
|
|
2740
|
-
* interface ResourceExtensions {
|
|
2741
|
-
* myPlugin?: import("my-arc-plugin").MyPluginDirective;
|
|
2742
|
-
* }
|
|
2743
|
-
* }
|
|
2744
|
-
* ```
|
|
2745
|
-
*
|
|
2746
|
-
* @example A host declares it on a resource
|
|
2747
|
-
* ```ts
|
|
2748
|
-
* defineResource({
|
|
2749
|
-
* name: "payment",
|
|
2750
|
-
* extensions: { encryption: { mode: "fields", fields: ["cardNumber", "cvv"] } },
|
|
2751
|
-
* });
|
|
2752
|
-
* ```
|
|
2753
|
-
*/
|
|
2754
|
-
interface ResourceExtensions {
|
|
2755
|
-
/**
|
|
2756
|
-
* Encrypt this resource's responses — full-body JWE or specific fields.
|
|
2757
|
-
* Read at request time by `@classytic/arc/encryption`'s `encryptionPlugin`
|
|
2758
|
-
* from `request.routeOptions.config.arcExtensions.encryption`; inert (and
|
|
2759
|
-
* dependency-free — the import is type-only) unless that plugin is
|
|
2760
|
-
* registered.
|
|
2761
|
-
*/
|
|
2762
|
-
encryption?: EncryptionDirective;
|
|
2763
|
-
}
|
|
2764
2818
|
interface ResourceConfig<TDoc = AnyRecord> {
|
|
2765
2819
|
name: string;
|
|
2766
2820
|
displayName?: string;
|
|
@@ -3191,6 +3245,28 @@ interface ResourceConfig<TDoc = AnyRecord> {
|
|
|
3191
3245
|
audit?: boolean | {
|
|
3192
3246
|
operations?: ("create" | "update" | "delete")[];
|
|
3193
3247
|
};
|
|
3248
|
+
/**
|
|
3249
|
+
* Per-record change timeline (2.22) — `history: true` adds
|
|
3250
|
+
* `GET /:prefix/:id/history`, serving this record's audit entries
|
|
3251
|
+
* (who/when/what-changed, newest first, `?limit=&offset=`). Implies
|
|
3252
|
+
* `audit: true` (the flag auto-enables it when unset) and requires
|
|
3253
|
+
* `auditPlugin` to be registered — the route answers 503 with
|
|
3254
|
+
* `history.audit_unavailable` otherwise.
|
|
3255
|
+
*
|
|
3256
|
+
* Change history exposes before/after snapshots, so the gate defaults
|
|
3257
|
+
* STRICTER than reads: the resource's `update` permission, falling back
|
|
3258
|
+
* to `get`, falling back to authenticated-only via the route's normal
|
|
3259
|
+
* auth. Override with the object form:
|
|
3260
|
+
*
|
|
3261
|
+
* ```ts
|
|
3262
|
+
* defineResource({ name: 'order', history: true });
|
|
3263
|
+
* defineResource({ name: 'payment', history: { permissions: requireRoles(['auditor']), limit: 100 } });
|
|
3264
|
+
* ```
|
|
3265
|
+
*/
|
|
3266
|
+
history?: boolean | {
|
|
3267
|
+
permissions?: PermissionCheck;
|
|
3268
|
+
limit?: number;
|
|
3269
|
+
};
|
|
3194
3270
|
/**
|
|
3195
3271
|
* Typed plugin-extension namespace — declarative per-resource config that
|
|
3196
3272
|
* arc plugins read at request time. Empty by default; each installed
|
|
@@ -3993,6 +4069,14 @@ interface RegistryEntry extends ResourceMetadata {
|
|
|
3993
4069
|
roles?: readonly string[];
|
|
3994
4070
|
redactValue?: unknown;
|
|
3995
4071
|
}>;
|
|
4072
|
+
/**
|
|
4073
|
+
* The LIVE field-permission map (2.22) — the same object `defineResource`
|
|
4074
|
+
* consumes, surfaced so request-time consumers (the realtime change feed)
|
|
4075
|
+
* can apply `applyFieldReadPermissions` masking to event payloads without
|
|
4076
|
+
* re-reading the resource definition. `fieldPermissions` above stays the
|
|
4077
|
+
* introspection-safe metadata twin.
|
|
4078
|
+
*/
|
|
4079
|
+
fields?: FieldPermissionMap;
|
|
3996
4080
|
/** Pipeline step names (for OpenAPI data) */
|
|
3997
4081
|
pipelineSteps?: Array<{
|
|
3998
4082
|
type: string;
|
|
@@ -4175,4 +4259,4 @@ interface ValidateOptions {
|
|
|
4175
4259
|
strict?: boolean;
|
|
4176
4260
|
}
|
|
4177
4261
|
//#endregion
|
|
4178
|
-
export {
|
|
4262
|
+
export { OpenApiSchemas as $, UserLike as $t, RequestIdOptions as A, defineHook as An, RouteDefinition as At, defineResource as B, BodySanitizer as Bn, RouteHandler as Bt, RequestContext as C, afterCreate as Cn, PipelineStep as Ct, HealthCheck as D, beforeDelete as Dn, ActionEntry as Dt, GracefulShutdownOptions as E, beforeCreate as En, ActionDefinition as Et, FastifyWithDecorators as F, ArcUpdateResult as Fn, ControllerLike as Ft, OnTenantDeleteConfig as G, AuthenticatorContext as Gt, CrudController as H, AuthHelpers as Ht, MiddlewareHandler as I, BaseControllerOptions as In, FastifyHandler as It, PresetHook as J, AnyRecord as Jt, ResolvedTenantPurge as K, JwtContext as Kt, RequestWithExtras as L, ControllerConfigurableOptions as Ln, IController as Lt, EventsDecorator as M, ArcDeleteResult as Mn, RouteMethod as Mt, FastifyRequestExtras as N, ArcGetResult as Nn, RateLimitConfig as Nt, HealthOptions as O, beforeUpdate as On, ActionHandlerFn as Ot, FastifyWithAuth as P, ArcListResult as Pn, ControllerHandler as Pt, MiddlewareConfig as Q, ObjectId as Qt, RegisterOptions as R, ControllerConstructionOptions as Rn, IControllerResponse as Rt, QueryParserInterface as S, HookSystemOptions as Sn, PipelineContext as St, CrudRouterOptions as T, afterUpdate as Tn, ResourceCacheConfig as Tt, ResourceConfig as U, AuthPluginOptions as Ut, ResourceDefinition as V, BodySanitizerConfig as Vn, ServerAccessor as Vt, ResourcePermissions as W, Authenticator as Wt, CrudRouteKey as X, ArcRequest as Xt, PresetResult as Y, ApiResponse as Yt, CrudSchemas as Z, JWTPayload as Zt, ControllerQueryOptions as _, HookHandler as _n, Guard as _t, InferAdapterDoc as a, TreeMixin as an, ResourceExtensions as at, ParsedQuery as b, HookRegistration as bn, OperationFilter as bt, TypedController as c, BulkExt as cn, AggMeasureShorthand as ct, PaginationResult as d, QueryResolver as dn, AggregationDateRangeRequirement as dt, UserOrganization as en, RouteSchemaOptions as et, IntrospectionData as f, QueryResolverConfig as fn, AggregationIndexHint as ft, ArcInternalMetadata as g, HookContext as gn, AggregationsMap as gt, ResourceMetadata as h, DefineHookOptions as hn, AggregationRateLimit as ht, ValidationResult as i, TreeExt as in, ResourceHooks as it, ArcDecorator as j, ArcCreateResult as jn, RouteMcpConfig as jt, IntrospectionPluginOptions as k, createHookSystem as kn, ActionsMap as kt, TypedRepository as l, BulkMixin as ln, AggregationCacheConfig as lt, RegistryStats as m, AccessControlConfig as mn, AggregationMaterializedResult as mt, ConfigError as n, SoftDeleteExt as nn, FieldRule$1 as nt, InferDocType as o, SlugExt as on, EventDefinition as ot, RegistryEntry as p, AccessControl as pn, AggregationMaterializedContext as pt, PresetFunction as q, TokenPair as qt, ValidateOptions as r, SoftDeleteMixin as rn, ResourceHookContext as rt, InferResourceDoc as s, SlugMixin as sn, AggMeasureInput as st, RouteHandlerMethod$1 as t, BaseController as tn, ArcFieldRule as tt, TypedResourceConfig as u, BaseCrudController as un, AggregationConfig as ut, LookupOption as v, HookOperation as vn, Interceptor as vt, ServiceContext as w, afterDelete as wn, Transform as wt, PopulateOption as x, HookSystem as xn, PipelineConfig as xt, OwnershipCheck as y, HookPhase as yn, NextFunction as yt, ResourceRegistry as z, ListResult as zn, IRequestContext as zt };
|