@classytic/arc 2.25.0 → 2.30.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--M8EMzt0.mjs → BaseController-B5DwufbB.mjs} +15 -11
- package/dist/EventTransport-C6YA8xY9.mjs +103 -0
- package/dist/{HookSystem-C1vB_Gc9.mjs → HookSystem-WkIougo6.mjs} +15 -35
- package/dist/applyPermissionResult-Cf6zUCvb.mjs +257 -0
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +1 -1
- package/dist/{buildHandler-Dtwd8fdz.mjs → buildHandler-BYS3Vkfz.mjs} +32 -6
- 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/core/index.d.mts +2 -2
- package/dist/core/index.mjs +5 -3
- package/dist/core-poalJLvp.mjs +175 -0
- package/dist/{createActionRouter-B6GvAuAX.mjs → createActionRouter-BxuywVRB.mjs} +2 -2
- package/dist/{createAggregationRouter-wzwFa_dW.mjs → createAggregationRouter-BiY-lGdY.mjs} +7 -20
- package/dist/{preloadResources-DfyngIN9.mjs → createApp-CXrKFciH.mjs} +25 -76
- package/dist/defineAggregation-ufWjUwml.mjs +47 -0
- package/dist/{core-EUNu0u07.mjs → defineResource-CxWqDCAy.mjs} +36 -261
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/{eventPlugin-DrQ_FesK.mjs → eventPlugin-D2NNR4py.mjs} +2 -103
- package/dist/events/index.d.mts +28 -2
- package/dist/events/index.mjs +24 -343
- package/dist/factory/index.d.mts +4 -3
- package/dist/factory/index.mjs +3 -1
- package/dist/{fastify-augmentation-dPNBVeIj.d.mts → fastify-augmentation-0WhPnUH7.d.mts} +3 -2
- package/dist/{applyPermissionResult-Tw4iXtlf.mjs → fields-CQ_7KQWw.mjs} +1 -97
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.mjs +1 -1
- package/dist/index-CCzVR-N3.d.mts +107 -0
- package/dist/{index-BMnbzvtO.d.mts → index-Ch16aAah.d.mts} +201 -21
- package/dist/{index-f4PTrk3B.d.mts → index-Cx7bro4R.d.mts} +4 -4
- package/dist/{index-DPRH0Cme.d.mts → index-Q5gzEIWY.d.mts} +97 -66
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +8 -6
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +6 -6
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/{mcpPlugin-Cb7olw0j.mjs → mcpPlugin-nRzm5g2s.mjs} +36 -43
- package/dist/middleware/index.d.mts +1 -1
- package/dist/migrations/index.d.mts +0 -3
- package/dist/migrations/index.mjs +0 -3
- package/dist/module-_-M7C5ES.mjs +29 -0
- package/dist/{openapi-DrLtog_B.mjs → openapi-B8Vkbxbn.mjs} +1 -1
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +4 -3
- package/dist/{permissions-BrtiBKGs.mjs → permissions-Bfu_8y1U.mjs} +318 -289
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/{pipeline-BN5G05rd.mjs → pipeline-BJnpkY9i.mjs} +4 -18
- package/dist/plugins/index.d.mts +4 -4
- package/dist/plugins/index.mjs +2 -2
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{preloadResources-0oe-JSSS.d.mts → preloadResources-DCfa2egz.d.mts} +3 -2
- package/dist/preloadResources-DSY5HGpS.mjs +45 -0
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +14 -2
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-DwFDVKP0.mjs → presets-CJOEXLuk.mjs} +25 -9
- package/dist/projection-UBDgfMwu.mjs +17 -0
- package/dist/{realtime-Cuw_deA-.mjs → realtime-B9nmIY5V.mjs} +2 -1
- package/dist/{registerRoute-v0Wb6Plt.mjs → registerRoute-JkxIEBJq.mjs} +3 -1
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/{registry-vcOCtPij.mjs → registry-BYXUUg21.mjs} +16 -1
- package/dist/repositoryFilter-Dz8k3S4P.mjs +44 -0
- package/dist/subscribe-helpers-DfzsivM6.mjs +339 -0
- package/dist/testing/cleanupStoreContract.d.mts +10 -0
- package/dist/testing/cleanupStoreContract.mjs +375 -0
- package/dist/testing/index.d.mts +34 -5
- package/dist/testing/index.mjs +316 -211
- package/dist/testing/outboxStoreContract.d.mts +13 -0
- package/dist/testing/outboxStoreContract.mjs +247 -0
- package/dist/types/index.d.mts +3 -3
- package/dist/{types-QFGc3Cjv.d.mts → types-Bmmn-fO0.d.mts} +1 -1
- package/dist/types-C_ZZRcD6.d.mts +450 -0
- package/dist/{types-Dmw-6Qeo.d.mts → types-ChvvVNP8.d.mts} +93 -88
- package/dist/{index-KUoxjIq1.d.mts → types-D_n8L6VY.d.mts} +33 -106
- package/dist/usage/index.d.mts +1 -1
- package/dist/usage/index.mjs +0 -0
- package/dist/utils/index.d.mts +2 -5
- package/package.json +539 -524
|
@@ -3,17 +3,17 @@ import { arcLog } from "./logger/index.mjs";
|
|
|
3
3
|
import { r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
|
|
4
4
|
import { n as getUserRoles } from "./userHelpers-Cg_y3fBz.mjs";
|
|
5
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
|
|
6
|
+
import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-B5DwufbB.mjs";
|
|
7
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 {
|
|
9
|
-
import { r as buildCrudPermissionMw, t as tryRegisterRoute } from "./registerRoute-
|
|
8
|
+
import { i as resolveEffectiveRoles, n as applyFieldWritePermissions } from "./fields-CQ_7KQWw.mjs";
|
|
9
|
+
import { r as buildCrudPermissionMw, t as tryRegisterRoute } from "./registerRoute-JkxIEBJq.mjs";
|
|
10
10
|
import { c as getDefaultCrudSchemas } from "./utils-taUVD4p3.mjs";
|
|
11
11
|
import { n as convertRouteSchema, r as ensureSchemaConverter, t as convertOpenApiSchemas } from "./schemaConverter-DJvodiHj.mjs";
|
|
12
12
|
import { i as pipeUIMessageStreamToReply, r as isReadableStream } from "./streaming-BQcW6_Kq.mjs";
|
|
13
13
|
import { t as hasEvents } from "./typeGuards-C9KXUly6.mjs";
|
|
14
|
-
import { a as createFastifyHandler, i as createCrudHandlers, n as buildPipelineHandler, o as createRequestContext, r as resolvePipelineSteps
|
|
15
|
-
import {
|
|
16
|
-
import { r as getAvailablePresets, t as applyPresets } from "./presets-
|
|
14
|
+
import { a as createFastifyHandler, i as createCrudHandlers, n as buildPipelineHandler, o as createRequestContext, r as resolvePipelineSteps } from "./pipeline-BJnpkY9i.mjs";
|
|
15
|
+
import { I as requireAuth } from "./permissions-Bfu_8y1U.mjs";
|
|
16
|
+
import { r as getAvailablePresets, t as applyPresets } from "./presets-CJOEXLuk.mjs";
|
|
17
17
|
import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
|
|
18
18
|
import { t as resolveActionPermission } from "./actionPermissions-CAphcmUG.mjs";
|
|
19
19
|
//#region src/core/middlewares/fieldWrite.ts
|
|
@@ -55,52 +55,6 @@ function methodCarriesBody(method) {
|
|
|
55
55
|
return m === "POST" || m === "PUT" || m === "PATCH";
|
|
56
56
|
}
|
|
57
57
|
//#endregion
|
|
58
|
-
//#region src/core/aggregation/defineAggregation.ts
|
|
59
|
-
/**
|
|
60
|
-
* Declare a single resource aggregation. Exported configs flow into
|
|
61
|
-
* `defineResource({ aggregations: { ... } })` either as named keys or
|
|
62
|
-
* via auto-discovery patterns (loadAggregations — future).
|
|
63
|
-
*
|
|
64
|
-
* @example Inline
|
|
65
|
-
* ```ts
|
|
66
|
-
* defineResource({
|
|
67
|
-
* name: 'order',
|
|
68
|
-
* aggregations: {
|
|
69
|
-
* revenueByStatus: defineAggregation({
|
|
70
|
-
* groupBy: 'status',
|
|
71
|
-
* measures: { count: 'count', revenue: 'sum:totalPrice' },
|
|
72
|
-
* permissions: requireRoles(['admin']),
|
|
73
|
-
* }),
|
|
74
|
-
* },
|
|
75
|
-
* });
|
|
76
|
-
* ```
|
|
77
|
-
*
|
|
78
|
-
* @example Multi-file
|
|
79
|
-
* ```ts
|
|
80
|
-
* // orders/aggregations/revenue-by-status.ts
|
|
81
|
-
* import { defineAggregation } from '@classytic/arc';
|
|
82
|
-
*
|
|
83
|
-
* export const revenueByStatus = defineAggregation({
|
|
84
|
-
* groupBy: 'status',
|
|
85
|
-
* measures: { count: 'count', revenue: 'sum:totalPrice' },
|
|
86
|
-
* permissions: requireRoles(['admin']),
|
|
87
|
-
* timeout: 5000,
|
|
88
|
-
* cache: { staleTime: 60 },
|
|
89
|
-
* });
|
|
90
|
-
*
|
|
91
|
-
* // orders/order.resource.ts
|
|
92
|
-
* import * as aggregations from './aggregations/index.js';
|
|
93
|
-
*
|
|
94
|
-
* defineResource({
|
|
95
|
-
* name: 'order',
|
|
96
|
-
* aggregations, // 30+ entries flow in
|
|
97
|
-
* });
|
|
98
|
-
* ```
|
|
99
|
-
*/
|
|
100
|
-
function defineAggregation(config) {
|
|
101
|
-
return config;
|
|
102
|
-
}
|
|
103
|
-
//#endregion
|
|
104
58
|
//#region src/core/createCrudRouter.ts
|
|
105
59
|
/**
|
|
106
60
|
* Mount custom routes (from presets or user-defined `routes`) on Fastify.
|
|
@@ -340,80 +294,6 @@ function createPermissionMiddleware(permission, resourceName, action) {
|
|
|
340
294
|
return buildCrudPermissionMw(permission, resourceName, action);
|
|
341
295
|
}
|
|
342
296
|
//#endregion
|
|
343
|
-
//#region src/core/defineAction.ts
|
|
344
|
-
/**
|
|
345
|
-
* Build an `ActionDefinition` with a typed handler. The literal schema
|
|
346
|
-
* type captured here flows into `data`, so `defineAction({ schema:
|
|
347
|
-
* z.object({...}), handler })` produces fully-typed code with no
|
|
348
|
-
* `as MyShape` cast.
|
|
349
|
-
*
|
|
350
|
-
* Behaviorally identical to a bare `ActionDefinition` object — same
|
|
351
|
-
* validation path (AJV), same permission resolution, same MCP wiring.
|
|
352
|
-
* The runtime shape is unchanged; only the type-level inference is new.
|
|
353
|
-
*/
|
|
354
|
-
function defineAction(config) {
|
|
355
|
-
return {
|
|
356
|
-
handler: config.handler,
|
|
357
|
-
permissions: config.permissions,
|
|
358
|
-
schema: config.schema,
|
|
359
|
-
description: config.description,
|
|
360
|
-
id: config.id,
|
|
361
|
-
mcp: config.mcp
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
//#endregion
|
|
365
|
-
//#region src/core/defineResource/seams.ts
|
|
366
|
-
/** Plain object test — literals only; class instances must replace, not merge. */
|
|
367
|
-
function isPlainObject(value) {
|
|
368
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
369
|
-
const proto = Object.getPrototypeOf(value);
|
|
370
|
-
return proto === Object.prototype || proto === null;
|
|
371
|
-
}
|
|
372
|
-
/** Recursive plain-object merge; nested arrays REPLACE (they're value lists). */
|
|
373
|
-
function mergePlain(base, next) {
|
|
374
|
-
const out = { ...base };
|
|
375
|
-
for (const [key, value] of Object.entries(next)) {
|
|
376
|
-
if (value === void 0) continue;
|
|
377
|
-
const existing = out[key];
|
|
378
|
-
out[key] = isPlainObject(existing) && isPlainObject(value) ? mergePlain(existing, value) : value;
|
|
379
|
-
}
|
|
380
|
-
return out;
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* Canonical slot-aware merge of a base resource config with host seams.
|
|
384
|
-
* Later seams win over earlier ones; `undefined` values never clobber.
|
|
385
|
-
*
|
|
386
|
-
* Merge semantics (value-driven — no per-slot registry to drift):
|
|
387
|
-
*
|
|
388
|
-
* - **Top-level arrays CONCAT** — `routes`, `middlewares`, `presets`,
|
|
389
|
-
* `routeGuards`, `disabledRoutes`: additive collections, base first.
|
|
390
|
-
* - **Plain objects merge recursively** — `permissions`, `actions`,
|
|
391
|
-
* `schemaOptions` (incl. `fieldRules`), `aggregations`, `customSchemas`,
|
|
392
|
-
* `fields`, `cache`, `events`: per-key composition. Arrays NESTED inside
|
|
393
|
-
* them replace (they're value lists — enums, sort specs), not concat.
|
|
394
|
-
* - **Everything else last-wins** — class instances (`adapter`,
|
|
395
|
-
* `controller`, `queryParser`), functions, scalars.
|
|
396
|
-
*
|
|
397
|
-
* The single `as` at the return is the one documented erasure for the
|
|
398
|
-
* {@link AdapterLike}-widened seam slots — inside the helper so no module
|
|
399
|
-
* author ever writes one (mirrors mongokit's one-cast-at-the-boundary rule).
|
|
400
|
-
*/
|
|
401
|
-
function mergeResourceConfig(base, ...seams) {
|
|
402
|
-
const out = { ...base };
|
|
403
|
-
for (const seam of seams) {
|
|
404
|
-
if (!seam) continue;
|
|
405
|
-
for (const [key, value] of Object.entries(seam)) {
|
|
406
|
-
if (value === void 0) continue;
|
|
407
|
-
const existing = out[key];
|
|
408
|
-
if (Array.isArray(existing) && Array.isArray(value)) out[key] = [...existing, ...value];
|
|
409
|
-
else if (isPlainObject(existing) && isPlainObject(value)) out[key] = mergePlain(existing, value);
|
|
410
|
-
else if (Array.isArray(value)) out[key] = [...value];
|
|
411
|
-
else out[key] = value;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return out;
|
|
415
|
-
}
|
|
416
|
-
//#endregion
|
|
417
297
|
//#region src/core/defineResource/controller.ts
|
|
418
298
|
/**
|
|
419
299
|
* Phase 4 — pick (or auto-create) the resource's controller.
|
|
@@ -582,26 +462,6 @@ function buildBaseController(resolvedConfig, adapter, repository) {
|
|
|
582
462
|
//#endregion
|
|
583
463
|
//#region src/core/defineResource/hooks.ts
|
|
584
464
|
/**
|
|
585
|
-
* Phase 6 — wire preset hooks + inline `config.hooks` onto the
|
|
586
|
-
* resource's `_pendingHooks`.
|
|
587
|
-
*
|
|
588
|
-
* Two sources feed the same array:
|
|
589
|
-
* 1. Preset hooks collected during `applyPresets()` (raw `_hooks`
|
|
590
|
-
* on the internal config). Already in the canonical
|
|
591
|
-
* `{ operation, phase, handler, priority }` shape — copied
|
|
592
|
-
* verbatim (priority defaults to 10).
|
|
593
|
-
* 2. Inline `config.hooks.beforeCreate` / `afterCreate` / etc.
|
|
594
|
-
* Authored by the user on the original `ResourceConfig`.
|
|
595
|
-
* Wrapped in a `ResourceHookContext` projection (v2.10.8) so
|
|
596
|
-
* authors can read `scope` / `context` without reaching into
|
|
597
|
-
* internal request fields.
|
|
598
|
-
*
|
|
599
|
-
* The 6 inline hook keys (before/after × create/update/delete) used
|
|
600
|
-
* to be 6 nearly-identical blocks; collapsed into a table + loop so
|
|
601
|
-
* a future hook (e.g. `beforeRead`) is one row, not seven scattered
|
|
602
|
-
* edits.
|
|
603
|
-
*/
|
|
604
|
-
/**
|
|
605
465
|
* Combined entry-point for Phase 6. Pushes preset-collected hooks
|
|
606
466
|
* first, then inline `config.hooks` (so hosts can rely on
|
|
607
467
|
* registration order if priorities tie).
|
|
@@ -664,19 +524,17 @@ const INLINE_HOOK_SPECS = [
|
|
|
664
524
|
}
|
|
665
525
|
];
|
|
666
526
|
/**
|
|
667
|
-
* Project a raw HookSystem context into a `ResourceHookContext` for
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
* fields.
|
|
527
|
+
* Project a raw HookSystem context into a `ResourceHookContext` for inline
|
|
528
|
+
* `config.hooks.*` handlers. `scope` rides in already computed — `HookContext`
|
|
529
|
+
* carries the same projection — so the two surfaces cannot drift.
|
|
671
530
|
*/
|
|
672
531
|
function buildHookContext(ctx) {
|
|
673
532
|
const context = ctx.context;
|
|
674
|
-
const rawScope = context?._scope;
|
|
675
533
|
return {
|
|
676
534
|
data: ctx.data ?? ctx.result ?? {},
|
|
677
535
|
user: ctx.user,
|
|
678
536
|
context,
|
|
679
|
-
scope:
|
|
537
|
+
scope: ctx.scope,
|
|
680
538
|
meta: ctx.meta
|
|
681
539
|
};
|
|
682
540
|
}
|
|
@@ -922,7 +780,7 @@ function stripFromRequired(schema, fieldName) {
|
|
|
922
780
|
}
|
|
923
781
|
/**
|
|
924
782
|
* Strip framework-injected fields from the `required[]` list of every
|
|
925
|
-
* body-shaped slot in an adapter's generated schemas
|
|
783
|
+
* body-shaped slot in an adapter's generated schemas.
|
|
926
784
|
*
|
|
927
785
|
* A "framework-injected field" is any field marked `systemManaged: true`
|
|
928
786
|
* in `schemaOptions.fieldRules`. Arc populates those fields from the
|
|
@@ -1077,22 +935,32 @@ const WRITE_OPERATIONS = /* @__PURE__ */ new Set([
|
|
|
1077
935
|
* wiring, ...) count as gates and don't false-positive.
|
|
1078
936
|
*
|
|
1079
937
|
* Severity ladder:
|
|
1080
|
-
* - `
|
|
1081
|
-
*
|
|
938
|
+
* - `error` when any enabled WRITE op is ungated AND `strict` is on — the
|
|
939
|
+
* resource fails boot (`defineResource()` throws). This is the fail-closed
|
|
940
|
+
* production invariant: an unauthenticated write must never ship silently.
|
|
941
|
+
* - `warn` (default) when any enabled WRITE op (create/update/delete) is
|
|
942
|
+
* ungated — real exposure the host must acknowledge, but non-fatal so
|
|
943
|
+
* existing hosts are not broken.
|
|
1082
944
|
* - `info` when only reads are ungated — public catalogs
|
|
1083
945
|
* (`referenceData: true`) are a legitimate shape, but the host should
|
|
1084
946
|
* still state intent with `permissions.fullPublic()` / `allowPublic()`.
|
|
947
|
+
*
|
|
948
|
+
* `strict` is opt-in (resource `strictPermissions: true` or the
|
|
949
|
+
* `ARC_STRICT_PERMISSIONS` env), so the default behavior is unchanged.
|
|
1085
950
|
*/
|
|
1086
|
-
function collectUngatedCrudDiagnostics(config) {
|
|
951
|
+
function collectUngatedCrudDiagnostics(config, strict = false) {
|
|
1087
952
|
if (config.disableDefaultRoutes) return [];
|
|
1088
953
|
const disabled = new Set(config.disabledRoutes ?? []);
|
|
1089
954
|
const permissions = config.permissions ?? {};
|
|
1090
955
|
const ungated = CRUD_OPERATIONS.filter((op) => !disabled.has(op) && typeof permissions[op] !== "function");
|
|
1091
956
|
if (ungated.length === 0) return [];
|
|
957
|
+
const severity = ungated.some((op) => WRITE_OPERATIONS.has(op)) ? strict ? "error" : "warn" : "info";
|
|
1092
958
|
return [{
|
|
1093
|
-
severity
|
|
959
|
+
severity,
|
|
1094
960
|
code: "crud-public-by-omission",
|
|
1095
|
-
message: `[Arc] Resource '${config.name}' mounts ${ungated.length} CRUD route(s) with no permission gate: ${ungated.join(", ")}.\
|
|
961
|
+
message: `[Arc] Resource '${config.name}' mounts ${ungated.length} CRUD route(s) with no permission gate: ${ungated.join(", ")}.\n` + (severity === "error" ? "strictPermissions is on and an ungated WRITE is fatal. State intent explicitly:\n" : "An omitted permission mounts the route WITHOUT auth. State intent explicitly:\n") + ` permissions: permissions.fullPublic() // intentionally public
|
|
962
|
+
permissions: permissions.authenticated() // require auth on every op
|
|
963
|
+
permissions: { ${ungated[0]}: requireAuth(), ... } // per-operation`
|
|
1096
964
|
}];
|
|
1097
965
|
}
|
|
1098
966
|
//#endregion
|
|
@@ -1337,7 +1205,7 @@ function buildResourcePlugin(resource) {
|
|
|
1337
1205
|
const isFirstMount = !resource._sharedStateRegisteredOn.has(sharedRoot);
|
|
1338
1206
|
if (isFirstMount) resource._sharedStateRegisteredOn.add(sharedRoot);
|
|
1339
1207
|
if (isFirstMount && resource._diagnostics?.length) for (const diagnostic of resource._diagnostics) {
|
|
1340
|
-
const level = diagnostic.severity === "info" ? "info" : "warn";
|
|
1208
|
+
const level = diagnostic.severity === "info" ? "info" : diagnostic.severity === "error" ? "error" : "warn";
|
|
1341
1209
|
fastify.log?.[level]?.(diagnostic.message);
|
|
1342
1210
|
}
|
|
1343
1211
|
const arc = fastify.arc;
|
|
@@ -1398,7 +1266,7 @@ function buildResourcePlugin(resource) {
|
|
|
1398
1266
|
extensions: resource.extensions
|
|
1399
1267
|
});
|
|
1400
1268
|
if (resource.actions && Object.keys(resource.actions).length > 0) {
|
|
1401
|
-
const { createActionRouter } = await import("./createActionRouter-
|
|
1269
|
+
const { createActionRouter } = await import("./createActionRouter-BxuywVRB.mjs").then((n) => n.n);
|
|
1402
1270
|
createActionRouter(typedInstance, {
|
|
1403
1271
|
...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
|
|
1404
1272
|
resourceName: resource.name,
|
|
@@ -1413,7 +1281,7 @@ function buildResourcePlugin(resource) {
|
|
|
1413
1281
|
});
|
|
1414
1282
|
}
|
|
1415
1283
|
if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
|
|
1416
|
-
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-
|
|
1284
|
+
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-BiY-lGdY.mjs"), import("./validate-Bk44F2yT.mjs").then((n) => n.r)]);
|
|
1417
1285
|
const repoForAgg = resource.controller?.repository ?? resource.repository;
|
|
1418
1286
|
if (!adapterSupportsAggregate(repoForAgg)) {
|
|
1419
1287
|
const undispatchable = Object.entries(resource.aggregations).filter(([, cfg]) => !cfg.materialized).map(([name]) => name);
|
|
@@ -2289,7 +2157,12 @@ function defineResource(config) {
|
|
|
2289
2157
|
const configWithId = resolveIdField(normalisedConfig, repository);
|
|
2290
2158
|
const resolvedConfig = applyPresetsAndAutoInject(configWithId);
|
|
2291
2159
|
const hasCrudRoutes = computeHasCrudRoutes(resolvedConfig);
|
|
2292
|
-
if (!normalisedConfig.skipValidation)
|
|
2160
|
+
if (!normalisedConfig.skipValidation) {
|
|
2161
|
+
const strict = resolvedConfig.strictPermissions ?? process.env.ARC_STRICT_PERMISSIONS === "true";
|
|
2162
|
+
diagnostics = diagnostics.concat(collectUngatedCrudDiagnostics(resolvedConfig, strict));
|
|
2163
|
+
}
|
|
2164
|
+
const fatal = diagnostics.filter((d) => d.severity === "error");
|
|
2165
|
+
if (fatal.length > 0) throw new Error(fatal.map((d) => d.message).join("\n\n"));
|
|
2293
2166
|
const narrowedConfig = resolvedConfig;
|
|
2294
2167
|
const narrowedAdapter = configWithId.adapter;
|
|
2295
2168
|
const controller = resolveOrAutoCreateController(narrowedConfig, narrowedAdapter, repository, hasCrudRoutes);
|
|
@@ -2308,102 +2181,4 @@ function defineResource(config) {
|
|
|
2308
2181
|
return resource;
|
|
2309
2182
|
}
|
|
2310
2183
|
//#endregion
|
|
2311
|
-
|
|
2312
|
-
/**
|
|
2313
|
-
* Define multiple resources from a shared base config and per-variant overrides.
|
|
2314
|
-
*
|
|
2315
|
-
* Each variant is independently passed through `defineResource()` — the
|
|
2316
|
-
* returned `ResourceDefinition`s are real, fully-registered resources.
|
|
2317
|
-
* Register each one's plugin in your app:
|
|
2318
|
-
*
|
|
2319
|
-
* ```typescript
|
|
2320
|
-
* await app.register(articlePublic.toPlugin());
|
|
2321
|
-
* await app.register(articleAdmin.toPlugin());
|
|
2322
|
-
* ```
|
|
2323
|
-
*
|
|
2324
|
-
* @param base Shared config — adapter, queryParser, schemaOptions, hooks, etc.
|
|
2325
|
-
* Must NOT include `name` or `prefix` (those are per-variant).
|
|
2326
|
-
* @param variants Map of variant key → override. Each variant must declare
|
|
2327
|
-
* its own `name` and `prefix`. Other fields override the base.
|
|
2328
|
-
* @returns A record where each key from `variants` maps to a real
|
|
2329
|
-
* `ResourceDefinition` ready for `.toPlugin()` registration.
|
|
2330
|
-
*/
|
|
2331
|
-
function defineResourceVariants(base, variants) {
|
|
2332
|
-
const out = {};
|
|
2333
|
-
for (const key of Object.keys(variants)) {
|
|
2334
|
-
const override = variants[key];
|
|
2335
|
-
out[key] = defineResource({
|
|
2336
|
-
...base,
|
|
2337
|
-
...override
|
|
2338
|
-
});
|
|
2339
|
-
}
|
|
2340
|
-
return out;
|
|
2341
|
-
}
|
|
2342
|
-
//#endregion
|
|
2343
|
-
//#region src/core/entityHelpers.ts
|
|
2344
|
-
/**
|
|
2345
|
-
* Per-request entity helpers — read the resource binding (idField + the
|
|
2346
|
-
* URL `:id` value) off `req.arc`.
|
|
2347
|
-
*
|
|
2348
|
-
* Action handlers receive their `id` argument as the raw URL `:id` value.
|
|
2349
|
-
* When the resource declares a custom `idField` (`slug`, `reportId`, …)
|
|
2350
|
-
* that's NOT the document `_id`, a naive `Model.findById(id)` silently
|
|
2351
|
-
* returns null. The historical footgun was a `findById(id)` typo where
|
|
2352
|
-
* the handler author hadn't realised that `:id` resolves to the friendly
|
|
2353
|
-
* handle.
|
|
2354
|
-
*
|
|
2355
|
-
* `getEntityQuery(req)` produces the canonical filter shape so handlers
|
|
2356
|
-
* compose lookups with no resource-config recall:
|
|
2357
|
-
*
|
|
2358
|
-
* ```ts
|
|
2359
|
-
* actions: {
|
|
2360
|
-
* archive: {
|
|
2361
|
-
* handler: async (id, data, req) => {
|
|
2362
|
-
* const doc = await Model.findOne(getEntityQuery(req));
|
|
2363
|
-
* if (!doc) throw new NotFoundError("Order");
|
|
2364
|
-
* // ...
|
|
2365
|
-
* },
|
|
2366
|
-
* },
|
|
2367
|
-
* }
|
|
2368
|
-
* ```
|
|
2369
|
-
*
|
|
2370
|
-
* The router populates `req.arc.idField` and `req.arc.entityId` before
|
|
2371
|
-
* invoking the handler — these helpers are zero-cost reads.
|
|
2372
|
-
*/
|
|
2373
|
-
/**
|
|
2374
|
-
* Read the resource's configured `idField` for the current request.
|
|
2375
|
-
* Falls back to the framework default (`_id`) when the route hasn't
|
|
2376
|
-
* bound an idField — keeps handlers safe to author without checking
|
|
2377
|
-
* the resource config.
|
|
2378
|
-
*/
|
|
2379
|
-
function getEntityIdField(req) {
|
|
2380
|
-
return req.arc?.idField ?? "_id";
|
|
2381
|
-
}
|
|
2382
|
-
/**
|
|
2383
|
-
* Read the URL `:id` path param value as the resource handle.
|
|
2384
|
-
* Returns `undefined` when the route has no `:id` segment (collection
|
|
2385
|
-
* routes) — handlers that need the entity must be on row routes.
|
|
2386
|
-
*/
|
|
2387
|
-
function getEntityId(req) {
|
|
2388
|
-
if (req.arc?.entityId !== void 0) return req.arc.entityId;
|
|
2389
|
-
return req.params?.id;
|
|
2390
|
-
}
|
|
2391
|
-
/**
|
|
2392
|
-
* Compose a `findOne` filter that resolves the current request's
|
|
2393
|
-
* entity, regardless of whether the resource binds `_id` or a custom
|
|
2394
|
-
* field. Idiomatic shape for arc action handlers.
|
|
2395
|
-
*
|
|
2396
|
-
* ```ts
|
|
2397
|
-
* const doc = await Model.findOne(getEntityQuery(req));
|
|
2398
|
-
* ```
|
|
2399
|
-
*
|
|
2400
|
-
* Returns `{}` when the route has no entity context (collection routes
|
|
2401
|
-
* or tests bypassing the router) — caller decides what that means.
|
|
2402
|
-
*/
|
|
2403
|
-
function getEntityQuery(req) {
|
|
2404
|
-
const id = getEntityId(req);
|
|
2405
|
-
if (id === void 0) return {};
|
|
2406
|
-
return { [getEntityIdField(req)]: id };
|
|
2407
|
-
}
|
|
2408
|
-
//#endregion
|
|
2409
|
-
export { defineResource as a, validateResourceConfig as c, defineAction as d, createCrudRouter as f, defineResourceVariants as i, ResourceDefinition as l, defineAggregation as m, getEntityIdField as n, assertValidConfig as o, createPermissionMiddleware as p, getEntityQuery as r, formatValidationErrors as s, getEntityId as t, mergeResourceConfig as u };
|
|
2184
|
+
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-Q5gzEIWY.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
1
|
import { n as getUserRoles } from "../userHelpers-Cg_y3fBz.mjs";
|
|
2
|
-
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-
|
|
2
|
+
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-B8Vkbxbn.mjs";
|
|
3
3
|
import fp from "fastify-plugin";
|
|
4
4
|
//#region src/docs/scalar.ts
|
|
5
5
|
const scalarPlugin = async (fastify, opts = {}) => {
|
|
@@ -2,109 +2,8 @@ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
3
|
import { d as createDomainError } from "./errors-DkO8Nk6-.mjs";
|
|
4
4
|
import { n as requestContext } from "./requestContext-MrlJwECf.mjs";
|
|
5
|
+
import { n as createEvent, t as MemoryEventTransport } from "./EventTransport-C6YA8xY9.mjs";
|
|
5
6
|
import fp from "fastify-plugin";
|
|
6
|
-
import { createEvent, matchEventPattern } from "@classytic/primitives/events";
|
|
7
|
-
//#region src/events/EventTransport.ts
|
|
8
|
-
/**
|
|
9
|
-
* Internal arc events module — concrete in-memory transport.
|
|
10
|
-
*
|
|
11
|
-
* **Public type contracts moved out (arc 2.12).** `EventMeta`, `DomainEvent`,
|
|
12
|
-
* `EventHandler`, `EventLogger`, `EventTransport`, `DeadLetteredEvent`,
|
|
13
|
-
* `PublishManyResult`, plus the `createEvent` / `createChildEvent` /
|
|
14
|
-
* `matchEventPattern` helpers, are now owned by `@classytic/primitives/events`.
|
|
15
|
-
* Two reasons:
|
|
16
|
-
*
|
|
17
|
-
* 1. The shapes ARE the cross-package contract. arc, arc-next, mongokit's
|
|
18
|
-
* audit-log plugin, future kits and downstream services all share them.
|
|
19
|
-
* Owning them in `primitives` (pure types, zero runtime, zero deps)
|
|
20
|
-
* eliminates the "manually mirror in two places" problem the previous
|
|
21
|
-
* `primitives/src/events.ts` header explicitly called out.
|
|
22
|
-
* 2. Inverting the dependency direction lets future packages (sqlitekit
|
|
23
|
-
* audit, billing services) consume the contract without depending on
|
|
24
|
-
* arc's HTTP-coupled stack.
|
|
25
|
-
*
|
|
26
|
-
* Hosts MUST import the types from primitives directly:
|
|
27
|
-
*
|
|
28
|
-
* ```typescript
|
|
29
|
-
* import type {
|
|
30
|
-
* EventMeta, DomainEvent, EventHandler, EventTransport,
|
|
31
|
-
* DeadLetteredEvent, PublishManyResult,
|
|
32
|
-
* } from '@classytic/primitives/events';
|
|
33
|
-
* import { createEvent, createChildEvent } from '@classytic/primitives/events';
|
|
34
|
-
* ```
|
|
35
|
-
*
|
|
36
|
-
* arc's `events/index.ts` barrel does NOT re-export these — by design, so
|
|
37
|
-
* that consumer code is forced into the canonical import path and the org's
|
|
38
|
-
* "no re-exports" rule holds at every public surface.
|
|
39
|
-
*
|
|
40
|
-
* What stays here:
|
|
41
|
-
* - `MemoryEventTransport` — the in-memory `EventTransport` implementation
|
|
42
|
-
* used as arc's default transport. Its handler-set state is process-local;
|
|
43
|
-
* wrong layer for primitives.
|
|
44
|
-
* - `MemoryEventTransportOptions` — its options shape.
|
|
45
|
-
*
|
|
46
|
-
* Inside arc, files that previously imported types from `./EventTransport.js`
|
|
47
|
-
* keep working: this file re-exports them from primitives for arc's own
|
|
48
|
-
* call sites. That re-export is an internal refactor convenience — the
|
|
49
|
-
* public `events/index.ts` barrel stays clean.
|
|
50
|
-
*/
|
|
51
|
-
/**
|
|
52
|
-
* In-memory event transport (default).
|
|
53
|
-
*
|
|
54
|
-
* Events are delivered synchronously within the process. Not suitable for
|
|
55
|
-
* multi-instance deployments — pair with `RedisEventTransport` /
|
|
56
|
-
* `RedisStreamTransport` (subpath imports) for those.
|
|
57
|
-
*/
|
|
58
|
-
var MemoryEventTransport = class {
|
|
59
|
-
name = "memory";
|
|
60
|
-
handlers = /* @__PURE__ */ new Map();
|
|
61
|
-
logger;
|
|
62
|
-
constructor(options) {
|
|
63
|
-
this.logger = options?.logger ?? console;
|
|
64
|
-
}
|
|
65
|
-
async publish(event) {
|
|
66
|
-
const allHandlers = /* @__PURE__ */ new Set();
|
|
67
|
-
for (const [pattern, handlers] of this.handlers) if (matchEventPattern(pattern, event.type)) for (const h of handlers) allHandlers.add(h);
|
|
68
|
-
for (const handler of allHandlers) try {
|
|
69
|
-
await handler(event);
|
|
70
|
-
} catch (err) {
|
|
71
|
-
this.logger.error(`[EventTransport] Handler error for ${event.type}:`, err);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Reference `publishMany` implementation — delegates to `publish()` in order.
|
|
76
|
-
*
|
|
77
|
-
* Production transports (Kafka, Redis pipeline, SQS batch) should override
|
|
78
|
-
* this with a single batched network call. Memory transport has nothing to
|
|
79
|
-
* batch, so we just loop — the loop still returns a proper result map so
|
|
80
|
-
* `EventOutbox.relay` can exercise the batched code path in tests.
|
|
81
|
-
*/
|
|
82
|
-
async publishMany(events) {
|
|
83
|
-
const results = /* @__PURE__ */ new Map();
|
|
84
|
-
for (const event of events) try {
|
|
85
|
-
await this.publish(event);
|
|
86
|
-
results.set(event.meta.id, null);
|
|
87
|
-
} catch (err) {
|
|
88
|
-
results.set(event.meta.id, err instanceof Error ? err : new Error(String(err)));
|
|
89
|
-
}
|
|
90
|
-
return results;
|
|
91
|
-
}
|
|
92
|
-
async subscribe(pattern, handler) {
|
|
93
|
-
if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
|
|
94
|
-
this.handlers.get(pattern)?.add(handler);
|
|
95
|
-
return () => {
|
|
96
|
-
const set = this.handlers.get(pattern);
|
|
97
|
-
if (set) {
|
|
98
|
-
set.delete(handler);
|
|
99
|
-
if (set.size === 0) this.handlers.delete(pattern);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
async close() {
|
|
104
|
-
this.handlers.clear();
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
//#endregion
|
|
108
7
|
//#region src/events/retry.ts
|
|
109
8
|
/**
|
|
110
9
|
* Wrap an event handler with retry logic and dead letter support.
|
|
@@ -338,4 +237,4 @@ var eventPlugin_default = fp(eventPlugin, {
|
|
|
338
237
|
fastify: "5.x"
|
|
339
238
|
});
|
|
340
239
|
//#endregion
|
|
341
|
-
export {
|
|
240
|
+
export { withRetry as i, eventPlugin_exports as n, createDeadLetterPublisher as r, eventPlugin as t };
|
package/dist/events/index.d.mts
CHANGED
|
@@ -72,6 +72,16 @@ interface MemoryEntry {
|
|
|
72
72
|
declare class MemoryOutboxStore implements OutboxStore {
|
|
73
73
|
private readonly entries;
|
|
74
74
|
private readonly seenDedupeKeys;
|
|
75
|
+
/**
|
|
76
|
+
* Drop every entry (including delivered / failed / dead-lettered rows) and
|
|
77
|
+
* the dedupe-key memory.
|
|
78
|
+
*
|
|
79
|
+
* For tests only — it is the in-memory analogue of truncating the table, and
|
|
80
|
+
* exists so a suite can isolate cases against ONE store instance (the
|
|
81
|
+
* `reset` hook of `runOutboxStoreContract`). Not part of `OutboxStore`; a
|
|
82
|
+
* production store must never expose this.
|
|
83
|
+
*/
|
|
84
|
+
clear(): void;
|
|
75
85
|
save(event: DomainEvent, options?: OutboxWriteOptions): Promise<void>;
|
|
76
86
|
getPending(limit: number): Promise<DomainEvent[]>;
|
|
77
87
|
claimPending(options?: OutboxClaimOptions): Promise<DomainEvent[]>;
|
|
@@ -395,7 +405,23 @@ declare class EventOutbox {
|
|
|
395
405
|
}
|
|
396
406
|
//#endregion
|
|
397
407
|
//#region src/events/repository-outbox-adapter.d.ts
|
|
398
|
-
|
|
408
|
+
interface RepositoryOutboxStoreOptions {
|
|
409
|
+
/**
|
|
410
|
+
* Column holding the "not claimable before" timestamp. Default
|
|
411
|
+
* `'visibleAt'`.
|
|
412
|
+
*
|
|
413
|
+
* Exists so a host with an EXISTING outbox table can adopt this adapter
|
|
414
|
+
* without renaming a column and rebuilding its claim index — the one thing
|
|
415
|
+
* that otherwise forces a migration on the table that guarantees delivery.
|
|
416
|
+
* A store using `nextVisibleAt` (with a `{status, nextVisibleAt, createdAt}`
|
|
417
|
+
* index) passes `{ visibleAtField: 'nextVisibleAt' }` and keeps both.
|
|
418
|
+
*
|
|
419
|
+
* The field is used in the claimable filter, on `save`, and on `fail`'s
|
|
420
|
+
* retry scheduling — all three stay in lockstep by construction.
|
|
421
|
+
*/
|
|
422
|
+
readonly visibleAtField?: string;
|
|
423
|
+
}
|
|
424
|
+
declare function repositoryAsOutboxStore(repository: RepositoryLike, options?: RepositoryOutboxStoreOptions): OutboxStore;
|
|
399
425
|
//#endregion
|
|
400
426
|
//#region src/events/subscribe-helpers.d.ts
|
|
401
427
|
/**
|
|
@@ -556,4 +582,4 @@ interface FastifyEventBus {
|
|
|
556
582
|
};
|
|
557
583
|
}
|
|
558
584
|
//#endregion
|
|
559
|
-
export { ARC_LIFECYCLE_EVENTS, type ArcLifecycleEvent, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, type CacheEvent, type CrudEventSuffix, type CustomValidator, type EventDefinitionInput, type EventDefinitionOutput, EventOutbox, type EventOutboxOptions, type EventPluginOptions, type EventRegistry, type EventRegistryOptions, type EventSchema, type ExponentialBackoffOptions, MemoryEventTransport, type MemoryEventTransportOptions, MemoryOutboxStore, type OutboxRelayErrorHandler, type OutboxRelayErrorKind, type PayloadOf, type RedisEventTransportOptions, type RedisLike, type RedisStreamLike, type RedisStreamTransportOptions, type RelayResult, type RetryOptions, type ValidationResult, type WrapWithBoundaryOptions, type WrapWithSchemaOptions, createDeadLetterPublisher, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, repositoryAsOutboxStore, subscribeWithBoundary, subscribeWithSchema, withRetry, wrapWithBoundary, wrapWithSchema };
|
|
585
|
+
export { ARC_LIFECYCLE_EVENTS, type ArcLifecycleEvent, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, type CacheEvent, type CrudEventSuffix, type CustomValidator, type EventDefinitionInput, type EventDefinitionOutput, EventOutbox, type EventOutboxOptions, type EventPluginOptions, type EventRegistry, type EventRegistryOptions, type EventSchema, type ExponentialBackoffOptions, MemoryEventTransport, type MemoryEventTransportOptions, MemoryOutboxStore, type OutboxRelayErrorHandler, type OutboxRelayErrorKind, type PayloadOf, type RedisEventTransportOptions, type RedisLike, type RedisStreamLike, type RedisStreamTransportOptions, type RelayResult, type RepositoryOutboxStoreOptions, type RetryOptions, type ValidationResult, type WrapWithBoundaryOptions, type WrapWithSchemaOptions, createDeadLetterPublisher, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, repositoryAsOutboxStore, subscribeWithBoundary, subscribeWithSchema, withRetry, wrapWithBoundary, wrapWithSchema };
|