@classytic/arc 2.15.4 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/bin/arc.js +12 -0
- package/dist/{BaseController-dx3m2J8V.mjs → BaseController-DlCCTIxJ.mjs} +61 -19
- package/dist/{HookSystem-Iiebom92.mjs → HookSystem-Cmf7-Etp.mjs} +8 -4
- package/dist/{QueryCache-D41bfdBB.d.mts → QueryCache-SvmT_9ti.d.mts} +1 -1
- package/dist/{ResourceRegistry-CTERg_2x.mjs → ResourceRegistry-f48hFk3m.mjs} +52 -9
- package/dist/audit/index.d.mts +1 -1
- package/dist/audit/index.mjs +4 -2
- 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--M_i87dQ.mjs → betterAuthOpenApi-ClWxaceA.mjs} +10 -6
- package/dist/buildHandler-BZX6zzDM.mjs +300 -0
- package/dist/cache/index.d.mts +3 -3
- package/dist/cache/index.mjs +3 -3
- package/dist/{caching-SM8gghN6.mjs → caching-TeHE8G-v.mjs} +1 -1
- package/dist/cli/commands/describe.d.mts +35 -1
- package/dist/cli/commands/describe.mjs +52 -12
- package/dist/cli/commands/docs.d.mts +1 -4
- package/dist/cli/commands/docs.mjs +4 -16
- package/dist/cli/commands/generate.d.mts +2 -20
- package/dist/cli/commands/generate.mjs +1 -546
- package/dist/cli/commands/init.d.mts +2 -40
- package/dist/cli/commands/init.mjs +1 -3045
- package/dist/cli/commands/introspect.mjs +53 -64
- package/dist/cli/index.d.mts +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/{constants-Cxde4rpC.mjs → constants-TrJVIJl0.mjs} +7 -0
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -5
- package/dist/{core-CvmOqEms.mjs → core-DBJ_j6rX.mjs} +222 -44
- package/dist/createActionRouter-DUpN3Dd1.mjs +288 -0
- package/dist/{createAggregationRouter-B0bPDf5b.mjs → createAggregationRouter-Dq-TUCuY.mjs} +3 -2
- package/dist/{createApp-PFegs47-.mjs → createApp-DNccuhyI.mjs} +16 -14
- package/dist/{defineEvent-D5h7EvAx.mjs → defineEvent-DRwY0fYm.mjs} +1 -1
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{errorHandler-Bk-AGhkU.mjs → errorHandler-DpoXQHZ9.mjs} +17 -14
- package/dist/errors-C1lX_jlm.d.mts +91 -0
- package/dist/{eventPlugin-CaKTYkYM.mjs → eventPlugin-C2cGqtRO.mjs} +1 -1
- package/dist/{eventPlugin-qXpqTebY.d.mts → eventPlugin-CtHC_av1.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 +1 -1
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-COhcH3fk.d.mts → fields-Anj0xdih.d.mts} +1 -1
- package/dist/generate-BWFwgcCM.d.mts +38 -0
- package/dist/generate-CYac-OLv.mjs +654 -0
- 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-BTqLEvhu.d.mts → index-3oIimXQn.d.mts} +12 -12
- package/dist/{index-BstGxcc3.d.mts → index-B-ulKx5P.d.mts} +55 -4
- package/dist/{index-BswOSJCE.d.mts → index-CkW0flkU.d.mts} +355 -16
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +7 -8
- package/dist/init-Dv71MsJr.d.mts +71 -0
- package/dist/init-HDvoO9L5.mjs +3098 -0
- 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 +3 -3
- package/dist/integrations/mcp/index.d.mts +239 -7
- package/dist/integrations/mcp/index.mjs +2 -528
- package/dist/integrations/mcp/testing.d.mts +2 -2
- package/dist/integrations/mcp/testing.mjs +6 -10
- package/dist/integrations/streamline.mjs +26 -1
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +1 -0
- package/dist/loadResourcesFromEntry-BLMEI2Xa.mjs +51 -0
- package/dist/{resourceToTools-tFYUNmM0.mjs → mcpPlugin-7vGV51ED.mjs} +1021 -318
- package/dist/{memory-UBydS5ku.mjs → memory-QOLe11D5.mjs} +2 -0
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/{openapi-BHXhoX8O.mjs → openapi-34T9yNwd.mjs} +47 -36
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +1 -1
- package/dist/{permissions-ohQyv50e.mjs → permissions-CTxMrreC.mjs} +2 -2
- package/dist/{pipe-Zr0KXjQe.mjs → pipe-DiCyvyPN.mjs} +1 -0
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +5 -5
- package/dist/plugins/index.mjs +10 -10
- package/dist/plugins/response-cache.mjs +5 -5
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{pluralize-DQgqgifU.mjs → pluralize-B9M8xvy-.mjs} +2 -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/multiTenant.mjs +4 -3
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-BbkjdPeH.mjs → presets-C9BE6WaZ.mjs} +2 -2
- package/dist/{queryCachePlugin-m1XsgAIJ.mjs → queryCachePlugin-B4XMSSe7.mjs} +2 -2
- package/dist/{queryCachePlugin-CqMdLI2-.d.mts → queryCachePlugin-Biqzfbi5.d.mts} +2 -2
- package/dist/{redis-DiMkdHEl.d.mts → redis-Cyzrz6SX.d.mts} +1 -1
- package/dist/{redis-stream-D6HzR1Z_.d.mts → redis-stream-DT-YjzrB.d.mts} +1 -1
- package/dist/registry/index.d.mts +319 -2
- package/dist/registry/index.mjs +3 -3
- package/dist/registry-BBE23CDj.mjs +576 -0
- package/dist/{routerShared-DrOa-26E.mjs → routerShared-CZV5aabX.mjs} +3 -3
- package/dist/scope/index.d.mts +3 -3
- package/dist/scope/index.mjs +3 -3
- package/dist/{sse-Bz-5ZeTt.mjs → sse-BY6sTy4P.mjs} +1 -1
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +16 -7
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +5 -5
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-C_s5moIu.mjs → types-Bi0r0vjG.mjs} +53 -1
- package/dist/{types-BQsjgQzS.d.mts → types-BsJMEQ4D.d.mts} +106 -12
- package/dist/{types-DrBaUwyV.d.mts → types-D-fYtKjb.d.mts} +33 -10
- package/dist/{types-CTYvcwHe.d.mts → types-DVfpSfx2.d.mts} +42 -1
- package/dist/utils/index.d.mts +1286 -2
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-_h9B3c57.mjs → utils-DC5ycPfr.mjs} +89 -40
- package/dist/{buildHandler-CcFOpJLh.mjs → validate-By96rH0r.mjs} +8 -299
- package/dist/{versioning-hmkPcDlX.d.mts → versioning-ZwX9tmbS.d.mts} +1 -1
- package/package.json +21 -28
- package/skills/arc/SKILL.md +300 -706
- package/skills/arc/references/auth.md +19 -7
- package/skills/arc-code-review/SKILL.md +1 -1
- package/skills/arc-code-review/references/arc-cheatsheet.md +100 -322
- package/dist/createActionRouter-S3MLVYot.mjs +0 -220
- package/dist/index-bRjYu21O.d.mts +0 -1320
- package/dist/org/index.d.mts +0 -66
- package/dist/org/index.mjs +0 -486
- package/dist/org/types.d.mts +0 -82
- package/dist/org/types.mjs +0 -1
- package/dist/registry-I-ogLgL9.mjs +0 -46
- /package/dist/{EventTransport-CT_52aWU.d.mts → EventTransport-C-2oAHtw.d.mts} +0 -0
- /package/dist/{EventTransport-DLWoUMHy.mjs → EventTransport-Hxvv5QQz.mjs} +0 -0
- /package/dist/{actionPermissions-CyUkQu6O.mjs → actionPermissions-Bjmvn7Eb.mjs} +0 -0
- /package/dist/{elevation-BXOWoGCF.d.mts → elevation-0YBpa663.d.mts} +0 -0
- /package/dist/{elevation-DgoeTyfX.mjs → elevation-Dci0AYLT.mjs} +0 -0
- /package/dist/{errorHandler-DFr45ZG4.d.mts → errorHandler-mHuyWzZE.d.mts} +0 -0
- /package/dist/{externalPaths-BD5nw6St.d.mts → externalPaths-DFg-2KTp.d.mts} +0 -0
- /package/dist/{interface-beEtJyWM.d.mts → interface-CH0OQudo.d.mts} +0 -0
- /package/dist/{interface-DfLGcus7.d.mts → interface-NwJ_qPlY.d.mts} +0 -0
- /package/dist/{keys-CGcCbNyu.mjs → keys-DopsCuyQ.mjs} +0 -0
- /package/dist/{loadResources-DBMQg_Aj.mjs → loadResources-ChQEj8ih.mjs} +0 -0
- /package/dist/{metrics-Qnvwc-LQ.mjs → metrics-TuOmguhi.mjs} +0 -0
- /package/dist/{replyHelpers-CK-FNO8E.mjs → replyHelpers-C-gD32oF.mjs} +0 -0
- /package/dist/{schemaIR-lYhC2gE5.mjs → schemaIR-Ctc89DSn.mjs} +0 -0
- /package/dist/{sessionManager-C4Le_UB3.d.mts → sessionManager-BqFegc0W.d.mts} +0 -0
- /package/dist/{storage-Dfzt4VTl.d.mts → storage-D2KZJAmn.d.mts} +0 -0
- /package/dist/{store-helpers-BkIN9-vu.mjs → store-helpers-B0sunfZZ.mjs} +0 -0
- /package/dist/{tracing-QJVprktp.d.mts → tracing-Dm8n7Cnn.d.mts} +0 -0
- /package/dist/{versioning-BUrT5aP4.mjs → versioning-B6mimogM.mjs} +0 -0
- /package/dist/{websocket-ChC2rqe1.d.mts → websocket-BkjeGZRn.d.mts} +0 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { n as compileAggRequest, t as adapterSupportsAggregate } from "./validate-By96rH0r.mjs";
|
|
2
|
+
//#region src/core/aggregation/buildHandler.ts
|
|
3
|
+
/**
|
|
4
|
+
* Framework-agnostic aggregation execution. Runs safety guards,
|
|
5
|
+
* compiles the AggRequest, dispatches to the materialized hook or
|
|
6
|
+
* `repo.aggregate()`, and applies the post-execution `maxGroups` cap.
|
|
7
|
+
*
|
|
8
|
+
* Returns an envelope describing the response — Fastify wrappers
|
|
9
|
+
* apply it to a reply, MCP wrappers convert it to a tool-call result.
|
|
10
|
+
*
|
|
11
|
+
* **Does NOT run the per-aggregation permission check.** Auth runs
|
|
12
|
+
* upstream (Fastify preHandler chain or MCP `evaluatePermission`)
|
|
13
|
+
* because the permission shape differs by surface (FastifyRequest vs
|
|
14
|
+
* MCP session). Both surfaces fail-closed BEFORE reaching this
|
|
15
|
+
* function; this is purely the runtime executor.
|
|
16
|
+
*/
|
|
17
|
+
async function executeAggregation(normalized, deps, ctx) {
|
|
18
|
+
const { repo } = deps;
|
|
19
|
+
const config = normalized.base;
|
|
20
|
+
const aggregationName = normalized.name;
|
|
21
|
+
const { query, tenantOptions } = ctx;
|
|
22
|
+
const guardError = checkRequestGuards(query, config);
|
|
23
|
+
if (guardError) return {
|
|
24
|
+
status: 400,
|
|
25
|
+
body: guardError
|
|
26
|
+
};
|
|
27
|
+
const aggReq = compileAggRequest(normalized, extractCallerFilter(query), tenantOptions);
|
|
28
|
+
if (config.materialized) {
|
|
29
|
+
const matCtx = {
|
|
30
|
+
filter: aggReq.filter,
|
|
31
|
+
orgId: pickString(tenantOptions.organizationId),
|
|
32
|
+
userId: pickString(tenantOptions.userId),
|
|
33
|
+
requestId: pickString(tenantOptions.requestId),
|
|
34
|
+
query
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
status: 200,
|
|
38
|
+
headers: { "x-aggregation-source": "materialized" },
|
|
39
|
+
body: { rows: (await config.materialized(matCtx)).rows }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (!adapterSupportsAggregate(repo)) return {
|
|
43
|
+
status: 501,
|
|
44
|
+
body: {
|
|
45
|
+
code: "arc.adapter.capability_required",
|
|
46
|
+
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.`,
|
|
47
|
+
status: 501,
|
|
48
|
+
meta: {
|
|
49
|
+
capability: "aggregate",
|
|
50
|
+
aggregation: aggregationName
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
let result;
|
|
55
|
+
try {
|
|
56
|
+
result = await repo.aggregate(aggReq, tenantOptions);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return mapAggregateError(err, aggregationName);
|
|
59
|
+
}
|
|
60
|
+
if (config.maxGroups !== void 0 && result.rows.length > config.maxGroups) return {
|
|
61
|
+
status: 422,
|
|
62
|
+
body: {
|
|
63
|
+
code: "arc.aggregation.max_groups_exceeded",
|
|
64
|
+
message: `Aggregation "${aggregationName}" produced ${result.rows.length} groups, exceeding maxGroups (${config.maxGroups}). Narrow the filter or raise the cap.`,
|
|
65
|
+
status: 422,
|
|
66
|
+
meta: {
|
|
67
|
+
aggregation: aggregationName,
|
|
68
|
+
produced: result.rows.length,
|
|
69
|
+
maxGroups: config.maxGroups
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
status: 200,
|
|
75
|
+
body: { rows: result.rows }
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Build the Fastify handler for a single aggregation.
|
|
80
|
+
*
|
|
81
|
+
* The returned function calls the repo (or materialized hook), shapes
|
|
82
|
+
* the response envelope, and writes status/headers via Fastify's
|
|
83
|
+
* `reply` API. Errors throw — the router's error handler converts to
|
|
84
|
+
* the standard arc response shape.
|
|
85
|
+
*/
|
|
86
|
+
/**
|
|
87
|
+
* Build the Fastify handler for a single aggregation.
|
|
88
|
+
*
|
|
89
|
+
* Caching lives in the kit's repo-core `cachePlugin` — when the host
|
|
90
|
+
* declares `cache:` on the aggregation, `compileAggRequest` translates
|
|
91
|
+
* to `aggReq.cache: CacheOptions` and the kit handles SWR + tag
|
|
92
|
+
* invalidation + version-bump on writes. Arc passes the request
|
|
93
|
+
* through; no duplicate cache layer at the HTTP handler.
|
|
94
|
+
*/
|
|
95
|
+
function buildAggregationHandler(normalized, deps) {
|
|
96
|
+
const { buildOptions } = deps;
|
|
97
|
+
return async (request, reply) => {
|
|
98
|
+
const result = await executeAggregation(normalized, deps, {
|
|
99
|
+
query: request.query ?? {},
|
|
100
|
+
tenantOptions: buildOptions(request)
|
|
101
|
+
});
|
|
102
|
+
reply.status(result.status);
|
|
103
|
+
if (result.headers) for (const [k, v] of Object.entries(result.headers)) reply.header(k, v);
|
|
104
|
+
return result.body;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function pickString(value) {
|
|
108
|
+
return typeof value === "string" ? value : void 0;
|
|
109
|
+
}
|
|
110
|
+
function checkRequestGuards(query, config) {
|
|
111
|
+
if (config.requireFilters) {
|
|
112
|
+
for (const field of config.requireFilters) if (!hasFilterOnField(query, field)) return {
|
|
113
|
+
code: "arc.aggregation.required_filter_missing",
|
|
114
|
+
message: `Aggregation requires filter on "${field}" — supply ?${field}=... or ?${field}[op]=... in the query string.`,
|
|
115
|
+
status: 400,
|
|
116
|
+
meta: { field }
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (config.requireDateRange) {
|
|
120
|
+
const { field, maxRangeDays } = config.requireDateRange;
|
|
121
|
+
const range = parseDateRange(query, field);
|
|
122
|
+
if (!range) return {
|
|
123
|
+
code: "arc.aggregation.required_date_range_missing",
|
|
124
|
+
message: `Aggregation requires a bounded date range on "${field}" — supply ?${field}[gte]=... and ?${field}[lt]=... (or ?${field}[lte]=...).`,
|
|
125
|
+
status: 400,
|
|
126
|
+
meta: { field }
|
|
127
|
+
};
|
|
128
|
+
if (maxRangeDays !== void 0) {
|
|
129
|
+
const days = (range.upper.getTime() - range.lower.getTime()) / 864e5;
|
|
130
|
+
if (days > maxRangeDays) return {
|
|
131
|
+
code: "arc.aggregation.date_range_exceeded",
|
|
132
|
+
message: `Aggregation date range on "${field}" exceeds the cap (${maxRangeDays} days). Requested range: ${days.toFixed(1)} days. Narrow the range and retry.`,
|
|
133
|
+
status: 400,
|
|
134
|
+
meta: {
|
|
135
|
+
field,
|
|
136
|
+
maxRangeDays,
|
|
137
|
+
requestedDays: days
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
function hasFilterOnField(query, field) {
|
|
145
|
+
const direct = query[field];
|
|
146
|
+
if (direct !== void 0 && direct !== "") return true;
|
|
147
|
+
for (const key of Object.keys(query)) if (key.startsWith(`${field}[`)) return true;
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
function parseDateRange(query, field) {
|
|
151
|
+
let gte;
|
|
152
|
+
let lte;
|
|
153
|
+
const nested = query[field];
|
|
154
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
155
|
+
const ops = nested;
|
|
156
|
+
gte = pickString(ops.gte) ?? pickString(ops.gt);
|
|
157
|
+
lte = pickString(ops.lte) ?? pickString(ops.lt);
|
|
158
|
+
}
|
|
159
|
+
if (!gte) gte = pickString(query[`${field}[gte]`]) ?? pickString(query[`${field}[gt]`]);
|
|
160
|
+
if (!lte) lte = pickString(query[`${field}[lte]`]) ?? pickString(query[`${field}[lt]`]);
|
|
161
|
+
if (!gte || !lte) return null;
|
|
162
|
+
const lower = new Date(gte);
|
|
163
|
+
const upper = new Date(lte);
|
|
164
|
+
if (Number.isNaN(lower.getTime()) || Number.isNaN(upper.getTime())) return null;
|
|
165
|
+
if (upper <= lower) return null;
|
|
166
|
+
return {
|
|
167
|
+
lower,
|
|
168
|
+
upper
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Bracket-syntax operator shorthand → canonical Mongo operator. Mirrors
|
|
173
|
+
* the `operators` map in `ArcQueryParser` so the aggregation route emits
|
|
174
|
+
* the same shape the CRUD list route produces. Aggregations don't run
|
|
175
|
+
* through the resource-level QueryParser (they have their own URL→IR
|
|
176
|
+
* compile path), so this translation has to happen in arc itself —
|
|
177
|
+
* downstream kits' filter compilers expect canonical `$gte/$lte/$in/...`
|
|
178
|
+
* keys, not bare `gte/lte/in/...` shorthand.
|
|
179
|
+
*/
|
|
180
|
+
const OPERATOR_SHORTHAND = {
|
|
181
|
+
eq: "$eq",
|
|
182
|
+
ne: "$ne",
|
|
183
|
+
gt: "$gt",
|
|
184
|
+
gte: "$gte",
|
|
185
|
+
lt: "$lt",
|
|
186
|
+
lte: "$lte",
|
|
187
|
+
in: "$in",
|
|
188
|
+
nin: "$nin",
|
|
189
|
+
like: "$regex",
|
|
190
|
+
contains: "$regex",
|
|
191
|
+
regex: "$regex",
|
|
192
|
+
exists: "$exists",
|
|
193
|
+
size: "$size",
|
|
194
|
+
type: "$type"
|
|
195
|
+
};
|
|
196
|
+
const SHORTHAND_RANGE_OPS = new Set([
|
|
197
|
+
"gt",
|
|
198
|
+
"gte",
|
|
199
|
+
"lt",
|
|
200
|
+
"lte"
|
|
201
|
+
]);
|
|
202
|
+
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
203
|
+
function tryCoerceDate(v) {
|
|
204
|
+
if (typeof v !== "string" || !ISO_DATE_RE.test(v)) return v;
|
|
205
|
+
const d = new Date(v);
|
|
206
|
+
return Number.isNaN(d.getTime()) ? v : d;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Translate a qs-parsed nested-operator object (`{ field: { gte, lte } }`)
|
|
210
|
+
* into Mongo-shape (`{ field: { $gte: Date, $lte: Date } }`). Only fires
|
|
211
|
+
* when EVERY key is a known shorthand operator — leaves user-data
|
|
212
|
+
* objects untouched so callers can still equality-match on a stored
|
|
213
|
+
* sub-document.
|
|
214
|
+
*/
|
|
215
|
+
function expandShorthandOperators(value) {
|
|
216
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
217
|
+
const nested = value;
|
|
218
|
+
const keys = Object.keys(nested);
|
|
219
|
+
if (keys.length === 0) return value;
|
|
220
|
+
if (!keys.every((k) => !k.startsWith("$") && OPERATOR_SHORTHAND[k] !== void 0)) return value;
|
|
221
|
+
const expanded = {};
|
|
222
|
+
for (const [op, opVal] of Object.entries(nested)) {
|
|
223
|
+
const mongoOp = OPERATOR_SHORTHAND[op];
|
|
224
|
+
if (!mongoOp) continue;
|
|
225
|
+
expanded[mongoOp] = SHORTHAND_RANGE_OPS.has(op) ? tryCoerceDate(opVal) : opVal;
|
|
226
|
+
}
|
|
227
|
+
return expanded;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Strip control params (page/limit/sort/select/...) and the resource-
|
|
231
|
+
* dispatch verbs from the query, leaving only filter predicates the
|
|
232
|
+
* caller used to narrow the aggregation. Bracket-syntax operator
|
|
233
|
+
* shorthand (`createdAt[gte]=...`) gets translated to canonical Mongo-
|
|
234
|
+
* shape here so kits don't have to reimplement the URL grammar — same
|
|
235
|
+
* contract `ArcQueryParser` enforces for the CRUD list route.
|
|
236
|
+
*
|
|
237
|
+
* The resulting record is shallow-merged into the AggRequest filter
|
|
238
|
+
* via `compileAggRequest`.
|
|
239
|
+
*/
|
|
240
|
+
function extractCallerFilter(query) {
|
|
241
|
+
const out = {};
|
|
242
|
+
const reserved = new Set([
|
|
243
|
+
"page",
|
|
244
|
+
"limit",
|
|
245
|
+
"after",
|
|
246
|
+
"sort",
|
|
247
|
+
"select",
|
|
248
|
+
"populate",
|
|
249
|
+
"search",
|
|
250
|
+
"_count",
|
|
251
|
+
"_distinct",
|
|
252
|
+
"_exists"
|
|
253
|
+
]);
|
|
254
|
+
for (const [key, value] of Object.entries(query)) {
|
|
255
|
+
if (reserved.has(key)) continue;
|
|
256
|
+
if (value === void 0 || value === "") continue;
|
|
257
|
+
out[key] = expandShorthandOperators(value);
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Map a kit-thrown error to the framework-agnostic execute response.
|
|
263
|
+
* Detects two well-known signals:
|
|
264
|
+
* - "unsupported" / "not implemented" → 501 with upgrade hint
|
|
265
|
+
* - timeout markers → 504
|
|
266
|
+
* - everything else → 500
|
|
267
|
+
*/
|
|
268
|
+
function mapAggregateError(err, aggregationName) {
|
|
269
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
270
|
+
const lower = message.toLowerCase();
|
|
271
|
+
if (lower.includes("unsupported") || lower.includes("not implemented")) return {
|
|
272
|
+
status: 501,
|
|
273
|
+
body: {
|
|
274
|
+
code: "arc.adapter.capability_required",
|
|
275
|
+
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.`,
|
|
276
|
+
status: 501,
|
|
277
|
+
meta: { aggregation: aggregationName }
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
if (lower.includes("maxtimems") || lower.includes("timeout") || lower.includes("timed out")) return {
|
|
281
|
+
status: 504,
|
|
282
|
+
body: {
|
|
283
|
+
code: "arc.gateway_timeout",
|
|
284
|
+
message: `Aggregation "${aggregationName}" timed out: ${message}. Narrow the filter or raise the timeout.`,
|
|
285
|
+
status: 504,
|
|
286
|
+
meta: { aggregation: aggregationName }
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
return {
|
|
290
|
+
status: 500,
|
|
291
|
+
body: {
|
|
292
|
+
code: "arc.internal_error",
|
|
293
|
+
message: `Aggregation "${aggregationName}" failed: ${message}`,
|
|
294
|
+
status: 500,
|
|
295
|
+
meta: { aggregation: aggregationName }
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
//#endregion
|
|
300
|
+
export { executeAggregation as n, buildAggregationHandler as t };
|
package/dist/cache/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as CacheStats, r as CacheStore, t as CacheLogger } from "../interface-
|
|
2
|
-
import { a as QueryCacheConfig, i as QueryCache, n as CacheResult, r as CacheStatus, t as CacheEnvelope } from "../QueryCache-
|
|
3
|
-
import { i as queryCachePlugin, n as QueryCacheDefaults, r as QueryCachePluginOptions, t as CrossResourceRule } from "../queryCachePlugin-
|
|
1
|
+
import { n as CacheStats, r as CacheStore, t as CacheLogger } from "../interface-CH0OQudo.mjs";
|
|
2
|
+
import { a as QueryCacheConfig, i as QueryCache, n as CacheResult, r as CacheStatus, t as CacheEnvelope } from "../QueryCache-SvmT_9ti.mjs";
|
|
3
|
+
import { i as queryCachePlugin, n as QueryCacheDefaults, r as QueryCachePluginOptions, t as CrossResourceRule } from "../queryCachePlugin-Biqzfbi5.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/cache/keys.d.ts
|
|
6
6
|
/**
|
package/dist/cache/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as MemoryCacheStore } from "../memory-
|
|
2
|
-
import { i as versionKey, n as hashParams, r as tagVersionKey, t as buildQueryKey } from "../keys-
|
|
3
|
-
import { r as QueryCache, t as queryCachePlugin } from "../queryCachePlugin-
|
|
1
|
+
import { t as MemoryCacheStore } from "../memory-QOLe11D5.mjs";
|
|
2
|
+
import { i as versionKey, n as hashParams, r as tagVersionKey, t as buildQueryKey } from "../keys-DopsCuyQ.mjs";
|
|
3
|
+
import { r as QueryCache, t as queryCachePlugin } from "../queryCachePlugin-B4XMSSe7.mjs";
|
|
4
4
|
//#region src/cache/redis.ts
|
|
5
5
|
/**
|
|
6
6
|
* Redis-backed cache store.
|
|
@@ -21,7 +21,7 @@ const cachingPlugin = async (fastify, opts = {}) => {
|
|
|
21
21
|
const methodSet = new Set(methods.map((m) => m.toUpperCase()));
|
|
22
22
|
/** Find the first matching rule for a URL path */
|
|
23
23
|
function findRule(url) {
|
|
24
|
-
const path = url.split("?")[0];
|
|
24
|
+
const path = url.split("?")[0] ?? url;
|
|
25
25
|
return rules.find((r) => path.startsWith(r.match));
|
|
26
26
|
}
|
|
27
27
|
/** Build Cache-Control header value */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { V as ResourceDefinition,
|
|
1
|
+
import { V as ResourceDefinition, it as RateLimitConfig, mt as RouteSchemaOptions } from "../../index-CkW0flkU.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/cli/commands/describe.d.ts
|
|
4
4
|
interface DescribedResource {
|
|
@@ -32,12 +32,46 @@ interface DescribedResource {
|
|
|
32
32
|
schemaOptions?: RouteSchemaOptions;
|
|
33
33
|
rateLimit?: RateLimitConfig | false;
|
|
34
34
|
middlewares: string[];
|
|
35
|
+
/**
|
|
36
|
+
* 2.15.5+: multi-tenant + cascade metadata. Surfaced so audit scripts
|
|
37
|
+
* answering "what cascades on org-delete?" or "what scopes by org?"
|
|
38
|
+
* read from one source instead of grepping resource files. Mirrors
|
|
39
|
+
* the registry projection — same shape consumed by
|
|
40
|
+
* `getCascadingResources()` and `assertNoTenantData()`.
|
|
41
|
+
*/
|
|
42
|
+
tenancy: {
|
|
43
|
+
tenantField: string | false | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Resolved purge strategy — what `cascadeDeleteForOrganization`
|
|
46
|
+
* actually runs (`hard` / `soft` / `anonymize` / `skip`) plus where
|
|
47
|
+
* the rule came from (`declared` / `inferred-soft` / `inferred-hard`
|
|
48
|
+
* / `disabled`).
|
|
49
|
+
*/
|
|
50
|
+
purgeStrategy?: {
|
|
51
|
+
type: string;
|
|
52
|
+
source: string;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* 2.16: configured primary-key field. Defaults to `_id`; hosts pin it
|
|
57
|
+
* to a domain handle (`slug`, `reportId`, `sku`) for non-Mongo schemas
|
|
58
|
+
* — the CLI surfaces it so generated code / tooling can compose
|
|
59
|
+
* `findOne` filters via the right key.
|
|
60
|
+
*/
|
|
61
|
+
idField: string;
|
|
35
62
|
}
|
|
36
63
|
interface ActionDescription {
|
|
37
64
|
name: string;
|
|
38
65
|
description?: string;
|
|
39
66
|
hasSchema: boolean;
|
|
40
67
|
permission: PermissionDescription;
|
|
68
|
+
/**
|
|
69
|
+
* 2.15.5: mount preference — `true` (default) for `POST /<prefix>/:id/action`,
|
|
70
|
+
* `false` for `POST /<prefix>/action` (resource-root, no `:id`).
|
|
71
|
+
*/
|
|
72
|
+
requiresId: boolean;
|
|
73
|
+
/** Mount-point URL suffix derived from `requiresId`. */
|
|
74
|
+
mount: "/:id/action" | "/action";
|
|
41
75
|
}
|
|
42
76
|
interface AggregationDescription {
|
|
43
77
|
name: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as CRUD_OPERATIONS } from "../../constants-
|
|
2
|
-
import { n as stringifyMeasureMap } from "../../ResourceRegistry-
|
|
1
|
+
import { t as CRUD_OPERATIONS } from "../../constants-TrJVIJl0.mjs";
|
|
2
|
+
import { n as stringifyMeasureMap } from "../../ResourceRegistry-f48hFk3m.mjs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
//#region src/cli/commands/describe.ts
|
|
@@ -129,13 +129,40 @@ function describeRoutes(resource) {
|
|
|
129
129
|
routes.push(route);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
-
for (const ar of resource.routes ?? [])
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
132
|
+
for (const ar of resource.routes ?? []) {
|
|
133
|
+
const arWithRef = ar;
|
|
134
|
+
let operation;
|
|
135
|
+
if (typeof ar.handler === "string") operation = ar.handler;
|
|
136
|
+
else if (typeof arWithRef.controllerMethod === "function") operation = ar.operation ?? arWithRef.controllerMethod.name ?? "controllerMethod";
|
|
137
|
+
else operation = ar.operation ?? "custom";
|
|
138
|
+
routes.push({
|
|
139
|
+
method: ar.method,
|
|
140
|
+
path: `${resource.prefix}${ar.path}`,
|
|
141
|
+
operation,
|
|
142
|
+
summary: ar.summary,
|
|
143
|
+
description: ar.description,
|
|
144
|
+
permission: describePermission(ar.permissions)
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (resource.actions && Object.keys(resource.actions).length > 0) {
|
|
148
|
+
const entries = Object.entries(resource.actions);
|
|
149
|
+
const hasIdBound = entries.some(([, e]) => typeof e === "function" || e.id !== false);
|
|
150
|
+
const hasIdLess = entries.some(([, e]) => typeof e !== "function" && e.id === false);
|
|
151
|
+
if (hasIdBound) routes.push({
|
|
152
|
+
method: "POST",
|
|
153
|
+
path: `${resource.prefix}/:id/action`,
|
|
154
|
+
operation: "action"
|
|
155
|
+
});
|
|
156
|
+
if (hasIdLess) routes.push({
|
|
157
|
+
method: "POST",
|
|
158
|
+
path: `${resource.prefix}/action`,
|
|
159
|
+
operation: "action"
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (resource.aggregations) for (const name of Object.keys(resource.aggregations)) routes.push({
|
|
163
|
+
method: "GET",
|
|
164
|
+
path: `${resource.prefix}/aggregations/${name}`,
|
|
165
|
+
operation: `aggregation:${name}`
|
|
139
166
|
});
|
|
140
167
|
return routes;
|
|
141
168
|
}
|
|
@@ -160,13 +187,18 @@ function describeActions(actions, fallback) {
|
|
|
160
187
|
if (typeof entry === "function") return {
|
|
161
188
|
name,
|
|
162
189
|
hasSchema: false,
|
|
163
|
-
permission: describePermission(fallback)
|
|
190
|
+
permission: describePermission(fallback),
|
|
191
|
+
requiresId: true,
|
|
192
|
+
mount: "/:id/action"
|
|
164
193
|
};
|
|
194
|
+
const requiresId = entry.id !== false;
|
|
165
195
|
return {
|
|
166
196
|
name,
|
|
167
197
|
description: entry.description,
|
|
168
198
|
hasSchema: !!entry.schema,
|
|
169
|
-
permission: describePermission(entry.permissions ?? fallback)
|
|
199
|
+
permission: describePermission(entry.permissions ?? fallback),
|
|
200
|
+
requiresId,
|
|
201
|
+
mount: requiresId ? "/:id/action" : "/action"
|
|
170
202
|
};
|
|
171
203
|
});
|
|
172
204
|
}
|
|
@@ -224,7 +256,15 @@ function describeResource(resource, module) {
|
|
|
224
256
|
aggregations: describeAggregations(resource.aggregations),
|
|
225
257
|
schemaOptions: Object.keys(resource.schemaOptions ?? {}).length > 0 ? resource.schemaOptions : void 0,
|
|
226
258
|
rateLimit: resource.rateLimit,
|
|
227
|
-
middlewares: describeMiddlewares(resource.middlewares)
|
|
259
|
+
middlewares: describeMiddlewares(resource.middlewares),
|
|
260
|
+
tenancy: {
|
|
261
|
+
tenantField: resource.tenantField,
|
|
262
|
+
...resource.resolvedTenantPurge ? { purgeStrategy: {
|
|
263
|
+
type: resource.resolvedTenantPurge.strategy.type,
|
|
264
|
+
source: resource.resolvedTenantPurge.source
|
|
265
|
+
} } : {}
|
|
266
|
+
},
|
|
267
|
+
idField: resource.idField ?? "_id"
|
|
228
268
|
};
|
|
229
269
|
}
|
|
230
270
|
async function describe(args) {
|
|
@@ -6,8 +6,5 @@
|
|
|
6
6
|
* Requires an entry file that exports defineResource() results.
|
|
7
7
|
*/
|
|
8
8
|
declare function exportDocs(args: string[]): Promise<void>;
|
|
9
|
-
declare const _default: {
|
|
10
|
-
exportDocs: typeof exportDocs;
|
|
11
|
-
};
|
|
12
9
|
//#endregion
|
|
13
|
-
export {
|
|
10
|
+
export { exportDocs };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
import { t as
|
|
1
|
+
import { t as buildOpenApiSpec } from "../../openapi-34T9yNwd.mjs";
|
|
2
|
+
import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-BLMEI2Xa.mjs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
4
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
6
5
|
//#region src/cli/commands/docs.ts
|
|
7
6
|
/**
|
|
@@ -21,17 +20,7 @@ async function exportDocs(args) {
|
|
|
21
20
|
const { entryPath, outputPath } = parseDocsArgs(args);
|
|
22
21
|
console.log("Exporting OpenAPI specification...\n");
|
|
23
22
|
if (!entryPath) throw new Error("Missing entry file.\n\nUsage: arc docs <entry-file> [output.json]\nExample: arc docs ./src/resources.js ./openapi.json");
|
|
24
|
-
const
|
|
25
|
-
const registry = new ResourceRegistry();
|
|
26
|
-
let registered = 0;
|
|
27
|
-
function tryRegister(value) {
|
|
28
|
-
if (value && typeof value === "object" && "name" in value && "_registryMeta" in value && "toPlugin" in value) {
|
|
29
|
-
registry.register(value, value._registryMeta ?? {});
|
|
30
|
-
registered++;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
for (const exported of Object.values(entryModule)) if (Array.isArray(exported)) exported.forEach(tryRegister);
|
|
34
|
-
else tryRegister(exported);
|
|
23
|
+
const { registry, registered } = await loadResourcesFromEntry(entryPath);
|
|
35
24
|
if (registered === 0) throw new Error("No resource definitions found in entry file.\nMake sure your file exports defineResource() results:\n export const productResource = defineResource({ ... });");
|
|
36
25
|
const resources = registry.getAll();
|
|
37
26
|
const spec = buildOpenApiSpec(resources, {
|
|
@@ -46,6 +35,5 @@ async function exportDocs(args) {
|
|
|
46
35
|
console.log(`\nResources included: ${resources.length}`);
|
|
47
36
|
console.log(`Total endpoints: ${Object.keys(spec.paths).length}`);
|
|
48
37
|
}
|
|
49
|
-
var docs_default = { exportDocs };
|
|
50
38
|
//#endregion
|
|
51
|
-
export {
|
|
39
|
+
export { exportDocs };
|
|
@@ -1,20 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Arc CLI - Generate Command
|
|
4
|
-
*
|
|
5
|
-
* Scaffolds resources with consistent naming:
|
|
6
|
-
* - src/resources/product/product.model.ts
|
|
7
|
-
* - src/resources/product/product.repository.ts
|
|
8
|
-
* - src/resources/product/product.resource.ts
|
|
9
|
-
*
|
|
10
|
-
* Handles kebab-case names: `arc g r org-profile` generates:
|
|
11
|
-
* - Class names: OrgProfile, OrgProfileRepository
|
|
12
|
-
* - Variable names: orgProfileSchema, orgProfileRepository
|
|
13
|
-
* - File names: org-profile.model.ts, org-profile.repository.ts
|
|
14
|
-
*/
|
|
15
|
-
/**
|
|
16
|
-
* Generate command handler
|
|
17
|
-
*/
|
|
18
|
-
declare function generate(type: string | undefined, args: string[]): Promise<void>;
|
|
19
|
-
//#endregion
|
|
20
|
-
export { generate };
|
|
1
|
+
import { n as generate, t as ArcProjectConfig } from "../../generate-BWFwgcCM.mjs";
|
|
2
|
+
export { ArcProjectConfig, generate };
|