@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.
Files changed (92) hide show
  1. package/README.md +2 -0
  2. package/dist/{BaseController--M8EMzt0.mjs → BaseController-B5DwufbB.mjs} +15 -11
  3. package/dist/EventTransport-C6YA8xY9.mjs +103 -0
  4. package/dist/{HookSystem-C1vB_Gc9.mjs → HookSystem-WkIougo6.mjs} +15 -35
  5. package/dist/applyPermissionResult-Cf6zUCvb.mjs +257 -0
  6. package/dist/audit/index.d.mts +2 -2
  7. package/dist/audit/index.mjs +1 -1
  8. package/dist/auth/index.d.mts +2 -2
  9. package/dist/auth/index.mjs +1 -1
  10. package/dist/{buildHandler-Dtwd8fdz.mjs → buildHandler-BYS3Vkfz.mjs} +32 -6
  11. package/dist/cleanup/index.d.mts +313 -0
  12. package/dist/cleanup/index.mjs +1267 -0
  13. package/dist/cli/commands/describe.d.mts +1 -1
  14. package/dist/cli/commands/docs.mjs +1 -1
  15. package/dist/core/index.d.mts +2 -2
  16. package/dist/core/index.mjs +5 -3
  17. package/dist/core-poalJLvp.mjs +175 -0
  18. package/dist/{createActionRouter-B6GvAuAX.mjs → createActionRouter-BxuywVRB.mjs} +2 -2
  19. package/dist/{createAggregationRouter-wzwFa_dW.mjs → createAggregationRouter-BiY-lGdY.mjs} +7 -20
  20. package/dist/{preloadResources-DfyngIN9.mjs → createApp-CXrKFciH.mjs} +25 -76
  21. package/dist/defineAggregation-ufWjUwml.mjs +47 -0
  22. package/dist/{core-EUNu0u07.mjs → defineResource-CxWqDCAy.mjs} +36 -261
  23. package/dist/docs/index.d.mts +1 -1
  24. package/dist/docs/index.mjs +1 -1
  25. package/dist/{eventPlugin-DrQ_FesK.mjs → eventPlugin-D2NNR4py.mjs} +2 -103
  26. package/dist/events/index.d.mts +28 -2
  27. package/dist/events/index.mjs +24 -343
  28. package/dist/factory/index.d.mts +4 -3
  29. package/dist/factory/index.mjs +3 -1
  30. package/dist/{fastify-augmentation-dPNBVeIj.d.mts → fastify-augmentation-0WhPnUH7.d.mts} +3 -2
  31. package/dist/{applyPermissionResult-Tw4iXtlf.mjs → fields-CQ_7KQWw.mjs} +1 -97
  32. package/dist/hooks/index.d.mts +1 -1
  33. package/dist/hooks/index.mjs +1 -1
  34. package/dist/idempotency/index.mjs +1 -1
  35. package/dist/index-CCzVR-N3.d.mts +107 -0
  36. package/dist/{index-BMnbzvtO.d.mts → index-Ch16aAah.d.mts} +201 -21
  37. package/dist/{index-f4PTrk3B.d.mts → index-Cx7bro4R.d.mts} +4 -4
  38. package/dist/{index-DPRH0Cme.d.mts → index-Q5gzEIWY.d.mts} +97 -66
  39. package/dist/index.d.mts +6 -6
  40. package/dist/index.mjs +8 -6
  41. package/dist/integrations/index.d.mts +1 -1
  42. package/dist/integrations/mcp/index.d.mts +6 -6
  43. package/dist/integrations/mcp/index.mjs +1 -1
  44. package/dist/integrations/mcp/testing.d.mts +1 -1
  45. package/dist/integrations/mcp/testing.mjs +1 -1
  46. package/dist/{mcpPlugin-Cb7olw0j.mjs → mcpPlugin-nRzm5g2s.mjs} +36 -43
  47. package/dist/middleware/index.d.mts +1 -1
  48. package/dist/migrations/index.d.mts +0 -3
  49. package/dist/migrations/index.mjs +0 -3
  50. package/dist/module-_-M7C5ES.mjs +29 -0
  51. package/dist/{openapi-DrLtog_B.mjs → openapi-B8Vkbxbn.mjs} +1 -1
  52. package/dist/permissions/index.d.mts +3 -3
  53. package/dist/permissions/index.mjs +4 -3
  54. package/dist/{permissions-BrtiBKGs.mjs → permissions-Bfu_8y1U.mjs} +318 -289
  55. package/dist/pipeline/index.d.mts +1 -1
  56. package/dist/{pipeline-BN5G05rd.mjs → pipeline-BJnpkY9i.mjs} +4 -18
  57. package/dist/plugins/index.d.mts +4 -4
  58. package/dist/plugins/index.mjs +2 -2
  59. package/dist/plugins/tracing-entry.mjs +1 -1
  60. package/dist/{preloadResources-0oe-JSSS.d.mts → preloadResources-DCfa2egz.d.mts} +3 -2
  61. package/dist/preloadResources-DSY5HGpS.mjs +45 -0
  62. package/dist/presets/filesUpload.d.mts +2 -2
  63. package/dist/presets/filesUpload.mjs +1 -1
  64. package/dist/presets/index.d.mts +14 -2
  65. package/dist/presets/index.mjs +1 -1
  66. package/dist/presets/multiTenant.d.mts +1 -1
  67. package/dist/presets/search.d.mts +2 -2
  68. package/dist/presets/search.mjs +1 -1
  69. package/dist/{presets-DwFDVKP0.mjs → presets-CJOEXLuk.mjs} +25 -9
  70. package/dist/projection-UBDgfMwu.mjs +17 -0
  71. package/dist/{realtime-Cuw_deA-.mjs → realtime-B9nmIY5V.mjs} +2 -1
  72. package/dist/{registerRoute-v0Wb6Plt.mjs → registerRoute-JkxIEBJq.mjs} +3 -1
  73. package/dist/registry/index.d.mts +2 -2
  74. package/dist/registry/index.mjs +1 -1
  75. package/dist/{registry-vcOCtPij.mjs → registry-BYXUUg21.mjs} +16 -1
  76. package/dist/repositoryFilter-Dz8k3S4P.mjs +44 -0
  77. package/dist/subscribe-helpers-DfzsivM6.mjs +339 -0
  78. package/dist/testing/cleanupStoreContract.d.mts +10 -0
  79. package/dist/testing/cleanupStoreContract.mjs +375 -0
  80. package/dist/testing/index.d.mts +34 -5
  81. package/dist/testing/index.mjs +316 -211
  82. package/dist/testing/outboxStoreContract.d.mts +13 -0
  83. package/dist/testing/outboxStoreContract.mjs +247 -0
  84. package/dist/types/index.d.mts +3 -3
  85. package/dist/{types-QFGc3Cjv.d.mts → types-Bmmn-fO0.d.mts} +1 -1
  86. package/dist/types-C_ZZRcD6.d.mts +450 -0
  87. package/dist/{types-Dmw-6Qeo.d.mts → types-ChvvVNP8.d.mts} +93 -88
  88. package/dist/{index-KUoxjIq1.d.mts → types-D_n8L6VY.d.mts} +33 -106
  89. package/dist/usage/index.d.mts +1 -1
  90. package/dist/usage/index.mjs +0 -0
  91. package/dist/utils/index.d.mts +2 -5
  92. package/package.json +539 -524
