@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
package/dist/cache/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as MemoryCacheStore } from "../memory-CLXA2VeJ.mjs";
|
|
2
1
|
import { i as versionKey, n as hashParams, r as tagVersionKey, t as buildQueryKey } from "../keys-Cd316u_y.mjs";
|
|
3
|
-
import {
|
|
2
|
+
import { t as MemoryCacheStore } from "../memory-CmnzXE5Z.mjs";
|
|
3
|
+
import { r as QueryCache, t as queryCachePlugin } from "../queryCachePlugin-DSGuSVLW.mjs";
|
|
4
4
|
//#region src/cache/redis.ts
|
|
5
5
|
/**
|
|
6
6
|
* Redis-backed cache store.
|
|
@@ -47,6 +47,30 @@ var RedisCacheStore = class {
|
|
|
47
47
|
await this.client.del(this.withPrefix(key));
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
|
+
* Atomic increment (canonical `CacheAdapter` signature: key, by, ttl).
|
|
51
|
+
*
|
|
52
|
+
* The atomic path requires `incrBy` AND — when a TTL is requested —
|
|
53
|
+
* `expireIfAbsent`: TTL-on-create cannot be inferred from INCRBY's
|
|
54
|
+
* return value (an existing counter at 0 also returns `by`), so the
|
|
55
|
+
* NX-expiry must be its own atomic server-side operation. A client that
|
|
56
|
+
* can't honor the COMPLETE contract falls back to read-modify-write
|
|
57
|
+
* (monotonic, but a concurrent replica's bump can be lost, and the TTL
|
|
58
|
+
* refreshes per write since plain SET can't preserve expiry).
|
|
59
|
+
*/
|
|
60
|
+
async increment(key, by = 1, ttlSeconds) {
|
|
61
|
+
const prefixed = this.withPrefix(key);
|
|
62
|
+
const wantsTtl = ttlSeconds !== void 0 && ttlSeconds > 0;
|
|
63
|
+
if (this.client.incrBy && (!wantsTtl || this.client.expireIfAbsent)) {
|
|
64
|
+
const next = await this.client.incrBy(prefixed, by);
|
|
65
|
+
if (wantsTtl) await this.client.expireIfAbsent?.(prefixed, Math.ceil(ttlSeconds));
|
|
66
|
+
return next;
|
|
67
|
+
}
|
|
68
|
+
const current = await this.get(key);
|
|
69
|
+
const next = (typeof current === "number" ? current : 0) + by;
|
|
70
|
+
await this.set(key, next, ttlSeconds ?? this.defaultTtlSeconds);
|
|
71
|
+
return next;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
50
74
|
* Invalidate keys. Pass a glob pattern to delete a subset (`user:*:v2`);
|
|
51
75
|
* omit to clear every key under this store's prefix.
|
|
52
76
|
*/
|
|
@@ -125,9 +149,12 @@ function ioredisAsCacheClient(client) {
|
|
|
125
149
|
const [next, keys] = await client.scan(cursor, ...args);
|
|
126
150
|
return [next, keys];
|
|
127
151
|
},
|
|
128
|
-
pipeline: client.pipeline ? () => client.pipeline() : void 0
|
|
152
|
+
pipeline: client.pipeline ? () => client.pipeline() : void 0,
|
|
153
|
+
incrBy: client.incrby ? (key, by) => client.incrby(key, by) : void 0,
|
|
154
|
+
expireIfAbsent: client.eval ? (key, seconds) => client.eval(EXPIRE_IF_ABSENT_LUA, 1, key, seconds) : void 0
|
|
129
155
|
};
|
|
130
156
|
}
|
|
157
|
+
const EXPIRE_IF_ABSENT_LUA = "if redis.call('TTL', KEYS[1]) < 0 then return redis.call('EXPIRE', KEYS[1], ARGV[1]) else return 0 end";
|
|
131
158
|
/**
|
|
132
159
|
* Wrap an `@upstash/redis` REST client as a `RedisCacheClient`.
|
|
133
160
|
*
|
|
@@ -176,7 +203,9 @@ function upstashAsCacheClient(client) {
|
|
|
176
203
|
}
|
|
177
204
|
const [next, keys] = await client.scan(cursor, opts);
|
|
178
205
|
return [next, keys];
|
|
179
|
-
}
|
|
206
|
+
},
|
|
207
|
+
incrBy: client.incrby ? (key, by) => client.incrby(key, by) : void 0,
|
|
208
|
+
expireIfAbsent: client.eval ? (key, seconds) => client.eval(EXPIRE_IF_ABSENT_LUA, [key], [seconds]) : void 0
|
|
180
209
|
};
|
|
181
210
|
}
|
|
182
211
|
//#endregion
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Nt as RateLimitConfig, V as ResourceDefinition, et as RouteSchemaOptions } from "../../index-
|
|
1
|
+
import { Nt as RateLimitConfig, V as ResourceDefinition, et as RouteSchemaOptions } from "../../index-DPRH0Cme.mjs";
|
|
2
2
|
//#region src/cli/commands/describe.d.ts
|
|
3
3
|
interface DescribedResource {
|
|
4
4
|
name: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as CRUD_OPERATIONS } from "../../constants-DNre0wRg.mjs";
|
|
2
|
-
import { n as stringifyMeasureMap } from "../../ResourceRegistry-
|
|
2
|
+
import { n as stringifyMeasureMap } from "../../ResourceRegistry-y9Hnypus.mjs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
//#region src/cli/commands/describe.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as buildOpenApiSpec } from "../../openapi-
|
|
2
|
-
import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-
|
|
1
|
+
import { t as buildOpenApiSpec } from "../../openapi-DrLtog_B.mjs";
|
|
2
|
+
import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-kvGT6EcO.mjs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
//#region src/cli/commands/docs.ts
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as generate } from "../../generate-
|
|
1
|
+
import { t as generate } from "../../generate-D0wZupBg.mjs";
|
|
2
2
|
export { generate };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as init } from "../../init-
|
|
1
|
+
import { t as init } from "../../init-tlRSt667.mjs";
|
|
2
2
|
export { init };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-
|
|
1
|
+
import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-kvGT6EcO.mjs";
|
|
2
2
|
//#region src/cli/commands/introspect.ts
|
|
3
3
|
function describePermission(value) {
|
|
4
4
|
if (!value) return "none";
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe } from "./commands/describe.mjs";
|
|
2
2
|
import { exportDocs } from "./commands/docs.mjs";
|
|
3
3
|
import { doctor } from "./commands/doctor.mjs";
|
|
4
|
-
import { t as generate } from "../generate-
|
|
5
|
-
import { t as init } from "../init-
|
|
4
|
+
import { t as generate } from "../generate-D0wZupBg.mjs";
|
|
5
|
+
import { t as init } from "../init-tlRSt667.mjs";
|
|
6
6
|
import { introspect } from "./commands/introspect.mjs";
|
|
7
7
|
export { describe, doctor, exportDocs, generate, init, introspect };
|
package/dist/context/index.mjs
CHANGED
|
@@ -1,65 +1,3 @@
|
|
|
1
|
-
import { n as requestContext, t as getTraceHeaders } from "../requestContext-
|
|
2
|
-
import {
|
|
3
|
-
//#region src/context/transactionContext.ts
|
|
4
|
-
/**
|
|
5
|
-
* Transaction Context via AsyncLocalStorage
|
|
6
|
-
*
|
|
7
|
-
* Provides a Unit-of-Work (UoW) hook so any code in the call stack can
|
|
8
|
-
* retrieve the active database session/transaction without it being threaded
|
|
9
|
-
* through every function parameter.
|
|
10
|
-
*
|
|
11
|
-
* Arc is DB-agnostic — the session type is `unknown` so any backend
|
|
12
|
-
* (Mongoose session, Prisma transaction, pg client, Drizzle client) can
|
|
13
|
-
* participate. Kit adapters call `runInTransaction(session, fn)` to install
|
|
14
|
-
* the active context; the outbox's `store()` reads it automatically when no
|
|
15
|
-
* explicit `options.session` is passed.
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```typescript
|
|
19
|
-
* // In a Mongoose-backed service (mongokit adapter)
|
|
20
|
-
* import { transactionContext } from '@classytic/arc/context';
|
|
21
|
-
*
|
|
22
|
-
* await mongoose.connection.withSession(async (session) => {
|
|
23
|
-
* await session.withTransaction(async () => {
|
|
24
|
-
* await transactionContext.run(session, async () => {
|
|
25
|
-
* await orders.insertOne(order, { session });
|
|
26
|
-
* // outbox.store() auto-picks up `session` — no manual threading
|
|
27
|
-
* await outbox.store(orderCreatedEvent);
|
|
28
|
-
* });
|
|
29
|
-
* });
|
|
30
|
-
* });
|
|
31
|
-
* ```
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```typescript
|
|
35
|
-
* // Opt-out: explicit session always wins over the ALS context
|
|
36
|
-
* await outbox.store(event, { session: explicitSession });
|
|
37
|
-
* ```
|
|
38
|
-
*/
|
|
39
|
-
const storage = new AsyncLocalStorage();
|
|
40
|
-
/**
|
|
41
|
-
* Transaction context API.
|
|
42
|
-
*
|
|
43
|
-
* - `run(session, fn)` — install an active DB session for the duration of `fn`
|
|
44
|
-
* - `get()` — read the current session (undefined if outside a transaction scope)
|
|
45
|
-
*/
|
|
46
|
-
const transactionContext = {
|
|
47
|
-
/**
|
|
48
|
-
* Run `fn` with `session` as the active DB transaction context.
|
|
49
|
-
* Any call to `get()` within `fn`'s async call stack returns `session`.
|
|
50
|
-
*/
|
|
51
|
-
run(session, fn) {
|
|
52
|
-
return storage.run(session, fn);
|
|
53
|
-
},
|
|
54
|
-
/**
|
|
55
|
-
* Get the current DB session, or `undefined` if not inside a
|
|
56
|
-
* `transactionContext.run(...)` scope.
|
|
57
|
-
*/
|
|
58
|
-
get() {
|
|
59
|
-
return storage.getStore();
|
|
60
|
-
},
|
|
61
|
-
/** The underlying AsyncLocalStorage instance. Exposed for advanced integrations. */
|
|
62
|
-
storage
|
|
63
|
-
};
|
|
64
|
-
//#endregion
|
|
1
|
+
import { n as requestContext, t as getTraceHeaders } from "../requestContext-MrlJwECf.mjs";
|
|
2
|
+
import { t as transactionContext } from "../transactionContext-PNdoyLLm.mjs";
|
|
65
3
|
export { getTraceHeaders, requestContext, transactionContext };
|
package/dist/core/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { B as defineResource,
|
|
2
|
-
import { A as
|
|
3
|
-
export { AccessControl, type AccessControlConfig, type AdapterLike, type AggMeasureInput, type AggMeasureShorthand, type AggregationCacheConfig, type AggregationConfig, type AggregationDateRangeRequirement, type AggregationIndexHint, type AggregationMaterializedContext, type AggregationMaterializedResult, type AggregationRateLimit, type AggregationsMap, BaseController, type BaseControllerOptions, BaseCrudController, BodySanitizer, type BodySanitizerConfig, type BulkExt, BulkMixin, CRUD_OPERATIONS, type ControllerConfigurableOptions, type ControllerConstructionOptions, CrudOperation, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, type DefineActionConfig, HOOK_OPERATIONS, HOOK_PHASES, HookOperation, HookPhase, type ListResult, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, type QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, type ResourceSeams, SYSTEM_FIELDS, type SeamedResourceConfig, type SlugExt, SlugMixin, type SoftDeleteExt, SoftDeleteMixin, type TreeExt, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAction, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, mergeResourceConfig, sendControllerResponse };
|
|
1
|
+
import { $t as SlugExt, An as ControllerConfigurableOptions, B as defineResource, Jt as BaseController, Mn as ListResult, Nn as BodySanitizer, Pn as BodySanitizerConfig, Qt as TreeMixin, V as ResourceDefinition, Xt as SoftDeleteMixin, Yt as SoftDeleteExt, Zt as TreeExt, an as QueryResolverConfig, ct as AggMeasureShorthand, dt as AggregationDateRangeRequirement, en as SlugMixin, ft as AggregationIndexHint, gt as AggregationsMap, ht as AggregationRateLimit, in as QueryResolver, jn as ControllerConstructionOptions, kn as BaseControllerOptions, lt as AggregationCacheConfig, mt as AggregationMaterializedResult, nn as BulkMixin, on as AccessControl, pt as AggregationMaterializedContext, rn as BaseCrudController, sn as AccessControlConfig, st as AggMeasureInput, tn as BulkExt, ut as AggregationConfig } from "../index-DPRH0Cme.mjs";
|
|
2
|
+
import { A as DEFAULT_ID_FIELD, B as MAX_FILTER_DEPTH, C as DefineActionConfig, D as defineAggregation, E as createPermissionMiddleware, F as DEFAULT_UPDATE_METHOD, G as RESERVED_QUERY_PARAMS, H as MAX_SEARCH_LENGTH, I as HOOK_OPERATIONS, K as SYSTEM_FIELDS, L as HOOK_PHASES, M as DEFAULT_MAX_LIMIT, N as DEFAULT_SORT, O as CRUD_OPERATIONS, P as DEFAULT_TENANT_FIELD, R as HookOperation, S as mergeResourceConfig, T as createCrudRouter, U as MUTATION_OPERATIONS, V as MAX_REGEX_LENGTH, W as MutationOperation, _ as getEntityQuery, a as formatValidationErrors, b as ResourceSeams, c as isFieldReadable, d as createRequestContext, f as getControllerContext, g as getEntityIdField, h as getEntityId, i as assertValidConfig, j as DEFAULT_LIMIT, k as CrudOperation, l as createCrudHandlers, m as sendControllerResponse, n as ValidateOptions, o as validateResourceConfig, p as getControllerScope, r as ValidationResult, s as collectReadBlockedFields, t as ConfigError, u as createFastifyHandler, v as defineResourceVariants, w as defineAction, x as SeamedResourceConfig, y as AdapterLike, z as HookPhase } from "../index-f4PTrk3B.mjs";
|
|
3
|
+
export { AccessControl, type AccessControlConfig, type AdapterLike, type AggMeasureInput, type AggMeasureShorthand, type AggregationCacheConfig, type AggregationConfig, type AggregationDateRangeRequirement, type AggregationIndexHint, type AggregationMaterializedContext, type AggregationMaterializedResult, type AggregationRateLimit, type AggregationsMap, BaseController, type BaseControllerOptions, BaseCrudController, BodySanitizer, type BodySanitizerConfig, type BulkExt, BulkMixin, CRUD_OPERATIONS, type ConfigError, type ControllerConfigurableOptions, type ControllerConstructionOptions, CrudOperation, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, type DefineActionConfig, HOOK_OPERATIONS, HOOK_PHASES, HookOperation, HookPhase, type ListResult, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, type QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, type ResourceSeams, SYSTEM_FIELDS, type SeamedResourceConfig, type SlugExt, SlugMixin, type SoftDeleteExt, SoftDeleteMixin, type TreeExt, TreeMixin, type ValidateOptions, type ValidationResult, assertValidConfig, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAction, defineAggregation, defineResource, defineResourceVariants, formatValidationErrors, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, mergeResourceConfig, sendControllerResponse, validateResourceConfig };
|
package/dist/core/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as DEFAULT_SORT, c as HOOK_OPERATIONS, d as MAX_REGEX_LENGTH, f as MAX_SEARCH_LENGTH, h as SYSTEM_FIELDS, i as DEFAULT_MAX_LIMIT, l as HOOK_PHASES, m as RESERVED_QUERY_PARAMS, n as DEFAULT_ID_FIELD, o as DEFAULT_TENANT_FIELD, p as MUTATION_OPERATIONS, r as DEFAULT_LIMIT, s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS, u as MAX_FILTER_DEPTH } from "../constants-DNre0wRg.mjs";
|
|
2
|
-
import { a as BulkMixin, c as collectReadBlockedFields, f as AccessControl, i as SlugMixin, l as isFieldReadable, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, s as QueryResolver, t as BaseController, u as BodySanitizer } from "../BaseController
|
|
3
|
-
import { a as defineResource, c as
|
|
4
|
-
import { a as
|
|
5
|
-
export { AccessControl, BaseController, BaseCrudController, BodySanitizer, BulkMixin, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, QueryResolver, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugMixin, SoftDeleteMixin, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAction, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, mergeResourceConfig, sendControllerResponse };
|
|
2
|
+
import { a as BulkMixin, c as collectReadBlockedFields, f as AccessControl, i as SlugMixin, l as isFieldReadable, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, s as QueryResolver, t as BaseController, u as BodySanitizer } from "../BaseController--M8EMzt0.mjs";
|
|
3
|
+
import { a as defineResource, c as validateResourceConfig, d as defineAction, f as createCrudRouter, i as defineResourceVariants, l as ResourceDefinition, m as defineAggregation, n as getEntityIdField, o as assertValidConfig, p as createPermissionMiddleware, r as getEntityQuery, s as formatValidationErrors, t as getEntityId, u as mergeResourceConfig } from "../core-EUNu0u07.mjs";
|
|
4
|
+
import { a as createFastifyHandler, c as getControllerScope, i as createCrudHandlers, l as sendControllerResponse, o as createRequestContext, s as getControllerContext } from "../pipeline-BN5G05rd.mjs";
|
|
5
|
+
export { AccessControl, BaseController, BaseCrudController, BodySanitizer, BulkMixin, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, QueryResolver, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, SlugMixin, SoftDeleteMixin, TreeMixin, assertValidConfig, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAction, defineAggregation, defineResource, defineResourceVariants, formatValidationErrors, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, mergeResourceConfig, sendControllerResponse, validateResourceConfig };
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-DNre0wRg.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
3
|
import { r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController
|
|
7
|
-
import { a as
|
|
8
|
-
import {
|
|
9
|
-
import { t as
|
|
10
|
-
import {
|
|
11
|
-
import { n as convertRouteSchema, r as ensureSchemaConverter, t as convertOpenApiSchemas } from "./schemaConverter-
|
|
4
|
+
import { n as getUserRoles } from "./userHelpers-Cg_y3fBz.mjs";
|
|
5
|
+
import { b as isElevated, n as PUBLIC_SCOPE, x as isMember } from "./types-CSyCxl2V.mjs";
|
|
6
|
+
import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController--M8EMzt0.mjs";
|
|
7
|
+
import { a as buildRouteHooks, c as buildAuthMiddleware, i as selectPluginMw, n as buildRouteConfig, o as resolveRoutePreHandlers, r as resolveRouterPluginMw, s as routeHookOptions, t as buildRateLimitConfig, u as buildArcDecorator } from "./rateLimit-DrED-C4v.mjs";
|
|
8
|
+
import { a as applyFieldWritePermissions, s as resolveEffectiveRoles } from "./applyPermissionResult-Tw4iXtlf.mjs";
|
|
9
|
+
import { r as buildCrudPermissionMw, t as tryRegisterRoute } from "./registerRoute-v0Wb6Plt.mjs";
|
|
10
|
+
import { c as getDefaultCrudSchemas } from "./utils-taUVD4p3.mjs";
|
|
11
|
+
import { n as convertRouteSchema, r as ensureSchemaConverter, t as convertOpenApiSchemas } from "./schemaConverter-DJvodiHj.mjs";
|
|
12
12
|
import { i as pipeUIMessageStreamToReply, r as isReadableStream } from "./streaming-BQcW6_Kq.mjs";
|
|
13
13
|
import { t as hasEvents } from "./typeGuards-C9KXUly6.mjs";
|
|
14
|
-
import { a as
|
|
14
|
+
import { a as createFastifyHandler, i as createCrudHandlers, n as buildPipelineHandler, o as createRequestContext, r as resolvePipelineSteps, u as buildRequestScopeProjection } from "./pipeline-BN5G05rd.mjs";
|
|
15
|
+
import { A as requireAuth } from "./permissions-BrtiBKGs.mjs";
|
|
16
|
+
import { r as getAvailablePresets, t as applyPresets } from "./presets-DwFDVKP0.mjs";
|
|
15
17
|
import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
|
|
18
|
+
import { t as resolveActionPermission } from "./actionPermissions-CAphcmUG.mjs";
|
|
16
19
|
//#region src/core/middlewares/fieldWrite.ts
|
|
17
20
|
/**
|
|
18
21
|
* Build a preHandler that enforces field-write permissions on the request body.
|
|
@@ -1395,7 +1398,7 @@ function buildResourcePlugin(resource) {
|
|
|
1395
1398
|
extensions: resource.extensions
|
|
1396
1399
|
});
|
|
1397
1400
|
if (resource.actions && Object.keys(resource.actions).length > 0) {
|
|
1398
|
-
const { createActionRouter } = await import("./createActionRouter-
|
|
1401
|
+
const { createActionRouter } = await import("./createActionRouter-B6GvAuAX.mjs").then((n) => n.n);
|
|
1399
1402
|
createActionRouter(typedInstance, {
|
|
1400
1403
|
...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
|
|
1401
1404
|
resourceName: resource.name,
|
|
@@ -1410,7 +1413,7 @@ function buildResourcePlugin(resource) {
|
|
|
1410
1413
|
});
|
|
1411
1414
|
}
|
|
1412
1415
|
if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
|
|
1413
|
-
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-
|
|
1416
|
+
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-wzwFa_dW.mjs"), import("./validate-Bk44F2yT.mjs").then((n) => n.r)]);
|
|
1414
1417
|
const repoForAgg = resource.controller?.repository ?? resource.repository;
|
|
1415
1418
|
if (!adapterSupportsAggregate(repoForAgg)) {
|
|
1416
1419
|
const undispatchable = Object.entries(resource.aggregations).filter(([, cfg]) => !cfg.materialized).map(([name]) => name);
|
|
@@ -1860,6 +1863,289 @@ function mergeUserOpenApiOverrides(openApiSchemas, userOverrides) {
|
|
|
1860
1863
|
};
|
|
1861
1864
|
}
|
|
1862
1865
|
//#endregion
|
|
1866
|
+
//#region src/core/validateResourceConfig.ts
|
|
1867
|
+
/**
|
|
1868
|
+
* Resource Configuration Validator
|
|
1869
|
+
*
|
|
1870
|
+
* Fail-fast validation at definition time.
|
|
1871
|
+
* Invalid configs throw immediately with clear, actionable errors.
|
|
1872
|
+
*
|
|
1873
|
+
* @example
|
|
1874
|
+
* const result = validateResourceConfig(config);
|
|
1875
|
+
* if (!result.valid) {
|
|
1876
|
+
* console.error(formatValidationErrors(result.errors));
|
|
1877
|
+
* }
|
|
1878
|
+
*/
|
|
1879
|
+
/**
|
|
1880
|
+
* Validate a resource configuration
|
|
1881
|
+
*/
|
|
1882
|
+
function validateResourceConfig(config, options = {}) {
|
|
1883
|
+
const errors = [];
|
|
1884
|
+
const warnings = [];
|
|
1885
|
+
if (!config.name) errors.push({
|
|
1886
|
+
field: "name",
|
|
1887
|
+
message: "Resource name is required",
|
|
1888
|
+
suggestion: "Add a unique resource name (e.g., \"product\", \"user\")"
|
|
1889
|
+
});
|
|
1890
|
+
else if (!/^[a-z][a-z0-9-]*$/i.test(config.name)) errors.push({
|
|
1891
|
+
field: "name",
|
|
1892
|
+
message: `Invalid resource name "${config.name}"`,
|
|
1893
|
+
suggestion: "Use alphanumeric characters and hyphens, starting with a letter"
|
|
1894
|
+
});
|
|
1895
|
+
const crudRoutes = CRUD_OPERATIONS;
|
|
1896
|
+
const disabledRoutes = new Set(config.disabledRoutes ?? []);
|
|
1897
|
+
const enabledCrudRoutes = crudRoutes.filter((route) => !disabledRoutes.has(route));
|
|
1898
|
+
if (!config.disableDefaultRoutes && enabledCrudRoutes.length > 0) {
|
|
1899
|
+
if (!config.adapter) errors.push({
|
|
1900
|
+
field: "adapter",
|
|
1901
|
+
message: "Data adapter is required when CRUD routes are enabled",
|
|
1902
|
+
suggestion: "Provide an adapter: createMongooseAdapter({ model, repository })"
|
|
1903
|
+
});
|
|
1904
|
+
else if (!config.adapter.repository) errors.push({
|
|
1905
|
+
field: "adapter.repository",
|
|
1906
|
+
message: "Adapter must provide a repository",
|
|
1907
|
+
suggestion: "Ensure your adapter returns a valid StandardRepo (see @classytic/repo-core)"
|
|
1908
|
+
});
|
|
1909
|
+
} else if (!config.adapter && !config.routes?.length) warnings.push({
|
|
1910
|
+
field: "config",
|
|
1911
|
+
message: "Resource has no adapter and no routes",
|
|
1912
|
+
suggestion: "Provide either adapter for CRUD or routes for custom logic"
|
|
1913
|
+
});
|
|
1914
|
+
if (config.controller && !options.skipControllerCheck && !config.disableDefaultRoutes) {
|
|
1915
|
+
const ctrl = config.controller;
|
|
1916
|
+
const requiredMethods = CRUD_OPERATIONS;
|
|
1917
|
+
for (const method of requiredMethods) if (typeof ctrl[method] !== "function") errors.push({
|
|
1918
|
+
field: `controller.${method}`,
|
|
1919
|
+
message: `Missing required CRUD method "${method}"`,
|
|
1920
|
+
suggestion: "Extend BaseController which implements IController interface"
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
if (config.controller && config.routes) validateRouteHandlers(config.controller, config.routes, errors);
|
|
1924
|
+
if (config.permissions) validatePermissionKeys(config, options, errors, warnings);
|
|
1925
|
+
if (config.presets && !options.allowUnknownPresets) validatePresets(config.presets, errors, warnings);
|
|
1926
|
+
if (config.prefix) {
|
|
1927
|
+
if (!config.prefix.startsWith("/")) errors.push({
|
|
1928
|
+
field: "prefix",
|
|
1929
|
+
message: `Prefix must start with "/" (got "${config.prefix}")`,
|
|
1930
|
+
suggestion: `Change to "/${config.prefix}"`
|
|
1931
|
+
});
|
|
1932
|
+
if (config.prefix.endsWith("/") && config.prefix !== "/") warnings.push({
|
|
1933
|
+
field: "prefix",
|
|
1934
|
+
message: `Prefix should not end with "/" (got "${config.prefix}")`,
|
|
1935
|
+
suggestion: `Change to "${config.prefix.slice(0, -1)}"`
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
if (config.routes) {
|
|
1939
|
+
validateRoutes(config.routes, errors);
|
|
1940
|
+
validateRouteCrudCollisions(config, errors);
|
|
1941
|
+
}
|
|
1942
|
+
return {
|
|
1943
|
+
valid: errors.length === 0,
|
|
1944
|
+
errors,
|
|
1945
|
+
warnings
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
function validateRouteHandlers(controller, routes, errors) {
|
|
1949
|
+
const ctrl = controller;
|
|
1950
|
+
for (const route of routes) if (typeof route.handler === "string") {
|
|
1951
|
+
if (typeof ctrl[route.handler] !== "function") errors.push({
|
|
1952
|
+
field: `routes[${route.method} ${route.path}]`,
|
|
1953
|
+
message: `Handler "${route.handler}" not found on controller`,
|
|
1954
|
+
suggestion: `Add method "${route.handler}" to controller or use a function handler`
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
function validatePermissionKeys(config, options, _errors, warnings) {
|
|
1959
|
+
const validKeys = /* @__PURE__ */ new Set([...CRUD_OPERATIONS, ...options.additionalPermissionKeys ?? []]);
|
|
1960
|
+
for (const route of config.routes ?? []) if (typeof route.handler === "string") validKeys.add(route.handler);
|
|
1961
|
+
for (const preset of config.presets ?? []) {
|
|
1962
|
+
const presetName = typeof preset === "string" ? preset : preset.name;
|
|
1963
|
+
if (presetName === "softDelete") {
|
|
1964
|
+
validKeys.add("deleted");
|
|
1965
|
+
validKeys.add("restore");
|
|
1966
|
+
}
|
|
1967
|
+
if (presetName === "slugLookup") validKeys.add("getBySlug");
|
|
1968
|
+
if (presetName === "tree") {
|
|
1969
|
+
validKeys.add("tree");
|
|
1970
|
+
validKeys.add("children");
|
|
1971
|
+
validKeys.add("getTree");
|
|
1972
|
+
validKeys.add("getChildren");
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
for (const key of Object.keys(config.permissions ?? {})) if (!validKeys.has(key)) warnings.push({
|
|
1976
|
+
field: `permissions.${key}`,
|
|
1977
|
+
message: `Unknown permission key "${key}"`,
|
|
1978
|
+
suggestion: `Valid keys: ${Array.from(validKeys).join(", ")}`
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
function validatePresets(presets, errors, warnings) {
|
|
1982
|
+
const availablePresets = getAvailablePresets();
|
|
1983
|
+
for (const preset of presets) {
|
|
1984
|
+
if (typeof preset === "object" && ("middlewares" in preset || "routes" in preset)) continue;
|
|
1985
|
+
const presetName = typeof preset === "string" ? preset : preset.name;
|
|
1986
|
+
if (!availablePresets.includes(presetName)) errors.push({
|
|
1987
|
+
field: "presets",
|
|
1988
|
+
message: `Unknown preset "${presetName}"`,
|
|
1989
|
+
suggestion: `Available presets: ${availablePresets.join(", ")}`
|
|
1990
|
+
});
|
|
1991
|
+
if (typeof preset === "object") validatePresetOptions(preset, warnings);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
function validatePresetOptions(preset, warnings) {
|
|
1995
|
+
const validOptions = {
|
|
1996
|
+
slugLookup: ["slugField"],
|
|
1997
|
+
tree: ["parentField"],
|
|
1998
|
+
softDelete: ["deletedField"],
|
|
1999
|
+
ownedByUser: ["ownerField"],
|
|
2000
|
+
multiTenant: ["tenantField", "allowPublic"]
|
|
2001
|
+
}[preset.name] ?? [];
|
|
2002
|
+
const providedOptions = Object.keys(preset).filter((k) => k !== "name");
|
|
2003
|
+
for (const opt of providedOptions) if (!validOptions.includes(opt)) warnings.push({
|
|
2004
|
+
field: `presets[${preset.name}].${opt}`,
|
|
2005
|
+
message: `Unknown option "${opt}" for preset "${preset.name}"`,
|
|
2006
|
+
suggestion: validOptions.length > 0 ? `Valid options: ${validOptions.join(", ")}` : `Preset "${preset.name}" has no configurable options`
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Detect collisions between user-declared `routes:` entries and the
|
|
2011
|
+
* auto-CRUD route table that `createCrudRouter` will register.
|
|
2012
|
+
*
|
|
2013
|
+
* Without this check, a custom `POST /` on the same prefix as auto-CRUD's
|
|
2014
|
+
* `create` boots fine through `defineResource()` and only blows up at
|
|
2015
|
+
* `app.register()` time with Fastify's opaque `FST_ERR_DUPLICATED_ROUTE`.
|
|
2016
|
+
* That error doesn't mention `disabledRoutes` and doesn't distinguish
|
|
2017
|
+
* "you collided with auto-CRUD" from "you collided with yourself" — so
|
|
2018
|
+
* the consumer has to grep arc's source to discover the fix.
|
|
2019
|
+
*
|
|
2020
|
+
* We surface the fix in the error message, naming the conflicting op
|
|
2021
|
+
* and the literal line to add.
|
|
2022
|
+
*
|
|
2023
|
+
* Honored config:
|
|
2024
|
+
* - `disableDefaultRoutes: true` → no auto-CRUD, no collisions possible
|
|
2025
|
+
* - `disabledRoutes: ['create']` → that op's path is free for custom use
|
|
2026
|
+
* - `updateMethod: 'PUT' | 'both'` → update slot covers PUT/PATCH accordingly
|
|
2027
|
+
*/
|
|
2028
|
+
function validateRouteCrudCollisions(config, errors) {
|
|
2029
|
+
if (config.disableDefaultRoutes) return;
|
|
2030
|
+
if (!config.routes || config.routes.length === 0) return;
|
|
2031
|
+
const updateMethod = config.updateMethod ?? "PATCH";
|
|
2032
|
+
const updateMethods = updateMethod === "both" ? ["PUT", "PATCH"] : [updateMethod];
|
|
2033
|
+
const disabled = new Set(config.disabledRoutes ?? []);
|
|
2034
|
+
const crudTable = [
|
|
2035
|
+
{
|
|
2036
|
+
op: "list",
|
|
2037
|
+
method: "GET",
|
|
2038
|
+
path: "/"
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
op: "get",
|
|
2042
|
+
method: "GET",
|
|
2043
|
+
path: "/:id"
|
|
2044
|
+
},
|
|
2045
|
+
{
|
|
2046
|
+
op: "create",
|
|
2047
|
+
method: "POST",
|
|
2048
|
+
path: "/"
|
|
2049
|
+
},
|
|
2050
|
+
...updateMethods.map((m) => ({
|
|
2051
|
+
op: "update",
|
|
2052
|
+
method: m,
|
|
2053
|
+
path: "/:id"
|
|
2054
|
+
})),
|
|
2055
|
+
{
|
|
2056
|
+
op: "delete",
|
|
2057
|
+
method: "DELETE",
|
|
2058
|
+
path: "/:id"
|
|
2059
|
+
}
|
|
2060
|
+
];
|
|
2061
|
+
for (const [i, route] of config.routes.entries()) {
|
|
2062
|
+
if (!route.method || !route.path) continue;
|
|
2063
|
+
for (const crud of crudTable) {
|
|
2064
|
+
if (disabled.has(crud.op)) continue;
|
|
2065
|
+
if (route.method !== crud.method || route.path !== crud.path) continue;
|
|
2066
|
+
errors.push({
|
|
2067
|
+
field: `routes[${i}]`,
|
|
2068
|
+
message: `Route ${route.method} ${route.path} collides with auto-CRUD "${crud.op}"`,
|
|
2069
|
+
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.`
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
function validateRoutes(routes, errors) {
|
|
2075
|
+
const validMethods = [
|
|
2076
|
+
"GET",
|
|
2077
|
+
"POST",
|
|
2078
|
+
"PUT",
|
|
2079
|
+
"PATCH",
|
|
2080
|
+
"DELETE",
|
|
2081
|
+
"OPTIONS",
|
|
2082
|
+
"HEAD"
|
|
2083
|
+
];
|
|
2084
|
+
const seenRoutes = /* @__PURE__ */ new Set();
|
|
2085
|
+
for (const [i, route] of routes.entries()) {
|
|
2086
|
+
if (!validMethods.includes(route.method)) errors.push({
|
|
2087
|
+
field: `routes[${i}].method`,
|
|
2088
|
+
message: `Invalid HTTP method "${route.method}"`,
|
|
2089
|
+
suggestion: `Valid methods: ${validMethods.join(", ")}`
|
|
2090
|
+
});
|
|
2091
|
+
if (!route.path) errors.push({
|
|
2092
|
+
field: `routes[${i}].path`,
|
|
2093
|
+
message: "Route path is required"
|
|
2094
|
+
});
|
|
2095
|
+
else if (!route.path.startsWith("/")) errors.push({
|
|
2096
|
+
field: `routes[${i}].path`,
|
|
2097
|
+
message: `Route path must start with "/" (got "${route.path}")`,
|
|
2098
|
+
suggestion: `Change to "/${route.path}"`
|
|
2099
|
+
});
|
|
2100
|
+
const routeWithRefs = route;
|
|
2101
|
+
if (!route.handler && typeof routeWithRefs.controllerMethod !== "function") errors.push({
|
|
2102
|
+
field: `routes[${i}].handler`,
|
|
2103
|
+
message: "Route must declare either `handler` (string / function) or `controllerMethod`",
|
|
2104
|
+
suggestion: "Prefer `controllerMethod: (c: MyController) => c.method` for typed handler refs."
|
|
2105
|
+
});
|
|
2106
|
+
const routeKey = `${route.method} ${route.path}`;
|
|
2107
|
+
if (seenRoutes.has(routeKey)) errors.push({
|
|
2108
|
+
field: `routes[${i}]`,
|
|
2109
|
+
message: `Duplicate route "${routeKey}"`
|
|
2110
|
+
});
|
|
2111
|
+
seenRoutes.add(routeKey);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Format validation errors for display
|
|
2116
|
+
*/
|
|
2117
|
+
function formatValidationErrors(resourceName, result) {
|
|
2118
|
+
const lines = [];
|
|
2119
|
+
if (result.errors.length > 0) {
|
|
2120
|
+
lines.push(`Resource "${resourceName}" validation failed:`);
|
|
2121
|
+
lines.push("");
|
|
2122
|
+
lines.push("ERRORS:");
|
|
2123
|
+
for (const err of result.errors) {
|
|
2124
|
+
lines.push(` ✗ ${err.field}: ${err.message}`);
|
|
2125
|
+
if (err.suggestion) lines.push(` → ${err.suggestion}`);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
if (result.warnings.length > 0) {
|
|
2129
|
+
if (lines.length > 0) lines.push("");
|
|
2130
|
+
lines.push("WARNINGS:");
|
|
2131
|
+
for (const warn of result.warnings) {
|
|
2132
|
+
lines.push(` ⚠ ${warn.field}: ${warn.message}`);
|
|
2133
|
+
if (warn.suggestion) lines.push(` → ${warn.suggestion}`);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
return lines.join("\n");
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Validate and throw if invalid
|
|
2140
|
+
*/
|
|
2141
|
+
function assertValidConfig(config, options) {
|
|
2142
|
+
const result = validateResourceConfig(config, options);
|
|
2143
|
+
if (!result.valid) {
|
|
2144
|
+
const errorMsg = formatValidationErrors(config.name ?? "unknown", result);
|
|
2145
|
+
throw new Error(errorMsg);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
//#endregion
|
|
1863
2149
|
//#region src/core/defineResource/validate.ts
|
|
1864
2150
|
/**
|
|
1865
2151
|
* CRUD op names — kept module-scope (vs allocated per `defineResource()`
|
|
@@ -2120,4 +2406,4 @@ function getEntityQuery(req) {
|
|
|
2120
2406
|
return { [getEntityIdField(req)]: id };
|
|
2121
2407
|
}
|
|
2122
2408
|
//#endregion
|
|
2123
|
-
export { defineResource as a,
|
|
2409
|
+
export { defineResource as a, validateResourceConfig as c, defineAction as d, createCrudRouter as f, defineResourceVariants as i, ResourceDefinition as l, defineAggregation as m, getEntityIdField as n, assertValidConfig as o, createPermissionMiddleware as p, getEntityQuery as r, formatValidationErrors as s, getEntityId as t, mergeResourceConfig as u };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
2
|
import "./constants-DNre0wRg.mjs";
|
|
3
3
|
import { f as createError } from "./errors-DkO8Nk6-.mjs";
|
|
4
|
-
import { a as
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { a as buildRouteHooks, i as selectPluginMw, l as buildAuthMiddlewareForPermissions, n as buildRouteConfig, r as resolveRouterPluginMw, s as routeHookOptions, t as buildRateLimitConfig, u as buildArcDecorator } from "./rateLimit-DrED-C4v.mjs";
|
|
5
|
+
import { n as buildActionPermissionMw, t as tryRegisterRoute } from "./registerRoute-v0Wb6Plt.mjs";
|
|
6
|
+
import { l as sendControllerResponse, r as resolvePipelineSteps, t as buildActionPipelineHandler } from "./pipeline-BN5G05rd.mjs";
|
|
7
|
+
import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-Ca7ySNPx.mjs";
|
|
7
8
|
//#region src/core/createActionRouter.ts
|
|
8
9
|
var createActionRouter_exports = /* @__PURE__ */ __exportAll({
|
|
9
10
|
buildActionBodySchema: () => buildActionBodySchema,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { f as createError, l as UnauthorizedError, r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
|
|
2
|
-
import { a as
|
|
3
|
-
import { n as validateAggregations } from "./validate-
|
|
4
|
-
import { t as buildAggregationHandler } from "./buildHandler-
|
|
2
|
+
import { a as buildRouteHooks, c as buildAuthMiddleware, i as selectPluginMw, r as resolveRouterPluginMw, s as routeHookOptions, t as buildRateLimitConfig, u as buildArcDecorator } from "./rateLimit-DrED-C4v.mjs";
|
|
3
|
+
import { n as validateAggregations } from "./validate-Bk44F2yT.mjs";
|
|
4
|
+
import { t as buildAggregationHandler } from "./buildHandler-Dtwd8fdz.mjs";
|
|
5
5
|
//#region src/core/aggregation/createAggregationRouter.ts
|
|
6
6
|
/**
|
|
7
7
|
* Register one Fastify route per aggregation. No-op when the map is
|
package/dist/docs/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { p as RegistryEntry } from "../index-
|
|
1
|
+
import { p as RegistryEntry } from "../index-DPRH0Cme.mjs";
|
|
2
2
|
import { t as ExternalOpenApiPaths } from "../externalPaths-BE6wFpYv.mjs";
|
|
3
3
|
import { FastifyPluginAsync } from "fastify";
|
|
4
4
|
//#region src/docs/openapi/types.d.ts
|
package/dist/docs/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-
|
|
1
|
+
import { n as getUserRoles } from "../userHelpers-Cg_y3fBz.mjs";
|
|
2
|
+
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-DrLtog_B.mjs";
|
|
3
3
|
import fp from "fastify-plugin";
|
|
4
4
|
//#region src/docs/scalar.ts
|
|
5
5
|
const scalarPlugin = async (fastify, opts = {}) => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { n as requireSingleHeaderValue } from "./headers-BvlQSDMB.mjs";
|
|
4
|
+
import { n as getUserRoles } from "./userHelpers-Cg_y3fBz.mjs";
|
|
4
5
|
import fp from "fastify-plugin";
|
|
5
6
|
//#region src/scope/elevation.ts
|
|
6
7
|
var elevation_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -18,7 +19,7 @@ const elevationPlugin = async (fastify, opts = {}) => {
|
|
|
18
19
|
const authenticateWithElevation = async (request, reply) => {
|
|
19
20
|
await originalAuthenticate.call(fastify, request, reply);
|
|
20
21
|
if (reply.sent) return;
|
|
21
|
-
if (request.headers
|
|
22
|
+
if (requireSingleHeaderValue(request.headers, scopeHeader) !== "platform") return;
|
|
22
23
|
const user = request.user;
|
|
23
24
|
if (!user) {
|
|
24
25
|
log.debug("Elevation requested but no user after auth");
|
|
@@ -45,7 +46,7 @@ const elevationPlugin = async (fastify, opts = {}) => {
|
|
|
45
46
|
});
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
48
|
-
const orgId = request.headers
|
|
49
|
+
const orgId = requireSingleHeaderValue(request.headers, orgHeader);
|
|
49
50
|
const userId = String(user.id ?? user._id ?? "unknown");
|
|
50
51
|
request.scope = {
|
|
51
52
|
kind: "elevated",
|