@classytic/arc 2.16.1 → 2.17.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/README.md +22 -2
- package/dist/{BaseController-Vk8I7C0x.mjs → BaseController-Dmeh4Y_y.mjs} +5 -47
- package/dist/audit/index.mjs +2 -1
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.mjs +2 -2
- package/dist/auth/redis-session.mjs +28 -2
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -3
- package/dist/{core-wKeGXn38.mjs → core-DYJvu7aI.mjs} +174 -31
- package/dist/{createActionRouter-eiOFtnC3.mjs → createActionRouter-BHUMb1-y.mjs} +5 -2
- package/dist/{createAggregationRouter-C4unhBnV.mjs → createAggregationRouter-BYVvhfc5.mjs} +1 -1
- package/dist/{createApp-DdPBtmzp.mjs → createApp-CzCEEsEm.mjs} +2 -2
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-Dci0AYLT.mjs → elevation-6mfTLBC-.mjs} +1 -1
- package/dist/{errorHandler-DNPzqaTT.mjs → errorHandler-wQejMBT3.mjs} +45 -4
- package/dist/events/transports/redis-stream-entry.mjs +34 -1
- package/dist/events/transports/redis.mjs +17 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +1 -1
- package/dist/{fastify-augmentation-CJm0vh-l.d.mts → fastify-augmentation-7LIR0nDv.d.mts} +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +20 -4
- package/dist/{index-DBDgy6Hw.d.mts → index-C1JSBan-.d.mts} +177 -1
- package/dist/{index-CC_pgZjY.d.mts → index-CruZir1o.d.mts} +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +5 -5
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/jobs.d.mts +36 -1
- package/dist/integrations/jobs.mjs +82 -9
- package/dist/integrations/mcp/index.d.mts +65 -3
- package/dist/integrations/mcp/index.mjs +93 -2
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/websocket.mjs +2 -1
- package/dist/{mcpPlugin-Dbgz9_Rj.mjs → mcpPlugin-hZE00H74.mjs} +111 -8
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/{multipartBody-BOvVSVCD.mjs → multipartBody-VjVWmZl8.mjs} +2 -1
- package/dist/{openapi-BRnd3pT7.mjs → openapi-DKDs9NfD.mjs} +2 -2
- package/dist/permissions/index.mjs +2 -2
- package/dist/{permissions-CTxMrreC.mjs → permissions-B1Qy55fg.mjs} +169 -169
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/index.mjs +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +1 -1
- package/dist/presets/filesUpload.mjs +2 -2
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/search.d.mts +1 -1
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-C9BE6WaZ.mjs → presets-BYbPobbF.mjs} +5 -3
- package/dist/registry/index.d.mts +1 -1
- package/dist/{routerShared-ClV4cCaa.mjs → routerShared-nCyJGOKX.mjs} +127 -4
- package/dist/scope/index.mjs +2 -2
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +1 -1
- package/dist/types/index.d.mts +1 -1
- package/dist/{types-Dw--SyTq.d.mts → types-BRmS8TNa.d.mts} +1 -1
- package/dist/{types-BD0BCdf6.d.mts → types-CoMjNmkF.d.mts} +12 -1
- package/dist/utils/index.d.mts +79 -2
- package/dist/utils/index.mjs +2 -2
- package/dist/{utils-DPXZnZUx.mjs → utils-CvD3glpM.mjs} +99 -2
- package/package.json +1 -1
- package/skills/arc/SKILL.md +43 -1
- package/skills/arc/references/api-reference.md +2 -0
- package/skills/arc/references/mcp.md +30 -1
- /package/dist/{types-D57iXYb8.mjs → types-DV9WDfeg.mjs} +0 -0
package/README.md
CHANGED
|
@@ -122,12 +122,31 @@ export default defineResource({
|
|
|
122
122
|
actions: {
|
|
123
123
|
approve: { handler: approveOrder, permissions: requireRoles(['admin']) },
|
|
124
124
|
},
|
|
125
|
-
// mcp: false, // opt out of MCP tool generation for this resource
|
|
125
|
+
// mcp: false, // opt out of MCP tool generation for this resource
|
|
126
126
|
});
|
|
127
127
|
```
|
|
128
128
|
|
|
129
129
|
Auto-generates: `GET /products`, `GET /products/:id`, `POST /products`, `PATCH /products/:id`, `DELETE /products/:id` + softDelete adds `GET /products/deleted`, `POST /products/:id/restore` + slugLookup adds `GET /products/by-slug/:slug` + custom routes + `POST /products/:id/action`.
|
|
130
130
|
|
|
131
|
+
### Resource shorthands
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// Reference data — read-only list+get, fetch-all limits (1000), 5min/10min cache.
|
|
135
|
+
defineResource({ name: 'currency', adapter, referenceData: true });
|
|
136
|
+
|
|
137
|
+
// Service resource — custom routes only, no adapter, no auto-CRUD, no registry entry.
|
|
138
|
+
defineResource({
|
|
139
|
+
name: 'health',
|
|
140
|
+
customRoutesOnly: true,
|
|
141
|
+
routes: [{ method: 'GET', path: '/ping', permissions: allowPublic(), handler: () => ({ ok: true }) }],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Tune pagination caps inline (no custom queryParser needed).
|
|
145
|
+
defineResource({ name: 'pipeline', adapter, defaultLimit: 200, maxLimit: 500 });
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`?limit=` violations respond 400 with a cap-aware envelope: `message: "Query parameter 'limit' must be <= 500 (got 800) (cap is 500)"` plus `meta: { field: 'limit', cap: 500 }` so callers can self-correct without scraping the message.
|
|
149
|
+
|
|
131
150
|
---
|
|
132
151
|
|
|
133
152
|
## Permissions
|
|
@@ -243,7 +262,8 @@ Tree-shake by importing only the subpath you need:
|
|
|
243
262
|
| `@classytic/arc/plugins` | Health, request-id, versioning, tracing, response-cache |
|
|
244
263
|
| `@classytic/arc/integrations/jobs` | BullMQ job dispatcher |
|
|
245
264
|
| `@classytic/arc/integrations/websocket` | WebSocket integration |
|
|
246
|
-
| `@classytic/arc/mcp` | Model Context Protocol tools |
|
|
265
|
+
| `@classytic/arc/mcp` | Model Context Protocol tools + `createMcpAuthFromBetterAuthApiKey` (Better Auth API-key resolver), `resolveToolCollisions` |
|
|
266
|
+
| `@classytic/arc/utils` | `pipeUIMessageStreamToReply`, `UI_MESSAGE_STREAM_HEADERS` (Vercel AI SDK → Fastify reply); errors, query parser, circuit breaker |
|
|
247
267
|
| `@classytic/arc/testing` | `createTestApp`, `expectArc`, `TestAuthProvider`, `createTestFixtures` |
|
|
248
268
|
| `@classytic/arc/types` | Type-only barrel (zero runtime cost) |
|
|
249
269
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { o as DEFAULT_TENANT_FIELD } from "./constants-TrJVIJl0.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
3
|
import { d as createDomainError, f as createError, i as NotFoundError, r as ForbiddenError } from "./errors-j4aJm1Wg.mjs";
|
|
4
|
-
import { b as isElevated, c as getOrgId
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { t as getUserRoles } from "./types-D57iXYb8.mjs";
|
|
4
|
+
import { b as isElevated, c as getOrgId } from "./types-Bi0r0vjG.mjs";
|
|
5
|
+
import { I as simpleEqualityMatcher, b as ArcQueryParser, o as scheduleBackground, t as getUserId } from "./utils-CvD3glpM.mjs";
|
|
6
|
+
import { w as BodySanitizer } from "./routerShared-nCyJGOKX.mjs";
|
|
8
7
|
import { t as buildQueryKey } from "./keys-DopsCuyQ.mjs";
|
|
9
8
|
//#region src/core/AccessControl.ts
|
|
10
9
|
const log = arcLog("access-control");
|
|
@@ -241,47 +240,6 @@ var AccessControl = class {
|
|
|
241
240
|
return req.metadata;
|
|
242
241
|
}
|
|
243
242
|
};
|
|
244
|
-
var BodySanitizer = class {
|
|
245
|
-
schemaOptions;
|
|
246
|
-
onFieldWriteDenied;
|
|
247
|
-
constructor(config) {
|
|
248
|
-
this.schemaOptions = config.schemaOptions;
|
|
249
|
-
this.onFieldWriteDenied = config.onFieldWriteDenied ?? "reject";
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Strip readonly and system-managed fields from request body.
|
|
253
|
-
* Prevents clients from overwriting _id, timestamps, __v, etc.
|
|
254
|
-
*
|
|
255
|
-
* Also applies field-level write permissions when the request has
|
|
256
|
-
* field permission metadata.
|
|
257
|
-
*/
|
|
258
|
-
sanitize(body, _operation, req, meta) {
|
|
259
|
-
let sanitized = { ...body };
|
|
260
|
-
for (const field of SYSTEM_FIELDS) delete sanitized[field];
|
|
261
|
-
const scopeForRules = req ? (meta ?? req.metadata)?._scope ?? PUBLIC_SCOPE : void 0;
|
|
262
|
-
const scopeIsElevated = scopeForRules ? isElevated(scopeForRules) : false;
|
|
263
|
-
const fieldRules = this.schemaOptions.fieldRules ?? {};
|
|
264
|
-
for (const [field, rules] of Object.entries(fieldRules)) {
|
|
265
|
-
const bypass = Boolean(rules.preserveForElevated) && scopeIsElevated;
|
|
266
|
-
if ((rules.systemManaged || rules.readonly) && !bypass) delete sanitized[field];
|
|
267
|
-
if (_operation === "update" && (rules.immutable || rules.immutableAfterCreate) && !bypass) delete sanitized[field];
|
|
268
|
-
}
|
|
269
|
-
if (req) {
|
|
270
|
-
const arcContext = meta ?? req.metadata;
|
|
271
|
-
const scope = arcContext?._scope ?? PUBLIC_SCOPE;
|
|
272
|
-
if (!isElevated(scope)) {
|
|
273
|
-
const fieldPerms = arcContext?.arc?.fields;
|
|
274
|
-
if (fieldPerms) {
|
|
275
|
-
const effectiveRoles = resolveEffectiveRoles(getUserRoles(req.user), isMember(scope) ? scope.orgRoles : []);
|
|
276
|
-
const { body: filtered, deniedFields } = applyFieldWritePermissions(sanitized, fieldPerms, effectiveRoles);
|
|
277
|
-
if (deniedFields.length > 0 && this.onFieldWriteDenied === "reject") throw new ForbiddenError(`Not permitted to write field${deniedFields.length === 1 ? "" : "s"}: ${deniedFields.join(", ")}`);
|
|
278
|
-
sanitized = filtered;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
return sanitized;
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
243
|
//#endregion
|
|
286
244
|
//#region src/core/fieldRulePredicates.ts
|
|
287
245
|
/**
|
|
@@ -1531,4 +1489,4 @@ function TreeMixin(Base) {
|
|
|
1531
1489
|
*/
|
|
1532
1490
|
var BaseController = class extends SoftDeleteMixin(TreeMixin(SlugMixin(BulkMixin(BaseCrudController)))) {};
|
|
1533
1491
|
//#endregion
|
|
1534
|
-
export { BulkMixin as a, collectReadBlockedFields as c,
|
|
1492
|
+
export { BulkMixin as a, collectReadBlockedFields as c, SlugMixin as i, isFieldReadable as l, TreeMixin as n, BaseCrudController as o, SoftDeleteMixin as r, QueryResolver as s, BaseController as t, AccessControl as u };
|
package/dist/audit/index.mjs
CHANGED
package/dist/auth/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Bt as AuthHelpers, Vt as AuthPluginOptions } from "../index-
|
|
1
|
+
import { Bt as AuthHelpers, Vt as AuthPluginOptions } from "../index-C1JSBan-.mjs";
|
|
2
2
|
import { c as PermissionCheck } from "../fields-DqZLp5d6.mjs";
|
|
3
3
|
import { t as ExternalOpenApiPaths } from "../externalPaths-ClykOQqa.mjs";
|
|
4
4
|
import { a as SessionManagerOptions, c as createSessionManager, i as SessionData, n as MemorySessionStoreOptions, o as SessionManagerResult, r as SessionCookieOptions, s as SessionStore, t as MemorySessionStore } from "../sessionManager-DlmaHUAl.mjs";
|
package/dist/auth/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { d as createDomainError, l as UnauthorizedError, p as isArcError, t as ArcError } from "../errors-j4aJm1Wg.mjs";
|
|
2
|
-
import { _ as
|
|
3
|
-
import { n as normalizeRoles, t as getUserRoles } from "../types-
|
|
2
|
+
import { _ as requireTeamMembership, m as requireOrgRole, p as requireOrgMembership } from "../permissions-B1Qy55fg.mjs";
|
|
3
|
+
import { n as normalizeRoles, t as getUserRoles } from "../types-DV9WDfeg.mjs";
|
|
4
4
|
import { n as extractBetterAuthOpenApi } from "../betterAuthOpenApi-ClWxaceA.mjs";
|
|
5
5
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
6
6
|
import fp from "fastify-plugin";
|
|
@@ -1,5 +1,31 @@
|
|
|
1
|
+
import sjson from "secure-json-parse";
|
|
1
2
|
//#region src/auth/redis-session.ts
|
|
2
3
|
/**
|
|
4
|
+
* Redis Session Store for Arc
|
|
5
|
+
*
|
|
6
|
+
* Implements the SessionStore interface using Redis for distributed session storage.
|
|
7
|
+
* Use this in multi-instance/clustered deployments where MemorySessionStore won't work.
|
|
8
|
+
*
|
|
9
|
+
* This is a SEPARATE subpath import — only loaded when explicitly used:
|
|
10
|
+
* import { RedisSessionStore } from '@classytic/arc/auth/redis';
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { createSessionManager } from '@classytic/arc/auth';
|
|
15
|
+
* import { RedisSessionStore } from '@classytic/arc/auth/redis';
|
|
16
|
+
* import Redis from 'ioredis';
|
|
17
|
+
*
|
|
18
|
+
* const redis = new Redis(process.env.REDIS_URL);
|
|
19
|
+
*
|
|
20
|
+
* const sessions = createSessionManager({
|
|
21
|
+
* store: new RedisSessionStore({ redis }),
|
|
22
|
+
* secret: process.env.SESSION_SECRET!,
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* await fastify.register(sessions.plugin);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
3
29
|
* Redis-backed session store for distributed deployments.
|
|
4
30
|
*
|
|
5
31
|
* Uses two key patterns:
|
|
@@ -22,7 +48,7 @@ var RedisSessionStore = class {
|
|
|
22
48
|
if (!raw) return null;
|
|
23
49
|
let session;
|
|
24
50
|
try {
|
|
25
|
-
session =
|
|
51
|
+
session = sjson.parse(raw);
|
|
26
52
|
} catch {
|
|
27
53
|
await this.delete(sessionId);
|
|
28
54
|
return null;
|
|
@@ -46,7 +72,7 @@ var RedisSessionStore = class {
|
|
|
46
72
|
async delete(sessionId) {
|
|
47
73
|
const raw = await this.redis.get(this.prefix + sessionId);
|
|
48
74
|
if (raw) try {
|
|
49
|
-
const session =
|
|
75
|
+
const session = sjson.parse(raw);
|
|
50
76
|
await this.redis.srem(this.userPrefix + session.userId, sessionId);
|
|
51
77
|
} catch {}
|
|
52
78
|
await this.redis.del(this.prefix + sessionId);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { V as ResourceDefinition, it as RateLimitConfig, mt as RouteSchemaOptions } from "../../index-
|
|
1
|
+
import { V as ResourceDefinition, it as RateLimitConfig, mt as RouteSchemaOptions } from "../../index-C1JSBan-.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/cli/commands/describe.d.ts
|
|
4
4
|
interface DescribedResource {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as buildOpenApiSpec } from "../../openapi-
|
|
1
|
+
import { t as buildOpenApiSpec } from "../../openapi-DKDs9NfD.mjs";
|
|
2
2
|
import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-DCjYMgfC.mjs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { mkdirSync, writeFileSync } from "node:fs";
|
package/dist/core/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { $t as BaseController, At as AggregationRateLimit, B as defineResource, Cn as AccessControlConfig, Ct as AggMeasureShorthand, Dt as AggregationIndexHint, Et as AggregationDateRangeRequirement, Ot as AggregationMaterializedContext, Sn as AccessControl, St as AggMeasureInput, Tt as AggregationConfig, V as ResourceDefinition, an as SlugMixin, bn as BodySanitizer, cn as BaseControllerOptions, dn as ControllerConstructionOptions, en as SoftDeleteExt, fn as QueryResolver, in as SlugExt, jt as AggregationsMap, kt as AggregationMaterializedResult, ln as BaseCrudController, nn as TreeExt, on as BulkExt, pn as QueryResolverConfig, rn as TreeMixin, sn as BulkMixin, tn as SoftDeleteMixin, un as ControllerConfigurableOptions, wt as AggregationCacheConfig, xn as BodySanitizerConfig, yn as ListResult } from "../index-
|
|
2
|
-
import { A as MAX_FILTER_DEPTH, C as DEFAULT_SORT, D as HOOK_PHASES, E as HOOK_OPERATIONS, F as RESERVED_QUERY_PARAMS, I as SYSTEM_FIELDS, M as MAX_SEARCH_LENGTH, N as MUTATION_OPERATIONS, O as HookOperation, P as MutationOperation, S as DEFAULT_MAX_LIMIT, T as DEFAULT_UPDATE_METHOD, _ as defineAggregation, a as createRequestContext, b as DEFAULT_ID_FIELD, c as sendControllerResponse, d as getEntityQuery, f as defineResourceVariants, g as createPermissionMiddleware, h as createCrudRouter, i as createFastifyHandler, j as MAX_REGEX_LENGTH, k as HookPhase, l as getEntityId, m as defineAction, n as isFieldReadable, o as getControllerContext, p as DefineActionConfig, r as createCrudHandlers, s as getControllerScope, t as collectReadBlockedFields, u as getEntityIdField, v as CRUD_OPERATIONS, w as DEFAULT_TENANT_FIELD, x as DEFAULT_LIMIT, y as CrudOperation } from "../index-
|
|
1
|
+
import { $t as BaseController, At as AggregationRateLimit, B as defineResource, Cn as AccessControlConfig, Ct as AggMeasureShorthand, Dt as AggregationIndexHint, Et as AggregationDateRangeRequirement, Ot as AggregationMaterializedContext, Sn as AccessControl, St as AggMeasureInput, Tt as AggregationConfig, V as ResourceDefinition, an as SlugMixin, bn as BodySanitizer, cn as BaseControllerOptions, dn as ControllerConstructionOptions, en as SoftDeleteExt, fn as QueryResolver, in as SlugExt, jt as AggregationsMap, kt as AggregationMaterializedResult, ln as BaseCrudController, nn as TreeExt, on as BulkExt, pn as QueryResolverConfig, rn as TreeMixin, sn as BulkMixin, tn as SoftDeleteMixin, un as ControllerConfigurableOptions, wt as AggregationCacheConfig, xn as BodySanitizerConfig, yn as ListResult } from "../index-C1JSBan-.mjs";
|
|
2
|
+
import { A as MAX_FILTER_DEPTH, C as DEFAULT_SORT, D as HOOK_PHASES, E as HOOK_OPERATIONS, F as RESERVED_QUERY_PARAMS, I as SYSTEM_FIELDS, M as MAX_SEARCH_LENGTH, N as MUTATION_OPERATIONS, O as HookOperation, P as MutationOperation, S as DEFAULT_MAX_LIMIT, T as DEFAULT_UPDATE_METHOD, _ as defineAggregation, a as createRequestContext, b as DEFAULT_ID_FIELD, c as sendControllerResponse, d as getEntityQuery, f as defineResourceVariants, g as createPermissionMiddleware, h as createCrudRouter, i as createFastifyHandler, j as MAX_REGEX_LENGTH, k as HookPhase, l as getEntityId, m as defineAction, n as isFieldReadable, o as getControllerContext, p as DefineActionConfig, r as createCrudHandlers, s as getControllerScope, t as collectReadBlockedFields, u as getEntityIdField, v as CRUD_OPERATIONS, w as DEFAULT_TENANT_FIELD, x as DEFAULT_LIMIT, y as CrudOperation } from "../index-CruZir1o.mjs";
|
|
3
3
|
export { AccessControl, AccessControlConfig, AggMeasureInput, AggMeasureShorthand, AggregationCacheConfig, AggregationConfig, AggregationDateRangeRequirement, AggregationIndexHint, AggregationMaterializedContext, AggregationMaterializedResult, AggregationRateLimit, AggregationsMap, BaseController, BaseControllerOptions, BaseCrudController, BodySanitizer, BodySanitizerConfig, BulkExt, BulkMixin, CRUD_OPERATIONS, ControllerConfigurableOptions, ControllerConstructionOptions, CrudOperation, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, DefineActionConfig, HOOK_OPERATIONS, HOOK_PHASES, HookOperation, HookPhase, ListResult, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugExt, SlugMixin, SoftDeleteExt, SoftDeleteMixin, TreeExt, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAction, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, sendControllerResponse };
|
package/dist/core/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as DEFAULT_SORT, c as HOOK_OPERATIONS, d as MAX_REGEX_LENGTH, f as MAX_SEARCH_LENGTH, h as SYSTEM_FIELDS, i as DEFAULT_MAX_LIMIT, l as HOOK_PHASES, m as RESERVED_QUERY_PARAMS, n as DEFAULT_ID_FIELD, o as DEFAULT_TENANT_FIELD, p as MUTATION_OPERATIONS, r as DEFAULT_LIMIT, s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS, u as MAX_FILTER_DEPTH } from "../constants-TrJVIJl0.mjs";
|
|
2
|
-
import { a as BulkMixin, c as collectReadBlockedFields,
|
|
3
|
-
import { _ as
|
|
4
|
-
import { a as defineResource, c as createCrudRouter, i as defineResourceVariants, l as createPermissionMiddleware, n as getEntityIdField, o as ResourceDefinition, r as getEntityQuery, s as defineAction, t as getEntityId, u as defineAggregation } from "../core-
|
|
2
|
+
import { a as BulkMixin, c as collectReadBlockedFields, i as SlugMixin, l as isFieldReadable, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, s as QueryResolver, t as BaseController, u as AccessControl } from "../BaseController-Dmeh4Y_y.mjs";
|
|
3
|
+
import { S as sendControllerResponse, _ as createCrudHandlers, b as getControllerContext, v as createFastifyHandler, w as BodySanitizer, x as getControllerScope, y as createRequestContext } from "../routerShared-nCyJGOKX.mjs";
|
|
4
|
+
import { a as defineResource, c as createCrudRouter, i as defineResourceVariants, l as createPermissionMiddleware, n as getEntityIdField, o as ResourceDefinition, r as getEntityQuery, s as defineAction, t as getEntityId, u as defineAggregation } from "../core-DYJvu7aI.mjs";
|
|
5
5
|
export { AccessControl, BaseController, BaseCrudController, BodySanitizer, BulkMixin, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, QueryResolver, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugMixin, SoftDeleteMixin, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAction, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, sendControllerResponse };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-TrJVIJl0.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { t as BaseController } from "./BaseController-
|
|
5
|
-
import { t as applyPresets } from "./presets-
|
|
3
|
+
import { N as assertValidConfig, f as getDefaultCrudSchemas, i as pipeUIMessageStreamToReply, r as isReadableStream } from "./utils-CvD3glpM.mjs";
|
|
4
|
+
import { t as BaseController } from "./BaseController-Dmeh4Y_y.mjs";
|
|
5
|
+
import { t as applyPresets } from "./presets-BYbPobbF.mjs";
|
|
6
6
|
import { n as convertRouteSchema, t as convertOpenApiSchemas } from "./schemaConverter-De34B1ZG.mjs";
|
|
7
7
|
import { t as hasEvents } from "./typeGuards-BzkXkvVv.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import { C as buildRequestScopeProjection, _ as createCrudHandlers, c as buildPipelineHandler, d as methodCarriesBody, f as resolvePipelineSteps, g as tryRegisterRoute, h as selectPluginMw, i as buildAuthMiddleware, l as buildPreHandlerChain, m as resolveRouterPluginMw, o as buildCrudPermissionMw, p as resolveRoutePreHandlers, r as buildArcDecorator, s as buildFieldWritePreHandler, u as buildRateLimitConfig, v as createFastifyHandler, y as createRequestContext } from "./routerShared-nCyJGOKX.mjs";
|
|
9
9
|
import { t as pluralize } from "./pluralize-B9M8xvy-.mjs";
|
|
10
10
|
import { t as resolveActionPermission } from "./actionPermissions-Bjmvn7Eb.mjs";
|
|
11
11
|
//#region src/core/aggregation/defineAggregation.ts
|
|
@@ -60,7 +60,7 @@ function defineAggregation(config) {
|
|
|
60
60
|
* `wrapHandler` is derived inline from `!route.raw`.
|
|
61
61
|
*/
|
|
62
62
|
function createCustomRoutes(fastify, routes, controller, options) {
|
|
63
|
-
const { tag, resourceName, arcDecorator, rateLimitConfig, pluginMw, pipeline, routeGuards, tenantScopeMw } = options;
|
|
63
|
+
const { tag, resourceName, arcDecorator, rateLimitConfig, pluginMw, pipeline, routeGuards, tenantScopeMw, fieldPermissions, onFieldWriteDenied } = options;
|
|
64
64
|
for (const route of routes) {
|
|
65
65
|
const routeWithRefs = route;
|
|
66
66
|
const hasHandler = route.handler !== void 0;
|
|
@@ -103,6 +103,10 @@ function createCustomRoutes(fastify, routes, controller, options) {
|
|
|
103
103
|
if (!tenantScopeMw || tenantScopeMw.length === 0) throw new Error(`Route ${route.method} ${route.path}: \`tenantScope: true\` requires a multi-tenant preset. Add \`multiTenantPreset()\` (or \`flexibleMultiTenantPreset()\`) to the resource's \`presets\`, or remove the \`tenantScope\` flag from this route.`);
|
|
104
104
|
customPreHandlers.unshift(...tenantScopeMw);
|
|
105
105
|
}
|
|
106
|
+
if (route.raw !== true && route.fieldWrite !== false && methodCarriesBody(route.method)) {
|
|
107
|
+
const fieldWriteMw = buildFieldWritePreHandler(fieldPermissions, onFieldWriteDenied);
|
|
108
|
+
if (fieldWriteMw) customPreHandlers.push(fieldWriteMw);
|
|
109
|
+
}
|
|
106
110
|
const preHandler = buildPreHandlerChain({
|
|
107
111
|
preAuth: route.preAuth ?? [],
|
|
108
112
|
arcDecorator,
|
|
@@ -113,7 +117,7 @@ function createCustomRoutes(fastify, routes, controller, options) {
|
|
|
113
117
|
customMws: customPreHandlers
|
|
114
118
|
});
|
|
115
119
|
const isStream = route.streamResponse === true;
|
|
116
|
-
fastify
|
|
120
|
+
tryRegisterRoute(fastify, {
|
|
117
121
|
method: route.method,
|
|
118
122
|
url: route.path,
|
|
119
123
|
schema,
|
|
@@ -122,9 +126,14 @@ function createCustomRoutes(fastify, routes, controller, options) {
|
|
|
122
126
|
reply.raw.setHeader("Content-Type", "text/event-stream");
|
|
123
127
|
reply.raw.setHeader("Cache-Control", "no-cache");
|
|
124
128
|
reply.raw.setHeader("Connection", "keep-alive");
|
|
125
|
-
|
|
129
|
+
const result = await handler(request, reply);
|
|
130
|
+
if (isReadableStream(result)) await pipeUIMessageStreamToReply(reply, result);
|
|
131
|
+
return result;
|
|
126
132
|
} : handler,
|
|
127
133
|
...rateLimitConfig ? { config: rateLimitConfig } : {}
|
|
134
|
+
}, {
|
|
135
|
+
resourceName,
|
|
136
|
+
op: opName
|
|
128
137
|
});
|
|
129
138
|
}
|
|
130
139
|
}
|
|
@@ -138,7 +147,7 @@ function createCustomRoutes(fastify, routes, controller, options) {
|
|
|
138
147
|
* @param options - Router configuration
|
|
139
148
|
*/
|
|
140
149
|
function createCrudRouter(fastify, controller, options = {}) {
|
|
141
|
-
const { tag = "Resource", schemas = {}, permissions = {}, middlewares = {}, routeGuards = [], routes: customRoutes = [], disableDefaultRoutes = false, disabledRoutes = [], resourceName = "unknown", schemaOptions, rateLimit, pipe: pipeline, fields: fieldPermissions, updateMethod = DEFAULT_UPDATE_METHOD, idField } = options;
|
|
150
|
+
const { tag = "Resource", schemas = {}, permissions = {}, middlewares = {}, routeGuards = [], routes: customRoutes = [], disableDefaultRoutes = false, disabledRoutes = [], resourceName = "unknown", schemaOptions, rateLimit, pipe: pipeline, fields: fieldPermissions, onFieldWriteDenied, updateMethod = DEFAULT_UPDATE_METHOD, idField } = options;
|
|
142
151
|
const rateLimitConfig = buildRateLimitConfig(rateLimit);
|
|
143
152
|
const resourceHasQueryCache = fastify.hasDecorator("queryCache") && controller && typeof controller._cacheConfig !== "undefined" && controller._cacheConfig !== void 0;
|
|
144
153
|
const pluginMw = resolveRouterPluginMw(fastify, Boolean(resourceHasQueryCache));
|
|
@@ -229,7 +238,7 @@ function createCrudRouter(fastify, controller, options = {}) {
|
|
|
229
238
|
const methodsToRegister = spec.op === "update" ? updateMethod === "both" ? ["PUT", "PATCH"] : [updateMethod] : [spec.method];
|
|
230
239
|
for (const method of methodsToRegister) {
|
|
231
240
|
const summary = spec.op === "update" ? `${method === "PUT" ? "Replace" : "Update"} ${tag}` : spec.summary;
|
|
232
|
-
fastify
|
|
241
|
+
tryRegisterRoute(fastify, {
|
|
233
242
|
method,
|
|
234
243
|
url: spec.url,
|
|
235
244
|
schema: buildSchema({
|
|
@@ -240,6 +249,9 @@ function createCrudRouter(fastify, controller, options = {}) {
|
|
|
240
249
|
preHandler: preHandler.length > 0 ? preHandler : void 0,
|
|
241
250
|
handler: handlers[spec.op],
|
|
242
251
|
...rateLimitConfig ? { config: rateLimitConfig } : {}
|
|
252
|
+
}, {
|
|
253
|
+
resourceName,
|
|
254
|
+
op: spec.op
|
|
243
255
|
});
|
|
244
256
|
}
|
|
245
257
|
}
|
|
@@ -252,7 +264,9 @@ function createCrudRouter(fastify, controller, options = {}) {
|
|
|
252
264
|
pluginMw,
|
|
253
265
|
pipeline,
|
|
254
266
|
routeGuards,
|
|
255
|
-
tenantScopeMw
|
|
267
|
+
tenantScopeMw,
|
|
268
|
+
fieldPermissions,
|
|
269
|
+
onFieldWriteDenied
|
|
256
270
|
});
|
|
257
271
|
}
|
|
258
272
|
function buildCrudHandlers(ctrl, pipeline, resourceName) {
|
|
@@ -442,11 +456,14 @@ function buildBaseController(resolvedConfig, adapter, repository) {
|
|
|
442
456
|
const limitProp = qp.getQuerySchema()?.properties?.limit;
|
|
443
457
|
if (limitProp?.maximum) maxLimitFromParser = limitProp.maximum;
|
|
444
458
|
}
|
|
459
|
+
const resourceConfigMaxLimit = resolvedConfig.maxLimit;
|
|
460
|
+
const resourceConfigDefaultLimit = resolvedConfig.defaultLimit;
|
|
445
461
|
return new BaseController(repository, {
|
|
446
462
|
resourceName: resolvedConfig.name,
|
|
447
463
|
schemaOptions: resolvedConfig.schemaOptions,
|
|
448
464
|
queryParser: qp,
|
|
449
|
-
maxLimit: maxLimitFromParser,
|
|
465
|
+
maxLimit: resourceConfigMaxLimit ?? maxLimitFromParser,
|
|
466
|
+
...resourceConfigDefaultLimit !== void 0 ? { defaultLimit: resourceConfigDefaultLimit } : {},
|
|
450
467
|
tenantField: resolvedConfig.tenantField,
|
|
451
468
|
idField: resolvedConfig.idField,
|
|
452
469
|
...resolvedConfig.defaultSort !== void 0 ? { defaultSort: resolvedConfig.defaultSort } : {},
|
|
@@ -1033,10 +1050,21 @@ function buildResourcePlugin(resource) {
|
|
|
1033
1050
|
handler: hook.handler,
|
|
1034
1051
|
priority: hook.priority
|
|
1035
1052
|
});
|
|
1036
|
-
if (isFirstMount && resource.cache?.invalidateOn
|
|
1053
|
+
if (isFirstMount && resource.cache?.invalidateOn) if (fastify.registerCacheInvalidationRule) for (const [pattern, tags] of Object.entries(resource.cache.invalidateOn)) fastify.registerCacheInvalidationRule({
|
|
1037
1054
|
pattern,
|
|
1038
1055
|
tags
|
|
1039
1056
|
});
|
|
1057
|
+
else fastify.log?.warn?.(`[Arc] Resource '${resource.name}' declares \`cache.invalidateOn\` but \`queryCachePlugin\` is not registered. Cache-invalidation rules are silently ignored. Either register the plugin (\`await fastify.register(queryCachePlugin, ...)\`) or remove the \`cache.invalidateOn\` config to clean up the resource definition.`);
|
|
1058
|
+
if (isFirstMount && resource.audit) {
|
|
1059
|
+
if (!(fastify.hasDecorator("audit") && typeof fastify.audit?.create === "function" && fastify.audit?._noop !== true)) fastify.log?.warn?.(`[Arc] Resource '${resource.name}' declares \`audit: ${JSON.stringify(resource.audit)}\` but \`auditPlugin\` is not registered (or is registered with a noop logger). Audit events for this resource will NOT be recorded. Either register \`auditPlugin\` (\`await fastify.register(auditPlugin, { store: ... })\`) or remove the \`audit\` flag from the resource definition.`);
|
|
1060
|
+
}
|
|
1061
|
+
if (isFirstMount && resource.events && Object.keys(resource.events).length > 0) {
|
|
1062
|
+
const eventsDecor = fastify.events;
|
|
1063
|
+
if (!(eventsDecor != null && typeof eventsDecor.publish === "function")) {
|
|
1064
|
+
const eventNames = Object.keys(resource.events).join(", ");
|
|
1065
|
+
fastify.log?.warn?.(`[Arc] Resource '${resource.name}' declares events (${eventNames}) but \`eventPlugin\` is not registered. The events will silently NOT be emitted on CRUD mutations. Either register the plugin (\`await fastify.register(eventPlugin, { transport: ... })\`) or remove the \`events\` declaration from the resource.`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1040
1068
|
await fastify.register(async (instance) => {
|
|
1041
1069
|
const typedInstance = instance;
|
|
1042
1070
|
let schemas = buildGeneratedCrudSchemas(resource._registryMeta?.openApiSchemas, resource.customSchemas);
|
|
@@ -1061,10 +1089,11 @@ function buildResourcePlugin(resource) {
|
|
|
1061
1089
|
updateMethod: resource.updateMethod,
|
|
1062
1090
|
pipe: resource.pipe,
|
|
1063
1091
|
fields: resource.fields,
|
|
1092
|
+
onFieldWriteDenied: resource.onFieldWriteDenied,
|
|
1064
1093
|
idField: resource.idField
|
|
1065
1094
|
});
|
|
1066
1095
|
if (resource.actions && Object.keys(resource.actions).length > 0) {
|
|
1067
|
-
const { createActionRouter } = await import("./createActionRouter-
|
|
1096
|
+
const { createActionRouter } = await import("./createActionRouter-BHUMb1-y.mjs").then((n) => n.n);
|
|
1068
1097
|
createActionRouter(typedInstance, {
|
|
1069
1098
|
...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
|
|
1070
1099
|
resourceName: resource.name,
|
|
@@ -1078,7 +1107,7 @@ function buildResourcePlugin(resource) {
|
|
|
1078
1107
|
});
|
|
1079
1108
|
}
|
|
1080
1109
|
if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
|
|
1081
|
-
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-
|
|
1110
|
+
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-BYVvhfc5.mjs"), import("./validate-DFIP2VrM.mjs").then((n) => n.i)]);
|
|
1082
1111
|
const repoForAgg = resource.controller?.repository ?? resource.repository;
|
|
1083
1112
|
if (!adapterSupportsAggregate(repoForAgg)) {
|
|
1084
1113
|
const undispatchable = Object.entries(resource.aggregations).filter(([, cfg]) => !cfg.materialized).map(([name]) => name);
|
|
@@ -1142,7 +1171,16 @@ var ResourceDefinition = class {
|
|
|
1142
1171
|
updateMethod;
|
|
1143
1172
|
pipe;
|
|
1144
1173
|
fields;
|
|
1174
|
+
/**
|
|
1175
|
+
* Policy for field-write denials — `'reject'` (default, secure) raises 403
|
|
1176
|
+
* listing the denied fields; `'strip'` silently drops them. Honored by
|
|
1177
|
+
* both `BodySanitizer` (auto-CRUD) and `buildFieldWritePreHandler`
|
|
1178
|
+
* (custom routes) so the policy is consistent across all body-bearing
|
|
1179
|
+
* routes for the resource.
|
|
1180
|
+
*/
|
|
1181
|
+
onFieldWriteDenied;
|
|
1145
1182
|
cache;
|
|
1183
|
+
referenceData;
|
|
1146
1184
|
/**
|
|
1147
1185
|
* Per-resource MCP opt-out. `false` keeps the resource out of every
|
|
1148
1186
|
* `mcpPlugin` registration regardless of the plugin's `expose` /
|
|
@@ -1174,9 +1212,37 @@ var ResourceDefinition = class {
|
|
|
1174
1212
|
* `buildResourcePlugin` so the host's configured logger owns the
|
|
1175
1213
|
* output — the framework never speaks to `console.*` from `src/`
|
|
1176
1214
|
* outside of the CLI.
|
|
1215
|
+
*
|
|
1216
|
+
* Prefer the public {@link warnings} getter for programmatic access
|
|
1217
|
+
* (CI fail-on-warnings, lint integrations) — it returns a frozen
|
|
1218
|
+
* snapshot so callers can't accidentally mutate the diagnostic list.
|
|
1177
1219
|
*/
|
|
1178
1220
|
_diagnostics;
|
|
1179
1221
|
/**
|
|
1222
|
+
* Public view of {@link _diagnostics} for programmatic consumption.
|
|
1223
|
+
*
|
|
1224
|
+
* Returns the collected define-time diagnostics for this resource.
|
|
1225
|
+
* Empty when the resource validated cleanly. Useful for CI gates that
|
|
1226
|
+
* want to fail the build on any non-fatal warning:
|
|
1227
|
+
*
|
|
1228
|
+
* @example
|
|
1229
|
+
* ```ts
|
|
1230
|
+
* import { orderResource } from './resources/order.resource.js';
|
|
1231
|
+
* if (orderResource.warnings.length > 0) {
|
|
1232
|
+
* for (const w of orderResource.warnings) console.error(w.message);
|
|
1233
|
+
* process.exit(1);
|
|
1234
|
+
* }
|
|
1235
|
+
* ```
|
|
1236
|
+
*
|
|
1237
|
+
* **Scope.** Covers define-time only — diagnostics emitted at
|
|
1238
|
+
* first-mount (e.g. `cache.invalidateOn` without `queryCachePlugin`)
|
|
1239
|
+
* surface through `fastify.log.warn` instead and are not collected
|
|
1240
|
+
* here; they require a Fastify instance to detect.
|
|
1241
|
+
*/
|
|
1242
|
+
get warnings() {
|
|
1243
|
+
return this._diagnostics ?? [];
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1180
1246
|
* Per-host idempotency guard used by `buildResourcePlugin` to
|
|
1181
1247
|
* skip duplicate shared-state writes when the same resource is
|
|
1182
1248
|
* mounted at multiple prefixes (`/v1`, `/v2`). See the plugin
|
|
@@ -1209,8 +1275,10 @@ var ResourceDefinition = class {
|
|
|
1209
1275
|
this.updateMethod = config.updateMethod;
|
|
1210
1276
|
this.pipe = config.pipe;
|
|
1211
1277
|
this.fields = config.fields;
|
|
1278
|
+
this.onFieldWriteDenied = config.onFieldWriteDenied;
|
|
1212
1279
|
this.cache = config.cache;
|
|
1213
1280
|
this.mcp = config.mcp !== false;
|
|
1281
|
+
this.referenceData = config.referenceData === true;
|
|
1214
1282
|
this.tenantField = config.tenantField;
|
|
1215
1283
|
this.onTenantDelete = config.onTenantDelete;
|
|
1216
1284
|
this.resolvedTenantPurge = resolveTenantPurge({
|
|
@@ -1364,6 +1432,7 @@ function resolveOpenApiSchemas(resolvedConfig) {
|
|
|
1364
1432
|
openApiSchemas = stripSystemManagedFromBodyRequired(openApiSchemas, resolvedConfig.schemaOptions);
|
|
1365
1433
|
openApiSchemas = cleanLegacyObjectIdParams(openApiSchemas, resolvedConfig.idField);
|
|
1366
1434
|
openApiSchemas = layerQueryParserListQuery(openApiSchemas, resolvedConfig.queryParser);
|
|
1435
|
+
openApiSchemas = applyResourcePaginationCaps(openApiSchemas, resolvedConfig);
|
|
1367
1436
|
openApiSchemas = mergeUserOpenApiOverrides(openApiSchemas, resolvedConfig.openApiSchemas);
|
|
1368
1437
|
if (openApiSchemas) openApiSchemas = convertOpenApiSchemas(openApiSchemas);
|
|
1369
1438
|
return {
|
|
@@ -1435,6 +1504,45 @@ function layerQueryParserListQuery(openApiSchemas, queryParser) {
|
|
|
1435
1504
|
};
|
|
1436
1505
|
}
|
|
1437
1506
|
/**
|
|
1507
|
+
* Apply resource-level `defaultLimit` / `maxLimit` (2.17.0) onto the
|
|
1508
|
+
* listQuery schema. Resource-level caps win over both the framework
|
|
1509
|
+
* default and the parser-derived value because they're the most-explicit
|
|
1510
|
+
* declaration — a host that wrote `defaultLimit: 1000` on the resource
|
|
1511
|
+
* means "this resource specifically returns up to 1000", regardless of
|
|
1512
|
+
* what the parser's default would have been. The `referenceData: true`
|
|
1513
|
+
* shorthand lifts both to 1000 so reference-data resources don't need
|
|
1514
|
+
* a custom queryParser just to get the caps right.
|
|
1515
|
+
*
|
|
1516
|
+
* Bootstraps a minimal listQuery slot when none exists (no queryParser
|
|
1517
|
+
* registered, no adapter-generated schema) so the AJV layer still gets
|
|
1518
|
+
* a `maximum:` and surfaces the cap in the 400 envelope via
|
|
1519
|
+
* `errorHandler.describePaginationCap()` instead of producing the
|
|
1520
|
+
* generic "Bad Request" we ate pre-2.17.0.
|
|
1521
|
+
*/
|
|
1522
|
+
function applyResourcePaginationCaps(openApiSchemas, resolvedConfig) {
|
|
1523
|
+
const cfg = resolvedConfig;
|
|
1524
|
+
if (cfg.defaultLimit === void 0 && cfg.maxLimit === void 0) return openApiSchemas;
|
|
1525
|
+
const existing = openApiSchemas?.listQuery;
|
|
1526
|
+
const existingProps = existing?.properties ?? {};
|
|
1527
|
+
const patchedLimit = { ...existingProps.limit ?? {
|
|
1528
|
+
type: "integer",
|
|
1529
|
+
minimum: 1
|
|
1530
|
+
} };
|
|
1531
|
+
if (cfg.maxLimit !== void 0) patchedLimit.maximum = cfg.maxLimit;
|
|
1532
|
+
if (cfg.defaultLimit !== void 0) patchedLimit.default = cfg.defaultLimit;
|
|
1533
|
+
return {
|
|
1534
|
+
...openApiSchemas,
|
|
1535
|
+
listQuery: {
|
|
1536
|
+
type: "object",
|
|
1537
|
+
...existing ?? {},
|
|
1538
|
+
properties: {
|
|
1539
|
+
...existingProps,
|
|
1540
|
+
limit: patchedLimit
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1438
1546
|
* Apply per-resource `openApiSchemas` overrides on top of the kit's
|
|
1439
1547
|
* generated schemas. Shallow merge by slot — users who want field-level
|
|
1440
1548
|
* surgery should compose at the schema-options layer before this point.
|
|
@@ -1547,24 +1655,8 @@ function validateActionsShape(config) {
|
|
|
1547
1655
|
}
|
|
1548
1656
|
//#endregion
|
|
1549
1657
|
//#region src/core/defineResource.ts
|
|
1550
|
-
/**
|
|
1551
|
-
* `TDoc` is **unconstrained** at this layer. The previous `TDoc
|
|
1552
|
-
* extends AnyRecord` bound leaked out of `BaseController`'s
|
|
1553
|
-
* mixin-composition requirement into every host's adapter boundary:
|
|
1554
|
-
* Mongoose's `HydratedDocument<T>`, Prisma's generated row types,
|
|
1555
|
-
* and any domain interface without an explicit index signature all
|
|
1556
|
-
* failed to satisfy `Record<string, unknown>` even though at runtime
|
|
1557
|
-
* they ARE string-keyed objects. Hosts were forced to cast at every
|
|
1558
|
-
* adapter (`as RepositoryLike<Record<string, unknown>>`) — a type
|
|
1559
|
-
* escape with no runtime purpose, since arc's pipeline only reads
|
|
1560
|
-
* known envelope fields.
|
|
1561
|
-
*
|
|
1562
|
-
* The cast moved inside `resolveOrAutoCreateController` where
|
|
1563
|
-
* `BaseController<TDoc extends AnyRecord>` actually requires it.
|
|
1564
|
-
* One internal boundary cast replaces N host-side casts.
|
|
1565
|
-
*/
|
|
1566
1658
|
function defineResource(config) {
|
|
1567
|
-
const normalisedConfig = resolveCrudAllowList(config);
|
|
1659
|
+
const normalisedConfig = resolveCrudAllowList(expandCustomRoutesOnly(expandReferenceData(config)));
|
|
1568
1660
|
let diagnostics = [];
|
|
1569
1661
|
if (!normalisedConfig.skipValidation) diagnostics = validateDefineResourceConfig(normalisedConfig);
|
|
1570
1662
|
const repository = normalisedConfig.adapter?.repository;
|
|
@@ -1606,6 +1698,57 @@ function defineResource(config) {
|
|
|
1606
1698
|
* doesn't false-positive "Data adapter required when CRUD routes are
|
|
1607
1699
|
* enabled" on a host that explicitly opted CRUD out.
|
|
1608
1700
|
*/
|
|
1701
|
+
/**
|
|
1702
|
+
* Expand the `referenceData: true` shorthand. Reference data is the
|
|
1703
|
+
* recurring "small static catalogue" shape (currencies, plans,
|
|
1704
|
+
* pipeline stages, credential types) where callers want "fetch all"
|
|
1705
|
+
* semantics and aggressive caching — pre-2.17.0 hosts hand-wired
|
|
1706
|
+
* `crud` + `defaultLimit` + `maxLimit` + `cache` on every such
|
|
1707
|
+
* resource. The shorthand pins canonical defaults; explicit narrow
|
|
1708
|
+
* settings always win so a host can opt INTO mutations
|
|
1709
|
+
* (`referenceData: true, crud: { list: true, get: true, create: true }`)
|
|
1710
|
+
* or override the cache window without giving up the read-only +
|
|
1711
|
+
* fetch-all defaults.
|
|
1712
|
+
*/
|
|
1713
|
+
function expandReferenceData(config) {
|
|
1714
|
+
if (config.referenceData !== true) return config;
|
|
1715
|
+
return {
|
|
1716
|
+
...config,
|
|
1717
|
+
crud: config.crud ?? {
|
|
1718
|
+
list: true,
|
|
1719
|
+
get: true
|
|
1720
|
+
},
|
|
1721
|
+
defaultLimit: config.defaultLimit ?? 1e3,
|
|
1722
|
+
maxLimit: config.maxLimit ?? 1e3,
|
|
1723
|
+
cache: config.cache ?? {
|
|
1724
|
+
staleTime: 300,
|
|
1725
|
+
gcTime: 600
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Expand the `customRoutesOnly: true` shorthand into its three primitive
|
|
1731
|
+
* flags. The shorthand exists because hosts wiring a "service resource"
|
|
1732
|
+
* (custom routes only, no adapter, no auto-CRUD) had to remember to set
|
|
1733
|
+
* `disableDefaultRoutes` + `skipValidation` + `skipRegistry` in lockstep —
|
|
1734
|
+
* forgetting any one of them produced confusing errors ("controller
|
|
1735
|
+
* required when CRUD routes are enabled" for a resource that ships none).
|
|
1736
|
+
*
|
|
1737
|
+
* Explicit narrow settings always win — the shorthand only fills in
|
|
1738
|
+
* flags that were not set so power users can opt back into a specific
|
|
1739
|
+
* primitive (`customRoutesOnly: true, skipRegistry: false` keeps OpenAPI
|
|
1740
|
+
* docs while still skipping CRUD + validation).
|
|
1741
|
+
*/
|
|
1742
|
+
function expandCustomRoutesOnly(config) {
|
|
1743
|
+
if (config.customRoutesOnly !== true) return config;
|
|
1744
|
+
return {
|
|
1745
|
+
...config,
|
|
1746
|
+
customRoutesOnly: void 0,
|
|
1747
|
+
disableDefaultRoutes: config.disableDefaultRoutes ?? true,
|
|
1748
|
+
skipValidation: config.skipValidation ?? true,
|
|
1749
|
+
skipRegistry: config.skipRegistry ?? true
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1609
1752
|
function resolveCrudAllowList(config) {
|
|
1610
1753
|
const { crud, disabledRoutes: legacyDisabled, disableDefaultRoutes: legacyDisableAll } = config;
|
|
1611
1754
|
if (crud === void 0) return config;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
|
|
2
2
|
import "./constants-TrJVIJl0.mjs";
|
|
3
3
|
import { f as createError } from "./errors-j4aJm1Wg.mjs";
|
|
4
|
-
import { a as buildAuthMiddlewareForPermissions,
|
|
4
|
+
import { S as sendControllerResponse, a as buildAuthMiddlewareForPermissions, f as resolvePipelineSteps, g as tryRegisterRoute, h as selectPluginMw, l as buildPreHandlerChain, m as resolveRouterPluginMw, n as buildActionPipelineHandler, r as buildArcDecorator, t as buildActionPermissionMw, u as buildRateLimitConfig } from "./routerShared-nCyJGOKX.mjs";
|
|
5
5
|
import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-DAOvBWgT.mjs";
|
|
6
6
|
//#region src/core/createActionRouter.ts
|
|
7
7
|
var createActionRouter_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -114,7 +114,7 @@ function createActionRouter(fastify, config) {
|
|
|
114
114
|
pluginMw: selectPluginMw("POST", pluginMw),
|
|
115
115
|
routeGuards
|
|
116
116
|
})];
|
|
117
|
-
fastify
|
|
117
|
+
tryRegisterRoute(fastify, {
|
|
118
118
|
method: "POST",
|
|
119
119
|
url: urlPath,
|
|
120
120
|
schema: routeSchema,
|
|
@@ -152,6 +152,9 @@ function createActionRouter(fastify, config) {
|
|
|
152
152
|
throw createError(statusCode, err.message || `Failed to execute '${action}' action`, { code: errorCode });
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
+
}, {
|
|
156
|
+
resourceName,
|
|
157
|
+
op: `action${urlPath}`
|
|
155
158
|
});
|
|
156
159
|
fastify.log.debug({
|
|
157
160
|
actions: actionSubset,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { f as createError, l as UnauthorizedError, r as ForbiddenError } from "./errors-j4aJm1Wg.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { h as selectPluginMw, i as buildAuthMiddleware, l as buildPreHandlerChain, m as resolveRouterPluginMw, r as buildArcDecorator, u as buildRateLimitConfig } from "./routerShared-nCyJGOKX.mjs";
|
|
3
3
|
import { r as validateAggregations } from "./validate-DFIP2VrM.mjs";
|
|
4
4
|
import { t as buildAggregationHandler } from "./buildHandler-C_SkBNF3.mjs";
|
|
5
5
|
//#region src/core/aggregation/createAggregationRouter.ts
|