@classytic/arc 2.15.4 → 2.16.1

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 (160) hide show
  1. package/README.md +1 -0
  2. package/bin/arc.js +12 -0
  3. package/dist/{BaseController-dx3m2J8V.mjs → BaseController-Vk8I7C0x.mjs} +61 -19
  4. package/dist/{HookSystem-Iiebom92.mjs → HookSystem-Cmf7-Etp.mjs} +8 -4
  5. package/dist/{QueryCache-D41bfdBB.d.mts → QueryCache-8YmAQ-zW.d.mts} +1 -1
  6. package/dist/{ResourceRegistry-CTERg_2x.mjs → ResourceRegistry-CzSzVK1m.mjs} +55 -9
  7. package/dist/audit/index.d.mts +1 -1
  8. package/dist/audit/index.mjs +4 -2
  9. package/dist/auth/index.d.mts +4 -4
  10. package/dist/auth/index.mjs +4 -4
  11. package/dist/auth/redis-session.d.mts +1 -1
  12. package/dist/{betterAuthOpenApi--M_i87dQ.mjs → betterAuthOpenApi-ClWxaceA.mjs} +10 -6
  13. package/dist/buildHandler-C_SkBNF3.mjs +302 -0
  14. package/dist/cache/index.d.mts +3 -3
  15. package/dist/cache/index.mjs +3 -3
  16. package/dist/{caching-SM8gghN6.mjs → caching-8MfrrUYU.mjs} +1 -1
  17. package/dist/cli/commands/describe.d.mts +35 -1
  18. package/dist/cli/commands/describe.mjs +52 -12
  19. package/dist/cli/commands/docs.d.mts +1 -4
  20. package/dist/cli/commands/docs.mjs +4 -16
  21. package/dist/cli/commands/generate.d.mts +2 -20
  22. package/dist/cli/commands/generate.mjs +1 -546
  23. package/dist/cli/commands/init.d.mts +2 -40
  24. package/dist/cli/commands/init.mjs +1 -3045
  25. package/dist/cli/commands/introspect.mjs +53 -64
  26. package/dist/cli/index.d.mts +2 -2
  27. package/dist/cli/index.mjs +2 -2
  28. package/dist/{constants-Cxde4rpC.mjs → constants-TrJVIJl0.mjs} +7 -0
  29. package/dist/core/index.d.mts +3 -3
  30. package/dist/core/index.mjs +5 -5
  31. package/dist/{core-CvmOqEms.mjs → core-wKeGXn38.mjs} +222 -44
  32. package/dist/createActionRouter-eiOFtnC3.mjs +288 -0
  33. package/dist/{createAggregationRouter-B0bPDf5b.mjs → createAggregationRouter-C4unhBnV.mjs} +3 -2
  34. package/dist/{createApp-PFegs47-.mjs → createApp-DdPBtmzp.mjs} +16 -14
  35. package/dist/{defineEvent-D5h7EvAx.mjs → defineEvent-NPKUJRt8.mjs} +1 -1
  36. package/dist/docs/index.d.mts +2 -2
  37. package/dist/docs/index.mjs +1 -1
  38. package/dist/{errorHandler-Bk-AGhkU.mjs → errorHandler-DNPzqaTT.mjs} +17 -14
  39. package/dist/errors-4ZloYFvm.d.mts +91 -0
  40. package/dist/{eventPlugin-CaKTYkYM.mjs → eventPlugin-CnJCUyM0.mjs} +1 -1
  41. package/dist/{eventPlugin-qXpqTebY.d.mts → eventPlugin-DjEx_J3j.d.mts} +1 -1
  42. package/dist/events/index.d.mts +3 -3
  43. package/dist/events/index.mjs +5 -5
  44. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  45. package/dist/events/transports/redis.d.mts +1 -1
  46. package/dist/factory/index.d.mts +2 -2
  47. package/dist/factory/index.mjs +4 -3
  48. package/dist/fastify-augmentation-CJm0vh-l.d.mts +42 -0
  49. package/dist/{fields-COhcH3fk.d.mts → fields-DqZLp5d6.d.mts} +1 -1
  50. package/dist/generate-CcM24hoq.mjs +654 -0
  51. package/dist/generate-cRnsafMS.d.mts +38 -0
  52. package/dist/hooks/index.d.mts +1 -1
  53. package/dist/hooks/index.mjs +1 -1
  54. package/dist/idempotency/index.d.mts +2 -2
  55. package/dist/idempotency/index.mjs +1 -1
  56. package/dist/idempotency/redis.d.mts +1 -1
  57. package/dist/{index-BTqLEvhu.d.mts → index-3L-adFJO.d.mts} +12 -12
  58. package/dist/{index-BstGxcc3.d.mts → index-CC_pgZjY.d.mts} +88 -17
  59. package/dist/{index-BswOSJCE.d.mts → index-DBDgy6Hw.d.mts} +430 -19
  60. package/dist/index.d.mts +6 -6
  61. package/dist/index.mjs +7 -8
  62. package/dist/init-D-vrTZ1G.d.mts +71 -0
  63. package/dist/init-DYzfniuX.mjs +3098 -0
  64. package/dist/integrations/event-gateway.d.mts +2 -2
  65. package/dist/integrations/event-gateway.mjs +1 -1
  66. package/dist/integrations/index.d.mts +2 -2
  67. package/dist/integrations/jobs.mjs +3 -3
  68. package/dist/integrations/mcp/index.d.mts +239 -7
  69. package/dist/integrations/mcp/index.mjs +2 -528
  70. package/dist/integrations/mcp/testing.d.mts +2 -2
  71. package/dist/integrations/mcp/testing.mjs +6 -10
  72. package/dist/integrations/streamline.mjs +26 -1
  73. package/dist/integrations/websocket-redis.d.mts +1 -1
  74. package/dist/integrations/websocket.d.mts +1 -1
  75. package/dist/integrations/websocket.mjs +1 -0
  76. package/dist/loadResourcesFromEntry-DCjYMgfC.mjs +51 -0
  77. package/dist/{resourceToTools-tFYUNmM0.mjs → mcpPlugin-Dbgz9_Rj.mjs} +1021 -318
  78. package/dist/{memory-UBydS5ku.mjs → memory-QOLe11D5.mjs} +2 -0
  79. package/dist/middleware/index.d.mts +1 -1
  80. package/dist/middleware/index.mjs +1 -1
  81. package/dist/{openapi-BHXhoX8O.mjs → openapi-BRnd3pT7.mjs} +82 -44
  82. package/dist/permissions/index.d.mts +2 -2
  83. package/dist/permissions/index.mjs +1 -1
  84. package/dist/{permissions-ohQyv50e.mjs → permissions-CTxMrreC.mjs} +2 -2
  85. package/dist/{pipe-Zr0KXjQe.mjs → pipe-DiCyvyPN.mjs} +1 -0
  86. package/dist/pipeline/index.d.mts +1 -1
  87. package/dist/pipeline/index.mjs +1 -1
  88. package/dist/plugins/index.d.mts +5 -59
  89. package/dist/plugins/index.mjs +10 -10
  90. package/dist/plugins/response-cache.mjs +5 -5
  91. package/dist/plugins/tracing-entry.d.mts +1 -1
  92. package/dist/plugins/tracing-entry.mjs +1 -1
  93. package/dist/{pluralize-DQgqgifU.mjs → pluralize-B9M8xvy-.mjs} +2 -1
  94. package/dist/preloadResources-BOXzPhKU.mjs +45 -0
  95. package/dist/presets/filesUpload.d.mts +4 -4
  96. package/dist/presets/filesUpload.mjs +2 -2
  97. package/dist/presets/index.d.mts +1 -1
  98. package/dist/presets/index.mjs +1 -1
  99. package/dist/presets/multiTenant.d.mts +1 -1
  100. package/dist/presets/multiTenant.mjs +4 -3
  101. package/dist/presets/search.d.mts +2 -2
  102. package/dist/presets/search.mjs +1 -1
  103. package/dist/{presets-BbkjdPeH.mjs → presets-C9BE6WaZ.mjs} +2 -2
  104. package/dist/{queryCachePlugin-m1XsgAIJ.mjs → queryCachePlugin-B4XMSSe7.mjs} +2 -2
  105. package/dist/{queryCachePlugin-CqMdLI2-.d.mts → queryCachePlugin-Do4P_WBC.d.mts} +2 -2
  106. package/dist/{redis-DiMkdHEl.d.mts → redis-cpqVb85w.d.mts} +1 -1
  107. package/dist/{redis-stream-D6HzR1Z_.d.mts → redis-stream-BuiAS3aE.d.mts} +1 -1
  108. package/dist/registry/index.d.mts +341 -2
  109. package/dist/registry/index.mjs +3 -3
  110. package/dist/registry-BrlHkrRS.mjs +582 -0
  111. package/dist/{routerShared-DrOa-26E.mjs → routerShared-ClV4cCaa.mjs} +40 -16
  112. package/dist/scope/index.d.mts +3 -3
  113. package/dist/scope/index.mjs +3 -3
  114. package/dist/{sse-Bz-5ZeTt.mjs → sse-jh5OqFjR.mjs} +1 -1
  115. package/dist/testing/index.d.mts +2 -25
  116. package/dist/testing/index.mjs +17 -51
  117. package/dist/testing/storageContract.d.mts +1 -1
  118. package/dist/types/index.d.mts +5 -5
  119. package/dist/types/storage.d.mts +1 -1
  120. package/dist/{types-BQsjgQzS.d.mts → types-BD0BCdf6.d.mts} +106 -12
  121. package/dist/{types-C_s5moIu.mjs → types-Bi0r0vjG.mjs} +53 -1
  122. package/dist/{types-CTYvcwHe.d.mts → types-CfapW2t8.d.mts} +42 -1
  123. package/dist/{types-DrBaUwyV.d.mts → types-Dw--SyTq.d.mts} +57 -11
  124. package/dist/utils/index.d.mts +1287 -2
  125. package/dist/utils/index.mjs +1 -1
  126. package/dist/{utils-_h9B3c57.mjs → utils-DPXZnZUx.mjs} +158 -41
  127. package/dist/{buildHandler-CcFOpJLh.mjs → validate-DFIP2VrM.mjs} +73 -302
  128. package/dist/{versioning-hmkPcDlX.d.mts → versioning-5H4930tj.d.mts} +1 -1
  129. package/package.json +22 -29
  130. package/skills/arc/SKILL.md +300 -706
  131. package/skills/arc/references/auth.md +19 -7
  132. package/skills/arc-code-review/SKILL.md +1 -1
  133. package/skills/arc-code-review/references/arc-cheatsheet.md +100 -322
  134. package/dist/createActionRouter-S3MLVYot.mjs +0 -220
  135. package/dist/index-bRjYu21O.d.mts +0 -1320
  136. package/dist/org/index.d.mts +0 -66
  137. package/dist/org/index.mjs +0 -486
  138. package/dist/org/types.d.mts +0 -82
  139. package/dist/org/types.mjs +0 -1
  140. package/dist/registry-I-ogLgL9.mjs +0 -46
  141. /package/dist/{EventTransport-CT_52aWU.d.mts → EventTransport-B1GFiYUy.d.mts} +0 -0
  142. /package/dist/{EventTransport-DLWoUMHy.mjs → EventTransport-ClEDfiMS.mjs} +0 -0
  143. /package/dist/{actionPermissions-CyUkQu6O.mjs → actionPermissions-Bjmvn7Eb.mjs} +0 -0
  144. /package/dist/{elevation-DgoeTyfX.mjs → elevation-Dci0AYLT.mjs} +0 -0
  145. /package/dist/{elevation-BXOWoGCF.d.mts → elevation-DvwtjDwu.d.mts} +0 -0
  146. /package/dist/{errorHandler-DFr45ZG4.d.mts → errorHandler-DuqTqeqn.d.mts} +0 -0
  147. /package/dist/{externalPaths-BD5nw6St.d.mts → externalPaths-ClykOQqa.d.mts} +0 -0
  148. /package/dist/{interface-beEtJyWM.d.mts → interface-BVkZsvEe.d.mts} +0 -0
  149. /package/dist/{interface-DfLGcus7.d.mts → interface-DoE6UoC2.d.mts} +0 -0
  150. /package/dist/{keys-CGcCbNyu.mjs → keys-DopsCuyQ.mjs} +0 -0
  151. /package/dist/{loadResources-DBMQg_Aj.mjs → loadResources-fol6JuvW.mjs} +0 -0
  152. /package/dist/{metrics-Qnvwc-LQ.mjs → metrics-B-PU4-Yu.mjs} +0 -0
  153. /package/dist/{replyHelpers-CK-FNO8E.mjs → replyHelpers-boS5Rx26.mjs} +0 -0
  154. /package/dist/{schemaIR-lYhC2gE5.mjs → schemaIR-DAOvBWgT.mjs} +0 -0
  155. /package/dist/{sessionManager-C4Le_UB3.d.mts → sessionManager-DlmaHUAl.d.mts} +0 -0
  156. /package/dist/{storage-Dfzt4VTl.d.mts → storage-HcAgP5Ni.d.mts} +0 -0
  157. /package/dist/{store-helpers-BkIN9-vu.mjs → store-helpers-5JgaxieH.mjs} +0 -0
  158. /package/dist/{tracing-QJVprktp.d.mts → tracing-bRqqUxFO.d.mts} +0 -0
  159. /package/dist/{versioning-BUrT5aP4.mjs → versioning-CuIwUQZ2.mjs} +0 -0
  160. /package/dist/{websocket-ChC2rqe1.d.mts → websocket-BJ0w-3Dt.d.mts} +0 -0
