@classytic/arc 2.22.0 → 2.25.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/dist/{BaseController-C6FlRWxF.mjs → BaseController--M8EMzt0.mjs} +92 -22
- package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
- package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
- package/dist/actionPermissions-CAphcmUG.mjs +24 -0
- package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
- package/dist/audit/index.d.mts +32 -3
- package/dist/audit/index.mjs +7 -4
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +264 -260
- package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
- package/dist/cache/index.d.mts +41 -3
- package/dist/cache/index.mjs +33 -4
- 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.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/cli/index.mjs +2 -2
- package/dist/context/index.mjs +2 -64
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -4
- package/dist/{core-b6fgFgnd.mjs → core-EUNu0u07.mjs} +298 -12
- package/dist/{createActionRouter-h1cEEaEX.mjs → createActionRouter-B6GvAuAX.mjs} +4 -3
- package/dist/{createAggregationRouter-B1C0kvTL.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
- package/dist/{errorHandler-BTzXSwIg.mjs → errorHandler-Bq2OeEQL.mjs} +10 -7
- package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
- package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
- package/dist/events/index.d.mts +57 -314
- package/dist/events/index.mjs +168 -93
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +24 -2
- package/dist/factory/index.d.mts +21 -6
- package/dist/factory/index.mjs +58 -18
- package/dist/{fastify-augmentation-9_fuetB4.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
- package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
- package/dist/headers-BvlQSDMB.mjs +45 -0
- package/dist/health-CB_m4S32.mjs +377 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +7 -0
- package/dist/idempotency/index.mjs +32 -20
- package/dist/{index-ogIHvOjo.d.mts → index-BMnbzvtO.d.mts} +70 -4
- package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
- package/dist/{index-DbtgCOCj.d.mts → index-DPRH0Cme.d.mts} +46 -94
- package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
- package/dist/{index-D8MKYGDa.d.mts → index-f4PTrk3B.d.mts} +37 -5
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +6 -5
- package/dist/{init-DleK3p9f.mjs → init-tlRSt667.mjs} +206 -205
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +3 -3
- package/dist/integrations/jobs/index.d.mts +2 -0
- package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
- package/dist/integrations/mcp/index.d.mts +68 -5
- package/dist/integrations/mcp/index.mjs +2 -2
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +149 -22
- package/dist/integrations/webhooks.mjs +186 -42
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +41 -14
- package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
- package/dist/interface-n5hpKg1d.mjs +16 -0
- package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
- package/dist/{mcpPlugin-ByA2tf6D.mjs → mcpPlugin-Cb7olw0j.mjs} +316 -45
- package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
- package/dist/middleware/index.d.mts +17 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/migrations/index.d.mts +94 -44
- package/dist/migrations/index.mjs +196 -44
- package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
- package/dist/{openapi-BRpMZD7C.mjs → openapi-DrLtog_B.mjs} +4 -4
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +4 -3
- package/dist/{permissions-BJMuKMl7.mjs → permissions-BrtiBKGs.mjs} +305 -492
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/{actionPermissions-ze70xXJi.mjs → pipeline-BN5G05rd.mjs} +4 -27
- package/dist/plugins/index.d.mts +7 -78
- package/dist/plugins/index.mjs +14 -97
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{index-BEu4wjbL.d.mts → preloadResources-0oe-JSSS.d.mts} +59 -50
- package/dist/{createApp-Ci7yG5gv.mjs → preloadResources-DfyngIN9.mjs} +240 -68
- package/dist/presets/filesUpload.d.mts +3 -3
- package/dist/presets/filesUpload.mjs +3 -3
- package/dist/presets/index.d.mts +2 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-gw0ciIUD.mjs → presets-DwFDVKP0.mjs} +2 -2
- package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
- package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
- package/dist/{registerRoute-xHc42fwB.mjs → rateLimit-DrED-C4v.mjs} +2 -109
- package/dist/realtime-Cuw_deA-.mjs +375 -0
- package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
- package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/renewingLease-3cuQH7h8.mjs +44 -0
- package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
- package/dist/schedules-CjOd2MG9.mjs +113 -0
- package/dist/{schemaConverter-Ds5reSGs.mjs → schemaConverter-DJvodiHj.mjs} +4 -1
- package/dist/{schemaIR-B-6SPe7g.mjs → schemaIR-Ca7ySNPx.mjs} +1 -1
- package/dist/schemas/index.d.mts +67 -67
- package/dist/schemas/index.mjs +2 -3
- package/dist/scim/index.mjs +1 -2
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +4 -4
- package/dist/scope-DfaPntpo.mjs +36 -0
- package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
- package/dist/{sse-CfUjwjDk.mjs → sse-BQ1w0dfl.mjs} +8 -31
- package/dist/sseStream-BQ7Qaynp.mjs +57 -0
- package/dist/testing/index.d.mts +4 -3
- package/dist/testing/index.mjs +2 -2
- package/dist/transactionContext-PNdoyLLm.mjs +64 -0
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
- package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
- package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
- package/dist/{types-Bc42CUyk.d.mts → types-QFGc3Cjv.d.mts} +37 -2
- package/dist/usage/index.d.mts +3 -52
- package/dist/usage/index.mjs +0 -0
- package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
- package/dist/utils/index.d.mts +90 -57
- package/dist/utils/index.mjs +7 -3
- package/dist/{utils-FhJ88TJE.mjs → utils-taUVD4p3.mjs} +47 -385
- package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
- package/dist/{versioning-DLwklPlG.d.mts → versioning-DXgY5BUm.d.mts} +52 -31
- package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
- package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
- package/package.json +526 -453
- package/skills/arc/references/api-reference.md +3 -1
- package/skills/arc/references/integrations.md +20 -11
- package/skills/arc/references/production.md +734 -696
- package/dist/QueryCache-Z1B38koh.d.mts +0 -40
- package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
- package/dist/health-ScyjIVo5.mjs +0 -202
- package/dist/requestId-5BhTVzBv.mjs +0 -113
- package/dist/scope-k15RXfcq.mjs +0 -64
- package/dist/types-XTApa8b6.mjs +0 -24
|
@@ -1,358 +1,8 @@
|
|
|
1
|
-
import { m as RESERVED_QUERY_PARAMS
|
|
1
|
+
import { m as RESERVED_QUERY_PARAMS } from "./constants-DNre0wRg.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
3
|
import { t as ArcError } from "./errors-DkO8Nk6-.mjs";
|
|
4
|
-
import { r as getAvailablePresets } from "./presets-gw0ciIUD.mjs";
|
|
5
4
|
import { errorContractSchema as errorContractSchema$1, errorDetailSchema as errorDetailSchema$1 } from "@classytic/repo-core/errors";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* `simpleEqualityMatcher` — a minimal, dialect-agnostic flat-key equality
|
|
9
|
-
* matcher for `DataAdapter.matchesFilter` / `BaseController({ matchesFilter })`.
|
|
10
|
-
*
|
|
11
|
-
* **What it does:** for each `[key, expected]` in the filter, compares
|
|
12
|
-
* `item[key]` to `expected` via string coercion (so Mongo `ObjectId` values
|
|
13
|
-
* match their string representation) and returns `true` only if every
|
|
14
|
-
* filter entry matches. Array item values are matched implicitly (contains).
|
|
15
|
-
*
|
|
16
|
-
* **What it does NOT do:**
|
|
17
|
-
* - No `$eq` / `$ne` / `$in` / `$nin` / `$gt` / `$lt` / `$regex` / `$exists`
|
|
18
|
-
* - No `$and` / `$or`
|
|
19
|
-
* - No dot-path traversal (`"owner.id"`)
|
|
20
|
-
* - No schema-specific coercion
|
|
21
|
-
*
|
|
22
|
-
* **Why it exists:** 95%+ of arc's `_policyFilters` are produced by built-in
|
|
23
|
-
* permission helpers and are shaped like `{ ownerId: "u1" }` or
|
|
24
|
-
* `{ organizationId: "org_x" }` — flat equality. For that common shape,
|
|
25
|
-
* this helper is a safe, tested, 15-line defense-in-depth matcher that
|
|
26
|
-
* hosts using minimal repos (no `getOne(compoundFilter)` DB path) can opt
|
|
27
|
-
* into without arc shipping a full Mongo-syntax engine.
|
|
28
|
-
*
|
|
29
|
-
* **When to use:**
|
|
30
|
-
* - Your adapter/repo doesn't natively filter on `getOne(compoundFilter)`
|
|
31
|
-
* - Your `_policyFilters` are flat equality (from arc's built-in permission helpers)
|
|
32
|
-
* - You want defense-in-depth on `validateItemAccess` / `fetchDetailed`'s `getById` fallback
|
|
33
|
-
*
|
|
34
|
-
* **When NOT to use:**
|
|
35
|
-
* - Your `_policyFilters` use operators (`$in`, `$ne`, etc.) — supply a
|
|
36
|
-
* native matcher (mongokit's repo does the filter at the DB layer; for
|
|
37
|
-
* custom repos, wrap the kit's own predicate engine).
|
|
38
|
-
* - You're a mongokit / sqlitekit / Prisma user — the DB-level filter
|
|
39
|
-
* applied by `getOne(compoundFilter)` already covers this.
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* ```ts
|
|
43
|
-
* import { simpleEqualityMatcher } from '@classytic/arc/utils';
|
|
44
|
-
*
|
|
45
|
-
* // On a custom adapter
|
|
46
|
-
* const adapter: DataAdapter = {
|
|
47
|
-
* repository,
|
|
48
|
-
* type: 'custom',
|
|
49
|
-
* name: 'in-memory',
|
|
50
|
-
* matchesFilter: simpleEqualityMatcher,
|
|
51
|
-
* };
|
|
52
|
-
*
|
|
53
|
-
* // Or directly on BaseController for ad-hoc controllers
|
|
54
|
-
* new BaseController(repo, { matchesFilter: simpleEqualityMatcher });
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
function simpleEqualityMatcher(item, filters) {
|
|
58
|
-
if (!item || typeof item !== "object") return false;
|
|
59
|
-
const obj = item;
|
|
60
|
-
for (const [key, expected] of Object.entries(filters)) {
|
|
61
|
-
if (expected && typeof expected === "object" && !Array.isArray(expected) && Object.getPrototypeOf(expected) === Object.prototype && Object.keys(expected).some((k) => k.startsWith("$"))) return false;
|
|
62
|
-
const actual = obj[key];
|
|
63
|
-
if (Array.isArray(actual)) {
|
|
64
|
-
const expectedStr = String(expected);
|
|
65
|
-
if (!actual.some((v) => String(v) === expectedStr)) return false;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (String(actual) !== String(expected)) return false;
|
|
69
|
-
}
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
//#endregion
|
|
73
|
-
//#region src/core/validateResourceConfig.ts
|
|
74
|
-
/**
|
|
75
|
-
* Resource Configuration Validator
|
|
76
|
-
*
|
|
77
|
-
* Fail-fast validation at definition time.
|
|
78
|
-
* Invalid configs throw immediately with clear, actionable errors.
|
|
79
|
-
*
|
|
80
|
-
* @example
|
|
81
|
-
* const result = validateResourceConfig(config);
|
|
82
|
-
* if (!result.valid) {
|
|
83
|
-
* console.error(formatValidationErrors(result.errors));
|
|
84
|
-
* }
|
|
85
|
-
*/
|
|
86
|
-
/**
|
|
87
|
-
* Validate a resource configuration
|
|
88
|
-
*/
|
|
89
|
-
function validateResourceConfig(config, options = {}) {
|
|
90
|
-
const errors = [];
|
|
91
|
-
const warnings = [];
|
|
92
|
-
if (!config.name) errors.push({
|
|
93
|
-
field: "name",
|
|
94
|
-
message: "Resource name is required",
|
|
95
|
-
suggestion: "Add a unique resource name (e.g., \"product\", \"user\")"
|
|
96
|
-
});
|
|
97
|
-
else if (!/^[a-z][a-z0-9-]*$/i.test(config.name)) errors.push({
|
|
98
|
-
field: "name",
|
|
99
|
-
message: `Invalid resource name "${config.name}"`,
|
|
100
|
-
suggestion: "Use alphanumeric characters and hyphens, starting with a letter"
|
|
101
|
-
});
|
|
102
|
-
const crudRoutes = CRUD_OPERATIONS;
|
|
103
|
-
const disabledRoutes = new Set(config.disabledRoutes ?? []);
|
|
104
|
-
const enabledCrudRoutes = crudRoutes.filter((route) => !disabledRoutes.has(route));
|
|
105
|
-
if (!config.disableDefaultRoutes && enabledCrudRoutes.length > 0) {
|
|
106
|
-
if (!config.adapter) errors.push({
|
|
107
|
-
field: "adapter",
|
|
108
|
-
message: "Data adapter is required when CRUD routes are enabled",
|
|
109
|
-
suggestion: "Provide an adapter: createMongooseAdapter({ model, repository })"
|
|
110
|
-
});
|
|
111
|
-
else if (!config.adapter.repository) errors.push({
|
|
112
|
-
field: "adapter.repository",
|
|
113
|
-
message: "Adapter must provide a repository",
|
|
114
|
-
suggestion: "Ensure your adapter returns a valid StandardRepo (see @classytic/repo-core)"
|
|
115
|
-
});
|
|
116
|
-
} else if (!config.adapter && !config.routes?.length) warnings.push({
|
|
117
|
-
field: "config",
|
|
118
|
-
message: "Resource has no adapter and no routes",
|
|
119
|
-
suggestion: "Provide either adapter for CRUD or routes for custom logic"
|
|
120
|
-
});
|
|
121
|
-
if (config.controller && !options.skipControllerCheck && !config.disableDefaultRoutes) {
|
|
122
|
-
const ctrl = config.controller;
|
|
123
|
-
const requiredMethods = CRUD_OPERATIONS;
|
|
124
|
-
for (const method of requiredMethods) if (typeof ctrl[method] !== "function") errors.push({
|
|
125
|
-
field: `controller.${method}`,
|
|
126
|
-
message: `Missing required CRUD method "${method}"`,
|
|
127
|
-
suggestion: "Extend BaseController which implements IController interface"
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
if (config.controller && config.routes) validateRouteHandlers(config.controller, config.routes, errors);
|
|
131
|
-
if (config.permissions) validatePermissionKeys(config, options, errors, warnings);
|
|
132
|
-
if (config.presets && !options.allowUnknownPresets) validatePresets(config.presets, errors, warnings);
|
|
133
|
-
if (config.prefix) {
|
|
134
|
-
if (!config.prefix.startsWith("/")) errors.push({
|
|
135
|
-
field: "prefix",
|
|
136
|
-
message: `Prefix must start with "/" (got "${config.prefix}")`,
|
|
137
|
-
suggestion: `Change to "/${config.prefix}"`
|
|
138
|
-
});
|
|
139
|
-
if (config.prefix.endsWith("/") && config.prefix !== "/") warnings.push({
|
|
140
|
-
field: "prefix",
|
|
141
|
-
message: `Prefix should not end with "/" (got "${config.prefix}")`,
|
|
142
|
-
suggestion: `Change to "${config.prefix.slice(0, -1)}"`
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
if (config.routes) {
|
|
146
|
-
validateRoutes(config.routes, errors);
|
|
147
|
-
validateRouteCrudCollisions(config, errors);
|
|
148
|
-
}
|
|
149
|
-
return {
|
|
150
|
-
valid: errors.length === 0,
|
|
151
|
-
errors,
|
|
152
|
-
warnings
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
function validateRouteHandlers(controller, routes, errors) {
|
|
156
|
-
const ctrl = controller;
|
|
157
|
-
for (const route of routes) if (typeof route.handler === "string") {
|
|
158
|
-
if (typeof ctrl[route.handler] !== "function") errors.push({
|
|
159
|
-
field: `routes[${route.method} ${route.path}]`,
|
|
160
|
-
message: `Handler "${route.handler}" not found on controller`,
|
|
161
|
-
suggestion: `Add method "${route.handler}" to controller or use a function handler`
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
function validatePermissionKeys(config, options, _errors, warnings) {
|
|
166
|
-
const validKeys = /* @__PURE__ */ new Set([...CRUD_OPERATIONS, ...options.additionalPermissionKeys ?? []]);
|
|
167
|
-
for (const route of config.routes ?? []) if (typeof route.handler === "string") validKeys.add(route.handler);
|
|
168
|
-
for (const preset of config.presets ?? []) {
|
|
169
|
-
const presetName = typeof preset === "string" ? preset : preset.name;
|
|
170
|
-
if (presetName === "softDelete") {
|
|
171
|
-
validKeys.add("deleted");
|
|
172
|
-
validKeys.add("restore");
|
|
173
|
-
}
|
|
174
|
-
if (presetName === "slugLookup") validKeys.add("getBySlug");
|
|
175
|
-
if (presetName === "tree") {
|
|
176
|
-
validKeys.add("tree");
|
|
177
|
-
validKeys.add("children");
|
|
178
|
-
validKeys.add("getTree");
|
|
179
|
-
validKeys.add("getChildren");
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
for (const key of Object.keys(config.permissions ?? {})) if (!validKeys.has(key)) warnings.push({
|
|
183
|
-
field: `permissions.${key}`,
|
|
184
|
-
message: `Unknown permission key "${key}"`,
|
|
185
|
-
suggestion: `Valid keys: ${Array.from(validKeys).join(", ")}`
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
function validatePresets(presets, errors, warnings) {
|
|
189
|
-
const availablePresets = getAvailablePresets();
|
|
190
|
-
for (const preset of presets) {
|
|
191
|
-
if (typeof preset === "object" && ("middlewares" in preset || "routes" in preset)) continue;
|
|
192
|
-
const presetName = typeof preset === "string" ? preset : preset.name;
|
|
193
|
-
if (!availablePresets.includes(presetName)) errors.push({
|
|
194
|
-
field: "presets",
|
|
195
|
-
message: `Unknown preset "${presetName}"`,
|
|
196
|
-
suggestion: `Available presets: ${availablePresets.join(", ")}`
|
|
197
|
-
});
|
|
198
|
-
if (typeof preset === "object") validatePresetOptions(preset, warnings);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
function validatePresetOptions(preset, warnings) {
|
|
202
|
-
const validOptions = {
|
|
203
|
-
slugLookup: ["slugField"],
|
|
204
|
-
tree: ["parentField"],
|
|
205
|
-
softDelete: ["deletedField"],
|
|
206
|
-
ownedByUser: ["ownerField"],
|
|
207
|
-
multiTenant: ["tenantField", "allowPublic"]
|
|
208
|
-
}[preset.name] ?? [];
|
|
209
|
-
const providedOptions = Object.keys(preset).filter((k) => k !== "name");
|
|
210
|
-
for (const opt of providedOptions) if (!validOptions.includes(opt)) warnings.push({
|
|
211
|
-
field: `presets[${preset.name}].${opt}`,
|
|
212
|
-
message: `Unknown option "${opt}" for preset "${preset.name}"`,
|
|
213
|
-
suggestion: validOptions.length > 0 ? `Valid options: ${validOptions.join(", ")}` : `Preset "${preset.name}" has no configurable options`
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Detect collisions between user-declared `routes:` entries and the
|
|
218
|
-
* auto-CRUD route table that `createCrudRouter` will register.
|
|
219
|
-
*
|
|
220
|
-
* Without this check, a custom `POST /` on the same prefix as auto-CRUD's
|
|
221
|
-
* `create` boots fine through `defineResource()` and only blows up at
|
|
222
|
-
* `app.register()` time with Fastify's opaque `FST_ERR_DUPLICATED_ROUTE`.
|
|
223
|
-
* That error doesn't mention `disabledRoutes` and doesn't distinguish
|
|
224
|
-
* "you collided with auto-CRUD" from "you collided with yourself" — so
|
|
225
|
-
* the consumer has to grep arc's source to discover the fix.
|
|
226
|
-
*
|
|
227
|
-
* We surface the fix in the error message, naming the conflicting op
|
|
228
|
-
* and the literal line to add.
|
|
229
|
-
*
|
|
230
|
-
* Honored config:
|
|
231
|
-
* - `disableDefaultRoutes: true` → no auto-CRUD, no collisions possible
|
|
232
|
-
* - `disabledRoutes: ['create']` → that op's path is free for custom use
|
|
233
|
-
* - `updateMethod: 'PUT' | 'both'` → update slot covers PUT/PATCH accordingly
|
|
234
|
-
*/
|
|
235
|
-
function validateRouteCrudCollisions(config, errors) {
|
|
236
|
-
if (config.disableDefaultRoutes) return;
|
|
237
|
-
if (!config.routes || config.routes.length === 0) return;
|
|
238
|
-
const updateMethod = config.updateMethod ?? "PATCH";
|
|
239
|
-
const updateMethods = updateMethod === "both" ? ["PUT", "PATCH"] : [updateMethod];
|
|
240
|
-
const disabled = new Set(config.disabledRoutes ?? []);
|
|
241
|
-
const crudTable = [
|
|
242
|
-
{
|
|
243
|
-
op: "list",
|
|
244
|
-
method: "GET",
|
|
245
|
-
path: "/"
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
op: "get",
|
|
249
|
-
method: "GET",
|
|
250
|
-
path: "/:id"
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
op: "create",
|
|
254
|
-
method: "POST",
|
|
255
|
-
path: "/"
|
|
256
|
-
},
|
|
257
|
-
...updateMethods.map((m) => ({
|
|
258
|
-
op: "update",
|
|
259
|
-
method: m,
|
|
260
|
-
path: "/:id"
|
|
261
|
-
})),
|
|
262
|
-
{
|
|
263
|
-
op: "delete",
|
|
264
|
-
method: "DELETE",
|
|
265
|
-
path: "/:id"
|
|
266
|
-
}
|
|
267
|
-
];
|
|
268
|
-
for (const [i, route] of config.routes.entries()) {
|
|
269
|
-
if (!route.method || !route.path) continue;
|
|
270
|
-
for (const crud of crudTable) {
|
|
271
|
-
if (disabled.has(crud.op)) continue;
|
|
272
|
-
if (route.method !== crud.method || route.path !== crud.path) continue;
|
|
273
|
-
errors.push({
|
|
274
|
-
field: `routes[${i}]`,
|
|
275
|
-
message: `Route ${route.method} ${route.path} collides with auto-CRUD "${crud.op}"`,
|
|
276
|
-
suggestion: `Add \`disabledRoutes: ['${crud.op}']\` to suppress the auto-CRUD route, or move your custom handler to a different path. Set \`disableDefaultRoutes: true\` to disable all auto-CRUD on this resource.`
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
function validateRoutes(routes, errors) {
|
|
282
|
-
const validMethods = [
|
|
283
|
-
"GET",
|
|
284
|
-
"POST",
|
|
285
|
-
"PUT",
|
|
286
|
-
"PATCH",
|
|
287
|
-
"DELETE",
|
|
288
|
-
"OPTIONS",
|
|
289
|
-
"HEAD"
|
|
290
|
-
];
|
|
291
|
-
const seenRoutes = /* @__PURE__ */ new Set();
|
|
292
|
-
for (const [i, route] of routes.entries()) {
|
|
293
|
-
if (!validMethods.includes(route.method)) errors.push({
|
|
294
|
-
field: `routes[${i}].method`,
|
|
295
|
-
message: `Invalid HTTP method "${route.method}"`,
|
|
296
|
-
suggestion: `Valid methods: ${validMethods.join(", ")}`
|
|
297
|
-
});
|
|
298
|
-
if (!route.path) errors.push({
|
|
299
|
-
field: `routes[${i}].path`,
|
|
300
|
-
message: "Route path is required"
|
|
301
|
-
});
|
|
302
|
-
else if (!route.path.startsWith("/")) errors.push({
|
|
303
|
-
field: `routes[${i}].path`,
|
|
304
|
-
message: `Route path must start with "/" (got "${route.path}")`,
|
|
305
|
-
suggestion: `Change to "/${route.path}"`
|
|
306
|
-
});
|
|
307
|
-
const routeWithRefs = route;
|
|
308
|
-
if (!route.handler && typeof routeWithRefs.controllerMethod !== "function") errors.push({
|
|
309
|
-
field: `routes[${i}].handler`,
|
|
310
|
-
message: "Route must declare either `handler` (string / function) or `controllerMethod`",
|
|
311
|
-
suggestion: "Prefer `controllerMethod: (c: MyController) => c.method` for typed handler refs."
|
|
312
|
-
});
|
|
313
|
-
const routeKey = `${route.method} ${route.path}`;
|
|
314
|
-
if (seenRoutes.has(routeKey)) errors.push({
|
|
315
|
-
field: `routes[${i}]`,
|
|
316
|
-
message: `Duplicate route "${routeKey}"`
|
|
317
|
-
});
|
|
318
|
-
seenRoutes.add(routeKey);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* Format validation errors for display
|
|
323
|
-
*/
|
|
324
|
-
function formatValidationErrors(resourceName, result) {
|
|
325
|
-
const lines = [];
|
|
326
|
-
if (result.errors.length > 0) {
|
|
327
|
-
lines.push(`Resource "${resourceName}" validation failed:`);
|
|
328
|
-
lines.push("");
|
|
329
|
-
lines.push("ERRORS:");
|
|
330
|
-
for (const err of result.errors) {
|
|
331
|
-
lines.push(` ✗ ${err.field}: ${err.message}`);
|
|
332
|
-
if (err.suggestion) lines.push(` → ${err.suggestion}`);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
if (result.warnings.length > 0) {
|
|
336
|
-
if (lines.length > 0) lines.push("");
|
|
337
|
-
lines.push("WARNINGS:");
|
|
338
|
-
for (const warn of result.warnings) {
|
|
339
|
-
lines.push(` ⚠ ${warn.field}: ${warn.message}`);
|
|
340
|
-
if (warn.suggestion) lines.push(` → ${warn.suggestion}`);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return lines.join("\n");
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Validate and throw if invalid
|
|
347
|
-
*/
|
|
348
|
-
function assertValidConfig(config, options) {
|
|
349
|
-
const result = validateResourceConfig(config, options);
|
|
350
|
-
if (!result.valid) {
|
|
351
|
-
const errorMsg = formatValidationErrors(config.name ?? "unknown", result);
|
|
352
|
-
throw new Error(errorMsg);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
//#endregion
|
|
5
|
+
import { scheduleBackground } from "@classytic/repo-core/cache";
|
|
356
6
|
//#region src/utils/circuitBreaker.ts
|
|
357
7
|
/**
|
|
358
8
|
* Circuit Breaker Pattern
|
|
@@ -404,10 +54,13 @@ var CircuitBreaker = class {
|
|
|
404
54
|
nextAttempt = 0;
|
|
405
55
|
lastCallAt = null;
|
|
406
56
|
openedAt = null;
|
|
57
|
+
halfOpenInFlight = 0;
|
|
407
58
|
failureThreshold;
|
|
408
59
|
resetTimeout;
|
|
409
60
|
timeout;
|
|
410
61
|
successThreshold;
|
|
62
|
+
halfOpenMaxProbes;
|
|
63
|
+
propagateAbort;
|
|
411
64
|
fallback;
|
|
412
65
|
onStateChange;
|
|
413
66
|
onError;
|
|
@@ -419,6 +72,8 @@ var CircuitBreaker = class {
|
|
|
419
72
|
this.resetTimeout = options.resetTimeout ?? 6e4;
|
|
420
73
|
this.timeout = options.timeout ?? 1e4;
|
|
421
74
|
this.successThreshold = options.successThreshold ?? 1;
|
|
75
|
+
this.halfOpenMaxProbes = options.halfOpenMaxProbes ?? 1;
|
|
76
|
+
this.propagateAbort = options.propagateAbort ?? false;
|
|
422
77
|
this.fallback = options.fallback;
|
|
423
78
|
this.onStateChange = options.onStateChange;
|
|
424
79
|
this.onError = options.onError;
|
|
@@ -438,6 +93,15 @@ var CircuitBreaker = class {
|
|
|
438
93
|
}
|
|
439
94
|
this.setState(CircuitState.HALF_OPEN);
|
|
440
95
|
}
|
|
96
|
+
let isProbe = false;
|
|
97
|
+
if (this.state === CircuitState.HALF_OPEN) {
|
|
98
|
+
if (this.halfOpenInFlight >= this.halfOpenMaxProbes) {
|
|
99
|
+
if (this.fallback) return this.fallback(...args);
|
|
100
|
+
throw new CircuitBreakerError(`Circuit breaker is HALF_OPEN for ${this.name} — probe slots full`, CircuitState.HALF_OPEN);
|
|
101
|
+
}
|
|
102
|
+
this.halfOpenInFlight++;
|
|
103
|
+
isProbe = true;
|
|
104
|
+
}
|
|
441
105
|
try {
|
|
442
106
|
const result = await this.executeWithTimeout(args);
|
|
443
107
|
this.onSuccess();
|
|
@@ -445,17 +109,24 @@ var CircuitBreaker = class {
|
|
|
445
109
|
} catch (err) {
|
|
446
110
|
this.onFailure(err instanceof Error ? err : new Error(String(err)));
|
|
447
111
|
throw err;
|
|
112
|
+
} finally {
|
|
113
|
+
if (isProbe) this.halfOpenInFlight--;
|
|
448
114
|
}
|
|
449
115
|
}
|
|
450
116
|
/**
|
|
451
|
-
* Execute function with timeout
|
|
117
|
+
* Execute function with timeout. With `propagateAbort`, the wrapped fn
|
|
118
|
+
* receives a trailing AbortSignal that fires on timeout — the breaker's
|
|
119
|
+
* timeout otherwise only rejects the CALLER while the underlying work
|
|
120
|
+
* keeps consuming sockets/pool slots to completion.
|
|
452
121
|
*/
|
|
453
122
|
async executeWithTimeout(args) {
|
|
123
|
+
const controller = this.propagateAbort ? new AbortController() : void 0;
|
|
454
124
|
return new Promise((resolve, reject) => {
|
|
455
125
|
const timeoutId = setTimeout(() => {
|
|
126
|
+
controller?.abort(/* @__PURE__ */ new Error(`Request timeout after ${this.timeout}ms`));
|
|
456
127
|
reject(/* @__PURE__ */ new Error(`Request timeout after ${this.timeout}ms`));
|
|
457
128
|
}, this.timeout);
|
|
458
|
-
this.fn(...args).then((result) => {
|
|
129
|
+
(controller ? this.fn(...args, controller.signal) : this.fn(...args)).then((result) => {
|
|
459
130
|
clearTimeout(timeoutId);
|
|
460
131
|
resolve(result);
|
|
461
132
|
}).catch((error) => {
|
|
@@ -638,6 +309,21 @@ function createCircuitBreakerRegistry() {
|
|
|
638
309
|
//#endregion
|
|
639
310
|
//#region src/utils/compensation.ts
|
|
640
311
|
/**
|
|
312
|
+
* Observability hooks must never alter the transaction: a throwing metrics
|
|
313
|
+
* sink must not fail a step that succeeded, trigger compensation, or become
|
|
314
|
+
* an unhandled rejection from a detached fire-and-forget chain. Containment
|
|
315
|
+
* is reported via `hooks.onHookError` so broken sinks stay visible.
|
|
316
|
+
*/
|
|
317
|
+
function safeHook(name, invoke, hooks) {
|
|
318
|
+
try {
|
|
319
|
+
invoke();
|
|
320
|
+
} catch (err) {
|
|
321
|
+
try {
|
|
322
|
+
hooks?.onHookError?.(name, err instanceof Error ? err : new Error(String(err)));
|
|
323
|
+
} catch {}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
641
327
|
* Run steps in order with automatic compensation on failure.
|
|
642
328
|
*
|
|
643
329
|
* @typeParam TCtx - Context type shared across steps (defaults to Record<string, unknown>)
|
|
@@ -649,8 +335,7 @@ async function withCompensation(_name, steps, initialContext, hooks) {
|
|
|
649
335
|
const completed = [];
|
|
650
336
|
for (const step of steps) {
|
|
651
337
|
if (step.fireAndForget) {
|
|
652
|
-
|
|
653
|
-
step.execute(ctx).then((result) => hooks?.onStepComplete?.(step.name, result), () => {});
|
|
338
|
+
step.execute(ctx).then((result) => safeHook("onStepComplete", () => hooks?.onStepComplete?.(step.name, result), hooks), (err) => safeHook("onStepFailed", () => hooks?.onStepFailed?.(step.name, err instanceof Error ? err : new Error(String(err))), hooks)).catch(() => {});
|
|
654
339
|
continue;
|
|
655
340
|
}
|
|
656
341
|
try {
|
|
@@ -661,10 +346,10 @@ async function withCompensation(_name, steps, initialContext, hooks) {
|
|
|
661
346
|
step,
|
|
662
347
|
result
|
|
663
348
|
});
|
|
664
|
-
hooks?.onStepComplete?.(step.name, result);
|
|
349
|
+
safeHook("onStepComplete", () => hooks?.onStepComplete?.(step.name, result), hooks);
|
|
665
350
|
} catch (err) {
|
|
666
351
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
667
|
-
hooks?.onStepFailed?.(step.name, error);
|
|
352
|
+
safeHook("onStepFailed", () => hooks?.onStepFailed?.(step.name, error), hooks);
|
|
668
353
|
const compensationErrors = await rollback(ctx, completed, hooks);
|
|
669
354
|
return {
|
|
670
355
|
success: false,
|
|
@@ -690,7 +375,7 @@ async function rollback(ctx, completed, hooks) {
|
|
|
690
375
|
const compensateFn = entry.step.compensate;
|
|
691
376
|
try {
|
|
692
377
|
await compensateFn(ctx, entry.result);
|
|
693
|
-
hooks?.onCompensate?.(entry.step.name);
|
|
378
|
+
safeHook("onCompensate", () => hooks?.onCompensate?.(entry.step.name), hooks);
|
|
694
379
|
} catch (err) {
|
|
695
380
|
errors.push({
|
|
696
381
|
step: entry.step.name,
|
|
@@ -850,6 +535,8 @@ function handleRaw(handler, statusCode = 200) {
|
|
|
850
535
|
* defineResource({ name: 'product', adapter: ... });
|
|
851
536
|
*
|
|
852
537
|
* // Use MongoKit's QueryParser (recommended for MongoDB - has $lookup, aggregations, etc.)
|
|
538
|
+
* // Fail-closed by default (mongokit >=3.25): invalid/blocked query input
|
|
539
|
+
* // becomes HTTP 400 instead of a silently broadened result set.
|
|
853
540
|
* import { QueryParser } from '@classytic/mongokit';
|
|
854
541
|
* defineResource({
|
|
855
542
|
* name: 'product',
|
|
@@ -1652,13 +1339,6 @@ function getDefaultCrudSchemas() {
|
|
|
1652
1339
|
});
|
|
1653
1340
|
}
|
|
1654
1341
|
//#endregion
|
|
1655
|
-
//#region src/utils/runtime.ts
|
|
1656
|
-
/**
|
|
1657
|
-
* Portable "run on next tick" scheduler. `setImmediate` is Node-only — not
|
|
1658
|
-
* available in Bun workers, Deno, Cloudflare Workers, or edge runtimes.
|
|
1659
|
-
*/
|
|
1660
|
-
const scheduleBackground = typeof setImmediate === "function" ? (cb) => void setImmediate(cb) : (cb) => queueMicrotask(cb);
|
|
1661
|
-
//#endregion
|
|
1662
1342
|
//#region src/utils/stateMachine.ts
|
|
1663
1343
|
/**
|
|
1664
1344
|
* Create a state machine for validating transitions
|
|
@@ -1760,22 +1440,4 @@ function createStateMachine(name, transitions = {}, options = {}) {
|
|
|
1760
1440
|
};
|
|
1761
1441
|
}
|
|
1762
1442
|
//#endregion
|
|
1763
|
-
|
|
1764
|
-
/**
|
|
1765
|
-
* Extract a user ID from a user object. Accepts `id` or `_id` — returns
|
|
1766
|
-
* `undefined` when neither is present. Used by arc's controllers to
|
|
1767
|
-
* populate `createdBy` / `updatedBy` fields and for cache scoping.
|
|
1768
|
-
*
|
|
1769
|
-
* @example
|
|
1770
|
-
* ```ts
|
|
1771
|
-
* import { getUserId } from '@classytic/arc/utils';
|
|
1772
|
-
* const uid = getUserId(request.user);
|
|
1773
|
-
* ```
|
|
1774
|
-
*/
|
|
1775
|
-
function getUserId(user) {
|
|
1776
|
-
if (!user) return void 0;
|
|
1777
|
-
const id = user.id ?? user._id;
|
|
1778
|
-
return id ? String(id) : void 0;
|
|
1779
|
-
}
|
|
1780
|
-
//#endregion
|
|
1781
|
-
export { createCircuitBreaker as A, defineErrorMapper as C, CircuitBreakerError as D, CircuitBreaker as E, simpleEqualityMatcher as F, assertValidConfig as M, formatValidationErrors as N, CircuitBreakerRegistry as O, validateResourceConfig as P, defineGuard as S, withCompensation as T, ArcQueryParser as _, bareListResponse as a, MONGO_DIALECT as b, errorDetailSchema$1 as c, keysetListResponse as d, listResponse as f, responses as g, queryParams as h, aggregateListResponse as i, createCircuitBreakerRegistry as j, CircuitState as k, getDefaultCrudSchemas as l, paginationSchema as m, createStateMachine as n, deleteResponse as o, offsetListResponse as p, scheduleBackground as r, errorContractSchema$1 as s, getUserId as t, getListQueryParams as u, createQueryParser as v, defineCompensation as w, NEUTRAL_DIALECT as x, handleRaw as y };
|
|
1443
|
+
export { createCircuitBreakerRegistry as A, defineCompensation as C, CircuitBreakerRegistry as D, CircuitBreakerError as E, CircuitState as O, defineErrorMapper as S, CircuitBreaker as T, createQueryParser as _, deleteResponse as a, NEUTRAL_DIALECT as b, getDefaultCrudSchemas as c, listResponse as d, offsetListResponse as f, ArcQueryParser as g, responses as h, bareListResponse as i, createCircuitBreaker as k, getListQueryParams as l, queryParams as m, scheduleBackground as n, errorContractSchema$1 as o, paginationSchema as p, aggregateListResponse as r, errorDetailSchema$1 as s, createStateMachine as t, keysetListResponse as u, handleRaw as v, withCompensation as w, defineGuard as x, MONGO_DIALECT as y };
|
|
@@ -123,7 +123,7 @@ function resolveLimitPolicy(config) {
|
|
|
123
123
|
function resolveAggRequestLimit(normalized, query) {
|
|
124
124
|
const policy = normalized.compiled.limitPolicy;
|
|
125
125
|
if (!policy) return normalized.compiled.limit;
|
|
126
|
-
const raw = query
|
|
126
|
+
const raw = query.limit;
|
|
127
127
|
if (raw === void 0 || raw === "") return policy.defaultLimit;
|
|
128
128
|
const parsed = typeof raw === "number" ? raw : Number.parseInt(String(raw), 10);
|
|
129
129
|
if (!Number.isFinite(parsed) || parsed <= 0) return policy.defaultLimit;
|
|
@@ -1,33 +1,5 @@
|
|
|
1
1
|
import { n as DomainEvent } from "./EventTransport-QmY7A258.mjs";
|
|
2
2
|
import { FastifyPluginAsync, FastifyRequest } from "fastify";
|
|
3
|
-
//#region src/plugins/health.d.ts
|
|
4
|
-
interface HealthCheck {
|
|
5
|
-
/** Name of the dependency */
|
|
6
|
-
name: string;
|
|
7
|
-
/** Function that returns true if healthy, false otherwise */
|
|
8
|
-
check: () => Promise<boolean> | boolean;
|
|
9
|
-
/** Optional timeout in ms (default: 5000) */
|
|
10
|
-
timeout?: number;
|
|
11
|
-
/** Whether this check is critical for readiness (default: true) */
|
|
12
|
-
critical?: boolean;
|
|
13
|
-
}
|
|
14
|
-
interface HealthOptions {
|
|
15
|
-
/** Route prefix (default: '/_health') */
|
|
16
|
-
prefix?: string;
|
|
17
|
-
/** Health check dependencies */
|
|
18
|
-
checks?: HealthCheck[];
|
|
19
|
-
/** Enable metrics endpoint (default: false) */
|
|
20
|
-
metrics?: boolean;
|
|
21
|
-
/** Custom metrics collector function */
|
|
22
|
-
metricsCollector?: () => Promise<string> | string;
|
|
23
|
-
/** Version info to include in responses */
|
|
24
|
-
version?: string;
|
|
25
|
-
/** Collect HTTP request metrics (default: true if metrics enabled) */
|
|
26
|
-
collectHttpMetrics?: boolean;
|
|
27
|
-
}
|
|
28
|
-
declare const healthPlugin: FastifyPluginAsync<HealthOptions>;
|
|
29
|
-
declare const _default$4: FastifyPluginAsync<HealthOptions>;
|
|
30
|
-
//#endregion
|
|
31
3
|
//#region src/plugins/caching.d.ts
|
|
32
4
|
interface CachingRule {
|
|
33
5
|
/** Path prefix to match (e.g., '/api/products') */
|
|
@@ -54,7 +26,7 @@ interface CachingOptions {
|
|
|
54
26
|
rules?: CachingRule[];
|
|
55
27
|
}
|
|
56
28
|
declare const cachingPlugin: FastifyPluginAsync<CachingOptions>;
|
|
57
|
-
declare const _default$
|
|
29
|
+
declare const _default$4: FastifyPluginAsync<CachingOptions>;
|
|
58
30
|
//#endregion
|
|
59
31
|
//#region src/plugins/sse.d.ts
|
|
60
32
|
interface SSEOptions {
|
|
@@ -79,7 +51,46 @@ interface SSEOptions {
|
|
|
79
51
|
tokenQueryParam?: string | null;
|
|
80
52
|
}
|
|
81
53
|
declare const ssePlugin: FastifyPluginAsync<SSEOptions>;
|
|
82
|
-
declare const _default$
|
|
54
|
+
declare const _default$3: FastifyPluginAsync<SSEOptions>;
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/plugins/realtime.d.ts
|
|
57
|
+
declare const CHANGE_OPERATIONS: readonly ["created", "updated", "deleted"];
|
|
58
|
+
type ChangeOperation = (typeof CHANGE_OPERATIONS)[number];
|
|
59
|
+
interface RealtimeOptions {
|
|
60
|
+
/**
|
|
61
|
+
* Route path — must contain `:resource` (default: `/realtime/:resource`).
|
|
62
|
+
*/
|
|
63
|
+
path?: string;
|
|
64
|
+
/** Heartbeat interval in ms (default: 30000). */
|
|
65
|
+
heartbeat?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Subscribable resources. Default: every registered resource — safe
|
|
68
|
+
* because each subscription runs that resource's own `list` permission;
|
|
69
|
+
* narrow it when only some resources should have a change feed at all.
|
|
70
|
+
*/
|
|
71
|
+
resources?: readonly string[];
|
|
72
|
+
/** Operations streamed (default: created + updated + deleted). */
|
|
73
|
+
operations?: readonly ChangeOperation[];
|
|
74
|
+
/**
|
|
75
|
+
* Query parameter carrying the bearer token for browser `EventSource`
|
|
76
|
+
* clients (default `'token'`, matching the sse plugin / arc-next).
|
|
77
|
+
* Set `null` to disable promotion.
|
|
78
|
+
*/
|
|
79
|
+
tokenQueryParam?: string | null;
|
|
80
|
+
/**
|
|
81
|
+
* Hard ceiling (ms) on any single connection's lifetime. Bounds
|
|
82
|
+
* permission STALENESS: the row filters are snapshotted at connect, so a
|
|
83
|
+
* caller whose permissions are revoked mid-connection keeps receiving
|
|
84
|
+
* their old view until they reconnect. Independently, arc ALWAYS closes
|
|
85
|
+
* the feed when the auth token's `exp` passes (forcing reconnect +
|
|
86
|
+
* re-authorization with fresh filters) — this option adds a cap for
|
|
87
|
+
* session/cookie auth that carries no `exp`, or to tighten below the
|
|
88
|
+
* token TTL. Omit for token-exp-only bounding.
|
|
89
|
+
*/
|
|
90
|
+
maxConnectionMs?: number;
|
|
91
|
+
}
|
|
92
|
+
declare const realtimePlugin: FastifyPluginAsync<RealtimeOptions>;
|
|
93
|
+
declare const _default$2: FastifyPluginAsync<RealtimeOptions>;
|
|
83
94
|
//#endregion
|
|
84
95
|
//#region src/plugins/metrics.d.ts
|
|
85
96
|
interface MetricsOptions {
|
|
@@ -135,6 +146,16 @@ interface VersioningOptions {
|
|
|
135
146
|
headerName?: string;
|
|
136
147
|
/** Response header name (default: 'x-api-version') */
|
|
137
148
|
responseHeader?: string;
|
|
149
|
+
/**
|
|
150
|
+
* Supported versions. When set, a request for any OTHER version is
|
|
151
|
+
* rejected with `400 arc.unsupported_api_version` (details list the
|
|
152
|
+
* supported set). When omitted, every requested version is accepted
|
|
153
|
+
* and merely annotated — detection-only mode.
|
|
154
|
+
*
|
|
155
|
+
* The `defaultVersion` must be in this list (boot-time check) so a
|
|
156
|
+
* versionless request can never be rejected by its own default.
|
|
157
|
+
*/
|
|
158
|
+
versions?: string[];
|
|
138
159
|
/** Deprecated versions — adds Deprecation + Sunset headers */
|
|
139
160
|
deprecated?: string[];
|
|
140
161
|
/** Sunset date for deprecated versions (ISO 8601) */
|
|
@@ -148,4 +169,4 @@ declare module "fastify" {
|
|
|
148
169
|
declare const versioningPlugin: FastifyPluginAsync<VersioningOptions>;
|
|
149
170
|
declare const _default: FastifyPluginAsync<VersioningOptions>;
|
|
150
171
|
//#endregion
|
|
151
|
-
export {
|
|
172
|
+
export { CachingRule as _, MetricsCollector as a, metricsPlugin as c, _default$2 as d, realtimePlugin as f, CachingOptions as g, ssePlugin as h, MetricEntry as i, ChangeOperation as l, _default$3 as m, _default as n, MetricsOptions as o, SSEOptions as p, versioningPlugin as r, _default$1 as s, VersioningOptions as t, RealtimeOptions as u, _default$4 as v, cachingPlugin as y };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
|
+
import { d as createDomainError } from "./errors-DkO8Nk6-.mjs";
|
|
2
3
|
import fp from "fastify-plugin";
|
|
3
4
|
//#region src/plugins/versioning.ts
|
|
4
5
|
var versioning_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -7,8 +8,10 @@ var versioning_exports = /* @__PURE__ */ __exportAll({
|
|
|
7
8
|
});
|
|
8
9
|
const PREFIX_REGEX = /^\/v(\d+)\//;
|
|
9
10
|
const versioningPlugin = async (fastify, opts) => {
|
|
10
|
-
const { type, defaultVersion = "1", headerName = "accept-version", responseHeader = "x-api-version", deprecated = [], sunset } = opts;
|
|
11
|
+
const { type, defaultVersion = "1", headerName = "accept-version", responseHeader = "x-api-version", versions, deprecated = [], sunset } = opts;
|
|
11
12
|
const deprecatedSet = new Set(deprecated);
|
|
13
|
+
const supportedSet = versions ? new Set(versions) : void 0;
|
|
14
|
+
if (supportedSet && !supportedSet.has(defaultVersion)) throw new Error(`versioningPlugin: defaultVersion '${defaultVersion}' is not in versions [${[...supportedSet].join(", ")}]`);
|
|
12
15
|
fastify.decorateRequest("apiVersion", defaultVersion);
|
|
13
16
|
fastify.addHook("onRequest", async (request, reply) => {
|
|
14
17
|
let version = defaultVersion;
|
|
@@ -19,6 +22,10 @@ const versioningPlugin = async (fastify, opts) => {
|
|
|
19
22
|
const match = request.url.match(PREFIX_REGEX);
|
|
20
23
|
if (match) version = match[1] ?? defaultVersion;
|
|
21
24
|
}
|
|
25
|
+
if (supportedSet && !supportedSet.has(version)) throw createDomainError("arc.unsupported_api_version", `API version '${version}' is not supported`, 400, {
|
|
26
|
+
requested: version,
|
|
27
|
+
supported: [...supportedSet]
|
|
28
|
+
});
|
|
22
29
|
request.apiVersion = version;
|
|
23
30
|
reply.header(responseHeader, version);
|
|
24
31
|
if (deprecatedSet.has(version)) {
|