@classytic/arc 2.15.3 → 2.16.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 +1 -0
- package/bin/arc.js +12 -0
- package/dist/{BaseController-dx3m2J8V.mjs → BaseController-DlCCTIxJ.mjs} +61 -19
- package/dist/{HookSystem-Iiebom92.mjs → HookSystem-Cmf7-Etp.mjs} +8 -4
- package/dist/{QueryCache-D41bfdBB.d.mts → QueryCache-SvmT_9ti.d.mts} +1 -1
- package/dist/{ResourceRegistry-CTERg_2x.mjs → ResourceRegistry-f48hFk3m.mjs} +52 -9
- package/dist/audit/index.d.mts +1 -1
- package/dist/audit/index.mjs +4 -2
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +4 -4
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi--M_i87dQ.mjs → betterAuthOpenApi-ClWxaceA.mjs} +10 -6
- package/dist/buildHandler-BZX6zzDM.mjs +300 -0
- package/dist/cache/index.d.mts +3 -3
- package/dist/cache/index.mjs +3 -3
- package/dist/{caching-SM8gghN6.mjs → caching-TeHE8G-v.mjs} +1 -1
- package/dist/cli/commands/describe.d.mts +35 -1
- package/dist/cli/commands/describe.mjs +52 -12
- package/dist/cli/commands/docs.d.mts +1 -4
- package/dist/cli/commands/docs.mjs +4 -16
- package/dist/cli/commands/generate.d.mts +2 -20
- package/dist/cli/commands/generate.mjs +1 -546
- package/dist/cli/commands/init.d.mts +2 -40
- package/dist/cli/commands/init.mjs +1 -3036
- package/dist/cli/commands/introspect.mjs +53 -64
- package/dist/cli/index.d.mts +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/{constants-Cxde4rpC.mjs → constants-TrJVIJl0.mjs} +7 -0
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -5
- package/dist/{core-CvmOqEms.mjs → core-DBJ_j6rX.mjs} +222 -44
- package/dist/createActionRouter-DUpN3Dd1.mjs +288 -0
- package/dist/{createAggregationRouter-B0bPDf5b.mjs → createAggregationRouter-Dq-TUCuY.mjs} +3 -2
- package/dist/{createApp-PFegs47-.mjs → createApp-DNccuhyI.mjs} +16 -14
- package/dist/{defineEvent-D5h7EvAx.mjs → defineEvent-DRwY0fYm.mjs} +1 -1
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{errorHandler-Bk-AGhkU.mjs → errorHandler-DpoXQHZ9.mjs} +17 -14
- package/dist/errors-C1lX_jlm.d.mts +91 -0
- package/dist/{eventPlugin-CaKTYkYM.mjs → eventPlugin-C2cGqtRO.mjs} +1 -1
- package/dist/{eventPlugin-qXpqTebY.d.mts → eventPlugin-CtHC_av1.d.mts} +1 -1
- package/dist/events/index.d.mts +3 -3
- package/dist/events/index.mjs +5 -5
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-COhcH3fk.d.mts → fields-Anj0xdih.d.mts} +1 -1
- package/dist/generate-BWFwgcCM.d.mts +38 -0
- package/dist/generate-CYac-OLv.mjs +654 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +2 -2
- package/dist/idempotency/index.mjs +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-BTqLEvhu.d.mts → index-3oIimXQn.d.mts} +12 -12
- package/dist/{index-BstGxcc3.d.mts → index-B-ulKx5P.d.mts} +55 -4
- package/dist/{index-BswOSJCE.d.mts → index-CkW0flkU.d.mts} +355 -16
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +7 -8
- package/dist/init-Dv71MsJr.d.mts +71 -0
- package/dist/init-HDvoO9L5.mjs +3098 -0
- 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/jobs.mjs +3 -3
- package/dist/integrations/mcp/index.d.mts +239 -7
- package/dist/integrations/mcp/index.mjs +2 -528
- package/dist/integrations/mcp/testing.d.mts +2 -2
- package/dist/integrations/mcp/testing.mjs +6 -10
- package/dist/integrations/streamline.d.mts +71 -2
- package/dist/integrations/streamline.mjs +81 -8
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +1 -0
- package/dist/loadResourcesFromEntry-BLMEI2Xa.mjs +51 -0
- package/dist/{resourceToTools-tFYUNmM0.mjs → mcpPlugin-7vGV51ED.mjs} +1021 -318
- package/dist/{memory-UBydS5ku.mjs → memory-QOLe11D5.mjs} +2 -0
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/{openapi-BHXhoX8O.mjs → openapi-34T9yNwd.mjs} +47 -36
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +1 -1
- package/dist/{permissions-ohQyv50e.mjs → permissions-CTxMrreC.mjs} +2 -2
- package/dist/{pipe-Zr0KXjQe.mjs → pipe-DiCyvyPN.mjs} +1 -0
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +5 -5
- package/dist/plugins/index.mjs +10 -10
- package/dist/plugins/response-cache.mjs +5 -5
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{pluralize-DQgqgifU.mjs → pluralize-B9M8xvy-.mjs} +2 -1
- package/dist/presets/filesUpload.d.mts +4 -4
- 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/multiTenant.mjs +4 -3
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-BbkjdPeH.mjs → presets-C9BE6WaZ.mjs} +2 -2
- package/dist/{queryCachePlugin-m1XsgAIJ.mjs → queryCachePlugin-B4XMSSe7.mjs} +2 -2
- package/dist/{queryCachePlugin-CqMdLI2-.d.mts → queryCachePlugin-Biqzfbi5.d.mts} +2 -2
- package/dist/{redis-DiMkdHEl.d.mts → redis-Cyzrz6SX.d.mts} +1 -1
- package/dist/{redis-stream-D6HzR1Z_.d.mts → redis-stream-DT-YjzrB.d.mts} +1 -1
- package/dist/registry/index.d.mts +319 -2
- package/dist/registry/index.mjs +3 -3
- package/dist/registry-BBE23CDj.mjs +576 -0
- package/dist/{routerShared-DrOa-26E.mjs → routerShared-CZV5aabX.mjs} +3 -3
- package/dist/scope/index.d.mts +3 -3
- package/dist/scope/index.mjs +3 -3
- package/dist/{sse-Bz-5ZeTt.mjs → sse-BY6sTy4P.mjs} +1 -1
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +16 -7
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +5 -5
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-C_s5moIu.mjs → types-Bi0r0vjG.mjs} +53 -1
- package/dist/{types-BQsjgQzS.d.mts → types-BsJMEQ4D.d.mts} +106 -12
- package/dist/{types-DrBaUwyV.d.mts → types-D-fYtKjb.d.mts} +33 -10
- package/dist/{types-CTYvcwHe.d.mts → types-DVfpSfx2.d.mts} +42 -1
- package/dist/utils/index.d.mts +1286 -2
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-_h9B3c57.mjs → utils-DC5ycPfr.mjs} +89 -40
- package/dist/{buildHandler-CcFOpJLh.mjs → validate-By96rH0r.mjs} +8 -299
- package/dist/{versioning-hmkPcDlX.d.mts → versioning-ZwX9tmbS.d.mts} +1 -1
- package/package.json +22 -29
- package/skills/arc/SKILL.md +299 -689
- package/skills/arc/references/auth.md +19 -7
- package/skills/arc-code-review/SKILL.md +1 -1
- package/skills/arc-code-review/references/arc-cheatsheet.md +100 -322
- package/dist/createActionRouter-S3MLVYot.mjs +0 -220
- package/dist/index-bRjYu21O.d.mts +0 -1320
- package/dist/org/index.d.mts +0 -66
- package/dist/org/index.mjs +0 -486
- package/dist/org/types.d.mts +0 -82
- package/dist/org/types.mjs +0 -1
- package/dist/registry-I-ogLgL9.mjs +0 -46
- /package/dist/{EventTransport-CT_52aWU.d.mts → EventTransport-C-2oAHtw.d.mts} +0 -0
- /package/dist/{EventTransport-DLWoUMHy.mjs → EventTransport-Hxvv5QQz.mjs} +0 -0
- /package/dist/{actionPermissions-CyUkQu6O.mjs → actionPermissions-Bjmvn7Eb.mjs} +0 -0
- /package/dist/{elevation-BXOWoGCF.d.mts → elevation-0YBpa663.d.mts} +0 -0
- /package/dist/{elevation-DgoeTyfX.mjs → elevation-Dci0AYLT.mjs} +0 -0
- /package/dist/{errorHandler-DFr45ZG4.d.mts → errorHandler-mHuyWzZE.d.mts} +0 -0
- /package/dist/{externalPaths-BD5nw6St.d.mts → externalPaths-DFg-2KTp.d.mts} +0 -0
- /package/dist/{interface-beEtJyWM.d.mts → interface-CH0OQudo.d.mts} +0 -0
- /package/dist/{interface-DfLGcus7.d.mts → interface-NwJ_qPlY.d.mts} +0 -0
- /package/dist/{keys-CGcCbNyu.mjs → keys-DopsCuyQ.mjs} +0 -0
- /package/dist/{loadResources-DBMQg_Aj.mjs → loadResources-ChQEj8ih.mjs} +0 -0
- /package/dist/{metrics-Qnvwc-LQ.mjs → metrics-TuOmguhi.mjs} +0 -0
- /package/dist/{replyHelpers-CK-FNO8E.mjs → replyHelpers-C-gD32oF.mjs} +0 -0
- /package/dist/{schemaIR-lYhC2gE5.mjs → schemaIR-Ctc89DSn.mjs} +0 -0
- /package/dist/{sessionManager-C4Le_UB3.d.mts → sessionManager-BqFegc0W.d.mts} +0 -0
- /package/dist/{storage-Dfzt4VTl.d.mts → storage-D2KZJAmn.d.mts} +0 -0
- /package/dist/{store-helpers-BkIN9-vu.mjs → store-helpers-B0sunfZZ.mjs} +0 -0
- /package/dist/{tracing-QJVprktp.d.mts → tracing-Dm8n7Cnn.d.mts} +0 -0
- /package/dist/{versioning-BUrT5aP4.mjs → versioning-B6mimogM.mjs} +0 -0
- /package/dist/{websocket-ChC2rqe1.d.mts → websocket-BkjeGZRn.d.mts} +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-
|
|
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 { A as assertValidConfig, l as getDefaultCrudSchemas } from "./utils-
|
|
4
|
-
import { t as BaseController } from "./BaseController-
|
|
5
|
-
import { t as applyPresets } from "./presets-
|
|
3
|
+
import { A as assertValidConfig, l as getDefaultCrudSchemas } from "./utils-DC5ycPfr.mjs";
|
|
4
|
+
import { t as BaseController } from "./BaseController-DlCCTIxJ.mjs";
|
|
5
|
+
import { t as applyPresets } from "./presets-C9BE6WaZ.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 { b as buildRequestScopeProjection, c as buildPreHandlerChain, d as resolveRoutePreHandlers, f as resolveRouterPluginMw, g as createRequestContext, h as createFastifyHandler, i as buildAuthMiddleware, l as buildRateLimitConfig, m as createCrudHandlers, o as buildCrudPermissionMw, p as selectPluginMw, r as buildArcDecorator, s as buildPipelineHandler, u as resolvePipelineSteps } from "./routerShared-
|
|
9
|
-
import { t as
|
|
8
|
+
import { b as buildRequestScopeProjection, c as buildPreHandlerChain, d as resolveRoutePreHandlers, f as resolveRouterPluginMw, g as createRequestContext, h as createFastifyHandler, i as buildAuthMiddleware, l as buildRateLimitConfig, m as createCrudHandlers, o as buildCrudPermissionMw, p as selectPluginMw, r as buildArcDecorator, s as buildPipelineHandler, u as resolvePipelineSteps } from "./routerShared-CZV5aabX.mjs";
|
|
9
|
+
import { t as pluralize } from "./pluralize-B9M8xvy-.mjs";
|
|
10
|
+
import { t as resolveActionPermission } from "./actionPermissions-Bjmvn7Eb.mjs";
|
|
10
11
|
//#region src/core/aggregation/defineAggregation.ts
|
|
11
12
|
/**
|
|
12
13
|
* Declare a single resource aggregation. Exported configs flow into
|
|
@@ -59,15 +60,27 @@ function defineAggregation(config) {
|
|
|
59
60
|
* `wrapHandler` is derived inline from `!route.raw`.
|
|
60
61
|
*/
|
|
61
62
|
function createCustomRoutes(fastify, routes, controller, options) {
|
|
62
|
-
const { tag, resourceName, arcDecorator, rateLimitConfig, pluginMw, pipeline, routeGuards } = options;
|
|
63
|
+
const { tag, resourceName, arcDecorator, rateLimitConfig, pluginMw, pipeline, routeGuards, tenantScopeMw } = options;
|
|
63
64
|
for (const route of routes) {
|
|
64
|
-
const
|
|
65
|
+
const routeWithRefs = route;
|
|
66
|
+
const hasHandler = route.handler !== void 0;
|
|
67
|
+
const hasControllerMethod = typeof routeWithRefs.controllerMethod === "function";
|
|
68
|
+
if (hasHandler && hasControllerMethod) throw new Error(`Route ${route.method} ${route.path}: pass either \`handler\` or \`controllerMethod\`, not both. Prefer \`controllerMethod: (c: MyController) => c.method\` for typed handler refs (TS catches typos).`);
|
|
69
|
+
if (!hasHandler && !hasControllerMethod) throw new Error(`Route ${route.method} ${route.path}: must declare either \`handler\` (string / function) or \`controllerMethod: (c) => c.method\` (typed function-ref form).`);
|
|
70
|
+
let resolvedHandler;
|
|
71
|
+
if (hasControllerMethod) {
|
|
72
|
+
if (!controller) throw new Error(`Route ${route.method} ${route.path}: \`controllerMethod\` requires a controller. Provide one via \`defineResource({ controller, … })\`, or use \`defineResource\` with an \`adapter\` so arc auto-creates a BaseController.`);
|
|
73
|
+
const referenced = routeWithRefs.controllerMethod?.(controller);
|
|
74
|
+
if (typeof referenced !== "function") throw new Error(`Route ${route.method} ${route.path}: \`controllerMethod\` did not return a function. Return the method itself: \`controllerMethod: (c) => c.myMethod\`.`);
|
|
75
|
+
resolvedHandler = referenced.bind(controller);
|
|
76
|
+
} else resolvedHandler = route.handler;
|
|
77
|
+
const opName = route.operation ?? (typeof resolvedHandler === "string" ? resolvedHandler : `${route.method.toLowerCase()}${route.path.replace(/[/:]/g, "_")}`);
|
|
65
78
|
const wrapHandler = !route.raw;
|
|
66
79
|
let handler;
|
|
67
|
-
if (typeof
|
|
68
|
-
if (!controller) throw new Error(`Route ${route.method} ${route.path}: string handler '${
|
|
69
|
-
const method = controller[
|
|
70
|
-
if (typeof method !== "function") throw new Error(`Handler '${
|
|
80
|
+
if (typeof resolvedHandler === "string") {
|
|
81
|
+
if (!controller) throw new Error(`Route ${route.method} ${route.path}: string handler '${resolvedHandler}' requires a controller. Either provide a controller or use a function handler instead.`);
|
|
82
|
+
const method = controller[resolvedHandler];
|
|
83
|
+
if (typeof method !== "function") throw new Error(`Handler '${resolvedHandler}' not found on controller`);
|
|
71
84
|
const boundMethod = method.bind(controller);
|
|
72
85
|
if (wrapHandler) {
|
|
73
86
|
const steps = resolvePipelineSteps(pipeline, opName);
|
|
@@ -75,8 +88,8 @@ function createCustomRoutes(fastify, routes, controller, options) {
|
|
|
75
88
|
} else handler = boundMethod;
|
|
76
89
|
} else if (wrapHandler) {
|
|
77
90
|
const steps = resolvePipelineSteps(pipeline, opName);
|
|
78
|
-
handler = steps.length > 0 ? buildPipelineHandler(
|
|
79
|
-
} else handler =
|
|
91
|
+
handler = steps.length > 0 ? buildPipelineHandler(resolvedHandler, steps, opName, resourceName) : createFastifyHandler(resolvedHandler);
|
|
92
|
+
} else handler = resolvedHandler;
|
|
80
93
|
const routeTags = route.tags ?? (tag ? [tag] : void 0);
|
|
81
94
|
const convertedSchema = route.schema ? convertRouteSchema(route.schema) : void 0;
|
|
82
95
|
const schema = {
|
|
@@ -86,6 +99,10 @@ function createCustomRoutes(fastify, routes, controller, options) {
|
|
|
86
99
|
...convertedSchema ?? {}
|
|
87
100
|
};
|
|
88
101
|
const customPreHandlers = resolveRoutePreHandlers(route.preHandler, fastify, `${route.method} ${route.path}`);
|
|
102
|
+
if (route.tenantScope === true) {
|
|
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
|
+
customPreHandlers.unshift(...tenantScopeMw);
|
|
105
|
+
}
|
|
89
106
|
const preHandler = buildPreHandlerChain({
|
|
90
107
|
preAuth: route.preAuth ?? [],
|
|
91
108
|
arcDecorator,
|
|
@@ -141,6 +158,7 @@ function createCrudRouter(fastify, controller, options = {}) {
|
|
|
141
158
|
update: middlewares.update ?? [],
|
|
142
159
|
delete: middlewares.delete ?? []
|
|
143
160
|
};
|
|
161
|
+
const tenantScopeMw = middlewares.tenantScope;
|
|
144
162
|
const idParamsSchema = {
|
|
145
163
|
type: "object",
|
|
146
164
|
properties: { id: { type: "string" } },
|
|
@@ -233,7 +251,8 @@ function createCrudRouter(fastify, controller, options = {}) {
|
|
|
233
251
|
rateLimitConfig,
|
|
234
252
|
pluginMw,
|
|
235
253
|
pipeline,
|
|
236
|
-
routeGuards
|
|
254
|
+
routeGuards,
|
|
255
|
+
tenantScopeMw
|
|
237
256
|
});
|
|
238
257
|
}
|
|
239
258
|
function buildCrudHandlers(ctrl, pipeline, resourceName) {
|
|
@@ -256,6 +275,28 @@ function createPermissionMiddleware(permission, resourceName, action) {
|
|
|
256
275
|
return buildCrudPermissionMw(permission, resourceName, action);
|
|
257
276
|
}
|
|
258
277
|
//#endregion
|
|
278
|
+
//#region src/core/defineAction.ts
|
|
279
|
+
/**
|
|
280
|
+
* Build an `ActionDefinition` with a typed handler. The literal schema
|
|
281
|
+
* type captured here flows into `data`, so `defineAction({ schema:
|
|
282
|
+
* z.object({...}), handler })` produces fully-typed code with no
|
|
283
|
+
* `as MyShape` cast.
|
|
284
|
+
*
|
|
285
|
+
* Behaviorally identical to a bare `ActionDefinition` object — same
|
|
286
|
+
* validation path (AJV), same permission resolution, same MCP wiring.
|
|
287
|
+
* The runtime shape is unchanged; only the type-level inference is new.
|
|
288
|
+
*/
|
|
289
|
+
function defineAction(config) {
|
|
290
|
+
return {
|
|
291
|
+
handler: config.handler,
|
|
292
|
+
permissions: config.permissions,
|
|
293
|
+
schema: config.schema,
|
|
294
|
+
description: config.description,
|
|
295
|
+
id: config.id,
|
|
296
|
+
mcp: config.mcp
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
//#endregion
|
|
259
300
|
//#region src/core/defineResource/controller.ts
|
|
260
301
|
/**
|
|
261
302
|
* Phase 4 — pick (or auto-create) the resource's controller.
|
|
@@ -739,6 +780,39 @@ function computeHasCrudRoutes(config) {
|
|
|
739
780
|
return !config.disableDefaultRoutes && CRUD_OPERATIONS.some((op) => !disabled.has(op));
|
|
740
781
|
}
|
|
741
782
|
//#endregion
|
|
783
|
+
//#region src/registry/resolveTenantPurge.ts
|
|
784
|
+
const DEFAULT_PRIORITY = 100;
|
|
785
|
+
function resolveTenantPurge(input) {
|
|
786
|
+
const { resourceName, tenantField, onTenantDelete } = input;
|
|
787
|
+
if (onTenantDelete) {
|
|
788
|
+
assertTenantFieldUsable(resourceName, tenantField, onTenantDelete.strategy.type);
|
|
789
|
+
return {
|
|
790
|
+
strategy: onTenantDelete.strategy,
|
|
791
|
+
priority: onTenantDelete.priority ?? DEFAULT_PRIORITY,
|
|
792
|
+
batchSize: onTenantDelete.batchSize,
|
|
793
|
+
source: "declared"
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
return {
|
|
797
|
+
strategy: {
|
|
798
|
+
type: "skip",
|
|
799
|
+
reason: "no `onTenantDelete` declared"
|
|
800
|
+
},
|
|
801
|
+
priority: DEFAULT_PRIORITY,
|
|
802
|
+
source: "disabled"
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Boot-time invariant: any non-skip strategy requires a real
|
|
807
|
+
* `tenantField`. Company-wide tables (`tenantField: false`) can't be
|
|
808
|
+
* cascaded by org. Throw rather than silently skip so the misconfig
|
|
809
|
+
* surfaces in CI / the auth-event path instead of leaking org data
|
|
810
|
+
* on the next delete.
|
|
811
|
+
*/
|
|
812
|
+
function assertTenantFieldUsable(resourceName, tenantField, strategyType) {
|
|
813
|
+
if (tenantField === false) throw new Error(`[Arc/Cascade] Resource '${resourceName}' declares onTenantDelete (strategy: ${strategyType}) but \`tenantField: false\`. Company-wide resources can't be cascaded by org — set a real \`tenantField\` or remove the \`onTenantDelete\` declaration.`);
|
|
814
|
+
}
|
|
815
|
+
//#endregion
|
|
742
816
|
//#region src/core/defineResource/plugin.ts
|
|
743
817
|
/**
|
|
744
818
|
* Build the CRUD schema map from the adapter's `OpenApiSchemas` plus
|
|
@@ -896,6 +970,7 @@ function normalizeActionsToRouterConfig(actions, resourceActionPermissions, tag,
|
|
|
896
970
|
const handlers = {};
|
|
897
971
|
const permissions = {};
|
|
898
972
|
const schemas = {};
|
|
973
|
+
const idLessActionNames = [];
|
|
899
974
|
for (const [name, entry] of Object.entries(actions)) {
|
|
900
975
|
const explicit = typeof entry !== "function" && entry.permissions ? entry.permissions : void 0;
|
|
901
976
|
if (typeof entry === "function") handlers[name] = entry;
|
|
@@ -903,6 +978,7 @@ function normalizeActionsToRouterConfig(actions, resourceActionPermissions, tag,
|
|
|
903
978
|
const def = entry;
|
|
904
979
|
handlers[name] = def.handler;
|
|
905
980
|
if (def.schema) schemas[name] = def.schema;
|
|
981
|
+
if (def.id === false) idLessActionNames.push(name);
|
|
906
982
|
}
|
|
907
983
|
const effective = resolveActionPermission({
|
|
908
984
|
action: entry,
|
|
@@ -922,7 +998,8 @@ function normalizeActionsToRouterConfig(actions, resourceActionPermissions, tag,
|
|
|
922
998
|
tag,
|
|
923
999
|
actions: handlers,
|
|
924
1000
|
actionPermissions: permissions,
|
|
925
|
-
actionSchemas: schemas
|
|
1001
|
+
actionSchemas: schemas,
|
|
1002
|
+
idLessActionNames
|
|
926
1003
|
};
|
|
927
1004
|
}
|
|
928
1005
|
/**
|
|
@@ -936,9 +1013,13 @@ function normalizeActionsToRouterConfig(actions, resourceActionPermissions, tag,
|
|
|
936
1013
|
*/
|
|
937
1014
|
function buildResourcePlugin(resource) {
|
|
938
1015
|
return async function resourcePlugin(fastify, _opts) {
|
|
939
|
-
const sharedRoot = fastify.server
|
|
1016
|
+
const sharedRoot = fastify.server;
|
|
940
1017
|
const isFirstMount = !resource._sharedStateRegisteredOn.has(sharedRoot);
|
|
941
1018
|
if (isFirstMount) resource._sharedStateRegisteredOn.add(sharedRoot);
|
|
1019
|
+
if (isFirstMount && resource._diagnostics?.length) for (const diagnostic of resource._diagnostics) {
|
|
1020
|
+
const level = diagnostic.severity === "info" ? "info" : "warn";
|
|
1021
|
+
fastify.log?.[level]?.(diagnostic.message);
|
|
1022
|
+
}
|
|
942
1023
|
const arc = fastify.arc;
|
|
943
1024
|
if (isFirstMount && arc?.registry && resource._registryMeta) try {
|
|
944
1025
|
arc.registry.register(resource, resource._registryMeta);
|
|
@@ -952,8 +1033,7 @@ function buildResourcePlugin(resource) {
|
|
|
952
1033
|
handler: hook.handler,
|
|
953
1034
|
priority: hook.priority
|
|
954
1035
|
});
|
|
955
|
-
const
|
|
956
|
-
if (isFirstMount && resource.cache?.invalidateOn && typeof registerRule === "function") for (const [pattern, tags] of Object.entries(resource.cache.invalidateOn)) registerRule({
|
|
1036
|
+
if (isFirstMount && resource.cache?.invalidateOn && fastify.registerCacheInvalidationRule) for (const [pattern, tags] of Object.entries(resource.cache.invalidateOn)) fastify.registerCacheInvalidationRule({
|
|
957
1037
|
pattern,
|
|
958
1038
|
tags
|
|
959
1039
|
});
|
|
@@ -984,7 +1064,7 @@ function buildResourcePlugin(resource) {
|
|
|
984
1064
|
idField: resource.idField
|
|
985
1065
|
});
|
|
986
1066
|
if (resource.actions && Object.keys(resource.actions).length > 0) {
|
|
987
|
-
const { createActionRouter } = await import("./createActionRouter-
|
|
1067
|
+
const { createActionRouter } = await import("./createActionRouter-DUpN3Dd1.mjs").then((n) => n.n);
|
|
988
1068
|
createActionRouter(typedInstance, {
|
|
989
1069
|
...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
|
|
990
1070
|
resourceName: resource.name,
|
|
@@ -998,8 +1078,12 @@ function buildResourcePlugin(resource) {
|
|
|
998
1078
|
});
|
|
999
1079
|
}
|
|
1000
1080
|
if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
|
|
1001
|
-
const { createAggregationRouter } = await import("./createAggregationRouter-
|
|
1002
|
-
const repoForAgg = resource.controller?.repository;
|
|
1081
|
+
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-Dq-TUCuY.mjs"), import("./validate-By96rH0r.mjs").then((n) => n.i)]);
|
|
1082
|
+
const repoForAgg = resource.controller?.repository ?? resource.repository;
|
|
1083
|
+
if (!adapterSupportsAggregate(repoForAgg)) {
|
|
1084
|
+
const undispatchable = Object.entries(resource.aggregations).filter(([, cfg]) => !cfg.materialized).map(([name]) => name);
|
|
1085
|
+
if (undispatchable.length > 0) throw new ArcAggregationConfigError(`Resource '${resource.name}' declares aggregations [${undispatchable.join(", ")}] but no repository implementing 'aggregate(req, options?)' is wired. Either (a) attach an adapter whose repo ships StandardRepo.aggregate (mongokit >= 3.13 / sqlitekit >= 0.3), (b) pass a custom controller exposing such a repository, or (c) declare 'materialized' on each aggregation so it dispatches through your own hook.`);
|
|
1086
|
+
}
|
|
1003
1087
|
const buildOptions = (req) => {
|
|
1004
1088
|
const ctrl = resource.controller;
|
|
1005
1089
|
if (!ctrl?.tenantRepoOptions) return {};
|
|
@@ -1059,13 +1143,40 @@ var ResourceDefinition = class {
|
|
|
1059
1143
|
pipe;
|
|
1060
1144
|
fields;
|
|
1061
1145
|
cache;
|
|
1146
|
+
/**
|
|
1147
|
+
* Per-resource MCP opt-out. `false` keeps the resource out of every
|
|
1148
|
+
* `mcpPlugin` registration regardless of the plugin's `expose` /
|
|
1149
|
+
* `include` allowlist — local opt-out is authoritative. See
|
|
1150
|
+
* `ResourceConfig.mcp` for the host-facing surface.
|
|
1151
|
+
*/
|
|
1152
|
+
mcp;
|
|
1062
1153
|
tenantField;
|
|
1154
|
+
/** Tenant-cleanup strategy on org delete — see `OnTenantDeleteConfig`. */
|
|
1155
|
+
onTenantDelete;
|
|
1156
|
+
/**
|
|
1157
|
+
* Resolved tenant-purge strategy — what `cascadeDeleteForOrganization`
|
|
1158
|
+
* actually runs. Computed once at boot from `onTenantDelete`. Audit /
|
|
1159
|
+
* introspection tooling reads this instead of re-running the rule at
|
|
1160
|
+
* the call site.
|
|
1161
|
+
*/
|
|
1162
|
+
resolvedTenantPurge;
|
|
1063
1163
|
idField;
|
|
1064
1164
|
queryParser;
|
|
1065
1165
|
_appliedPresets;
|
|
1066
1166
|
_pendingHooks;
|
|
1067
1167
|
_registryMeta;
|
|
1068
1168
|
/**
|
|
1169
|
+
* Boot-time validation diagnostics (non-fatal — hard errors throw
|
|
1170
|
+
* synchronously in `validateDefineResourceConfig`). Populated when
|
|
1171
|
+
* the host's config contains redundant / ambiguous flags that
|
|
1172
|
+
* shouldn't crash the boot but the host should clean up. Flushed
|
|
1173
|
+
* through `fastify.log.warn` on first mount inside
|
|
1174
|
+
* `buildResourcePlugin` so the host's configured logger owns the
|
|
1175
|
+
* output — the framework never speaks to `console.*` from `src/`
|
|
1176
|
+
* outside of the CLI.
|
|
1177
|
+
*/
|
|
1178
|
+
_diagnostics;
|
|
1179
|
+
/**
|
|
1069
1180
|
* Per-host idempotency guard used by `buildResourcePlugin` to
|
|
1070
1181
|
* skip duplicate shared-state writes when the same resource is
|
|
1071
1182
|
* mounted at multiple prefixes (`/v1`, `/v2`). See the plugin
|
|
@@ -1075,8 +1186,8 @@ var ResourceDefinition = class {
|
|
|
1075
1186
|
_sharedStateRegisteredOn = /* @__PURE__ */ new WeakSet();
|
|
1076
1187
|
constructor(config) {
|
|
1077
1188
|
this.name = config.name;
|
|
1078
|
-
this.displayName = config.displayName ??
|
|
1079
|
-
this.tag = config.tag ?? this.displayName;
|
|
1189
|
+
this.displayName = config.displayName ?? capitalize(config.name);
|
|
1190
|
+
this.tag = config.tag ?? pluralize(this.displayName);
|
|
1080
1191
|
this.prefix = config.prefix ?? `/${config.name}s`;
|
|
1081
1192
|
this.skipGlobalPrefix = config.skipGlobalPrefix ?? false;
|
|
1082
1193
|
this.adapter = config.adapter;
|
|
@@ -1099,7 +1210,14 @@ var ResourceDefinition = class {
|
|
|
1099
1210
|
this.pipe = config.pipe;
|
|
1100
1211
|
this.fields = config.fields;
|
|
1101
1212
|
this.cache = config.cache;
|
|
1213
|
+
this.mcp = config.mcp !== false;
|
|
1102
1214
|
this.tenantField = config.tenantField;
|
|
1215
|
+
this.onTenantDelete = config.onTenantDelete;
|
|
1216
|
+
this.resolvedTenantPurge = resolveTenantPurge({
|
|
1217
|
+
resourceName: this.name,
|
|
1218
|
+
tenantField: this.tenantField,
|
|
1219
|
+
onTenantDelete: this.onTenantDelete
|
|
1220
|
+
});
|
|
1103
1221
|
this.idField = config.idField;
|
|
1104
1222
|
this.queryParser = config.queryParser;
|
|
1105
1223
|
this._appliedPresets = config._appliedPresets ?? [];
|
|
@@ -1343,17 +1461,19 @@ const CRUD_OP_NAMES = new Set([
|
|
|
1343
1461
|
"get"
|
|
1344
1462
|
]);
|
|
1345
1463
|
/**
|
|
1346
|
-
* Run the structural validation pipeline.
|
|
1347
|
-
*
|
|
1348
|
-
*
|
|
1349
|
-
*
|
|
1464
|
+
* Run the structural validation pipeline.
|
|
1465
|
+
*
|
|
1466
|
+
* Throws synchronously on hard errors. Returns the (possibly empty)
|
|
1467
|
+
* list of non-fatal diagnostics — defineResource attaches them to the
|
|
1468
|
+
* resulting `ResourceDefinition` for the plugin layer to flush through
|
|
1469
|
+
* `fastify.log.warn` on first mount.
|
|
1350
1470
|
*/
|
|
1351
1471
|
function validateDefineResourceConfig(config) {
|
|
1352
1472
|
assertValidConfig(config, { skipControllerCheck: true });
|
|
1353
1473
|
validatePermissionsShape(config);
|
|
1354
1474
|
validateCustomRoutePermissions(config);
|
|
1355
1475
|
validateActionsShape(config);
|
|
1356
|
-
|
|
1476
|
+
return collectRedundantFieldRuleDiagnostics(config);
|
|
1357
1477
|
}
|
|
1358
1478
|
/** Permissions must be `PermissionCheck` functions, not arbitrary values. */
|
|
1359
1479
|
function validatePermissionsShape(config) {
|
|
@@ -1369,8 +1489,7 @@ function validateCustomRoutePermissions(config) {
|
|
|
1369
1489
|
for (const route of config.routes ?? []) if (typeof route.permissions !== "function") throw new Error(`[Arc] Resource '${config.name}' route ${route.method} ${route.path}: permissions is required and must be a PermissionCheck function.`);
|
|
1370
1490
|
}
|
|
1371
1491
|
/**
|
|
1372
|
-
* Surface common field-rule misconfigurations
|
|
1373
|
-
* just a `console.warn` so hosts notice and clean up.
|
|
1492
|
+
* Surface common field-rule misconfigurations as boot-time diagnostics.
|
|
1374
1493
|
*
|
|
1375
1494
|
* Catches:
|
|
1376
1495
|
* 1. `immutable: true` + `immutableAfterCreate: true` — `immutable`
|
|
@@ -1381,19 +1500,34 @@ function validateCustomRoutePermissions(config) {
|
|
|
1381
1500
|
* 3. `hidden: true` + `aggregable: false` — `hidden` already blocks
|
|
1382
1501
|
* aggregation; `aggregable: false` is redundant.
|
|
1383
1502
|
*
|
|
1384
|
-
* NOT
|
|
1385
|
-
* noisy in code review.
|
|
1503
|
+
* NOT hard errors — write-rule overlap is harmless at runtime, just
|
|
1504
|
+
* noisy in code review. Returned as `ResourceDiagnostic[]` so the
|
|
1505
|
+
* caller can route them through the host logger; never logged here.
|
|
1386
1506
|
*/
|
|
1387
|
-
function
|
|
1507
|
+
function collectRedundantFieldRuleDiagnostics(config) {
|
|
1388
1508
|
const fieldRules = config.schemaOptions?.fieldRules;
|
|
1389
|
-
if (!fieldRules) return;
|
|
1509
|
+
if (!fieldRules) return [];
|
|
1510
|
+
const diagnostics = [];
|
|
1390
1511
|
for (const [field, rule] of Object.entries(fieldRules)) {
|
|
1391
1512
|
if (!rule) continue;
|
|
1392
1513
|
const r = rule;
|
|
1393
|
-
if (r.immutable === true && r.immutableAfterCreate === true)
|
|
1394
|
-
|
|
1395
|
-
|
|
1514
|
+
if (r.immutable === true && r.immutableAfterCreate === true) diagnostics.push({
|
|
1515
|
+
severity: "warn",
|
|
1516
|
+
code: "field-rule-redundant-immutable",
|
|
1517
|
+
message: `[Arc] Resource '${config.name}' fieldRules.${field}: \`immutable: true\` already implies \`immutableAfterCreate: true\` — drop the second flag.`
|
|
1518
|
+
});
|
|
1519
|
+
if (r.systemManaged === true && r.readonly === true) diagnostics.push({
|
|
1520
|
+
severity: "warn",
|
|
1521
|
+
code: "field-rule-redundant-system-managed",
|
|
1522
|
+
message: `[Arc] Resource '${config.name}' fieldRules.${field}: \`systemManaged\` and \`readonly\` both strip writes — pick one (\`systemManaged\` is the canonical name).`
|
|
1523
|
+
});
|
|
1524
|
+
if (r.hidden === true && r.aggregable === false) diagnostics.push({
|
|
1525
|
+
severity: "warn",
|
|
1526
|
+
code: "field-rule-redundant-hidden",
|
|
1527
|
+
message: `[Arc] Resource '${config.name}' fieldRules.${field}: \`hidden: true\` already blocks aggregation — \`aggregable: false\` is redundant.`
|
|
1528
|
+
});
|
|
1396
1529
|
}
|
|
1530
|
+
return diagnostics;
|
|
1397
1531
|
}
|
|
1398
1532
|
/**
|
|
1399
1533
|
* Actions (v2.8) — name must not collide with CRUD ops; handler +
|
|
@@ -1430,9 +1564,11 @@ function validateActionsShape(config) {
|
|
|
1430
1564
|
* One internal boundary cast replaces N host-side casts.
|
|
1431
1565
|
*/
|
|
1432
1566
|
function defineResource(config) {
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1567
|
+
const normalisedConfig = resolveCrudAllowList(config);
|
|
1568
|
+
let diagnostics = [];
|
|
1569
|
+
if (!normalisedConfig.skipValidation) diagnostics = validateDefineResourceConfig(normalisedConfig);
|
|
1570
|
+
const repository = normalisedConfig.adapter?.repository;
|
|
1571
|
+
const configWithId = resolveIdField(normalisedConfig, repository);
|
|
1436
1572
|
const resolvedConfig = applyPresetsAndAutoInject(configWithId);
|
|
1437
1573
|
const hasCrudRoutes = computeHasCrudRoutes(resolvedConfig);
|
|
1438
1574
|
const narrowedConfig = resolvedConfig;
|
|
@@ -1443,14 +1579,56 @@ function defineResource(config) {
|
|
|
1443
1579
|
adapter: configWithId.adapter,
|
|
1444
1580
|
controller
|
|
1445
1581
|
});
|
|
1446
|
-
if (!
|
|
1582
|
+
if (!normalisedConfig.skipValidation && controller) resource._validateControllerMethods();
|
|
1447
1583
|
wireHooks(resource, narrowedConfig, configWithId.hooks);
|
|
1448
|
-
if (!
|
|
1584
|
+
if (!normalisedConfig.skipRegistry) {
|
|
1449
1585
|
const registryMeta = resolveOpenApiSchemas(narrowedConfig);
|
|
1450
1586
|
if (registryMeta) resource._registryMeta = registryMeta;
|
|
1451
1587
|
}
|
|
1588
|
+
if (diagnostics.length > 0) resource._diagnostics = diagnostics;
|
|
1452
1589
|
return resource;
|
|
1453
1590
|
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Normalise the 2.16 `crud:` positive-form allow-list into the canonical
|
|
1593
|
+
* `{ disabledRoutes, disableDefaultRoutes }` pair the rest of arc reads.
|
|
1594
|
+
*
|
|
1595
|
+
* Three input forms collapse to one output:
|
|
1596
|
+
* - `crud: false` → `disableDefaultRoutes: true`
|
|
1597
|
+
* - `crud: { list: true }` → `disabledRoutes: [get,create,update,delete]`
|
|
1598
|
+
* - legacy `disabledRoutes` → passed through unchanged
|
|
1599
|
+
*
|
|
1600
|
+
* Mutually exclusive: `crud` + `disabledRoutes` together is a config bug
|
|
1601
|
+
* (the host meant ONE of two intents) — throw rather than pick.
|
|
1602
|
+
*
|
|
1603
|
+
* Lifted out of the `ResourceDefinition` constructor in 2.16 so the
|
|
1604
|
+
* validator (Phase 1) observes the post-resolve shape — `crud: false`
|
|
1605
|
+
* now looks like `disableDefaultRoutes: true` to the validator, so it
|
|
1606
|
+
* doesn't false-positive "Data adapter required when CRUD routes are
|
|
1607
|
+
* enabled" on a host that explicitly opted CRUD out.
|
|
1608
|
+
*/
|
|
1609
|
+
function resolveCrudAllowList(config) {
|
|
1610
|
+
const { crud, disabledRoutes: legacyDisabled, disableDefaultRoutes: legacyDisableAll } = config;
|
|
1611
|
+
if (crud === void 0) return config;
|
|
1612
|
+
if (legacyDisabled !== void 0) throw new Error(`[Arc] Resource '${config.name}': pass either \`crud\` (positive allow-list) or \`disabledRoutes\` (negative opt-out), not both. The positive form is the documented default going forward; drop \`disabledRoutes\` when both are set.`);
|
|
1613
|
+
if (crud === false) return {
|
|
1614
|
+
...config,
|
|
1615
|
+
crud: void 0,
|
|
1616
|
+
disableDefaultRoutes: true
|
|
1617
|
+
};
|
|
1618
|
+
const disabled = [
|
|
1619
|
+
"list",
|
|
1620
|
+
"get",
|
|
1621
|
+
"create",
|
|
1622
|
+
"update",
|
|
1623
|
+
"delete"
|
|
1624
|
+
].filter((op) => crud[op] !== true);
|
|
1625
|
+
return {
|
|
1626
|
+
...config,
|
|
1627
|
+
crud: void 0,
|
|
1628
|
+
disabledRoutes: disabled,
|
|
1629
|
+
disableDefaultRoutes: legacyDisableAll ?? false
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1454
1632
|
//#endregion
|
|
1455
1633
|
//#region src/core/defineResourceVariants.ts
|
|
1456
1634
|
/**
|
|
@@ -1550,4 +1728,4 @@ function getEntityQuery(req) {
|
|
|
1550
1728
|
return { [getEntityIdField(req)]: id };
|
|
1551
1729
|
}
|
|
1552
1730
|
//#endregion
|
|
1553
|
-
export { defineResource as a,
|
|
1731
|
+
export { defineResource as a, createCrudRouter as c, defineResourceVariants as i, createPermissionMiddleware as l, getEntityIdField as n, ResourceDefinition as o, getEntityQuery as r, defineAction as s, getEntityId as t, defineAggregation as u };
|