@classytic/arc 2.23.0 → 2.28.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 +2 -0
- package/dist/{BaseController-BQ8KNvWC.mjs → BaseController-B65zbz9m.mjs} +94 -23
- package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
- package/dist/actionPermissions-CAphcmUG.mjs +24 -0
- package/dist/{applyPermissionResult-Tw4iXtlf.mjs → applyPermissionResult-BnyVE9eD.mjs} +10 -0
- package/dist/audit/index.d.mts +32 -3
- package/dist/audit/index.mjs +7 -4
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +263 -259
- package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
- package/dist/cache/index.d.mts +41 -3
- package/dist/cache/index.mjs +33 -4
- package/dist/cleanup/index.d.mts +313 -0
- package/dist/cleanup/index.mjs +1267 -0
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/index.mjs +2 -2
- package/dist/context/index.mjs +2 -64
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -4
- package/dist/core-BGa8TXJR.mjs +221 -0
- package/dist/{createActionRouter-BuOv-m4Y.mjs → createActionRouter-C-N7z2EL.mjs} +3 -2
- package/dist/{createAggregationRouter-CgJv_BU_.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
- package/dist/{createApp-CjOeWJ9_.mjs → createApp-Du5G54TM.mjs} +187 -96
- package/dist/{core-DQ6rDTyH.mjs → defineResource-2zvVVpxg.mjs} +297 -230
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
- package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
- package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
- package/dist/events/index.d.mts +83 -314
- package/dist/events/index.mjs +183 -95
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +24 -2
- package/dist/factory/index.d.mts +5 -5
- package/dist/factory/index.mjs +30 -8
- package/dist/{fastify-augmentation-g_f9omSc.d.mts → fastify-augmentation-OzYGIiU1.d.mts} +26 -2
- package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
- package/dist/headers-BvlQSDMB.mjs +45 -0
- package/dist/health-CB_m4S32.mjs +377 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +27 -3
- package/dist/{index-wMoWBLkn.d.mts → index-BMnbzvtO.d.mts} +3 -3
- package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
- package/dist/index-CZY4EGga.d.mts +107 -0
- package/dist/{index-CC5ruDGh.d.mts → index-CjVg-Epg.d.mts} +37 -5
- package/dist/{index-BBsvQt7U.d.mts → index-DPRH0Cme.d.mts} +36 -106
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +7 -6
- package/dist/{init-CjO4p_q1.mjs → init-tlRSt667.mjs} +206 -208
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +3 -3
- package/dist/integrations/jobs/index.d.mts +2 -0
- package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
- package/dist/integrations/mcp/index.d.mts +4 -4
- package/dist/integrations/mcp/index.mjs +2 -2
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +37 -30
- package/dist/integrations/webhooks.mjs +51 -62
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +41 -14
- package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
- package/dist/{mcpPlugin-BtzIL_kM.mjs → mcpPlugin-Cs6nRGCt.mjs} +12 -10
- package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
- package/dist/middleware/index.d.mts +17 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/migrations/index.d.mts +94 -44
- package/dist/migrations/index.mjs +196 -44
- package/dist/module-_-M7C5ES.mjs +29 -0
- package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
- package/dist/{openapi-jSDx-VDv.mjs → openapi-BLV4OMXb.mjs} +3 -3
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +3 -3
- package/dist/{permissions-tbxFHxjZ.mjs → permissions-BrtiBKGs.mjs} +229 -229
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/{actionPermissions-lszoJle1.mjs → pipeline-BGRnV-To.mjs} +4 -27
- package/dist/plugins/index.d.mts +6 -77
- package/dist/plugins/index.mjs +11 -95
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{index-Ba10fdKp.d.mts → preloadResources-CM4uUaFv.d.mts} +51 -50
- package/dist/preloadResources-DSY5HGpS.mjs +45 -0
- package/dist/presets/filesUpload.d.mts +3 -3
- package/dist/presets/filesUpload.mjs +3 -3
- package/dist/presets/index.d.mts +2 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-BOe4Ugkl.mjs → presets-DwFDVKP0.mjs} +2 -2
- package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
- package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
- package/dist/{registerRoute-CWO6mdYe.mjs → rateLimit-DrED-C4v.mjs} +2 -109
- package/dist/{realtime-I8OrfnMR.mjs → realtime-DEuDu9io.mjs} +3 -3
- package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
- package/dist/registerRoute-DTp18Ah_.mjs +108 -0
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/{registry-vcOCtPij.mjs → registry-9km_w1aP.mjs} +15 -0
- package/dist/renewingLease-3cuQH7h8.mjs +44 -0
- package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
- package/dist/schedules-CjOd2MG9.mjs +113 -0
- package/dist/schemas/index.d.mts +67 -67
- package/dist/schemas/index.mjs +2 -3
- package/dist/scim/index.mjs +1 -2
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +4 -4
- package/dist/{scope-LONQ3mfZ.mjs → scope-DfaPntpo.mjs} +4 -5
- package/dist/{sse-0u4P4-OI.mjs → sse-BQ1w0dfl.mjs} +1 -1
- package/dist/testing/cleanupStoreContract.d.mts +10 -0
- package/dist/testing/cleanupStoreContract.mjs +375 -0
- package/dist/testing/index.d.mts +5 -3
- package/dist/testing/index.mjs +4 -2
- package/dist/testing/outboxStoreContract.d.mts +13 -0
- package/dist/testing/outboxStoreContract.mjs +247 -0
- package/dist/transactionContext-PNdoyLLm.mjs +64 -0
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
- package/dist/types-C_ZZRcD6.d.mts +450 -0
- package/dist/{module-B4u10iOJ.d.mts → types-Cu6vVJmh.d.mts} +196 -60
- package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
- package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
- package/dist/{types-5OuqeTDX.d.mts → types-QFGc3Cjv.d.mts} +1 -1
- package/dist/usage/index.d.mts +3 -52
- package/dist/usage/index.mjs +0 -0
- package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
- package/dist/utils/index.d.mts +58 -85
- package/dist/utils/index.mjs +4 -2
- package/dist/{utils-CZyeRXTh.mjs → utils-taUVD4p3.mjs} +47 -337
- package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
- package/dist/{versioning-D6PvCwpq.d.mts → versioning-DXgY5BUm.d.mts} +11 -29
- package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
- package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
- package/package.json +541 -513
- package/skills/arc/references/api-reference.md +3 -1
- package/skills/arc/references/integrations.md +20 -11
- package/dist/QueryCache-Z1B38koh.d.mts +0 -40
- package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
- package/dist/health-ScyjIVo5.mjs +0 -202
- package/dist/requestId-5BhTVzBv.mjs +0 -113
- package/dist/types-XTApa8b6.mjs +0 -24
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-DNre0wRg.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
3
|
import { r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { a as
|
|
8
|
-
import {
|
|
9
|
-
import { t as
|
|
10
|
-
import {
|
|
11
|
-
import { N as assertValidConfig, l as getDefaultCrudSchemas } from "./utils-CZyeRXTh.mjs";
|
|
4
|
+
import { n as getUserRoles } from "./userHelpers-Cg_y3fBz.mjs";
|
|
5
|
+
import { b as isElevated, n as PUBLIC_SCOPE, x as isMember } from "./types-CSyCxl2V.mjs";
|
|
6
|
+
import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-B65zbz9m.mjs";
|
|
7
|
+
import { a as buildRouteHooks, c as buildAuthMiddleware, i as selectPluginMw, n as buildRouteConfig, o as resolveRoutePreHandlers, r as resolveRouterPluginMw, s as routeHookOptions, t as buildRateLimitConfig, u as buildArcDecorator } from "./rateLimit-DrED-C4v.mjs";
|
|
8
|
+
import { a as applyFieldWritePermissions, s as resolveEffectiveRoles } from "./applyPermissionResult-BnyVE9eD.mjs";
|
|
9
|
+
import { r as buildCrudPermissionMw, t as tryRegisterRoute } from "./registerRoute-DTp18Ah_.mjs";
|
|
10
|
+
import { c as getDefaultCrudSchemas } from "./utils-taUVD4p3.mjs";
|
|
12
11
|
import { n as convertRouteSchema, r as ensureSchemaConverter, t as convertOpenApiSchemas } from "./schemaConverter-DJvodiHj.mjs";
|
|
13
12
|
import { i as pipeUIMessageStreamToReply, r as isReadableStream } from "./streaming-BQcW6_Kq.mjs";
|
|
14
13
|
import { t as hasEvents } from "./typeGuards-C9KXUly6.mjs";
|
|
15
|
-
import { a as
|
|
14
|
+
import { a as createFastifyHandler, i as createCrudHandlers, n as buildPipelineHandler, o as createRequestContext, r as resolvePipelineSteps, u as buildRequestScopeProjection } from "./pipeline-BGRnV-To.mjs";
|
|
15
|
+
import { A as requireAuth } from "./permissions-BrtiBKGs.mjs";
|
|
16
|
+
import { r as getAvailablePresets, t as applyPresets } from "./presets-DwFDVKP0.mjs";
|
|
16
17
|
import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
|
|
18
|
+
import { t as resolveActionPermission } from "./actionPermissions-CAphcmUG.mjs";
|
|
17
19
|
//#region src/core/middlewares/fieldWrite.ts
|
|
18
20
|
/**
|
|
19
21
|
* Build a preHandler that enforces field-write permissions on the request body.
|
|
@@ -53,52 +55,6 @@ function methodCarriesBody(method) {
|
|
|
53
55
|
return m === "POST" || m === "PUT" || m === "PATCH";
|
|
54
56
|
}
|
|
55
57
|
//#endregion
|
|
56
|
-
//#region src/core/aggregation/defineAggregation.ts
|
|
57
|
-
/**
|
|
58
|
-
* Declare a single resource aggregation. Exported configs flow into
|
|
59
|
-
* `defineResource({ aggregations: { ... } })` either as named keys or
|
|
60
|
-
* via auto-discovery patterns (loadAggregations — future).
|
|
61
|
-
*
|
|
62
|
-
* @example Inline
|
|
63
|
-
* ```ts
|
|
64
|
-
* defineResource({
|
|
65
|
-
* name: 'order',
|
|
66
|
-
* aggregations: {
|
|
67
|
-
* revenueByStatus: defineAggregation({
|
|
68
|
-
* groupBy: 'status',
|
|
69
|
-
* measures: { count: 'count', revenue: 'sum:totalPrice' },
|
|
70
|
-
* permissions: requireRoles(['admin']),
|
|
71
|
-
* }),
|
|
72
|
-
* },
|
|
73
|
-
* });
|
|
74
|
-
* ```
|
|
75
|
-
*
|
|
76
|
-
* @example Multi-file
|
|
77
|
-
* ```ts
|
|
78
|
-
* // orders/aggregations/revenue-by-status.ts
|
|
79
|
-
* import { defineAggregation } from '@classytic/arc';
|
|
80
|
-
*
|
|
81
|
-
* export const revenueByStatus = defineAggregation({
|
|
82
|
-
* groupBy: 'status',
|
|
83
|
-
* measures: { count: 'count', revenue: 'sum:totalPrice' },
|
|
84
|
-
* permissions: requireRoles(['admin']),
|
|
85
|
-
* timeout: 5000,
|
|
86
|
-
* cache: { staleTime: 60 },
|
|
87
|
-
* });
|
|
88
|
-
*
|
|
89
|
-
* // orders/order.resource.ts
|
|
90
|
-
* import * as aggregations from './aggregations/index.js';
|
|
91
|
-
*
|
|
92
|
-
* defineResource({
|
|
93
|
-
* name: 'order',
|
|
94
|
-
* aggregations, // 30+ entries flow in
|
|
95
|
-
* });
|
|
96
|
-
* ```
|
|
97
|
-
*/
|
|
98
|
-
function defineAggregation(config) {
|
|
99
|
-
return config;
|
|
100
|
-
}
|
|
101
|
-
//#endregion
|
|
102
58
|
//#region src/core/createCrudRouter.ts
|
|
103
59
|
/**
|
|
104
60
|
* Mount custom routes (from presets or user-defined `routes`) on Fastify.
|
|
@@ -338,80 +294,6 @@ function createPermissionMiddleware(permission, resourceName, action) {
|
|
|
338
294
|
return buildCrudPermissionMw(permission, resourceName, action);
|
|
339
295
|
}
|
|
340
296
|
//#endregion
|
|
341
|
-
//#region src/core/defineAction.ts
|
|
342
|
-
/**
|
|
343
|
-
* Build an `ActionDefinition` with a typed handler. The literal schema
|
|
344
|
-
* type captured here flows into `data`, so `defineAction({ schema:
|
|
345
|
-
* z.object({...}), handler })` produces fully-typed code with no
|
|
346
|
-
* `as MyShape` cast.
|
|
347
|
-
*
|
|
348
|
-
* Behaviorally identical to a bare `ActionDefinition` object — same
|
|
349
|
-
* validation path (AJV), same permission resolution, same MCP wiring.
|
|
350
|
-
* The runtime shape is unchanged; only the type-level inference is new.
|
|
351
|
-
*/
|
|
352
|
-
function defineAction(config) {
|
|
353
|
-
return {
|
|
354
|
-
handler: config.handler,
|
|
355
|
-
permissions: config.permissions,
|
|
356
|
-
schema: config.schema,
|
|
357
|
-
description: config.description,
|
|
358
|
-
id: config.id,
|
|
359
|
-
mcp: config.mcp
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
//#endregion
|
|
363
|
-
//#region src/core/defineResource/seams.ts
|
|
364
|
-
/** Plain object test — literals only; class instances must replace, not merge. */
|
|
365
|
-
function isPlainObject(value) {
|
|
366
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
367
|
-
const proto = Object.getPrototypeOf(value);
|
|
368
|
-
return proto === Object.prototype || proto === null;
|
|
369
|
-
}
|
|
370
|
-
/** Recursive plain-object merge; nested arrays REPLACE (they're value lists). */
|
|
371
|
-
function mergePlain(base, next) {
|
|
372
|
-
const out = { ...base };
|
|
373
|
-
for (const [key, value] of Object.entries(next)) {
|
|
374
|
-
if (value === void 0) continue;
|
|
375
|
-
const existing = out[key];
|
|
376
|
-
out[key] = isPlainObject(existing) && isPlainObject(value) ? mergePlain(existing, value) : value;
|
|
377
|
-
}
|
|
378
|
-
return out;
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Canonical slot-aware merge of a base resource config with host seams.
|
|
382
|
-
* Later seams win over earlier ones; `undefined` values never clobber.
|
|
383
|
-
*
|
|
384
|
-
* Merge semantics (value-driven — no per-slot registry to drift):
|
|
385
|
-
*
|
|
386
|
-
* - **Top-level arrays CONCAT** — `routes`, `middlewares`, `presets`,
|
|
387
|
-
* `routeGuards`, `disabledRoutes`: additive collections, base first.
|
|
388
|
-
* - **Plain objects merge recursively** — `permissions`, `actions`,
|
|
389
|
-
* `schemaOptions` (incl. `fieldRules`), `aggregations`, `customSchemas`,
|
|
390
|
-
* `fields`, `cache`, `events`: per-key composition. Arrays NESTED inside
|
|
391
|
-
* them replace (they're value lists — enums, sort specs), not concat.
|
|
392
|
-
* - **Everything else last-wins** — class instances (`adapter`,
|
|
393
|
-
* `controller`, `queryParser`), functions, scalars.
|
|
394
|
-
*
|
|
395
|
-
* The single `as` at the return is the one documented erasure for the
|
|
396
|
-
* {@link AdapterLike}-widened seam slots — inside the helper so no module
|
|
397
|
-
* author ever writes one (mirrors mongokit's one-cast-at-the-boundary rule).
|
|
398
|
-
*/
|
|
399
|
-
function mergeResourceConfig(base, ...seams) {
|
|
400
|
-
const out = { ...base };
|
|
401
|
-
for (const seam of seams) {
|
|
402
|
-
if (!seam) continue;
|
|
403
|
-
for (const [key, value] of Object.entries(seam)) {
|
|
404
|
-
if (value === void 0) continue;
|
|
405
|
-
const existing = out[key];
|
|
406
|
-
if (Array.isArray(existing) && Array.isArray(value)) out[key] = [...existing, ...value];
|
|
407
|
-
else if (isPlainObject(existing) && isPlainObject(value)) out[key] = mergePlain(existing, value);
|
|
408
|
-
else if (Array.isArray(value)) out[key] = [...value];
|
|
409
|
-
else out[key] = value;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
return out;
|
|
413
|
-
}
|
|
414
|
-
//#endregion
|
|
415
297
|
//#region src/core/defineResource/controller.ts
|
|
416
298
|
/**
|
|
417
299
|
* Phase 4 — pick (or auto-create) the resource's controller.
|
|
@@ -1396,7 +1278,7 @@ function buildResourcePlugin(resource) {
|
|
|
1396
1278
|
extensions: resource.extensions
|
|
1397
1279
|
});
|
|
1398
1280
|
if (resource.actions && Object.keys(resource.actions).length > 0) {
|
|
1399
|
-
const { createActionRouter } = await import("./createActionRouter-
|
|
1281
|
+
const { createActionRouter } = await import("./createActionRouter-C-N7z2EL.mjs").then((n) => n.n);
|
|
1400
1282
|
createActionRouter(typedInstance, {
|
|
1401
1283
|
...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
|
|
1402
1284
|
resourceName: resource.name,
|
|
@@ -1411,7 +1293,7 @@ function buildResourcePlugin(resource) {
|
|
|
1411
1293
|
});
|
|
1412
1294
|
}
|
|
1413
1295
|
if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
|
|
1414
|
-
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-
|
|
1296
|
+
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-wzwFa_dW.mjs"), import("./validate-Bk44F2yT.mjs").then((n) => n.r)]);
|
|
1415
1297
|
const repoForAgg = resource.controller?.repository ?? resource.repository;
|
|
1416
1298
|
if (!adapterSupportsAggregate(repoForAgg)) {
|
|
1417
1299
|
const undispatchable = Object.entries(resource.aggregations).filter(([, cfg]) => !cfg.materialized).map(([name]) => name);
|
|
@@ -1861,6 +1743,289 @@ function mergeUserOpenApiOverrides(openApiSchemas, userOverrides) {
|
|
|
1861
1743
|
};
|
|
1862
1744
|
}
|
|
1863
1745
|
//#endregion
|
|
1746
|
+
//#region src/core/validateResourceConfig.ts
|
|
1747
|
+
/**
|
|
1748
|
+
* Resource Configuration Validator
|
|
1749
|
+
*
|
|
1750
|
+
* Fail-fast validation at definition time.
|
|
1751
|
+
* Invalid configs throw immediately with clear, actionable errors.
|
|
1752
|
+
*
|
|
1753
|
+
* @example
|
|
1754
|
+
* const result = validateResourceConfig(config);
|
|
1755
|
+
* if (!result.valid) {
|
|
1756
|
+
* console.error(formatValidationErrors(result.errors));
|
|
1757
|
+
* }
|
|
1758
|
+
*/
|
|
1759
|
+
/**
|
|
1760
|
+
* Validate a resource configuration
|
|
1761
|
+
*/
|
|
1762
|
+
function validateResourceConfig(config, options = {}) {
|
|
1763
|
+
const errors = [];
|
|
1764
|
+
const warnings = [];
|
|
1765
|
+
if (!config.name) errors.push({
|
|
1766
|
+
field: "name",
|
|
1767
|
+
message: "Resource name is required",
|
|
1768
|
+
suggestion: "Add a unique resource name (e.g., \"product\", \"user\")"
|
|
1769
|
+
});
|
|
1770
|
+
else if (!/^[a-z][a-z0-9-]*$/i.test(config.name)) errors.push({
|
|
1771
|
+
field: "name",
|
|
1772
|
+
message: `Invalid resource name "${config.name}"`,
|
|
1773
|
+
suggestion: "Use alphanumeric characters and hyphens, starting with a letter"
|
|
1774
|
+
});
|
|
1775
|
+
const crudRoutes = CRUD_OPERATIONS;
|
|
1776
|
+
const disabledRoutes = new Set(config.disabledRoutes ?? []);
|
|
1777
|
+
const enabledCrudRoutes = crudRoutes.filter((route) => !disabledRoutes.has(route));
|
|
1778
|
+
if (!config.disableDefaultRoutes && enabledCrudRoutes.length > 0) {
|
|
1779
|
+
if (!config.adapter) errors.push({
|
|
1780
|
+
field: "adapter",
|
|
1781
|
+
message: "Data adapter is required when CRUD routes are enabled",
|
|
1782
|
+
suggestion: "Provide an adapter: createMongooseAdapter({ model, repository })"
|
|
1783
|
+
});
|
|
1784
|
+
else if (!config.adapter.repository) errors.push({
|
|
1785
|
+
field: "adapter.repository",
|
|
1786
|
+
message: "Adapter must provide a repository",
|
|
1787
|
+
suggestion: "Ensure your adapter returns a valid StandardRepo (see @classytic/repo-core)"
|
|
1788
|
+
});
|
|
1789
|
+
} else if (!config.adapter && !config.routes?.length) warnings.push({
|
|
1790
|
+
field: "config",
|
|
1791
|
+
message: "Resource has no adapter and no routes",
|
|
1792
|
+
suggestion: "Provide either adapter for CRUD or routes for custom logic"
|
|
1793
|
+
});
|
|
1794
|
+
if (config.controller && !options.skipControllerCheck && !config.disableDefaultRoutes) {
|
|
1795
|
+
const ctrl = config.controller;
|
|
1796
|
+
const requiredMethods = CRUD_OPERATIONS;
|
|
1797
|
+
for (const method of requiredMethods) if (typeof ctrl[method] !== "function") errors.push({
|
|
1798
|
+
field: `controller.${method}`,
|
|
1799
|
+
message: `Missing required CRUD method "${method}"`,
|
|
1800
|
+
suggestion: "Extend BaseController which implements IController interface"
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
if (config.controller && config.routes) validateRouteHandlers(config.controller, config.routes, errors);
|
|
1804
|
+
if (config.permissions) validatePermissionKeys(config, options, errors, warnings);
|
|
1805
|
+
if (config.presets && !options.allowUnknownPresets) validatePresets(config.presets, errors, warnings);
|
|
1806
|
+
if (config.prefix) {
|
|
1807
|
+
if (!config.prefix.startsWith("/")) errors.push({
|
|
1808
|
+
field: "prefix",
|
|
1809
|
+
message: `Prefix must start with "/" (got "${config.prefix}")`,
|
|
1810
|
+
suggestion: `Change to "/${config.prefix}"`
|
|
1811
|
+
});
|
|
1812
|
+
if (config.prefix.endsWith("/") && config.prefix !== "/") warnings.push({
|
|
1813
|
+
field: "prefix",
|
|
1814
|
+
message: `Prefix should not end with "/" (got "${config.prefix}")`,
|
|
1815
|
+
suggestion: `Change to "${config.prefix.slice(0, -1)}"`
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
if (config.routes) {
|
|
1819
|
+
validateRoutes(config.routes, errors);
|
|
1820
|
+
validateRouteCrudCollisions(config, errors);
|
|
1821
|
+
}
|
|
1822
|
+
return {
|
|
1823
|
+
valid: errors.length === 0,
|
|
1824
|
+
errors,
|
|
1825
|
+
warnings
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
function validateRouteHandlers(controller, routes, errors) {
|
|
1829
|
+
const ctrl = controller;
|
|
1830
|
+
for (const route of routes) if (typeof route.handler === "string") {
|
|
1831
|
+
if (typeof ctrl[route.handler] !== "function") errors.push({
|
|
1832
|
+
field: `routes[${route.method} ${route.path}]`,
|
|
1833
|
+
message: `Handler "${route.handler}" not found on controller`,
|
|
1834
|
+
suggestion: `Add method "${route.handler}" to controller or use a function handler`
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
function validatePermissionKeys(config, options, _errors, warnings) {
|
|
1839
|
+
const validKeys = /* @__PURE__ */ new Set([...CRUD_OPERATIONS, ...options.additionalPermissionKeys ?? []]);
|
|
1840
|
+
for (const route of config.routes ?? []) if (typeof route.handler === "string") validKeys.add(route.handler);
|
|
1841
|
+
for (const preset of config.presets ?? []) {
|
|
1842
|
+
const presetName = typeof preset === "string" ? preset : preset.name;
|
|
1843
|
+
if (presetName === "softDelete") {
|
|
1844
|
+
validKeys.add("deleted");
|
|
1845
|
+
validKeys.add("restore");
|
|
1846
|
+
}
|
|
1847
|
+
if (presetName === "slugLookup") validKeys.add("getBySlug");
|
|
1848
|
+
if (presetName === "tree") {
|
|
1849
|
+
validKeys.add("tree");
|
|
1850
|
+
validKeys.add("children");
|
|
1851
|
+
validKeys.add("getTree");
|
|
1852
|
+
validKeys.add("getChildren");
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
for (const key of Object.keys(config.permissions ?? {})) if (!validKeys.has(key)) warnings.push({
|
|
1856
|
+
field: `permissions.${key}`,
|
|
1857
|
+
message: `Unknown permission key "${key}"`,
|
|
1858
|
+
suggestion: `Valid keys: ${Array.from(validKeys).join(", ")}`
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
function validatePresets(presets, errors, warnings) {
|
|
1862
|
+
const availablePresets = getAvailablePresets();
|
|
1863
|
+
for (const preset of presets) {
|
|
1864
|
+
if (typeof preset === "object" && ("middlewares" in preset || "routes" in preset)) continue;
|
|
1865
|
+
const presetName = typeof preset === "string" ? preset : preset.name;
|
|
1866
|
+
if (!availablePresets.includes(presetName)) errors.push({
|
|
1867
|
+
field: "presets",
|
|
1868
|
+
message: `Unknown preset "${presetName}"`,
|
|
1869
|
+
suggestion: `Available presets: ${availablePresets.join(", ")}`
|
|
1870
|
+
});
|
|
1871
|
+
if (typeof preset === "object") validatePresetOptions(preset, warnings);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
function validatePresetOptions(preset, warnings) {
|
|
1875
|
+
const validOptions = {
|
|
1876
|
+
slugLookup: ["slugField"],
|
|
1877
|
+
tree: ["parentField"],
|
|
1878
|
+
softDelete: ["deletedField"],
|
|
1879
|
+
ownedByUser: ["ownerField"],
|
|
1880
|
+
multiTenant: ["tenantField", "allowPublic"]
|
|
1881
|
+
}[preset.name] ?? [];
|
|
1882
|
+
const providedOptions = Object.keys(preset).filter((k) => k !== "name");
|
|
1883
|
+
for (const opt of providedOptions) if (!validOptions.includes(opt)) warnings.push({
|
|
1884
|
+
field: `presets[${preset.name}].${opt}`,
|
|
1885
|
+
message: `Unknown option "${opt}" for preset "${preset.name}"`,
|
|
1886
|
+
suggestion: validOptions.length > 0 ? `Valid options: ${validOptions.join(", ")}` : `Preset "${preset.name}" has no configurable options`
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Detect collisions between user-declared `routes:` entries and the
|
|
1891
|
+
* auto-CRUD route table that `createCrudRouter` will register.
|
|
1892
|
+
*
|
|
1893
|
+
* Without this check, a custom `POST /` on the same prefix as auto-CRUD's
|
|
1894
|
+
* `create` boots fine through `defineResource()` and only blows up at
|
|
1895
|
+
* `app.register()` time with Fastify's opaque `FST_ERR_DUPLICATED_ROUTE`.
|
|
1896
|
+
* That error doesn't mention `disabledRoutes` and doesn't distinguish
|
|
1897
|
+
* "you collided with auto-CRUD" from "you collided with yourself" — so
|
|
1898
|
+
* the consumer has to grep arc's source to discover the fix.
|
|
1899
|
+
*
|
|
1900
|
+
* We surface the fix in the error message, naming the conflicting op
|
|
1901
|
+
* and the literal line to add.
|
|
1902
|
+
*
|
|
1903
|
+
* Honored config:
|
|
1904
|
+
* - `disableDefaultRoutes: true` → no auto-CRUD, no collisions possible
|
|
1905
|
+
* - `disabledRoutes: ['create']` → that op's path is free for custom use
|
|
1906
|
+
* - `updateMethod: 'PUT' | 'both'` → update slot covers PUT/PATCH accordingly
|
|
1907
|
+
*/
|
|
1908
|
+
function validateRouteCrudCollisions(config, errors) {
|
|
1909
|
+
if (config.disableDefaultRoutes) return;
|
|
1910
|
+
if (!config.routes || config.routes.length === 0) return;
|
|
1911
|
+
const updateMethod = config.updateMethod ?? "PATCH";
|
|
1912
|
+
const updateMethods = updateMethod === "both" ? ["PUT", "PATCH"] : [updateMethod];
|
|
1913
|
+
const disabled = new Set(config.disabledRoutes ?? []);
|
|
1914
|
+
const crudTable = [
|
|
1915
|
+
{
|
|
1916
|
+
op: "list",
|
|
1917
|
+
method: "GET",
|
|
1918
|
+
path: "/"
|
|
1919
|
+
},
|
|
1920
|
+
{
|
|
1921
|
+
op: "get",
|
|
1922
|
+
method: "GET",
|
|
1923
|
+
path: "/:id"
|
|
1924
|
+
},
|
|
1925
|
+
{
|
|
1926
|
+
op: "create",
|
|
1927
|
+
method: "POST",
|
|
1928
|
+
path: "/"
|
|
1929
|
+
},
|
|
1930
|
+
...updateMethods.map((m) => ({
|
|
1931
|
+
op: "update",
|
|
1932
|
+
method: m,
|
|
1933
|
+
path: "/:id"
|
|
1934
|
+
})),
|
|
1935
|
+
{
|
|
1936
|
+
op: "delete",
|
|
1937
|
+
method: "DELETE",
|
|
1938
|
+
path: "/:id"
|
|
1939
|
+
}
|
|
1940
|
+
];
|
|
1941
|
+
for (const [i, route] of config.routes.entries()) {
|
|
1942
|
+
if (!route.method || !route.path) continue;
|
|
1943
|
+
for (const crud of crudTable) {
|
|
1944
|
+
if (disabled.has(crud.op)) continue;
|
|
1945
|
+
if (route.method !== crud.method || route.path !== crud.path) continue;
|
|
1946
|
+
errors.push({
|
|
1947
|
+
field: `routes[${i}]`,
|
|
1948
|
+
message: `Route ${route.method} ${route.path} collides with auto-CRUD "${crud.op}"`,
|
|
1949
|
+
suggestion: `Add \`disabledRoutes: ['${crud.op}']\` to suppress the auto-CRUD route, or move your custom handler to a different path. Set \`disableDefaultRoutes: true\` to disable all auto-CRUD on this resource.`
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
function validateRoutes(routes, errors) {
|
|
1955
|
+
const validMethods = [
|
|
1956
|
+
"GET",
|
|
1957
|
+
"POST",
|
|
1958
|
+
"PUT",
|
|
1959
|
+
"PATCH",
|
|
1960
|
+
"DELETE",
|
|
1961
|
+
"OPTIONS",
|
|
1962
|
+
"HEAD"
|
|
1963
|
+
];
|
|
1964
|
+
const seenRoutes = /* @__PURE__ */ new Set();
|
|
1965
|
+
for (const [i, route] of routes.entries()) {
|
|
1966
|
+
if (!validMethods.includes(route.method)) errors.push({
|
|
1967
|
+
field: `routes[${i}].method`,
|
|
1968
|
+
message: `Invalid HTTP method "${route.method}"`,
|
|
1969
|
+
suggestion: `Valid methods: ${validMethods.join(", ")}`
|
|
1970
|
+
});
|
|
1971
|
+
if (!route.path) errors.push({
|
|
1972
|
+
field: `routes[${i}].path`,
|
|
1973
|
+
message: "Route path is required"
|
|
1974
|
+
});
|
|
1975
|
+
else if (!route.path.startsWith("/")) errors.push({
|
|
1976
|
+
field: `routes[${i}].path`,
|
|
1977
|
+
message: `Route path must start with "/" (got "${route.path}")`,
|
|
1978
|
+
suggestion: `Change to "/${route.path}"`
|
|
1979
|
+
});
|
|
1980
|
+
const routeWithRefs = route;
|
|
1981
|
+
if (!route.handler && typeof routeWithRefs.controllerMethod !== "function") errors.push({
|
|
1982
|
+
field: `routes[${i}].handler`,
|
|
1983
|
+
message: "Route must declare either `handler` (string / function) or `controllerMethod`",
|
|
1984
|
+
suggestion: "Prefer `controllerMethod: (c: MyController) => c.method` for typed handler refs."
|
|
1985
|
+
});
|
|
1986
|
+
const routeKey = `${route.method} ${route.path}`;
|
|
1987
|
+
if (seenRoutes.has(routeKey)) errors.push({
|
|
1988
|
+
field: `routes[${i}]`,
|
|
1989
|
+
message: `Duplicate route "${routeKey}"`
|
|
1990
|
+
});
|
|
1991
|
+
seenRoutes.add(routeKey);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Format validation errors for display
|
|
1996
|
+
*/
|
|
1997
|
+
function formatValidationErrors(resourceName, result) {
|
|
1998
|
+
const lines = [];
|
|
1999
|
+
if (result.errors.length > 0) {
|
|
2000
|
+
lines.push(`Resource "${resourceName}" validation failed:`);
|
|
2001
|
+
lines.push("");
|
|
2002
|
+
lines.push("ERRORS:");
|
|
2003
|
+
for (const err of result.errors) {
|
|
2004
|
+
lines.push(` ✗ ${err.field}: ${err.message}`);
|
|
2005
|
+
if (err.suggestion) lines.push(` → ${err.suggestion}`);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
if (result.warnings.length > 0) {
|
|
2009
|
+
if (lines.length > 0) lines.push("");
|
|
2010
|
+
lines.push("WARNINGS:");
|
|
2011
|
+
for (const warn of result.warnings) {
|
|
2012
|
+
lines.push(` ⚠ ${warn.field}: ${warn.message}`);
|
|
2013
|
+
if (warn.suggestion) lines.push(` → ${warn.suggestion}`);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
return lines.join("\n");
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Validate and throw if invalid
|
|
2020
|
+
*/
|
|
2021
|
+
function assertValidConfig(config, options) {
|
|
2022
|
+
const result = validateResourceConfig(config, options);
|
|
2023
|
+
if (!result.valid) {
|
|
2024
|
+
const errorMsg = formatValidationErrors(config.name ?? "unknown", result);
|
|
2025
|
+
throw new Error(errorMsg);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
//#endregion
|
|
1864
2029
|
//#region src/core/defineResource/validate.ts
|
|
1865
2030
|
/**
|
|
1866
2031
|
* CRUD op names — kept module-scope (vs allocated per `defineResource()`
|
|
@@ -2023,102 +2188,4 @@ function defineResource(config) {
|
|
|
2023
2188
|
return resource;
|
|
2024
2189
|
}
|
|
2025
2190
|
//#endregion
|
|
2026
|
-
|
|
2027
|
-
/**
|
|
2028
|
-
* Define multiple resources from a shared base config and per-variant overrides.
|
|
2029
|
-
*
|
|
2030
|
-
* Each variant is independently passed through `defineResource()` — the
|
|
2031
|
-
* returned `ResourceDefinition`s are real, fully-registered resources.
|
|
2032
|
-
* Register each one's plugin in your app:
|
|
2033
|
-
*
|
|
2034
|
-
* ```typescript
|
|
2035
|
-
* await app.register(articlePublic.toPlugin());
|
|
2036
|
-
* await app.register(articleAdmin.toPlugin());
|
|
2037
|
-
* ```
|
|
2038
|
-
*
|
|
2039
|
-
* @param base Shared config — adapter, queryParser, schemaOptions, hooks, etc.
|
|
2040
|
-
* Must NOT include `name` or `prefix` (those are per-variant).
|
|
2041
|
-
* @param variants Map of variant key → override. Each variant must declare
|
|
2042
|
-
* its own `name` and `prefix`. Other fields override the base.
|
|
2043
|
-
* @returns A record where each key from `variants` maps to a real
|
|
2044
|
-
* `ResourceDefinition` ready for `.toPlugin()` registration.
|
|
2045
|
-
*/
|
|
2046
|
-
function defineResourceVariants(base, variants) {
|
|
2047
|
-
const out = {};
|
|
2048
|
-
for (const key of Object.keys(variants)) {
|
|
2049
|
-
const override = variants[key];
|
|
2050
|
-
out[key] = defineResource({
|
|
2051
|
-
...base,
|
|
2052
|
-
...override
|
|
2053
|
-
});
|
|
2054
|
-
}
|
|
2055
|
-
return out;
|
|
2056
|
-
}
|
|
2057
|
-
//#endregion
|
|
2058
|
-
//#region src/core/entityHelpers.ts
|
|
2059
|
-
/**
|
|
2060
|
-
* Per-request entity helpers — read the resource binding (idField + the
|
|
2061
|
-
* URL `:id` value) off `req.arc`.
|
|
2062
|
-
*
|
|
2063
|
-
* Action handlers receive their `id` argument as the raw URL `:id` value.
|
|
2064
|
-
* When the resource declares a custom `idField` (`slug`, `reportId`, …)
|
|
2065
|
-
* that's NOT the document `_id`, a naive `Model.findById(id)` silently
|
|
2066
|
-
* returns null. The historical footgun was a `findById(id)` typo where
|
|
2067
|
-
* the handler author hadn't realised that `:id` resolves to the friendly
|
|
2068
|
-
* handle.
|
|
2069
|
-
*
|
|
2070
|
-
* `getEntityQuery(req)` produces the canonical filter shape so handlers
|
|
2071
|
-
* compose lookups with no resource-config recall:
|
|
2072
|
-
*
|
|
2073
|
-
* ```ts
|
|
2074
|
-
* actions: {
|
|
2075
|
-
* archive: {
|
|
2076
|
-
* handler: async (id, data, req) => {
|
|
2077
|
-
* const doc = await Model.findOne(getEntityQuery(req));
|
|
2078
|
-
* if (!doc) throw new NotFoundError("Order");
|
|
2079
|
-
* // ...
|
|
2080
|
-
* },
|
|
2081
|
-
* },
|
|
2082
|
-
* }
|
|
2083
|
-
* ```
|
|
2084
|
-
*
|
|
2085
|
-
* The router populates `req.arc.idField` and `req.arc.entityId` before
|
|
2086
|
-
* invoking the handler — these helpers are zero-cost reads.
|
|
2087
|
-
*/
|
|
2088
|
-
/**
|
|
2089
|
-
* Read the resource's configured `idField` for the current request.
|
|
2090
|
-
* Falls back to the framework default (`_id`) when the route hasn't
|
|
2091
|
-
* bound an idField — keeps handlers safe to author without checking
|
|
2092
|
-
* the resource config.
|
|
2093
|
-
*/
|
|
2094
|
-
function getEntityIdField(req) {
|
|
2095
|
-
return req.arc?.idField ?? "_id";
|
|
2096
|
-
}
|
|
2097
|
-
/**
|
|
2098
|
-
* Read the URL `:id` path param value as the resource handle.
|
|
2099
|
-
* Returns `undefined` when the route has no `:id` segment (collection
|
|
2100
|
-
* routes) — handlers that need the entity must be on row routes.
|
|
2101
|
-
*/
|
|
2102
|
-
function getEntityId(req) {
|
|
2103
|
-
if (req.arc?.entityId !== void 0) return req.arc.entityId;
|
|
2104
|
-
return req.params?.id;
|
|
2105
|
-
}
|
|
2106
|
-
/**
|
|
2107
|
-
* Compose a `findOne` filter that resolves the current request's
|
|
2108
|
-
* entity, regardless of whether the resource binds `_id` or a custom
|
|
2109
|
-
* field. Idiomatic shape for arc action handlers.
|
|
2110
|
-
*
|
|
2111
|
-
* ```ts
|
|
2112
|
-
* const doc = await Model.findOne(getEntityQuery(req));
|
|
2113
|
-
* ```
|
|
2114
|
-
*
|
|
2115
|
-
* Returns `{}` when the route has no entity context (collection routes
|
|
2116
|
-
* or tests bypassing the router) — caller decides what that means.
|
|
2117
|
-
*/
|
|
2118
|
-
function getEntityQuery(req) {
|
|
2119
|
-
const id = getEntityId(req);
|
|
2120
|
-
if (id === void 0) return {};
|
|
2121
|
-
return { [getEntityIdField(req)]: id };
|
|
2122
|
-
}
|
|
2123
|
-
//#endregion
|
|
2124
|
-
export { defineResource as a, defineAction as c, defineAggregation as d, defineResourceVariants as i, createCrudRouter as l, getEntityIdField as n, ResourceDefinition as o, getEntityQuery as r, mergeResourceConfig as s, getEntityId as t, createPermissionMiddleware as u };
|
|
2191
|
+
export { ResourceDefinition as a, validateResourceConfig as i, assertValidConfig as n, createCrudRouter as o, formatValidationErrors as r, createPermissionMiddleware as s, defineResource as t };
|
package/dist/docs/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { p as RegistryEntry } from "../index-
|
|
1
|
+
import { p as RegistryEntry } from "../index-DPRH0Cme.mjs";
|
|
2
2
|
import { t as ExternalOpenApiPaths } from "../externalPaths-BE6wFpYv.mjs";
|
|
3
3
|
import { FastifyPluginAsync } from "fastify";
|
|
4
4
|
//#region src/docs/openapi/types.d.ts
|
package/dist/docs/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-
|
|
1
|
+
import { n as getUserRoles } from "../userHelpers-Cg_y3fBz.mjs";
|
|
2
|
+
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-BLV4OMXb.mjs";
|
|
3
3
|
import fp from "fastify-plugin";
|
|
4
4
|
//#region src/docs/scalar.ts
|
|
5
5
|
const scalarPlugin = async (fastify, opts = {}) => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { n as requireSingleHeaderValue } from "./headers-BvlQSDMB.mjs";
|
|
4
|
+
import { n as getUserRoles } from "./userHelpers-Cg_y3fBz.mjs";
|
|
4
5
|
import fp from "fastify-plugin";
|
|
5
6
|
//#region src/scope/elevation.ts
|
|
6
7
|
var elevation_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -18,7 +19,7 @@ const elevationPlugin = async (fastify, opts = {}) => {
|
|
|
18
19
|
const authenticateWithElevation = async (request, reply) => {
|
|
19
20
|
await originalAuthenticate.call(fastify, request, reply);
|
|
20
21
|
if (reply.sent) return;
|
|
21
|
-
if (request.headers
|
|
22
|
+
if (requireSingleHeaderValue(request.headers, scopeHeader) !== "platform") return;
|
|
22
23
|
const user = request.user;
|
|
23
24
|
if (!user) {
|
|
24
25
|
log.debug("Elevation requested but no user after auth");
|
|
@@ -45,7 +46,7 @@ const elevationPlugin = async (fastify, opts = {}) => {
|
|
|
45
46
|
});
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
48
|
-
const orgId = request.headers
|
|
49
|
+
const orgId = requireSingleHeaderValue(request.headers, orgHeader);
|
|
49
50
|
const userId = String(user.id ?? user._id ?? "unknown");
|
|
50
51
|
request.scope = {
|
|
51
52
|
kind: "elevated",
|
|
@@ -140,12 +140,41 @@ interface RetryOptions {
|
|
|
140
140
|
*/
|
|
141
141
|
transport?: Pick<EventTransport, "deadLetter">;
|
|
142
142
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
143
|
+
* DURABLE dead-letter persistence for exhausted events (custom store,
|
|
144
|
+
* `$deadLetter` publisher, ...). Treated exactly like
|
|
145
|
+
* {@link transport}.deadLetter: a failure follows {@link dlqFailureMode}
|
|
146
|
+
* — rethrow by default, so the transport redelivers instead of
|
|
147
|
+
* acknowledging an event that is neither processed nor persisted.
|
|
145
148
|
*
|
|
146
|
-
*
|
|
149
|
+
* This is what `eventPlugin({ deadLetterQueue })` wires. Use
|
|
150
|
+
* {@link onDead} for metrics/alerts whose failure must NOT affect
|
|
151
|
+
* acknowledgement.
|
|
152
|
+
*/
|
|
153
|
+
deadLetter?: (event: DomainEvent, errors: Error[]) => void | Promise<void>;
|
|
154
|
+
/**
|
|
155
|
+
* OBSERVABILITY callback when all retries are exhausted (metrics,
|
|
156
|
+
* alerting, logging). Fires in addition to {@link transport} /
|
|
157
|
+
* {@link deadLetter} routing. Failures are logged and swallowed — this
|
|
158
|
+
* callback can never affect whether the event is acknowledged; durable
|
|
159
|
+
* persistence belongs in {@link deadLetter}.
|
|
147
160
|
*/
|
|
148
161
|
onDead?: (event: DomainEvent, errors: Error[]) => void | Promise<void>;
|
|
162
|
+
/**
|
|
163
|
+
* What to do when `transport.deadLetter()` ITSELF fails after retries are
|
|
164
|
+
* exhausted.
|
|
165
|
+
*
|
|
166
|
+
* - `'rethrow'` (default) — the wrapper throws, so the transport treats
|
|
167
|
+
* the handler as failed: an at-least-once transport (Redis Streams,
|
|
168
|
+
* SQS) keeps the original message pending and redelivers it. The only
|
|
169
|
+
* mode that cannot LOSE the event — returning normally would
|
|
170
|
+
* acknowledge a message that was neither processed nor persisted.
|
|
171
|
+
* - `'log-and-drop'` — log the DLQ failure and return normally,
|
|
172
|
+
* acknowledging the message. Only correct when losing the event is
|
|
173
|
+
* acceptable (metrics, notifications).
|
|
174
|
+
*
|
|
175
|
+
* @default 'rethrow'
|
|
176
|
+
*/
|
|
177
|
+
dlqFailureMode?: "rethrow" | "log-and-drop";
|
|
149
178
|
/**
|
|
150
179
|
* Optional name for logging + written into `DeadLetteredEvent.handlerName`.
|
|
151
180
|
*/
|
|
@@ -246,6 +275,12 @@ interface EventPluginOptions {
|
|
|
246
275
|
* Dead letter queue for events that exhaust all retries.
|
|
247
276
|
* Requires `retry` to be enabled. If `retry` is set but no custom `store`,
|
|
248
277
|
* failed events are published to the `$deadLetter` event type by default.
|
|
278
|
+
*
|
|
279
|
+
* The store is DURABLE persistence: if it throws, the wrapped handler
|
|
280
|
+
* rethrows (per `retry.dlqFailureMode`, default `'rethrow'`) so an
|
|
281
|
+
* at-least-once transport redelivers instead of acknowledging an event
|
|
282
|
+
* that was neither processed nor dead-lettered. Metrics/alerting belongs
|
|
283
|
+
* in `retry.onDead`, whose failures never affect acknowledgement.
|
|
249
284
|
*/
|
|
250
285
|
deadLetterQueue?: {
|
|
251
286
|
/** Custom store function. If omitted, publishes to '$deadLetter' event type. */
|