@@ -1,9 +1,23 @@
1
+ import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
1
2
  //#region src/core/aggregation/validate.ts
3
+ var validate_exports = /* @__PURE__ */ __exportAll({
4
+ ArcAggregationConfigError: () => ArcAggregationConfigError,
5
+ adapterSupportsAggregate: () => adapterSupportsAggregate,
6
+ compileAggRequest: () => compileAggRequest,
7
+ validateAggregations: () => validateAggregations
8
+ });
2
9
  /** Thrown on aggregation misconfig at boot time. */
3
10
  var ArcAggregationConfigError = class extends Error {
4
11
  name = "ArcAggregationConfigError";
5
12
  };
6
13
  /**
14
+ * Framework cap on URL-driven aggregation limits when the host opts
15
+ * in to `defaultLimit` without explicit `maxLimit`. Set to the same
16
+ * order of magnitude as a "large dashboard page" — bigger than every
17
+ * realistic grouped tile, smaller than "we returned the whole table."
18
+ */
19
+ const AGG_FRAMEWORK_MAX_LIMIT = 1e3;
20
+ /**
7
21
  * Validate + normalize all aggregations on a resource. Throws on first
8
22
  * misconfig with the offending aggregation name in the message — hosts
9
23
  * see exactly which entry needs fixing.
@@ -46,6 +60,8 @@ function validateAggregations(resourceName, aggregations, schemaOptions) {
46
60
  bucketAliases
47
61
  });
48
62
  if (config.topN) validateTopNConfig(resourceName, name, config.topN, groupBy, measures, bucketAliases);
63
+ validateLimitConfig(resourceName, name, config);
64
+ const limitPolicy = resolveLimitPolicy(config);
49
65
  out.push({
50
66
  name,
51
67
  base: config,
@@ -58,13 +74,47 @@ function validateAggregations(resourceName, aggregations, schemaOptions) {
58
74
  having: config.having,
59
75
  sort: config.sort,
60
76
  limit: config.limit,
61
- topN: config.topN
77
+ topN: config.topN,
78
+ ...limitPolicy ? { limitPolicy } : {}
62
79
  }
63
80
  });
64
81
  }
65
82
  return out;
66
83
  }
67
84
  /**
85
+ * Validate `limit` / `defaultLimit` / `maxLimit` triad. Throws on
86
+ * misconfig at boot — boundary value, hosts see exactly what to fix.
87
+ */
88
+ function validateLimitConfig(resourceName, aggName, config) {
89
+ const hasStatic = config.limit !== void 0;
90
+ const hasDynamic = config.defaultLimit !== void 0;
91
+ if (hasStatic && hasDynamic) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggName}" sets both \`limit\` (static cap) and \`defaultLimit\` (URL-driven cap). Pick one — static \`limit\` ignores the URL; \`defaultLimit\` reads \`?limit=N\` and caps it at \`maxLimit\`.`);
92
+ if (config.maxLimit !== void 0 && !hasDynamic) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggName}" sets \`maxLimit\` without \`defaultLimit\`. \`maxLimit\` only applies to URL-driven limits — set \`defaultLimit\` or remove \`maxLimit\`.`);
93
+ if (hasStatic) {
94
+ if (!Number.isInteger(config.limit) || config.limit <= 0) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggName}" \`limit\` must be a positive integer — got ${String(config.limit)}.`);
95
+ }
96
+ if (hasDynamic) {
97
+ if (!Number.isInteger(config.defaultLimit) || config.defaultLimit <= 0) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggName}" \`defaultLimit\` must be a positive integer — got ${String(config.defaultLimit)}.`);
98
+ if (config.maxLimit !== void 0) {
99
+ if (!Number.isInteger(config.maxLimit) || config.maxLimit <= 0) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggName}" \`maxLimit\` must be a positive integer — got ${String(config.maxLimit)}.`);
100
+ if (config.maxLimit < config.defaultLimit) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggName}" \`maxLimit\` (${config.maxLimit}) is less than \`defaultLimit\` (${config.defaultLimit}). The ceiling must be ≥ the default.`);
101
+ }
102
+ }
103
+ }
104
+ /**
105
+ * Resolve the URL-driven limit policy into a concrete `{ default, max }`
106
+ * pair. Returns `undefined` when the host uses the static `limit` form
107
+ * (or no limit at all) — the request handler reads the absence to know
108
+ * "do not parse `?limit=N` from the URL."
109
+ */
110
+ function resolveLimitPolicy(config) {
111
+ if (config.defaultLimit === void 0) return void 0;
112
+ return {
113
+ defaultLimit: config.defaultLimit,
114
+ maxLimit: config.maxLimit ?? AGG_FRAMEWORK_MAX_LIMIT
115
+ };
116
+ }
117
+ /**
68
118
  * Adapter feature-detect for `aggregate()`. Called at boot when the
69
119
  * repository instance is available. Returns `true` when the kit ships
70
120
  * `aggregate`; `false` when missing.
@@ -76,14 +126,33 @@ function adapterSupportsAggregate(repo) {
76
126
  if (!repo || typeof repo !== "object") return false;
77
127
  return typeof repo.aggregate === "function";
78
128
  }
129
+ /**
130
+ * Resolve the effective per-request `limit` — three cases:
131
+ *
132
+ * 1. Host declared static `limit` → URL `?limit` ignored, static wins.
133
+ * 2. Host declared `defaultLimit` → parse `?limit=N`, cap at
134
+ * `maxLimit`, fall back to default on absent / invalid input.
135
+ * 3. Host declared neither → returns `undefined` (no limit applied,
136
+ * preserves pre-2.16.1 behavior).
137
+ */
138
+ function resolveAggRequestLimit(normalized, query) {
139
+ const policy = normalized.compiled.limitPolicy;
140
+ if (!policy) return normalized.compiled.limit;
141
+ const raw = query["limit"];
142
+ if (raw === void 0 || raw === "") return policy.defaultLimit;
143
+ const parsed = typeof raw === "number" ? raw : Number.parseInt(String(raw), 10);
144
+ if (!Number.isFinite(parsed) || parsed <= 0) return policy.defaultLimit;
145
+ return Math.min(Math.floor(parsed), policy.maxLimit);
146
+ }
79
147
  /** Compile to canonical `AggRequest` for `repo.aggregate()` at request time. */
