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