@classytic/arc 2.11.4 → 2.13.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.
- package/README.md +16 -12
- package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
- package/dist/EventTransport-CT_52aWU.d.mts +34 -0
- package/dist/EventTransport-DLWoUMHy.mjs +103 -0
- package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/audit.d.mts +199 -0
- package/dist/auth/audit.mjs +288 -0
- package/dist/auth/index.d.mts +3 -3
- package/dist/auth/index.mjs +117 -191
- package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
- package/dist/buildHandler-olo-gt94.mjs +610 -0
- package/dist/cache/index.mjs +3 -3
- package/dist/cli/commands/describe.d.mts +89 -13
- package/dist/cli/commands/describe.mjs +56 -2
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +147 -48
- package/dist/cli/commands/init.d.mts +13 -0
- package/dist/cli/commands/init.mjs +130 -87
- package/dist/cli/commands/introspect.mjs +8 -1
- package/dist/context/index.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -5
- package/dist/core-D72ia0EH.mjs +1399 -0
- package/dist/{createActionRouter-CIKOcNA7.mjs → createActionRouter-CEvzKcy8.mjs} +7 -20
- package/dist/createAggregationRouter-CyecOxnO.mjs +114 -0
- package/dist/{createApp-C9bRrqlX.mjs → createApp-XX2-N0Yd.mjs} +28 -22
- package/dist/{defineEvent-D1Ky9M1D.mjs → defineEvent-D5h7EvAx.mjs} +1 -1
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
- package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
- package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
- package/dist/errors-j4aJm1Wg.mjs +184 -0
- package/dist/{eventPlugin-Cts2-Tfj.mjs → eventPlugin-CaKTYkYM.mjs} +28 -4
- package/dist/{eventPlugin-DDJoNEPL.d.mts → eventPlugin-qXpqTebY.d.mts} +24 -1
- package/dist/events/index.d.mts +6 -6
- package/dist/events/index.mjs +11 -35
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +2 -2
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-BRjxOAFp.d.mts → fields-COhcH3fk.d.mts} +23 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +1 -20
- package/dist/idempotency/redis.mjs +1 -1
- package/dist/{index-rHjXmJar.d.mts → index-BTqLEvhu.d.mts} +163 -3
- package/dist/{index-CXXRbnf8.d.mts → index-BtW7qYwa.d.mts} +660 -326
- package/dist/{index-m8mOOlFW.d.mts → index-Ds61mrJE.d.mts} +50 -4
- package/dist/{index-D9t1KNaB.d.mts → index-Dz5IKsrE.d.mts} +360 -219
- package/dist/index.d.mts +6 -7
- package/dist/index.mjs +9 -10
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +60 -11
- package/dist/integrations/streamline.mjs +75 -85
- package/dist/integrations/websocket.mjs +2 -8
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +2 -2
- package/dist/migrations/index.d.mts +23 -3
- package/dist/migrations/index.mjs +0 -7
- package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
- package/dist/{openapi-D7G1V7ex.mjs → openapi-CiOMVW1p.mjs} +143 -13
- package/dist/org/index.d.mts +2 -2
- package/dist/org/index.mjs +1 -1
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +3 -3
- package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
- package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +16 -31
- package/dist/plugins/index.mjs +33 -13
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +4 -4
- package/dist/presets/filesUpload.mjs +6 -9
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +2 -2
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +6 -8
- package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
- package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
- package/dist/{redis-stream-xTGxB2bm.d.mts → redis-stream-D6HzR1Z_.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
- package/dist/{resourceToTools-CxNmI6xF.mjs → resourceToTools-C5coh64w.mjs} +224 -71
- package/dist/{routerShared-BqLRb5l7.mjs → routerShared-D6_fEGHh.mjs} +40 -36
- package/dist/{schemaIR-Dy2p4MxS.mjs → schemaIR-7Vl611Qs.mjs} +1 -1
- package/dist/schemas/index.d.mts +100 -30
- package/dist/schemas/index.mjs +86 -29
- package/dist/scim/index.d.mts +264 -0
- package/dist/scim/index.mjs +963 -0
- package/dist/scope/index.d.mts +3 -3
- package/dist/scope/index.mjs +4 -4
- package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
- package/dist/{store-helpers-Cp4uKC1U.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
- package/dist/testing/index.d.mts +2 -8
- package/dist/testing/index.mjs +16 -24
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-D7KpfiL1.d.mts → types-BvqwCCSx.d.mts} +73 -25
- package/dist/{types-DDyTPc6y.d.mts → types-CTYvcwHe.d.mts} +195 -1
- package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
- package/dist/{types-BQ9TJQNy.d.mts → types-DQHFc8PM.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +5 -5
- package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
- package/dist/{versioning-DsglKfM_.d.mts → versioning-DTTvc80y.d.mts} +1 -1
- package/package.json +24 -34
- package/skills/arc/SKILL.md +147 -51
- package/skills/arc/references/agent-auth.md +238 -0
- package/skills/arc/references/api-reference.md +187 -0
- package/skills/arc/references/auth.md +354 -7
- package/skills/arc/references/enterprise-auth.md +94 -0
- package/skills/arc/references/events.md +8 -6
- package/skills/arc/references/mcp.md +2 -2
- package/skills/arc/references/multi-tenancy.md +11 -2
- package/skills/arc/references/production.md +10 -9
- package/skills/arc/references/scim.md +247 -0
- package/skills/arc/references/testing.md +1 -1
- package/skills/arc-code-review/SKILL.md +141 -0
- package/skills/arc-code-review/references/anti-patterns.md +911 -0
- package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
- package/skills/arc-code-review/references/migration-recipes.md +700 -0
- package/skills/arc-code-review/references/mongokit-migration.md +386 -0
- package/skills/arc-code-review/references/scaffolding.md +230 -0
- package/skills/arc-code-review/references/severity.md +127 -0
- package/dist/EventTransport-BFQjw9pB.mjs +0 -133
- package/dist/EventTransport-CYNUXdCJ.d.mts +0 -293
- package/dist/adapters/index.d.mts +0 -3
- package/dist/adapters/index.mjs +0 -2
- package/dist/adapters-DUUiiimH.mjs +0 -964
- package/dist/auth/mongoose.d.mts +0 -191
- package/dist/auth/mongoose.mjs +0 -73
- package/dist/core-CbcQRIch.mjs +0 -1054
- package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
- package/dist/errorHandler-DEWmGWPz.d.mts +0 -114
- package/dist/errors-D5c-5BJL.mjs +0 -232
- package/dist/index-Rg8axYPz.d.mts +0 -370
- /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
- /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
- /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
- /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
- /package/dist/{elevation-BQQXZ_VR.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
- /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
- /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
- /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
- /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
- /package/dist/{pluralize-CWP6MB39.mjs → pluralize-DQgqgifU.mjs} +0 -0
- /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
- /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
- /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
- /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
- /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
- /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
//#region src/core/aggregation/validate.ts
|
|
2
|
+
/** Thrown on aggregation misconfig at boot time. */
|
|
3
|
+
var ArcAggregationConfigError = class extends Error {
|
|
4
|
+
name = "ArcAggregationConfigError";
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Validate + normalize all aggregations on a resource. Throws on first
|
|
8
|
+
* misconfig with the offending aggregation name in the message — hosts
|
|
9
|
+
* see exactly which entry needs fixing.
|
|
10
|
+
*
|
|
11
|
+
* Adapter feature-detection runs only when an `adapter` is present;
|
|
12
|
+
* boot order means the controller's `repository` may be the
|
|
13
|
+
* `RepositoryLike` shape. Best-effort `'aggregate' in repo` check covers
|
|
14
|
+
* mongokit / sqlitekit; missing `aggregate()` deferred to request time
|
|
15
|
+
* with a clear 501 (handled in the request handler).
|
|
16
|
+
*/
|
|
17
|
+
function validateAggregations(resourceName, aggregations, schemaOptions) {
|
|
18
|
+
const out = [];
|
|
19
|
+
const blockedFields = collectBlockedFields(schemaOptions);
|
|
20
|
+
for (const [name, config] of Object.entries(aggregations)) {
|
|
21
|
+
if (!isValidAggregationName(name)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation key "${name}" is invalid — keys map to URL segments and must be alphanumeric or underscore/hyphen.`);
|
|
22
|
+
if (typeof config.permissions !== "function") throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${name}" is missing a "permissions" check. Aggregations must declare permissions explicitly — no default-allow. Use a permission helper from @classytic/arc/permissions.`);
|
|
23
|
+
if (!config.measures || Object.keys(config.measures).length === 0) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${name}" has no measures. An empty "measures" map is a wiring bug — at least one measure is required.`);
|
|
24
|
+
const lookupAliases = collectLookupAliases(config.lookups);
|
|
25
|
+
const measures = compileMeasures(resourceName, name, config.measures);
|
|
26
|
+
const groupBy = normalizeGroupBy(config.groupBy);
|
|
27
|
+
const bucketAliases = config.dateBuckets ? Object.keys(config.dateBuckets) : [];
|
|
28
|
+
if (config.dateBuckets) validateDateBuckets({
|
|
29
|
+
resourceName,
|
|
30
|
+
aggregationName: name,
|
|
31
|
+
dateBuckets: config.dateBuckets,
|
|
32
|
+
groupBy,
|
|
33
|
+
measures,
|
|
34
|
+
lookupAliases,
|
|
35
|
+
blockedFields
|
|
36
|
+
});
|
|
37
|
+
validateFieldReferences({
|
|
38
|
+
resourceName,
|
|
39
|
+
aggregationName: name,
|
|
40
|
+
groupBy,
|
|
41
|
+
measures,
|
|
42
|
+
sort: config.sort,
|
|
43
|
+
having: config.having,
|
|
44
|
+
lookupAliases,
|
|
45
|
+
blockedFields,
|
|
46
|
+
bucketAliases
|
|
47
|
+
});
|
|
48
|
+
if (config.topN) validateTopNConfig(resourceName, name, config.topN, groupBy, measures, bucketAliases);
|
|
49
|
+
out.push({
|
|
50
|
+
name,
|
|
51
|
+
base: config,
|
|
52
|
+
compiled: {
|
|
53
|
+
filter: config.filter,
|
|
54
|
+
lookups: config.lookups,
|
|
55
|
+
groupBy: groupBy.length > 0 ? groupBy : void 0,
|
|
56
|
+
dateBuckets: config.dateBuckets,
|
|
57
|
+
measures,
|
|
58
|
+
having: config.having,
|
|
59
|
+
sort: config.sort,
|
|
60
|
+
limit: config.limit,
|
|
61
|
+
topN: config.topN
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Adapter feature-detect for `aggregate()`. Called at boot when the
|
|
69
|
+
* repository instance is available. Returns `true` when the kit ships
|
|
70
|
+
* `aggregate`; `false` when missing.
|
|
71
|
+
*
|
|
72
|
+
* `materialized`-only aggregations bypass this check at the request
|
|
73
|
+
* handler — they never call `repo.aggregate`.
|
|
74
|
+
*/
|
|
75
|
+
function adapterSupportsAggregate(repo) {
|
|
76
|
+
if (!repo || typeof repo !== "object") return false;
|
|
77
|
+
return typeof repo.aggregate === "function";
|
|
78
|
+
}
|
|
79
|
+
/** Compile to canonical `AggRequest` for `repo.aggregate()` at request time. */
|
|
80
|
+
function compileAggRequest(normalized, callerFilter, tenantOptions) {
|
|
81
|
+
const baseFilter = normalized.compiled.filter ?? {};
|
|
82
|
+
const filter = {
|
|
83
|
+
...extractTenantFilter(tenantOptions),
|
|
84
|
+
...baseFilter,
|
|
85
|
+
...callerFilter
|
|
86
|
+
};
|
|
87
|
+
const executionHints = buildExecutionHints(normalized.base);
|
|
88
|
+
const cache = buildCacheOptions(normalized.base);
|
|
89
|
+
return {
|
|
90
|
+
measures: normalized.compiled.measures,
|
|
91
|
+
...Object.keys(filter).length > 0 ? { filter } : {},
|
|
92
|
+
...normalized.compiled.lookups ? { lookups: normalized.compiled.lookups } : {},
|
|
93
|
+
...normalized.compiled.groupBy ? { groupBy: normalized.compiled.groupBy } : {},
|
|
94
|
+
...normalized.compiled.dateBuckets ? { dateBuckets: normalized.compiled.dateBuckets } : {},
|
|
95
|
+
...normalized.compiled.having ? { having: normalized.compiled.having } : {},
|
|
96
|
+
...normalized.compiled.sort ? { sort: normalized.compiled.sort } : {},
|
|
97
|
+
...normalized.compiled.limit !== void 0 ? { limit: normalized.compiled.limit } : {},
|
|
98
|
+
...normalized.compiled.topN ? { topN: normalized.compiled.topN } : {},
|
|
99
|
+
...executionHints ? { executionHints } : {},
|
|
100
|
+
...cache ? { cache } : {}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Translate the host's declarative `cache:` config into the TanStack-
|
|
105
|
+
* shaped `CacheOptions` repo-core's unified cache plugin reads from
|
|
106
|
+
* `req.cache`. The plugin handles SWR semantics, version-bump
|
|
107
|
+
* invalidation, and tag side-index — arc just declares the policy.
|
|
108
|
+
*
|
|
109
|
+
* No translation needed when the host disabled cache (returns
|
|
110
|
+
* undefined, kit falls through to a non-cached call).
|
|
111
|
+
*/
|
|
112
|
+
function buildCacheOptions(config) {
|
|
113
|
+
const c = config.cache;
|
|
114
|
+
if (!c) return void 0;
|
|
115
|
+
return {
|
|
116
|
+
...c.staleTime !== void 0 ? { staleTime: c.staleTime } : {},
|
|
117
|
+
...c.gcTime !== void 0 ? { gcTime: c.gcTime } : {},
|
|
118
|
+
...c.tags ? { tags: c.tags } : {},
|
|
119
|
+
swr: c.swr ?? true
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Boot validation for `topN`. Mirrors the contract mongokit + sqlitekit
|
|
124
|
+
* enforce at request time — both kits check the same three rules and
|
|
125
|
+
* throw with kit-prefixed messages. Running them at boot gives hosts
|
|
126
|
+
* the misconfig surface BEFORE the first dashboard request, with the
|
|
127
|
+
* offending aggregation name included for debugging. Same logic /
|
|
128
|
+
* messages stay aligned across the kits and arc.
|
|
129
|
+
*/
|
|
130
|
+
function validateTopNConfig(resource, aggregation, topN, groupBy, measures, bucketAliases) {
|
|
131
|
+
if (!Number.isInteger(topN.limit) || topN.limit <= 0) throw new ArcAggregationConfigError(`Resource "${resource}" aggregation "${aggregation}" topN.limit must be a positive integer — got ${String(topN.limit)}.`);
|
|
132
|
+
if (!topN.sortBy || Object.keys(topN.sortBy).length === 0) throw new ArcAggregationConfigError(`Resource "${resource}" aggregation "${aggregation}" topN.sortBy must declare at least one ranking field.`);
|
|
133
|
+
const partitionList = Array.isArray(topN.partitionBy) ? topN.partitionBy : [topN.partitionBy];
|
|
134
|
+
if (partitionList.length === 0) throw new ArcAggregationConfigError(`Resource "${resource}" aggregation "${aggregation}" topN.partitionBy must declare at least one partition column.`);
|
|
135
|
+
const validKeys = new Set([
|
|
136
|
+
...groupBy,
|
|
137
|
+
...bucketAliases,
|
|
138
|
+
...Object.keys(measures)
|
|
139
|
+
]);
|
|
140
|
+
for (const key of partitionList) if (!validKeys.has(key)) throw new ArcAggregationConfigError(`Resource "${resource}" aggregation "${aggregation}" topN.partitionBy "${key}" is not a groupBy field, dateBucket alias, or measure alias. Available: ${[...validKeys].join(", ") || "(none — declare groupBy, dateBuckets, or measures)"}.`);
|
|
141
|
+
}
|
|
142
|
+
const VALID_BUCKET_UNITS = new Set([
|
|
143
|
+
"minute",
|
|
144
|
+
"hour",
|
|
145
|
+
"day",
|
|
146
|
+
"week",
|
|
147
|
+
"month",
|
|
148
|
+
"quarter",
|
|
149
|
+
"year"
|
|
150
|
+
]);
|
|
151
|
+
const CUSTOM_BIN_UNIT_BLOCKLIST = new Set(["quarter", "year"]);
|
|
152
|
+
/**
|
|
153
|
+
* Validate `dateBuckets`. Catches the two classes of misconfig kits
|
|
154
|
+
* already throw on at runtime — alias collisions and field-rule
|
|
155
|
+
* violations — at boot, with the offending aggregation name in the
|
|
156
|
+
* message.
|
|
157
|
+
*
|
|
158
|
+
* Rules (parity with mongokit's `validateBucketAliases` + sqlitekit's
|
|
159
|
+
* `compileDateBucket` field-rule pass):
|
|
160
|
+
* 1. Bucket alias MUST NOT collide with a groupBy field or measure
|
|
161
|
+
* alias — output row would have an ambiguous key.
|
|
162
|
+
* 2. Bucket `field` (resolves to a base column or joined-alias path)
|
|
163
|
+
* must NOT be hidden / systemManaged.
|
|
164
|
+
* 3. Custom-bin form (`{ every, unit }`): `every` is a positive
|
|
165
|
+
* integer; `unit` is in the supported set (minute/hour/day/week/
|
|
166
|
+
* month — quarter and year aren't valid in custom-bin form).
|
|
167
|
+
*/
|
|
168
|
+
function validateDateBuckets(input) {
|
|
169
|
+
const { resourceName, aggregationName, dateBuckets, groupBy, measures, lookupAliases, blockedFields } = input;
|
|
170
|
+
const groupBySet = new Set(groupBy);
|
|
171
|
+
const measureAliases = new Set(Object.keys(measures));
|
|
172
|
+
for (const [alias, bucket] of Object.entries(dateBuckets)) {
|
|
173
|
+
if (groupBySet.has(alias)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" dateBucket alias "${alias}" collides with a groupBy field. Pick a unique alias.`);
|
|
174
|
+
if (measureAliases.has(alias)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" dateBucket alias "${alias}" collides with a measure alias. Pick a unique alias.`);
|
|
175
|
+
assertBucketFieldAllowed({
|
|
176
|
+
resourceName,
|
|
177
|
+
aggregationName,
|
|
178
|
+
alias,
|
|
179
|
+
field: bucket.field,
|
|
180
|
+
lookupAliases,
|
|
181
|
+
blockedFields
|
|
182
|
+
});
|
|
183
|
+
if (typeof bucket.interval === "string") {
|
|
184
|
+
if (!VALID_BUCKET_UNITS.has(bucket.interval)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" dateBucket "${alias}" interval "${bucket.interval}" is not a recognized unit. Use one of: ${[...VALID_BUCKET_UNITS].join(", ")}.`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const { every, unit } = bucket.interval;
|
|
188
|
+
if (!Number.isInteger(every) || every <= 0) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" dateBucket "${alias}" interval.every must be a positive integer — got ${String(every)}.`);
|
|
189
|
+
if (!VALID_BUCKET_UNITS.has(unit) || CUSTOM_BIN_UNIT_BLOCKLIST.has(unit)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" dateBucket "${alias}" interval.unit "${unit}" is not valid in custom-bin form. Use minute / hour / day / week / month (quarter and year aren't supported as custom bins).`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function assertBucketFieldAllowed(input) {
|
|
193
|
+
const { resourceName, aggregationName, alias, field, lookupAliases, blockedFields } = input;
|
|
194
|
+
const dot = field.indexOf(".");
|
|
195
|
+
if (dot > 0) {
|
|
196
|
+
const a = field.slice(0, dot);
|
|
197
|
+
if (lookupAliases.has(a)) return;
|
|
198
|
+
if (blockedFields.has(a)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" dateBucket "${alias}" references field "${field}" whose root "${a}" is marked hidden or systemManaged in schemaOptions.fieldRules. Bucketing on hidden fields would leak temporal info.`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (blockedFields.has(field)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" dateBucket "${alias}" references field "${field}", but the field is marked hidden or systemManaged in schemaOptions.fieldRules. Bucketing on hidden fields would leak temporal info.`);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Map arc's declarative knobs onto repo-core's portable `AggExecutionHints`.
|
|
205
|
+
* Kits that don't honor a given hint silently ignore it (per IR contract);
|
|
206
|
+
* mongokit threads `maxTimeMs` → `maxTimeMS` and `indexHint` → `aggregate.option({ hint })`.
|
|
207
|
+
*/
|
|
208
|
+
function buildExecutionHints(config) {
|
|
209
|
+
const hints = {};
|
|
210
|
+
if (typeof config.timeout === "number" && config.timeout > 0) hints.maxTimeMs = config.timeout;
|
|
211
|
+
if (config.indexHint && config.indexHint.leadingKeys.length > 0) {
|
|
212
|
+
const hintObj = {};
|
|
213
|
+
for (const key of config.indexHint.leadingKeys) hintObj[key] = 1;
|
|
214
|
+
hints.indexHint = hintObj;
|
|
215
|
+
}
|
|
216
|
+
return Object.keys(hints).length > 0 ? hints : void 0;
|
|
217
|
+
}
|
|
218
|
+
const NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
|
|
219
|
+
function isValidAggregationName(name) {
|
|
220
|
+
return NAME_PATTERN.test(name);
|
|
221
|
+
}
|
|
222
|
+
function compileMeasures(resource, aggregation, measures) {
|
|
223
|
+
const out = {};
|
|
224
|
+
for (const [alias, input] of Object.entries(measures)) out[alias] = expandMeasure(input, resource, aggregation, alias);
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
function expandMeasure(input, resource, aggregation, alias) {
|
|
228
|
+
let measure;
|
|
229
|
+
if (typeof input === "object" && input !== null) measure = input;
|
|
230
|
+
else if (typeof input === "string") {
|
|
231
|
+
const expanded = parseMeasureShorthand(input);
|
|
232
|
+
if (!expanded) throw new ArcAggregationConfigError(`Resource "${resource}" aggregation "${aggregation}" measure "${alias}" has invalid shorthand "${input}". Use 'count', 'count:field', 'sum:field', 'avg:field', 'min:field', 'max:field', 'countDistinct:field', or 'percentile:field:p' (p ∈ [0, 1]).`);
|
|
233
|
+
measure = expanded;
|
|
234
|
+
} else throw new ArcAggregationConfigError(`Resource "${resource}" aggregation "${aggregation}" measure "${alias}" is not a string or object: got ${typeof input}.`);
|
|
235
|
+
validateMeasure(measure, resource, aggregation, alias);
|
|
236
|
+
return measure;
|
|
237
|
+
}
|
|
238
|
+
function parseMeasureShorthand(s) {
|
|
239
|
+
if (s === "count") return { op: "count" };
|
|
240
|
+
const colon = s.indexOf(":");
|
|
241
|
+
if (colon < 0) return null;
|
|
242
|
+
const op = s.slice(0, colon);
|
|
243
|
+
const rest = s.slice(colon + 1);
|
|
244
|
+
if (!rest) return null;
|
|
245
|
+
if (op === "percentile") {
|
|
246
|
+
const lastColon = rest.lastIndexOf(":");
|
|
247
|
+
if (lastColon < 0) return null;
|
|
248
|
+
const field = rest.slice(0, lastColon);
|
|
249
|
+
const pStr = rest.slice(lastColon + 1);
|
|
250
|
+
if (!field || !pStr) return null;
|
|
251
|
+
const p = Number(pStr);
|
|
252
|
+
if (!Number.isFinite(p)) return null;
|
|
253
|
+
return {
|
|
254
|
+
op: "percentile",
|
|
255
|
+
field,
|
|
256
|
+
p
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
switch (op) {
|
|
260
|
+
case "count": return {
|
|
261
|
+
op: "count",
|
|
262
|
+
field: rest
|
|
263
|
+
};
|
|
264
|
+
case "countDistinct": return {
|
|
265
|
+
op: "countDistinct",
|
|
266
|
+
field: rest
|
|
267
|
+
};
|
|
268
|
+
case "sum": return {
|
|
269
|
+
op: "sum",
|
|
270
|
+
field: rest
|
|
271
|
+
};
|
|
272
|
+
case "avg": return {
|
|
273
|
+
op: "avg",
|
|
274
|
+
field: rest
|
|
275
|
+
};
|
|
276
|
+
case "min": return {
|
|
277
|
+
op: "min",
|
|
278
|
+
field: rest
|
|
279
|
+
};
|
|
280
|
+
case "max": return {
|
|
281
|
+
op: "max",
|
|
282
|
+
field: rest
|
|
283
|
+
};
|
|
284
|
+
default: return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Per-measure boot validation. Currently only `percentile` carries a
|
|
289
|
+
* numeric constraint — `p ∈ [0, 1]` matches mongokit's request-time
|
|
290
|
+
* check (and SQL's `PERCENTILE_CONT` semantics). Running it at boot
|
|
291
|
+
* surfaces the misconfig with the offending aggregation + measure
|
|
292
|
+
* alias instead of a kit-side error at first traffic.
|
|
293
|
+
*/
|
|
294
|
+
function validateMeasure(measure, resource, aggregation, alias) {
|
|
295
|
+
if (measure.op === "percentile") {
|
|
296
|
+
if (!Number.isFinite(measure.p) || measure.p < 0 || measure.p > 1) throw new ArcAggregationConfigError(`Resource "${resource}" aggregation "${aggregation}" measure "${alias}" has invalid percentile p=${String(measure.p)} — must be a finite number in [0, 1] (e.g. 0.5 for median, 0.95 for P95).`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function normalizeGroupBy(groupBy) {
|
|
300
|
+
if (!groupBy) return [];
|
|
301
|
+
if (typeof groupBy === "string") return [groupBy];
|
|
302
|
+
return [...groupBy];
|
|
303
|
+
}
|
|
304
|
+
function collectLookupAliases(lookups) {
|
|
305
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
306
|
+
if (!lookups) return aliases;
|
|
307
|
+
for (const lookup of lookups) aliases.add(lookup.as ?? lookup.from);
|
|
308
|
+
return aliases;
|
|
309
|
+
}
|
|
310
|
+
function collectBlockedFields(schemaOptions) {
|
|
311
|
+
const blocked = /* @__PURE__ */ new Set();
|
|
312
|
+
const fieldRules = schemaOptions?.fieldRules;
|
|
313
|
+
if (!fieldRules) return blocked;
|
|
314
|
+
for (const [field, rules] of Object.entries(fieldRules)) {
|
|
315
|
+
if (!rules) continue;
|
|
316
|
+
if (rules.hidden || rules.systemManaged) blocked.add(field);
|
|
317
|
+
}
|
|
318
|
+
return blocked;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Reject:
|
|
322
|
+
* - groupBy / measure.field / sort key referencing a hidden /
|
|
323
|
+
* systemManaged field
|
|
324
|
+
* - dotted-path references (`alias.field`) where `alias` doesn't
|
|
325
|
+
* match a `LookupSpec.as` (or `from` default)
|
|
326
|
+
*
|
|
327
|
+
* Sort keys may also reference measure aliases, groupBy fields, or
|
|
328
|
+
* dateBucket aliases (all auto-valid — already validated upstream) —
|
|
329
|
+
* those branches accept without further checks.
|
|
330
|
+
*/
|
|
331
|
+
function validateFieldReferences(input) {
|
|
332
|
+
const { groupBy, measures, sort, bucketAliases } = input;
|
|
333
|
+
for (const key of groupBy) assertFieldAllowed("groupBy", key, input);
|
|
334
|
+
for (const [alias, measure] of Object.entries(measures)) if ("field" in measure && measure.field) assertFieldAllowed(`measures.${alias}`, measure.field, input);
|
|
335
|
+
if (sort) {
|
|
336
|
+
const measureAliases = new Set(Object.keys(measures));
|
|
337
|
+
const groupBySet = new Set(groupBy);
|
|
338
|
+
const bucketSet = new Set(bucketAliases);
|
|
339
|
+
for (const key of Object.keys(sort)) {
|
|
340
|
+
if (measureAliases.has(key) || groupBySet.has(key) || bucketSet.has(key)) continue;
|
|
341
|
+
assertFieldAllowed(`sort.${key}`, key, input);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function assertFieldAllowed(context, ref, input) {
|
|
346
|
+
const { resourceName, aggregationName, lookupAliases, blockedFields } = input;
|
|
347
|
+
const dot = ref.indexOf(".");
|
|
348
|
+
if (dot > 0) {
|
|
349
|
+
const alias = ref.slice(0, dot);
|
|
350
|
+
if (lookupAliases.has(alias)) return;
|
|
351
|
+
if (blockedFields.has(alias)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" references field "${ref}" in ${context} whose root "${alias}" is marked hidden or systemManaged in schemaOptions.fieldRules. Aggregating hidden fields would leak cardinality information.`);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (blockedFields.has(ref)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" references field "${ref}" in ${context}, but the field is marked hidden or systemManaged in schemaOptions.fieldRules. Aggregating hidden fields would leak cardinality information.`);
|
|
355
|
+
}
|
|
356
|
+
function extractTenantFilter(tenantOptions) {
|
|
357
|
+
const out = {};
|
|
358
|
+
const optionOnlyKeys = new Set([
|
|
359
|
+
"userId",
|
|
360
|
+
"user",
|
|
361
|
+
"session",
|
|
362
|
+
"requestId"
|
|
363
|
+
]);
|
|
364
|
+
for (const [key, value] of Object.entries(tenantOptions)) {
|
|
365
|
+
if (optionOnlyKeys.has(key)) continue;
|
|
366
|
+
if (value === void 0 || value === null) continue;
|
|
367
|
+
out[key] = value;
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
//#endregion
|
|
372
|
+
//#region src/core/aggregation/buildHandler.ts
|
|
373
|
+
/**
|
|
374
|
+
* Framework-agnostic aggregation execution. Runs safety guards,
|
|
375
|
+
* compiles the AggRequest, dispatches to the materialized hook or
|
|
376
|
+
* `repo.aggregate()`, and applies the post-execution `maxGroups` cap.
|
|
377
|
+
*
|
|
378
|
+
* Returns an envelope describing the response — Fastify wrappers
|
|
379
|
+
* apply it to a reply, MCP wrappers convert it to a tool-call result.
|
|
380
|
+
*
|
|
381
|
+
* **Does NOT run the per-aggregation permission check.** Auth runs
|
|
382
|
+
* upstream (Fastify preHandler chain or MCP `evaluatePermission`)
|
|
383
|
+
* because the permission shape differs by surface (FastifyRequest vs
|
|
384
|
+
* MCP session). Both surfaces fail-closed BEFORE reaching this
|
|
385
|
+
* function; this is purely the runtime executor.
|
|
386
|
+
*/
|
|
387
|
+
async function executeAggregation(normalized, deps, ctx) {
|
|
388
|
+
const { repo } = deps;
|
|
389
|
+
const config = normalized.base;
|
|
390
|
+
const aggregationName = normalized.name;
|
|
391
|
+
const { query, tenantOptions } = ctx;
|
|
392
|
+
const guardError = checkRequestGuards(query, config);
|
|
393
|
+
if (guardError) return {
|
|
394
|
+
status: 400,
|
|
395
|
+
body: guardError
|
|
396
|
+
};
|
|
397
|
+
const aggReq = compileAggRequest(normalized, extractCallerFilter(query), tenantOptions);
|
|
398
|
+
if (config.materialized) {
|
|
399
|
+
const matCtx = {
|
|
400
|
+
filter: aggReq.filter,
|
|
401
|
+
orgId: pickString(tenantOptions.organizationId),
|
|
402
|
+
userId: pickString(tenantOptions.userId),
|
|
403
|
+
requestId: pickString(tenantOptions.requestId),
|
|
404
|
+
query
|
|
405
|
+
};
|
|
406
|
+
return {
|
|
407
|
+
status: 200,
|
|
408
|
+
headers: { "x-aggregation-source": "materialized" },
|
|
409
|
+
body: { rows: (await config.materialized(matCtx)).rows }
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
if (!adapterSupportsAggregate(repo)) return {
|
|
413
|
+
status: 501,
|
|
414
|
+
body: {
|
|
415
|
+
code: "arc.adapter.capability_required",
|
|
416
|
+
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.`,
|
|
417
|
+
status: 501,
|
|
418
|
+
meta: {
|
|
419
|
+
capability: "aggregate",
|
|
420
|
+
aggregation: aggregationName
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
let result;
|
|
425
|
+
try {
|
|
426
|
+
result = await repo.aggregate(aggReq);
|
|
427
|
+
} catch (err) {
|
|
428
|
+
return mapAggregateError(err, aggregationName);
|
|
429
|
+
}
|
|
430
|
+
if (config.maxGroups !== void 0 && result.rows.length > config.maxGroups) return {
|
|
431
|
+
status: 422,
|
|
432
|
+
body: {
|
|
433
|
+
code: "arc.aggregation.max_groups_exceeded",
|
|
434
|
+
message: `Aggregation "${aggregationName}" produced ${result.rows.length} groups, exceeding maxGroups (${config.maxGroups}). Narrow the filter or raise the cap.`,
|
|
435
|
+
status: 422,
|
|
436
|
+
meta: {
|
|
437
|
+
aggregation: aggregationName,
|
|
438
|
+
produced: result.rows.length,
|
|
439
|
+
maxGroups: config.maxGroups
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
return {
|
|
444
|
+
status: 200,
|
|
445
|
+
body: { rows: result.rows }
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Build the Fastify handler for a single aggregation.
|
|
450
|
+
*
|
|
451
|
+
* The returned function calls the repo (or materialized hook), shapes
|
|
452
|
+
* the response envelope, and writes status/headers via Fastify's
|
|
453
|
+
* `reply` API. Errors throw — the router's error handler converts to
|
|
454
|
+
* the standard arc response shape.
|
|
455
|
+
*/
|
|
456
|
+
/**
|
|
457
|
+
* Build the Fastify handler for a single aggregation.
|
|
458
|
+
*
|
|
459
|
+
* Caching lives in the kit's repo-core `cachePlugin` — when the host
|
|
460
|
+
* declares `cache:` on the aggregation, `compileAggRequest` translates
|
|
461
|
+
* to `aggReq.cache: CacheOptions` and the kit handles SWR + tag
|
|
462
|
+
* invalidation + version-bump on writes. Arc passes the request
|
|
463
|
+
* through; no duplicate cache layer at the HTTP handler.
|
|
464
|
+
*/
|
|
465
|
+
function buildAggregationHandler(normalized, deps) {
|
|
466
|
+
const { buildOptions } = deps;
|
|
467
|
+
return async (request, reply) => {
|
|
468
|
+
const result = await executeAggregation(normalized, deps, {
|
|
469
|
+
query: request.query ?? {},
|
|
470
|
+
tenantOptions: buildOptions(request)
|
|
471
|
+
});
|
|
472
|
+
reply.status(result.status);
|
|
473
|
+
if (result.headers) for (const [k, v] of Object.entries(result.headers)) reply.header(k, v);
|
|
474
|
+
return result.body;
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function pickString(value) {
|
|
478
|
+
return typeof value === "string" ? value : void 0;
|
|
479
|
+
}
|
|
480
|
+
function checkRequestGuards(query, config) {
|
|
481
|
+
if (config.requireFilters) {
|
|
482
|
+
for (const field of config.requireFilters) if (!hasFilterOnField(query, field)) return {
|
|
483
|
+
code: "arc.aggregation.required_filter_missing",
|
|
484
|
+
message: `Aggregation requires filter on "${field}" — supply ?${field}=... or ?${field}[op]=... in the query string.`,
|
|
485
|
+
status: 400,
|
|
486
|
+
meta: { field }
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (config.requireDateRange) {
|
|
490
|
+
const { field, maxRangeDays } = config.requireDateRange;
|
|
491
|
+
const range = parseDateRange(query, field);
|
|
492
|
+
if (!range) return {
|
|
493
|
+
code: "arc.aggregation.required_date_range_missing",
|
|
494
|
+
message: `Aggregation requires a bounded date range on "${field}" — supply ?${field}[gte]=... and ?${field}[lt]=... (or ?${field}[lte]=...).`,
|
|
495
|
+
status: 400,
|
|
496
|
+
meta: { field }
|
|
497
|
+
};
|
|
498
|
+
if (maxRangeDays !== void 0) {
|
|
499
|
+
const days = (range.upper.getTime() - range.lower.getTime()) / 864e5;
|
|
500
|
+
if (days > maxRangeDays) return {
|
|
501
|
+
code: "arc.aggregation.date_range_exceeded",
|
|
502
|
+
message: `Aggregation date range on "${field}" exceeds the cap (${maxRangeDays} days). Requested range: ${days.toFixed(1)} days. Narrow the range and retry.`,
|
|
503
|
+
status: 400,
|
|
504
|
+
meta: {
|
|
505
|
+
field,
|
|
506
|
+
maxRangeDays,
|
|
507
|
+
requestedDays: days
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
function hasFilterOnField(query, field) {
|
|
515
|
+
const direct = query[field];
|
|
516
|
+
if (direct !== void 0 && direct !== "") return true;
|
|
517
|
+
for (const key of Object.keys(query)) if (key.startsWith(`${field}[`)) return true;
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
function parseDateRange(query, field) {
|
|
521
|
+
let gte;
|
|
522
|
+
let lte;
|
|
523
|
+
const nested = query[field];
|
|
524
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
525
|
+
const ops = nested;
|
|
526
|
+
gte = pickString(ops.gte) ?? pickString(ops.gt);
|
|
527
|
+
lte = pickString(ops.lte) ?? pickString(ops.lt);
|
|
528
|
+
}
|
|
529
|
+
if (!gte) gte = pickString(query[`${field}[gte]`]) ?? pickString(query[`${field}[gt]`]);
|
|
530
|
+
if (!lte) lte = pickString(query[`${field}[lte]`]) ?? pickString(query[`${field}[lt]`]);
|
|
531
|
+
if (!gte || !lte) return null;
|
|
532
|
+
const lower = new Date(gte);
|
|
533
|
+
const upper = new Date(lte);
|
|
534
|
+
if (Number.isNaN(lower.getTime()) || Number.isNaN(upper.getTime())) return null;
|
|
535
|
+
if (upper <= lower) return null;
|
|
536
|
+
return {
|
|
537
|
+
lower,
|
|
538
|
+
upper
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Strip control params (page/limit/sort/select/...) and the resource-
|
|
543
|
+
* dispatch verbs from the query, leaving only filter predicates the
|
|
544
|
+
* caller used to narrow the aggregation.
|
|
545
|
+
*
|
|
546
|
+
* The resulting record is shallow-merged into the AggRequest filter
|
|
547
|
+
* via `compileAggRequest`. Bracket-syntax keys (`createdAt[gte]`) are
|
|
548
|
+
* preserved — the kit's filter compiler handles them.
|
|
549
|
+
*/
|
|
550
|
+
function extractCallerFilter(query) {
|
|
551
|
+
const out = {};
|
|
552
|
+
const reserved = new Set([
|
|
553
|
+
"page",
|
|
554
|
+
"limit",
|
|
555
|
+
"after",
|
|
556
|
+
"sort",
|
|
557
|
+
"select",
|
|
558
|
+
"populate",
|
|
559
|
+
"search",
|
|
560
|
+
"_count",
|
|
561
|
+
"_distinct",
|
|
562
|
+
"_exists"
|
|
563
|
+
]);
|
|
564
|
+
for (const [key, value] of Object.entries(query)) {
|
|
565
|
+
if (reserved.has(key)) continue;
|
|
566
|
+
if (value === void 0 || value === "") continue;
|
|
567
|
+
out[key] = value;
|
|
568
|
+
}
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Map a kit-thrown error to the framework-agnostic execute response.
|
|
573
|
+
* Detects two well-known signals:
|
|
574
|
+
* - "unsupported" / "not implemented" → 501 with upgrade hint
|
|
575
|
+
* - timeout markers → 504
|
|
576
|
+
* - everything else → 500
|
|
577
|
+
*/
|
|
578
|
+
function mapAggregateError(err, aggregationName) {
|
|
579
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
580
|
+
const lower = message.toLowerCase();
|
|
581
|
+
if (lower.includes("unsupported") || lower.includes("not implemented")) return {
|
|
582
|
+
status: 501,
|
|
583
|
+
body: {
|
|
584
|
+
code: "arc.adapter.capability_required",
|
|
585
|
+
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.`,
|
|
586
|
+
status: 501,
|
|
587
|
+
meta: { aggregation: aggregationName }
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
if (lower.includes("maxtimems") || lower.includes("timeout") || lower.includes("timed out")) return {
|
|
591
|
+
status: 504,
|
|
592
|
+
body: {
|
|
593
|
+
code: "arc.gateway_timeout",
|
|
594
|
+
message: `Aggregation "${aggregationName}" timed out: ${message}. Narrow the filter or raise the timeout.`,
|
|
595
|
+
status: 504,
|
|
596
|
+
meta: { aggregation: aggregationName }
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
return {
|
|
600
|
+
status: 500,
|
|
601
|
+
body: {
|
|
602
|
+
code: "arc.internal_error",
|
|
603
|
+
message: `Aggregation "${aggregationName}" failed: ${message}`,
|
|
604
|
+
status: 500,
|
|
605
|
+
meta: { aggregation: aggregationName }
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
//#endregion
|
|
610
|
+
export { executeAggregation as n, validateAggregations as r, buildAggregationHandler as t };
|
package/dist/cache/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as
|
|
3
|
-
import { r as QueryCache, t as queryCachePlugin } from "../queryCachePlugin-
|
|
1
|
+
import { t as MemoryCacheStore } from "../memory-UBydS5ku.mjs";
|
|
2
|
+
import { i as versionKey, n as hashParams, r as tagVersionKey, t as buildQueryKey } from "../keys-CGcCbNyu.mjs";
|
|
3
|
+
import { r as QueryCache, t as queryCachePlugin } from "../queryCachePlugin-m1XsgAIJ.mjs";
|
|
4
4
|
//#region src/cache/redis.ts
|
|
5
5
|
/**
|
|
6
6
|
* Redis-backed cache store.
|