80
- function compileAggRequest(normalized, callerFilter, tenantOptions) {
148
+ function compileAggRequest(normalized, callerFilter, tenantOptions, query) {
81
149
  const filter = {
82
150
  ...normalized.compiled.filter ?? {},
83
151
  ...callerFilter
84
152
  };
85
153
  const executionHints = buildExecutionHints(normalized.base);
86
154
  const cache = buildCacheOptions(normalized.base);
155
+ const limit = query ? resolveAggRequestLimit(normalized, query) : normalized.compiled.limit;
87
156
  return {
88
157
  measures: normalized.compiled.measures,
89
158
  ...Object.keys(filter).length > 0 ? { filter } : {},
@@ -92,7 +161,7 @@ function compileAggRequest(normalized, callerFilter, tenantOptions) {
92
161
  ...normalized.compiled.dateBuckets ? { dateBuckets: normalized.compiled.dateBuckets } : {},
93
162
  ...normalized.compiled.having ? { having: normalized.compiled.having } : {},
94
163
  ...normalized.compiled.sort ? { sort: normalized.compiled.sort } : {},
95
- ...normalized.compiled.limit !== void 0 ? { limit: normalized.compiled.limit } : {},
164
+ ...limit !== void 0 ? { limit } : {},
96
165
  ...normalized.compiled.topN ? { topN: normalized.compiled.topN } : {},
97
166
  ...executionHints ? { executionHints } : {},
98
167
  ...cache ? { cache } : {}
@@ -373,302 +442,4 @@ function assertFieldAllowed(context, ref, input) {
373
442
  if (blockedFields.has(ref)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" references field "${ref}" in ${context}, but the field is blocked from aggregation (\`hidden: true\` or \`aggregable: false\` in schemaOptions.fieldRules). Aggregating hidden fields would leak cardinality information.`);
374
443
  }
375
444
  //#endregion
376
- //#region src/core/aggregation/buildHandler.ts
377
- /**
378
- * Framework-agnostic aggregation execution. Runs safety guards,
379
- * compiles the AggRequest, dispatches to the materialized hook or
380
- * `repo.aggregate()`, and applies the post-execution `maxGroups` cap.
381
- *
382
- * Returns an envelope describing the response — Fastify wrappers
383
- * apply it to a reply, MCP wrappers convert it to a tool-call result.
384
- *
385
- * **Does NOT run the per-aggregation permission check.** Auth runs
386
- * upstream (Fastify preHandler chain or MCP `evaluatePermission`)
387
- * because the permission shape differs by surface (FastifyRequest vs
388
- * MCP session). Both surfaces fail-closed BEFORE reaching this
389
- * function; this is purely the runtime executor.
390
- */
391
- async function executeAggregation(normalized, deps, ctx) {
392
- const { repo } = deps;
393
- const config = normalized.base;
394
- const aggregationName = normalized.name;
395
- const { query, tenantOptions } = ctx;
396
- const guardError = checkRequestGuards(query, config);
397
- if (guardError) return {
398
- status: 400,
399
- body: guardError
400
- };
401
- const aggReq = compileAggRequest(normalized, extractCallerFilter(query), tenantOptions);
402
- if (config.materialized) {
403
- const matCtx = {
404
- filter: aggReq.filter,
405
- orgId: pickString(tenantOptions.organizationId),
406
- userId: pickString(tenantOptions.userId),
407
- requestId: pickString(tenantOptions.requestId),
408
- query
409
- };
410
- return {
411
- status: 200,
412
- headers: { "x-aggregation-source": "materialized" },
413
- body: { rows: (await config.materialized(matCtx)).rows }
414
- };
415
- }
416
- if (!adapterSupportsAggregate(repo)) return {
417
- status: 501,
418
- body: {
419
- code: "arc.adapter.capability_required",
420
- message: `Aggregation "${aggregationName}" is not supported: the resource's storage adapter does not implement repo.aggregate(). Use a kit that ships StandardRepo.aggregate (mongokit / sqlitekit), or remove the aggregations entry.`,
421
- status: 501,
422
- meta: {
423
- capability: "aggregate",
424
- aggregation: aggregationName
425
- }
426
- }
427
- };
428
- let result;
429
- try {
430
- result = await repo.aggregate(aggReq, tenantOptions);
431
- } catch (err) {
432
- return mapAggregateError(err, aggregationName);
433
- }
434
- if (config.maxGroups !== void 0 && result.rows.length > config.maxGroups) return {
435
- status: 422,
436
- body: {
437
- code: "arc.aggregation.max_groups_exceeded",
438
- message: `Aggregation "${aggregationName}" produced ${result.rows.length} groups, exceeding maxGroups (${config.maxGroups}). Narrow the filter or raise the cap.`,
439
- status: 422,
440
- meta: {
441
- aggregation: aggregationName,
442
- produced: result.rows.length,
443
- maxGroups: config.maxGroups
444
- }
445
- }
446
- };
447
- return {
448
- status: 200,
449
- body: { rows: result.rows }
450
- };
451
- }
452
- /**
453
- * Build the Fastify handler for a single aggregation.
454
- *
455
- * The returned function calls the repo (or materialized hook), shapes
456
- * the response envelope, and writes status/headers via Fastify's
457
- * `reply` API. Errors throw — the router's error handler converts to
458
- * the standard arc response shape.
459
- */
460
- /**
461
- * Build the Fastify handler for a single aggregation.
462
- *
463
- * Caching lives in the kit's repo-core `cachePlugin` — when the host
464
- * declares `cache:` on the aggregation, `compileAggRequest` translates
465
- * to `aggReq.cache: CacheOptions` and the kit handles SWR + tag
466
- * invalidation + version-bump on writes. Arc passes the request
467
- * through; no duplicate cache layer at the HTTP handler.
468
- */
469
- function buildAggregationHandler(normalized, deps) {
470
- const { buildOptions } = deps;
471
- return async (request, reply) => {
472
- const result = await executeAggregation(normalized, deps, {
473
- query: request.query ?? {},
474
- tenantOptions: buildOptions(request)
475
- });
476
- reply.status(result.status);
477
- if (result.headers) for (const [k, v] of Object.entries(result.headers)) reply.header(k, v);
478
- return result.body;
479
- };
480
- }
481
- function pickString(value) {
482
- return typeof value === "string" ? value : void 0;
483
- }
484
- function checkRequestGuards(query, config) {
485
- if (config.requireFilters) {
486
- for (const field of config.requireFilters) if (!hasFilterOnField(query, field)) return {
487
- code: "arc.aggregation.required_filter_missing",
488
- message: `Aggregation requires filter on "${field}" — supply ?${field}=... or ?${field}[op]=... in the query string.`,
489
- status: 400,
490
- meta: { field }
491
- };
492
- }
493
- if (config.requireDateRange) {
494
- const { field, maxRangeDays } = config.requireDateRange;
495
- const range = parseDateRange(query, field);
496
- if (!range) return {
497
- code: "arc.aggregation.required_date_range_missing",
498
- message: `Aggregation requires a bounded date range on "${field}" — supply ?${field}[gte]=... and ?${field}[lt]=... (or ?${field}[lte]=...).`,
499
- status: 400,
500
- meta: { field }
501
- };
502
- if (maxRangeDays !== void 0) {
503
- const days = (range.upper.getTime() - range.lower.getTime()) / 864e5;
504
- if (days > maxRangeDays) return {
505
- code: "arc.aggregation.date_range_exceeded",
506
- message: `Aggregation date range on "${field}" exceeds the cap (${maxRangeDays} days). Requested range: ${days.toFixed(1)} days. Narrow the range and retry.`,
507
- status: 400,
508
- meta: {
509
- field,
510
- maxRangeDays,
511
- requestedDays: days
512
- }
513
- };
514
- }
515
- }
516
- return null;
517
- }
518
- function hasFilterOnField(query, field) {
519
- const direct = query[field];
520
- if (direct !== void 0 && direct !== "") return true;
521
- for (const key of Object.keys(query)) if (key.startsWith(`${field}[`)) return true;
522
- return false;
523
- }
524
- function parseDateRange(query, field) {
525
- let gte;
526
- let lte;
527
- const nested = query[field];
528
- if (nested && typeof nested === "object" && !Array.isArray(nested)) {
529
- const ops = nested;
530
- gte = pickString(ops.gte) ?? pickString(ops.gt);
531
- lte = pickString(ops.lte) ?? pickString(ops.lt);
532
- }
533
- if (!gte) gte = pickString(query[`${field}[gte]`]) ?? pickString(query[`${field}[gt]`]);
534
- if (!lte) lte = pickString(query[`${field}[lte]`]) ?? pickString(query[`${field}[lt]`]);
535
- if (!gte || !lte) return null;
536
- const lower = new Date(gte);
537
- const upper = new Date(lte);
538
- if (Number.isNaN(lower.getTime()) || Number.isNaN(upper.getTime())) return null;
539
- if (upper <= lower) return null;
540
- return {
541
- lower,
542
- upper
543
- };
544
- }
545
- /**
546
- * Bracket-syntax operator shorthand → canonical Mongo operator. Mirrors
547
- * the `operators` map in `ArcQueryParser` so the aggregation route emits
548
- * the same shape the CRUD list route produces. Aggregations don't run
549
- * through the resource-level QueryParser (they have their own URL→IR
550
- * compile path), so this translation has to happen in arc itself —
551
- * downstream kits' filter compilers expect canonical `$gte/$lte/$in/...`
552
- * keys, not bare `gte/lte/in/...` shorthand.
553
- */
554
- const OPERATOR_SHORTHAND = {
555
- eq: "$eq",
556
- ne: "$ne",
557
- gt: "$gt",
558
- gte: "$gte",
559
- lt: "$lt",
560
- lte: "$lte",
561
- in: "$in",
562
- nin: "$nin",
563
- like: "$regex",
564
- contains: "$regex",
565
- regex: "$regex",
566
- exists: "$exists",
567
- size: "$size",
568
- type: "$type"
569
- };
570
- const SHORTHAND_RANGE_OPS = new Set([
571
- "gt",
572
- "gte",
573
- "lt",
574
- "lte"
575
- ]);
576
- const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
577
- function tryCoerceDate(v) {
578
- if (typeof v !== "string" || !ISO_DATE_RE.test(v)) return v;
579
- const d = new Date(v);
580
- return Number.isNaN(d.getTime()) ? v : d;
581
- }
582
- /**
583
- * Translate a qs-parsed nested-operator object (`{ field: { gte, lte } }`)
584
- * into Mongo-shape (`{ field: { $gte: Date, $lte: Date } }`). Only fires
585
- * when EVERY key is a known shorthand operator — leaves user-data
586
- * objects untouched so callers can still equality-match on a stored
587
- * sub-document.
588
- */
589
- function expandShorthandOperators(value) {
590
- if (!value || typeof value !== "object" || Array.isArray(value)) return value;
591
- const nested = value;
592
- const keys = Object.keys(nested);
593
- if (keys.length === 0) return value;
594
- if (!keys.every((k) => !k.startsWith("$") && OPERATOR_SHORTHAND[k] !== void 0)) return value;
595
- const expanded = {};
596
- for (const [op, opVal] of Object.entries(nested)) {
597
- const mongoOp = OPERATOR_SHORTHAND[op];
598
- if (!mongoOp) continue;
599
- expanded[mongoOp] = SHORTHAND_RANGE_OPS.has(op) ? tryCoerceDate(opVal) : opVal;
600
- }
601
- return expanded;
602
- }
603
- /**
604
- * Strip control params (page/limit/sort/select/...) and the resource-
605
- * dispatch verbs from the query, leaving only filter predicates the
606
- * caller used to narrow the aggregation. Bracket-syntax operator
607
- * shorthand (`createdAt[gte]=...`) gets translated to canonical Mongo-
608
- * shape here so kits don't have to reimplement the URL grammar — same
609
- * contract `ArcQueryParser` enforces for the CRUD list route.
610
- *
611
- * The resulting record is shallow-merged into the AggRequest filter
612
- * via `compileAggRequest`.
613
- */
614
- function extractCallerFilter(query) {
615
- const out = {};
616
- const reserved = new Set([
617
- "page",
618
- "limit",
619
- "after",
620
- "sort",
621
- "select",
622
- "populate",
623
- "search",
624
- "_count",
625
- "_distinct",
626
- "_exists"
627
- ]);
628
- for (const [key, value] of Object.entries(query)) {
629
- if (reserved.has(key)) continue;
630
- if (value === void 0 || value === "") continue;
631
- out[key] = expandShorthandOperators(value);
632
- }
633
- return out;
634
- }
635
- /**
636
- * Map a kit-thrown error to the framework-agnostic execute response.
637
- * Detects two well-known signals:
638
- * - "unsupported" / "not implemented" → 501 with upgrade hint
639
- * - timeout markers → 504
640
- * - everything else → 500
641
- */
642
- function mapAggregateError(err, aggregationName) {
643
- const message = err instanceof Error ? err.message : String(err);
644
- const lower = message.toLowerCase();
645
- if (lower.includes("unsupported") || lower.includes("not implemented")) return {
646
- status: 501,
647
- body: {
648
- code: "arc.adapter.capability_required",
649
- message: `Aggregation "${aggregationName}" failed: ${message}. The kit may not yet support this feature (e.g. lookups in aggregate). Upgrade the kit or remove the unsupported field.`,
650
- status: 501,
651
- meta: { aggregation: aggregationName }
652
- }
653
- };
654
- if (lower.includes("maxtimems") || lower.includes("timeout") || lower.includes("timed out")) return {
655
- status: 504,
656
- body: {
657
- code: "arc.gateway_timeout",
658
- message: `Aggregation "${aggregationName}" timed out: ${message}. Narrow the filter or raise the timeout.`,
659
- status: 504,
660
- meta: { aggregation: aggregationName }
661
- }
662
- };
663
- return {
664
- status: 500,
665
- body: {
666
- code: "arc.internal_error",
667
- message: `Aggregation "${aggregationName}" failed: ${message}`,
668
- status: 500,
669
- meta: { aggregation: aggregationName }
670
- }
671
- };
672
- }
673
- //#endregion
674
- export { executeAggregation as n, validateAggregations as r, buildAggregationHandler as t };
445
+ export { validate_exports as i, compileAggRequest as n, validateAggregations as r, adapterSupportsAggregate as t };
@@ -1,4 +1,4 @@
1
- import { n as DomainEvent } from "./EventTransport-CT_52aWU.mjs";
1
+ import { n as DomainEvent } from "./EventTransport-B1GFiYUy.mjs";
2
2
  import { FastifyPluginAsync, FastifyRequest } from "fastify";
3
3
 
4
4
  //#region src/plugins/health.d.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc",
3
- "version": "2.15.4",
3
+ "version": "2.16.1",
4
4
  "description": "Resource-oriented backend framework for Fastify - clean, minimal, powerful, tree-shakable",
5
5
  "type": "module",
6
6
  "exports": {
@@ -48,14 +48,6 @@
48
48
  "types": "./dist/auth/index.d.mts",
49
49
  "default": "./dist/auth/index.mjs"
50
50
  },
51
- "./org": {
52
- "types": "./dist/org/index.d.mts",
53
- "default": "./dist/org/index.mjs"
54
- },
55
- "./org/types": {
56
- "types": "./dist/org/types.d.mts",
57
- "default": "./dist/org/types.mjs"
58
- },
59
51
  "./hooks": {
60
52
  "types": "./dist/hooks/index.d.mts",
61
53
  "default": "./dist/hooks/index.mjs"
@@ -227,8 +219,8 @@
227
219
  "lint:fix": "biome check --fix src/",
228
220
  "lint:all": "biome check src/ tests/",
229
221
  "test": "vitest run",
230
- "test:main": "vitest run",
231
- "test:perf": "node --expose-gc ./node_modules/vitest/vitest.mjs run --config vitest.perf.config.ts",
222
+ "test:main": "vitest run --no-file-parallelism",
223
+ "test:perf": "node --expose-gc ./node_modules/vitest/vitest.mjs run --config vitest.perf.config.ts --no-file-parallelism",
232
224
  "test:ci": "npm run test:main && npm run test:perf",
233
225
  "test:watch": "vitest",
234
226
  "test:ui": "vitest --ui",
@@ -239,14 +231,14 @@
239
231
  "push": "classytic-push",
240
232
  "release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
241
233
  "release": "npm run push -- main && npm run release:tag && npm publish",
242
- "prepublishOnly": "npm run typecheck && npm run lint && npm run build && npm run test:ci && npm run smoke"
234
+ "prepublishOnly": "npm run typecheck && npm run lint && npm run build && npm run test:main && npm run smoke"
243
235
  },
244
236
  "engines": {
245
237
  "node": ">=22"
246
238
  },
247
239
  "peerDependencies": {
248
- "@classytic/primitives": ">=0.5.0",
249
- "@classytic/repo-core": ">=0.4.1",
240
+ "@classytic/primitives": ">=0.6.0",
241
+ "@classytic/repo-core": ">=0.5.0",
250
242
  "@classytic/streamline": ">=2.3.3",
251
243
  "@fastify/cors": ">=11.0.0",
252
244
  "@fastify/helmet": ">=13.0.0",
@@ -349,18 +341,18 @@
349
341
  },
350
342
  "dependencies": {
351
343
  "fastify-plugin": "^5.0.1",
352
- "qs": "^6.15.1",
344
+ "qs": "^6.15.2",
353
345
  "secure-json-parse": "^4.1.0"
354
346
  },
355
347
  "devDependencies": {
356
348
  "@better-auth/drizzle-adapter": "^1.6.9",
357
349
  "@better-auth/mongo-adapter": "^1.6.9",
358
- "@biomejs/biome": "^2.4.11",
350
+ "@biomejs/biome": "^2.4.15",
359
351
  "@classytic/dev-tools": "^0.2.0",
360
- "@classytic/mongokit": "^3.13.2",
361
- "@classytic/primitives": "^0.4.0",
362
- "@classytic/repo-core": "^0.4.1",
363
- "@classytic/sqlitekit": "^0.3.1",
352
+ "@classytic/mongokit": "^3.14.0",
353
+ "@classytic/primitives": "^0.6.0",
354
+ "@classytic/repo-core": "^0.5.0",
355
+ "@classytic/sqlitekit": "^0.5.0",
364
356
  "@classytic/streamline": "^2.3.3",
365
357
  "@fastify/cors": "^11.2.0",
366
358
  "@fastify/helmet": "^13.0.2",
@@ -374,27 +366,28 @@
374
366
  "@modelcontextprotocol/sdk": "^1.29.0",
375
367
  "@opentelemetry/api": "^1.9.1",
376
368
  "@sinclair/typebox": "^0.34.0",
377
- "@types/node": "^22.10.0",
378
- "@types/qs": "^6.14.0",
369
+ "@types/node": "^22.19.19",
370
+ "@types/qs": "^6.15.1",
379
371
  "@vitest/coverage-v8": "^3.2.4",
380
372
  "ajv": "^8.18.0",
381
373
  "better-auth": "^1.6.9",
382
- "better-sqlite3": "^12.9.0",
383
- "bullmq": "^5.73.5",
374
+ "better-sqlite3": "^12.10.0",
375
+ "bullmq": "^5.76.9",
384
376
  "dotenv": "^17.4.2",
385
377
  "drizzle-orm": "^0.45.2",
386
- "fast-check": "^4.6.0",
378
+ "fast-check": "^4.8.0",
379
+ "fastify": "^5.8.5",
387
380
  "fastify-raw-body": "^5.0.0",
388
381
  "ioredis": "^5.10.1",
389
382
  "jsonwebtoken": "^9.0.0",
390
- "knip": "^6.4.1",
383
+ "knip": "^6.14.1",
391
384
  "mongodb": "^7.1.0",
392
385
  "mongodb-memory-server": "^11.0.1",
393
- "mongoose": ">=9.4.1",
386
+ "mongoose": "^9.6.2",
394
387
  "tsdown": "^0.21.7",
395
388
  "typescript": "^6.0.2",
396
389
  "vitest": "^3.0.0",
397
- "ws": "^8.0.0",
390
+ "ws": "^8.20.1",
398
391
  "zod": "^4.3.6"
399
392
  },
400
393
  "keywords": [
@@ -420,4 +413,4 @@
420
413
  "type": "git",
421
414
  "url": "https://github.com/classytic/arc.git"
422
415
  }
423
- }
416
+ }