@classytic/arc 2.16.0 → 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.
- package/dist/{BaseController-DlCCTIxJ.mjs → BaseController-Vk8I7C0x.mjs} +1 -1
- package/dist/{QueryCache-SvmT_9ti.d.mts → QueryCache-8YmAQ-zW.d.mts} +1 -1
- package/dist/{ResourceRegistry-f48hFk3m.mjs → ResourceRegistry-CzSzVK1m.mjs} +3 -0
- package/dist/audit/index.d.mts +1 -1
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{buildHandler-BZX6zzDM.mjs → buildHandler-C_SkBNF3.mjs} +6 -4
- package/dist/cache/index.d.mts +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-DBJ_j6rX.mjs → core-wKeGXn38.mjs} +5 -5
- package/dist/{createActionRouter-DUpN3Dd1.mjs → createActionRouter-eiOFtnC3.mjs} +2 -2
- package/dist/{createAggregationRouter-Dq-TUCuY.mjs → createAggregationRouter-C4unhBnV.mjs} +3 -3
- package/dist/{createApp-DNccuhyI.mjs → createApp-DdPBtmzp.mjs} +8 -8
- package/dist/{defineEvent-DRwY0fYm.mjs → defineEvent-NPKUJRt8.mjs} +1 -1
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{eventPlugin-C2cGqtRO.mjs → eventPlugin-CnJCUyM0.mjs} +1 -1
- package/dist/{eventPlugin-CtHC_av1.d.mts → eventPlugin-DjEx_J3j.d.mts} +1 -1
- package/dist/events/index.d.mts +3 -3
- package/dist/events/index.mjs +5 -5
- 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 +4 -3
- package/dist/fastify-augmentation-CJm0vh-l.d.mts +42 -0
- package/dist/{fields-Anj0xdih.d.mts → fields-DqZLp5d6.d.mts} +1 -1
- package/dist/hooks/index.d.mts +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-3oIimXQn.d.mts → index-3L-adFJO.d.mts} +3 -3
- package/dist/{index-B-ulKx5P.d.mts → index-CC_pgZjY.d.mts} +36 -16
- package/dist/{index-CkW0flkU.d.mts → index-DBDgy6Hw.d.mts} +79 -7
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +4 -4
- 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/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/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/{loadResourcesFromEntry-BLMEI2Xa.mjs → loadResourcesFromEntry-DCjYMgfC.mjs} +1 -1
- package/dist/{mcpPlugin-7vGV51ED.mjs → mcpPlugin-Dbgz9_Rj.mjs} +5 -5
- package/dist/middleware/index.d.mts +1 -1
- package/dist/{openapi-34T9yNwd.mjs → openapi-BRnd3pT7.mjs} +36 -9
- package/dist/permissions/index.d.mts +2 -2
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +5 -59
- package/dist/plugins/index.mjs +7 -7
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/preloadResources-BOXzPhKU.mjs +45 -0
- package/dist/presets/filesUpload.d.mts +4 -4
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/search.d.mts +2 -2
- package/dist/{queryCachePlugin-Biqzfbi5.d.mts → queryCachePlugin-Do4P_WBC.d.mts} +2 -2
- package/dist/{redis-Cyzrz6SX.d.mts → redis-cpqVb85w.d.mts} +1 -1
- package/dist/{redis-stream-DT-YjzrB.d.mts → redis-stream-BuiAS3aE.d.mts} +1 -1
- package/dist/registry/index.d.mts +30 -8
- package/dist/registry/index.mjs +2 -2
- package/dist/{registry-BBE23CDj.mjs → registry-BrlHkrRS.mjs} +14 -8
- package/dist/{routerShared-CZV5aabX.mjs → routerShared-ClV4cCaa.mjs} +37 -13
- package/dist/scope/index.d.mts +2 -2
- package/dist/testing/index.d.mts +2 -25
- package/dist/testing/index.mjs +2 -45
- 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-BsJMEQ4D.d.mts → types-BD0BCdf6.d.mts} +1 -1
- package/dist/{types-D-fYtKjb.d.mts → types-Dw--SyTq.d.mts} +34 -11
- package/dist/utils/index.d.mts +5 -4
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-DC5ycPfr.mjs → utils-DPXZnZUx.mjs} +69 -1
- package/dist/{validate-By96rH0r.mjs → validate-DFIP2VrM.mjs} +65 -3
- package/dist/{versioning-ZwX9tmbS.d.mts → versioning-5H4930tj.d.mts} +1 -1
- package/package.json +3 -3
- /package/dist/{EventTransport-C-2oAHtw.d.mts → EventTransport-B1GFiYUy.d.mts} +0 -0
- /package/dist/{EventTransport-Hxvv5QQz.mjs → EventTransport-ClEDfiMS.mjs} +0 -0
- /package/dist/{caching-TeHE8G-v.mjs → caching-8MfrrUYU.mjs} +0 -0
- /package/dist/{elevation-0YBpa663.d.mts → elevation-DvwtjDwu.d.mts} +0 -0
- /package/dist/{errorHandler-DpoXQHZ9.mjs → errorHandler-DNPzqaTT.mjs} +0 -0
- /package/dist/{errorHandler-mHuyWzZE.d.mts → errorHandler-DuqTqeqn.d.mts} +0 -0
- /package/dist/{errors-C1lX_jlm.d.mts → errors-4ZloYFvm.d.mts} +0 -0
- /package/dist/{externalPaths-DFg-2KTp.d.mts → externalPaths-ClykOQqa.d.mts} +0 -0
- /package/dist/{generate-CYac-OLv.mjs → generate-CcM24hoq.mjs} +0 -0
- /package/dist/{generate-BWFwgcCM.d.mts → generate-cRnsafMS.d.mts} +0 -0
- /package/dist/{init-Dv71MsJr.d.mts → init-D-vrTZ1G.d.mts} +0 -0
- /package/dist/{init-HDvoO9L5.mjs → init-DYzfniuX.mjs} +0 -0
- /package/dist/{interface-CH0OQudo.d.mts → interface-BVkZsvEe.d.mts} +0 -0
- /package/dist/{interface-NwJ_qPlY.d.mts → interface-DoE6UoC2.d.mts} +0 -0
- /package/dist/{loadResources-ChQEj8ih.mjs → loadResources-fol6JuvW.mjs} +0 -0
- /package/dist/{metrics-TuOmguhi.mjs → metrics-B-PU4-Yu.mjs} +0 -0
- /package/dist/{replyHelpers-C-gD32oF.mjs → replyHelpers-boS5Rx26.mjs} +0 -0
- /package/dist/{schemaIR-Ctc89DSn.mjs → schemaIR-DAOvBWgT.mjs} +0 -0
- /package/dist/{sessionManager-BqFegc0W.d.mts → sessionManager-DlmaHUAl.d.mts} +0 -0
- /package/dist/{sse-BY6sTy4P.mjs → sse-jh5OqFjR.mjs} +0 -0
- /package/dist/{storage-D2KZJAmn.d.mts → storage-HcAgP5Ni.d.mts} +0 -0
- /package/dist/{store-helpers-B0sunfZZ.mjs → store-helpers-5JgaxieH.mjs} +0 -0
- /package/dist/{tracing-Dm8n7Cnn.d.mts → tracing-bRqqUxFO.d.mts} +0 -0
- /package/dist/{types-DVfpSfx2.d.mts → types-CfapW2t8.d.mts} +0 -0
- /package/dist/{versioning-B6mimogM.mjs → versioning-CuIwUQZ2.mjs} +0 -0
- /package/dist/{websocket-BkjeGZRn.d.mts → websocket-BJ0w-3Dt.d.mts} +0 -0
|
@@ -11,6 +11,13 @@ var ArcAggregationConfigError = class extends Error {
|
|
|
11
11
|
name = "ArcAggregationConfigError";
|
|
12
12
|
};
|
|
13
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
|
+
/**
|
|
14
21
|
* Validate + normalize all aggregations on a resource. Throws on first
|
|
15
22
|
* misconfig with the offending aggregation name in the message — hosts
|
|
16
23
|
* see exactly which entry needs fixing.
|
|
@@ -53,6 +60,8 @@ function validateAggregations(resourceName, aggregations, schemaOptions) {
|
|
|
53
60
|
bucketAliases
|
|
54
61
|
});
|
|
55
62
|
if (config.topN) validateTopNConfig(resourceName, name, config.topN, groupBy, measures, bucketAliases);
|
|
63
|
+
validateLimitConfig(resourceName, name, config);
|
|
64
|
+
const limitPolicy = resolveLimitPolicy(config);
|
|
56
65
|
out.push({
|
|
57
66
|
name,
|
|
58
67
|
base: config,
|
|
@@ -65,13 +74,47 @@ function validateAggregations(resourceName, aggregations, schemaOptions) {
|
|
|
65
74
|
having: config.having,
|
|
66
75
|
sort: config.sort,
|
|
67
76
|
limit: config.limit,
|
|
68
|
-
topN: config.topN
|
|
77
|
+
topN: config.topN,
|
|
78
|
+
...limitPolicy ? { limitPolicy } : {}
|
|
69
79
|
}
|
|
70
80
|
});
|
|
71
81
|
}
|
|
72
82
|
return out;
|
|
73
83
|
}
|
|
74
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
|
+
/**
|
|
75
118
|
* Adapter feature-detect for `aggregate()`. Called at boot when the
|
|
76
119
|
* repository instance is available. Returns `true` when the kit ships
|
|
77
120
|
* `aggregate`; `false` when missing.
|
|
@@ -83,14 +126,33 @@ function adapterSupportsAggregate(repo) {
|
|
|
83
126
|
if (!repo || typeof repo !== "object") return false;
|
|
84
127
|
return typeof repo.aggregate === "function";
|
|
85
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
|
+
}
|
|
86
147
|
/** Compile to canonical `AggRequest` for `repo.aggregate()` at request time. */
|
|
87
|
-
function compileAggRequest(normalized, callerFilter, tenantOptions) {
|
|
148
|
+
function compileAggRequest(normalized, callerFilter, tenantOptions, query) {
|
|
88
149
|
const filter = {
|
|
89
150
|
...normalized.compiled.filter ?? {},
|
|
90
151
|
...callerFilter
|
|
91
152
|
};
|
|
92
153
|
const executionHints = buildExecutionHints(normalized.base);
|
|
93
154
|
const cache = buildCacheOptions(normalized.base);
|
|
155
|
+
const limit = query ? resolveAggRequestLimit(normalized, query) : normalized.compiled.limit;
|
|
94
156
|
return {
|
|
95
157
|
measures: normalized.compiled.measures,
|
|
96
158
|
...Object.keys(filter).length > 0 ? { filter } : {},
|
|
@@ -99,7 +161,7 @@ function compileAggRequest(normalized, callerFilter, tenantOptions) {
|
|
|
99
161
|
...normalized.compiled.dateBuckets ? { dateBuckets: normalized.compiled.dateBuckets } : {},
|
|
100
162
|
...normalized.compiled.having ? { having: normalized.compiled.having } : {},
|
|
101
163
|
...normalized.compiled.sort ? { sort: normalized.compiled.sort } : {},
|
|
102
|
-
...
|
|
164
|
+
...limit !== void 0 ? { limit } : {},
|
|
103
165
|
...normalized.compiled.topN ? { topN: normalized.compiled.topN } : {},
|
|
104
166
|
...executionHints ? { executionHints } : {},
|
|
105
167
|
...cache ? { cache } : {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/arc",
|
|
3
|
-
"version": "2.16.
|
|
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": {
|
|
@@ -231,7 +231,7 @@
|
|
|
231
231
|
"push": "classytic-push",
|
|
232
232
|
"release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
|
|
233
233
|
"release": "npm run push -- main && npm run release:tag && npm publish",
|
|
234
|
-
"prepublishOnly": "npm run typecheck && npm run lint && npm run build && npm run test:
|
|
234
|
+
"prepublishOnly": "npm run typecheck && npm run lint && npm run build && npm run test:main && npm run smoke"
|
|
235
235
|
},
|
|
236
236
|
"engines": {
|
|
237
237
|
"node": ">=22"
|
|
@@ -352,7 +352,7 @@
|
|
|
352
352
|
"@classytic/mongokit": "^3.14.0",
|
|
353
353
|
"@classytic/primitives": "^0.6.0",
|
|
354
354
|
"@classytic/repo-core": "^0.5.0",
|
|
355
|
-
"@classytic/sqlitekit": "^0.
|
|
355
|
+
"@classytic/sqlitekit": "^0.5.0",
|
|
356
356
|
"@classytic/streamline": "^2.3.3",
|
|
357
357
|
"@fastify/cors": "^11.2.0",
|
|
358
358
|
"@fastify/helmet": "^13.0.2",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|