@classytic/arc 2.21.0 → 2.23.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.
Files changed (115) hide show
  1. package/dist/{BaseController-BnLYgE9A.mjs → BaseController-BQ8KNvWC.mjs} +3 -2
  2. package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
  3. package/dist/{actionPermissions-ze70xXJi.mjs → actionPermissions-lszoJle1.mjs} +1 -1
  4. package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
  5. package/dist/audit/index.d.mts +2 -2
  6. package/dist/auth/index.d.mts +30 -2
  7. package/dist/auth/index.mjs +6 -3
  8. package/dist/cli/commands/describe.d.mts +1 -1
  9. package/dist/cli/commands/describe.mjs +1 -1
  10. package/dist/cli/commands/docs.mjs +2 -2
  11. package/dist/cli/commands/init.mjs +1 -1
  12. package/dist/cli/commands/introspect.mjs +1 -1
  13. package/dist/cli/index.mjs +1 -1
  14. package/dist/core/index.d.mts +2 -2
  15. package/dist/core/index.mjs +3 -3
  16. package/dist/{core-DZ6Xe1jR.mjs → core-DQ6rDTyH.mjs} +126 -68
  17. package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-BuOv-m4Y.mjs} +10 -6
  18. package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-CgJv_BU_.mjs} +3 -3
  19. package/dist/{createApp-Bi61XxqU.mjs → createApp-CjOeWJ9_.mjs} +252 -57
  20. package/dist/docs/index.d.mts +1 -1
  21. package/dist/docs/index.mjs +1 -1
  22. package/dist/encryption/index.mjs +9 -5
  23. package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-Bq2OeEQL.mjs} +34 -10
  24. package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
  25. package/dist/events/index.mjs +5 -2
  26. package/dist/factory/index.d.mts +46 -9
  27. package/dist/factory/index.mjs +64 -13
  28. package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-g_f9omSc.d.mts} +2 -2
  29. package/dist/fields-D3lhgzhD.d.mts +121 -0
  30. package/dist/health-ScyjIVo5.mjs +202 -0
  31. package/dist/hooks/index.d.mts +1 -1
  32. package/dist/idempotency/index.d.mts +7 -0
  33. package/dist/idempotency/index.mjs +7 -17
  34. package/dist/{index-Dtrj6XZk.d.mts → index-BBsvQt7U.d.mts} +1310 -1226
  35. package/dist/{types-DpxoNX5v.d.mts → index-Ba10fdKp.d.mts} +196 -66
  36. package/dist/{index-B5N-NaHs.d.mts → index-CC5ruDGh.d.mts} +3 -3
  37. package/dist/{index-B5PoNbfu.d.mts → index-wMoWBLkn.d.mts} +68 -2
  38. package/dist/index.d.mts +6 -5
  39. package/dist/index.mjs +6 -5
  40. package/dist/{init-0YhHkXGk.mjs → init-CjO4p_q1.mjs} +123 -4
  41. package/dist/integrations/event-gateway.mjs +1 -1
  42. package/dist/integrations/index.d.mts +1 -1
  43. package/dist/integrations/mcp/index.d.mts +67 -4
  44. package/dist/integrations/mcp/index.mjs +1 -1
  45. package/dist/integrations/mcp/testing.d.mts +1 -1
  46. package/dist/integrations/mcp/testing.mjs +1 -1
  47. package/dist/integrations/webhooks.d.mts +131 -7
  48. package/dist/integrations/webhooks.mjs +169 -10
  49. package/dist/interface-n5hpKg1d.mjs +16 -0
  50. package/dist/jsonBody-BlzAoGCe.mjs +43 -0
  51. package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
  52. package/dist/logger/index.d.mts +26 -1
  53. package/dist/logger/index.mjs +39 -1
  54. package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-BtzIL_kM.mjs} +312 -39
  55. package/dist/middleware/index.d.mts +1 -1
  56. package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
  57. package/dist/{openapi-Kyod_di6.mjs → openapi-jSDx-VDv.mjs} +3 -3
  58. package/dist/permissions/index.d.mts +4 -3
  59. package/dist/permissions/index.mjs +3 -2
  60. package/dist/{permissions-BJMuKMl7.mjs → permissions-tbxFHxjZ.mjs} +77 -264
  61. package/dist/pipeline/index.d.mts +1 -1
  62. package/dist/plugins/index.d.mts +24 -7
  63. package/dist/plugins/index.mjs +14 -282
  64. package/dist/plugins/response-cache.mjs +1 -0
  65. package/dist/plugins/tracing-entry.mjs +7 -4
  66. package/dist/presets/filesUpload.d.mts +2 -2
  67. package/dist/presets/filesUpload.mjs +1 -1
  68. package/dist/presets/index.d.mts +1 -1
  69. package/dist/presets/index.mjs +1 -1
  70. package/dist/presets/multiTenant.d.mts +1 -1
  71. package/dist/presets/search.d.mts +2 -2
  72. package/dist/presets/search.mjs +1 -1
  73. package/dist/{presets-gw0ciIUD.mjs → presets-BOe4Ugkl.mjs} +1 -1
  74. package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
  75. package/dist/realtime-I8OrfnMR.mjs +375 -0
  76. package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-CWO6mdYe.mjs} +38 -18
  77. package/dist/registry/index.d.mts +2 -2
  78. package/dist/registry/index.mjs +1 -1
  79. package/dist/requestId-5BhTVzBv.mjs +113 -0
  80. package/dist/schemaConverter-DJvodiHj.mjs +171 -0
  81. package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-Ca7ySNPx.mjs} +2 -2
  82. package/dist/scim/index.mjs +6 -7
  83. package/dist/scope/index.mjs +2 -76
  84. package/dist/scope-LONQ3mfZ.mjs +37 -0
  85. package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
  86. package/dist/{sse-CfUjwjDk.mjs → sse-0u4P4-OI.mjs} +7 -30
  87. package/dist/sseStream-BQ7Qaynp.mjs +57 -0
  88. package/dist/testing/index.d.mts +83 -3
  89. package/dist/testing/index.mjs +70 -3
  90. package/dist/types/index.d.mts +2 -2
  91. package/dist/{types-DRj-Ut4n.d.mts → types-5OuqeTDX.d.mts} +37 -2
  92. package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
  93. package/dist/usage/index.d.mts +142 -0
  94. package/dist/usage/index.mjs +0 -0
  95. package/dist/utils/index.d.mts +101 -19
  96. package/dist/utils/index.mjs +5 -3
  97. package/dist/{utils-CO8a3B_1.mjs → utils-CZyeRXTh.mjs} +80 -82
  98. package/dist/{versioning-DQ4mCvg0.d.mts → versioning-D6PvCwpq.d.mts} +43 -9
  99. package/llms.txt +39 -0
  100. package/package.json +513 -447
  101. package/skills/arc/SKILL.md +14 -8
  102. package/skills/arc/references/api-reference.md +34 -0
  103. package/skills/arc/references/auth.md +21 -4
  104. package/skills/arc/references/mcp.md +1 -1
  105. package/skills/arc/references/production.md +734 -654
  106. package/skills/arc-code-review/SKILL.md +1 -1
  107. package/skills/arc-code-review/references/anti-patterns.md +71 -18
  108. package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
  109. package/skills/arc-code-review/references/migration-recipes.md +7 -7
  110. package/skills/arc-code-review/references/mongokit-migration.md +4 -3
  111. package/skills/arc-module-publishing/SKILL.md +2 -1
  112. package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
  113. package/skills/arc-module-publishing/references/package-manifest.md +4 -2
  114. package/dist/preloadResources-CHw3_oA_.mjs +0 -45
  115. package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
