@classytic/arc 2.18.2 → 2.18.3
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/dist/{BaseController-DPwe89qs.mjs → BaseController-B-uXzf6I.mjs} +403 -216
- package/dist/{QueryCache-8YmAQ-zW.d.mts → QueryCache-Dsyv2mLS.d.mts} +1 -1
- package/dist/audit/index.d.mts +1 -1
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +4 -4
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi-ClWxaceA.mjs → betterAuthOpenApi-DAAhMZff.mjs} +1 -1
- package/dist/{buildHandler-C_SkBNF3.mjs → buildHandler-Cvrc2p7r.mjs} +1 -1
- package/dist/cache/index.d.mts +3 -3
- package/dist/cache/index.mjs +3 -3
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +1 -1
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.d.mts +1 -1
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.d.mts +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/cli/index.d.mts +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -3
- package/dist/{core-B41eZxyM.mjs → core-C88uIwd3.mjs} +161 -103
- package/dist/{createActionRouter-CRaIwB5S.mjs → createActionRouter-DGCCp-kP.mjs} +4 -2
- package/dist/{createAggregationRouter-n3O6WdLO.mjs → createAggregationRouter-Ca77b8iK.mjs} +3 -3
- package/dist/{createApp-DZvrb587.mjs → createApp-DLuUGVZQ.mjs} +11 -11
- package/dist/{defineEvent-NPKUJRt8.mjs → defineEvent-DC981g-o.mjs} +1 -1
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-6mfTLBC-.mjs → elevation-BzhyVWfn.mjs} +1 -1
- package/dist/{eventPlugin-Dxp9JlKK.mjs → eventPlugin-Bw4bUDEO.mjs} +2 -2
- package/dist/{eventPlugin-DjEx_J3j.d.mts → eventPlugin-C1W1CRbf.d.mts} +1 -1
- package/dist/events/index.d.mts +8 -5
- package/dist/events/index.mjs +26 -7
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +52 -5
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +3 -3
- package/dist/{fastify-augmentation-CL48B6gW.d.mts → fastify-augmentation-BdCl_eNo.d.mts} +2 -2
- package/dist/{fields-DqZLp5d6.d.mts → fields-CKLrhyOb.d.mts} +1 -1
- package/dist/{generate-CcM24hoq.mjs → generate-BaxDdmJ8.mjs} +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +2 -2
- package/dist/idempotency/index.mjs +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-CNiryUMN.d.mts → index-Cvo1u9n6.d.mts} +3 -3
- package/dist/{index-C000faom.d.mts → index-QYma8mdC.d.mts} +215 -241
- package/dist/{index-3L-adFJO.d.mts → index-vcWUGkwf.d.mts} +3 -3
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +5 -5
- package/dist/{init-DQ_rY1J7.mjs → init-DSsdRfk9.mjs} +3 -3
- package/dist/integrations/event-gateway.d.mts +2 -2
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/jobs.mjs +66 -1
- package/dist/integrations/mcp/index.d.mts +4 -4
- 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 +16 -1
- package/dist/integrations/streamline.mjs +33 -2
- package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +2 -2
- package/dist/{loadResourcesFromEntry-DCjYMgfC.mjs → loadResourcesFromEntry-XHNeUWpu.mjs} +1 -1
- package/dist/{mcpPlugin-BrepZ0cV.mjs → mcpPlugin-CzKdt3B8.mjs} +9 -9
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/{openapi-CLZx9bwS.mjs → openapi-ButnRyOM.mjs} +5 -5
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +2 -2
- package/dist/{permissions-B1Qy55fg.mjs → permissions-3uzwkbQt.mjs} +99 -99
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/pipeline-D4sqiokR.mjs +336 -0
- package/dist/plugins/index.d.mts +5 -5
- package/dist/plugins/index.mjs +9 -9
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +4 -4
- package/dist/presets/filesUpload.mjs +2 -2
- 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/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-BYbPobbF.mjs → presets-CF79RHEo.mjs} +1 -1
- package/dist/{queryCachePlugin-Do4P_WBC.d.mts → queryCachePlugin-Bd2tYgh5.d.mts} +2 -2
- package/dist/{queryCachePlugin-B4XMSSe7.mjs → queryCachePlugin-CnAeFqju.mjs} +3 -3
- package/dist/rateLimit-yKvxWAky.mjs +207 -0
- package/dist/{redis-cpqVb85w.d.mts → redis-3hqH_Qk_.d.mts} +1 -1
- package/dist/{redis-stream-BuiAS3aE.d.mts → redis-stream-BtEUtjxZ.d.mts} +5 -4
- package/dist/registerRoute-BEYn0ylk.mjs +108 -0
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{schemaIR-Bk6Tns82.mjs → schemaIR-CE6USIRi.mjs} +1 -1
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +2 -2
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +12 -2
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-DH-7WO64.d.mts → types-BWBcb88c.d.mts} +1 -1
- package/dist/{types-DEEWwQFR.d.mts → types-WVlAVp8T.d.mts} +10 -10
- package/dist/utils/index.d.mts +3 -3
- package/dist/utils/index.mjs +3 -3
- package/dist/{utils-BMQK8mRl.mjs → utils-xAQeDyU_.mjs} +1 -1
- package/dist/{validate-DFIP2VrM.mjs → validate-0__MuUsK.mjs} +201 -190
- package/dist/{versioning-5H4930tj.d.mts → versioning-BSEDGXKC.d.mts} +1 -1
- package/dist/{websocket-BZMUP7Gr.d.mts → websocket-BFBT5rH-.d.mts} +1 -1
- package/package.json +5 -5
- package/skills/arc/references/api-reference.md +1 -1
- package/dist/routerShared-Dl0A77Ba.mjs +0 -724
- /package/dist/{EventTransport-ClEDfiMS.mjs → EventTransport-7LPXLEUX.mjs} +0 -0
- /package/dist/{EventTransport-B1GFiYUy.d.mts → EventTransport-CaaXM5Y1.d.mts} +0 -0
- /package/dist/{HookSystem-Cmf7-Etp.mjs → HookSystem-BfCpWbwM.mjs} +0 -0
- /package/dist/{ResourceRegistry-CzSzVK1m.mjs → ResourceRegistry-CWmOggxS.mjs} +0 -0
- /package/dist/{actionPermissions-Bjmvn7Eb.mjs → actionPermissions-CX9QIMTd.mjs} +0 -0
- /package/dist/{caching-8MfrrUYU.mjs → caching-C0DdKiYB.mjs} +0 -0
- /package/dist/{elevation-DvwtjDwu.d.mts → elevation-DW90z5bT.d.mts} +0 -0
- /package/dist/{errorHandler-wQejMBT3.mjs → errorHandler-Bua97SEt.mjs} +0 -0
- /package/dist/{errorHandler-DuqTqeqn.d.mts → errorHandler-_Lp2k9M3.d.mts} +0 -0
- /package/dist/{errors-4ZloYFvm.d.mts → errors-DZOemeGa.d.mts} +0 -0
- /package/dist/{externalPaths-ClykOQqa.d.mts → externalPaths-DLle0R07.d.mts} +0 -0
- /package/dist/{generate-cRnsafMS.d.mts → generate-Dmk8cfvX.d.mts} +0 -0
- /package/dist/{init-D-vrTZ1G.d.mts → init-BS4Daqnv.d.mts} +0 -0
- /package/dist/{interface-DoE6UoC2.d.mts → interface-CsrGR8LF.d.mts} +0 -0
- /package/dist/{interface-BVkZsvEe.d.mts → interface-DL7LlWS5.d.mts} +0 -0
- /package/dist/{keys-DopsCuyQ.mjs → keys-B3yyBEyH.mjs} +0 -0
- /package/dist/{loadResources-fol6JuvW.mjs → loadResources-CwXt9MCh.mjs} +0 -0
- /package/dist/{memory-QOLe11D5.mjs → memory-TdTHCS9-.mjs} +0 -0
- /package/dist/{metrics-B-PU4-Yu.mjs → metrics-Da2EUPi9.mjs} +0 -0
- /package/dist/{multipartBody-VjVWmZl8.mjs → multipartBody-709YcXi1.mjs} +0 -0
- /package/dist/{pipe-DiCyvyPN.mjs → pipe-9Tl-OAus.mjs} +0 -0
- /package/dist/{pluralize-B9M8xvy-.mjs → pluralize-BxdUl2P4.mjs} +0 -0
- /package/dist/{preloadResources-BOXzPhKU.mjs → preloadResources-B-j-iqo-.mjs} +0 -0
- /package/dist/{pushref-registry-Bq_-EVsC.d.mts → pushref-registry-Dqclzjy7.d.mts} +0 -0
- /package/dist/{registry-BrlHkrRS.mjs → registry-Br15xdGM.mjs} +0 -0
- /package/dist/{replyHelpers-boS5Rx26.mjs → replyHelpers-DXHVxrrj.mjs} +0 -0
- /package/dist/{schemaConverter-De34B1ZG.mjs → schemaConverter-DatAaVNj.mjs} +0 -0
- /package/dist/{sessionManager-DlmaHUAl.d.mts → sessionManager-CyHpYXoB.d.mts} +0 -0
- /package/dist/{sse-jh5OqFjR.mjs → sse-Xq8UtuQx.mjs} +0 -0
- /package/dist/{storage-BFmamN5m.d.mts → storage-C5q8fuEt.d.mts} +0 -0
- /package/dist/{store-helpers-5JgaxieH.mjs → store-helpers-DjSYI2uP.mjs} +0 -0
- /package/dist/{tracing-BYQ3hsbM.d.mts → tracing-Bnb1-YVd.d.mts} +0 -0
- /package/dist/{typeGuards-BzkXkvVv.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
- /package/dist/{types-CfapW2t8.d.mts → types-BJwJN2Pg.d.mts} +0 -0
- /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
- /package/dist/{versioning-CuIwUQZ2.mjs → versioning-C0K8KQLx.mjs} +0 -0
|
@@ -1,4 +1,192 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
|
|
2
|
+
//#region src/core/aggregation/normalize.ts
|
|
3
|
+
/**
|
|
4
|
+
* Framework cap on URL-driven aggregation limits when the host opts
|
|
5
|
+
* in to `defaultLimit` without explicit `maxLimit`. Set to the same
|
|
6
|
+
* order of magnitude as a "large dashboard page" — bigger than every
|
|
7
|
+
* realistic grouped tile, smaller than "we returned the whole table."
|
|
8
|
+
*/
|
|
9
|
+
const AGG_FRAMEWORK_MAX_LIMIT = 1e3;
|
|
10
|
+
function parseMeasureShorthand(s) {
|
|
11
|
+
if (s === "count") return { op: "count" };
|
|
12
|
+
const colon = s.indexOf(":");
|
|
13
|
+
if (colon < 0) return null;
|
|
14
|
+
const op = s.slice(0, colon);
|
|
15
|
+
const rest = s.slice(colon + 1);
|
|
16
|
+
if (!rest) return null;
|
|
17
|
+
if (op === "percentile") {
|
|
18
|
+
const lastColon = rest.lastIndexOf(":");
|
|
19
|
+
if (lastColon < 0) return null;
|
|
20
|
+
const field = rest.slice(0, lastColon);
|
|
21
|
+
const pStr = rest.slice(lastColon + 1);
|
|
22
|
+
if (!field || !pStr) return null;
|
|
23
|
+
const p = Number(pStr);
|
|
24
|
+
if (!Number.isFinite(p)) return null;
|
|
25
|
+
return {
|
|
26
|
+
op: "percentile",
|
|
27
|
+
field,
|
|
28
|
+
p
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
switch (op) {
|
|
32
|
+
case "count": return {
|
|
33
|
+
op: "count",
|
|
34
|
+
field: rest
|
|
35
|
+
};
|
|
36
|
+
case "countDistinct": return {
|
|
37
|
+
op: "countDistinct",
|
|
38
|
+
field: rest
|
|
39
|
+
};
|
|
40
|
+
case "sum": return {
|
|
41
|
+
op: "sum",
|
|
42
|
+
field: rest
|
|
43
|
+
};
|
|
44
|
+
case "avg": return {
|
|
45
|
+
op: "avg",
|
|
46
|
+
field: rest
|
|
47
|
+
};
|
|
48
|
+
case "min": return {
|
|
49
|
+
op: "min",
|
|
50
|
+
field: rest
|
|
51
|
+
};
|
|
52
|
+
case "max": return {
|
|
53
|
+
op: "max",
|
|
54
|
+
field: rest
|
|
55
|
+
};
|
|
56
|
+
default: return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function normalizeGroupBy(groupBy) {
|
|
60
|
+
if (!groupBy) return [];
|
|
61
|
+
if (typeof groupBy === "string") return [groupBy];
|
|
62
|
+
return [...groupBy];
|
|
63
|
+
}
|
|
64
|
+
function collectLookupAliases(lookups) {
|
|
65
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
66
|
+
if (!lookups) return aliases;
|
|
67
|
+
for (const lookup of lookups) aliases.add(lookup.as ?? lookup.from);
|
|
68
|
+
return aliases;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Collect fields that aggregation MUST NOT reference.
|
|
72
|
+
*
|
|
73
|
+
* Default rule: only `hidden: true` blocks. `hidden` means the field is
|
|
74
|
+
* omitted from list/get responses, so exposing it via aggregation would
|
|
75
|
+
* leak data the client can't otherwise see. `systemManaged` is a write
|
|
76
|
+
* rule (server stamps the value, clients can't PATCH it) — those fields
|
|
77
|
+
* are still visible per-row, so aggregating them leaks nothing.
|
|
78
|
+
*
|
|
79
|
+
* Two opt-in overrides via `ArcFieldRule.aggregable`:
|
|
80
|
+
* - `aggregable: false` — explicit deny on a visible field (rare; use
|
|
81
|
+
* when the per-row value is fine but the across-row distribution is
|
|
82
|
+
* itself sensitive).
|
|
83
|
+
* - `aggregable: true` — explicit allow, even on a `hidden` field
|
|
84
|
+
* (escape hatch — caller asserts cardinality leak isn't a concern).
|
|
85
|
+
*/
|
|
86
|
+
function collectBlockedFields(schemaOptions) {
|
|
87
|
+
const blocked = /* @__PURE__ */ new Set();
|
|
88
|
+
const fieldRules = schemaOptions?.fieldRules;
|
|
89
|
+
if (!fieldRules) return blocked;
|
|
90
|
+
for (const [field, rules] of Object.entries(fieldRules)) {
|
|
91
|
+
if (!rules) continue;
|
|
92
|
+
if (rules.aggregable === true) continue;
|
|
93
|
+
if (rules.aggregable === false) {
|
|
94
|
+
blocked.add(field);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (rules.hidden) blocked.add(field);
|
|
98
|
+
}
|
|
99
|
+
return blocked;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Resolve the URL-driven limit policy into a concrete `{ default, max }`
|
|
103
|
+
* pair. Returns `undefined` when the host uses the static `limit` form
|
|
104
|
+
* (or no limit at all) — the request handler reads the absence to know
|
|
105
|
+
* "do not parse `?limit=N` from the URL."
|
|
106
|
+
*/
|
|
107
|
+
function resolveLimitPolicy(config) {
|
|
108
|
+
if (config.defaultLimit === void 0) return void 0;
|
|
109
|
+
return {
|
|
110
|
+
defaultLimit: config.defaultLimit,
|
|
111
|
+
maxLimit: config.maxLimit ?? AGG_FRAMEWORK_MAX_LIMIT
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve the effective per-request `limit` — three cases:
|
|
116
|
+
*
|
|
117
|
+
* 1. Host declared static `limit` → URL `?limit` ignored, static wins.
|
|
118
|
+
* 2. Host declared `defaultLimit` → parse `?limit=N`, cap at
|
|
119
|
+
* `maxLimit`, fall back to default on absent / invalid input.
|
|
120
|
+
* 3. Host declared neither → returns `undefined` (no limit applied,
|
|
121
|
+
* preserves pre-2.16.1 behavior).
|
|
122
|
+
*/
|
|
123
|
+
function resolveAggRequestLimit(normalized, query) {
|
|
124
|
+
const policy = normalized.compiled.limitPolicy;
|
|
125
|
+
if (!policy) return normalized.compiled.limit;
|
|
126
|
+
const raw = query["limit"];
|
|
127
|
+
if (raw === void 0 || raw === "") return policy.defaultLimit;
|
|
128
|
+
const parsed = typeof raw === "number" ? raw : Number.parseInt(String(raw), 10);
|
|
129
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return policy.defaultLimit;
|
|
130
|
+
return Math.min(Math.floor(parsed), policy.maxLimit);
|
|
131
|
+
}
|
|
132
|
+
/** Compile to canonical `AggRequest` for `repo.aggregate()` at request time. */
|
|
133
|
+
function compileAggRequest(normalized, callerFilter, tenantOptions, query) {
|
|
134
|
+
const filter = {
|
|
135
|
+
...normalized.compiled.filter ?? {},
|
|
136
|
+
...callerFilter
|
|
137
|
+
};
|
|
138
|
+
const executionHints = buildExecutionHints(normalized.base);
|
|
139
|
+
const cache = buildCacheOptions(normalized.base);
|
|
140
|
+
const limit = query ? resolveAggRequestLimit(normalized, query) : normalized.compiled.limit;
|
|
141
|
+
return {
|
|
142
|
+
measures: normalized.compiled.measures,
|
|
143
|
+
...Object.keys(filter).length > 0 ? { filter } : {},
|
|
144
|
+
...normalized.compiled.lookups ? { lookups: normalized.compiled.lookups } : {},
|
|
145
|
+
...normalized.compiled.groupBy ? { groupBy: normalized.compiled.groupBy } : {},
|
|
146
|
+
...normalized.compiled.dateBuckets ? { dateBuckets: normalized.compiled.dateBuckets } : {},
|
|
147
|
+
...normalized.compiled.having ? { having: normalized.compiled.having } : {},
|
|
148
|
+
...normalized.compiled.sort ? { sort: normalized.compiled.sort } : {},
|
|
149
|
+
...limit !== void 0 ? { limit } : {},
|
|
150
|
+
...normalized.compiled.topN ? { topN: normalized.compiled.topN } : {},
|
|
151
|
+
...executionHints ? { executionHints } : {},
|
|
152
|
+
...cache ? { cache } : {}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Translate the host's declarative `cache:` config into the TanStack-
|
|
157
|
+
* shaped `CacheOptions` repo-core's unified cache plugin reads from
|
|
158
|
+
* `req.cache`. The plugin handles SWR semantics, version-bump
|
|
159
|
+
* invalidation, and tag side-index — arc just declares the policy.
|
|
160
|
+
*
|
|
161
|
+
* No translation needed when the host disabled cache (returns
|
|
162
|
+
* undefined, kit falls through to a non-cached call).
|
|
163
|
+
*/
|
|
164
|
+
function buildCacheOptions(config) {
|
|
165
|
+
const c = config.cache;
|
|
166
|
+
if (!c) return void 0;
|
|
167
|
+
return {
|
|
168
|
+
...c.staleTime !== void 0 ? { staleTime: c.staleTime } : {},
|
|
169
|
+
...c.gcTime !== void 0 ? { gcTime: c.gcTime } : {},
|
|
170
|
+
...c.tags ? { tags: c.tags } : {},
|
|
171
|
+
swr: c.swr ?? true
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Map arc's declarative knobs onto repo-core's portable `AggExecutionHints`.
|
|
176
|
+
* Kits that don't honor a given hint silently ignore it (per IR contract);
|
|
177
|
+
* mongokit threads `maxTimeMs` → `maxTimeMS` and `indexHint` → `aggregate.option({ hint })`.
|
|
178
|
+
*/
|
|
179
|
+
function buildExecutionHints(config) {
|
|
180
|
+
const hints = {};
|
|
181
|
+
if (typeof config.timeout === "number" && config.timeout > 0) hints.maxTimeMs = config.timeout;
|
|
182
|
+
if (config.indexHint && config.indexHint.leadingKeys.length > 0) {
|
|
183
|
+
const hintObj = {};
|
|
184
|
+
for (const key of config.indexHint.leadingKeys) hintObj[key] = 1;
|
|
185
|
+
hints.indexHint = hintObj;
|
|
186
|
+
}
|
|
187
|
+
return Object.keys(hints).length > 0 ? hints : void 0;
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
2
190
|
//#region src/core/aggregation/validate.ts
|
|
3
191
|
var validate_exports = /* @__PURE__ */ __exportAll({
|
|
4
192
|
ArcAggregationConfigError: () => ArcAggregationConfigError,
|
|
@@ -11,13 +199,6 @@ var ArcAggregationConfigError = class extends Error {
|
|
|
11
199
|
name = "ArcAggregationConfigError";
|
|
12
200
|
};
|
|
13
201
|
/**
|
|
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
|
-
/**
|
|
21
202
|
* Validate + normalize all aggregations on a resource. Throws on first
|
|
22
203
|
* misconfig with the offending aggregation name in the message — hosts
|
|
23
204
|
* see exactly which entry needs fixing.
|
|
@@ -32,9 +213,9 @@ function validateAggregations(resourceName, aggregations, schemaOptions) {
|
|
|
32
213
|
const out = [];
|
|
33
214
|
const blockedFields = collectBlockedFields(schemaOptions);
|
|
34
215
|
for (const [name, config] of Object.entries(aggregations)) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
216
|
+
validateAggregationName(resourceName, name);
|
|
217
|
+
validatePermissionsDeclared(resourceName, name, config);
|
|
218
|
+
validateMeasuresNonEmpty(resourceName, name, config);
|
|
38
219
|
const lookupAliases = collectLookupAliases(config.lookups);
|
|
39
220
|
const measures = compileMeasures(resourceName, name, config.measures);
|
|
40
221
|
const groupBy = normalizeGroupBy(config.groupBy);
|
|
@@ -81,6 +262,15 @@ function validateAggregations(resourceName, aggregations, schemaOptions) {
|
|
|
81
262
|
}
|
|
82
263
|
return out;
|
|
83
264
|
}
|
|
265
|
+
function validateAggregationName(resourceName, name) {
|
|
266
|
+
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.`);
|
|
267
|
+
}
|
|
268
|
+
function validatePermissionsDeclared(resourceName, name, config) {
|
|
269
|
+
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.`);
|
|
270
|
+
}
|
|
271
|
+
function validateMeasuresNonEmpty(resourceName, name, config) {
|
|
272
|
+
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.`);
|
|
273
|
+
}
|
|
84
274
|
/**
|
|
85
275
|
* Validate `limit` / `defaultLimit` / `maxLimit` triad. Throws on
|
|
86
276
|
* misconfig at boot — boundary value, hosts see exactly what to fix.
|
|
@@ -102,19 +292,6 @@ function validateLimitConfig(resourceName, aggName, config) {
|
|
|
102
292
|
}
|
|
103
293
|
}
|
|
104
294
|
/**
|
|
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
|
-
/**
|
|
118
295
|
* Adapter feature-detect for `aggregate()`. Called at boot when the
|
|
119
296
|
* repository instance is available. Returns `true` when the kit ships
|
|
120
297
|
* `aggregate`; `false` when missing.
|
|
@@ -127,66 +304,6 @@ function adapterSupportsAggregate(repo) {
|
|
|
127
304
|
return typeof repo.aggregate === "function";
|
|
128
305
|
}
|
|
129
306
|
/**
|
|
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
|
-
}
|
|
147
|
-
/** Compile to canonical `AggRequest` for `repo.aggregate()` at request time. */
|
|
148
|
-
function compileAggRequest(normalized, callerFilter, tenantOptions, query) {
|
|
149
|
-
const filter = {
|
|
150
|
-
...normalized.compiled.filter ?? {},
|
|
151
|
-
...callerFilter
|
|
152
|
-
};
|
|
153
|
-
const executionHints = buildExecutionHints(normalized.base);
|
|
154
|
-
const cache = buildCacheOptions(normalized.base);
|
|
155
|
-
const limit = query ? resolveAggRequestLimit(normalized, query) : normalized.compiled.limit;
|
|
156
|
-
return {
|
|
157
|
-
measures: normalized.compiled.measures,
|
|
158
|
-
...Object.keys(filter).length > 0 ? { filter } : {},
|
|
159
|
-
...normalized.compiled.lookups ? { lookups: normalized.compiled.lookups } : {},
|
|
160
|
-
...normalized.compiled.groupBy ? { groupBy: normalized.compiled.groupBy } : {},
|
|
161
|
-
...normalized.compiled.dateBuckets ? { dateBuckets: normalized.compiled.dateBuckets } : {},
|
|
162
|
-
...normalized.compiled.having ? { having: normalized.compiled.having } : {},
|
|
163
|
-
...normalized.compiled.sort ? { sort: normalized.compiled.sort } : {},
|
|
164
|
-
...limit !== void 0 ? { limit } : {},
|
|
165
|
-
...normalized.compiled.topN ? { topN: normalized.compiled.topN } : {},
|
|
166
|
-
...executionHints ? { executionHints } : {},
|
|
167
|
-
...cache ? { cache } : {}
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Translate the host's declarative `cache:` config into the TanStack-
|
|
172
|
-
* shaped `CacheOptions` repo-core's unified cache plugin reads from
|
|
173
|
-
* `req.cache`. The plugin handles SWR semantics, version-bump
|
|
174
|
-
* invalidation, and tag side-index — arc just declares the policy.
|
|
175
|
-
*
|
|
176
|
-
* No translation needed when the host disabled cache (returns
|
|
177
|
-
* undefined, kit falls through to a non-cached call).
|
|
178
|
-
*/
|
|
179
|
-
function buildCacheOptions(config) {
|
|
180
|
-
const c = config.cache;
|
|
181
|
-
if (!c) return void 0;
|
|
182
|
-
return {
|
|
183
|
-
...c.staleTime !== void 0 ? { staleTime: c.staleTime } : {},
|
|
184
|
-
...c.gcTime !== void 0 ? { gcTime: c.gcTime } : {},
|
|
185
|
-
...c.tags ? { tags: c.tags } : {},
|
|
186
|
-
swr: c.swr ?? true
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
307
|
* Boot validation for `topN`. Mirrors the contract mongokit + sqlitekit
|
|
191
308
|
* enforce at request time — both kits check the same three rules and
|
|
192
309
|
* throw with kit-prefixed messages. Running them at boot gives hosts
|
|
@@ -267,21 +384,6 @@ function assertBucketFieldAllowed(input) {
|
|
|
267
384
|
}
|
|
268
385
|
if (blockedFields.has(field)) throw new ArcAggregationConfigError(`Resource "${resourceName}" aggregation "${aggregationName}" dateBucket "${alias}" references field "${field}", but the field is blocked from aggregation (\`hidden: true\` or \`aggregable: false\` in schemaOptions.fieldRules). Bucketing hidden fields would leak temporal info.`);
|
|
269
386
|
}
|
|
270
|
-
/**
|
|
271
|
-
* Map arc's declarative knobs onto repo-core's portable `AggExecutionHints`.
|
|
272
|
-
* Kits that don't honor a given hint silently ignore it (per IR contract);
|
|
273
|
-
* mongokit threads `maxTimeMs` → `maxTimeMS` and `indexHint` → `aggregate.option({ hint })`.
|
|
274
|
-
*/
|
|
275
|
-
function buildExecutionHints(config) {
|
|
276
|
-
const hints = {};
|
|
277
|
-
if (typeof config.timeout === "number" && config.timeout > 0) hints.maxTimeMs = config.timeout;
|
|
278
|
-
if (config.indexHint && config.indexHint.leadingKeys.length > 0) {
|
|
279
|
-
const hintObj = {};
|
|
280
|
-
for (const key of config.indexHint.leadingKeys) hintObj[key] = 1;
|
|
281
|
-
hints.indexHint = hintObj;
|
|
282
|
-
}
|
|
283
|
-
return Object.keys(hints).length > 0 ? hints : void 0;
|
|
284
|
-
}
|
|
285
387
|
const NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/;
|
|
286
388
|
function isValidAggregationName(name) {
|
|
287
389
|
return NAME_PATTERN.test(name);
|
|
@@ -302,55 +404,6 @@ function expandMeasure(input, resource, aggregation, alias) {
|
|
|
302
404
|
validateMeasure(measure, resource, aggregation, alias);
|
|
303
405
|
return measure;
|
|
304
406
|
}
|
|
305
|
-
function parseMeasureShorthand(s) {
|
|
306
|
-
if (s === "count") return { op: "count" };
|
|
307
|
-
const colon = s.indexOf(":");
|
|
308
|
-
if (colon < 0) return null;
|
|
309
|
-
const op = s.slice(0, colon);
|
|
310
|
-
const rest = s.slice(colon + 1);
|
|
311
|
-
if (!rest) return null;
|
|
312
|
-
if (op === "percentile") {
|
|
313
|
-
const lastColon = rest.lastIndexOf(":");
|
|
314
|
-
if (lastColon < 0) return null;
|
|
315
|
-
const field = rest.slice(0, lastColon);
|
|
316
|
-
const pStr = rest.slice(lastColon + 1);
|
|
317
|
-
if (!field || !pStr) return null;
|
|
318
|
-
const p = Number(pStr);
|
|
319
|
-
if (!Number.isFinite(p)) return null;
|
|
320
|
-
return {
|
|
321
|
-
op: "percentile",
|
|
322
|
-
field,
|
|
323
|
-
p
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
switch (op) {
|
|
327
|
-
case "count": return {
|
|
328
|
-
op: "count",
|
|
329
|
-
field: rest
|
|
330
|
-
};
|
|
331
|
-
case "countDistinct": return {
|
|
332
|
-
op: "countDistinct",
|
|
333
|
-
field: rest
|
|
334
|
-
};
|
|
335
|
-
case "sum": return {
|
|
336
|
-
op: "sum",
|
|
337
|
-
field: rest
|
|
338
|
-
};
|
|
339
|
-
case "avg": return {
|
|
340
|
-
op: "avg",
|
|
341
|
-
field: rest
|
|
342
|
-
};
|
|
343
|
-
case "min": return {
|
|
344
|
-
op: "min",
|
|
345
|
-
field: rest
|
|
346
|
-
};
|
|
347
|
-
case "max": return {
|
|
348
|
-
op: "max",
|
|
349
|
-
field: rest
|
|
350
|
-
};
|
|
351
|
-
default: return null;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
407
|
/**
|
|
355
408
|
* Per-measure boot validation. Currently only `percentile` carries a
|
|
356
409
|
* numeric constraint — `p ∈ [0, 1]` matches mongokit's request-time
|
|
@@ -363,48 +416,6 @@ function validateMeasure(measure, resource, aggregation, alias) {
|
|
|
363
416
|
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).`);
|
|
364
417
|
}
|
|
365
418
|
}
|
|
366
|
-
function normalizeGroupBy(groupBy) {
|
|
367
|
-
if (!groupBy) return [];
|
|
368
|
-
if (typeof groupBy === "string") return [groupBy];
|
|
369
|
-
return [...groupBy];
|
|
370
|
-
}
|
|
371
|
-
function collectLookupAliases(lookups) {
|
|
372
|
-
const aliases = /* @__PURE__ */ new Set();
|
|
373
|
-
if (!lookups) return aliases;
|
|
374
|
-
for (const lookup of lookups) aliases.add(lookup.as ?? lookup.from);
|
|
375
|
-
return aliases;
|
|
376
|
-
}
|
|
377
|
-
/**
|
|
378
|
-
* Collect fields that aggregation MUST NOT reference.
|
|
379
|
-
*
|
|
380
|
-
* Default rule: only `hidden: true` blocks. `hidden` means the field is
|
|
381
|
-
* omitted from list/get responses, so exposing it via aggregation would
|
|
382
|
-
* leak data the client can't otherwise see. `systemManaged` is a write
|
|
383
|
-
* rule (server stamps the value, clients can't PATCH it) — those fields
|
|
384
|
-
* are still visible per-row, so aggregating them leaks nothing.
|
|
385
|
-
*
|
|
386
|
-
* Two opt-in overrides via `ArcFieldRule.aggregable`:
|
|
387
|
-
* - `aggregable: false` — explicit deny on a visible field (rare; use
|
|
388
|
-
* when the per-row value is fine but the across-row distribution is
|
|
389
|
-
* itself sensitive).
|
|
390
|
-
* - `aggregable: true` — explicit allow, even on a `hidden` field
|
|
391
|
-
* (escape hatch — caller asserts cardinality leak isn't a concern).
|
|
392
|
-
*/
|
|
393
|
-
function collectBlockedFields(schemaOptions) {
|
|
394
|
-
const blocked = /* @__PURE__ */ new Set();
|
|
395
|
-
const fieldRules = schemaOptions?.fieldRules;
|
|
396
|
-
if (!fieldRules) return blocked;
|
|
397
|
-
for (const [field, rules] of Object.entries(fieldRules)) {
|
|
398
|
-
if (!rules) continue;
|
|
399
|
-
if (rules.aggregable === true) continue;
|
|
400
|
-
if (rules.aggregable === false) {
|
|
401
|
-
blocked.add(field);
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
if (rules.hidden) blocked.add(field);
|
|
405
|
-
}
|
|
406
|
-
return blocked;
|
|
407
|
-
}
|
|
408
419
|
/**
|
|
409
420
|
* Reject:
|
|
410
421
|
* - groupBy / measure.field / sort key referencing a hidden /
|
|
@@ -442,4 +453,4 @@ function assertFieldAllowed(context, ref, input) {
|
|
|
442
453
|
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.`);
|
|
443
454
|
}
|
|
444
455
|
//#endregion
|
|
445
|
-
export {
|
|
456
|
+
export { compileAggRequest as i, validateAggregations as n, validate_exports as r, adapterSupportsAggregate as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as PushRefRegistry, l as EnvelopeWriter, s as PushRefStore } from "./pushref-registry-
|
|
1
|
+
import { a as PushRefRegistry, l as EnvelopeWriter, s as PushRefStore } from "./pushref-registry-Dqclzjy7.mjs";
|
|
2
2
|
import { FastifyBaseLogger, FastifyPluginAsync } from "fastify";
|
|
3
3
|
|
|
4
4
|
//#region src/integrations/websocket/adapter.d.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/arc",
|
|
3
|
-
"version": "2.18.
|
|
3
|
+
"version": "2.18.3",
|
|
4
4
|
"description": "Resource-oriented backend framework for Fastify - clean, minimal, powerful, tree-shakable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -237,7 +237,7 @@
|
|
|
237
237
|
"push": "classytic-push",
|
|
238
238
|
"release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
|
|
239
239
|
"release": "npm run push -- main && npm run release:tag && npm publish",
|
|
240
|
-
"prepublishOnly": "npm run typecheck && npm run lint && npm run build && npm run test:
|
|
240
|
+
"prepublishOnly": "npm run typecheck && npm run lint && npm run build && npm run test:ci && npm run smoke"
|
|
241
241
|
},
|
|
242
242
|
"engines": {
|
|
243
243
|
"node": ">=22"
|
|
@@ -359,10 +359,10 @@
|
|
|
359
359
|
"@better-auth/mongo-adapter": "^1.6.9",
|
|
360
360
|
"@biomejs/biome": "^2.4.15",
|
|
361
361
|
"@classytic/dev-tools": "^0.2.0",
|
|
362
|
-
"@classytic/mongokit": "^3.
|
|
362
|
+
"@classytic/mongokit": "^3.16.0",
|
|
363
363
|
"@classytic/primitives": "^0.6.0",
|
|
364
|
-
"@classytic/repo-core": "^0.
|
|
365
|
-
"@classytic/sqlitekit": "^0.
|
|
364
|
+
"@classytic/repo-core": "^0.6.0",
|
|
365
|
+
"@classytic/sqlitekit": "^0.6.0",
|
|
366
366
|
"@classytic/streamline": "^2.3.3",
|
|
367
367
|
"@fastify/cors": "^11.2.0",
|
|
368
368
|
"@fastify/helmet": "^13.0.2",
|
|
@@ -183,7 +183,7 @@ import type { ArcRequest, IRequestContext, IControllerResponse } from '@classyti
|
|
|
183
183
|
|
|
184
184
|
## Notes
|
|
185
185
|
|
|
186
|
-
- **No default exports** outside Fastify plugin entry files (`
|
|
186
|
+
- **No default exports** outside Fastify plugin entry files (`fp()`-wrapped plugin entries — grep `export default fp` in src/ for the authoritative list).
|
|
187
187
|
- **Type-only subpaths** produce `export {}` at runtime — interfaces are erased.
|
|
188
188
|
- **Adapters live in kits**, not arc. Arc 2.12+ ships zero kit-bound adapters.
|
|
189
189
|
- **Event types live in `@classytic/primitives/events`**, not `@classytic/arc/events`. Arc re-exports the runtime `MemoryEventTransport` only.
|