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