@@ -2,16 +2,17 @@ import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-DN
2
2
  import { arcLog } from "./logger/index.mjs";
3
3
  import { r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
4
4
  import { b as isElevated, n as PUBLIC_SCOPE, x as isMember } from "./types-bHblZ9YN.mjs";
5
- import { M as assertValidConfig, l as getDefaultCrudSchemas } from "./utils-CO8a3B_1.mjs";
6
- import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-BnLYgE9A.mjs";
7
- import { a as selectPluginMw, c as buildPreHandlerChain, f as buildArcDecorator, i as resolveRouterPluginMw, l as resolveRoutePreHandlers, n as buildRateLimitConfig, r as buildRouteConfig, s as buildCrudPermissionMw, t as tryRegisterRoute, u as buildAuthMiddleware } from "./registerRoute-oGAl0oA8.mjs";
8
- import { I as applyFieldWritePermissions, R as resolveEffectiveRoles } from "./permissions-BJMuKMl7.mjs";
5
+ import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-BQ8KNvWC.mjs";
6
+ import { a as selectPluginMw, c as buildRouteHooks, d as buildAuthMiddleware, i as resolveRouterPluginMw, l as resolveRoutePreHandlers, n as buildRateLimitConfig, p as buildArcDecorator, r as buildRouteConfig, s as buildCrudPermissionMw, t as tryRegisterRoute, u as routeHookOptions } from "./registerRoute-CWO6mdYe.mjs";
7
+ import { a as applyFieldWritePermissions, s as resolveEffectiveRoles } from "./applyPermissionResult-Tw4iXtlf.mjs";
9
8
  import { t as getUserRoles } from "./types-XTApa8b6.mjs";
10
- import { t as applyPresets } from "./presets-gw0ciIUD.mjs";
11
- import { n as convertRouteSchema, t as convertOpenApiSchemas } from "./schemaConverter-U0MnO9vd.mjs";
9
+ import { t as applyPresets } from "./presets-BOe4Ugkl.mjs";
10
+ import { D as requireAuth } from "./permissions-tbxFHxjZ.mjs";
11
+ import { N as assertValidConfig, l as getDefaultCrudSchemas } from "./utils-CZyeRXTh.mjs";
12
+ import { n as convertRouteSchema, r as ensureSchemaConverter, t as convertOpenApiSchemas } from "./schemaConverter-DJvodiHj.mjs";
12
13
  import { i as pipeUIMessageStreamToReply, r as isReadableStream } from "./streaming-BQcW6_Kq.mjs";
13
14
  import { t as hasEvents } from "./typeGuards-C9KXUly6.mjs";
14
- import { a as createCrudHandlers, d as buildRequestScopeProjection, i as resolvePipelineSteps, o as createFastifyHandler, r as buildPipelineHandler, s as createRequestContext, t as resolveActionPermission } from "./actionPermissions-ze70xXJi.mjs";
15
+ import { a as createCrudHandlers, d as buildRequestScopeProjection, i as resolvePipelineSteps, o as createFastifyHandler, r as buildPipelineHandler, s as createRequestContext, t as resolveActionPermission } from "./actionPermissions-lszoJle1.mjs";
15
16
  import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
16
17
  //#region src/core/middlewares/fieldWrite.ts
17
18
  /**
@@ -151,7 +152,7 @@ function createCustomRoutes(fastify, routes, controller, options) {
151
152
  const fieldWriteMw = buildFieldWritePreHandler(fieldPermissions, onFieldWriteDenied);
152
153
  if (fieldWriteMw) customPreHandlers.push(fieldWriteMw);
153
154
  }
154
- const preHandler = buildPreHandlerChain({
155
+ const hooks = buildRouteHooks({
155
156
  preAuth: route.preAuth ?? [],
156
157
  arcDecorator,
157
158
  authMw: buildAuthMiddleware(fastify, route.permissions),
@@ -165,7 +166,7 @@ function createCustomRoutes(fastify, routes, controller, options) {
165
166
  method: route.method,
166
167
  url: route.path,
167
168
  schema,
168
- preHandler: preHandler.length > 0 ? preHandler : void 0,
169
+ ...routeHookOptions(hooks),
169
170
  handler: isStream ? async (request, reply) => {
170
171
  reply.raw.setHeader("Content-Type", "text/event-stream");
171
172
  reply.raw.setHeader("Cache-Control", "no-cache");
@@ -274,7 +275,7 @@ function createCrudRouter(fastify, controller, options = {}) {
274
275
  for (const spec of crudTable) {
275
276
  if (disabledRoutes.includes(spec.op)) continue;
276
277
  const permission = permissions[spec.op];
277
- const preHandler = buildPreHandlerChain({
278
+ const hooks = buildRouteHooks({
278
279
  arcDecorator,
279
280
  authMw: buildAuthMiddleware(fastify, permission),
280
281
  permissionMw: buildCrudPermissionMw(permission, resourceName, spec.op),
@@ -293,7 +294,7 @@ function createCrudRouter(fastify, controller, options = {}) {
293
294
  summary,
294
295
  ...spec.hasIdParams ? { params: idParamsSchema } : {}
295
296
  }, defaultSchemas[spec.op], schemas[spec.op]),
296
- preHandler: preHandler.length > 0 ? preHandler : void 0,
297
+ ...routeHookOptions(hooks),
297
298
  handler: handlers[spec.op],
298
299
  ...buildRouteConfig(rateLimitConfig, extensions)
299
300
  }, {
@@ -359,6 +360,58 @@ function defineAction(config) {
359
360
  };
360
361
  }
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
362
415
  //#region src/core/defineResource/controller.ts
363
416
  /**
364
417
  * Phase 4 — pick (or auto-create) the resource's controller.
@@ -652,7 +705,7 @@ function resolveIdField(config, repository) {
652
705
  * enabled but no adapter" (a confusing false-positive error).
653
706
  */
654
707
  function normalizeResourceConfig(config) {
655
- return resolveCrudAllowList(expandCustomRoutesOnly(expandReferenceData(config)));
708
+ return resolveCrudAllowList(expandHistory(expandCustomRoutesOnly(expandReferenceData(config))));
656
709
  }
657
710
  /**
658
711
  * Expand the `referenceData: true` shorthand. Reference data is the
@@ -706,6 +759,61 @@ function expandCustomRoutesOnly(config) {
706
759
  };
707
760
  }
708
761
  /**
762
+ * Expand the 2.22 `history: true` shorthand — the per-record change
763
+ * timeline. Injects `GET /:id/history` (audit-entry page for one
764
+ * document, newest-first per the audit store's contract) and implies
765
+ * `audit: true` so the rows the route reads actually get written.
766
+ *
767
+ * The gate defaults STRICTER than reads (history exposes before/after
768
+ * snapshots): resource `update` permission → `get` → route auth.
769
+ * `history: { permissions }` overrides. The flag is CONSUMED here
770
+ * (set to undefined) — later phases and the registry see only its
771
+ * expansion, same contract as `customRoutesOnly`.
772
+ */
773
+ function expandHistory(config) {
774
+ if (!config.history) return config;
775
+ const opts = config.history === true ? {} : config.history;
776
+ const permissions = opts.permissions ?? config.permissions?.update ?? config.permissions?.get ?? requireAuth();
777
+ const defaultLimit = opts.limit ?? 50;
778
+ const resourceName = config.name;
779
+ const historyRoute = {
780
+ method: "GET",
781
+ path: "/:id/history",
782
+ summary: `Change history for a ${resourceName} record (audit-backed timeline)`,
783
+ raw: true,
784
+ handler: async (request, reply) => {
785
+ const audit = request.server.audit;
786
+ if (!audit?.query || audit._noop === true) return reply.code(503).send({
787
+ code: "history.audit_unavailable",
788
+ message: `history for '${resourceName}' requires auditPlugin to be registered (and enabled)`,
789
+ status: 503
790
+ });
791
+ const { id } = request.params;
792
+ const q = request.query;
793
+ const limit = Math.min(Number(q.limit) > 0 ? Number(q.limit) : defaultLimit, 200);
794
+ const offset = Number(q.offset) > 0 ? Number(q.offset) : 0;
795
+ const entries = await audit.query({
796
+ resource: resourceName,
797
+ documentId: id,
798
+ limit,
799
+ offset
800
+ });
801
+ return reply.send({
802
+ data: entries,
803
+ limit,
804
+ offset
805
+ });
806
+ },
807
+ permissions
808
+ };
809
+ return {
810
+ ...config,
811
+ history: void 0,
812
+ audit: config.audit ?? true,
813
+ routes: [...config.routes ?? [], historyRoute]
814
+ };
815
+ }
816
+ /**
709
817
  * Normalise the 2.16 `crud:` positive-form allow-list into the canonical
710
818
  * `{ disabledRoutes, disableDefaultRoutes }` pair the rest of arc reads.
711
819
  *
@@ -1221,7 +1329,8 @@ function normalizeActionsToRouterConfig(actions, resourceActionPermissions, tag,
1221
1329
  * Fastify instance via `resource._sharedStateRegisteredOn`.
1222
1330
  */
1223
1331
  function buildResourcePlugin(resource) {
1224
- return async function resourcePlugin(fastify, _opts) {
1332
+ return async function resourcePlugin(fastify, opts) {
1333
+ await ensureSchemaConverter();
1225
1334
  const sharedRoot = fastify.server;
1226
1335
  const isFirstMount = !resource._sharedStateRegisteredOn.has(sharedRoot);
1227
1336
  if (isFirstMount) resource._sharedStateRegisteredOn.add(sharedRoot);
@@ -1257,6 +1366,7 @@ function buildResourcePlugin(resource) {
1257
1366
  fastify.log?.warn?.(`[Arc] Resource '${resource.name}' declares events (${eventNames}) but \`eventPlugin\` is not registered. The events will silently NOT be emitted on CRUD mutations. Either register the plugin (\`await fastify.register(eventPlugin, { transport: ... })\`) or remove the \`events\` declaration from the resource.`);
1258
1367
  }
1259
1368
  }
1369
+ if (opts?.arcMountRoutes === false) return;
1260
1370
  await fastify.register(async (instance) => {
1261
1371
  const typedInstance = instance;
1262
1372
  let schemas = buildGeneratedCrudSchemas(resource._registryMeta?.openApiSchemas, resource.customSchemas);
@@ -1286,7 +1396,7 @@ function buildResourcePlugin(resource) {
1286
1396
  extensions: resource.extensions
1287
1397
  });
1288
1398
  if (resource.actions && Object.keys(resource.actions).length > 0) {
1289
- const { createActionRouter } = await import("./createActionRouter-BWMDW-dL.mjs").then((n) => n.n);
1399
+ const { createActionRouter } = await import("./createActionRouter-BuOv-m4Y.mjs").then((n) => n.n);
1290
1400
  createActionRouter(typedInstance, {
1291
1401
  ...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
1292
1402
  resourceName: resource.name,
@@ -1301,7 +1411,7 @@ function buildResourcePlugin(resource) {
1301
1411
  });
1302
1412
  }
1303
1413
  if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
1304
- const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-l5mNdYrZ.mjs"), import("./validate-C2mGgSXg.mjs").then((n) => n.r)]);
1414
+ const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-CgJv_BU_.mjs"), import("./validate-C2mGgSXg.mjs").then((n) => n.r)]);
1305
1415
  const repoForAgg = resource.controller?.repository ?? resource.repository;
1306
1416
  if (!adapterSupportsAggregate(repoForAgg)) {
1307
1417
  const undispatchable = Object.entries(resource.aggregations).filter(([, cfg]) => !cfg.materialized).map(([name]) => name);
@@ -1913,58 +2023,6 @@ function defineResource(config) {
1913
2023
  return resource;
1914
2024
  }
1915
2025
  //#endregion
1916
- //#region src/core/defineResource/seams.ts
1917
- /** Plain object test — literals only; class instances must replace, not merge. */
1918
- function isPlainObject(value) {
1919
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
1920
- const proto = Object.getPrototypeOf(value);
1921
- return proto === Object.prototype || proto === null;
1922
- }
1923
- /** Recursive plain-object merge; nested arrays REPLACE (they're value lists). */
1924
- function mergePlain(base, next) {
1925
- const out = { ...base };
1926
- for (const [key, value] of Object.entries(next)) {
1927
- if (value === void 0) continue;
1928
- const existing = out[key];
1929
- out[key] = isPlainObject(existing) && isPlainObject(value) ? mergePlain(existing, value) : value;
1930
- }
1931
- return out;
1932
- }
1933
- /**
1934
- * Canonical slot-aware merge of a base resource config with host seams.
1935
- * Later seams win over earlier ones; `undefined` values never clobber.
1936
- *
1937
- * Merge semantics (value-driven — no per-slot registry to drift):
1938
- *
1939
- * - **Top-level arrays CONCAT** — `routes`, `middlewares`, `presets`,
1940
- * `routeGuards`, `disabledRoutes`: additive collections, base first.
1941
- * - **Plain objects merge recursively** — `permissions`, `actions`,
1942
- * `schemaOptions` (incl. `fieldRules`), `aggregations`, `customSchemas`,
1943
- * `fields`, `cache`, `events`: per-key composition. Arrays NESTED inside
1944
- * them replace (they're value lists — enums, sort specs), not concat.
1945
- * - **Everything else last-wins** — class instances (`adapter`,
1946
- * `controller`, `queryParser`), functions, scalars.
1947
- *
1948
- * The single `as` at the return is the one documented erasure for the
1949
- * {@link AdapterLike}-widened seam slots — inside the helper so no module
1950
- * author ever writes one (mirrors mongokit's one-cast-at-the-boundary rule).
1951
- */
1952
- function mergeResourceConfig(base, ...seams) {
1953
- const out = { ...base };
1954
- for (const seam of seams) {
1955
- if (!seam) continue;
1956
- for (const [key, value] of Object.entries(seam)) {
1957
- if (value === void 0) continue;
1958
- const existing = out[key];
1959
- if (Array.isArray(existing) && Array.isArray(value)) out[key] = [...existing, ...value];
1960
- else if (isPlainObject(existing) && isPlainObject(value)) out[key] = mergePlain(existing, value);
1961
- else if (Array.isArray(value)) out[key] = [...value];
1962
- else out[key] = value;
1963
- }
1964
- }
1965
- return out;
1966
- }
1967
- //#endregion
1968
2026
  //#region src/core/defineResourceVariants.ts
1969
2027
  /**
1970
2028
  * Define multiple resources from a shared base config and per-variant overrides.
@@ -2063,4 +2121,4 @@ function getEntityQuery(req) {
2063
2121
  return { [getEntityIdField(req)]: id };
2064
2122
  }
2065
2123
  //#endregion
2066
- export { mergeResourceConfig as a, defineAction as c, defineAggregation as d, defineResourceVariants as i, createCrudRouter as l, getEntityIdField as n, defineResource as o, getEntityQuery as r, ResourceDefinition as s, getEntityId as t, createPermissionMiddleware as u };
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 };
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
2
2
  import "./constants-DNre0wRg.mjs";
3
3
  import { f as createError } from "./errors-DkO8Nk6-.mjs";
4
- import { a as selectPluginMw, c as buildPreHandlerChain, d as buildAuthMiddlewareForPermissions, f as buildArcDecorator, i as resolveRouterPluginMw, n as buildRateLimitConfig, o as buildActionPermissionMw, r as buildRouteConfig, t as tryRegisterRoute } from "./registerRoute-oGAl0oA8.mjs";
5
- import { i as resolvePipelineSteps, n as buildActionPipelineHandler, u as sendControllerResponse } from "./actionPermissions-ze70xXJi.mjs";
6
- import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-ajetw3Fv.mjs";
4
+ import { a as selectPluginMw, c as buildRouteHooks, f as buildAuthMiddlewareForPermissions, i as resolveRouterPluginMw, n as buildRateLimitConfig, o as buildActionPermissionMw, p as buildArcDecorator, r as buildRouteConfig, t as tryRegisterRoute, u as routeHookOptions } from "./registerRoute-CWO6mdYe.mjs";
5
+ import { i as resolvePipelineSteps, n as buildActionPipelineHandler, u as sendControllerResponse } from "./actionPermissions-lszoJle1.mjs";
6
+ import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-Ca7ySNPx.mjs";
7
7
  //#region src/core/createActionRouter.ts
8
8
  var createActionRouter_exports = /* @__PURE__ */ __exportAll({
9
9
  buildActionBodySchema: () => buildActionBodySchema,
@@ -108,19 +108,23 @@ function createActionRouter(fastify, config) {
108
108
  scopedError.meta = { action: submitted };
109
109
  throw scopedError;
110
110
  };
111
- const preHandler = [actionValidationFormatter, ...buildPreHandlerChain({
111
+ const hooks = buildRouteHooks({
112
112
  arcDecorator,
113
113
  authMw,
114
114
  permissionMw,
115
115
  pluginMw: selectPluginMw("POST", pluginMw),
116
116
  routeGuards
117
- })];
117
+ });
118
+ const preHandler = [actionValidationFormatter, ...hooks.preHandler];
118
119
  tryRegisterRoute(fastify, {
119
120
  method: "POST",
120
121
  url: urlPath,
121
122
  schema: routeSchema,
122
123
  attachValidation: true,
123
- preHandler: preHandler.length > 0 ? preHandler : void 0,
124
+ ...routeHookOptions({
125
+ onRequest: hooks.onRequest,
126
+ preHandler
127
+ }),
124
128
  ...buildRouteConfig(rateLimitConfig, extensions),
125
129
  handler: async (req, reply) => {
126
130
  const { action, ...data } = req.body;
@@ -1,5 +1,5 @@
1
1
  import { f as createError, l as UnauthorizedError, r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
2
- import { a as selectPluginMw, c as buildPreHandlerChain, f as buildArcDecorator, i as resolveRouterPluginMw, n as buildRateLimitConfig, u as buildAuthMiddleware } from "./registerRoute-oGAl0oA8.mjs";
2
+ import { a as selectPluginMw, c as buildRouteHooks, d as buildAuthMiddleware, i as resolveRouterPluginMw, n as buildRateLimitConfig, p as buildArcDecorator, u as routeHookOptions } from "./registerRoute-CWO6mdYe.mjs";
3
3
  import { n as validateAggregations } from "./validate-C2mGgSXg.mjs";
4
4
  import { t as buildAggregationHandler } from "./buildHandler-D_2ULWxZ.mjs";
5
5
  //#region src/core/aggregation/createAggregationRouter.ts
@@ -48,7 +48,7 @@ function registerOne(fastify, normalized, ctx) {
48
48
  throw new ForbiddenError(reason ?? "You do not have permission to access this aggregation.");
49
49
  }
50
50
  };
51
- const preHandler = buildPreHandlerChain({
51
+ const hooks = buildRouteHooks({
52
52
  arcDecorator,
53
53
  authMw,
54
54
  permissionMw,
@@ -73,7 +73,7 @@ function registerOne(fastify, normalized, ctx) {
73
73
  method: "GET",
74
74
  url: `/aggregations/${name}`,
75
75
  schema: routeSchema,
76
- preHandler: preHandler.length > 0 ? preHandler : void 0,
76
+ ...routeHookOptions(hooks),
77
77
  ...rateLimitConfig ? { config: rateLimitConfig } : {},
78
78
  handler: async (req, reply) => {
79
79
  try {