@@ -1,4 +1,4 @@
1
- import { Nt as RateLimitConfig, V as ResourceDefinition, et as RouteSchemaOptions } from "../../index-DPRH0Cme.mjs";
1
+ import { Nt as RateLimitConfig, V as ResourceDefinition, et as RouteSchemaOptions } from "../../index-Q5gzEIWY.mjs";
2
2
  //#region src/cli/commands/describe.d.ts
3
3
  interface DescribedResource {
4
4
  name: string;
@@ -1,4 +1,4 @@
1
- import { t as buildOpenApiSpec } from "../../openapi-DrLtog_B.mjs";
1
+ import { t as buildOpenApiSpec } from "../../openapi-B8Vkbxbn.mjs";
2
2
  import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-kvGT6EcO.mjs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { mkdirSync, writeFileSync } from "node:fs";
@@ -1,3 +1,3 @@
1
- import { $t as SlugExt, An as ControllerConfigurableOptions, B as defineResource, Jt as BaseController, Mn as ListResult, Nn as BodySanitizer, Pn as BodySanitizerConfig, Qt as TreeMixin, V as ResourceDefinition, Xt as SoftDeleteMixin, Yt as SoftDeleteExt, Zt as TreeExt, an as QueryResolverConfig, ct as AggMeasureShorthand, dt as AggregationDateRangeRequirement, en as SlugMixin, ft as AggregationIndexHint, gt as AggregationsMap, ht as AggregationRateLimit, in as QueryResolver, jn as ControllerConstructionOptions, kn as BaseControllerOptions, lt as AggregationCacheConfig, mt as AggregationMaterializedResult, nn as BulkMixin, on as AccessControl, pt as AggregationMaterializedContext, rn as BaseCrudController, sn as AccessControlConfig, st as AggMeasureInput, tn as BulkExt, ut as AggregationConfig } from "../index-DPRH0Cme.mjs";
2
- import { A as DEFAULT_ID_FIELD, B as MAX_FILTER_DEPTH, C as DefineActionConfig, D as defineAggregation, E as createPermissionMiddleware, F as DEFAULT_UPDATE_METHOD, G as RESERVED_QUERY_PARAMS, H as MAX_SEARCH_LENGTH, I as HOOK_OPERATIONS, K as SYSTEM_FIELDS, L as HOOK_PHASES, M as DEFAULT_MAX_LIMIT, N as DEFAULT_SORT, O as CRUD_OPERATIONS, P as DEFAULT_TENANT_FIELD, R as HookOperation, S as mergeResourceConfig, T as createCrudRouter, U as MUTATION_OPERATIONS, V as MAX_REGEX_LENGTH, W as MutationOperation, _ as getEntityQuery, a as formatValidationErrors, b as ResourceSeams, c as isFieldReadable, d as createRequestContext, f as getControllerContext, g as getEntityIdField, h as getEntityId, i as assertValidConfig, j as DEFAULT_LIMIT, k as CrudOperation, l as createCrudHandlers, m as sendControllerResponse, n as ValidateOptions, o as validateResourceConfig, p as getControllerScope, r as ValidationResult, s as collectReadBlockedFields, t as ConfigError, u as createFastifyHandler, v as defineResourceVariants, w as defineAction, x as SeamedResourceConfig, y as AdapterLike, z as HookPhase } from "../index-f4PTrk3B.mjs";
1
+ import { $t as SlugExt, An as ControllerConfigurableOptions, B as defineResource, Jt as BaseController, Mn as ListResult, Nn as BodySanitizer, Pn as BodySanitizerConfig, Qt as TreeMixin, V as ResourceDefinition, Xt as SoftDeleteMixin, Yt as SoftDeleteExt, Zt as TreeExt, an as QueryResolverConfig, ct as AggMeasureShorthand, dt as AggregationDateRangeRequirement, en as SlugMixin, ft as AggregationIndexHint, gt as AggregationsMap, ht as AggregationRateLimit, in as QueryResolver, jn as ControllerConstructionOptions, kn as BaseControllerOptions, lt as AggregationCacheConfig, mt as AggregationMaterializedResult, nn as BulkMixin, on as AccessControl, pt as AggregationMaterializedContext, rn as BaseCrudController, sn as AccessControlConfig, st as AggMeasureInput, tn as BulkExt, ut as AggregationConfig } from "../index-Q5gzEIWY.mjs";
2
+ import { A as DEFAULT_ID_FIELD, B as MAX_FILTER_DEPTH, C as DefineActionConfig, D as defineAggregation, E as createPermissionMiddleware, F as DEFAULT_UPDATE_METHOD, G as RESERVED_QUERY_PARAMS, H as MAX_SEARCH_LENGTH, I as HOOK_OPERATIONS, K as SYSTEM_FIELDS, L as HOOK_PHASES, M as DEFAULT_MAX_LIMIT, N as DEFAULT_SORT, O as CRUD_OPERATIONS, P as DEFAULT_TENANT_FIELD, R as HookOperation, S as mergeResourceConfig, T as createCrudRouter, U as MUTATION_OPERATIONS, V as MAX_REGEX_LENGTH, W as MutationOperation, _ as getEntityQuery, a as formatValidationErrors, b as ResourceSeams, c as isFieldReadable, d as createRequestContext, f as getControllerContext, g as getEntityIdField, h as getEntityId, i as assertValidConfig, j as DEFAULT_LIMIT, k as CrudOperation, l as createCrudHandlers, m as sendControllerResponse, n as ValidateOptions, o as validateResourceConfig, p as getControllerScope, r as ValidationResult, s as collectReadBlockedFields, t as ConfigError, u as createFastifyHandler, v as defineResourceVariants, w as defineAction, x as SeamedResourceConfig, y as AdapterLike, z as HookPhase } from "../index-Cx7bro4R.mjs";
3
3
  export { AccessControl, type AccessControlConfig, type AdapterLike, type AggMeasureInput, type AggMeasureShorthand, type AggregationCacheConfig, type AggregationConfig, type AggregationDateRangeRequirement, type AggregationIndexHint, type AggregationMaterializedContext, type AggregationMaterializedResult, type AggregationRateLimit, type AggregationsMap, BaseController, type BaseControllerOptions, BaseCrudController, BodySanitizer, type BodySanitizerConfig, type BulkExt, BulkMixin, CRUD_OPERATIONS, type ConfigError, type ControllerConfigurableOptions, type ControllerConstructionOptions, CrudOperation, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, type DefineActionConfig, HOOK_OPERATIONS, HOOK_PHASES, HookOperation, HookPhase, type ListResult, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, type QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, type ResourceSeams, SYSTEM_FIELDS, type SeamedResourceConfig, type SlugExt, SlugMixin, type SoftDeleteExt, SoftDeleteMixin, type TreeExt, TreeMixin, type ValidateOptions, type ValidationResult, assertValidConfig, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAction, defineAggregation, defineResource, defineResourceVariants, formatValidationErrors, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, mergeResourceConfig, sendControllerResponse, validateResourceConfig };
@@ -1,5 +1,7 @@
1
1
  import { a as DEFAULT_SORT, c as HOOK_OPERATIONS, d as MAX_REGEX_LENGTH, f as MAX_SEARCH_LENGTH, h as SYSTEM_FIELDS, i as DEFAULT_MAX_LIMIT, l as HOOK_PHASES, m as RESERVED_QUERY_PARAMS, n as DEFAULT_ID_FIELD, o as DEFAULT_TENANT_FIELD, p as MUTATION_OPERATIONS, r as DEFAULT_LIMIT, s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS, u as MAX_FILTER_DEPTH } from "../constants-DNre0wRg.mjs";
2
- import { a as BulkMixin, c as collectReadBlockedFields, f as AccessControl, i as SlugMixin, l as isFieldReadable, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, s as QueryResolver, t as BaseController, u as BodySanitizer } from "../BaseController--M8EMzt0.mjs";
3
- import { a as defineResource, c as validateResourceConfig, d as defineAction, f as createCrudRouter, i as defineResourceVariants, l as ResourceDefinition, m as defineAggregation, n as getEntityIdField, o as assertValidConfig, p as createPermissionMiddleware, r as getEntityQuery, s as formatValidationErrors, t as getEntityId, u as mergeResourceConfig } from "../core-EUNu0u07.mjs";
4
- import { a as createFastifyHandler, c as getControllerScope, i as createCrudHandlers, l as sendControllerResponse, o as createRequestContext, s as getControllerContext } from "../pipeline-BN5G05rd.mjs";
2
+ import { a as BulkMixin, c as collectReadBlockedFields, f as AccessControl, i as SlugMixin, l as isFieldReadable, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, s as QueryResolver, t as BaseController, u as BodySanitizer } from "../BaseController-B5DwufbB.mjs";
3
+ import { a as ResourceDefinition, i as validateResourceConfig, n as assertValidConfig, o as createCrudRouter, r as formatValidationErrors, s as createPermissionMiddleware, t as defineResource } from "../defineResource-CxWqDCAy.mjs";
4
+ import { a as createFastifyHandler, c as getControllerScope, i as createCrudHandlers, l as sendControllerResponse, o as createRequestContext, s as getControllerContext } from "../pipeline-BJnpkY9i.mjs";
5
+ import { t as defineAggregation } from "../defineAggregation-ufWjUwml.mjs";
6
+ import { a as mergeResourceConfig, i as defineResourceVariants, n as getEntityIdField, o as defineAction, r as getEntityQuery, t as getEntityId } from "../core-poalJLvp.mjs";
5
7
  export { AccessControl, BaseController, BaseCrudController, BodySanitizer, BulkMixin, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, QueryResolver, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugMixin, SoftDeleteMixin, TreeMixin, assertValidConfig, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAction, defineAggregation, defineResource, defineResourceVariants, formatValidationErrors, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, mergeResourceConfig, sendControllerResponse, validateResourceConfig };
@@ -0,0 +1,175 @@
1
+ import "./constants-DNre0wRg.mjs";
2
+ import { t as defineResource } from "./defineResource-CxWqDCAy.mjs";
3
+ //#region src/core/defineAction.ts
4
+ /**
5
+ * Build an `ActionDefinition` with a typed handler. The literal schema
6
+ * type captured here flows into `data`, so `defineAction({ schema:
7
+ * z.object({...}), handler })` produces fully-typed code with no
8
+ * `as MyShape` cast.
9
+ *
10
+ * Behaviorally identical to a bare `ActionDefinition` object — same
11
+ * validation path (AJV), same permission resolution, same MCP wiring.
12
+ * The runtime shape is unchanged; only the type-level inference is new.
13
+ */
14
+ function defineAction(config) {
15
+ return {
16
+ handler: config.handler,
17
+ permissions: config.permissions,
18
+ schema: config.schema,
19
+ description: config.description,
20
+ id: config.id,
21
+ mcp: config.mcp
22
+ };
23
+ }
24
+ //#endregion
25
+ //#region src/core/defineResource/seams.ts
26
+ /** Plain object test — literals only; class instances must replace, not merge. */
27
+ function isPlainObject(value) {
28
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
29
+ const proto = Object.getPrototypeOf(value);
30
+ return proto === Object.prototype || proto === null;
31
+ }
32
+ /** Recursive plain-object merge; nested arrays REPLACE (they're value lists). */
33
+ function mergePlain(base, next) {
34
+ const out = { ...base };
35
+ for (const [key, value] of Object.entries(next)) {
36
+ if (value === void 0) continue;
37
+ const existing = out[key];
38
+ out[key] = isPlainObject(existing) && isPlainObject(value) ? mergePlain(existing, value) : value;
39
+ }
40
+ return out;
41
+ }
42
+ /**
43
+ * Canonical slot-aware merge of a base resource config with host seams.
44
+ * Later seams win over earlier ones; `undefined` values never clobber.
45
+ *
46
+ * Merge semantics (value-driven — no per-slot registry to drift):
47
+ *
48
+ * - **Top-level arrays CONCAT** — `routes`, `middlewares`, `presets`,
49
+ * `routeGuards`, `disabledRoutes`: additive collections, base first.
50
+ * - **Plain objects merge recursively** — `permissions`, `actions`,
51
+ * `schemaOptions` (incl. `fieldRules`), `aggregations`, `customSchemas`,
52
+ * `fields`, `cache`, `events`: per-key composition. Arrays NESTED inside
53
+ * them replace (they're value lists — enums, sort specs), not concat.
54
+ * - **Everything else last-wins** — class instances (`adapter`,
55
+ * `controller`, `queryParser`), functions, scalars.
56
+ *
57
+ * The single `as` at the return is the one documented erasure for the
58
+ * {@link AdapterLike}-widened seam slots — inside the helper so no module
59
+ * author ever writes one (mirrors mongokit's one-cast-at-the-boundary rule).
60
+ */
61
+ function mergeResourceConfig(base, ...seams) {
62
+ const out = { ...base };
63
+ for (const seam of seams) {
64
+ if (!seam) continue;
65
+ for (const [key, value] of Object.entries(seam)) {
66
+ if (value === void 0) continue;
67
+ const existing = out[key];
68
+ if (Array.isArray(existing) && Array.isArray(value)) out[key] = [...existing, ...value];
69
+ else if (isPlainObject(existing) && isPlainObject(value)) out[key] = mergePlain(existing, value);
70
+ else if (Array.isArray(value)) out[key] = [...value];
71
+ else out[key] = value;
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+ //#endregion
77
+ //#region src/core/defineResourceVariants.ts
78
+ /**
79
+ * Define multiple resources from a shared base config and per-variant overrides.
80
+ *
81
+ * Each variant is independently passed through `defineResource()` — the
82
+ * returned `ResourceDefinition`s are real, fully-registered resources.
83
+ * Register each one's plugin in your app:
84
+ *
85
+ * ```typescript
86
+ * await app.register(articlePublic.toPlugin());
87
+ * await app.register(articleAdmin.toPlugin());
88
+ * ```
89
+ *
90
+ * @param base Shared config — adapter, queryParser, schemaOptions, hooks, etc.
91
+ * Must NOT include `name` or `prefix` (those are per-variant).
92
+ * @param variants Map of variant key → override. Each variant must declare
93
+ * its own `name` and `prefix`. Other fields override the base.
94
+ * @returns A record where each key from `variants` maps to a real
95
+ * `ResourceDefinition` ready for `.toPlugin()` registration.
96
+ */
97
+ function defineResourceVariants(base, variants) {
98
+ const out = {};
99
+ for (const key of Object.keys(variants)) {
100
+ const override = variants[key];
101
+ out[key] = defineResource({
102
+ ...base,
103
+ ...override
104
+ });
105
+ }
106
+ return out;
107
+ }
108
+ //#endregion
109
+ //#region src/core/entityHelpers.ts
110
+ /**
111
+ * Per-request entity helpers — read the resource binding (idField + the
112
+ * URL `:id` value) off `req.arc`.
113
+ *
114
+ * Action handlers receive their `id` argument as the raw URL `:id` value.
115
+ * When the resource declares a custom `idField` (`slug`, `reportId`, …)
116
+ * that's NOT the document `_id`, a naive `Model.findById(id)` silently
117
+ * returns null. The historical footgun was a `findById(id)` typo where
118
+ * the handler author hadn't realised that `:id` resolves to the friendly
119
+ * handle.
120
+ *
121
+ * `getEntityQuery(req)` produces the canonical filter shape so handlers
122
+ * compose lookups with no resource-config recall:
123
+ *
124
+ * ```ts
125
+ * actions: {
126
+ * archive: {
127
+ * handler: async (id, data, req) => {
128
+ * const doc = await Model.findOne(getEntityQuery(req));
129
+ * if (!doc) throw new NotFoundError("Order");
130
+ * // ...
131
+ * },
132
+ * },
133
+ * }
134
+ * ```
135
+ *
136
+ * The router populates `req.arc.idField` and `req.arc.entityId` before
137
+ * invoking the handler — these helpers are zero-cost reads.
138
+ */
139
+ /**
140
+ * Read the resource's configured `idField` for the current request.
141
+ * Falls back to the framework default (`_id`) when the route hasn't
142
+ * bound an idField — keeps handlers safe to author without checking
143
+ * the resource config.
144
+ */
145
+ function getEntityIdField(req) {
146
+ return req.arc?.idField ?? "_id";
147
+ }
148
+ /**
149
+ * Read the URL `:id` path param value as the resource handle.
150
+ * Returns `undefined` when the route has no `:id` segment (collection
151
+ * routes) — handlers that need the entity must be on row routes.
152
+ */
153
+ function getEntityId(req) {
154
+ if (req.arc?.entityId !== void 0) return req.arc.entityId;
155
+ return req.params?.id;
156
+ }
157
+ /**
158
+ * Compose a `findOne` filter that resolves the current request's
159
+ * entity, regardless of whether the resource binds `_id` or a custom
160
+ * field. Idiomatic shape for arc action handlers.
161
+ *
162
+ * ```ts
163
+ * const doc = await Model.findOne(getEntityQuery(req));
164
+ * ```
165
+ *
166
+ * Returns `{}` when the route has no entity context (collection routes
167
+ * or tests bypassing the router) — caller decides what that means.
168
+ */
169
+ function getEntityQuery(req) {
170
+ const id = getEntityId(req);
171
+ if (id === void 0) return {};
172
+ return { [getEntityIdField(req)]: id };
173
+ }
174
+ //#endregion
175
+ export { mergeResourceConfig as a, defineResourceVariants as i, getEntityIdField as n, defineAction as o, getEntityQuery as r, getEntityId as t };
@@ -2,8 +2,8 @@ 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
4
  import { a as buildRouteHooks, i as selectPluginMw, l as buildAuthMiddlewareForPermissions, n as buildRouteConfig, r as resolveRouterPluginMw, s as routeHookOptions, t as buildRateLimitConfig, u as buildArcDecorator } from "./rateLimit-DrED-C4v.mjs";
5
- import { n as buildActionPermissionMw, t as tryRegisterRoute } from "./registerRoute-v0Wb6Plt.mjs";
6
- import { l as sendControllerResponse, r as resolvePipelineSteps, t as buildActionPipelineHandler } from "./pipeline-BN5G05rd.mjs";
5
+ import { n as buildActionPermissionMw, t as tryRegisterRoute } from "./registerRoute-JkxIEBJq.mjs";
6
+ import { l as sendControllerResponse, r as resolvePipelineSteps, t as buildActionPipelineHandler } from "./pipeline-BJnpkY9i.mjs";
7
7
  import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-Ca7ySNPx.mjs";
8
8
  //#region src/core/createActionRouter.ts
9
9
  var createActionRouter_exports = /* @__PURE__ */ __exportAll({
@@ -1,7 +1,9 @@
1
- import { f as createError, l as UnauthorizedError, r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
1
+ import { f as createError } from "./errors-DkO8Nk6-.mjs";
2
+ import { u as getRequestScope } from "./types-CSyCxl2V.mjs";
3
+ import { r as evaluateAndApplyPermission } from "./applyPermissionResult-Cf6zUCvb.mjs";
2
4
  import { a as buildRouteHooks, c as buildAuthMiddleware, i as selectPluginMw, r as resolveRouterPluginMw, s as routeHookOptions, t as buildRateLimitConfig, u as buildArcDecorator } from "./rateLimit-DrED-C4v.mjs";
3
5
  import { n as validateAggregations } from "./validate-Bk44F2yT.mjs";
4
- import { t as buildAggregationHandler } from "./buildHandler-Dtwd8fdz.mjs";
6
+ import { t as buildAggregationHandler } from "./buildHandler-BYS3Vkfz.mjs";
5
7
  //#region src/core/aggregation/createAggregationRouter.ts
6
8
  /**
7
9
  * Register one Fastify route per aggregation. No-op when the map is
@@ -38,15 +40,9 @@ function registerOne(fastify, normalized, ctx) {
38
40
  const config = normalized.base;
39
41
  const authMw = buildAuthMiddleware(fastify, config.permissions);
40
42
  const permissionFn = config.permissions;
41
- const permissionMw = async (req, _reply) => {
43
+ const permissionMw = async (req, reply) => {
42
44
  const ctx = buildPermissionContextLite(req, normalized.name);
43
- const raw = await permissionFn(ctx);
44
- if (!normalizePermissionGranted(raw)) {
45
- const status = req.user ? 403 : 401;
46
- const reason = normalizePermissionReason(raw);
47
- if (status === 401) throw new UnauthorizedError(reason ?? "Authentication required to access this aggregation.");
48
- throw new ForbiddenError(reason ?? "You do not have permission to access this aggregation.");
49
- }
45
+ await evaluateAndApplyPermission(permissionFn, ctx, req, reply, { defaultDenialMessage: (user) => user ? "You do not have permission to access this aggregation." : "Authentication required to access this aggregation." });
50
46
  };
51
47
  const hooks = buildRouteHooks({
52
48
  arcDecorator,
@@ -99,20 +95,11 @@ function buildPermissionContextLite(req, aggregationName) {
99
95
  const reqWithExtras = req;
100
96
  return {
101
97
  user: reqWithExtras.user ?? null,
98
+ scope: getRequestScope(req),
102
99
  request: req,
103
100
  resource: reqWithExtras.arc?.resource ?? "aggregation",
104
101
  action: `aggregation:${aggregationName}`
105
102
  };
106
103
  }
107
- /** PermissionCheck returns `boolean | PermissionResult`. Pull `granted`. */
108
- function normalizePermissionGranted(raw) {
109
- if (typeof raw === "boolean") return raw;
110
- return raw.granted;
111
- }
112
- /** Pull `reason` when the check returned a structured `PermissionResult`. */
113
- function normalizePermissionReason(raw) {
114
- if (typeof raw === "boolean") return void 0;
115
- return raw.reason;
116
- }
117
104
  //#endregion
118
105
  export { createAggregationRouter };
@@ -4,6 +4,7 @@ import { n as PUBLIC_SCOPE } from "./types-CSyCxl2V.mjs";
4
4
  import { t as createTenantKeyGenerator } from "./rateLimitKey-BoWRH3e7.mjs";
5
5
  import { i as createRequestIdGenerator, r as mergeHealthChecks } from "./health-CB_m4S32.mjs";
6
6
  import { t as parseJsonBody } from "./jsonBody-BlzAoGCe.mjs";
7
+ import { r as wrapWithBoundary } from "./subscribe-helpers-DfzsivM6.mjs";
7
8
  import Fastify from "fastify";
8
9
  import qs from "qs";
9
10
  //#region src/factory/module/resolve.ts
@@ -100,8 +101,9 @@ async function subscribeModuleEventHandlers(fastify, modules) {
100
101
  if (prior !== void 0) throw new Error(`[arc] duplicate event-handler name "${def.name}" — declared by module "${prior}" and module "${m.name}". Named event handlers must be unique across the module graph; prefix names with the owning module.`);
101
102
  owner.set(def.name, m.name);
102
103
  }
104
+ const handler = applyBoundary(def, m.name, fastify);
103
105
  const patterns = Array.isArray(def.event) ? def.event : [def.event];
104
- for (const pattern of patterns) unsubscribes.push(await bus.subscribe(pattern, def.handler));
106
+ for (const pattern of patterns) unsubscribes.push(await bus.subscribe(pattern, handler));
105
107
  }
106
108
  }
107
109
  return unsubscribes;
@@ -111,6 +113,25 @@ async function subscribeModuleEventHandlers(fastify, modules) {
111
113
  throw err;
112
114
  }
113
115
  }
116
+ /**
117
+ * Apply the declaration's opt-in error boundary. Without `boundary`, the raw
118
+ * handler is subscribed and a throw reaches the transport — which is what makes
119
+ * the durable path work (unacked → redelivered → DLQ). With it, failures are
120
+ * logged through `fastify.log` and swallowed, for handlers whose retry would
121
+ * only delay the next event's resync.
122
+ *
123
+ * The boundary label falls back to `<module>.<pattern>` so an unnamed handler
124
+ * still produces an attributable log line instead of "anonymous".
125
+ */
126
+ function applyBoundary(def, moduleName, fastify) {
127
+ if (!def.boundary) return def.handler;
128
+ const onError = typeof def.boundary === "object" ? def.boundary.onError : void 0;
129
+ return wrapWithBoundary(def.handler, {
130
+ logger: fastify.log,
131
+ name: def.name ?? `${moduleName}.${String(def.event)}`,
132
+ ...onError ? { onError } : {}
133
+ });
134
+ }
114
135
  /** Best-effort reverse-order teardown; every unsubscribe is attempted. */
115
136
  async function unsubscribeModuleEventHandlers(unsubscribes) {
116
137
  const errors = [];
@@ -214,34 +235,6 @@ function describeModuleCycle(remaining, byName) {
214
235
  return `[arc] module dependency cycle: ${cycle ? cycle.join(" → ") : [...remaining].join(", ")}. Modules cannot dependsOn each other circularly — break it with a shared module both point at, or wire the softer direction through an event/port instead of a hard dependsOn.`;
215
236
  }
216
237
  //#endregion
217
- //#region src/factory/module/index.ts
218
- /**
219
- * Identity helper for authoring a typed module — mirrors `defineResource`.
220
- * Gives inference + a single obvious construction site; does no work.
221
- *
222
- * `TExports` is inferred from the `bootstrap` return value, so a module
223
- * author gets a typed public export for free:
224
- *
225
- * ```ts
226
- * export const accountingModule = (deps: Deps) =>
227
- * defineModule({
228
- * name: "accounting",
229
- * bootstrap: async () => createAccountingEngine(deps), // TExports inferred
230
- * });
231
- * ```
232
- */
233
- function defineModule(module) {
234
- return module;
235
- }
236
- function getModuleExports(fastify, name) {
237
- const modules = fastify.arc?.modules;
238
- if (!modules || !Object.hasOwn(modules, name)) {
239
- const available = modules ? Object.keys(modules) : [];
240
- throw new Error(`[arc] no public export recorded for module "${name}".\nA module's export is its bootstrap() return value; it is readable only AFTER that module's bootstrap ran (dependsOn / list order = init order).\nModules with recorded exports: ${available.length ? available.join(", ") : "(none)"}.`);
241
- }
242
- return modules[name];
243
- }
244
- //#endregion
245
238
  //#region src/factory/presets.ts
246
239
  /**
247
240
  * Production preset - strict security, performance optimized
@@ -467,7 +460,7 @@ async function registerArcCore(fastify, config, trackPlugin) {
467
460
  await fastify.register(arcCorePlugin, { emitEvents: config.arcPlugins?.emitEvents !== false });
468
461
  trackPlugin("arc-core");
469
462
  if (config.arcPlugins?.events !== false) {
470
- const { default: eventPlugin } = await import("./eventPlugin-DrQ_FesK.mjs").then((n) => n.n);
463
+ const { default: eventPlugin } = await import("./eventPlugin-D2NNR4py.mjs").then((n) => n.n);
471
464
  const eventOpts = typeof config.arcPlugins?.events === "object" ? config.arcPlugins.events : {};
472
465
  await fastify.register(eventPlugin, {
473
466
  ...eventOpts,
@@ -539,7 +532,7 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules, moduleH
539
532
  }
540
533
  if (config.arcPlugins?.realtime) if (config.arcPlugins?.events === false) fastify.log.warn("Realtime plugin requires events plugin (arcPlugins.events). Realtime disabled.");
541
534
  else {
542
- const { default: realtimePlugin } = await import("./realtime-Cuw_deA-.mjs").then((n) => n.r);
535
+ const { default: realtimePlugin } = await import("./realtime-B9nmIY5V.mjs").then((n) => n.r);
543
536
  const opts = config.arcPlugins.realtime === true ? {} : config.arcPlugins.realtime;
544
537
  await fastify.register(realtimePlugin, opts);
545
538
  trackPlugin("arc-realtime", opts);
@@ -1466,48 +1459,4 @@ const ArcFactory = {
1466
1459
  }
1467
1460
  };
1468
1461
  //#endregion
1469
- //#region src/factory/preloadResources.ts
1470
- /**
1471
- * Normalize an eager `import.meta.glob` result into a `ResourceLike[]`.
1472
- *
1473
- * Accepts either:
1474
- * - `{ import: 'default' }` form: values are the resource directly
1475
- * - default form: values are the full module — picks first export with `toPlugin()`
1476
- *
1477
- * Throws if any module doesn't yield a valid `ResourceLike`.
1478
- */
1479
- function preloadResources(globResult) {
1480
- const resources = [];
1481
- for (const [path, value] of Object.entries(globResult)) {
1482
- const resource = pickResource(value);
1483
- if (!resource) throw new Error(`preloadResources: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
1484
- resources.push(resource);
1485
- }
1486
- return resources.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
1487
- }
1488
- /**
1489
- * Normalize a lazy `import.meta.glob` result into a `Promise<ResourceLike[]>`.
1490
- *
1491
- * Use this when resources depend on prior bootstrap (e.g., engine init) and
1492
- * cannot be evaluated at import time of the preload file.
1493
- */
1494
- async function preloadResourcesAsync(globResult) {
1495
- return (await Promise.all(Object.entries(globResult).map(async ([path, loader]) => {
1496
- const resource = pickResource(await loader());
1497
- if (!resource) throw new Error(`preloadResourcesAsync: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
1498
- return resource;
1499
- }))).sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
1500
- }
1501
- function pickResource(value) {
1502
- if (!value || typeof value !== "object") return void 0;
1503
- if (typeof value.toPlugin === "function") return value;
1504
- const mod = value;
1505
- const candidates = [
1506
- mod.default,
1507
- mod.resource,
1508
- ...Object.values(mod)
1509
- ];
1510
- for (const c of candidates) if (c && typeof c === "object" && typeof c.toPlugin === "function") return c;
1511
- }
1512
- //#endregion
1513
- export { collectModuleScheduledJobs as _, createApp_exports as a, resolveModule as b, getPreset as c, workerPreset as d, defineModule as f, collectModuleHealthChecks as g, subscribeModuleEventHandlers as h, createApp as i, productionPreset as l, orderModules as m, preloadResourcesAsync as n, developmentPreset as o, getModuleExports as p, ArcFactory as r, edgePreset as s, preloadResources as t, testingPreset as u, collectModuleWorkflows as v, resolveContribution as y };
1462
+ export { edgePreset as a, testingPreset as c, subscribeModuleEventHandlers as d, collectModuleHealthChecks as f, resolveModule as g, resolveContribution as h, developmentPreset as i, workerPreset as l, collectModuleWorkflows as m, createApp as n, getPreset as o, collectModuleScheduledJobs as p, createApp_exports as r, productionPreset as s, ArcFactory as t, orderModules as u };
@@ -0,0 +1,47 @@
1
+ //#region src/core/aggregation/defineAggregation.ts
2
+ /**
3
+ * Declare a single resource aggregation. Exported configs flow into
4
+ * `defineResource({ aggregations: { ... } })` either as named keys or
5
+ * via auto-discovery patterns (loadAggregations — future).
6
+ *
7
+ * @example Inline
8
+ * ```ts
9
+ * defineResource({
10
+ * name: 'order',
11
+ * aggregations: {
12
+ * revenueByStatus: defineAggregation({
13
+ * groupBy: 'status',
14
+ * measures: { count: 'count', revenue: 'sum:totalPrice' },
15
+ * permissions: requireRoles(['admin']),
16
+ * }),
17
+ * },
18
+ * });
19
+ * ```
20
+ *
21
+ * @example Multi-file
22
+ * ```ts
23
+ * // orders/aggregations/revenue-by-status.ts
24
+ * import { defineAggregation } from '@classytic/arc';
25
+ *
26
+ * export const revenueByStatus = defineAggregation({
27
+ * groupBy: 'status',
28
+ * measures: { count: 'count', revenue: 'sum:totalPrice' },
29
+ * permissions: requireRoles(['admin']),
30
+ * timeout: 5000,
31
+ * cache: { staleTime: 60 },
32
+ * });
33
+ *
34
+ * // orders/order.resource.ts
35
+ * import * as aggregations from './aggregations/index.js';
36
+ *
37
+ * defineResource({
38
+ * name: 'order',
39
+ * aggregations, // 30+ entries flow in
40
+ * });
41
+ * ```
42
+ */
43
+ function defineAggregation(config) {
44
+ return config;
45
+ }
46
+ //#endregion
47
+ export { defineAggregation as t };