@classytic/arc 1.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -794
- package/bin/arc.js +91 -52
- package/dist/EventTransport-BD2U0BTc.d.mts +100 -0
- package/dist/EventTransport-BD2U0BTc.d.mts.map +1 -0
- package/dist/HookSystem-BsGV-j2l.mjs +405 -0
- package/dist/HookSystem-BsGV-j2l.mjs.map +1 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs +250 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs.map +1 -0
- package/dist/adapters/index.d.mts +5 -0
- package/dist/adapters/index.mjs +3 -0
- package/dist/audit/index.d.mts +82 -0
- package/dist/audit/index.d.mts.map +1 -0
- package/dist/audit/index.mjs +276 -0
- package/dist/audit/index.mjs.map +1 -0
- package/dist/audit/mongodb.d.mts +5 -0
- package/dist/audit/mongodb.mjs +3 -0
- package/dist/audited-C3T5DTUx.mjs +141 -0
- package/dist/audited-C3T5DTUx.mjs.map +1 -0
- package/dist/auth/index.d.mts +189 -0
- package/dist/auth/index.d.mts.map +1 -0
- package/dist/auth/index.mjs +1102 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/auth/redis-session.d.mts +44 -0
- package/dist/auth/redis-session.d.mts.map +1 -0
- package/dist/auth/redis-session.mjs +76 -0
- package/dist/auth/redis-session.mjs.map +1 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs +250 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +1 -0
- package/dist/cache/index.d.mts +146 -0
- package/dist/cache/index.d.mts.map +1 -0
- package/dist/cache/index.mjs +92 -0
- package/dist/cache/index.mjs.map +1 -0
- package/dist/caching-Bl28lYsR.mjs +94 -0
- package/dist/caching-Bl28lYsR.mjs.map +1 -0
- package/dist/chunk-C7Uep-_p.mjs +20 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs +1097 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs.map +1 -0
- package/dist/cli/commands/describe.d.mts +19 -0
- package/dist/cli/commands/describe.d.mts.map +1 -0
- package/dist/cli/commands/describe.mjs +239 -0
- package/dist/cli/commands/describe.mjs.map +1 -0
- package/dist/cli/commands/docs.d.mts +14 -0
- package/dist/cli/commands/docs.d.mts.map +1 -0
- package/dist/cli/commands/docs.mjs +53 -0
- package/dist/cli/commands/docs.mjs.map +1 -0
- package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -1
- package/dist/cli/commands/generate.d.mts.map +1 -0
- package/dist/cli/commands/generate.mjs +358 -0
- package/dist/cli/commands/generate.mjs.map +1 -0
- package/dist/cli/commands/{init.d.ts → init.d.mts} +12 -8
- package/dist/cli/commands/init.d.mts.map +1 -0
- package/dist/cli/commands/{init.js → init.mjs} +807 -616
- package/dist/cli/commands/init.mjs.map +1 -0
- package/dist/cli/commands/introspect.d.mts +11 -0
- package/dist/cli/commands/introspect.d.mts.map +1 -0
- package/dist/cli/commands/introspect.mjs +76 -0
- package/dist/cli/commands/introspect.mjs.map +1 -0
- package/dist/cli/index.d.mts +17 -0
- package/dist/cli/index.d.mts.map +1 -0
- package/dist/cli/index.mjs +157 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/constants-DdXFXQtN.mjs +85 -0
- package/dist/constants-DdXFXQtN.mjs.map +1 -0
- package/dist/core/index.d.mts +5 -0
- package/dist/core/index.mjs +4 -0
- package/dist/createApp-CUgNqegw.mjs +560 -0
- package/dist/createApp-CUgNqegw.mjs.map +1 -0
- package/dist/defineResource-k0_BDn8v.mjs +2197 -0
- package/dist/defineResource-k0_BDn8v.mjs.map +1 -0
- package/dist/discovery/index.d.mts +47 -0
- package/dist/discovery/index.d.mts.map +1 -0
- package/dist/discovery/index.mjs +110 -0
- package/dist/discovery/index.mjs.map +1 -0
- package/dist/docs/index.d.mts +163 -0
- package/dist/docs/index.d.mts.map +1 -0
- package/dist/docs/index.mjs +73 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/elevation-BRy3yFWT.mjs +113 -0
- package/dist/elevation-BRy3yFWT.mjs.map +1 -0
- package/dist/elevation-B_2dRLVP.d.mts +88 -0
- package/dist/elevation-B_2dRLVP.d.mts.map +1 -0
- package/dist/errorHandler-BbcgBmIH.d.mts +73 -0
- package/dist/errorHandler-BbcgBmIH.d.mts.map +1 -0
- package/dist/errorHandler-C1okiriz.mjs +109 -0
- package/dist/errorHandler-C1okiriz.mjs.map +1 -0
- package/dist/errors-B9bZok84.mjs +212 -0
- package/dist/errors-B9bZok84.mjs.map +1 -0
- package/dist/errors-ChKiFz62.d.mts +125 -0
- package/dist/errors-ChKiFz62.d.mts.map +1 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts +125 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts.map +1 -0
- package/dist/eventPlugin-DGR_B2on.mjs +230 -0
- package/dist/eventPlugin-DGR_B2on.mjs.map +1 -0
- package/dist/events/index.d.mts +54 -0
- package/dist/events/index.d.mts.map +1 -0
- package/dist/events/index.mjs +52 -0
- package/dist/events/index.mjs.map +1 -0
- package/dist/events/transports/redis-stream-entry.d.mts +2 -0
- package/dist/events/transports/redis-stream-entry.mjs +178 -0
- package/dist/events/transports/redis-stream-entry.mjs.map +1 -0
- package/dist/events/transports/redis.d.mts +77 -0
- package/dist/events/transports/redis.d.mts.map +1 -0
- package/dist/events/transports/redis.mjs +125 -0
- package/dist/events/transports/redis.mjs.map +1 -0
- package/dist/externalPaths-DlINfKbP.d.mts +51 -0
- package/dist/externalPaths-DlINfKbP.d.mts.map +1 -0
- package/dist/factory/index.d.mts +64 -0
- package/dist/factory/index.d.mts.map +1 -0
- package/dist/factory/index.mjs +3 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts +217 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +1 -0
- package/dist/fields-DyaDVX4J.d.mts +110 -0
- package/dist/fields-DyaDVX4J.d.mts.map +1 -0
- package/dist/fields-iagOozy0.mjs +115 -0
- package/dist/fields-iagOozy0.mjs.map +1 -0
- package/dist/hooks/index.d.mts +4 -0
- package/dist/hooks/index.mjs +3 -0
- package/dist/idempotency/index.d.mts +97 -0
- package/dist/idempotency/index.d.mts.map +1 -0
- package/dist/idempotency/index.mjs +320 -0
- package/dist/idempotency/index.mjs.map +1 -0
- package/dist/idempotency/mongodb.d.mts +2 -0
- package/dist/idempotency/mongodb.mjs +115 -0
- package/dist/idempotency/mongodb.mjs.map +1 -0
- package/dist/idempotency/redis.d.mts +2 -0
- package/dist/idempotency/redis.mjs +104 -0
- package/dist/idempotency/redis.mjs.map +1 -0
- package/dist/index.d.mts +261 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +105 -0
- package/dist/index.mjs.map +1 -0
- package/dist/integrations/event-gateway.d.mts +47 -0
- package/dist/integrations/event-gateway.d.mts.map +1 -0
- package/dist/integrations/event-gateway.mjs +44 -0
- package/dist/integrations/event-gateway.mjs.map +1 -0
- package/dist/integrations/index.d.mts +5 -0
- package/dist/integrations/index.mjs +1 -0
- package/dist/integrations/jobs.d.mts +104 -0
- package/dist/integrations/jobs.d.mts.map +1 -0
- package/dist/integrations/jobs.mjs +124 -0
- package/dist/integrations/jobs.mjs.map +1 -0
- package/dist/integrations/streamline.d.mts +61 -0
- package/dist/integrations/streamline.d.mts.map +1 -0
- package/dist/integrations/streamline.mjs +126 -0
- package/dist/integrations/streamline.mjs.map +1 -0
- package/dist/integrations/websocket.d.mts +83 -0
- package/dist/integrations/websocket.d.mts.map +1 -0
- package/dist/integrations/websocket.mjs +289 -0
- package/dist/integrations/websocket.mjs.map +1 -0
- package/dist/interface-B01JvPVc.d.mts +78 -0
- package/dist/interface-B01JvPVc.d.mts.map +1 -0
- package/dist/interface-CZe8IkMf.d.mts +55 -0
- package/dist/interface-CZe8IkMf.d.mts.map +1 -0
- package/dist/interface-Ch8HU9uM.d.mts +1098 -0
- package/dist/interface-Ch8HU9uM.d.mts.map +1 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs +54 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +1 -0
- package/dist/keys-BqNejWup.mjs +43 -0
- package/dist/keys-BqNejWup.mjs.map +1 -0
- package/dist/logger-Df2O2WsW.mjs +79 -0
- package/dist/logger-Df2O2WsW.mjs.map +1 -0
- package/dist/memory-cQgelFOj.mjs +144 -0
- package/dist/memory-cQgelFOj.mjs.map +1 -0
- package/dist/migrations/index.d.mts +157 -0
- package/dist/migrations/index.d.mts.map +1 -0
- package/dist/migrations/index.mjs +261 -0
- package/dist/migrations/index.mjs.map +1 -0
- package/dist/mongodb-BfJVlUJH.mjs +94 -0
- package/dist/mongodb-BfJVlUJH.mjs.map +1 -0
- package/dist/mongodb-CGzRbfAK.d.mts +119 -0
- package/dist/mongodb-CGzRbfAK.d.mts.map +1 -0
- package/dist/mongodb-JN-9JA7K.d.mts +72 -0
- package/dist/mongodb-JN-9JA7K.d.mts.map +1 -0
- package/dist/openapi-G3Cw7XuM.mjs +524 -0
- package/dist/openapi-G3Cw7XuM.mjs.map +1 -0
- package/dist/org/index.d.mts +69 -0
- package/dist/org/index.d.mts.map +1 -0
- package/dist/org/index.mjs +514 -0
- package/dist/org/index.mjs.map +1 -0
- package/dist/org/types.d.mts +83 -0
- package/dist/org/types.d.mts.map +1 -0
- package/dist/org/types.mjs +1 -0
- package/dist/permissions/index.d.mts +279 -0
- package/dist/permissions/index.d.mts.map +1 -0
- package/dist/permissions/index.mjs +579 -0
- package/dist/permissions/index.mjs.map +1 -0
- package/dist/plugins/index.d.mts +173 -0
- package/dist/plugins/index.d.mts.map +1 -0
- package/dist/plugins/index.mjs +523 -0
- package/dist/plugins/index.mjs.map +1 -0
- package/dist/plugins/response-cache.d.mts +88 -0
- package/dist/plugins/response-cache.d.mts.map +1 -0
- package/dist/plugins/response-cache.mjs +284 -0
- package/dist/plugins/response-cache.mjs.map +1 -0
- package/dist/plugins/tracing-entry.d.mts +2 -0
- package/dist/plugins/tracing-entry.mjs +186 -0
- package/dist/plugins/tracing-entry.mjs.map +1 -0
- package/dist/pluralize-CEweyOEm.mjs +87 -0
- package/dist/pluralize-CEweyOEm.mjs.map +1 -0
- package/dist/policies/{index.d.ts → index.d.mts} +204 -169
- package/dist/policies/index.d.mts.map +1 -0
- package/dist/policies/index.mjs +322 -0
- package/dist/policies/index.mjs.map +1 -0
- package/dist/presets/{index.d.ts → index.d.mts} +63 -131
- package/dist/presets/index.d.mts.map +1 -0
- package/dist/presets/index.mjs +144 -0
- package/dist/presets/index.mjs.map +1 -0
- package/dist/presets/multiTenant.d.mts +25 -0
- package/dist/presets/multiTenant.d.mts.map +1 -0
- package/dist/presets/multiTenant.mjs +114 -0
- package/dist/presets/multiTenant.mjs.map +1 -0
- package/dist/presets-BITljm96.mjs +120 -0
- package/dist/presets-BITljm96.mjs.map +1 -0
- package/dist/presets-DzSMwlKj.d.mts +58 -0
- package/dist/presets-DzSMwlKj.d.mts.map +1 -0
- package/dist/prisma-DJbMt3yf.mjs +628 -0
- package/dist/prisma-DJbMt3yf.mjs.map +1 -0
- package/dist/prisma-Dg9GoVdj.d.mts +275 -0
- package/dist/prisma-Dg9GoVdj.d.mts.map +1 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts +72 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts.map +1 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs +139 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +1 -0
- package/dist/redis-D-JAeLtm.d.mts +50 -0
- package/dist/redis-D-JAeLtm.d.mts.map +1 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts +104 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts.map +1 -0
- package/dist/registry/index.d.mts +12 -0
- package/dist/registry/index.d.mts.map +1 -0
- package/dist/registry/index.mjs +4 -0
- package/dist/requestContext-QQD6ROJc.mjs +56 -0
- package/dist/requestContext-QQD6ROJc.mjs.map +1 -0
- package/dist/schemaConverter-BwrmWroW.mjs +99 -0
- package/dist/schemaConverter-BwrmWroW.mjs.map +1 -0
- package/dist/schemas/index.d.mts +64 -0
- package/dist/schemas/index.d.mts.map +1 -0
- package/dist/schemas/index.mjs +83 -0
- package/dist/schemas/index.mjs.map +1 -0
- package/dist/scope/index.d.mts +22 -0
- package/dist/scope/index.d.mts.map +1 -0
- package/dist/scope/index.mjs +66 -0
- package/dist/scope/index.mjs.map +1 -0
- package/dist/sessionManager-jPKLbHE0.d.mts +187 -0
- package/dist/sessionManager-jPKLbHE0.d.mts.map +1 -0
- package/dist/sse-B3c3_yZp.mjs +124 -0
- package/dist/sse-B3c3_yZp.mjs.map +1 -0
- package/dist/testing/index.d.mts +908 -0
- package/dist/testing/index.d.mts.map +1 -0
- package/dist/testing/index.mjs +1977 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/tracing-Cc7vVQPp.d.mts +71 -0
- package/dist/tracing-Cc7vVQPp.d.mts.map +1 -0
- package/dist/typeGuards-DhMNLuvU.mjs +10 -0
- package/dist/typeGuards-DhMNLuvU.mjs.map +1 -0
- package/dist/types/index.d.mts +947 -0
- package/dist/types/index.d.mts.map +1 -0
- package/dist/types/index.mjs +15 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/types-Beqn1Un7.mjs +39 -0
- package/dist/types-Beqn1Un7.mjs.map +1 -0
- package/dist/types-CIgB7UUl.d.mts +446 -0
- package/dist/types-CIgB7UUl.d.mts.map +1 -0
- package/dist/types-aYB4V7uN.d.mts +87 -0
- package/dist/types-aYB4V7uN.d.mts.map +1 -0
- package/dist/utils/index.d.mts +748 -0
- package/dist/utils/index.d.mts.map +1 -0
- package/dist/utils/index.mjs +6 -0
- package/package.json +194 -68
- package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
- package/dist/adapters/index.d.ts +0 -237
- package/dist/adapters/index.js +0 -668
- package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
- package/dist/audit/index.d.ts +0 -195
- package/dist/audit/index.js +0 -319
- package/dist/auth/index.d.ts +0 -47
- package/dist/auth/index.js +0 -174
- package/dist/cli/commands/docs.d.ts +0 -11
- package/dist/cli/commands/docs.js +0 -474
- package/dist/cli/commands/generate.js +0 -334
- package/dist/cli/commands/introspect.d.ts +0 -8
- package/dist/cli/commands/introspect.js +0 -338
- package/dist/cli/index.d.ts +0 -4
- package/dist/cli/index.js +0 -3269
- package/dist/core/index.d.ts +0 -220
- package/dist/core/index.js +0 -2786
- package/dist/createApp-Ce9wl8W9.d.ts +0 -77
- package/dist/docs/index.d.ts +0 -166
- package/dist/docs/index.js +0 -658
- package/dist/errors-8WIxGS_6.d.ts +0 -122
- package/dist/events/index.d.ts +0 -117
- package/dist/events/index.js +0 -89
- package/dist/factory/index.d.ts +0 -38
- package/dist/factory/index.js +0 -1652
- package/dist/hooks/index.d.ts +0 -4
- package/dist/hooks/index.js +0 -199
- package/dist/idempotency/index.d.ts +0 -323
- package/dist/idempotency/index.js +0 -500
- package/dist/index-B4t03KQ0.d.ts +0 -1366
- package/dist/index.d.ts +0 -135
- package/dist/index.js +0 -4756
- package/dist/migrations/index.d.ts +0 -185
- package/dist/migrations/index.js +0 -274
- package/dist/org/index.d.ts +0 -129
- package/dist/org/index.js +0 -220
- package/dist/permissions/index.d.ts +0 -144
- package/dist/permissions/index.js +0 -103
- package/dist/plugins/index.d.ts +0 -46
- package/dist/plugins/index.js +0 -1069
- package/dist/policies/index.js +0 -196
- package/dist/presets/index.js +0 -384
- package/dist/presets/multiTenant.d.ts +0 -39
- package/dist/presets/multiTenant.js +0 -112
- package/dist/registry/index.d.ts +0 -16
- package/dist/registry/index.js +0 -253
- package/dist/testing/index.d.ts +0 -618
- package/dist/testing/index.js +0 -48020
- package/dist/types/index.d.ts +0 -4
- package/dist/types/index.js +0 -8
- package/dist/types-B99TBmFV.d.ts +0 -76
- package/dist/types-BvckRbs2.d.ts +0 -143
- package/dist/utils/index.d.ts +0 -679
- package/dist/utils/index.js +0 -931
package/dist/index.js
DELETED
|
@@ -1,4756 +0,0 @@
|
|
|
1
|
-
import fp from 'fastify-plugin';
|
|
2
|
-
import { randomUUID } from 'crypto';
|
|
3
|
-
import { createRequire } from 'module';
|
|
4
|
-
import Fastify from 'fastify';
|
|
5
|
-
import qs from 'qs';
|
|
6
|
-
|
|
7
|
-
var __defProp = Object.defineProperty;
|
|
8
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
9
|
-
var __esm = (fn, res) => function __init() {
|
|
10
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
-
};
|
|
12
|
-
var __export = (target, all) => {
|
|
13
|
-
for (var name in all)
|
|
14
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// src/hooks/HookSystem.ts
|
|
18
|
-
var HookSystem, hookSystem;
|
|
19
|
-
var init_HookSystem = __esm({
|
|
20
|
-
"src/hooks/HookSystem.ts"() {
|
|
21
|
-
HookSystem = class {
|
|
22
|
-
hooks;
|
|
23
|
-
logger;
|
|
24
|
-
constructor(options) {
|
|
25
|
-
this.hooks = /* @__PURE__ */ new Map();
|
|
26
|
-
this.logger = options?.logger ?? { error: (...args) => console.error(...args) };
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Generate hook key
|
|
30
|
-
*/
|
|
31
|
-
getKey(resource, operation, phase) {
|
|
32
|
-
return `${resource}:${operation}:${phase}`;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Register a hook
|
|
36
|
-
* Supports both object parameter and positional arguments
|
|
37
|
-
*/
|
|
38
|
-
register(resourceOrOptions, operation, phase, handler, priority = 10) {
|
|
39
|
-
let resource;
|
|
40
|
-
let finalOperation;
|
|
41
|
-
let finalPhase;
|
|
42
|
-
let finalHandler;
|
|
43
|
-
let finalPriority;
|
|
44
|
-
if (typeof resourceOrOptions === "object") {
|
|
45
|
-
resource = resourceOrOptions.resource;
|
|
46
|
-
finalOperation = resourceOrOptions.operation;
|
|
47
|
-
finalPhase = resourceOrOptions.phase;
|
|
48
|
-
finalHandler = resourceOrOptions.handler;
|
|
49
|
-
finalPriority = resourceOrOptions.priority ?? 10;
|
|
50
|
-
} else {
|
|
51
|
-
resource = resourceOrOptions;
|
|
52
|
-
finalOperation = operation;
|
|
53
|
-
finalPhase = phase;
|
|
54
|
-
finalHandler = handler;
|
|
55
|
-
finalPriority = priority;
|
|
56
|
-
}
|
|
57
|
-
const key = this.getKey(resource, finalOperation, finalPhase);
|
|
58
|
-
if (!this.hooks.has(key)) {
|
|
59
|
-
this.hooks.set(key, []);
|
|
60
|
-
}
|
|
61
|
-
const registration = {
|
|
62
|
-
resource,
|
|
63
|
-
operation: finalOperation,
|
|
64
|
-
phase: finalPhase,
|
|
65
|
-
handler: finalHandler,
|
|
66
|
-
priority: finalPriority
|
|
67
|
-
};
|
|
68
|
-
const hooks = this.hooks.get(key);
|
|
69
|
-
hooks.push(registration);
|
|
70
|
-
hooks.sort((a, b) => a.priority - b.priority);
|
|
71
|
-
return () => {
|
|
72
|
-
const idx = hooks.indexOf(registration);
|
|
73
|
-
if (idx !== -1) {
|
|
74
|
-
hooks.splice(idx, 1);
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Register before hook
|
|
80
|
-
*/
|
|
81
|
-
before(resource, operation, handler, priority = 10) {
|
|
82
|
-
return this.register(resource, operation, "before", handler, priority);
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Register after hook
|
|
86
|
-
*/
|
|
87
|
-
after(resource, operation, handler, priority = 10) {
|
|
88
|
-
return this.register(resource, operation, "after", handler, priority);
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Execute hooks for a given context
|
|
92
|
-
*/
|
|
93
|
-
async execute(ctx) {
|
|
94
|
-
const key = this.getKey(ctx.resource, ctx.operation, ctx.phase);
|
|
95
|
-
const hooks = this.hooks.get(key) ?? [];
|
|
96
|
-
const wildcardKey = this.getKey("*", ctx.operation, ctx.phase);
|
|
97
|
-
const wildcardHooks = this.hooks.get(wildcardKey) ?? [];
|
|
98
|
-
const allHooks = [...wildcardHooks, ...hooks];
|
|
99
|
-
allHooks.sort((a, b) => a.priority - b.priority);
|
|
100
|
-
let result = ctx.data;
|
|
101
|
-
for (const hook of allHooks) {
|
|
102
|
-
const handlerContext = {
|
|
103
|
-
resource: ctx.resource,
|
|
104
|
-
operation: ctx.operation,
|
|
105
|
-
phase: ctx.phase,
|
|
106
|
-
data: result,
|
|
107
|
-
result: ctx.result,
|
|
108
|
-
user: ctx.user,
|
|
109
|
-
context: ctx.context,
|
|
110
|
-
meta: ctx.meta
|
|
111
|
-
};
|
|
112
|
-
const hookResult = await hook.handler(handlerContext);
|
|
113
|
-
if (hookResult !== void 0 && hookResult !== null) {
|
|
114
|
-
result = hookResult;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return result;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Execute before hooks
|
|
121
|
-
*/
|
|
122
|
-
async executeBefore(resource, operation, data, options) {
|
|
123
|
-
const result = await this.execute({
|
|
124
|
-
resource,
|
|
125
|
-
operation,
|
|
126
|
-
phase: "before",
|
|
127
|
-
data,
|
|
128
|
-
user: options?.user,
|
|
129
|
-
context: options?.context,
|
|
130
|
-
meta: options?.meta
|
|
131
|
-
});
|
|
132
|
-
return result ?? data;
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Execute after hooks
|
|
136
|
-
* Errors in after hooks are logged but don't fail the request
|
|
137
|
-
*/
|
|
138
|
-
async executeAfter(resource, operation, result, options) {
|
|
139
|
-
try {
|
|
140
|
-
await this.execute({
|
|
141
|
-
resource,
|
|
142
|
-
operation,
|
|
143
|
-
phase: "after",
|
|
144
|
-
result,
|
|
145
|
-
user: options?.user,
|
|
146
|
-
context: options?.context,
|
|
147
|
-
meta: options?.meta
|
|
148
|
-
});
|
|
149
|
-
} catch (error) {
|
|
150
|
-
this.logger.error(
|
|
151
|
-
`[HookSystem] Error in after hook for ${resource}:${operation}:`,
|
|
152
|
-
error
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Get all registered hooks
|
|
158
|
-
*/
|
|
159
|
-
getAll() {
|
|
160
|
-
const all = [];
|
|
161
|
-
for (const hooks of this.hooks.values()) {
|
|
162
|
-
all.push(...hooks);
|
|
163
|
-
}
|
|
164
|
-
return all;
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Get hooks for a specific resource
|
|
168
|
-
*/
|
|
169
|
-
getForResource(resource) {
|
|
170
|
-
const all = [];
|
|
171
|
-
for (const [key, hooks] of this.hooks.entries()) {
|
|
172
|
-
if (key.startsWith(`${resource}:`)) {
|
|
173
|
-
all.push(...hooks);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return all;
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Clear all hooks
|
|
180
|
-
*/
|
|
181
|
-
clear() {
|
|
182
|
-
this.hooks.clear();
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Clear hooks for a specific resource
|
|
186
|
-
*/
|
|
187
|
-
clearResource(resource) {
|
|
188
|
-
for (const key of this.hooks.keys()) {
|
|
189
|
-
if (key.startsWith(`${resource}:`)) {
|
|
190
|
-
this.hooks.delete(key);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
hookSystem = new HookSystem();
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// src/registry/ResourceRegistry.ts
|
|
200
|
-
var ResourceRegistry, registryKey, globalScope, resourceRegistry;
|
|
201
|
-
var init_ResourceRegistry = __esm({
|
|
202
|
-
"src/registry/ResourceRegistry.ts"() {
|
|
203
|
-
ResourceRegistry = class {
|
|
204
|
-
_resources;
|
|
205
|
-
_frozen;
|
|
206
|
-
constructor() {
|
|
207
|
-
this._resources = /* @__PURE__ */ new Map();
|
|
208
|
-
this._frozen = false;
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Register a resource
|
|
212
|
-
*/
|
|
213
|
-
register(resource, options = {}) {
|
|
214
|
-
if (this._frozen) {
|
|
215
|
-
throw new Error(
|
|
216
|
-
`Registry frozen. Cannot register '${resource.name}' after startup.`
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
if (this._resources.has(resource.name)) {
|
|
220
|
-
throw new Error(`Resource '${resource.name}' already registered.`);
|
|
221
|
-
}
|
|
222
|
-
const entry = {
|
|
223
|
-
name: resource.name,
|
|
224
|
-
displayName: resource.displayName,
|
|
225
|
-
tag: resource.tag,
|
|
226
|
-
prefix: resource.prefix,
|
|
227
|
-
module: options.module ?? void 0,
|
|
228
|
-
adapter: resource.adapter ? {
|
|
229
|
-
type: resource.adapter.type,
|
|
230
|
-
name: resource.adapter.name
|
|
231
|
-
} : null,
|
|
232
|
-
permissions: resource.permissions,
|
|
233
|
-
presets: resource._appliedPresets ?? [],
|
|
234
|
-
routes: [],
|
|
235
|
-
// Populated later by getIntrospection()
|
|
236
|
-
additionalRoutes: resource.additionalRoutes.map((r) => ({
|
|
237
|
-
method: r.method,
|
|
238
|
-
path: r.path,
|
|
239
|
-
handler: typeof r.handler === "string" ? r.handler : r.handler.name || "anonymous",
|
|
240
|
-
summary: r.summary,
|
|
241
|
-
description: r.description,
|
|
242
|
-
permissions: r.permissions,
|
|
243
|
-
wrapHandler: r.wrapHandler,
|
|
244
|
-
schema: r.schema
|
|
245
|
-
// Include schema for OpenAPI docs
|
|
246
|
-
})),
|
|
247
|
-
events: Object.keys(resource.events ?? {}),
|
|
248
|
-
registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
249
|
-
disableDefaultRoutes: resource.disableDefaultRoutes,
|
|
250
|
-
openApiSchemas: options.openApiSchemas,
|
|
251
|
-
plugin: resource.toPlugin()
|
|
252
|
-
// Store plugin factory
|
|
253
|
-
};
|
|
254
|
-
this._resources.set(resource.name, entry);
|
|
255
|
-
return this;
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Get resource by name
|
|
259
|
-
*/
|
|
260
|
-
get(name) {
|
|
261
|
-
return this._resources.get(name);
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Get all resources
|
|
265
|
-
*/
|
|
266
|
-
getAll() {
|
|
267
|
-
return Array.from(this._resources.values());
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Get resources by module
|
|
271
|
-
*/
|
|
272
|
-
getByModule(moduleName) {
|
|
273
|
-
return this.getAll().filter((r) => r.module === moduleName);
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Get resources by preset
|
|
277
|
-
*/
|
|
278
|
-
getByPreset(presetName) {
|
|
279
|
-
return this.getAll().filter((r) => r.presets.includes(presetName));
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Check if resource exists
|
|
283
|
-
*/
|
|
284
|
-
has(name) {
|
|
285
|
-
return this._resources.has(name);
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Get registry statistics
|
|
289
|
-
*/
|
|
290
|
-
getStats() {
|
|
291
|
-
const resources = this.getAll();
|
|
292
|
-
const presetCounts = {};
|
|
293
|
-
for (const r of resources) {
|
|
294
|
-
for (const preset of r.presets) {
|
|
295
|
-
presetCounts[preset] = (presetCounts[preset] ?? 0) + 1;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
return {
|
|
299
|
-
totalResources: resources.length,
|
|
300
|
-
byModule: this._groupBy(resources, "module"),
|
|
301
|
-
presetUsage: presetCounts,
|
|
302
|
-
totalRoutes: resources.reduce((sum, r) => {
|
|
303
|
-
const defaultRouteCount = r.disableDefaultRoutes ? 0 : 5;
|
|
304
|
-
return sum + (r.additionalRoutes?.length ?? 0) + defaultRouteCount;
|
|
305
|
-
}, 0),
|
|
306
|
-
totalEvents: resources.reduce((sum, r) => sum + (r.events?.length ?? 0), 0)
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Get full introspection data
|
|
311
|
-
*/
|
|
312
|
-
getIntrospection() {
|
|
313
|
-
return {
|
|
314
|
-
resources: this.getAll().map((r) => {
|
|
315
|
-
const defaultRoutes = r.disableDefaultRoutes ? [] : [
|
|
316
|
-
{ method: "GET", path: r.prefix, operation: "list" },
|
|
317
|
-
{ method: "GET", path: `${r.prefix}/:id`, operation: "get" },
|
|
318
|
-
{ method: "POST", path: r.prefix, operation: "create" },
|
|
319
|
-
{ method: "PATCH", path: `${r.prefix}/:id`, operation: "update" },
|
|
320
|
-
{ method: "DELETE", path: `${r.prefix}/:id`, operation: "delete" }
|
|
321
|
-
];
|
|
322
|
-
return {
|
|
323
|
-
name: r.name,
|
|
324
|
-
displayName: r.displayName,
|
|
325
|
-
prefix: r.prefix,
|
|
326
|
-
module: r.module,
|
|
327
|
-
presets: r.presets,
|
|
328
|
-
permissions: r.permissions,
|
|
329
|
-
routes: [
|
|
330
|
-
...defaultRoutes,
|
|
331
|
-
...r.additionalRoutes?.map((ar) => ({
|
|
332
|
-
method: ar.method,
|
|
333
|
-
path: `${r.prefix}${ar.path}`,
|
|
334
|
-
operation: typeof ar.handler === "string" ? ar.handler : "custom",
|
|
335
|
-
handler: typeof ar.handler === "string" ? ar.handler : void 0,
|
|
336
|
-
summary: ar.summary
|
|
337
|
-
})) ?? []
|
|
338
|
-
],
|
|
339
|
-
events: r.events
|
|
340
|
-
};
|
|
341
|
-
}),
|
|
342
|
-
stats: this.getStats(),
|
|
343
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
/**
|
|
347
|
-
* Freeze registry (prevent further registrations)
|
|
348
|
-
*/
|
|
349
|
-
freeze() {
|
|
350
|
-
this._frozen = true;
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Check if frozen
|
|
354
|
-
*/
|
|
355
|
-
isFrozen() {
|
|
356
|
-
return this._frozen;
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* Unfreeze registry (for testing)
|
|
360
|
-
*/
|
|
361
|
-
_unfreeze() {
|
|
362
|
-
this._frozen = false;
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Clear all resources (for testing)
|
|
366
|
-
*/
|
|
367
|
-
_clear() {
|
|
368
|
-
this._resources.clear();
|
|
369
|
-
this._frozen = false;
|
|
370
|
-
}
|
|
371
|
-
/**
|
|
372
|
-
* Group by key
|
|
373
|
-
*/
|
|
374
|
-
_groupBy(arr, key) {
|
|
375
|
-
const result = {};
|
|
376
|
-
for (const item of arr) {
|
|
377
|
-
const k = String(item[key] ?? "uncategorized");
|
|
378
|
-
result[k] = (result[k] ?? 0) + 1;
|
|
379
|
-
}
|
|
380
|
-
return result;
|
|
381
|
-
}
|
|
382
|
-
};
|
|
383
|
-
registryKey = /* @__PURE__ */ Symbol.for("arc.resourceRegistry");
|
|
384
|
-
globalScope = globalThis;
|
|
385
|
-
resourceRegistry = globalScope[registryKey] ?? new ResourceRegistry();
|
|
386
|
-
if (!globalScope[registryKey]) {
|
|
387
|
-
globalScope[registryKey] = resourceRegistry;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
// src/utils/errors.ts
|
|
393
|
-
function isArcError(error) {
|
|
394
|
-
return error instanceof ArcError;
|
|
395
|
-
}
|
|
396
|
-
var ArcError, NotFoundError, ValidationError, UnauthorizedError, ForbiddenError;
|
|
397
|
-
var init_errors = __esm({
|
|
398
|
-
"src/utils/errors.ts"() {
|
|
399
|
-
ArcError = class extends Error {
|
|
400
|
-
name;
|
|
401
|
-
code;
|
|
402
|
-
statusCode;
|
|
403
|
-
details;
|
|
404
|
-
cause;
|
|
405
|
-
timestamp;
|
|
406
|
-
requestId;
|
|
407
|
-
constructor(message, options = {}) {
|
|
408
|
-
super(message);
|
|
409
|
-
this.name = "ArcError";
|
|
410
|
-
this.code = options.code ?? "ARC_ERROR";
|
|
411
|
-
this.statusCode = options.statusCode ?? 500;
|
|
412
|
-
this.details = options.details;
|
|
413
|
-
this.cause = options.cause;
|
|
414
|
-
this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
415
|
-
this.requestId = options.requestId;
|
|
416
|
-
if (Error.captureStackTrace) {
|
|
417
|
-
Error.captureStackTrace(this, this.constructor);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* Set request ID (typically from request context)
|
|
422
|
-
*/
|
|
423
|
-
withRequestId(requestId) {
|
|
424
|
-
this.requestId = requestId;
|
|
425
|
-
return this;
|
|
426
|
-
}
|
|
427
|
-
/**
|
|
428
|
-
* Convert to JSON response
|
|
429
|
-
*/
|
|
430
|
-
toJSON() {
|
|
431
|
-
return {
|
|
432
|
-
success: false,
|
|
433
|
-
error: this.message,
|
|
434
|
-
code: this.code,
|
|
435
|
-
timestamp: this.timestamp,
|
|
436
|
-
...this.requestId && { requestId: this.requestId },
|
|
437
|
-
...this.details && { details: this.details }
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
|
-
NotFoundError = class extends ArcError {
|
|
442
|
-
constructor(resource, identifier) {
|
|
443
|
-
const message = identifier ? `${resource} with identifier '${identifier}' not found` : `${resource} not found`;
|
|
444
|
-
super(message, {
|
|
445
|
-
code: "NOT_FOUND",
|
|
446
|
-
statusCode: 404,
|
|
447
|
-
details: { resource, identifier }
|
|
448
|
-
});
|
|
449
|
-
this.name = "NotFoundError";
|
|
450
|
-
}
|
|
451
|
-
};
|
|
452
|
-
ValidationError = class extends ArcError {
|
|
453
|
-
errors;
|
|
454
|
-
constructor(message, errors = []) {
|
|
455
|
-
super(message, {
|
|
456
|
-
code: "VALIDATION_ERROR",
|
|
457
|
-
statusCode: 400,
|
|
458
|
-
details: { errors }
|
|
459
|
-
});
|
|
460
|
-
this.name = "ValidationError";
|
|
461
|
-
this.errors = errors;
|
|
462
|
-
}
|
|
463
|
-
};
|
|
464
|
-
UnauthorizedError = class extends ArcError {
|
|
465
|
-
constructor(message = "Authentication required") {
|
|
466
|
-
super(message, {
|
|
467
|
-
code: "UNAUTHORIZED",
|
|
468
|
-
statusCode: 401
|
|
469
|
-
});
|
|
470
|
-
this.name = "UnauthorizedError";
|
|
471
|
-
}
|
|
472
|
-
};
|
|
473
|
-
ForbiddenError = class extends ArcError {
|
|
474
|
-
constructor(message = "Access denied") {
|
|
475
|
-
super(message, {
|
|
476
|
-
code: "FORBIDDEN",
|
|
477
|
-
statusCode: 403
|
|
478
|
-
});
|
|
479
|
-
this.name = "ForbiddenError";
|
|
480
|
-
}
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
});
|
|
484
|
-
var requestIdPlugin, requestId_default;
|
|
485
|
-
var init_requestId = __esm({
|
|
486
|
-
"src/plugins/requestId.ts"() {
|
|
487
|
-
requestIdPlugin = async (fastify, opts = {}) => {
|
|
488
|
-
const {
|
|
489
|
-
header = "x-request-id",
|
|
490
|
-
generator = randomUUID,
|
|
491
|
-
setResponseHeader = true
|
|
492
|
-
} = opts;
|
|
493
|
-
if (!fastify.hasRequestDecorator("requestId")) {
|
|
494
|
-
fastify.decorateRequest("requestId", "");
|
|
495
|
-
}
|
|
496
|
-
fastify.addHook("onRequest", async (request) => {
|
|
497
|
-
const incomingId = request.headers[header];
|
|
498
|
-
const requestId = typeof incomingId === "string" && incomingId.trim() ? incomingId.trim() : generator();
|
|
499
|
-
request.id = requestId;
|
|
500
|
-
request.requestId = requestId;
|
|
501
|
-
});
|
|
502
|
-
if (setResponseHeader) {
|
|
503
|
-
fastify.addHook("onSend", async (request, reply) => {
|
|
504
|
-
reply.header(header, request.requestId);
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
fastify.log?.debug?.("Request ID plugin registered");
|
|
508
|
-
};
|
|
509
|
-
requestId_default = fp(requestIdPlugin, {
|
|
510
|
-
name: "arc-request-id",
|
|
511
|
-
fastify: "5.x"
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
async function runChecks(checks) {
|
|
516
|
-
const results = [];
|
|
517
|
-
for (const check of checks) {
|
|
518
|
-
const start = Date.now();
|
|
519
|
-
const timeout = check.timeout ?? 5e3;
|
|
520
|
-
try {
|
|
521
|
-
const checkPromise = Promise.resolve(check.check());
|
|
522
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
523
|
-
setTimeout(() => reject(new Error("Health check timeout")), timeout);
|
|
524
|
-
});
|
|
525
|
-
const healthy = await Promise.race([checkPromise, timeoutPromise]);
|
|
526
|
-
results.push({
|
|
527
|
-
name: check.name,
|
|
528
|
-
healthy: Boolean(healthy),
|
|
529
|
-
duration: Date.now() - start
|
|
530
|
-
});
|
|
531
|
-
} catch (err) {
|
|
532
|
-
results.push({
|
|
533
|
-
name: check.name,
|
|
534
|
-
healthy: false,
|
|
535
|
-
duration: Date.now() - start,
|
|
536
|
-
error: err.message
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
return results;
|
|
541
|
-
}
|
|
542
|
-
var httpMetrics, healthPlugin, health_default;
|
|
543
|
-
var init_health = __esm({
|
|
544
|
-
"src/plugins/health.ts"() {
|
|
545
|
-
httpMetrics = {
|
|
546
|
-
requestsTotal: {},
|
|
547
|
-
requestDurations: [],
|
|
548
|
-
startTime: Date.now()
|
|
549
|
-
};
|
|
550
|
-
healthPlugin = async (fastify, opts = {}) => {
|
|
551
|
-
const {
|
|
552
|
-
prefix = "/_health",
|
|
553
|
-
checks = [],
|
|
554
|
-
metrics = false,
|
|
555
|
-
metricsCollector,
|
|
556
|
-
version: version2,
|
|
557
|
-
collectHttpMetrics = metrics
|
|
558
|
-
} = opts;
|
|
559
|
-
fastify.get(`${prefix}/live`, {
|
|
560
|
-
schema: {
|
|
561
|
-
tags: ["Health"],
|
|
562
|
-
summary: "Liveness probe",
|
|
563
|
-
description: "Returns 200 if the process is alive",
|
|
564
|
-
response: {
|
|
565
|
-
200: {
|
|
566
|
-
type: "object",
|
|
567
|
-
properties: {
|
|
568
|
-
status: { type: "string", enum: ["ok"] },
|
|
569
|
-
timestamp: { type: "string" },
|
|
570
|
-
version: { type: "string" }
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
}, async () => {
|
|
576
|
-
return {
|
|
577
|
-
status: "ok",
|
|
578
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
579
|
-
...version2 ? { version: version2 } : {}
|
|
580
|
-
};
|
|
581
|
-
});
|
|
582
|
-
fastify.get(`${prefix}/ready`, {
|
|
583
|
-
schema: {
|
|
584
|
-
tags: ["Health"],
|
|
585
|
-
summary: "Readiness probe",
|
|
586
|
-
description: "Returns 200 if all dependencies are healthy",
|
|
587
|
-
response: {
|
|
588
|
-
200: {
|
|
589
|
-
type: "object",
|
|
590
|
-
properties: {
|
|
591
|
-
status: { type: "string", enum: ["ready", "not_ready"] },
|
|
592
|
-
timestamp: { type: "string" },
|
|
593
|
-
checks: {
|
|
594
|
-
type: "array",
|
|
595
|
-
items: {
|
|
596
|
-
type: "object",
|
|
597
|
-
properties: {
|
|
598
|
-
name: { type: "string" },
|
|
599
|
-
healthy: { type: "boolean" },
|
|
600
|
-
duration: { type: "number" },
|
|
601
|
-
error: { type: "string" }
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
},
|
|
607
|
-
503: {
|
|
608
|
-
type: "object",
|
|
609
|
-
properties: {
|
|
610
|
-
status: { type: "string", enum: ["not_ready"] },
|
|
611
|
-
timestamp: { type: "string" },
|
|
612
|
-
checks: { type: "array" }
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}, async (_, reply) => {
|
|
618
|
-
const results = await runChecks(checks);
|
|
619
|
-
const criticalFailed = results.some(
|
|
620
|
-
(r) => !r.healthy && (checks.find((c) => c.name === r.name)?.critical ?? true)
|
|
621
|
-
);
|
|
622
|
-
const response = {
|
|
623
|
-
status: criticalFailed ? "not_ready" : "ready",
|
|
624
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
625
|
-
checks: results
|
|
626
|
-
};
|
|
627
|
-
if (criticalFailed) {
|
|
628
|
-
reply.code(503);
|
|
629
|
-
}
|
|
630
|
-
return response;
|
|
631
|
-
});
|
|
632
|
-
if (metrics) {
|
|
633
|
-
fastify.get(`${prefix}/metrics`, async (_, reply) => {
|
|
634
|
-
reply.type("text/plain; charset=utf-8");
|
|
635
|
-
if (metricsCollector) {
|
|
636
|
-
return await metricsCollector();
|
|
637
|
-
}
|
|
638
|
-
const uptime = process.uptime();
|
|
639
|
-
const memory = process.memoryUsage();
|
|
640
|
-
const cpu = process.cpuUsage();
|
|
641
|
-
const lines = [
|
|
642
|
-
"# HELP process_uptime_seconds Process uptime in seconds",
|
|
643
|
-
"# TYPE process_uptime_seconds gauge",
|
|
644
|
-
`process_uptime_seconds ${uptime.toFixed(2)}`,
|
|
645
|
-
"",
|
|
646
|
-
"# HELP process_memory_heap_bytes Heap memory usage in bytes",
|
|
647
|
-
"# TYPE process_memory_heap_bytes gauge",
|
|
648
|
-
`process_memory_heap_bytes{type="used"} ${memory.heapUsed}`,
|
|
649
|
-
`process_memory_heap_bytes{type="total"} ${memory.heapTotal}`,
|
|
650
|
-
"",
|
|
651
|
-
"# HELP process_memory_rss_bytes RSS memory in bytes",
|
|
652
|
-
"# TYPE process_memory_rss_bytes gauge",
|
|
653
|
-
`process_memory_rss_bytes ${memory.rss}`,
|
|
654
|
-
"",
|
|
655
|
-
"# HELP process_memory_external_bytes External memory in bytes",
|
|
656
|
-
"# TYPE process_memory_external_bytes gauge",
|
|
657
|
-
`process_memory_external_bytes ${memory.external}`,
|
|
658
|
-
"",
|
|
659
|
-
"# HELP process_cpu_user_microseconds User CPU time in microseconds",
|
|
660
|
-
"# TYPE process_cpu_user_microseconds counter",
|
|
661
|
-
`process_cpu_user_microseconds ${cpu.user}`,
|
|
662
|
-
"",
|
|
663
|
-
"# HELP process_cpu_system_microseconds System CPU time in microseconds",
|
|
664
|
-
"# TYPE process_cpu_system_microseconds counter",
|
|
665
|
-
`process_cpu_system_microseconds ${cpu.system}`,
|
|
666
|
-
""
|
|
667
|
-
];
|
|
668
|
-
if (collectHttpMetrics && Object.keys(httpMetrics.requestsTotal).length > 0) {
|
|
669
|
-
lines.push(
|
|
670
|
-
"# HELP http_requests_total Total HTTP requests by status code",
|
|
671
|
-
"# TYPE http_requests_total counter"
|
|
672
|
-
);
|
|
673
|
-
for (const [status, count] of Object.entries(httpMetrics.requestsTotal)) {
|
|
674
|
-
lines.push(`http_requests_total{status="${status}"} ${count}`);
|
|
675
|
-
}
|
|
676
|
-
lines.push("");
|
|
677
|
-
if (httpMetrics.requestDurations.length > 0) {
|
|
678
|
-
const sorted = [...httpMetrics.requestDurations].sort((a, b) => a - b);
|
|
679
|
-
const p50 = sorted[Math.floor(sorted.length * 0.5)] || 0;
|
|
680
|
-
const p95 = sorted[Math.floor(sorted.length * 0.95)] || 0;
|
|
681
|
-
const p99 = sorted[Math.floor(sorted.length * 0.99)] || 0;
|
|
682
|
-
const sum = sorted.reduce((a, b) => a + b, 0);
|
|
683
|
-
lines.push(
|
|
684
|
-
"# HELP http_request_duration_milliseconds HTTP request duration",
|
|
685
|
-
"# TYPE http_request_duration_milliseconds summary",
|
|
686
|
-
`http_request_duration_milliseconds{quantile="0.5"} ${p50.toFixed(2)}`,
|
|
687
|
-
`http_request_duration_milliseconds{quantile="0.95"} ${p95.toFixed(2)}`,
|
|
688
|
-
`http_request_duration_milliseconds{quantile="0.99"} ${p99.toFixed(2)}`,
|
|
689
|
-
`http_request_duration_milliseconds_sum ${sum.toFixed(2)}`,
|
|
690
|
-
`http_request_duration_milliseconds_count ${sorted.length}`,
|
|
691
|
-
""
|
|
692
|
-
);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
return lines.join("\n");
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
if (collectHttpMetrics) {
|
|
699
|
-
fastify.addHook("onRequest", async (request) => {
|
|
700
|
-
request._startTime = Date.now();
|
|
701
|
-
});
|
|
702
|
-
fastify.addHook("onResponse", async (request, reply) => {
|
|
703
|
-
const duration = Date.now() - (request._startTime || Date.now());
|
|
704
|
-
const statusBucket = `${Math.floor(reply.statusCode / 100)}xx`;
|
|
705
|
-
httpMetrics.requestsTotal[statusBucket] = (httpMetrics.requestsTotal[statusBucket] || 0) + 1;
|
|
706
|
-
httpMetrics.requestDurations.push(duration);
|
|
707
|
-
if (httpMetrics.requestDurations.length > 1e4) {
|
|
708
|
-
httpMetrics.requestDurations.shift();
|
|
709
|
-
}
|
|
710
|
-
});
|
|
711
|
-
}
|
|
712
|
-
fastify.log?.info?.(`Health plugin registered at ${prefix}`);
|
|
713
|
-
};
|
|
714
|
-
health_default = fp(healthPlugin, {
|
|
715
|
-
name: "arc-health",
|
|
716
|
-
fastify: "5.x"
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
});
|
|
720
|
-
function createTracerProvider(options) {
|
|
721
|
-
if (!isAvailable) {
|
|
722
|
-
return null;
|
|
723
|
-
}
|
|
724
|
-
const { serviceName = "@classytic/arc", exporterUrl = "http://localhost:4318/v1/traces" } = options;
|
|
725
|
-
const exporter = new OTLPTraceExporter({
|
|
726
|
-
url: exporterUrl
|
|
727
|
-
});
|
|
728
|
-
const provider = new NodeTracerProvider({
|
|
729
|
-
resource: {
|
|
730
|
-
attributes: {
|
|
731
|
-
"service.name": serviceName,
|
|
732
|
-
"service.version": "1.0.0"
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
});
|
|
736
|
-
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
|
|
737
|
-
provider.register();
|
|
738
|
-
return provider;
|
|
739
|
-
}
|
|
740
|
-
async function tracingPlugin(fastify, options = {}) {
|
|
741
|
-
const {
|
|
742
|
-
serviceName = "@classytic/arc",
|
|
743
|
-
autoInstrumentation = true,
|
|
744
|
-
traceRepository = true,
|
|
745
|
-
traceController = true,
|
|
746
|
-
sampleRate = 1
|
|
747
|
-
} = options;
|
|
748
|
-
if (!isAvailable) {
|
|
749
|
-
fastify.log.warn("OpenTelemetry not installed. Tracing disabled.");
|
|
750
|
-
fastify.log.warn("Install: npm install @opentelemetry/api @opentelemetry/sdk-node");
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
const provider = createTracerProvider(options);
|
|
754
|
-
if (!provider) {
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
if (autoInstrumentation && getNodeAutoInstrumentations) {
|
|
758
|
-
getNodeAutoInstrumentations({
|
|
759
|
-
"@opentelemetry/instrumentation-http": {
|
|
760
|
-
enabled: true
|
|
761
|
-
},
|
|
762
|
-
"@opentelemetry/instrumentation-mongodb": {
|
|
763
|
-
enabled: true
|
|
764
|
-
}
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
const tracer = trace.getTracer(serviceName);
|
|
768
|
-
fastify.decorateRequest("tracer", void 0);
|
|
769
|
-
fastify.addHook("onRequest", async (request, reply) => {
|
|
770
|
-
if (Math.random() > sampleRate) {
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
const span = tracer.startSpan(`HTTP ${request.method} ${request.url}`, {
|
|
774
|
-
kind: 1,
|
|
775
|
-
// SpanKind.SERVER
|
|
776
|
-
attributes: {
|
|
777
|
-
"http.method": request.method,
|
|
778
|
-
"http.url": request.url,
|
|
779
|
-
"http.target": request.routeOptions?.url ?? request.url,
|
|
780
|
-
"http.host": request.hostname,
|
|
781
|
-
"http.scheme": request.protocol,
|
|
782
|
-
"http.user_agent": request.headers["user-agent"]
|
|
783
|
-
}
|
|
784
|
-
});
|
|
785
|
-
request.tracer = {
|
|
786
|
-
tracer,
|
|
787
|
-
currentSpan: span
|
|
788
|
-
};
|
|
789
|
-
context.with(trace.setSpan(context.active(), span), () => {
|
|
790
|
-
});
|
|
791
|
-
});
|
|
792
|
-
fastify.addHook("onResponse", async (request, reply) => {
|
|
793
|
-
if (!request.tracer?.currentSpan) {
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
const span = request.tracer.currentSpan;
|
|
797
|
-
span.setAttributes({
|
|
798
|
-
"http.status_code": reply.statusCode,
|
|
799
|
-
"http.response_content_length": reply.getHeader("content-length")
|
|
800
|
-
});
|
|
801
|
-
if (reply.statusCode >= 500) {
|
|
802
|
-
span.setStatus({
|
|
803
|
-
code: SpanStatusCode.ERROR,
|
|
804
|
-
message: `HTTP ${reply.statusCode}`
|
|
805
|
-
});
|
|
806
|
-
} else {
|
|
807
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
808
|
-
}
|
|
809
|
-
span.end();
|
|
810
|
-
});
|
|
811
|
-
fastify.addHook("onError", async (request, reply, error) => {
|
|
812
|
-
if (!request.tracer?.currentSpan) {
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
const span = request.tracer.currentSpan;
|
|
816
|
-
span.recordException(error);
|
|
817
|
-
span.setStatus({
|
|
818
|
-
code: SpanStatusCode.ERROR,
|
|
819
|
-
message: error.message
|
|
820
|
-
});
|
|
821
|
-
});
|
|
822
|
-
fastify.log.info({ serviceName }, "OpenTelemetry tracing enabled");
|
|
823
|
-
}
|
|
824
|
-
function createSpan(request, name, fn, attributes) {
|
|
825
|
-
if (!isAvailable || !request.tracer) {
|
|
826
|
-
return fn(null);
|
|
827
|
-
}
|
|
828
|
-
const { tracer, currentSpan } = request.tracer;
|
|
829
|
-
const span = tracer.startSpan(
|
|
830
|
-
name,
|
|
831
|
-
{
|
|
832
|
-
parent: currentSpan,
|
|
833
|
-
attributes: attributes || {}
|
|
834
|
-
},
|
|
835
|
-
trace.setSpan(context.active(), currentSpan)
|
|
836
|
-
);
|
|
837
|
-
return context.with(trace.setSpan(context.active(), span), async () => {
|
|
838
|
-
try {
|
|
839
|
-
const result = await fn(span);
|
|
840
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
841
|
-
return result;
|
|
842
|
-
} catch (error) {
|
|
843
|
-
span.recordException(error);
|
|
844
|
-
span.setStatus({
|
|
845
|
-
code: SpanStatusCode.ERROR,
|
|
846
|
-
message: error.message
|
|
847
|
-
});
|
|
848
|
-
throw error;
|
|
849
|
-
} finally {
|
|
850
|
-
span.end();
|
|
851
|
-
}
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
function traced(spanName) {
|
|
855
|
-
return function(target, propertyKey, descriptor) {
|
|
856
|
-
const originalMethod = descriptor.value;
|
|
857
|
-
descriptor.value = async function(...args) {
|
|
858
|
-
const request = args.find((arg) => arg && arg.tracer);
|
|
859
|
-
if (!request?.tracer) {
|
|
860
|
-
return originalMethod.apply(this, args);
|
|
861
|
-
}
|
|
862
|
-
const name = spanName || `${target.constructor.name}.${propertyKey}`;
|
|
863
|
-
return createSpan(request, name, async (span) => {
|
|
864
|
-
if (span) {
|
|
865
|
-
span.setAttribute("db.operation", propertyKey);
|
|
866
|
-
span.setAttribute("db.system", "mongodb");
|
|
867
|
-
}
|
|
868
|
-
return originalMethod.apply(this, args);
|
|
869
|
-
});
|
|
870
|
-
};
|
|
871
|
-
return descriptor;
|
|
872
|
-
};
|
|
873
|
-
}
|
|
874
|
-
function isTracingAvailable() {
|
|
875
|
-
return isAvailable;
|
|
876
|
-
}
|
|
877
|
-
var require2, trace, context, SpanStatusCode, NodeTracerProvider, BatchSpanProcessor, OTLPTraceExporter, HttpInstrumentation, MongoDBInstrumentation, getNodeAutoInstrumentations, isAvailable, tracing_default;
|
|
878
|
-
var init_tracing = __esm({
|
|
879
|
-
"src/plugins/tracing.ts"() {
|
|
880
|
-
require2 = createRequire(import.meta.url);
|
|
881
|
-
isAvailable = false;
|
|
882
|
-
try {
|
|
883
|
-
const api = require2("@opentelemetry/api");
|
|
884
|
-
trace = api.trace;
|
|
885
|
-
context = api.context;
|
|
886
|
-
SpanStatusCode = api.SpanStatusCode;
|
|
887
|
-
const sdkNode = require2("@opentelemetry/sdk-node");
|
|
888
|
-
NodeTracerProvider = sdkNode.NodeTracerProvider;
|
|
889
|
-
BatchSpanProcessor = sdkNode.BatchSpanProcessor;
|
|
890
|
-
const exporterTraceOtlp = require2("@opentelemetry/exporter-trace-otlp-http");
|
|
891
|
-
OTLPTraceExporter = exporterTraceOtlp.OTLPTraceExporter;
|
|
892
|
-
const instrHttp = require2("@opentelemetry/instrumentation-http");
|
|
893
|
-
HttpInstrumentation = instrHttp.HttpInstrumentation;
|
|
894
|
-
const instrMongo = require2("@opentelemetry/instrumentation-mongodb");
|
|
895
|
-
MongoDBInstrumentation = instrMongo.MongoDBInstrumentation;
|
|
896
|
-
const autoInstr = require2("@opentelemetry/auto-instrumentations-node");
|
|
897
|
-
getNodeAutoInstrumentations = autoInstr.getNodeAutoInstrumentations;
|
|
898
|
-
isAvailable = true;
|
|
899
|
-
} catch (e) {
|
|
900
|
-
}
|
|
901
|
-
tracing_default = fp(tracingPlugin, {
|
|
902
|
-
name: "arc-tracing",
|
|
903
|
-
fastify: "5.x"
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
});
|
|
907
|
-
var gracefulShutdownPlugin, gracefulShutdown_default;
|
|
908
|
-
var init_gracefulShutdown = __esm({
|
|
909
|
-
"src/plugins/gracefulShutdown.ts"() {
|
|
910
|
-
gracefulShutdownPlugin = async (fastify, opts = {}) => {
|
|
911
|
-
const {
|
|
912
|
-
timeout = 3e4,
|
|
913
|
-
onShutdown,
|
|
914
|
-
signals = ["SIGTERM", "SIGINT"],
|
|
915
|
-
logEvents = true
|
|
916
|
-
} = opts;
|
|
917
|
-
let isShuttingDown = false;
|
|
918
|
-
const shutdown = async (signal) => {
|
|
919
|
-
if (isShuttingDown) {
|
|
920
|
-
if (logEvents) {
|
|
921
|
-
fastify.log?.warn?.({ signal }, "Shutdown already in progress, ignoring signal");
|
|
922
|
-
}
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
isShuttingDown = true;
|
|
926
|
-
if (logEvents) {
|
|
927
|
-
fastify.log?.info?.({ signal, timeout }, "Shutdown signal received, starting graceful shutdown");
|
|
928
|
-
}
|
|
929
|
-
const forceExitTimer = setTimeout(() => {
|
|
930
|
-
if (logEvents) {
|
|
931
|
-
fastify.log?.error?.("Graceful shutdown timeout exceeded, forcing exit");
|
|
932
|
-
}
|
|
933
|
-
process.exit(1);
|
|
934
|
-
}, timeout);
|
|
935
|
-
forceExitTimer.unref();
|
|
936
|
-
try {
|
|
937
|
-
if (logEvents) {
|
|
938
|
-
fastify.log?.info?.("Closing server to new connections");
|
|
939
|
-
}
|
|
940
|
-
await fastify.close();
|
|
941
|
-
if (onShutdown) {
|
|
942
|
-
if (logEvents) {
|
|
943
|
-
fastify.log?.info?.("Running custom shutdown handler");
|
|
944
|
-
}
|
|
945
|
-
await onShutdown();
|
|
946
|
-
}
|
|
947
|
-
if (logEvents) {
|
|
948
|
-
fastify.log?.info?.("Graceful shutdown complete");
|
|
949
|
-
}
|
|
950
|
-
clearTimeout(forceExitTimer);
|
|
951
|
-
process.exit(0);
|
|
952
|
-
} catch (err) {
|
|
953
|
-
if (logEvents) {
|
|
954
|
-
fastify.log?.error?.({ error: err.message }, "Error during shutdown");
|
|
955
|
-
}
|
|
956
|
-
clearTimeout(forceExitTimer);
|
|
957
|
-
process.exit(1);
|
|
958
|
-
}
|
|
959
|
-
};
|
|
960
|
-
for (const signal of signals) {
|
|
961
|
-
process.on(signal, () => {
|
|
962
|
-
void shutdown(signal);
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
|
-
fastify.decorate("shutdown", async () => {
|
|
966
|
-
await shutdown("MANUAL");
|
|
967
|
-
});
|
|
968
|
-
if (logEvents) {
|
|
969
|
-
fastify.log?.debug?.({ signals }, "Graceful shutdown plugin registered");
|
|
970
|
-
}
|
|
971
|
-
};
|
|
972
|
-
gracefulShutdown_default = fp(gracefulShutdownPlugin, {
|
|
973
|
-
name: "arc-graceful-shutdown",
|
|
974
|
-
fastify: "5.x"
|
|
975
|
-
});
|
|
976
|
-
}
|
|
977
|
-
});
|
|
978
|
-
async function errorHandlerPluginFn(fastify, options = {}) {
|
|
979
|
-
const {
|
|
980
|
-
includeStack = process.env.NODE_ENV !== "production",
|
|
981
|
-
onError,
|
|
982
|
-
errorMap = {}
|
|
983
|
-
} = options;
|
|
984
|
-
fastify.setErrorHandler(async (error, request, reply) => {
|
|
985
|
-
if (onError) {
|
|
986
|
-
try {
|
|
987
|
-
await onError(error, request);
|
|
988
|
-
} catch (callbackError) {
|
|
989
|
-
request.log.error({ err: callbackError }, "Error in onError callback");
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
const requestId = request.id;
|
|
993
|
-
const response = {
|
|
994
|
-
success: false,
|
|
995
|
-
error: error.message || "Internal Server Error",
|
|
996
|
-
code: "INTERNAL_ERROR",
|
|
997
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
998
|
-
...requestId && { requestId }
|
|
999
|
-
};
|
|
1000
|
-
let statusCode = 500;
|
|
1001
|
-
if (isArcError(error)) {
|
|
1002
|
-
statusCode = error.statusCode;
|
|
1003
|
-
response.code = error.code;
|
|
1004
|
-
if (error.details) {
|
|
1005
|
-
response.details = error.details;
|
|
1006
|
-
}
|
|
1007
|
-
if (error.requestId) {
|
|
1008
|
-
response.requestId = error.requestId;
|
|
1009
|
-
}
|
|
1010
|
-
} else if ("validation" in error && Array.isArray(error.validation)) {
|
|
1011
|
-
statusCode = 400;
|
|
1012
|
-
response.code = "VALIDATION_ERROR";
|
|
1013
|
-
response.error = "Validation failed";
|
|
1014
|
-
response.details = {
|
|
1015
|
-
errors: error.validation?.map((v) => ({
|
|
1016
|
-
field: v.instancePath?.replace(/^\//, "") || v.params?.missingProperty || "unknown",
|
|
1017
|
-
message: v.message || "Invalid value",
|
|
1018
|
-
keyword: v.keyword
|
|
1019
|
-
}))
|
|
1020
|
-
};
|
|
1021
|
-
} else if ("statusCode" in error && typeof error.statusCode === "number") {
|
|
1022
|
-
statusCode = error.statusCode;
|
|
1023
|
-
response.code = statusCodeToCode(statusCode);
|
|
1024
|
-
} else if (error.name && errorMap[error.name]) {
|
|
1025
|
-
const mapping = errorMap[error.name];
|
|
1026
|
-
statusCode = mapping.statusCode;
|
|
1027
|
-
response.code = mapping.code;
|
|
1028
|
-
if (mapping.message) {
|
|
1029
|
-
response.error = mapping.message;
|
|
1030
|
-
}
|
|
1031
|
-
} else if (error.name === "ValidationError" && "errors" in error) {
|
|
1032
|
-
statusCode = 400;
|
|
1033
|
-
response.code = "VALIDATION_ERROR";
|
|
1034
|
-
const mongooseErrors = error.errors;
|
|
1035
|
-
if (process.env.NODE_ENV === "production") {
|
|
1036
|
-
response.details = { errorCount: Object.keys(mongooseErrors).length };
|
|
1037
|
-
} else {
|
|
1038
|
-
response.details = {
|
|
1039
|
-
errors: Object.entries(mongooseErrors).map(([field, err]) => ({
|
|
1040
|
-
field: err.path || field,
|
|
1041
|
-
message: err.message
|
|
1042
|
-
}))
|
|
1043
|
-
};
|
|
1044
|
-
}
|
|
1045
|
-
} else if (error.name === "CastError") {
|
|
1046
|
-
statusCode = 400;
|
|
1047
|
-
response.code = "INVALID_ID";
|
|
1048
|
-
response.error = "Invalid identifier format";
|
|
1049
|
-
} else if (error.name === "MongoServerError" && error.code === 11e3) {
|
|
1050
|
-
statusCode = 409;
|
|
1051
|
-
response.code = "DUPLICATE_KEY";
|
|
1052
|
-
response.error = "Resource already exists";
|
|
1053
|
-
const keyValue = error.keyValue;
|
|
1054
|
-
if (keyValue && process.env.NODE_ENV !== "production") {
|
|
1055
|
-
response.details = { duplicateFields: Object.keys(keyValue) };
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
if (includeStack && error.stack) {
|
|
1059
|
-
response.stack = error.stack;
|
|
1060
|
-
}
|
|
1061
|
-
if (statusCode >= 500) {
|
|
1062
|
-
request.log.error({ err: error, statusCode }, "Server error");
|
|
1063
|
-
} else if (statusCode >= 400) {
|
|
1064
|
-
request.log.warn({ err: error, statusCode }, "Client error");
|
|
1065
|
-
}
|
|
1066
|
-
return reply.status(statusCode).send(response);
|
|
1067
|
-
});
|
|
1068
|
-
}
|
|
1069
|
-
function statusCodeToCode(statusCode) {
|
|
1070
|
-
const codes = {
|
|
1071
|
-
400: "BAD_REQUEST",
|
|
1072
|
-
401: "UNAUTHORIZED",
|
|
1073
|
-
403: "FORBIDDEN",
|
|
1074
|
-
404: "NOT_FOUND",
|
|
1075
|
-
405: "METHOD_NOT_ALLOWED",
|
|
1076
|
-
409: "CONFLICT",
|
|
1077
|
-
422: "UNPROCESSABLE_ENTITY",
|
|
1078
|
-
429: "RATE_LIMITED",
|
|
1079
|
-
500: "INTERNAL_ERROR",
|
|
1080
|
-
502: "BAD_GATEWAY",
|
|
1081
|
-
503: "SERVICE_UNAVAILABLE",
|
|
1082
|
-
504: "GATEWAY_TIMEOUT"
|
|
1083
|
-
};
|
|
1084
|
-
return codes[statusCode] ?? "ERROR";
|
|
1085
|
-
}
|
|
1086
|
-
var errorHandlerPlugin, errorHandler_default;
|
|
1087
|
-
var init_errorHandler = __esm({
|
|
1088
|
-
"src/plugins/errorHandler.ts"() {
|
|
1089
|
-
init_errors();
|
|
1090
|
-
errorHandlerPlugin = fp(errorHandlerPluginFn, {
|
|
1091
|
-
name: "arc-error-handler",
|
|
1092
|
-
fastify: "5.x"
|
|
1093
|
-
});
|
|
1094
|
-
errorHandler_default = errorHandlerPlugin;
|
|
1095
|
-
}
|
|
1096
|
-
});
|
|
1097
|
-
function hasEvents(instance) {
|
|
1098
|
-
return "events" in instance && instance.events != null && typeof instance.events.publish === "function";
|
|
1099
|
-
}
|
|
1100
|
-
var arcCorePlugin, arcCorePlugin_default;
|
|
1101
|
-
var init_arcCorePlugin = __esm({
|
|
1102
|
-
"src/core/arcCorePlugin.ts"() {
|
|
1103
|
-
init_HookSystem();
|
|
1104
|
-
init_ResourceRegistry();
|
|
1105
|
-
arcCorePlugin = async (fastify, opts = {}) => {
|
|
1106
|
-
const {
|
|
1107
|
-
emitEvents = true,
|
|
1108
|
-
hookSystem: hookSystem2,
|
|
1109
|
-
registry,
|
|
1110
|
-
useGlobalSingletons = false
|
|
1111
|
-
} = opts;
|
|
1112
|
-
const actualHookSystem = useGlobalSingletons ? hookSystem : hookSystem2 ?? new HookSystem();
|
|
1113
|
-
const actualRegistry = useGlobalSingletons ? resourceRegistry : registry ?? new ResourceRegistry();
|
|
1114
|
-
fastify.decorate("arc", {
|
|
1115
|
-
hooks: actualHookSystem,
|
|
1116
|
-
registry: actualRegistry,
|
|
1117
|
-
emitEvents
|
|
1118
|
-
});
|
|
1119
|
-
if (emitEvents) {
|
|
1120
|
-
const eventOperations = ["create", "update", "delete"];
|
|
1121
|
-
for (const operation of eventOperations) {
|
|
1122
|
-
actualHookSystem.after("*", operation, async (ctx) => {
|
|
1123
|
-
if (!hasEvents(fastify)) return;
|
|
1124
|
-
const eventType = `${ctx.resource}.${operation}d`;
|
|
1125
|
-
const payload = {
|
|
1126
|
-
resource: ctx.resource,
|
|
1127
|
-
operation: ctx.operation,
|
|
1128
|
-
data: ctx.result,
|
|
1129
|
-
userId: ctx.user?.id ?? ctx.user?._id,
|
|
1130
|
-
organizationId: ctx.context?.organizationId,
|
|
1131
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1132
|
-
};
|
|
1133
|
-
try {
|
|
1134
|
-
await fastify.events.publish(eventType, payload);
|
|
1135
|
-
} catch (error) {
|
|
1136
|
-
fastify.log?.warn?.(
|
|
1137
|
-
{ eventType, error },
|
|
1138
|
-
"Failed to emit event"
|
|
1139
|
-
);
|
|
1140
|
-
}
|
|
1141
|
-
});
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
fastify.addHook("onClose", async () => {
|
|
1145
|
-
actualHookSystem.clear();
|
|
1146
|
-
actualRegistry._clear();
|
|
1147
|
-
});
|
|
1148
|
-
fastify.log?.info?.("✅ Arc core plugin enabled (instance-scoped hooks & registry)");
|
|
1149
|
-
};
|
|
1150
|
-
arcCorePlugin_default = fp(arcCorePlugin, {
|
|
1151
|
-
name: "arc-core",
|
|
1152
|
-
fastify: "5.x"
|
|
1153
|
-
});
|
|
1154
|
-
}
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
// src/plugins/index.ts
|
|
1158
|
-
var plugins_exports = {};
|
|
1159
|
-
__export(plugins_exports, {
|
|
1160
|
-
arcCorePlugin: () => arcCorePlugin_default,
|
|
1161
|
-
arcCorePluginFn: () => arcCorePlugin,
|
|
1162
|
-
createSpan: () => createSpan,
|
|
1163
|
-
errorHandlerPlugin: () => errorHandler_default,
|
|
1164
|
-
errorHandlerPluginFn: () => errorHandlerPlugin,
|
|
1165
|
-
gracefulShutdownPlugin: () => gracefulShutdown_default,
|
|
1166
|
-
gracefulShutdownPluginFn: () => gracefulShutdownPlugin,
|
|
1167
|
-
healthPlugin: () => health_default,
|
|
1168
|
-
healthPluginFn: () => healthPlugin,
|
|
1169
|
-
isTracingAvailable: () => isTracingAvailable,
|
|
1170
|
-
requestIdPlugin: () => requestId_default,
|
|
1171
|
-
requestIdPluginFn: () => requestIdPlugin,
|
|
1172
|
-
traced: () => traced,
|
|
1173
|
-
tracingPlugin: () => tracing_default
|
|
1174
|
-
});
|
|
1175
|
-
var init_plugins = __esm({
|
|
1176
|
-
"src/plugins/index.ts"() {
|
|
1177
|
-
init_requestId();
|
|
1178
|
-
init_health();
|
|
1179
|
-
init_tracing();
|
|
1180
|
-
init_gracefulShutdown();
|
|
1181
|
-
init_errorHandler();
|
|
1182
|
-
init_arcCorePlugin();
|
|
1183
|
-
}
|
|
1184
|
-
});
|
|
1185
|
-
function parseExpiresIn(input, defaultValue) {
|
|
1186
|
-
if (!input) return defaultValue;
|
|
1187
|
-
if (/^\d+$/.test(input)) return parseInt(input, 10);
|
|
1188
|
-
const match = /^(\d+)\s*([smhd])$/i.exec(input);
|
|
1189
|
-
if (!match) return defaultValue;
|
|
1190
|
-
const value = parseInt(match[1], 10);
|
|
1191
|
-
const unit = match[2].toLowerCase();
|
|
1192
|
-
const multipliers = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
1193
|
-
return value * (multipliers[unit] ?? 1);
|
|
1194
|
-
}
|
|
1195
|
-
function extractBearerToken(request) {
|
|
1196
|
-
const auth = request.headers.authorization;
|
|
1197
|
-
if (!auth?.startsWith("Bearer ")) return null;
|
|
1198
|
-
return auth.slice(7);
|
|
1199
|
-
}
|
|
1200
|
-
var authPlugin, authPlugin_default;
|
|
1201
|
-
var init_authPlugin = __esm({
|
|
1202
|
-
"src/auth/authPlugin.ts"() {
|
|
1203
|
-
authPlugin = async (fastify, opts = {}) => {
|
|
1204
|
-
const { jwt: jwtConfig, authenticate: appAuthenticator, onFailure, userProperty = "user" } = opts;
|
|
1205
|
-
let jwtContext = null;
|
|
1206
|
-
if (jwtConfig?.secret) {
|
|
1207
|
-
if (jwtConfig.secret.length < 32) {
|
|
1208
|
-
throw new Error(
|
|
1209
|
-
`JWT secret must be at least 32 characters (current: ${jwtConfig.secret.length}).
|
|
1210
|
-
Use a strong random secret for production.`
|
|
1211
|
-
);
|
|
1212
|
-
}
|
|
1213
|
-
const jwtPlugin = await import('@fastify/jwt');
|
|
1214
|
-
await fastify.register(jwtPlugin.default ?? jwtPlugin, {
|
|
1215
|
-
secret: jwtConfig.secret,
|
|
1216
|
-
sign: {
|
|
1217
|
-
expiresIn: jwtConfig.expiresIn ?? "15m",
|
|
1218
|
-
...jwtConfig.sign ?? {}
|
|
1219
|
-
},
|
|
1220
|
-
verify: { ...jwtConfig.verify ?? {} }
|
|
1221
|
-
});
|
|
1222
|
-
const fastifyWithJwt = fastify;
|
|
1223
|
-
jwtContext = {
|
|
1224
|
-
verify: (token) => {
|
|
1225
|
-
return fastifyWithJwt.jwt.verify(token);
|
|
1226
|
-
},
|
|
1227
|
-
sign: (payload, options) => {
|
|
1228
|
-
return fastifyWithJwt.jwt.sign(payload, options);
|
|
1229
|
-
},
|
|
1230
|
-
decode: (token) => {
|
|
1231
|
-
try {
|
|
1232
|
-
return fastifyWithJwt.jwt.decode(token);
|
|
1233
|
-
} catch {
|
|
1234
|
-
return null;
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
};
|
|
1238
|
-
fastify.log.info("Auth: JWT infrastructure enabled");
|
|
1239
|
-
}
|
|
1240
|
-
const authContext = {
|
|
1241
|
-
jwt: jwtContext,
|
|
1242
|
-
fastify
|
|
1243
|
-
};
|
|
1244
|
-
const authenticate = async (request, reply) => {
|
|
1245
|
-
try {
|
|
1246
|
-
let user = null;
|
|
1247
|
-
if (appAuthenticator) {
|
|
1248
|
-
user = await appAuthenticator(request, authContext);
|
|
1249
|
-
} else if (jwtContext) {
|
|
1250
|
-
const token = extractBearerToken(request);
|
|
1251
|
-
if (token) {
|
|
1252
|
-
const decoded = jwtContext.verify(token);
|
|
1253
|
-
user = decoded;
|
|
1254
|
-
}
|
|
1255
|
-
} else {
|
|
1256
|
-
throw new Error(
|
|
1257
|
-
"No authenticator configured. Provide auth.authenticate function or auth.jwt.secret."
|
|
1258
|
-
);
|
|
1259
|
-
}
|
|
1260
|
-
if (!user) {
|
|
1261
|
-
throw new Error("Authentication required");
|
|
1262
|
-
}
|
|
1263
|
-
request[userProperty] = user;
|
|
1264
|
-
} catch (err) {
|
|
1265
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1266
|
-
if (onFailure) {
|
|
1267
|
-
await onFailure(request, reply, error);
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1270
|
-
const message = process.env.NODE_ENV === "production" ? "Authentication required" : error.message;
|
|
1271
|
-
reply.code(401).send({
|
|
1272
|
-
success: false,
|
|
1273
|
-
error: "Unauthorized",
|
|
1274
|
-
message
|
|
1275
|
-
});
|
|
1276
|
-
}
|
|
1277
|
-
};
|
|
1278
|
-
const refreshSecret = jwtConfig?.refreshSecret ?? jwtConfig?.secret;
|
|
1279
|
-
const accessExpiresIn = jwtConfig?.expiresIn ?? "15m";
|
|
1280
|
-
const refreshExpiresIn = jwtConfig?.refreshExpiresIn ?? "7d";
|
|
1281
|
-
const issueTokens = (payload, options) => {
|
|
1282
|
-
if (!jwtContext) {
|
|
1283
|
-
throw new Error("JWT not configured. Provide auth.jwt.secret to use issueTokens.");
|
|
1284
|
-
}
|
|
1285
|
-
const accessTtl = options?.expiresIn ?? accessExpiresIn;
|
|
1286
|
-
const refreshTtl = options?.refreshExpiresIn ?? refreshExpiresIn;
|
|
1287
|
-
const accessToken = jwtContext.sign(payload, { expiresIn: accessTtl });
|
|
1288
|
-
const refreshPayload = payload.id ? { id: payload.id, type: "refresh" } : payload._id ? { id: payload._id, type: "refresh" } : { ...payload, type: "refresh" };
|
|
1289
|
-
let refreshToken;
|
|
1290
|
-
if (refreshSecret) {
|
|
1291
|
-
const fastifyWithJwt = fastify;
|
|
1292
|
-
refreshToken = fastifyWithJwt.jwt.sign(refreshPayload, {
|
|
1293
|
-
expiresIn: refreshTtl,
|
|
1294
|
-
// Use refresh secret if different from main secret
|
|
1295
|
-
...refreshSecret !== jwtConfig?.secret ? { secret: refreshSecret } : {}
|
|
1296
|
-
});
|
|
1297
|
-
}
|
|
1298
|
-
return {
|
|
1299
|
-
accessToken,
|
|
1300
|
-
refreshToken,
|
|
1301
|
-
expiresIn: parseExpiresIn(accessTtl, 900),
|
|
1302
|
-
refreshExpiresIn: refreshToken ? parseExpiresIn(refreshTtl, 604800) : void 0,
|
|
1303
|
-
tokenType: "Bearer"
|
|
1304
|
-
};
|
|
1305
|
-
};
|
|
1306
|
-
const verifyRefreshToken = (token) => {
|
|
1307
|
-
if (!jwtContext) {
|
|
1308
|
-
throw new Error("JWT not configured. Provide auth.jwt.secret to use verifyRefreshToken.");
|
|
1309
|
-
}
|
|
1310
|
-
const fastifyWithJwt = fastify;
|
|
1311
|
-
return fastifyWithJwt.jwt.verify(token, {
|
|
1312
|
-
...refreshSecret !== jwtConfig?.secret ? { secret: refreshSecret } : {}
|
|
1313
|
-
});
|
|
1314
|
-
};
|
|
1315
|
-
const authorize = (...allowedRoles) => {
|
|
1316
|
-
return async (request, reply) => {
|
|
1317
|
-
const user = request[userProperty];
|
|
1318
|
-
if (!user) {
|
|
1319
|
-
reply.code(401).send({
|
|
1320
|
-
success: false,
|
|
1321
|
-
error: "Unauthorized",
|
|
1322
|
-
message: "No user context"
|
|
1323
|
-
});
|
|
1324
|
-
return;
|
|
1325
|
-
}
|
|
1326
|
-
const userRoles = user.roles ?? [];
|
|
1327
|
-
if (allowedRoles.length === 1 && allowedRoles[0] === "*") {
|
|
1328
|
-
return;
|
|
1329
|
-
}
|
|
1330
|
-
const hasRole = allowedRoles.some((role) => userRoles.includes(role));
|
|
1331
|
-
if (!hasRole) {
|
|
1332
|
-
reply.code(403).send({
|
|
1333
|
-
success: false,
|
|
1334
|
-
error: "Forbidden",
|
|
1335
|
-
message: `Requires one of: ${allowedRoles.join(", ")}`
|
|
1336
|
-
});
|
|
1337
|
-
return;
|
|
1338
|
-
}
|
|
1339
|
-
};
|
|
1340
|
-
};
|
|
1341
|
-
const authHelpers = {
|
|
1342
|
-
jwt: jwtContext,
|
|
1343
|
-
issueTokens,
|
|
1344
|
-
verifyRefreshToken
|
|
1345
|
-
};
|
|
1346
|
-
fastify.decorate("authenticate", authenticate);
|
|
1347
|
-
fastify.decorate("authorize", authorize);
|
|
1348
|
-
fastify.decorate("auth", authHelpers);
|
|
1349
|
-
fastify.log.info(
|
|
1350
|
-
`Auth: Plugin registered (jwt=${!!jwtContext}, customAuth=${!!appAuthenticator})`
|
|
1351
|
-
);
|
|
1352
|
-
};
|
|
1353
|
-
authPlugin_default = fp(authPlugin, {
|
|
1354
|
-
name: "arc-auth",
|
|
1355
|
-
fastify: "5.x"
|
|
1356
|
-
});
|
|
1357
|
-
}
|
|
1358
|
-
});
|
|
1359
|
-
|
|
1360
|
-
// src/auth/index.ts
|
|
1361
|
-
var auth_exports = {};
|
|
1362
|
-
__export(auth_exports, {
|
|
1363
|
-
authPlugin: () => authPlugin_default,
|
|
1364
|
-
authPluginFn: () => authPlugin
|
|
1365
|
-
});
|
|
1366
|
-
var init_auth = __esm({
|
|
1367
|
-
"src/auth/index.ts"() {
|
|
1368
|
-
init_authPlugin();
|
|
1369
|
-
}
|
|
1370
|
-
});
|
|
1371
|
-
|
|
1372
|
-
// src/adapters/types.ts
|
|
1373
|
-
function isMongooseModel(value) {
|
|
1374
|
-
return typeof value === "function" && value.prototype && "modelName" in value && "schema" in value;
|
|
1375
|
-
}
|
|
1376
|
-
function isRepository(value) {
|
|
1377
|
-
return typeof value === "object" && value !== null && "getAll" in value && "getById" in value && "create" in value && "update" in value && "delete" in value;
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
// src/adapters/mongoose.ts
|
|
1381
|
-
var MongooseAdapter = class {
|
|
1382
|
-
type = "mongoose";
|
|
1383
|
-
name;
|
|
1384
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1385
|
-
model;
|
|
1386
|
-
repository;
|
|
1387
|
-
constructor(options) {
|
|
1388
|
-
if (!isMongooseModel(options.model)) {
|
|
1389
|
-
throw new TypeError(
|
|
1390
|
-
"MongooseAdapter: Invalid model. Expected Mongoose Model instance.\nUsage: createMongooseAdapter({ model: YourModel, repository: yourRepo })"
|
|
1391
|
-
);
|
|
1392
|
-
}
|
|
1393
|
-
if (!isRepository(options.repository)) {
|
|
1394
|
-
throw new TypeError(
|
|
1395
|
-
"MongooseAdapter: Invalid repository. Expected CrudRepository instance.\nUsage: createMongooseAdapter({ model: YourModel, repository: yourRepo })"
|
|
1396
|
-
);
|
|
1397
|
-
}
|
|
1398
|
-
this.model = options.model;
|
|
1399
|
-
this.repository = options.repository;
|
|
1400
|
-
this.name = `MongooseAdapter<${options.model.modelName}>`;
|
|
1401
|
-
}
|
|
1402
|
-
/**
|
|
1403
|
-
* Get schema metadata from Mongoose model
|
|
1404
|
-
*/
|
|
1405
|
-
getSchemaMetadata() {
|
|
1406
|
-
const schema = this.model.schema;
|
|
1407
|
-
const paths = schema.paths;
|
|
1408
|
-
const fields = {};
|
|
1409
|
-
for (const [fieldName, schemaType] of Object.entries(paths)) {
|
|
1410
|
-
if (fieldName.startsWith("_") && fieldName !== "_id") continue;
|
|
1411
|
-
const typeInfo = schemaType;
|
|
1412
|
-
const mongooseType = typeInfo.instance || "Mixed";
|
|
1413
|
-
const typeMap = {
|
|
1414
|
-
String: "string",
|
|
1415
|
-
Number: "number",
|
|
1416
|
-
Boolean: "boolean",
|
|
1417
|
-
Date: "date",
|
|
1418
|
-
ObjectID: "objectId",
|
|
1419
|
-
Array: "array",
|
|
1420
|
-
Mixed: "object",
|
|
1421
|
-
Buffer: "object",
|
|
1422
|
-
Embedded: "object"
|
|
1423
|
-
};
|
|
1424
|
-
fields[fieldName] = {
|
|
1425
|
-
type: typeMap[mongooseType] ?? "object",
|
|
1426
|
-
required: !!typeInfo.isRequired,
|
|
1427
|
-
ref: typeInfo.options?.ref
|
|
1428
|
-
};
|
|
1429
|
-
}
|
|
1430
|
-
return {
|
|
1431
|
-
name: this.model.modelName,
|
|
1432
|
-
fields,
|
|
1433
|
-
relations: this.extractRelations(paths)
|
|
1434
|
-
};
|
|
1435
|
-
}
|
|
1436
|
-
/**
|
|
1437
|
-
* Generate OpenAPI schemas from Mongoose model
|
|
1438
|
-
*/
|
|
1439
|
-
generateSchemas(schemaOptions) {
|
|
1440
|
-
try {
|
|
1441
|
-
const schema = this.model.schema;
|
|
1442
|
-
const paths = schema.paths;
|
|
1443
|
-
const properties = {};
|
|
1444
|
-
const required = [];
|
|
1445
|
-
const fieldRules = schemaOptions?.fieldRules || {};
|
|
1446
|
-
const blockedFields = new Set(
|
|
1447
|
-
Object.entries(fieldRules).filter(([, rules]) => rules.systemManaged || rules.hidden).map(([field]) => field)
|
|
1448
|
-
);
|
|
1449
|
-
for (const [fieldName, schemaType] of Object.entries(paths)) {
|
|
1450
|
-
if (fieldName.startsWith("__")) continue;
|
|
1451
|
-
if (blockedFields.has(fieldName)) continue;
|
|
1452
|
-
const typeInfo = schemaType;
|
|
1453
|
-
properties[fieldName] = this.mongooseTypeToOpenApi(typeInfo);
|
|
1454
|
-
if (typeInfo.isRequired) {
|
|
1455
|
-
required.push(fieldName);
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
const baseSchema = {
|
|
1459
|
-
type: "object",
|
|
1460
|
-
properties,
|
|
1461
|
-
required: required.length > 0 ? required : void 0
|
|
1462
|
-
};
|
|
1463
|
-
return {
|
|
1464
|
-
create: {
|
|
1465
|
-
body: {
|
|
1466
|
-
...baseSchema,
|
|
1467
|
-
// Remove system-managed fields from create
|
|
1468
|
-
properties: Object.fromEntries(
|
|
1469
|
-
Object.entries(properties).filter(
|
|
1470
|
-
([field]) => !["_id", "createdAt", "updatedAt", "deletedAt"].includes(field)
|
|
1471
|
-
)
|
|
1472
|
-
)
|
|
1473
|
-
}
|
|
1474
|
-
},
|
|
1475
|
-
update: {
|
|
1476
|
-
body: {
|
|
1477
|
-
...baseSchema,
|
|
1478
|
-
// All fields optional for PATCH
|
|
1479
|
-
required: void 0,
|
|
1480
|
-
properties: Object.fromEntries(
|
|
1481
|
-
Object.entries(properties).filter(
|
|
1482
|
-
([field]) => !["_id", "createdAt", "updatedAt", "deletedAt"].includes(field)
|
|
1483
|
-
)
|
|
1484
|
-
)
|
|
1485
|
-
}
|
|
1486
|
-
},
|
|
1487
|
-
response: {
|
|
1488
|
-
type: "object",
|
|
1489
|
-
properties
|
|
1490
|
-
}
|
|
1491
|
-
};
|
|
1492
|
-
} catch {
|
|
1493
|
-
return null;
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
/**
|
|
1497
|
-
* Extract relation metadata
|
|
1498
|
-
*/
|
|
1499
|
-
extractRelations(paths) {
|
|
1500
|
-
const relations = {};
|
|
1501
|
-
for (const [fieldName, schemaType] of Object.entries(paths)) {
|
|
1502
|
-
const ref = schemaType.options?.ref;
|
|
1503
|
-
if (ref) {
|
|
1504
|
-
relations[fieldName] = {
|
|
1505
|
-
type: "one-to-one",
|
|
1506
|
-
// Mongoose refs are typically one-to-one
|
|
1507
|
-
target: ref,
|
|
1508
|
-
foreignKey: fieldName
|
|
1509
|
-
};
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
return Object.keys(relations).length > 0 ? relations : void 0;
|
|
1513
|
-
}
|
|
1514
|
-
/**
|
|
1515
|
-
* Convert Mongoose type to OpenAPI type
|
|
1516
|
-
*/
|
|
1517
|
-
mongooseTypeToOpenApi(typeInfo) {
|
|
1518
|
-
const instance = typeInfo.instance;
|
|
1519
|
-
const options = typeInfo.options || {};
|
|
1520
|
-
const baseType = {};
|
|
1521
|
-
switch (instance) {
|
|
1522
|
-
case "String":
|
|
1523
|
-
baseType.type = "string";
|
|
1524
|
-
if (options.enum) baseType.enum = options.enum;
|
|
1525
|
-
if (options.minlength) baseType.minLength = options.minlength;
|
|
1526
|
-
if (options.maxlength) baseType.maxLength = options.maxlength;
|
|
1527
|
-
break;
|
|
1528
|
-
case "Number":
|
|
1529
|
-
baseType.type = "number";
|
|
1530
|
-
if (options.min !== void 0) baseType.minimum = options.min;
|
|
1531
|
-
if (options.max !== void 0) baseType.maximum = options.max;
|
|
1532
|
-
break;
|
|
1533
|
-
case "Boolean":
|
|
1534
|
-
baseType.type = "boolean";
|
|
1535
|
-
break;
|
|
1536
|
-
case "Date":
|
|
1537
|
-
baseType.type = "string";
|
|
1538
|
-
baseType.format = "date-time";
|
|
1539
|
-
break;
|
|
1540
|
-
case "ObjectID":
|
|
1541
|
-
baseType.type = "string";
|
|
1542
|
-
baseType.pattern = "^[a-f\\d]{24}$";
|
|
1543
|
-
break;
|
|
1544
|
-
case "Array":
|
|
1545
|
-
baseType.type = "array";
|
|
1546
|
-
baseType.items = { type: "string" };
|
|
1547
|
-
break;
|
|
1548
|
-
default:
|
|
1549
|
-
baseType.type = "object";
|
|
1550
|
-
}
|
|
1551
|
-
return baseType;
|
|
1552
|
-
}
|
|
1553
|
-
};
|
|
1554
|
-
function createMongooseAdapter(options) {
|
|
1555
|
-
return new MongooseAdapter(options);
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
// src/adapters/prisma.ts
|
|
1559
|
-
var PrismaQueryParser = class {
|
|
1560
|
-
maxLimit;
|
|
1561
|
-
defaultLimit;
|
|
1562
|
-
softDeleteEnabled;
|
|
1563
|
-
softDeleteField;
|
|
1564
|
-
/** Map Arc operators to Prisma operators */
|
|
1565
|
-
operatorMap = {
|
|
1566
|
-
$eq: "equals",
|
|
1567
|
-
$ne: "not",
|
|
1568
|
-
$gt: "gt",
|
|
1569
|
-
$gte: "gte",
|
|
1570
|
-
$lt: "lt",
|
|
1571
|
-
$lte: "lte",
|
|
1572
|
-
$in: "in",
|
|
1573
|
-
$nin: "notIn",
|
|
1574
|
-
$regex: "contains",
|
|
1575
|
-
$exists: void 0
|
|
1576
|
-
// Handled specially
|
|
1577
|
-
};
|
|
1578
|
-
constructor(options = {}) {
|
|
1579
|
-
this.maxLimit = options.maxLimit ?? 1e3;
|
|
1580
|
-
this.defaultLimit = options.defaultLimit ?? 20;
|
|
1581
|
-
this.softDeleteEnabled = options.softDeleteEnabled ?? true;
|
|
1582
|
-
this.softDeleteField = options.softDeleteField ?? "deletedAt";
|
|
1583
|
-
}
|
|
1584
|
-
/**
|
|
1585
|
-
* Parse URL query parameters (delegates to ArcQueryParser format)
|
|
1586
|
-
*/
|
|
1587
|
-
parse(query) {
|
|
1588
|
-
const q = query ?? {};
|
|
1589
|
-
const page = this.parseNumber(q.page, 1);
|
|
1590
|
-
const limit = Math.min(this.parseNumber(q.limit, this.defaultLimit), this.maxLimit);
|
|
1591
|
-
return {
|
|
1592
|
-
filters: this.parseFilters(q),
|
|
1593
|
-
limit,
|
|
1594
|
-
page,
|
|
1595
|
-
sort: this.parseSort(q.sort),
|
|
1596
|
-
search: q.search,
|
|
1597
|
-
select: this.parseSelect(q.select)
|
|
1598
|
-
};
|
|
1599
|
-
}
|
|
1600
|
-
/**
|
|
1601
|
-
* Convert ParsedQuery to Prisma query options
|
|
1602
|
-
*/
|
|
1603
|
-
toPrismaQuery(parsed, policyFilters) {
|
|
1604
|
-
const where = {};
|
|
1605
|
-
if (parsed.filters) {
|
|
1606
|
-
Object.assign(where, this.translateFilters(parsed.filters));
|
|
1607
|
-
}
|
|
1608
|
-
if (policyFilters) {
|
|
1609
|
-
Object.assign(where, this.translateFilters(policyFilters));
|
|
1610
|
-
}
|
|
1611
|
-
if (this.softDeleteEnabled) {
|
|
1612
|
-
where[this.softDeleteField] = null;
|
|
1613
|
-
}
|
|
1614
|
-
const orderBy = parsed.sort ? Object.entries(parsed.sort).map(([field, dir]) => ({
|
|
1615
|
-
[field]: dir === 1 ? "asc" : "desc"
|
|
1616
|
-
})) : void 0;
|
|
1617
|
-
const take = parsed.limit ?? this.defaultLimit;
|
|
1618
|
-
const skip = parsed.page ? (parsed.page - 1) * take : 0;
|
|
1619
|
-
const select = parsed.select ? Object.fromEntries(
|
|
1620
|
-
Object.entries(parsed.select).filter(([, v]) => v === 1).map(([k]) => [k, true])
|
|
1621
|
-
) : void 0;
|
|
1622
|
-
return {
|
|
1623
|
-
where: Object.keys(where).length > 0 ? where : void 0,
|
|
1624
|
-
orderBy: orderBy && orderBy.length > 0 ? orderBy : void 0,
|
|
1625
|
-
take,
|
|
1626
|
-
skip,
|
|
1627
|
-
select: select && Object.keys(select).length > 0 ? select : void 0
|
|
1628
|
-
};
|
|
1629
|
-
}
|
|
1630
|
-
/**
|
|
1631
|
-
* Translate Arc/MongoDB-style filters to Prisma where clause
|
|
1632
|
-
*/
|
|
1633
|
-
translateFilters(filters) {
|
|
1634
|
-
const result = {};
|
|
1635
|
-
for (const [field, value] of Object.entries(filters)) {
|
|
1636
|
-
if (value === null || value === void 0) continue;
|
|
1637
|
-
if (typeof value === "object" && !Array.isArray(value)) {
|
|
1638
|
-
const prismaCondition = {};
|
|
1639
|
-
for (const [op, opValue] of Object.entries(value)) {
|
|
1640
|
-
if (op === "$exists") {
|
|
1641
|
-
result[field] = opValue ? { not: null } : null;
|
|
1642
|
-
continue;
|
|
1643
|
-
}
|
|
1644
|
-
const prismaOp = this.operatorMap[op];
|
|
1645
|
-
if (prismaOp) {
|
|
1646
|
-
prismaCondition[prismaOp] = opValue;
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
if (Object.keys(prismaCondition).length > 0) {
|
|
1650
|
-
result[field] = prismaCondition;
|
|
1651
|
-
}
|
|
1652
|
-
} else {
|
|
1653
|
-
result[field] = value;
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
return result;
|
|
1657
|
-
}
|
|
1658
|
-
parseNumber(value, defaultValue) {
|
|
1659
|
-
if (value === void 0 || value === null) return defaultValue;
|
|
1660
|
-
const num = parseInt(String(value), 10);
|
|
1661
|
-
return Number.isNaN(num) ? defaultValue : Math.max(1, num);
|
|
1662
|
-
}
|
|
1663
|
-
parseSort(value) {
|
|
1664
|
-
if (!value) return void 0;
|
|
1665
|
-
const sortStr = String(value);
|
|
1666
|
-
const result = {};
|
|
1667
|
-
for (const field of sortStr.split(",")) {
|
|
1668
|
-
const trimmed = field.trim();
|
|
1669
|
-
if (!trimmed || !/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) continue;
|
|
1670
|
-
if (trimmed.startsWith("-")) {
|
|
1671
|
-
result[trimmed.slice(1)] = -1;
|
|
1672
|
-
} else {
|
|
1673
|
-
result[trimmed] = 1;
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
return Object.keys(result).length > 0 ? result : void 0;
|
|
1677
|
-
}
|
|
1678
|
-
parseSelect(value) {
|
|
1679
|
-
if (!value) return void 0;
|
|
1680
|
-
const result = {};
|
|
1681
|
-
for (const field of String(value).split(",")) {
|
|
1682
|
-
const trimmed = field.trim();
|
|
1683
|
-
if (!trimmed || !/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) continue;
|
|
1684
|
-
result[trimmed.startsWith("-") ? trimmed.slice(1) : trimmed] = trimmed.startsWith("-") ? 0 : 1;
|
|
1685
|
-
}
|
|
1686
|
-
return Object.keys(result).length > 0 ? result : void 0;
|
|
1687
|
-
}
|
|
1688
|
-
parseFilters(query) {
|
|
1689
|
-
const reserved = /* @__PURE__ */ new Set(["page", "limit", "sort", "search", "select", "populate", "after", "cursor"]);
|
|
1690
|
-
const filters = {};
|
|
1691
|
-
const operators = {
|
|
1692
|
-
eq: "$eq",
|
|
1693
|
-
ne: "$ne",
|
|
1694
|
-
gt: "$gt",
|
|
1695
|
-
gte: "$gte",
|
|
1696
|
-
lt: "$lt",
|
|
1697
|
-
lte: "$lte",
|
|
1698
|
-
in: "$in",
|
|
1699
|
-
nin: "$nin",
|
|
1700
|
-
like: "$regex",
|
|
1701
|
-
contains: "$regex",
|
|
1702
|
-
exists: "$exists"
|
|
1703
|
-
};
|
|
1704
|
-
for (const [key, value] of Object.entries(query)) {
|
|
1705
|
-
if (reserved.has(key) || value === void 0 || value === null) continue;
|
|
1706
|
-
const match = key.match(/^([a-zA-Z_][a-zA-Z0-9_.]*)(?:\[([a-z]+)\])?$/);
|
|
1707
|
-
if (!match) continue;
|
|
1708
|
-
const [, fieldName, operator] = match;
|
|
1709
|
-
if (!fieldName) continue;
|
|
1710
|
-
if (operator && operators[operator]) {
|
|
1711
|
-
if (!filters[fieldName]) filters[fieldName] = {};
|
|
1712
|
-
filters[fieldName][operators[operator]] = this.coerceValue(value, operator);
|
|
1713
|
-
} else if (!operator) {
|
|
1714
|
-
filters[fieldName] = this.coerceValue(value);
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
return filters;
|
|
1718
|
-
}
|
|
1719
|
-
coerceValue(value, operator) {
|
|
1720
|
-
if (operator === "in" || operator === "nin") {
|
|
1721
|
-
if (Array.isArray(value)) return value.map((v) => this.coerceValue(v));
|
|
1722
|
-
if (typeof value === "string" && value.includes(",")) {
|
|
1723
|
-
return value.split(",").map((v) => this.coerceValue(v.trim()));
|
|
1724
|
-
}
|
|
1725
|
-
return [this.coerceValue(value)];
|
|
1726
|
-
}
|
|
1727
|
-
if (operator === "exists") {
|
|
1728
|
-
return String(value).toLowerCase() === "true" || value === "1";
|
|
1729
|
-
}
|
|
1730
|
-
if (value === "true") return true;
|
|
1731
|
-
if (value === "false") return false;
|
|
1732
|
-
if (value === "null") return null;
|
|
1733
|
-
if (typeof value === "string") {
|
|
1734
|
-
const num = Number(value);
|
|
1735
|
-
if (!Number.isNaN(num) && value.trim() !== "") return num;
|
|
1736
|
-
}
|
|
1737
|
-
return value;
|
|
1738
|
-
}
|
|
1739
|
-
};
|
|
1740
|
-
var PrismaAdapter = class {
|
|
1741
|
-
type = "prisma";
|
|
1742
|
-
name;
|
|
1743
|
-
repository;
|
|
1744
|
-
queryParser;
|
|
1745
|
-
client;
|
|
1746
|
-
modelName;
|
|
1747
|
-
dmmf;
|
|
1748
|
-
softDeleteEnabled;
|
|
1749
|
-
softDeleteField;
|
|
1750
|
-
constructor(options) {
|
|
1751
|
-
this.client = options.client;
|
|
1752
|
-
this.modelName = options.modelName;
|
|
1753
|
-
this.repository = options.repository;
|
|
1754
|
-
this.dmmf = options.dmmf;
|
|
1755
|
-
this.name = `prisma:${options.modelName}`;
|
|
1756
|
-
this.softDeleteEnabled = options.softDeleteEnabled ?? true;
|
|
1757
|
-
this.softDeleteField = options.softDeleteField ?? "deletedAt";
|
|
1758
|
-
this.queryParser = options.queryParser ?? new PrismaQueryParser({
|
|
1759
|
-
softDeleteEnabled: this.softDeleteEnabled,
|
|
1760
|
-
softDeleteField: this.softDeleteField
|
|
1761
|
-
});
|
|
1762
|
-
}
|
|
1763
|
-
/**
|
|
1764
|
-
* Parse URL query parameters and convert to Prisma query options
|
|
1765
|
-
*/
|
|
1766
|
-
parseQuery(query, policyFilters) {
|
|
1767
|
-
const parsed = this.queryParser.parse(query);
|
|
1768
|
-
return this.queryParser.toPrismaQuery(parsed, policyFilters);
|
|
1769
|
-
}
|
|
1770
|
-
/**
|
|
1771
|
-
* Apply policy filters to existing Prisma where clause
|
|
1772
|
-
* Used for multi-tenant, ownership, and other security filters
|
|
1773
|
-
*/
|
|
1774
|
-
applyPolicyFilters(where, policyFilters) {
|
|
1775
|
-
return { ...where, ...policyFilters };
|
|
1776
|
-
}
|
|
1777
|
-
generateSchemas(options) {
|
|
1778
|
-
if (!this.dmmf) return null;
|
|
1779
|
-
try {
|
|
1780
|
-
const model = this.dmmf.datamodel?.models?.find(
|
|
1781
|
-
(m) => m.name.toLowerCase() === this.modelName.toLowerCase()
|
|
1782
|
-
);
|
|
1783
|
-
if (!model) return null;
|
|
1784
|
-
const entitySchema = this.buildEntitySchema(model, options);
|
|
1785
|
-
const createBodySchema = this.buildCreateSchema(model, options);
|
|
1786
|
-
const updateBodySchema = this.buildUpdateSchema(model, options);
|
|
1787
|
-
return {
|
|
1788
|
-
entity: entitySchema,
|
|
1789
|
-
createBody: createBodySchema,
|
|
1790
|
-
updateBody: updateBodySchema,
|
|
1791
|
-
params: {
|
|
1792
|
-
type: "object",
|
|
1793
|
-
properties: {
|
|
1794
|
-
id: { type: "string" }
|
|
1795
|
-
},
|
|
1796
|
-
required: ["id"]
|
|
1797
|
-
},
|
|
1798
|
-
listQuery: {
|
|
1799
|
-
type: "object",
|
|
1800
|
-
properties: {
|
|
1801
|
-
page: { type: "number", minimum: 1, description: "Page number for pagination" },
|
|
1802
|
-
limit: { type: "number", minimum: 1, maximum: 100, description: "Items per page" },
|
|
1803
|
-
sort: { type: "string", description: 'Sort field (e.g., "name", "-createdAt")' }
|
|
1804
|
-
// Note: Actual filtering requires custom query parser implementation
|
|
1805
|
-
// This is placeholder documentation only
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
};
|
|
1809
|
-
} catch {
|
|
1810
|
-
return null;
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
getSchemaMetadata() {
|
|
1814
|
-
if (!this.dmmf) return null;
|
|
1815
|
-
try {
|
|
1816
|
-
const model = this.dmmf.datamodel?.models?.find(
|
|
1817
|
-
(m) => m.name.toLowerCase() === this.modelName.toLowerCase()
|
|
1818
|
-
);
|
|
1819
|
-
if (!model) return null;
|
|
1820
|
-
const fields = {};
|
|
1821
|
-
for (const field of model.fields) {
|
|
1822
|
-
fields[field.name] = this.convertPrismaFieldToMetadata(field);
|
|
1823
|
-
}
|
|
1824
|
-
return {
|
|
1825
|
-
name: model.name,
|
|
1826
|
-
fields,
|
|
1827
|
-
indexes: model.uniqueIndexes?.map((idx) => ({
|
|
1828
|
-
fields: idx.fields,
|
|
1829
|
-
unique: true
|
|
1830
|
-
}))
|
|
1831
|
-
};
|
|
1832
|
-
} catch (err) {
|
|
1833
|
-
return null;
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
async validate(data) {
|
|
1837
|
-
if (!data || typeof data !== "object") {
|
|
1838
|
-
return {
|
|
1839
|
-
valid: false,
|
|
1840
|
-
errors: [{ field: "root", message: "Data must be an object" }]
|
|
1841
|
-
};
|
|
1842
|
-
}
|
|
1843
|
-
if (this.dmmf) {
|
|
1844
|
-
try {
|
|
1845
|
-
const model = this.dmmf.datamodel?.models?.find(
|
|
1846
|
-
(m) => m.name.toLowerCase() === this.modelName.toLowerCase()
|
|
1847
|
-
);
|
|
1848
|
-
if (model) {
|
|
1849
|
-
const requiredFields = model.fields.filter(
|
|
1850
|
-
(f) => f.isRequired && !f.hasDefaultValue && !f.isGenerated
|
|
1851
|
-
);
|
|
1852
|
-
const errors = [];
|
|
1853
|
-
for (const field of requiredFields) {
|
|
1854
|
-
if (!(field.name in data)) {
|
|
1855
|
-
errors.push({
|
|
1856
|
-
field: field.name,
|
|
1857
|
-
message: `${field.name} is required`
|
|
1858
|
-
});
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
if (errors.length > 0) {
|
|
1862
|
-
return { valid: false, errors };
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
} catch (err) {
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
return { valid: true };
|
|
1869
|
-
}
|
|
1870
|
-
async healthCheck() {
|
|
1871
|
-
try {
|
|
1872
|
-
const delegateName = this.modelName.charAt(0).toLowerCase() + this.modelName.slice(1);
|
|
1873
|
-
const delegate = this.client[delegateName];
|
|
1874
|
-
if (!delegate) {
|
|
1875
|
-
return false;
|
|
1876
|
-
}
|
|
1877
|
-
await delegate.findMany({ take: 1 });
|
|
1878
|
-
return true;
|
|
1879
|
-
} catch (err) {
|
|
1880
|
-
return false;
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
async close() {
|
|
1884
|
-
try {
|
|
1885
|
-
await this.client.$disconnect();
|
|
1886
|
-
} catch (err) {
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
// ============================================================================
|
|
1890
|
-
// Private Helper Methods
|
|
1891
|
-
// ============================================================================
|
|
1892
|
-
buildEntitySchema(model, options) {
|
|
1893
|
-
const properties = {};
|
|
1894
|
-
const required = [];
|
|
1895
|
-
for (const field of model.fields) {
|
|
1896
|
-
if (this.shouldSkipField(field, options)) continue;
|
|
1897
|
-
properties[field.name] = this.convertPrismaFieldToJsonSchema(field);
|
|
1898
|
-
if (field.isRequired && !field.hasDefaultValue) {
|
|
1899
|
-
required.push(field.name);
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
return {
|
|
1903
|
-
type: "object",
|
|
1904
|
-
properties,
|
|
1905
|
-
...required.length > 0 && { required }
|
|
1906
|
-
};
|
|
1907
|
-
}
|
|
1908
|
-
buildCreateSchema(model, options) {
|
|
1909
|
-
const properties = {};
|
|
1910
|
-
const required = [];
|
|
1911
|
-
for (const field of model.fields) {
|
|
1912
|
-
if (field.isGenerated || field.relationName) continue;
|
|
1913
|
-
if (this.shouldSkipField(field, options)) continue;
|
|
1914
|
-
properties[field.name] = this.convertPrismaFieldToJsonSchema(field);
|
|
1915
|
-
if (field.isRequired && !field.hasDefaultValue) {
|
|
1916
|
-
required.push(field.name);
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
return {
|
|
1920
|
-
type: "object",
|
|
1921
|
-
properties,
|
|
1922
|
-
...required.length > 0 && { required }
|
|
1923
|
-
};
|
|
1924
|
-
}
|
|
1925
|
-
buildUpdateSchema(model, options) {
|
|
1926
|
-
const properties = {};
|
|
1927
|
-
for (const field of model.fields) {
|
|
1928
|
-
if (field.isGenerated || field.isId || field.relationName) continue;
|
|
1929
|
-
if (this.shouldSkipField(field, options)) continue;
|
|
1930
|
-
properties[field.name] = this.convertPrismaFieldToJsonSchema(field);
|
|
1931
|
-
}
|
|
1932
|
-
return {
|
|
1933
|
-
type: "object",
|
|
1934
|
-
properties
|
|
1935
|
-
};
|
|
1936
|
-
}
|
|
1937
|
-
shouldSkipField(field, options) {
|
|
1938
|
-
if (options?.excludeFields?.includes(field.name)) {
|
|
1939
|
-
return true;
|
|
1940
|
-
}
|
|
1941
|
-
if (field.name.startsWith("_")) {
|
|
1942
|
-
return true;
|
|
1943
|
-
}
|
|
1944
|
-
return false;
|
|
1945
|
-
}
|
|
1946
|
-
convertPrismaFieldToJsonSchema(field) {
|
|
1947
|
-
const schema = {};
|
|
1948
|
-
switch (field.type) {
|
|
1949
|
-
case "String":
|
|
1950
|
-
schema.type = "string";
|
|
1951
|
-
break;
|
|
1952
|
-
case "Int":
|
|
1953
|
-
case "BigInt":
|
|
1954
|
-
schema.type = "integer";
|
|
1955
|
-
break;
|
|
1956
|
-
case "Float":
|
|
1957
|
-
case "Decimal":
|
|
1958
|
-
schema.type = "number";
|
|
1959
|
-
break;
|
|
1960
|
-
case "Boolean":
|
|
1961
|
-
schema.type = "boolean";
|
|
1962
|
-
break;
|
|
1963
|
-
case "DateTime":
|
|
1964
|
-
schema.type = "string";
|
|
1965
|
-
schema.format = "date-time";
|
|
1966
|
-
break;
|
|
1967
|
-
case "Json":
|
|
1968
|
-
schema.type = "object";
|
|
1969
|
-
break;
|
|
1970
|
-
default:
|
|
1971
|
-
if (field.kind === "enum") {
|
|
1972
|
-
schema.type = "string";
|
|
1973
|
-
if (this.dmmf?.datamodel?.enums) {
|
|
1974
|
-
const enumDef = this.dmmf.datamodel.enums.find((e) => e.name === field.type);
|
|
1975
|
-
if (enumDef) {
|
|
1976
|
-
schema.enum = enumDef.values.map((v) => v.name);
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
} else {
|
|
1980
|
-
schema.type = "string";
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
if (field.isList) {
|
|
1984
|
-
return {
|
|
1985
|
-
type: "array",
|
|
1986
|
-
items: schema
|
|
1987
|
-
};
|
|
1988
|
-
}
|
|
1989
|
-
if (field.documentation) {
|
|
1990
|
-
schema.description = field.documentation;
|
|
1991
|
-
}
|
|
1992
|
-
return schema;
|
|
1993
|
-
}
|
|
1994
|
-
convertPrismaFieldToMetadata(field) {
|
|
1995
|
-
const metadata = {
|
|
1996
|
-
type: this.mapPrismaTypeToMetadataType(field.type, field.kind),
|
|
1997
|
-
required: field.isRequired,
|
|
1998
|
-
array: field.isList
|
|
1999
|
-
};
|
|
2000
|
-
if (field.isUnique) {
|
|
2001
|
-
metadata.unique = true;
|
|
2002
|
-
}
|
|
2003
|
-
if (field.hasDefaultValue) {
|
|
2004
|
-
metadata.default = field.default;
|
|
2005
|
-
}
|
|
2006
|
-
if (field.documentation) {
|
|
2007
|
-
metadata.description = field.documentation;
|
|
2008
|
-
}
|
|
2009
|
-
if (field.relationName) {
|
|
2010
|
-
metadata.ref = field.type;
|
|
2011
|
-
}
|
|
2012
|
-
return metadata;
|
|
2013
|
-
}
|
|
2014
|
-
mapPrismaTypeToMetadataType(type, kind) {
|
|
2015
|
-
if (kind === "enum") return "enum";
|
|
2016
|
-
switch (type) {
|
|
2017
|
-
case "String":
|
|
2018
|
-
return "string";
|
|
2019
|
-
case "Int":
|
|
2020
|
-
case "BigInt":
|
|
2021
|
-
case "Float":
|
|
2022
|
-
case "Decimal":
|
|
2023
|
-
return "number";
|
|
2024
|
-
case "Boolean":
|
|
2025
|
-
return "boolean";
|
|
2026
|
-
case "DateTime":
|
|
2027
|
-
return "date";
|
|
2028
|
-
case "Json":
|
|
2029
|
-
return "object";
|
|
2030
|
-
default:
|
|
2031
|
-
return "string";
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
};
|
|
2035
|
-
function createPrismaAdapter(options) {
|
|
2036
|
-
return new PrismaAdapter(options);
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
// src/types/index.ts
|
|
2040
|
-
function getUserId(user) {
|
|
2041
|
-
if (!user) return void 0;
|
|
2042
|
-
const id = user.id ?? user._id;
|
|
2043
|
-
return id ? String(id) : void 0;
|
|
2044
|
-
}
|
|
2045
|
-
|
|
2046
|
-
// src/core/BaseController.ts
|
|
2047
|
-
init_HookSystem();
|
|
2048
|
-
|
|
2049
|
-
// src/utils/queryParser.ts
|
|
2050
|
-
var DANGEROUS_REGEX_PATTERNS = /(\{[0-9,]+\}|\*\+|\+\+|\?\+|(\(.+\))\+|\(\?\:|\\[0-9]|(\[.+\]).+(\[.+\]))/;
|
|
2051
|
-
var MAX_REGEX_LENGTH = 500;
|
|
2052
|
-
var MAX_SEARCH_LENGTH = 200;
|
|
2053
|
-
var MAX_FILTER_DEPTH = 10;
|
|
2054
|
-
var MAX_LIMIT = 1e3;
|
|
2055
|
-
var DEFAULT_LIMIT = 20;
|
|
2056
|
-
var ArcQueryParser = class {
|
|
2057
|
-
maxLimit;
|
|
2058
|
-
defaultLimit;
|
|
2059
|
-
maxRegexLength;
|
|
2060
|
-
maxSearchLength;
|
|
2061
|
-
maxFilterDepth;
|
|
2062
|
-
/** Supported filter operators */
|
|
2063
|
-
operators = {
|
|
2064
|
-
eq: "$eq",
|
|
2065
|
-
ne: "$ne",
|
|
2066
|
-
gt: "$gt",
|
|
2067
|
-
gte: "$gte",
|
|
2068
|
-
lt: "$lt",
|
|
2069
|
-
lte: "$lte",
|
|
2070
|
-
in: "$in",
|
|
2071
|
-
nin: "$nin",
|
|
2072
|
-
like: "$regex",
|
|
2073
|
-
contains: "$regex",
|
|
2074
|
-
regex: "$regex",
|
|
2075
|
-
exists: "$exists"
|
|
2076
|
-
};
|
|
2077
|
-
constructor(options = {}) {
|
|
2078
|
-
this.maxLimit = options.maxLimit ?? MAX_LIMIT;
|
|
2079
|
-
this.defaultLimit = options.defaultLimit ?? DEFAULT_LIMIT;
|
|
2080
|
-
this.maxRegexLength = options.maxRegexLength ?? MAX_REGEX_LENGTH;
|
|
2081
|
-
this.maxSearchLength = options.maxSearchLength ?? MAX_SEARCH_LENGTH;
|
|
2082
|
-
this.maxFilterDepth = options.maxFilterDepth ?? MAX_FILTER_DEPTH;
|
|
2083
|
-
}
|
|
2084
|
-
/**
|
|
2085
|
-
* Parse URL query parameters into structured query options
|
|
2086
|
-
*/
|
|
2087
|
-
parse(query) {
|
|
2088
|
-
const q = query ?? {};
|
|
2089
|
-
const page = this.parseNumber(q.page, 1);
|
|
2090
|
-
const limit = Math.min(this.parseNumber(q.limit, this.defaultLimit), this.maxLimit);
|
|
2091
|
-
const after = this.parseString(q.after ?? q.cursor);
|
|
2092
|
-
const sort = this.parseSort(q.sort);
|
|
2093
|
-
const populate = this.parseString(q.populate);
|
|
2094
|
-
const search = this.parseSearch(q.search);
|
|
2095
|
-
const select = this.parseSelect(q.select);
|
|
2096
|
-
const filters = this.parseFilters(q);
|
|
2097
|
-
return {
|
|
2098
|
-
filters,
|
|
2099
|
-
limit,
|
|
2100
|
-
sort,
|
|
2101
|
-
populate,
|
|
2102
|
-
search,
|
|
2103
|
-
page: after ? void 0 : page,
|
|
2104
|
-
after,
|
|
2105
|
-
select
|
|
2106
|
-
};
|
|
2107
|
-
}
|
|
2108
|
-
// ============================================================================
|
|
2109
|
-
// Parse Helpers
|
|
2110
|
-
// ============================================================================
|
|
2111
|
-
parseNumber(value, defaultValue) {
|
|
2112
|
-
if (value === void 0 || value === null) return defaultValue;
|
|
2113
|
-
const num = parseInt(String(value), 10);
|
|
2114
|
-
return Number.isNaN(num) ? defaultValue : Math.max(1, num);
|
|
2115
|
-
}
|
|
2116
|
-
parseString(value) {
|
|
2117
|
-
if (value === void 0 || value === null) return void 0;
|
|
2118
|
-
const str = String(value).trim();
|
|
2119
|
-
return str.length > 0 ? str : void 0;
|
|
2120
|
-
}
|
|
2121
|
-
parseSort(value) {
|
|
2122
|
-
if (!value) return void 0;
|
|
2123
|
-
const sortStr = String(value);
|
|
2124
|
-
const result = {};
|
|
2125
|
-
for (const field of sortStr.split(",")) {
|
|
2126
|
-
const trimmed = field.trim();
|
|
2127
|
-
if (!trimmed) continue;
|
|
2128
|
-
if (!/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) continue;
|
|
2129
|
-
if (trimmed.startsWith("-")) {
|
|
2130
|
-
result[trimmed.slice(1)] = -1;
|
|
2131
|
-
} else {
|
|
2132
|
-
result[trimmed] = 1;
|
|
2133
|
-
}
|
|
2134
|
-
}
|
|
2135
|
-
return Object.keys(result).length > 0 ? result : void 0;
|
|
2136
|
-
}
|
|
2137
|
-
parseSearch(value) {
|
|
2138
|
-
if (!value) return void 0;
|
|
2139
|
-
const search = String(value).trim();
|
|
2140
|
-
if (search.length === 0) return void 0;
|
|
2141
|
-
if (search.length > this.maxSearchLength) {
|
|
2142
|
-
return search.slice(0, this.maxSearchLength);
|
|
2143
|
-
}
|
|
2144
|
-
return search;
|
|
2145
|
-
}
|
|
2146
|
-
parseSelect(value) {
|
|
2147
|
-
if (!value) return void 0;
|
|
2148
|
-
const selectStr = String(value);
|
|
2149
|
-
const result = {};
|
|
2150
|
-
for (const field of selectStr.split(",")) {
|
|
2151
|
-
const trimmed = field.trim();
|
|
2152
|
-
if (!trimmed) continue;
|
|
2153
|
-
if (!/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) continue;
|
|
2154
|
-
if (trimmed.startsWith("-")) {
|
|
2155
|
-
result[trimmed.slice(1)] = 0;
|
|
2156
|
-
} else {
|
|
2157
|
-
result[trimmed] = 1;
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
return Object.keys(result).length > 0 ? result : void 0;
|
|
2161
|
-
}
|
|
2162
|
-
parseFilters(query) {
|
|
2163
|
-
const reservedKeys = /* @__PURE__ */ new Set([
|
|
2164
|
-
"page",
|
|
2165
|
-
"limit",
|
|
2166
|
-
"sort",
|
|
2167
|
-
"populate",
|
|
2168
|
-
"search",
|
|
2169
|
-
"select",
|
|
2170
|
-
"after",
|
|
2171
|
-
"cursor",
|
|
2172
|
-
"lean",
|
|
2173
|
-
"_policyFilters"
|
|
2174
|
-
]);
|
|
2175
|
-
const filters = {};
|
|
2176
|
-
for (const [key, value] of Object.entries(query)) {
|
|
2177
|
-
if (reservedKeys.has(key)) continue;
|
|
2178
|
-
if (value === void 0 || value === null) continue;
|
|
2179
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(key)) continue;
|
|
2180
|
-
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
2181
|
-
const operatorObj = value;
|
|
2182
|
-
const operatorKeys = Object.keys(operatorObj);
|
|
2183
|
-
const allOperators = operatorKeys.every((op) => this.operators[op]);
|
|
2184
|
-
if (allOperators && operatorKeys.length > 0) {
|
|
2185
|
-
const mongoFilters = {};
|
|
2186
|
-
for (const [op, opValue] of Object.entries(operatorObj)) {
|
|
2187
|
-
const mongoOp = this.operators[op];
|
|
2188
|
-
if (mongoOp) {
|
|
2189
|
-
mongoFilters[mongoOp] = this.parseFilterValue(opValue, op);
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
filters[key] = mongoFilters;
|
|
2193
|
-
continue;
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
const match = key.match(/^([a-zA-Z_][a-zA-Z0-9_.]*)(?:\[([a-z]+)\])?$/);
|
|
2197
|
-
if (!match) continue;
|
|
2198
|
-
const [, fieldName, operator] = match;
|
|
2199
|
-
if (!fieldName) continue;
|
|
2200
|
-
if (operator && this.operators[operator]) {
|
|
2201
|
-
const mongoOp = this.operators[operator];
|
|
2202
|
-
const parsedValue = this.parseFilterValue(value, operator);
|
|
2203
|
-
if (!filters[fieldName]) {
|
|
2204
|
-
filters[fieldName] = {};
|
|
2205
|
-
}
|
|
2206
|
-
filters[fieldName][mongoOp] = parsedValue;
|
|
2207
|
-
} else if (!operator) {
|
|
2208
|
-
filters[fieldName] = this.parseFilterValue(value);
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
return filters;
|
|
2212
|
-
}
|
|
2213
|
-
parseFilterValue(value, operator) {
|
|
2214
|
-
if (operator === "in" || operator === "nin") {
|
|
2215
|
-
if (Array.isArray(value)) {
|
|
2216
|
-
return value.map((v) => this.coerceValue(v));
|
|
2217
|
-
}
|
|
2218
|
-
if (typeof value === "string" && value.includes(",")) {
|
|
2219
|
-
return value.split(",").map((v) => this.coerceValue(v.trim()));
|
|
2220
|
-
}
|
|
2221
|
-
return [this.coerceValue(value)];
|
|
2222
|
-
}
|
|
2223
|
-
if (operator === "like" || operator === "contains" || operator === "regex") {
|
|
2224
|
-
return this.sanitizeRegex(String(value));
|
|
2225
|
-
}
|
|
2226
|
-
if (operator === "exists") {
|
|
2227
|
-
const str = String(value).toLowerCase();
|
|
2228
|
-
return str === "true" || str === "1";
|
|
2229
|
-
}
|
|
2230
|
-
return this.coerceValue(value);
|
|
2231
|
-
}
|
|
2232
|
-
coerceValue(value) {
|
|
2233
|
-
if (value === "true") return true;
|
|
2234
|
-
if (value === "false") return false;
|
|
2235
|
-
if (value === "null") return null;
|
|
2236
|
-
if (typeof value === "string") {
|
|
2237
|
-
const num = Number(value);
|
|
2238
|
-
if (!Number.isNaN(num) && value.trim() !== "") {
|
|
2239
|
-
return num;
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2242
|
-
return value;
|
|
2243
|
-
}
|
|
2244
|
-
sanitizeRegex(pattern) {
|
|
2245
|
-
let sanitized = pattern.slice(0, this.maxRegexLength);
|
|
2246
|
-
if (DANGEROUS_REGEX_PATTERNS.test(sanitized)) {
|
|
2247
|
-
sanitized = sanitized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2248
|
-
}
|
|
2249
|
-
return sanitized;
|
|
2250
|
-
}
|
|
2251
|
-
};
|
|
2252
|
-
|
|
2253
|
-
// src/core/BaseController.ts
|
|
2254
|
-
var defaultParser = new ArcQueryParser();
|
|
2255
|
-
function getDefaultQueryParser() {
|
|
2256
|
-
return defaultParser;
|
|
2257
|
-
}
|
|
2258
|
-
var BaseController = class _BaseController {
|
|
2259
|
-
repository;
|
|
2260
|
-
schemaOptions;
|
|
2261
|
-
queryParser;
|
|
2262
|
-
maxLimit;
|
|
2263
|
-
defaultLimit;
|
|
2264
|
-
defaultSort;
|
|
2265
|
-
resourceName;
|
|
2266
|
-
disableEvents;
|
|
2267
|
-
/** Preset field names for dynamic param reading */
|
|
2268
|
-
_presetFields = {};
|
|
2269
|
-
constructor(repository, options = {}) {
|
|
2270
|
-
this.repository = repository;
|
|
2271
|
-
this.schemaOptions = options.schemaOptions ?? {};
|
|
2272
|
-
this.queryParser = options.queryParser ?? getDefaultQueryParser();
|
|
2273
|
-
this.maxLimit = options.maxLimit ?? 100;
|
|
2274
|
-
this.defaultLimit = options.defaultLimit ?? 20;
|
|
2275
|
-
this.defaultSort = options.defaultSort ?? "-createdAt";
|
|
2276
|
-
this.resourceName = options.resourceName;
|
|
2277
|
-
this.disableEvents = options.disableEvents ?? false;
|
|
2278
|
-
this.list = this.list.bind(this);
|
|
2279
|
-
this.get = this.get.bind(this);
|
|
2280
|
-
this.create = this.create.bind(this);
|
|
2281
|
-
this.update = this.update.bind(this);
|
|
2282
|
-
this.delete = this.delete.bind(this);
|
|
2283
|
-
}
|
|
2284
|
-
/**
|
|
2285
|
-
* Inject resource options from defineResource
|
|
2286
|
-
*/
|
|
2287
|
-
_setResourceOptions(options) {
|
|
2288
|
-
if (options.schemaOptions) {
|
|
2289
|
-
this.schemaOptions = { ...this.schemaOptions, ...options.schemaOptions };
|
|
2290
|
-
}
|
|
2291
|
-
if (options.presetFields) {
|
|
2292
|
-
this._presetFields = { ...this._presetFields, ...options.presetFields };
|
|
2293
|
-
}
|
|
2294
|
-
if (options.resourceName) {
|
|
2295
|
-
this.resourceName = options.resourceName;
|
|
2296
|
-
}
|
|
2297
|
-
if (options.queryParser) {
|
|
2298
|
-
this.queryParser = options.queryParser;
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
// ============================================================================
|
|
2302
|
-
// Context & Query Parsing
|
|
2303
|
-
// ============================================================================
|
|
2304
|
-
/**
|
|
2305
|
-
* Build service context from IRequestContext
|
|
2306
|
-
*/
|
|
2307
|
-
_buildContext(req) {
|
|
2308
|
-
const parsed = this.queryParser.parse(req.query);
|
|
2309
|
-
const arcContext = req.metadata;
|
|
2310
|
-
const selectString = this._selectToString(parsed.select) ?? req.query?.select;
|
|
2311
|
-
const sanitizedSelect = this._sanitizeSelect(selectString, this.schemaOptions);
|
|
2312
|
-
return {
|
|
2313
|
-
user: req.user,
|
|
2314
|
-
organizationId: arcContext?.organizationId ?? req.organizationId ?? void 0,
|
|
2315
|
-
select: sanitizedSelect ? sanitizedSelect.split(/\s+/) : void 0,
|
|
2316
|
-
populate: this._sanitizePopulate(parsed.populate, this.schemaOptions),
|
|
2317
|
-
lean: this._parseLean(req.query?.lean)
|
|
2318
|
-
};
|
|
2319
|
-
}
|
|
2320
|
-
/**
|
|
2321
|
-
* Parse query into QueryOptions using queryParser
|
|
2322
|
-
*/
|
|
2323
|
-
_parseQueryOptions(req) {
|
|
2324
|
-
const parsed = this.queryParser.parse(req.query);
|
|
2325
|
-
const arcContext = req.metadata;
|
|
2326
|
-
delete parsed.filters._policyFilters;
|
|
2327
|
-
const limit = Math.min(Math.max(1, parsed.limit || this.defaultLimit), this.maxLimit);
|
|
2328
|
-
const page = parsed.after ? void 0 : parsed.page ? Math.max(1, parsed.page) : 1;
|
|
2329
|
-
const sortString = parsed.sort ? Object.entries(parsed.sort).map(([k, v]) => v === -1 ? `-${k}` : k).join(",") : this.defaultSort;
|
|
2330
|
-
const selectString = this._selectToString(parsed.select) ?? req.query?.select;
|
|
2331
|
-
return {
|
|
2332
|
-
page,
|
|
2333
|
-
limit,
|
|
2334
|
-
sort: sortString,
|
|
2335
|
-
select: this._sanitizeSelect(selectString, this.schemaOptions),
|
|
2336
|
-
populate: this._sanitizePopulate(parsed.populate, this.schemaOptions),
|
|
2337
|
-
// Advanced populate options from MongoKit QueryParser (takes precedence over simple populate)
|
|
2338
|
-
populateOptions: parsed.populateOptions,
|
|
2339
|
-
filters: parsed.filters,
|
|
2340
|
-
// MongoKit features
|
|
2341
|
-
search: parsed.search,
|
|
2342
|
-
after: parsed.after,
|
|
2343
|
-
user: req.user,
|
|
2344
|
-
organizationId: arcContext?.organizationId ?? req.organizationId,
|
|
2345
|
-
context: arcContext
|
|
2346
|
-
};
|
|
2347
|
-
}
|
|
2348
|
-
/**
|
|
2349
|
-
* Apply org and policy filters
|
|
2350
|
-
*/
|
|
2351
|
-
_applyFilters(options, req) {
|
|
2352
|
-
const filters = { ...options.filters };
|
|
2353
|
-
const arcContext = req.metadata;
|
|
2354
|
-
const policyFilters = arcContext?._policyFilters;
|
|
2355
|
-
if (policyFilters) {
|
|
2356
|
-
Object.assign(filters, policyFilters);
|
|
2357
|
-
}
|
|
2358
|
-
const orgId = arcContext?.organizationId ?? req.organizationId;
|
|
2359
|
-
if (orgId) {
|
|
2360
|
-
filters.organizationId = orgId;
|
|
2361
|
-
}
|
|
2362
|
-
return { ...options, filters };
|
|
2363
|
-
}
|
|
2364
|
-
/**
|
|
2365
|
-
* Build filter for single-item operations (get/update/delete)
|
|
2366
|
-
* Combines ID filter with policy/org filters for proper security enforcement
|
|
2367
|
-
*/
|
|
2368
|
-
_buildIdFilter(id, req) {
|
|
2369
|
-
const filter = { _id: id };
|
|
2370
|
-
const arcContext = req.metadata;
|
|
2371
|
-
const policyFilters = arcContext?._policyFilters;
|
|
2372
|
-
if (policyFilters) {
|
|
2373
|
-
Object.assign(filter, policyFilters);
|
|
2374
|
-
}
|
|
2375
|
-
const orgId = arcContext?.organizationId ?? req.organizationId;
|
|
2376
|
-
if (orgId) {
|
|
2377
|
-
filter.organizationId = orgId;
|
|
2378
|
-
}
|
|
2379
|
-
return filter;
|
|
2380
|
-
}
|
|
2381
|
-
/**
|
|
2382
|
-
* Check if a value matches a MongoDB query operator
|
|
2383
|
-
*/
|
|
2384
|
-
_matchesOperator(itemValue, operator, filterValue) {
|
|
2385
|
-
switch (operator) {
|
|
2386
|
-
case "$eq":
|
|
2387
|
-
return itemValue === filterValue;
|
|
2388
|
-
case "$ne":
|
|
2389
|
-
return itemValue !== filterValue;
|
|
2390
|
-
case "$gt":
|
|
2391
|
-
return typeof itemValue === "number" && typeof filterValue === "number" && itemValue > filterValue;
|
|
2392
|
-
case "$gte":
|
|
2393
|
-
return typeof itemValue === "number" && typeof filterValue === "number" && itemValue >= filterValue;
|
|
2394
|
-
case "$lt":
|
|
2395
|
-
return typeof itemValue === "number" && typeof filterValue === "number" && itemValue < filterValue;
|
|
2396
|
-
case "$lte":
|
|
2397
|
-
return typeof itemValue === "number" && typeof filterValue === "number" && itemValue <= filterValue;
|
|
2398
|
-
case "$in":
|
|
2399
|
-
return Array.isArray(filterValue) && filterValue.includes(itemValue);
|
|
2400
|
-
case "$nin":
|
|
2401
|
-
return Array.isArray(filterValue) && !filterValue.includes(itemValue);
|
|
2402
|
-
case "$exists":
|
|
2403
|
-
return filterValue ? itemValue !== void 0 : itemValue === void 0;
|
|
2404
|
-
case "$regex":
|
|
2405
|
-
if (typeof itemValue === "string" && (typeof filterValue === "string" || filterValue instanceof RegExp)) {
|
|
2406
|
-
const regex = typeof filterValue === "string" ? new RegExp(filterValue) : filterValue;
|
|
2407
|
-
return regex.test(itemValue);
|
|
2408
|
-
}
|
|
2409
|
-
return false;
|
|
2410
|
-
default:
|
|
2411
|
-
return false;
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
/**
|
|
2415
|
-
* Forbidden paths that could lead to prototype pollution
|
|
2416
|
-
*/
|
|
2417
|
-
static FORBIDDEN_PATHS = ["__proto__", "constructor", "prototype"];
|
|
2418
|
-
/**
|
|
2419
|
-
* Get nested value from object using dot notation (e.g., "owner.id")
|
|
2420
|
-
* Security: Validates path against forbidden patterns to prevent prototype pollution
|
|
2421
|
-
*/
|
|
2422
|
-
_getNestedValue(obj, path) {
|
|
2423
|
-
if (_BaseController.FORBIDDEN_PATHS.some((p) => path.toLowerCase().includes(p))) {
|
|
2424
|
-
return void 0;
|
|
2425
|
-
}
|
|
2426
|
-
const keys = path.split(".");
|
|
2427
|
-
let value = obj;
|
|
2428
|
-
for (const key of keys) {
|
|
2429
|
-
if (value == null) return void 0;
|
|
2430
|
-
if (_BaseController.FORBIDDEN_PATHS.includes(key.toLowerCase())) {
|
|
2431
|
-
return void 0;
|
|
2432
|
-
}
|
|
2433
|
-
value = value[key];
|
|
2434
|
-
}
|
|
2435
|
-
return value;
|
|
2436
|
-
}
|
|
2437
|
-
/**
|
|
2438
|
-
* Check if item matches a single filter condition
|
|
2439
|
-
* Supports nested paths (e.g., "owner.id", "metadata.status")
|
|
2440
|
-
*/
|
|
2441
|
-
_matchesFilter(item, key, filterValue) {
|
|
2442
|
-
const itemValue = key.includes(".") ? this._getNestedValue(item, key) : item[key];
|
|
2443
|
-
if (filterValue && typeof filterValue === "object" && !Array.isArray(filterValue)) {
|
|
2444
|
-
const operators = Object.keys(filterValue);
|
|
2445
|
-
if (operators.some((op) => op.startsWith("$"))) {
|
|
2446
|
-
for (const [operator, opValue] of Object.entries(filterValue)) {
|
|
2447
|
-
if (!this._matchesOperator(itemValue, operator, opValue)) {
|
|
2448
|
-
return false;
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
return true;
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
return String(itemValue) === String(filterValue);
|
|
2455
|
-
}
|
|
2456
|
-
/**
|
|
2457
|
-
* Check if item matches policy filters (for get/update/delete operations)
|
|
2458
|
-
* Validates that fetched item satisfies all policy constraints
|
|
2459
|
-
* Supports MongoDB query operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $regex, $and, $or
|
|
2460
|
-
*/
|
|
2461
|
-
_checkPolicyFilters(item, req) {
|
|
2462
|
-
const arcContext = req.metadata;
|
|
2463
|
-
const policyFilters = arcContext?._policyFilters;
|
|
2464
|
-
if (!policyFilters) return true;
|
|
2465
|
-
if (policyFilters.$and && Array.isArray(policyFilters.$and)) {
|
|
2466
|
-
return policyFilters.$and.every((condition) => {
|
|
2467
|
-
return Object.entries(condition).every(([key, value]) => {
|
|
2468
|
-
return this._matchesFilter(item, key, value);
|
|
2469
|
-
});
|
|
2470
|
-
});
|
|
2471
|
-
}
|
|
2472
|
-
if (policyFilters.$or && Array.isArray(policyFilters.$or)) {
|
|
2473
|
-
return policyFilters.$or.some((condition) => {
|
|
2474
|
-
return Object.entries(condition).every(([key, value]) => {
|
|
2475
|
-
return this._matchesFilter(item, key, value);
|
|
2476
|
-
});
|
|
2477
|
-
});
|
|
2478
|
-
}
|
|
2479
|
-
for (const [key, value] of Object.entries(policyFilters)) {
|
|
2480
|
-
if (key.startsWith("$")) continue;
|
|
2481
|
-
if (!this._matchesFilter(item, key, value)) {
|
|
2482
|
-
return false;
|
|
2483
|
-
}
|
|
2484
|
-
}
|
|
2485
|
-
return true;
|
|
2486
|
-
}
|
|
2487
|
-
// ============================================================================
|
|
2488
|
-
// Sanitization Helpers
|
|
2489
|
-
// ============================================================================
|
|
2490
|
-
/** Parse lean option (default: true for performance) */
|
|
2491
|
-
_parseLean(leanValue) {
|
|
2492
|
-
if (typeof leanValue === "boolean") return leanValue;
|
|
2493
|
-
if (typeof leanValue === "string") return leanValue.toLowerCase() !== "false";
|
|
2494
|
-
return true;
|
|
2495
|
-
}
|
|
2496
|
-
/** Get blocked fields from schema options */
|
|
2497
|
-
_getBlockedFields(schemaOptions) {
|
|
2498
|
-
const fieldRules = schemaOptions.fieldRules ?? {};
|
|
2499
|
-
return Object.entries(fieldRules).filter(([, rules]) => rules.systemManaged || rules.hidden).map(([field]) => field);
|
|
2500
|
-
}
|
|
2501
|
-
/**
|
|
2502
|
-
* Convert parsed select object to string format
|
|
2503
|
-
* Converts { name: 1, email: 1, password: 0 } → 'name email -password'
|
|
2504
|
-
*/
|
|
2505
|
-
_selectToString(select) {
|
|
2506
|
-
if (!select) return void 0;
|
|
2507
|
-
if (typeof select === "string") return select;
|
|
2508
|
-
if (Array.isArray(select)) return select.join(" ");
|
|
2509
|
-
if (Object.keys(select).length === 0) return void 0;
|
|
2510
|
-
return Object.entries(select).map(([field, include]) => include === 0 ? `-${field}` : field).join(" ");
|
|
2511
|
-
}
|
|
2512
|
-
/** Sanitize select fields */
|
|
2513
|
-
_sanitizeSelect(select, schemaOptions) {
|
|
2514
|
-
if (!select) return void 0;
|
|
2515
|
-
const blockedFields = this._getBlockedFields(schemaOptions);
|
|
2516
|
-
if (blockedFields.length === 0) return select;
|
|
2517
|
-
const fields = select.split(/[\s,]+/).filter(Boolean);
|
|
2518
|
-
const sanitized = fields.filter((f) => {
|
|
2519
|
-
const fieldName = f.replace(/^-/, "");
|
|
2520
|
-
return !blockedFields.includes(fieldName);
|
|
2521
|
-
});
|
|
2522
|
-
return sanitized.length > 0 ? sanitized.join(" ") : void 0;
|
|
2523
|
-
}
|
|
2524
|
-
/** Sanitize populate fields */
|
|
2525
|
-
_sanitizePopulate(populate, schemaOptions) {
|
|
2526
|
-
if (!populate) return void 0;
|
|
2527
|
-
const allowedPopulate = schemaOptions.query?.allowedPopulate;
|
|
2528
|
-
const requested = typeof populate === "string" ? populate.split(",").map((p) => p.trim()) : Array.isArray(populate) ? populate.map(String) : [];
|
|
2529
|
-
if (requested.length === 0) return void 0;
|
|
2530
|
-
if (!allowedPopulate) return requested;
|
|
2531
|
-
const sanitized = requested.filter((p) => allowedPopulate.includes(p));
|
|
2532
|
-
return sanitized.length > 0 ? sanitized : void 0;
|
|
2533
|
-
}
|
|
2534
|
-
// ============================================================================
|
|
2535
|
-
// Access Control Helpers
|
|
2536
|
-
// ============================================================================
|
|
2537
|
-
/** Check org scope for a document */
|
|
2538
|
-
_checkOrgScope(item, arcContext) {
|
|
2539
|
-
if (!item || !arcContext?.organizationId) return true;
|
|
2540
|
-
const itemOrgId = item.organizationId;
|
|
2541
|
-
if (!itemOrgId) return true;
|
|
2542
|
-
return String(itemOrgId) === String(arcContext.organizationId);
|
|
2543
|
-
}
|
|
2544
|
-
/** Check ownership for update/delete (ownedByUser preset) */
|
|
2545
|
-
_checkOwnership(item, req) {
|
|
2546
|
-
const ownershipCheck = req.metadata?._ownershipCheck;
|
|
2547
|
-
if (!item || !ownershipCheck) return true;
|
|
2548
|
-
const { field, userId } = ownershipCheck;
|
|
2549
|
-
const itemOwnerId = item[field];
|
|
2550
|
-
if (!itemOwnerId) return true;
|
|
2551
|
-
return String(itemOwnerId) === String(userId);
|
|
2552
|
-
}
|
|
2553
|
-
/**
|
|
2554
|
-
* Get hook system from context (instance-scoped) or fall back to global singleton
|
|
2555
|
-
* This allows proper isolation when running multiple app instances (e.g., in tests)
|
|
2556
|
-
*/
|
|
2557
|
-
_getHooks(req) {
|
|
2558
|
-
const arcMeta = req.metadata?.arc;
|
|
2559
|
-
return arcMeta?.hooks ?? hookSystem;
|
|
2560
|
-
}
|
|
2561
|
-
// ============================================================================
|
|
2562
|
-
// IController Implementation - CRUD Operations
|
|
2563
|
-
// ============================================================================
|
|
2564
|
-
/**
|
|
2565
|
-
* List resources with filtering, pagination, sorting
|
|
2566
|
-
* Implements IController.list()
|
|
2567
|
-
*/
|
|
2568
|
-
async list(req) {
|
|
2569
|
-
const options = this._parseQueryOptions(req);
|
|
2570
|
-
const filteredOptions = this._applyFilters(options, req);
|
|
2571
|
-
const result = await this.repository.getAll(filteredOptions);
|
|
2572
|
-
if (Array.isArray(result)) {
|
|
2573
|
-
return {
|
|
2574
|
-
success: true,
|
|
2575
|
-
data: {
|
|
2576
|
-
docs: result,
|
|
2577
|
-
page: 1,
|
|
2578
|
-
limit: result.length,
|
|
2579
|
-
total: result.length,
|
|
2580
|
-
pages: 1,
|
|
2581
|
-
hasNext: false,
|
|
2582
|
-
hasPrev: false
|
|
2583
|
-
},
|
|
2584
|
-
status: 200
|
|
2585
|
-
};
|
|
2586
|
-
}
|
|
2587
|
-
return {
|
|
2588
|
-
success: true,
|
|
2589
|
-
data: result,
|
|
2590
|
-
status: 200
|
|
2591
|
-
};
|
|
2592
|
-
}
|
|
2593
|
-
/**
|
|
2594
|
-
* Get single resource by ID
|
|
2595
|
-
* Implements IController.get()
|
|
2596
|
-
*/
|
|
2597
|
-
async get(req) {
|
|
2598
|
-
const id = req.params.id;
|
|
2599
|
-
if (!id) {
|
|
2600
|
-
return {
|
|
2601
|
-
success: false,
|
|
2602
|
-
error: "ID parameter is required",
|
|
2603
|
-
status: 400
|
|
2604
|
-
};
|
|
2605
|
-
}
|
|
2606
|
-
const options = this._parseQueryOptions(req);
|
|
2607
|
-
const arcContext = req.metadata;
|
|
2608
|
-
try {
|
|
2609
|
-
const item = await this.repository.getById(id, options);
|
|
2610
|
-
const hasItem = !!item;
|
|
2611
|
-
const orgScopeOk = this._checkOrgScope(item, arcContext);
|
|
2612
|
-
const policyFiltersOk = this._checkPolicyFilters(item, req);
|
|
2613
|
-
if (!hasItem || !orgScopeOk || !policyFiltersOk) {
|
|
2614
|
-
return {
|
|
2615
|
-
success: false,
|
|
2616
|
-
error: "Resource not found",
|
|
2617
|
-
status: 404
|
|
2618
|
-
};
|
|
2619
|
-
}
|
|
2620
|
-
return {
|
|
2621
|
-
success: true,
|
|
2622
|
-
data: item,
|
|
2623
|
-
status: 200
|
|
2624
|
-
};
|
|
2625
|
-
} catch (error) {
|
|
2626
|
-
if (error instanceof Error && error.message?.includes("not found")) {
|
|
2627
|
-
return {
|
|
2628
|
-
success: false,
|
|
2629
|
-
error: "Resource not found",
|
|
2630
|
-
status: 404
|
|
2631
|
-
};
|
|
2632
|
-
}
|
|
2633
|
-
throw error;
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
2636
|
-
/**
|
|
2637
|
-
* Create new resource
|
|
2638
|
-
* Implements IController.create()
|
|
2639
|
-
*/
|
|
2640
|
-
async create(req) {
|
|
2641
|
-
const data = { ...req.body };
|
|
2642
|
-
const arcContext = req.metadata;
|
|
2643
|
-
if (arcContext?.organizationId) {
|
|
2644
|
-
data.organizationId = arcContext.organizationId;
|
|
2645
|
-
}
|
|
2646
|
-
const userId = getUserId(req.user);
|
|
2647
|
-
if (userId) {
|
|
2648
|
-
data.createdBy = userId;
|
|
2649
|
-
}
|
|
2650
|
-
const hooks = this._getHooks(req);
|
|
2651
|
-
const user = req.user;
|
|
2652
|
-
let processedData = data;
|
|
2653
|
-
if (this.resourceName) {
|
|
2654
|
-
processedData = await hooks.executeBefore(this.resourceName, "create", data, {
|
|
2655
|
-
user,
|
|
2656
|
-
context: arcContext
|
|
2657
|
-
});
|
|
2658
|
-
}
|
|
2659
|
-
const item = await this.repository.create(processedData, {
|
|
2660
|
-
user,
|
|
2661
|
-
context: arcContext
|
|
2662
|
-
});
|
|
2663
|
-
if (this.resourceName) {
|
|
2664
|
-
await hooks.executeAfter(this.resourceName, "create", item, {
|
|
2665
|
-
user,
|
|
2666
|
-
context: arcContext
|
|
2667
|
-
});
|
|
2668
|
-
}
|
|
2669
|
-
return {
|
|
2670
|
-
success: true,
|
|
2671
|
-
data: item,
|
|
2672
|
-
status: 201,
|
|
2673
|
-
meta: { message: "Created successfully" }
|
|
2674
|
-
};
|
|
2675
|
-
}
|
|
2676
|
-
/**
|
|
2677
|
-
* Update existing resource
|
|
2678
|
-
* Implements IController.update()
|
|
2679
|
-
*/
|
|
2680
|
-
async update(req) {
|
|
2681
|
-
const id = req.params.id;
|
|
2682
|
-
if (!id) {
|
|
2683
|
-
return {
|
|
2684
|
-
success: false,
|
|
2685
|
-
error: "ID parameter is required",
|
|
2686
|
-
status: 400
|
|
2687
|
-
};
|
|
2688
|
-
}
|
|
2689
|
-
const data = { ...req.body };
|
|
2690
|
-
const arcContext = req.metadata;
|
|
2691
|
-
const user = req.user;
|
|
2692
|
-
const userId = getUserId(user);
|
|
2693
|
-
if (userId) {
|
|
2694
|
-
data.updatedBy = userId;
|
|
2695
|
-
}
|
|
2696
|
-
const existing = await this.repository.getById(id);
|
|
2697
|
-
if (!existing) {
|
|
2698
|
-
return {
|
|
2699
|
-
success: false,
|
|
2700
|
-
error: "Resource not found",
|
|
2701
|
-
status: 404
|
|
2702
|
-
};
|
|
2703
|
-
}
|
|
2704
|
-
if (!this._checkOrgScope(existing, arcContext) || !this._checkPolicyFilters(existing, req)) {
|
|
2705
|
-
return {
|
|
2706
|
-
success: false,
|
|
2707
|
-
error: "Resource not found",
|
|
2708
|
-
status: 404
|
|
2709
|
-
};
|
|
2710
|
-
}
|
|
2711
|
-
if (!this._checkOwnership(existing, req)) {
|
|
2712
|
-
return {
|
|
2713
|
-
success: false,
|
|
2714
|
-
error: "You do not have permission to modify this resource",
|
|
2715
|
-
details: { code: "OWNERSHIP_DENIED" },
|
|
2716
|
-
status: 403
|
|
2717
|
-
};
|
|
2718
|
-
}
|
|
2719
|
-
const hooks = this._getHooks(req);
|
|
2720
|
-
let processedData = data;
|
|
2721
|
-
if (this.resourceName) {
|
|
2722
|
-
processedData = await hooks.executeBefore(this.resourceName, "update", data, {
|
|
2723
|
-
user,
|
|
2724
|
-
context: arcContext,
|
|
2725
|
-
meta: { id, existing }
|
|
2726
|
-
});
|
|
2727
|
-
}
|
|
2728
|
-
const item = await this.repository.update(id, processedData, {
|
|
2729
|
-
user,
|
|
2730
|
-
context: arcContext
|
|
2731
|
-
});
|
|
2732
|
-
if (!item) {
|
|
2733
|
-
return {
|
|
2734
|
-
success: false,
|
|
2735
|
-
error: "Resource not found",
|
|
2736
|
-
status: 404
|
|
2737
|
-
};
|
|
2738
|
-
}
|
|
2739
|
-
if (this.resourceName) {
|
|
2740
|
-
await hooks.executeAfter(this.resourceName, "update", item, {
|
|
2741
|
-
user,
|
|
2742
|
-
context: arcContext,
|
|
2743
|
-
meta: { id, existing }
|
|
2744
|
-
});
|
|
2745
|
-
}
|
|
2746
|
-
return {
|
|
2747
|
-
success: true,
|
|
2748
|
-
data: item,
|
|
2749
|
-
status: 200,
|
|
2750
|
-
meta: { message: "Updated successfully" }
|
|
2751
|
-
};
|
|
2752
|
-
}
|
|
2753
|
-
/**
|
|
2754
|
-
* Delete resource
|
|
2755
|
-
* Implements IController.delete()
|
|
2756
|
-
*/
|
|
2757
|
-
async delete(req) {
|
|
2758
|
-
const id = req.params.id;
|
|
2759
|
-
if (!id) {
|
|
2760
|
-
return {
|
|
2761
|
-
success: false,
|
|
2762
|
-
error: "ID parameter is required",
|
|
2763
|
-
status: 400
|
|
2764
|
-
};
|
|
2765
|
-
}
|
|
2766
|
-
const arcContext = req.metadata;
|
|
2767
|
-
const user = req.user;
|
|
2768
|
-
const existing = await this.repository.getById(id);
|
|
2769
|
-
if (!existing) {
|
|
2770
|
-
return {
|
|
2771
|
-
success: false,
|
|
2772
|
-
error: "Resource not found",
|
|
2773
|
-
status: 404
|
|
2774
|
-
};
|
|
2775
|
-
}
|
|
2776
|
-
if (!this._checkOrgScope(existing, arcContext) || !this._checkPolicyFilters(existing, req)) {
|
|
2777
|
-
return {
|
|
2778
|
-
success: false,
|
|
2779
|
-
error: "Resource not found",
|
|
2780
|
-
status: 404
|
|
2781
|
-
};
|
|
2782
|
-
}
|
|
2783
|
-
if (!this._checkOwnership(existing, req)) {
|
|
2784
|
-
return {
|
|
2785
|
-
success: false,
|
|
2786
|
-
error: "You do not have permission to delete this resource",
|
|
2787
|
-
details: { code: "OWNERSHIP_DENIED" },
|
|
2788
|
-
status: 403
|
|
2789
|
-
};
|
|
2790
|
-
}
|
|
2791
|
-
const hooks = this._getHooks(req);
|
|
2792
|
-
if (this.resourceName) {
|
|
2793
|
-
await hooks.executeBefore(this.resourceName, "delete", existing, {
|
|
2794
|
-
user,
|
|
2795
|
-
context: arcContext,
|
|
2796
|
-
meta: { id }
|
|
2797
|
-
});
|
|
2798
|
-
}
|
|
2799
|
-
const result = await this.repository.delete(id, {
|
|
2800
|
-
user,
|
|
2801
|
-
context: arcContext
|
|
2802
|
-
});
|
|
2803
|
-
const deleteSuccess = typeof result === "object" ? result?.success : result;
|
|
2804
|
-
if (!deleteSuccess) {
|
|
2805
|
-
return {
|
|
2806
|
-
success: false,
|
|
2807
|
-
error: "Resource not found",
|
|
2808
|
-
status: 404
|
|
2809
|
-
};
|
|
2810
|
-
}
|
|
2811
|
-
if (this.resourceName) {
|
|
2812
|
-
await hooks.executeAfter(this.resourceName, "delete", existing, {
|
|
2813
|
-
user,
|
|
2814
|
-
context: arcContext,
|
|
2815
|
-
meta: { id }
|
|
2816
|
-
});
|
|
2817
|
-
}
|
|
2818
|
-
return {
|
|
2819
|
-
success: true,
|
|
2820
|
-
data: { message: "Deleted successfully" },
|
|
2821
|
-
status: 200
|
|
2822
|
-
};
|
|
2823
|
-
}
|
|
2824
|
-
// ============================================================================
|
|
2825
|
-
// Preset Methods (framework-agnostic versions)
|
|
2826
|
-
// ============================================================================
|
|
2827
|
-
/** Get resource by slug (slugLookup preset) */
|
|
2828
|
-
async getBySlug(req) {
|
|
2829
|
-
const repo = this.repository;
|
|
2830
|
-
if (!repo.getBySlug) {
|
|
2831
|
-
return {
|
|
2832
|
-
success: false,
|
|
2833
|
-
error: "Slug lookup not implemented",
|
|
2834
|
-
status: 501
|
|
2835
|
-
};
|
|
2836
|
-
}
|
|
2837
|
-
const slugField = this._presetFields.slugField ?? "slug";
|
|
2838
|
-
const slug = req.params[slugField] ?? req.params.slug;
|
|
2839
|
-
const options = this._parseQueryOptions(req);
|
|
2840
|
-
const arcContext = req.metadata;
|
|
2841
|
-
const item = await repo.getBySlug(slug, options);
|
|
2842
|
-
if (!item || !this._checkOrgScope(item, arcContext)) {
|
|
2843
|
-
return {
|
|
2844
|
-
success: false,
|
|
2845
|
-
error: "Resource not found",
|
|
2846
|
-
status: 404
|
|
2847
|
-
};
|
|
2848
|
-
}
|
|
2849
|
-
return {
|
|
2850
|
-
success: true,
|
|
2851
|
-
data: item,
|
|
2852
|
-
status: 200
|
|
2853
|
-
};
|
|
2854
|
-
}
|
|
2855
|
-
/** Get soft-deleted resources (softDelete preset) */
|
|
2856
|
-
async getDeleted(req) {
|
|
2857
|
-
const repo = this.repository;
|
|
2858
|
-
if (!repo.getDeleted) {
|
|
2859
|
-
return {
|
|
2860
|
-
success: false,
|
|
2861
|
-
error: "Soft delete not implemented",
|
|
2862
|
-
status: 501
|
|
2863
|
-
};
|
|
2864
|
-
}
|
|
2865
|
-
const options = this._parseQueryOptions(req);
|
|
2866
|
-
const filteredOptions = this._applyFilters(options, req);
|
|
2867
|
-
const result = await repo.getDeleted(filteredOptions);
|
|
2868
|
-
if (Array.isArray(result)) {
|
|
2869
|
-
return {
|
|
2870
|
-
success: true,
|
|
2871
|
-
data: {
|
|
2872
|
-
docs: result,
|
|
2873
|
-
page: 1,
|
|
2874
|
-
limit: result.length,
|
|
2875
|
-
total: result.length,
|
|
2876
|
-
pages: 1,
|
|
2877
|
-
hasNext: false,
|
|
2878
|
-
hasPrev: false
|
|
2879
|
-
},
|
|
2880
|
-
status: 200
|
|
2881
|
-
};
|
|
2882
|
-
}
|
|
2883
|
-
return {
|
|
2884
|
-
success: true,
|
|
2885
|
-
data: result,
|
|
2886
|
-
status: 200
|
|
2887
|
-
};
|
|
2888
|
-
}
|
|
2889
|
-
/** Restore soft-deleted resource (softDelete preset) */
|
|
2890
|
-
async restore(req) {
|
|
2891
|
-
const repo = this.repository;
|
|
2892
|
-
if (!repo.restore) {
|
|
2893
|
-
return {
|
|
2894
|
-
success: false,
|
|
2895
|
-
error: "Restore not implemented",
|
|
2896
|
-
status: 501
|
|
2897
|
-
};
|
|
2898
|
-
}
|
|
2899
|
-
const id = req.params.id;
|
|
2900
|
-
if (!id) {
|
|
2901
|
-
return {
|
|
2902
|
-
success: false,
|
|
2903
|
-
error: "ID parameter is required",
|
|
2904
|
-
status: 400
|
|
2905
|
-
};
|
|
2906
|
-
}
|
|
2907
|
-
const item = await repo.restore(id);
|
|
2908
|
-
if (!item) {
|
|
2909
|
-
return {
|
|
2910
|
-
success: false,
|
|
2911
|
-
error: "Resource not found",
|
|
2912
|
-
status: 404
|
|
2913
|
-
};
|
|
2914
|
-
}
|
|
2915
|
-
return {
|
|
2916
|
-
success: true,
|
|
2917
|
-
data: item,
|
|
2918
|
-
status: 200,
|
|
2919
|
-
meta: { message: "Restored successfully" }
|
|
2920
|
-
};
|
|
2921
|
-
}
|
|
2922
|
-
/** Get hierarchical tree (tree preset) */
|
|
2923
|
-
async getTree(req) {
|
|
2924
|
-
const repo = this.repository;
|
|
2925
|
-
if (!repo.getTree) {
|
|
2926
|
-
return {
|
|
2927
|
-
success: false,
|
|
2928
|
-
error: "Tree structure not implemented",
|
|
2929
|
-
status: 501
|
|
2930
|
-
};
|
|
2931
|
-
}
|
|
2932
|
-
const options = this._parseQueryOptions(req);
|
|
2933
|
-
const filteredOptions = this._applyFilters(options, req);
|
|
2934
|
-
const tree = await repo.getTree(filteredOptions);
|
|
2935
|
-
return {
|
|
2936
|
-
success: true,
|
|
2937
|
-
data: tree,
|
|
2938
|
-
status: 200
|
|
2939
|
-
};
|
|
2940
|
-
}
|
|
2941
|
-
/** Get children of parent (tree preset) */
|
|
2942
|
-
async getChildren(req) {
|
|
2943
|
-
const repo = this.repository;
|
|
2944
|
-
if (!repo.getChildren) {
|
|
2945
|
-
return {
|
|
2946
|
-
success: false,
|
|
2947
|
-
error: "Tree structure not implemented",
|
|
2948
|
-
status: 501
|
|
2949
|
-
};
|
|
2950
|
-
}
|
|
2951
|
-
const parentField = this._presetFields.parentField ?? "parent";
|
|
2952
|
-
const parentId = req.params[parentField] ?? req.params.parent ?? req.params.id;
|
|
2953
|
-
const options = this._parseQueryOptions(req);
|
|
2954
|
-
const filteredOptions = this._applyFilters(options, req);
|
|
2955
|
-
const children = await repo.getChildren(parentId, filteredOptions);
|
|
2956
|
-
return {
|
|
2957
|
-
success: true,
|
|
2958
|
-
data: children,
|
|
2959
|
-
status: 200
|
|
2960
|
-
};
|
|
2961
|
-
}
|
|
2962
|
-
};
|
|
2963
|
-
|
|
2964
|
-
// src/core/fastifyAdapter.ts
|
|
2965
|
-
function applyFieldMaskToObject(obj, fieldMask) {
|
|
2966
|
-
if (!obj || typeof obj !== "object") return obj;
|
|
2967
|
-
const { include, exclude } = fieldMask;
|
|
2968
|
-
if (include && include.length > 0) {
|
|
2969
|
-
const filtered = {};
|
|
2970
|
-
for (const field of include) {
|
|
2971
|
-
if (field in obj) {
|
|
2972
|
-
filtered[field] = obj[field];
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
2975
|
-
return filtered;
|
|
2976
|
-
}
|
|
2977
|
-
if (exclude && exclude.length > 0) {
|
|
2978
|
-
const filtered = { ...obj };
|
|
2979
|
-
for (const field of exclude) {
|
|
2980
|
-
delete filtered[field];
|
|
2981
|
-
}
|
|
2982
|
-
return filtered;
|
|
2983
|
-
}
|
|
2984
|
-
return obj;
|
|
2985
|
-
}
|
|
2986
|
-
function applyFieldMask(data, fieldMask) {
|
|
2987
|
-
if (!fieldMask) return data;
|
|
2988
|
-
if (Array.isArray(data)) {
|
|
2989
|
-
return data.map((item) => applyFieldMaskToObject(item, fieldMask));
|
|
2990
|
-
}
|
|
2991
|
-
if (data && typeof data === "object") {
|
|
2992
|
-
return applyFieldMaskToObject(data, fieldMask);
|
|
2993
|
-
}
|
|
2994
|
-
return data;
|
|
2995
|
-
}
|
|
2996
|
-
function createRequestContext(req) {
|
|
2997
|
-
const reqWithExtras = req;
|
|
2998
|
-
return {
|
|
2999
|
-
query: reqWithExtras.query ?? {},
|
|
3000
|
-
body: reqWithExtras.body ?? {},
|
|
3001
|
-
params: reqWithExtras.params ?? {},
|
|
3002
|
-
headers: reqWithExtras.headers,
|
|
3003
|
-
user: reqWithExtras.user ? (() => {
|
|
3004
|
-
const user = reqWithExtras.user;
|
|
3005
|
-
return {
|
|
3006
|
-
...user,
|
|
3007
|
-
// Normalize ID for MongoDB compatibility
|
|
3008
|
-
id: String(user._id ?? user.id),
|
|
3009
|
-
_id: user._id ?? user.id
|
|
3010
|
-
// Preserve original role/roles/permissions as-is
|
|
3011
|
-
// Devs can define their own authorization structure
|
|
3012
|
-
};
|
|
3013
|
-
})() : void 0,
|
|
3014
|
-
organizationId: reqWithExtras.organizationId,
|
|
3015
|
-
metadata: {
|
|
3016
|
-
...reqWithExtras.context,
|
|
3017
|
-
// Include Arc metadata for hook execution
|
|
3018
|
-
arc: reqWithExtras.arc,
|
|
3019
|
-
// Include ownership check for access control
|
|
3020
|
-
_ownershipCheck: reqWithExtras._ownershipCheck,
|
|
3021
|
-
// Merge policy filters - TRUSTED sources override user input
|
|
3022
|
-
// Order matters: query (can be user-injected) FIRST, then trusted middleware LAST
|
|
3023
|
-
_policyFilters: {
|
|
3024
|
-
...reqWithExtras.query?._policyFilters ?? {},
|
|
3025
|
-
...reqWithExtras._policyFilters ?? {}
|
|
3026
|
-
},
|
|
3027
|
-
// Include logger for logging
|
|
3028
|
-
log: reqWithExtras.log
|
|
3029
|
-
}
|
|
3030
|
-
};
|
|
3031
|
-
}
|
|
3032
|
-
function sendControllerResponse(reply, response, request) {
|
|
3033
|
-
const reqWithExtras = request;
|
|
3034
|
-
const fieldMask = reqWithExtras?.fieldMask;
|
|
3035
|
-
const fieldMaskConfig = fieldMask ? { include: fieldMask } : void 0;
|
|
3036
|
-
if (response.success && response.data && typeof response.data === "object" && "docs" in response.data) {
|
|
3037
|
-
const paginatedData = response.data;
|
|
3038
|
-
const filteredDocs = fieldMaskConfig ? applyFieldMask(paginatedData.docs, fieldMaskConfig) : paginatedData.docs;
|
|
3039
|
-
reply.code(response.status ?? 200).send({
|
|
3040
|
-
success: true,
|
|
3041
|
-
docs: filteredDocs,
|
|
3042
|
-
page: paginatedData.page,
|
|
3043
|
-
limit: paginatedData.limit,
|
|
3044
|
-
total: paginatedData.total,
|
|
3045
|
-
pages: paginatedData.pages,
|
|
3046
|
-
hasNext: paginatedData.hasNext,
|
|
3047
|
-
hasPrev: paginatedData.hasPrev,
|
|
3048
|
-
...response.meta ?? {}
|
|
3049
|
-
});
|
|
3050
|
-
return;
|
|
3051
|
-
}
|
|
3052
|
-
const filteredData = fieldMaskConfig ? applyFieldMask(response.data, fieldMaskConfig) : response.data;
|
|
3053
|
-
reply.code(response.status ?? (response.success ? 200 : 400)).send({
|
|
3054
|
-
success: response.success,
|
|
3055
|
-
data: filteredData,
|
|
3056
|
-
error: response.error,
|
|
3057
|
-
details: response.details,
|
|
3058
|
-
...response.meta ?? {}
|
|
3059
|
-
});
|
|
3060
|
-
}
|
|
3061
|
-
function createFastifyHandler(controllerMethod) {
|
|
3062
|
-
return async (req, reply) => {
|
|
3063
|
-
const requestContext = createRequestContext(req);
|
|
3064
|
-
const response = await controllerMethod(requestContext);
|
|
3065
|
-
sendControllerResponse(reply, response, req);
|
|
3066
|
-
};
|
|
3067
|
-
}
|
|
3068
|
-
function createCrudHandlers(controller) {
|
|
3069
|
-
return {
|
|
3070
|
-
list: createFastifyHandler(controller.list.bind(controller)),
|
|
3071
|
-
get: createFastifyHandler(controller.get.bind(controller)),
|
|
3072
|
-
create: createFastifyHandler(controller.create.bind(controller)),
|
|
3073
|
-
update: createFastifyHandler(controller.update.bind(controller)),
|
|
3074
|
-
delete: createFastifyHandler(controller.delete.bind(controller))
|
|
3075
|
-
};
|
|
3076
|
-
}
|
|
3077
|
-
|
|
3078
|
-
// src/core/createCrudRouter.ts
|
|
3079
|
-
function requiresAuthentication(permission) {
|
|
3080
|
-
if (!permission) return false;
|
|
3081
|
-
return !permission._isPublic;
|
|
3082
|
-
}
|
|
3083
|
-
function buildAuthMiddleware(fastify, permission) {
|
|
3084
|
-
if (!requiresAuthentication(permission)) return null;
|
|
3085
|
-
if (!fastify.authenticate) return null;
|
|
3086
|
-
return fastify.authenticate;
|
|
3087
|
-
}
|
|
3088
|
-
function buildPermissionMiddleware(permissionCheck, resourceName, action) {
|
|
3089
|
-
if (!permissionCheck) return null;
|
|
3090
|
-
return async (request, reply) => {
|
|
3091
|
-
const reqWithExtras = request;
|
|
3092
|
-
const context2 = {
|
|
3093
|
-
user: reqWithExtras.user ?? null,
|
|
3094
|
-
request,
|
|
3095
|
-
resource: resourceName,
|
|
3096
|
-
action,
|
|
3097
|
-
resourceId: request.params?.id,
|
|
3098
|
-
organizationId: reqWithExtras.organizationId,
|
|
3099
|
-
data: request.body
|
|
3100
|
-
};
|
|
3101
|
-
const result = await permissionCheck(context2);
|
|
3102
|
-
if (typeof result === "boolean") {
|
|
3103
|
-
if (!result) {
|
|
3104
|
-
reply.code(context2.user ? 403 : 401).send({
|
|
3105
|
-
success: false,
|
|
3106
|
-
error: context2.user ? "Permission denied" : "Authentication required"
|
|
3107
|
-
});
|
|
3108
|
-
return;
|
|
3109
|
-
}
|
|
3110
|
-
return;
|
|
3111
|
-
}
|
|
3112
|
-
const permResult = result;
|
|
3113
|
-
if (!permResult.granted) {
|
|
3114
|
-
reply.code(context2.user ? 403 : 401).send({
|
|
3115
|
-
success: false,
|
|
3116
|
-
error: permResult.reason ?? (context2.user ? "Permission denied" : "Authentication required")
|
|
3117
|
-
});
|
|
3118
|
-
return;
|
|
3119
|
-
}
|
|
3120
|
-
if (permResult.filters) {
|
|
3121
|
-
reqWithExtras._policyFilters = {
|
|
3122
|
-
...reqWithExtras._policyFilters ?? {},
|
|
3123
|
-
...permResult.filters
|
|
3124
|
-
};
|
|
3125
|
-
}
|
|
3126
|
-
};
|
|
3127
|
-
}
|
|
3128
|
-
function buildOrgScopedMiddleware(fastify, orgScoped, globalOrgScoped) {
|
|
3129
|
-
const shouldApplyOrgScoped = orgScoped ?? globalOrgScoped;
|
|
3130
|
-
if (!shouldApplyOrgScoped) return [];
|
|
3131
|
-
if (!fastify.organizationScoped) {
|
|
3132
|
-
throw new Error(
|
|
3133
|
-
"Organization scoping is enabled but fastify.organizationScoped decorator is not registered.\nRegister the org scope plugin before mounting resources:\nawait app.register(orgScopePlugin);\nDocs: https://github.com/classytic/arc#multi-tenant"
|
|
3134
|
-
);
|
|
3135
|
-
}
|
|
3136
|
-
return [fastify.organizationScoped()];
|
|
3137
|
-
}
|
|
3138
|
-
function createAdditionalRoutes(fastify, routes, controller, options) {
|
|
3139
|
-
const { tag, resourceName, orgMw, arcDecorator } = options;
|
|
3140
|
-
for (const route of routes) {
|
|
3141
|
-
let handler;
|
|
3142
|
-
if (typeof route.handler === "string") {
|
|
3143
|
-
if (!controller) {
|
|
3144
|
-
throw new Error(
|
|
3145
|
-
`Route ${route.method} ${route.path}: string handler '${route.handler}' requires a controller. Either provide a controller or use a function handler instead.`
|
|
3146
|
-
);
|
|
3147
|
-
}
|
|
3148
|
-
const ctrl = controller;
|
|
3149
|
-
const method = ctrl[route.handler];
|
|
3150
|
-
if (typeof method !== "function") {
|
|
3151
|
-
throw new Error(`Handler '${route.handler}' not found on controller`);
|
|
3152
|
-
}
|
|
3153
|
-
const boundMethod = method.bind(controller);
|
|
3154
|
-
handler = route.wrapHandler ? createFastifyHandler(boundMethod) : boundMethod;
|
|
3155
|
-
} else {
|
|
3156
|
-
handler = route.wrapHandler ? createFastifyHandler(route.handler) : route.handler;
|
|
3157
|
-
}
|
|
3158
|
-
const routeTags = route.tags ?? (tag ? [tag] : void 0);
|
|
3159
|
-
const schema = {
|
|
3160
|
-
...routeTags ? { tags: routeTags } : {},
|
|
3161
|
-
...route.summary ? { summary: route.summary } : {},
|
|
3162
|
-
...route.description ? { description: route.description } : {},
|
|
3163
|
-
...route.schema ?? {}
|
|
3164
|
-
};
|
|
3165
|
-
const authMw = buildAuthMiddleware(fastify, route.permissions);
|
|
3166
|
-
const permissionMw = buildPermissionMiddleware(route.permissions, resourceName, route.method.toLowerCase());
|
|
3167
|
-
const customPreHandlers = typeof route.preHandler === "function" ? route.preHandler(fastify) : route.preHandler ?? [];
|
|
3168
|
-
const preHandler = [
|
|
3169
|
-
arcDecorator,
|
|
3170
|
-
authMw,
|
|
3171
|
-
// Authenticate first (populates request.user)
|
|
3172
|
-
permissionMw,
|
|
3173
|
-
// Then check permissions
|
|
3174
|
-
...orgMw(),
|
|
3175
|
-
...customPreHandlers
|
|
3176
|
-
].filter(Boolean);
|
|
3177
|
-
fastify.route({
|
|
3178
|
-
method: route.method,
|
|
3179
|
-
url: route.path,
|
|
3180
|
-
schema,
|
|
3181
|
-
// Fastify schema is flexible - allow any valid JSON schema
|
|
3182
|
-
preHandler: preHandler.length > 0 ? preHandler : void 0,
|
|
3183
|
-
handler
|
|
3184
|
-
});
|
|
3185
|
-
}
|
|
3186
|
-
}
|
|
3187
|
-
function createCrudRouter(fastify, controller, options = {}) {
|
|
3188
|
-
const {
|
|
3189
|
-
tag = "Resource",
|
|
3190
|
-
schemas = {},
|
|
3191
|
-
permissions = {},
|
|
3192
|
-
middlewares = {},
|
|
3193
|
-
additionalRoutes = [],
|
|
3194
|
-
disableDefaultRoutes = false,
|
|
3195
|
-
disabledRoutes = [],
|
|
3196
|
-
organizationScoped = false,
|
|
3197
|
-
resourceName = "unknown",
|
|
3198
|
-
schemaOptions
|
|
3199
|
-
} = options;
|
|
3200
|
-
const orgMw = (orgScoped) => {
|
|
3201
|
-
return buildOrgScopedMiddleware(fastify, orgScoped, organizationScoped);
|
|
3202
|
-
};
|
|
3203
|
-
const arcDecorator = async (req, _reply) => {
|
|
3204
|
-
req.arc = {
|
|
3205
|
-
resourceName,
|
|
3206
|
-
schemaOptions,
|
|
3207
|
-
permissions,
|
|
3208
|
-
// Include instance-scoped hooks if available (for proper isolation)
|
|
3209
|
-
hooks: fastify.arc?.hooks,
|
|
3210
|
-
// Include events emitter if available
|
|
3211
|
-
events: fastify.events
|
|
3212
|
-
};
|
|
3213
|
-
};
|
|
3214
|
-
const mw = {
|
|
3215
|
-
list: middlewares.list ?? [],
|
|
3216
|
-
get: middlewares.get ?? [],
|
|
3217
|
-
create: middlewares.create ?? [],
|
|
3218
|
-
update: middlewares.update ?? [],
|
|
3219
|
-
delete: middlewares.delete ?? []
|
|
3220
|
-
};
|
|
3221
|
-
const idParamsSchema = {
|
|
3222
|
-
type: "object",
|
|
3223
|
-
properties: { id: { type: "string" } },
|
|
3224
|
-
required: ["id"]
|
|
3225
|
-
};
|
|
3226
|
-
let handlers;
|
|
3227
|
-
if (!disableDefaultRoutes) {
|
|
3228
|
-
if (!controller) {
|
|
3229
|
-
throw new Error(
|
|
3230
|
-
"Controller is required when disableDefaultRoutes is not true. Provide a controller or use defineResource which auto-creates BaseController."
|
|
3231
|
-
);
|
|
3232
|
-
}
|
|
3233
|
-
handlers = createCrudHandlers(controller);
|
|
3234
|
-
}
|
|
3235
|
-
if (!disableDefaultRoutes && handlers) {
|
|
3236
|
-
if (!disabledRoutes.includes("list")) {
|
|
3237
|
-
const authMw = buildAuthMiddleware(fastify, permissions.list);
|
|
3238
|
-
const permMw = buildPermissionMiddleware(permissions.list, resourceName, "list");
|
|
3239
|
-
const listPreHandler = [arcDecorator, authMw, permMw, ...orgMw(), ...mw.list].filter(Boolean);
|
|
3240
|
-
fastify.route({
|
|
3241
|
-
method: "GET",
|
|
3242
|
-
url: "/",
|
|
3243
|
-
schema: {
|
|
3244
|
-
tags: [tag],
|
|
3245
|
-
summary: `List ${tag}`,
|
|
3246
|
-
...schemas.list ?? {}
|
|
3247
|
-
},
|
|
3248
|
-
preHandler: listPreHandler.length > 0 ? listPreHandler : void 0,
|
|
3249
|
-
handler: handlers.list
|
|
3250
|
-
});
|
|
3251
|
-
}
|
|
3252
|
-
if (!disabledRoutes.includes("get")) {
|
|
3253
|
-
const authMw = buildAuthMiddleware(fastify, permissions.get);
|
|
3254
|
-
const permMw = buildPermissionMiddleware(permissions.get, resourceName, "get");
|
|
3255
|
-
const getPreHandler = [arcDecorator, authMw, permMw, ...orgMw(), ...mw.get].filter(Boolean);
|
|
3256
|
-
fastify.route({
|
|
3257
|
-
method: "GET",
|
|
3258
|
-
url: "/:id",
|
|
3259
|
-
schema: {
|
|
3260
|
-
tags: [tag],
|
|
3261
|
-
summary: `Get ${tag} by ID`,
|
|
3262
|
-
params: idParamsSchema,
|
|
3263
|
-
...schemas.get ?? {}
|
|
3264
|
-
},
|
|
3265
|
-
preHandler: getPreHandler.length > 0 ? getPreHandler : void 0,
|
|
3266
|
-
handler: handlers.get
|
|
3267
|
-
});
|
|
3268
|
-
}
|
|
3269
|
-
if (!disabledRoutes.includes("create")) {
|
|
3270
|
-
const authMw = buildAuthMiddleware(fastify, permissions.create);
|
|
3271
|
-
const permMw = buildPermissionMiddleware(permissions.create, resourceName, "create");
|
|
3272
|
-
const createPreHandler = [arcDecorator, authMw, permMw, ...orgMw(), ...mw.create].filter(Boolean);
|
|
3273
|
-
fastify.route({
|
|
3274
|
-
method: "POST",
|
|
3275
|
-
url: "/",
|
|
3276
|
-
schema: {
|
|
3277
|
-
tags: [tag],
|
|
3278
|
-
summary: `Create ${tag}`,
|
|
3279
|
-
...schemas.create ?? {}
|
|
3280
|
-
},
|
|
3281
|
-
preHandler: createPreHandler.length > 0 ? createPreHandler : void 0,
|
|
3282
|
-
handler: handlers.create
|
|
3283
|
-
});
|
|
3284
|
-
}
|
|
3285
|
-
if (!disabledRoutes.includes("update")) {
|
|
3286
|
-
const authMw = buildAuthMiddleware(fastify, permissions.update);
|
|
3287
|
-
const permMw = buildPermissionMiddleware(permissions.update, resourceName, "update");
|
|
3288
|
-
const updatePreHandler = [arcDecorator, authMw, permMw, ...orgMw(), ...mw.update].filter(Boolean);
|
|
3289
|
-
fastify.route({
|
|
3290
|
-
method: "PATCH",
|
|
3291
|
-
url: "/:id",
|
|
3292
|
-
schema: {
|
|
3293
|
-
tags: [tag],
|
|
3294
|
-
summary: `Update ${tag}`,
|
|
3295
|
-
params: idParamsSchema,
|
|
3296
|
-
...schemas.update ?? {}
|
|
3297
|
-
},
|
|
3298
|
-
preHandler: updatePreHandler.length > 0 ? updatePreHandler : void 0,
|
|
3299
|
-
handler: handlers.update
|
|
3300
|
-
});
|
|
3301
|
-
}
|
|
3302
|
-
if (!disabledRoutes.includes("delete")) {
|
|
3303
|
-
const authMw = buildAuthMiddleware(fastify, permissions.delete);
|
|
3304
|
-
const permMw = buildPermissionMiddleware(permissions.delete, resourceName, "delete");
|
|
3305
|
-
const deletePreHandler = [arcDecorator, authMw, permMw, ...orgMw(), ...mw.delete].filter(Boolean);
|
|
3306
|
-
fastify.route({
|
|
3307
|
-
method: "DELETE",
|
|
3308
|
-
url: "/:id",
|
|
3309
|
-
schema: {
|
|
3310
|
-
tags: [tag],
|
|
3311
|
-
summary: `Delete ${tag}`,
|
|
3312
|
-
params: idParamsSchema,
|
|
3313
|
-
...schemas.delete ?? {}
|
|
3314
|
-
},
|
|
3315
|
-
preHandler: deletePreHandler.length > 0 ? deletePreHandler : void 0,
|
|
3316
|
-
handler: handlers.delete
|
|
3317
|
-
});
|
|
3318
|
-
}
|
|
3319
|
-
}
|
|
3320
|
-
if (additionalRoutes.length > 0) {
|
|
3321
|
-
createAdditionalRoutes(fastify, additionalRoutes, controller, { tag, resourceName, orgMw, arcDecorator });
|
|
3322
|
-
}
|
|
3323
|
-
}
|
|
3324
|
-
|
|
3325
|
-
// src/permissions/index.ts
|
|
3326
|
-
function allowPublic() {
|
|
3327
|
-
const check = () => true;
|
|
3328
|
-
check._isPublic = true;
|
|
3329
|
-
return check;
|
|
3330
|
-
}
|
|
3331
|
-
function requireAuth() {
|
|
3332
|
-
const check = (ctx) => {
|
|
3333
|
-
if (!ctx.user) {
|
|
3334
|
-
return { granted: false, reason: "Authentication required" };
|
|
3335
|
-
}
|
|
3336
|
-
return true;
|
|
3337
|
-
};
|
|
3338
|
-
return check;
|
|
3339
|
-
}
|
|
3340
|
-
function requireRoles(roles, options) {
|
|
3341
|
-
const check = (ctx) => {
|
|
3342
|
-
if (!ctx.user) {
|
|
3343
|
-
return { granted: false, reason: "Authentication required" };
|
|
3344
|
-
}
|
|
3345
|
-
const userRoles = ctx.user.roles ?? [];
|
|
3346
|
-
if (options?.bypassRoles?.some((r) => userRoles.includes(r))) {
|
|
3347
|
-
return true;
|
|
3348
|
-
}
|
|
3349
|
-
if (roles.some((r) => userRoles.includes(r))) {
|
|
3350
|
-
return true;
|
|
3351
|
-
}
|
|
3352
|
-
return {
|
|
3353
|
-
granted: false,
|
|
3354
|
-
reason: `Required roles: ${roles.join(", ")}`
|
|
3355
|
-
};
|
|
3356
|
-
};
|
|
3357
|
-
check._roles = roles;
|
|
3358
|
-
return check;
|
|
3359
|
-
}
|
|
3360
|
-
function requireOwnership(ownerField = "userId", options) {
|
|
3361
|
-
return (ctx) => {
|
|
3362
|
-
if (!ctx.user) {
|
|
3363
|
-
return { granted: false, reason: "Authentication required" };
|
|
3364
|
-
}
|
|
3365
|
-
const userRoles = ctx.user.roles ?? [];
|
|
3366
|
-
if (options?.bypassRoles?.some((r) => userRoles.includes(r))) {
|
|
3367
|
-
return true;
|
|
3368
|
-
}
|
|
3369
|
-
const userId = ctx.user.id ?? ctx.user._id;
|
|
3370
|
-
return {
|
|
3371
|
-
granted: true,
|
|
3372
|
-
filters: { [ownerField]: userId }
|
|
3373
|
-
};
|
|
3374
|
-
};
|
|
3375
|
-
}
|
|
3376
|
-
function allOf(...checks) {
|
|
3377
|
-
return async (ctx) => {
|
|
3378
|
-
let mergedFilters = {};
|
|
3379
|
-
for (const check of checks) {
|
|
3380
|
-
const result = await check(ctx);
|
|
3381
|
-
const normalized = typeof result === "boolean" ? { granted: result } : result;
|
|
3382
|
-
if (!normalized.granted) {
|
|
3383
|
-
return normalized;
|
|
3384
|
-
}
|
|
3385
|
-
if (normalized.filters) {
|
|
3386
|
-
mergedFilters = { ...mergedFilters, ...normalized.filters };
|
|
3387
|
-
}
|
|
3388
|
-
}
|
|
3389
|
-
return {
|
|
3390
|
-
granted: true,
|
|
3391
|
-
filters: Object.keys(mergedFilters).length > 0 ? mergedFilters : void 0
|
|
3392
|
-
};
|
|
3393
|
-
};
|
|
3394
|
-
}
|
|
3395
|
-
function anyOf(...checks) {
|
|
3396
|
-
return async (ctx) => {
|
|
3397
|
-
const reasons = [];
|
|
3398
|
-
for (const check of checks) {
|
|
3399
|
-
const result = await check(ctx);
|
|
3400
|
-
const normalized = typeof result === "boolean" ? { granted: result } : result;
|
|
3401
|
-
if (normalized.granted) {
|
|
3402
|
-
return normalized;
|
|
3403
|
-
}
|
|
3404
|
-
if (normalized.reason) {
|
|
3405
|
-
reasons.push(normalized.reason);
|
|
3406
|
-
}
|
|
3407
|
-
}
|
|
3408
|
-
return {
|
|
3409
|
-
granted: false,
|
|
3410
|
-
reason: reasons.join("; ")
|
|
3411
|
-
};
|
|
3412
|
-
};
|
|
3413
|
-
}
|
|
3414
|
-
function denyAll(reason = "Access denied") {
|
|
3415
|
-
return () => ({ granted: false, reason });
|
|
3416
|
-
}
|
|
3417
|
-
function when(condition) {
|
|
3418
|
-
return async (ctx) => {
|
|
3419
|
-
const result = await condition(ctx);
|
|
3420
|
-
return {
|
|
3421
|
-
granted: result,
|
|
3422
|
-
reason: result ? void 0 : "Condition not met"
|
|
3423
|
-
};
|
|
3424
|
-
};
|
|
3425
|
-
}
|
|
3426
|
-
|
|
3427
|
-
// src/presets/softDelete.ts
|
|
3428
|
-
function softDeletePreset(options = {}) {
|
|
3429
|
-
const { deletedField: _deletedField = "deletedAt" } = options;
|
|
3430
|
-
return {
|
|
3431
|
-
name: "softDelete",
|
|
3432
|
-
additionalRoutes: (permissions) => [
|
|
3433
|
-
{
|
|
3434
|
-
method: "GET",
|
|
3435
|
-
path: "/deleted",
|
|
3436
|
-
handler: "getDeleted",
|
|
3437
|
-
summary: "Get soft-deleted items",
|
|
3438
|
-
permissions: permissions.list ?? requireRoles(["admin"]),
|
|
3439
|
-
wrapHandler: true
|
|
3440
|
-
},
|
|
3441
|
-
{
|
|
3442
|
-
method: "POST",
|
|
3443
|
-
path: "/:id/restore",
|
|
3444
|
-
handler: "restore",
|
|
3445
|
-
summary: "Restore soft-deleted item",
|
|
3446
|
-
permissions: permissions.update ?? requireRoles(["admin"]),
|
|
3447
|
-
wrapHandler: true
|
|
3448
|
-
}
|
|
3449
|
-
]
|
|
3450
|
-
};
|
|
3451
|
-
}
|
|
3452
|
-
|
|
3453
|
-
// src/presets/slugLookup.ts
|
|
3454
|
-
function slugLookupPreset(options = {}) {
|
|
3455
|
-
const { slugField = "slug" } = options;
|
|
3456
|
-
return {
|
|
3457
|
-
name: "slugLookup",
|
|
3458
|
-
additionalRoutes: (permissions) => [
|
|
3459
|
-
{
|
|
3460
|
-
method: "GET",
|
|
3461
|
-
path: `/slug/:${slugField}`,
|
|
3462
|
-
handler: "getBySlug",
|
|
3463
|
-
summary: "Get by slug",
|
|
3464
|
-
permissions: permissions.get ?? allowPublic(),
|
|
3465
|
-
wrapHandler: true
|
|
3466
|
-
// Handler is a ControllerHandler
|
|
3467
|
-
}
|
|
3468
|
-
],
|
|
3469
|
-
// Pass to controller so it knows which param to read
|
|
3470
|
-
controllerOptions: {
|
|
3471
|
-
slugField
|
|
3472
|
-
}
|
|
3473
|
-
};
|
|
3474
|
-
}
|
|
3475
|
-
|
|
3476
|
-
// src/presets/ownedByUser.ts
|
|
3477
|
-
function createOwnershipCheck(ownerField, bypassRoles) {
|
|
3478
|
-
return async (request, _reply) => {
|
|
3479
|
-
const user = request.user;
|
|
3480
|
-
if (!user) return;
|
|
3481
|
-
const userWithRoles = user;
|
|
3482
|
-
if (userWithRoles.roles && bypassRoles.some((r) => userWithRoles.roles.includes(r))) return;
|
|
3483
|
-
const userWithId = user;
|
|
3484
|
-
const userId = userWithId._id ?? userWithId.id;
|
|
3485
|
-
if (userId) {
|
|
3486
|
-
request._ownershipCheck = {
|
|
3487
|
-
field: ownerField,
|
|
3488
|
-
userId
|
|
3489
|
-
};
|
|
3490
|
-
}
|
|
3491
|
-
};
|
|
3492
|
-
}
|
|
3493
|
-
function ownedByUserPreset(options = {}) {
|
|
3494
|
-
const {
|
|
3495
|
-
ownerField = "userId",
|
|
3496
|
-
bypassRoles = ["admin", "superadmin"]
|
|
3497
|
-
} = options;
|
|
3498
|
-
const ownershipMiddleware = createOwnershipCheck(ownerField, bypassRoles);
|
|
3499
|
-
return {
|
|
3500
|
-
name: "ownedByUser",
|
|
3501
|
-
middlewares: {
|
|
3502
|
-
update: [ownershipMiddleware],
|
|
3503
|
-
delete: [ownershipMiddleware]
|
|
3504
|
-
}
|
|
3505
|
-
};
|
|
3506
|
-
}
|
|
3507
|
-
|
|
3508
|
-
// src/presets/multiTenant.ts
|
|
3509
|
-
function defaultExtractOrganizationId(request) {
|
|
3510
|
-
const context2 = request.context;
|
|
3511
|
-
if (context2?.organizationId) {
|
|
3512
|
-
return context2.organizationId;
|
|
3513
|
-
}
|
|
3514
|
-
const user = request.user;
|
|
3515
|
-
if (user?.organizationId) {
|
|
3516
|
-
return user.organizationId;
|
|
3517
|
-
}
|
|
3518
|
-
if (user?.organization) {
|
|
3519
|
-
const org = user.organization;
|
|
3520
|
-
return org._id || org.id || org;
|
|
3521
|
-
}
|
|
3522
|
-
return null;
|
|
3523
|
-
}
|
|
3524
|
-
function createTenantFilter(tenantField, bypassRoles, extractOrganizationId) {
|
|
3525
|
-
return async (request, reply) => {
|
|
3526
|
-
const user = request.user;
|
|
3527
|
-
if (!user) {
|
|
3528
|
-
reply.code(401).send({
|
|
3529
|
-
success: false,
|
|
3530
|
-
error: "Unauthorized",
|
|
3531
|
-
message: "Authentication required for multi-tenant resources"
|
|
3532
|
-
});
|
|
3533
|
-
return;
|
|
3534
|
-
}
|
|
3535
|
-
const userWithRoles = user;
|
|
3536
|
-
if (userWithRoles.roles && bypassRoles.some((r) => userWithRoles.roles.includes(r))) return;
|
|
3537
|
-
const orgId = extractOrganizationId(request);
|
|
3538
|
-
if (!orgId) {
|
|
3539
|
-
reply.code(403).send({
|
|
3540
|
-
success: false,
|
|
3541
|
-
error: "Forbidden",
|
|
3542
|
-
message: "Organization context required for this operation"
|
|
3543
|
-
});
|
|
3544
|
-
return;
|
|
3545
|
-
}
|
|
3546
|
-
request.query = request.query ?? {};
|
|
3547
|
-
request.query._policyFilters = {
|
|
3548
|
-
...request.query._policyFilters ?? {},
|
|
3549
|
-
[tenantField]: orgId
|
|
3550
|
-
};
|
|
3551
|
-
};
|
|
3552
|
-
}
|
|
3553
|
-
function createFlexibleTenantFilter(tenantField, bypassRoles, extractOrganizationId) {
|
|
3554
|
-
return async (request, reply) => {
|
|
3555
|
-
const user = request.user;
|
|
3556
|
-
const orgId = extractOrganizationId(request);
|
|
3557
|
-
if (!orgId) {
|
|
3558
|
-
return;
|
|
3559
|
-
}
|
|
3560
|
-
if (!user) {
|
|
3561
|
-
reply.code(401).send({
|
|
3562
|
-
success: false,
|
|
3563
|
-
error: "Unauthorized",
|
|
3564
|
-
message: "Authentication required for organization-scoped data"
|
|
3565
|
-
});
|
|
3566
|
-
return;
|
|
3567
|
-
}
|
|
3568
|
-
const userWithRoles = user;
|
|
3569
|
-
if (userWithRoles.roles && bypassRoles.some((r) => userWithRoles.roles.includes(r))) {
|
|
3570
|
-
return;
|
|
3571
|
-
}
|
|
3572
|
-
request.query = request.query ?? {};
|
|
3573
|
-
request.query._policyFilters = {
|
|
3574
|
-
...request.query._policyFilters ?? {},
|
|
3575
|
-
[tenantField]: orgId
|
|
3576
|
-
};
|
|
3577
|
-
};
|
|
3578
|
-
}
|
|
3579
|
-
function createTenantInjection(tenantField, extractOrganizationId) {
|
|
3580
|
-
return async (request, reply) => {
|
|
3581
|
-
const orgId = extractOrganizationId(request);
|
|
3582
|
-
if (!orgId) {
|
|
3583
|
-
reply.code(403).send({
|
|
3584
|
-
success: false,
|
|
3585
|
-
error: "Forbidden",
|
|
3586
|
-
message: "Organization context required to create resources"
|
|
3587
|
-
});
|
|
3588
|
-
return;
|
|
3589
|
-
}
|
|
3590
|
-
if (request.body) {
|
|
3591
|
-
request.body[tenantField] = orgId;
|
|
3592
|
-
}
|
|
3593
|
-
};
|
|
3594
|
-
}
|
|
3595
|
-
function multiTenantPreset(options = {}) {
|
|
3596
|
-
const {
|
|
3597
|
-
tenantField = "organizationId",
|
|
3598
|
-
bypassRoles = ["superadmin"],
|
|
3599
|
-
extractOrganizationId = defaultExtractOrganizationId,
|
|
3600
|
-
allowPublic: allowPublic2 = []
|
|
3601
|
-
} = options;
|
|
3602
|
-
const strictTenantFilter = createTenantFilter(tenantField, bypassRoles, extractOrganizationId);
|
|
3603
|
-
const flexibleTenantFilter = createFlexibleTenantFilter(tenantField, bypassRoles, extractOrganizationId);
|
|
3604
|
-
const tenantInjection = createTenantInjection(tenantField, extractOrganizationId);
|
|
3605
|
-
const getFilter = (route) => allowPublic2.includes(route) ? flexibleTenantFilter : strictTenantFilter;
|
|
3606
|
-
return {
|
|
3607
|
-
name: "multiTenant",
|
|
3608
|
-
middlewares: {
|
|
3609
|
-
list: [getFilter("list")],
|
|
3610
|
-
get: [getFilter("get")],
|
|
3611
|
-
create: [tenantInjection],
|
|
3612
|
-
update: [getFilter("update")],
|
|
3613
|
-
delete: [getFilter("delete")]
|
|
3614
|
-
}
|
|
3615
|
-
};
|
|
3616
|
-
}
|
|
3617
|
-
|
|
3618
|
-
// src/presets/tree.ts
|
|
3619
|
-
function treePreset(options = {}) {
|
|
3620
|
-
const { parentField = "parent" } = options;
|
|
3621
|
-
return {
|
|
3622
|
-
name: "tree",
|
|
3623
|
-
additionalRoutes: (permissions) => [
|
|
3624
|
-
{
|
|
3625
|
-
method: "GET",
|
|
3626
|
-
path: "/tree",
|
|
3627
|
-
handler: "getTree",
|
|
3628
|
-
summary: "Get hierarchical tree",
|
|
3629
|
-
permissions: permissions.list ?? allowPublic(),
|
|
3630
|
-
wrapHandler: true
|
|
3631
|
-
},
|
|
3632
|
-
{
|
|
3633
|
-
method: "GET",
|
|
3634
|
-
path: `/:${parentField}/children`,
|
|
3635
|
-
handler: "getChildren",
|
|
3636
|
-
summary: "Get children of parent",
|
|
3637
|
-
permissions: permissions.list ?? allowPublic(),
|
|
3638
|
-
wrapHandler: true
|
|
3639
|
-
}
|
|
3640
|
-
],
|
|
3641
|
-
// Pass to controller so it knows which param to read
|
|
3642
|
-
controllerOptions: {
|
|
3643
|
-
parentField
|
|
3644
|
-
}
|
|
3645
|
-
};
|
|
3646
|
-
}
|
|
3647
|
-
|
|
3648
|
-
// src/presets/audited.ts
|
|
3649
|
-
function auditedPreset(options = {}) {
|
|
3650
|
-
const { createdByField = "createdBy", updatedByField = "updatedBy" } = options;
|
|
3651
|
-
const injectCreatedBy = async (request, _reply) => {
|
|
3652
|
-
const userWithId = request.user;
|
|
3653
|
-
if (userWithId?._id || userWithId?.id) {
|
|
3654
|
-
const userId = userWithId._id ?? userWithId.id;
|
|
3655
|
-
request.body[createdByField] = userId;
|
|
3656
|
-
request.body[updatedByField] = userId;
|
|
3657
|
-
}
|
|
3658
|
-
return void 0;
|
|
3659
|
-
};
|
|
3660
|
-
const injectUpdatedBy = async (request, _reply) => {
|
|
3661
|
-
const userWithId = request.user;
|
|
3662
|
-
if (userWithId?._id || userWithId?.id) {
|
|
3663
|
-
request.body[updatedByField] = userWithId._id ?? userWithId.id;
|
|
3664
|
-
}
|
|
3665
|
-
return void 0;
|
|
3666
|
-
};
|
|
3667
|
-
return {
|
|
3668
|
-
name: "audited",
|
|
3669
|
-
schemaOptions: {
|
|
3670
|
-
fieldRules: {
|
|
3671
|
-
[createdByField]: { systemManaged: true },
|
|
3672
|
-
[updatedByField]: { systemManaged: true },
|
|
3673
|
-
createdAt: { systemManaged: true },
|
|
3674
|
-
updatedAt: { systemManaged: true }
|
|
3675
|
-
}
|
|
3676
|
-
},
|
|
3677
|
-
middlewares: {
|
|
3678
|
-
create: [injectCreatedBy],
|
|
3679
|
-
update: [injectUpdatedBy]
|
|
3680
|
-
}
|
|
3681
|
-
};
|
|
3682
|
-
}
|
|
3683
|
-
|
|
3684
|
-
// src/presets/index.ts
|
|
3685
|
-
var presetRegistry = {
|
|
3686
|
-
softDelete: softDeletePreset,
|
|
3687
|
-
slugLookup: slugLookupPreset,
|
|
3688
|
-
ownedByUser: ownedByUserPreset,
|
|
3689
|
-
multiTenant: multiTenantPreset,
|
|
3690
|
-
tree: treePreset,
|
|
3691
|
-
audited: auditedPreset
|
|
3692
|
-
};
|
|
3693
|
-
function resolvePreset(name, options = {}) {
|
|
3694
|
-
const factory = presetRegistry[name];
|
|
3695
|
-
if (!factory) {
|
|
3696
|
-
const available = Object.keys(presetRegistry).join(", ");
|
|
3697
|
-
throw new Error(
|
|
3698
|
-
`Unknown preset: '${name}'
|
|
3699
|
-
Available presets: ${available}
|
|
3700
|
-
Docs: https://github.com/classytic/arc#presets`
|
|
3701
|
-
);
|
|
3702
|
-
}
|
|
3703
|
-
return factory(options);
|
|
3704
|
-
}
|
|
3705
|
-
function getAvailablePresets() {
|
|
3706
|
-
return Object.keys(presetRegistry);
|
|
3707
|
-
}
|
|
3708
|
-
function applyPresets(config, presets = []) {
|
|
3709
|
-
let result = { ...config };
|
|
3710
|
-
for (const preset of presets) {
|
|
3711
|
-
const resolved = resolvePresetInput(preset);
|
|
3712
|
-
result = mergePreset(result, resolved);
|
|
3713
|
-
}
|
|
3714
|
-
return result;
|
|
3715
|
-
}
|
|
3716
|
-
function resolvePresetInput(preset) {
|
|
3717
|
-
if (typeof preset === "object" && ("middlewares" in preset || "additionalRoutes" in preset)) {
|
|
3718
|
-
return preset;
|
|
3719
|
-
}
|
|
3720
|
-
if (typeof preset === "object" && "name" in preset) {
|
|
3721
|
-
const { name, ...options } = preset;
|
|
3722
|
-
return resolvePreset(name, options);
|
|
3723
|
-
}
|
|
3724
|
-
return resolvePreset(preset);
|
|
3725
|
-
}
|
|
3726
|
-
function mergePreset(config, preset) {
|
|
3727
|
-
const result = { ...config };
|
|
3728
|
-
if (preset.additionalRoutes) {
|
|
3729
|
-
const routes = typeof preset.additionalRoutes === "function" ? preset.additionalRoutes(config.permissions ?? {}) : preset.additionalRoutes;
|
|
3730
|
-
result.additionalRoutes = [
|
|
3731
|
-
...result.additionalRoutes ?? [],
|
|
3732
|
-
...routes
|
|
3733
|
-
];
|
|
3734
|
-
}
|
|
3735
|
-
if (preset.middlewares) {
|
|
3736
|
-
result.middlewares = result.middlewares ?? {};
|
|
3737
|
-
for (const [op, mws] of Object.entries(preset.middlewares)) {
|
|
3738
|
-
const key = op;
|
|
3739
|
-
result.middlewares[key] = [
|
|
3740
|
-
...result.middlewares[key] ?? [],
|
|
3741
|
-
...mws ?? []
|
|
3742
|
-
];
|
|
3743
|
-
}
|
|
3744
|
-
}
|
|
3745
|
-
if (preset.schemaOptions) {
|
|
3746
|
-
result.schemaOptions = {
|
|
3747
|
-
...result.schemaOptions,
|
|
3748
|
-
...preset.schemaOptions
|
|
3749
|
-
};
|
|
3750
|
-
}
|
|
3751
|
-
if (preset.controllerOptions) {
|
|
3752
|
-
result._controllerOptions = {
|
|
3753
|
-
...result._controllerOptions,
|
|
3754
|
-
...preset.controllerOptions
|
|
3755
|
-
};
|
|
3756
|
-
}
|
|
3757
|
-
if (preset.hooks && preset.hooks.length > 0) {
|
|
3758
|
-
result._hooks = result._hooks ?? [];
|
|
3759
|
-
for (const hook of preset.hooks) {
|
|
3760
|
-
result._hooks.push({
|
|
3761
|
-
presetName: preset.name,
|
|
3762
|
-
operation: hook.operation,
|
|
3763
|
-
phase: hook.phase,
|
|
3764
|
-
handler: hook.handler,
|
|
3765
|
-
priority: hook.priority
|
|
3766
|
-
});
|
|
3767
|
-
}
|
|
3768
|
-
}
|
|
3769
|
-
return result;
|
|
3770
|
-
}
|
|
3771
|
-
|
|
3772
|
-
// src/registry/index.ts
|
|
3773
|
-
init_ResourceRegistry();
|
|
3774
|
-
|
|
3775
|
-
// src/core/validateResourceConfig.ts
|
|
3776
|
-
function validateResourceConfig(config, options = {}) {
|
|
3777
|
-
const errors = [];
|
|
3778
|
-
const warnings = [];
|
|
3779
|
-
if (!config.name) {
|
|
3780
|
-
errors.push({
|
|
3781
|
-
field: "name",
|
|
3782
|
-
message: "Resource name is required",
|
|
3783
|
-
suggestion: 'Add a unique resource name (e.g., "product", "user")'
|
|
3784
|
-
});
|
|
3785
|
-
} else if (!/^[a-z][a-z0-9-]*$/i.test(config.name)) {
|
|
3786
|
-
errors.push({
|
|
3787
|
-
field: "name",
|
|
3788
|
-
message: `Invalid resource name "${config.name}"`,
|
|
3789
|
-
suggestion: "Use alphanumeric characters and hyphens, starting with a letter"
|
|
3790
|
-
});
|
|
3791
|
-
}
|
|
3792
|
-
const crudRoutes = ["list", "get", "create", "update", "delete"];
|
|
3793
|
-
const disabledRoutes = new Set(config.disabledRoutes ?? []);
|
|
3794
|
-
const enabledCrudRoutes = crudRoutes.filter((route) => !disabledRoutes.has(route));
|
|
3795
|
-
const hasCrudRoutes = !config.disableDefaultRoutes && enabledCrudRoutes.length > 0;
|
|
3796
|
-
if (hasCrudRoutes) {
|
|
3797
|
-
if (!config.adapter) {
|
|
3798
|
-
errors.push({
|
|
3799
|
-
field: "adapter",
|
|
3800
|
-
message: "Data adapter is required when CRUD routes are enabled",
|
|
3801
|
-
suggestion: "Provide an adapter: createMongooseAdapter({ model, repository })"
|
|
3802
|
-
});
|
|
3803
|
-
} else if (!config.adapter.repository) {
|
|
3804
|
-
errors.push({
|
|
3805
|
-
field: "adapter.repository",
|
|
3806
|
-
message: "Adapter must provide a repository",
|
|
3807
|
-
suggestion: "Ensure your adapter returns a valid CrudRepository"
|
|
3808
|
-
});
|
|
3809
|
-
}
|
|
3810
|
-
if (!config.controller) {
|
|
3811
|
-
warnings.push({
|
|
3812
|
-
field: "controller",
|
|
3813
|
-
message: "No controller provided, will auto-create BaseController"
|
|
3814
|
-
});
|
|
3815
|
-
}
|
|
3816
|
-
} else {
|
|
3817
|
-
if (!config.adapter && !config.additionalRoutes?.length) {
|
|
3818
|
-
warnings.push({
|
|
3819
|
-
field: "config",
|
|
3820
|
-
message: "Resource has no adapter and no additionalRoutes",
|
|
3821
|
-
suggestion: "Provide either adapter for CRUD or additionalRoutes for custom logic"
|
|
3822
|
-
});
|
|
3823
|
-
}
|
|
3824
|
-
}
|
|
3825
|
-
if (config.controller && !options.skipControllerCheck && !config.disableDefaultRoutes) {
|
|
3826
|
-
const ctrl = config.controller;
|
|
3827
|
-
const requiredMethods = ["list", "get", "create", "update", "delete"];
|
|
3828
|
-
for (const method of requiredMethods) {
|
|
3829
|
-
if (typeof ctrl[method] !== "function") {
|
|
3830
|
-
errors.push({
|
|
3831
|
-
field: `controller.${method}`,
|
|
3832
|
-
message: `Missing required CRUD method "${method}"`,
|
|
3833
|
-
suggestion: "Extend BaseController which implements IController interface"
|
|
3834
|
-
});
|
|
3835
|
-
}
|
|
3836
|
-
}
|
|
3837
|
-
}
|
|
3838
|
-
if (config.controller && config.additionalRoutes) {
|
|
3839
|
-
validateAdditionalRouteHandlers(config.controller, config.additionalRoutes, errors);
|
|
3840
|
-
}
|
|
3841
|
-
if (config.permissions) {
|
|
3842
|
-
validatePermissionKeys(config, options, errors, warnings);
|
|
3843
|
-
}
|
|
3844
|
-
if (config.presets && !options.allowUnknownPresets) {
|
|
3845
|
-
validatePresets(config.presets, errors, warnings);
|
|
3846
|
-
}
|
|
3847
|
-
if (config.prefix) {
|
|
3848
|
-
if (!config.prefix.startsWith("/")) {
|
|
3849
|
-
errors.push({
|
|
3850
|
-
field: "prefix",
|
|
3851
|
-
message: `Prefix must start with "/" (got "${config.prefix}")`,
|
|
3852
|
-
suggestion: `Change to "/${config.prefix}"`
|
|
3853
|
-
});
|
|
3854
|
-
}
|
|
3855
|
-
if (config.prefix.endsWith("/") && config.prefix !== "/") {
|
|
3856
|
-
warnings.push({
|
|
3857
|
-
field: "prefix",
|
|
3858
|
-
message: `Prefix should not end with "/" (got "${config.prefix}")`,
|
|
3859
|
-
suggestion: `Change to "${config.prefix.slice(0, -1)}"`
|
|
3860
|
-
});
|
|
3861
|
-
}
|
|
3862
|
-
}
|
|
3863
|
-
if (config.additionalRoutes) {
|
|
3864
|
-
validateAdditionalRoutes(config.additionalRoutes, errors);
|
|
3865
|
-
}
|
|
3866
|
-
return {
|
|
3867
|
-
valid: errors.length === 0,
|
|
3868
|
-
errors,
|
|
3869
|
-
warnings
|
|
3870
|
-
};
|
|
3871
|
-
}
|
|
3872
|
-
function validateAdditionalRouteHandlers(controller, routes, errors) {
|
|
3873
|
-
const ctrl = controller;
|
|
3874
|
-
for (const route of routes) {
|
|
3875
|
-
if (typeof route.handler === "string") {
|
|
3876
|
-
if (typeof ctrl[route.handler] !== "function") {
|
|
3877
|
-
errors.push({
|
|
3878
|
-
field: `additionalRoutes[${route.method} ${route.path}]`,
|
|
3879
|
-
message: `Handler "${route.handler}" not found on controller`,
|
|
3880
|
-
suggestion: `Add method "${route.handler}" to controller or use a function handler`
|
|
3881
|
-
});
|
|
3882
|
-
}
|
|
3883
|
-
}
|
|
3884
|
-
}
|
|
3885
|
-
}
|
|
3886
|
-
function validatePermissionKeys(config, options, errors, warnings) {
|
|
3887
|
-
const validKeys = /* @__PURE__ */ new Set([
|
|
3888
|
-
"list",
|
|
3889
|
-
"get",
|
|
3890
|
-
"create",
|
|
3891
|
-
"update",
|
|
3892
|
-
"delete",
|
|
3893
|
-
...options.additionalPermissionKeys ?? []
|
|
3894
|
-
]);
|
|
3895
|
-
for (const route of config.additionalRoutes ?? []) {
|
|
3896
|
-
if (typeof route.handler === "string") {
|
|
3897
|
-
validKeys.add(route.handler);
|
|
3898
|
-
}
|
|
3899
|
-
}
|
|
3900
|
-
for (const preset of config.presets ?? []) {
|
|
3901
|
-
const presetName = typeof preset === "string" ? preset : preset.name;
|
|
3902
|
-
if (presetName === "softDelete") {
|
|
3903
|
-
validKeys.add("deleted");
|
|
3904
|
-
validKeys.add("restore");
|
|
3905
|
-
}
|
|
3906
|
-
if (presetName === "slugLookup") {
|
|
3907
|
-
validKeys.add("getBySlug");
|
|
3908
|
-
}
|
|
3909
|
-
if (presetName === "tree") {
|
|
3910
|
-
validKeys.add("tree");
|
|
3911
|
-
validKeys.add("children");
|
|
3912
|
-
validKeys.add("getTree");
|
|
3913
|
-
validKeys.add("getChildren");
|
|
3914
|
-
}
|
|
3915
|
-
}
|
|
3916
|
-
for (const key of Object.keys(config.permissions ?? {})) {
|
|
3917
|
-
if (!validKeys.has(key)) {
|
|
3918
|
-
warnings.push({
|
|
3919
|
-
field: `permissions.${key}`,
|
|
3920
|
-
message: `Unknown permission key "${key}"`,
|
|
3921
|
-
suggestion: `Valid keys: ${Array.from(validKeys).join(", ")}`
|
|
3922
|
-
});
|
|
3923
|
-
}
|
|
3924
|
-
}
|
|
3925
|
-
}
|
|
3926
|
-
function validatePresets(presets, errors, warnings) {
|
|
3927
|
-
const availablePresets = getAvailablePresets();
|
|
3928
|
-
for (const preset of presets) {
|
|
3929
|
-
if (typeof preset === "object" && ("middlewares" in preset || "additionalRoutes" in preset)) {
|
|
3930
|
-
continue;
|
|
3931
|
-
}
|
|
3932
|
-
const presetName = typeof preset === "string" ? preset : preset.name;
|
|
3933
|
-
if (!availablePresets.includes(presetName)) {
|
|
3934
|
-
errors.push({
|
|
3935
|
-
field: "presets",
|
|
3936
|
-
message: `Unknown preset "${presetName}"`,
|
|
3937
|
-
suggestion: `Available presets: ${availablePresets.join(", ")}`
|
|
3938
|
-
});
|
|
3939
|
-
}
|
|
3940
|
-
if (typeof preset === "object") {
|
|
3941
|
-
validatePresetOptions(preset, warnings);
|
|
3942
|
-
}
|
|
3943
|
-
}
|
|
3944
|
-
}
|
|
3945
|
-
function validatePresetOptions(preset, warnings) {
|
|
3946
|
-
const knownOptions = {
|
|
3947
|
-
slugLookup: ["slugField"],
|
|
3948
|
-
tree: ["parentField"],
|
|
3949
|
-
softDelete: ["deletedField"],
|
|
3950
|
-
ownedByUser: ["ownerField", "bypassRoles"],
|
|
3951
|
-
multiTenant: ["tenantField", "bypassRoles"]
|
|
3952
|
-
};
|
|
3953
|
-
const validOptions = knownOptions[preset.name] ?? [];
|
|
3954
|
-
const providedOptions = Object.keys(preset).filter((k) => k !== "name");
|
|
3955
|
-
for (const opt of providedOptions) {
|
|
3956
|
-
if (!validOptions.includes(opt)) {
|
|
3957
|
-
warnings.push({
|
|
3958
|
-
field: `presets[${preset.name}].${opt}`,
|
|
3959
|
-
message: `Unknown option "${opt}" for preset "${preset.name}"`,
|
|
3960
|
-
suggestion: validOptions.length > 0 ? `Valid options: ${validOptions.join(", ")}` : `Preset "${preset.name}" has no configurable options`
|
|
3961
|
-
});
|
|
3962
|
-
}
|
|
3963
|
-
}
|
|
3964
|
-
}
|
|
3965
|
-
function validateAdditionalRoutes(routes, errors) {
|
|
3966
|
-
const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
|
|
3967
|
-
const seenRoutes = /* @__PURE__ */ new Set();
|
|
3968
|
-
for (const [i, route] of routes.entries()) {
|
|
3969
|
-
if (!validMethods.includes(route.method)) {
|
|
3970
|
-
errors.push({
|
|
3971
|
-
field: `additionalRoutes[${i}].method`,
|
|
3972
|
-
message: `Invalid HTTP method "${route.method}"`,
|
|
3973
|
-
suggestion: `Valid methods: ${validMethods.join(", ")}`
|
|
3974
|
-
});
|
|
3975
|
-
}
|
|
3976
|
-
if (!route.path) {
|
|
3977
|
-
errors.push({
|
|
3978
|
-
field: `additionalRoutes[${i}].path`,
|
|
3979
|
-
message: "Route path is required"
|
|
3980
|
-
});
|
|
3981
|
-
} else if (!route.path.startsWith("/")) {
|
|
3982
|
-
errors.push({
|
|
3983
|
-
field: `additionalRoutes[${i}].path`,
|
|
3984
|
-
message: `Route path must start with "/" (got "${route.path}")`,
|
|
3985
|
-
suggestion: `Change to "/${route.path}"`
|
|
3986
|
-
});
|
|
3987
|
-
}
|
|
3988
|
-
if (!route.handler) {
|
|
3989
|
-
errors.push({
|
|
3990
|
-
field: `additionalRoutes[${i}].handler`,
|
|
3991
|
-
message: "Route handler is required"
|
|
3992
|
-
});
|
|
3993
|
-
}
|
|
3994
|
-
const routeKey = `${route.method} ${route.path}`;
|
|
3995
|
-
if (seenRoutes.has(routeKey)) {
|
|
3996
|
-
errors.push({
|
|
3997
|
-
field: `additionalRoutes[${i}]`,
|
|
3998
|
-
message: `Duplicate route "${routeKey}"`
|
|
3999
|
-
});
|
|
4000
|
-
}
|
|
4001
|
-
seenRoutes.add(routeKey);
|
|
4002
|
-
}
|
|
4003
|
-
}
|
|
4004
|
-
function formatValidationErrors(resourceName, result) {
|
|
4005
|
-
const lines = [];
|
|
4006
|
-
if (result.errors.length > 0) {
|
|
4007
|
-
lines.push(`Resource "${resourceName}" validation failed:`);
|
|
4008
|
-
lines.push("");
|
|
4009
|
-
lines.push("ERRORS:");
|
|
4010
|
-
for (const err of result.errors) {
|
|
4011
|
-
lines.push(` ✗ ${err.field}: ${err.message}`);
|
|
4012
|
-
if (err.suggestion) {
|
|
4013
|
-
lines.push(` → ${err.suggestion}`);
|
|
4014
|
-
}
|
|
4015
|
-
}
|
|
4016
|
-
}
|
|
4017
|
-
if (result.warnings.length > 0) {
|
|
4018
|
-
if (lines.length > 0) lines.push("");
|
|
4019
|
-
lines.push("WARNINGS:");
|
|
4020
|
-
for (const warn of result.warnings) {
|
|
4021
|
-
lines.push(` ⚠ ${warn.field}: ${warn.message}`);
|
|
4022
|
-
if (warn.suggestion) {
|
|
4023
|
-
lines.push(` → ${warn.suggestion}`);
|
|
4024
|
-
}
|
|
4025
|
-
}
|
|
4026
|
-
}
|
|
4027
|
-
return lines.join("\n");
|
|
4028
|
-
}
|
|
4029
|
-
function assertValidConfig(config, options) {
|
|
4030
|
-
const result = validateResourceConfig(config, options);
|
|
4031
|
-
if (!result.valid) {
|
|
4032
|
-
const errorMsg = formatValidationErrors(config.name ?? "unknown", result);
|
|
4033
|
-
throw new Error(errorMsg);
|
|
4034
|
-
}
|
|
4035
|
-
if (result.warnings.length > 0 && process.env.NODE_ENV !== "production") {
|
|
4036
|
-
console.warn(formatValidationErrors(config.name ?? "unknown", {
|
|
4037
|
-
errors: [],
|
|
4038
|
-
warnings: result.warnings
|
|
4039
|
-
}));
|
|
4040
|
-
}
|
|
4041
|
-
}
|
|
4042
|
-
|
|
4043
|
-
// src/hooks/index.ts
|
|
4044
|
-
init_HookSystem();
|
|
4045
|
-
|
|
4046
|
-
// src/core/defineResource.ts
|
|
4047
|
-
function defineResource(config) {
|
|
4048
|
-
if (!config.skipValidation) {
|
|
4049
|
-
assertValidConfig(config, { skipControllerCheck: true });
|
|
4050
|
-
if (config.permissions) {
|
|
4051
|
-
for (const [key, value] of Object.entries(config.permissions)) {
|
|
4052
|
-
if (value !== void 0 && typeof value !== "function") {
|
|
4053
|
-
throw new Error(
|
|
4054
|
-
`[Arc] Resource '${config.name}': permissions.${key} must be a PermissionCheck function.
|
|
4055
|
-
Use allowPublic(), requireAuth(), or requireRoles(['role']) from @classytic/arc/permissions.`
|
|
4056
|
-
);
|
|
4057
|
-
}
|
|
4058
|
-
}
|
|
4059
|
-
}
|
|
4060
|
-
for (const route of config.additionalRoutes ?? []) {
|
|
4061
|
-
if (typeof route.permissions !== "function") {
|
|
4062
|
-
throw new Error(
|
|
4063
|
-
`[Arc] Resource '${config.name}' route ${route.method} ${route.path}: permissions is required and must be a PermissionCheck function.
|
|
4064
|
-
Use allowPublic() or requireAuth() from @classytic/arc/permissions.`
|
|
4065
|
-
);
|
|
4066
|
-
}
|
|
4067
|
-
if (typeof route.wrapHandler !== "boolean") {
|
|
4068
|
-
throw new Error(
|
|
4069
|
-
`[Arc] Resource '${config.name}' route ${route.method} ${route.path}: wrapHandler is required.
|
|
4070
|
-
Set true for ControllerHandler (context object) or false for FastifyHandler (req, reply).`
|
|
4071
|
-
);
|
|
4072
|
-
}
|
|
4073
|
-
}
|
|
4074
|
-
}
|
|
4075
|
-
const repository = config.adapter?.repository;
|
|
4076
|
-
const crudRoutes = ["list", "get", "create", "update", "delete"];
|
|
4077
|
-
const disabledRoutes = new Set(config.disabledRoutes ?? []);
|
|
4078
|
-
const hasCrudRoutes = !config.disableDefaultRoutes && crudRoutes.some((route) => !disabledRoutes.has(route));
|
|
4079
|
-
let controller = config.controller;
|
|
4080
|
-
if (!controller && hasCrudRoutes && repository) {
|
|
4081
|
-
controller = new BaseController(repository, {
|
|
4082
|
-
resourceName: config.name,
|
|
4083
|
-
schemaOptions: config.schemaOptions,
|
|
4084
|
-
queryParser: config.queryParser
|
|
4085
|
-
});
|
|
4086
|
-
}
|
|
4087
|
-
const originalPresets = (config.presets ?? []).map(
|
|
4088
|
-
(p) => typeof p === "string" ? p : p.name
|
|
4089
|
-
);
|
|
4090
|
-
const resolvedConfig = config.presets?.length ? applyPresets(config, config.presets) : config;
|
|
4091
|
-
resolvedConfig._appliedPresets = originalPresets;
|
|
4092
|
-
if (controller) {
|
|
4093
|
-
const ctrl = controller;
|
|
4094
|
-
if (typeof ctrl._setResourceOptions === "function") {
|
|
4095
|
-
ctrl._setResourceOptions({
|
|
4096
|
-
schemaOptions: resolvedConfig.schemaOptions,
|
|
4097
|
-
presetFields: resolvedConfig._controllerOptions ? {
|
|
4098
|
-
slugField: resolvedConfig._controllerOptions.slugField,
|
|
4099
|
-
parentField: resolvedConfig._controllerOptions.parentField
|
|
4100
|
-
} : void 0,
|
|
4101
|
-
resourceName: resolvedConfig.name,
|
|
4102
|
-
queryParser: resolvedConfig.queryParser
|
|
4103
|
-
});
|
|
4104
|
-
}
|
|
4105
|
-
}
|
|
4106
|
-
const resource = new ResourceDefinition({
|
|
4107
|
-
...resolvedConfig,
|
|
4108
|
-
adapter: config.adapter,
|
|
4109
|
-
controller
|
|
4110
|
-
});
|
|
4111
|
-
if (!config.skipValidation && controller) {
|
|
4112
|
-
resource._validateControllerMethods();
|
|
4113
|
-
}
|
|
4114
|
-
if (resolvedConfig._hooks?.length) {
|
|
4115
|
-
for (const hook of resolvedConfig._hooks) {
|
|
4116
|
-
hookSystem.register({
|
|
4117
|
-
resource: resolvedConfig.name,
|
|
4118
|
-
operation: hook.operation,
|
|
4119
|
-
phase: hook.phase,
|
|
4120
|
-
handler: hook.handler,
|
|
4121
|
-
priority: hook.priority ?? 10
|
|
4122
|
-
});
|
|
4123
|
-
}
|
|
4124
|
-
}
|
|
4125
|
-
if (!config.skipRegistry) {
|
|
4126
|
-
try {
|
|
4127
|
-
let openApiSchemas = config.openApiSchemas;
|
|
4128
|
-
if (!openApiSchemas && config.adapter?.generateSchemas) {
|
|
4129
|
-
const generated = config.adapter.generateSchemas(config.schemaOptions);
|
|
4130
|
-
if (generated) {
|
|
4131
|
-
openApiSchemas = generated;
|
|
4132
|
-
}
|
|
4133
|
-
}
|
|
4134
|
-
const queryParser = config.queryParser;
|
|
4135
|
-
if (!openApiSchemas?.listQuery && queryParser?.getQuerySchema) {
|
|
4136
|
-
const querySchema = queryParser.getQuerySchema();
|
|
4137
|
-
if (querySchema) {
|
|
4138
|
-
openApiSchemas = {
|
|
4139
|
-
...openApiSchemas,
|
|
4140
|
-
listQuery: querySchema
|
|
4141
|
-
};
|
|
4142
|
-
}
|
|
4143
|
-
}
|
|
4144
|
-
resourceRegistry.register(resource, {
|
|
4145
|
-
module: config.module,
|
|
4146
|
-
openApiSchemas
|
|
4147
|
-
});
|
|
4148
|
-
} catch {
|
|
4149
|
-
}
|
|
4150
|
-
}
|
|
4151
|
-
return resource;
|
|
4152
|
-
}
|
|
4153
|
-
var ResourceDefinition = class {
|
|
4154
|
-
// Identity
|
|
4155
|
-
name;
|
|
4156
|
-
displayName;
|
|
4157
|
-
tag;
|
|
4158
|
-
prefix;
|
|
4159
|
-
// Adapter (database abstraction) - optional for service resources
|
|
4160
|
-
adapter;
|
|
4161
|
-
// Controller
|
|
4162
|
-
controller;
|
|
4163
|
-
// Schema & Validation
|
|
4164
|
-
schemaOptions;
|
|
4165
|
-
customSchemas;
|
|
4166
|
-
// Security
|
|
4167
|
-
permissions;
|
|
4168
|
-
// Customization
|
|
4169
|
-
additionalRoutes;
|
|
4170
|
-
middlewares;
|
|
4171
|
-
disableDefaultRoutes;
|
|
4172
|
-
disabledRoutes;
|
|
4173
|
-
organizationScoped;
|
|
4174
|
-
// Events
|
|
4175
|
-
events;
|
|
4176
|
-
// Presets tracking
|
|
4177
|
-
_appliedPresets;
|
|
4178
|
-
constructor(config) {
|
|
4179
|
-
this.name = config.name;
|
|
4180
|
-
this.displayName = config.displayName ?? capitalize(config.name) + "s";
|
|
4181
|
-
this.tag = config.tag ?? this.displayName;
|
|
4182
|
-
this.prefix = config.prefix ?? `/${config.name}s`;
|
|
4183
|
-
this.adapter = config.adapter;
|
|
4184
|
-
this.controller = config.controller;
|
|
4185
|
-
this.schemaOptions = config.schemaOptions ?? {};
|
|
4186
|
-
this.customSchemas = config.customSchemas ?? {};
|
|
4187
|
-
this.permissions = config.permissions ?? {};
|
|
4188
|
-
this.additionalRoutes = config.additionalRoutes ?? [];
|
|
4189
|
-
this.middlewares = config.middlewares ?? {};
|
|
4190
|
-
this.disableDefaultRoutes = config.disableDefaultRoutes ?? false;
|
|
4191
|
-
this.disabledRoutes = config.disabledRoutes ?? [];
|
|
4192
|
-
this.organizationScoped = config.organizationScoped ?? false;
|
|
4193
|
-
this.events = config.events ?? {};
|
|
4194
|
-
this._appliedPresets = config._appliedPresets ?? [];
|
|
4195
|
-
}
|
|
4196
|
-
/** Get repository from adapter (if available) */
|
|
4197
|
-
get repository() {
|
|
4198
|
-
return this.adapter?.repository;
|
|
4199
|
-
}
|
|
4200
|
-
/** Get model from adapter (if available) */
|
|
4201
|
-
get model() {
|
|
4202
|
-
if (!this.adapter) return void 0;
|
|
4203
|
-
return this.adapter.getSchemaMetadata?.() ? this.adapter.model : void 0;
|
|
4204
|
-
}
|
|
4205
|
-
_validateControllerMethods() {
|
|
4206
|
-
const errors = [];
|
|
4207
|
-
const crudRoutes = ["list", "get", "create", "update", "delete"];
|
|
4208
|
-
const disabledRoutes = new Set(this.disabledRoutes ?? []);
|
|
4209
|
-
const enabledCrudRoutes = crudRoutes.filter((route) => !disabledRoutes.has(route));
|
|
4210
|
-
const hasCrudRoutes = !this.disableDefaultRoutes && enabledCrudRoutes.length > 0;
|
|
4211
|
-
if (hasCrudRoutes) {
|
|
4212
|
-
if (!this.controller) {
|
|
4213
|
-
errors.push("Controller is required when CRUD routes are enabled");
|
|
4214
|
-
} else {
|
|
4215
|
-
const ctrl = this.controller;
|
|
4216
|
-
for (const method of enabledCrudRoutes) {
|
|
4217
|
-
if (typeof ctrl[method] !== "function") {
|
|
4218
|
-
errors.push(`CRUD method '${method}' not found on controller`);
|
|
4219
|
-
}
|
|
4220
|
-
}
|
|
4221
|
-
}
|
|
4222
|
-
}
|
|
4223
|
-
for (const route of this.additionalRoutes) {
|
|
4224
|
-
if (typeof route.handler === "string") {
|
|
4225
|
-
if (!this.controller) {
|
|
4226
|
-
errors.push(
|
|
4227
|
-
`Route ${route.method} ${route.path}: string handler '${route.handler}' requires a controller`
|
|
4228
|
-
);
|
|
4229
|
-
} else {
|
|
4230
|
-
const ctrl = this.controller;
|
|
4231
|
-
if (typeof ctrl[route.handler] !== "function") {
|
|
4232
|
-
errors.push(
|
|
4233
|
-
`Route ${route.method} ${route.path}: handler '${route.handler}' not found`
|
|
4234
|
-
);
|
|
4235
|
-
}
|
|
4236
|
-
}
|
|
4237
|
-
}
|
|
4238
|
-
}
|
|
4239
|
-
if (errors.length > 0) {
|
|
4240
|
-
const errorMsg = [
|
|
4241
|
-
`Resource '${this.name}' validation failed:`,
|
|
4242
|
-
...errors.map((e) => ` - ${e}`),
|
|
4243
|
-
"",
|
|
4244
|
-
"Ensure controller implements IController<TDoc> interface.",
|
|
4245
|
-
"For preset routes (softDelete, tree), add corresponding methods to controller."
|
|
4246
|
-
].join("\n");
|
|
4247
|
-
throw new Error(errorMsg);
|
|
4248
|
-
}
|
|
4249
|
-
}
|
|
4250
|
-
toPlugin() {
|
|
4251
|
-
const self = this;
|
|
4252
|
-
return async function resourcePlugin(fastify, _opts) {
|
|
4253
|
-
await fastify.register(async (instance) => {
|
|
4254
|
-
const typedInstance = instance;
|
|
4255
|
-
let schemas = null;
|
|
4256
|
-
if (self.adapter) {
|
|
4257
|
-
const metadata = self.adapter.getSchemaMetadata?.();
|
|
4258
|
-
if (metadata && typedInstance.generateSchemas) {
|
|
4259
|
-
const model = self.adapter.model;
|
|
4260
|
-
if (model && typeof typedInstance.generateSchemas === "function") {
|
|
4261
|
-
schemas = typedInstance.generateSchemas(model, self.schemaOptions);
|
|
4262
|
-
}
|
|
4263
|
-
}
|
|
4264
|
-
}
|
|
4265
|
-
if (self.customSchemas && Object.keys(self.customSchemas).length > 0) {
|
|
4266
|
-
schemas = schemas ?? {};
|
|
4267
|
-
for (const [op, customSchema] of Object.entries(self.customSchemas)) {
|
|
4268
|
-
const key = op;
|
|
4269
|
-
schemas[key] = schemas[key] ? deepMergeSchemas(schemas[key], customSchema) : customSchema;
|
|
4270
|
-
}
|
|
4271
|
-
}
|
|
4272
|
-
const resolvedRoutes = self.additionalRoutes;
|
|
4273
|
-
createCrudRouter(typedInstance, self.controller, {
|
|
4274
|
-
tag: self.tag,
|
|
4275
|
-
schemas: schemas ?? void 0,
|
|
4276
|
-
permissions: self.permissions,
|
|
4277
|
-
middlewares: self.middlewares,
|
|
4278
|
-
additionalRoutes: resolvedRoutes,
|
|
4279
|
-
disableDefaultRoutes: self.disableDefaultRoutes,
|
|
4280
|
-
disabledRoutes: self.disabledRoutes,
|
|
4281
|
-
organizationScoped: self.organizationScoped,
|
|
4282
|
-
resourceName: self.name,
|
|
4283
|
-
schemaOptions: self.schemaOptions
|
|
4284
|
-
});
|
|
4285
|
-
if (self.events && Object.keys(self.events).length > 0) {
|
|
4286
|
-
typedInstance.log?.info?.(
|
|
4287
|
-
`Resource '${self.name}' defined ${Object.keys(self.events).length} events`
|
|
4288
|
-
);
|
|
4289
|
-
}
|
|
4290
|
-
}, { prefix: self.prefix });
|
|
4291
|
-
};
|
|
4292
|
-
}
|
|
4293
|
-
/**
|
|
4294
|
-
* Get event definitions for registry
|
|
4295
|
-
*/
|
|
4296
|
-
getEvents() {
|
|
4297
|
-
return Object.entries(this.events).map(([action, meta]) => ({
|
|
4298
|
-
name: `${this.name}:${action}`,
|
|
4299
|
-
module: this.name,
|
|
4300
|
-
schema: meta.schema,
|
|
4301
|
-
description: meta.description
|
|
4302
|
-
}));
|
|
4303
|
-
}
|
|
4304
|
-
/**
|
|
4305
|
-
* Get resource metadata
|
|
4306
|
-
*/
|
|
4307
|
-
getMetadata() {
|
|
4308
|
-
return {
|
|
4309
|
-
name: this.name,
|
|
4310
|
-
displayName: this.displayName,
|
|
4311
|
-
tag: this.tag,
|
|
4312
|
-
prefix: this.prefix,
|
|
4313
|
-
presets: this._appliedPresets,
|
|
4314
|
-
permissions: this.permissions,
|
|
4315
|
-
additionalRoutes: this.additionalRoutes,
|
|
4316
|
-
routes: [],
|
|
4317
|
-
// Populated at runtime during registration
|
|
4318
|
-
events: Object.keys(this.events)
|
|
4319
|
-
};
|
|
4320
|
-
}
|
|
4321
|
-
};
|
|
4322
|
-
function deepMergeSchemas(base, override) {
|
|
4323
|
-
if (!override) return base;
|
|
4324
|
-
if (!base) return override;
|
|
4325
|
-
const result = { ...base };
|
|
4326
|
-
for (const [key, value] of Object.entries(override)) {
|
|
4327
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
4328
|
-
result[key] = deepMergeSchemas(result[key], value);
|
|
4329
|
-
} else {
|
|
4330
|
-
result[key] = value;
|
|
4331
|
-
}
|
|
4332
|
-
}
|
|
4333
|
-
return result;
|
|
4334
|
-
}
|
|
4335
|
-
function capitalize(str) {
|
|
4336
|
-
if (!str) return "";
|
|
4337
|
-
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
4338
|
-
}
|
|
4339
|
-
|
|
4340
|
-
// src/utils/index.ts
|
|
4341
|
-
init_errors();
|
|
4342
|
-
|
|
4343
|
-
// src/index.ts
|
|
4344
|
-
init_plugins();
|
|
4345
|
-
|
|
4346
|
-
// src/events/EventTransport.ts
|
|
4347
|
-
var MemoryEventTransport = class {
|
|
4348
|
-
name = "memory";
|
|
4349
|
-
handlers = /* @__PURE__ */ new Map();
|
|
4350
|
-
async publish(event) {
|
|
4351
|
-
const exactHandlers = this.handlers.get(event.type) ?? /* @__PURE__ */ new Set();
|
|
4352
|
-
const wildcardHandlers = this.handlers.get("*") ?? /* @__PURE__ */ new Set();
|
|
4353
|
-
const patternHandlers = /* @__PURE__ */ new Set();
|
|
4354
|
-
for (const [pattern, handlers] of this.handlers.entries()) {
|
|
4355
|
-
if (pattern.endsWith(".*")) {
|
|
4356
|
-
const prefix = pattern.slice(0, -2);
|
|
4357
|
-
if (event.type.startsWith(prefix + ".")) {
|
|
4358
|
-
handlers.forEach((h) => patternHandlers.add(h));
|
|
4359
|
-
}
|
|
4360
|
-
}
|
|
4361
|
-
}
|
|
4362
|
-
const allHandlers = /* @__PURE__ */ new Set([...exactHandlers, ...wildcardHandlers, ...patternHandlers]);
|
|
4363
|
-
for (const handler of allHandlers) {
|
|
4364
|
-
try {
|
|
4365
|
-
await handler(event);
|
|
4366
|
-
} catch (err) {
|
|
4367
|
-
console.error(`[EventTransport] Handler error for ${event.type}:`, err);
|
|
4368
|
-
}
|
|
4369
|
-
}
|
|
4370
|
-
}
|
|
4371
|
-
async subscribe(pattern, handler) {
|
|
4372
|
-
if (!this.handlers.has(pattern)) {
|
|
4373
|
-
this.handlers.set(pattern, /* @__PURE__ */ new Set());
|
|
4374
|
-
}
|
|
4375
|
-
this.handlers.get(pattern).add(handler);
|
|
4376
|
-
return () => {
|
|
4377
|
-
this.handlers.get(pattern)?.delete(handler);
|
|
4378
|
-
};
|
|
4379
|
-
}
|
|
4380
|
-
async close() {
|
|
4381
|
-
this.handlers.clear();
|
|
4382
|
-
}
|
|
4383
|
-
};
|
|
4384
|
-
function createEvent(type, payload, meta) {
|
|
4385
|
-
return {
|
|
4386
|
-
type,
|
|
4387
|
-
payload,
|
|
4388
|
-
meta: {
|
|
4389
|
-
id: crypto.randomUUID(),
|
|
4390
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
4391
|
-
...meta
|
|
4392
|
-
}
|
|
4393
|
-
};
|
|
4394
|
-
}
|
|
4395
|
-
var eventPlugin = async (fastify, opts = {}) => {
|
|
4396
|
-
const {
|
|
4397
|
-
transport = new MemoryEventTransport(),
|
|
4398
|
-
logEvents = false
|
|
4399
|
-
} = opts;
|
|
4400
|
-
fastify.decorate("events", {
|
|
4401
|
-
publish: async (type, payload, meta) => {
|
|
4402
|
-
const event = createEvent(type, payload, meta);
|
|
4403
|
-
if (logEvents) {
|
|
4404
|
-
fastify.log?.info?.({ eventType: type, eventId: event.meta.id }, "Publishing event");
|
|
4405
|
-
}
|
|
4406
|
-
await transport.publish(event);
|
|
4407
|
-
},
|
|
4408
|
-
subscribe: async (pattern, handler) => {
|
|
4409
|
-
if (logEvents) {
|
|
4410
|
-
fastify.log?.info?.({ pattern }, "Subscribing to events");
|
|
4411
|
-
}
|
|
4412
|
-
return transport.subscribe(pattern, handler);
|
|
4413
|
-
},
|
|
4414
|
-
transportName: transport.name
|
|
4415
|
-
});
|
|
4416
|
-
fastify.addHook("onClose", async () => {
|
|
4417
|
-
await transport.close?.();
|
|
4418
|
-
});
|
|
4419
|
-
if (transport.name === "memory") {
|
|
4420
|
-
fastify.log?.warn?.(
|
|
4421
|
-
"[Arc Events] Using in-memory transport. Events will not persist or scale across instances. For production, configure a durable transport (Redis, RabbitMQ, etc.)"
|
|
4422
|
-
);
|
|
4423
|
-
} else {
|
|
4424
|
-
fastify.log?.info?.(`[Arc Events] Using ${transport.name} transport`);
|
|
4425
|
-
}
|
|
4426
|
-
};
|
|
4427
|
-
fp(eventPlugin, {
|
|
4428
|
-
name: "arc-events",
|
|
4429
|
-
fastify: "5.x"
|
|
4430
|
-
});
|
|
4431
|
-
|
|
4432
|
-
// src/factory/presets.ts
|
|
4433
|
-
var productionPreset = {
|
|
4434
|
-
// Raw JSON logs for production (log aggregators like Datadog, CloudWatch, etc.)
|
|
4435
|
-
logger: {
|
|
4436
|
-
level: "info"
|
|
4437
|
-
},
|
|
4438
|
-
trustProxy: true,
|
|
4439
|
-
// Security
|
|
4440
|
-
helmet: {
|
|
4441
|
-
contentSecurityPolicy: {
|
|
4442
|
-
directives: {
|
|
4443
|
-
defaultSrc: ["'self'"],
|
|
4444
|
-
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
4445
|
-
scriptSrc: ["'self'"],
|
|
4446
|
-
imgSrc: ["'self'", "data:", "https:"]
|
|
4447
|
-
}
|
|
4448
|
-
}
|
|
4449
|
-
},
|
|
4450
|
-
// CORS - must be explicitly configured
|
|
4451
|
-
cors: {
|
|
4452
|
-
origin: false,
|
|
4453
|
-
// Disabled by default in production
|
|
4454
|
-
credentials: true,
|
|
4455
|
-
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
|
4456
|
-
allowedHeaders: ["Content-Type", "Authorization", "Accept"]
|
|
4457
|
-
},
|
|
4458
|
-
// Rate limiting - strict
|
|
4459
|
-
rateLimit: {
|
|
4460
|
-
max: 100,
|
|
4461
|
-
timeWindow: "1 minute"
|
|
4462
|
-
},
|
|
4463
|
-
// Note: Compression not included (use proxy/CDN instead)
|
|
4464
|
-
// Under pressure - health monitoring
|
|
4465
|
-
underPressure: {
|
|
4466
|
-
exposeStatusRoute: true,
|
|
4467
|
-
maxEventLoopDelay: 1e3,
|
|
4468
|
-
maxHeapUsedBytes: 1024 * 1024 * 1024,
|
|
4469
|
-
// 1GB
|
|
4470
|
-
maxRssBytes: 1024 * 1024 * 1024
|
|
4471
|
-
// 1GB
|
|
4472
|
-
}
|
|
4473
|
-
};
|
|
4474
|
-
var developmentPreset = {
|
|
4475
|
-
logger: {
|
|
4476
|
-
level: "debug",
|
|
4477
|
-
transport: {
|
|
4478
|
-
target: "pino-pretty",
|
|
4479
|
-
options: {
|
|
4480
|
-
colorize: true,
|
|
4481
|
-
translateTime: "SYS:HH:MM:ss",
|
|
4482
|
-
ignore: "pid,hostname"
|
|
4483
|
-
}
|
|
4484
|
-
}
|
|
4485
|
-
},
|
|
4486
|
-
trustProxy: true,
|
|
4487
|
-
// Security - relaxed for development
|
|
4488
|
-
helmet: {
|
|
4489
|
-
contentSecurityPolicy: false
|
|
4490
|
-
// Disable CSP in dev
|
|
4491
|
-
},
|
|
4492
|
-
// CORS - allow all origins in development
|
|
4493
|
-
cors: {
|
|
4494
|
-
origin: true,
|
|
4495
|
-
// Allow all origins
|
|
4496
|
-
credentials: true,
|
|
4497
|
-
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
|
4498
|
-
allowedHeaders: ["Content-Type", "Authorization", "Accept"]
|
|
4499
|
-
},
|
|
4500
|
-
// Rate limiting - very relaxed
|
|
4501
|
-
rateLimit: {
|
|
4502
|
-
max: 1e3,
|
|
4503
|
-
timeWindow: "1 minute"
|
|
4504
|
-
},
|
|
4505
|
-
// Note: Compression not included (use proxy/CDN instead)
|
|
4506
|
-
// Under pressure - relaxed
|
|
4507
|
-
underPressure: {
|
|
4508
|
-
exposeStatusRoute: true,
|
|
4509
|
-
maxEventLoopDelay: 5e3
|
|
4510
|
-
}
|
|
4511
|
-
};
|
|
4512
|
-
var testingPreset = {
|
|
4513
|
-
logger: false,
|
|
4514
|
-
// Disable logging in tests
|
|
4515
|
-
trustProxy: false,
|
|
4516
|
-
// Security - disabled for tests
|
|
4517
|
-
helmet: false,
|
|
4518
|
-
cors: false,
|
|
4519
|
-
rateLimit: false,
|
|
4520
|
-
underPressure: false,
|
|
4521
|
-
// Sensible plugins still enabled
|
|
4522
|
-
sensible: true,
|
|
4523
|
-
multipart: {
|
|
4524
|
-
limits: {
|
|
4525
|
-
fileSize: 1024 * 1024,
|
|
4526
|
-
// 1MB
|
|
4527
|
-
files: 5
|
|
4528
|
-
}
|
|
4529
|
-
}
|
|
4530
|
-
};
|
|
4531
|
-
function getPreset(name) {
|
|
4532
|
-
switch (name) {
|
|
4533
|
-
case "production":
|
|
4534
|
-
return productionPreset;
|
|
4535
|
-
case "development":
|
|
4536
|
-
return developmentPreset;
|
|
4537
|
-
case "testing":
|
|
4538
|
-
return testingPreset;
|
|
4539
|
-
default:
|
|
4540
|
-
throw new Error(`Unknown preset: ${name}`);
|
|
4541
|
-
}
|
|
4542
|
-
}
|
|
4543
|
-
|
|
4544
|
-
// src/factory/createApp.ts
|
|
4545
|
-
var PLUGIN_PACKAGES = {
|
|
4546
|
-
cors: "@fastify/cors",
|
|
4547
|
-
helmet: "@fastify/helmet",
|
|
4548
|
-
rateLimit: "@fastify/rate-limit",
|
|
4549
|
-
underPressure: "@fastify/under-pressure",
|
|
4550
|
-
sensible: "@fastify/sensible",
|
|
4551
|
-
multipart: "@fastify/multipart",
|
|
4552
|
-
rawBody: "fastify-raw-body"
|
|
4553
|
-
};
|
|
4554
|
-
var OPTIONAL_PLUGINS = /* @__PURE__ */ new Set(["multipart", "rawBody"]);
|
|
4555
|
-
async function loadPlugin(name, logger) {
|
|
4556
|
-
const packageName = PLUGIN_PACKAGES[name];
|
|
4557
|
-
if (!packageName) {
|
|
4558
|
-
throw new Error(`Unknown plugin: ${name}`);
|
|
4559
|
-
}
|
|
4560
|
-
try {
|
|
4561
|
-
switch (name) {
|
|
4562
|
-
case "cors":
|
|
4563
|
-
return (await import('@fastify/cors')).default;
|
|
4564
|
-
case "helmet":
|
|
4565
|
-
return (await import('@fastify/helmet')).default;
|
|
4566
|
-
case "rateLimit":
|
|
4567
|
-
return (await import('@fastify/rate-limit')).default;
|
|
4568
|
-
case "underPressure":
|
|
4569
|
-
return (await import('@fastify/under-pressure')).default;
|
|
4570
|
-
case "sensible":
|
|
4571
|
-
return (await import('@fastify/sensible')).default;
|
|
4572
|
-
case "multipart":
|
|
4573
|
-
return (await import('@fastify/multipart')).default;
|
|
4574
|
-
case "rawBody":
|
|
4575
|
-
return (await import('fastify-raw-body')).default;
|
|
4576
|
-
default:
|
|
4577
|
-
throw new Error(`Unknown plugin: ${name}`);
|
|
4578
|
-
}
|
|
4579
|
-
} catch (error) {
|
|
4580
|
-
const err = error;
|
|
4581
|
-
const isModuleNotFound = err.message.includes("Cannot find module") || err.message.includes("Cannot find package") || err.message.includes("MODULE_NOT_FOUND") || err.message.includes("Could not resolve");
|
|
4582
|
-
if (isModuleNotFound && OPTIONAL_PLUGINS.has(name)) {
|
|
4583
|
-
logger?.warn(`ℹ️ Optional plugin '${name}' skipped (${packageName} not installed)`);
|
|
4584
|
-
return null;
|
|
4585
|
-
}
|
|
4586
|
-
if (isModuleNotFound) {
|
|
4587
|
-
throw new Error(
|
|
4588
|
-
`Plugin '${name}' requires package '${packageName}' which is not installed.
|
|
4589
|
-
Install it with: npm install ${packageName}
|
|
4590
|
-
Or disable this plugin by setting ${name}: false in createApp options.`
|
|
4591
|
-
);
|
|
4592
|
-
}
|
|
4593
|
-
throw new Error(`Failed to load plugin '${name}': ${err.message}`);
|
|
4594
|
-
}
|
|
4595
|
-
}
|
|
4596
|
-
async function createApp(options) {
|
|
4597
|
-
const authConfig = options.auth;
|
|
4598
|
-
const isAuthDisabled = authConfig === false;
|
|
4599
|
-
const hasCustomPlugin = typeof authConfig === "object" && "plugin" in authConfig && authConfig.plugin;
|
|
4600
|
-
const hasCustomAuthenticator = typeof authConfig === "object" && "authenticate" in authConfig;
|
|
4601
|
-
const jwtSecret = typeof authConfig === "object" && "jwt" in authConfig ? authConfig.jwt?.secret : void 0;
|
|
4602
|
-
if (!isAuthDisabled && !hasCustomPlugin && !jwtSecret && !hasCustomAuthenticator) {
|
|
4603
|
-
throw new Error(
|
|
4604
|
-
"createApp: JWT secret required when Arc auth is enabled.\nProvide auth.jwt.secret, auth.authenticate, or set auth: false to disable.\nExample: auth: { jwt: { secret: process.env.JWT_SECRET } }"
|
|
4605
|
-
);
|
|
4606
|
-
}
|
|
4607
|
-
const presetConfig = options.preset ? getPreset(options.preset) : {};
|
|
4608
|
-
const config = { ...presetConfig, ...options };
|
|
4609
|
-
const fastify = Fastify({
|
|
4610
|
-
logger: config.logger ?? true,
|
|
4611
|
-
trustProxy: config.trustProxy ?? false,
|
|
4612
|
-
// Use qs parser to support nested bracket notation in query strings
|
|
4613
|
-
// e.g., ?populate[author][select]=name,email → { populate: { author: { select: 'name,email' } } }
|
|
4614
|
-
// This is required for MongoKit's advanced populate options to work
|
|
4615
|
-
querystringParser: (str) => qs.parse(str),
|
|
4616
|
-
ajv: {
|
|
4617
|
-
customOptions: {
|
|
4618
|
-
coerceTypes: true,
|
|
4619
|
-
useDefaults: true,
|
|
4620
|
-
removeAdditional: false
|
|
4621
|
-
}
|
|
4622
|
-
}
|
|
4623
|
-
});
|
|
4624
|
-
if (config.helmet !== false) {
|
|
4625
|
-
const helmet = await loadPlugin("helmet");
|
|
4626
|
-
await fastify.register(helmet, config.helmet ?? {});
|
|
4627
|
-
fastify.log.info("✅ Helmet (security headers) enabled");
|
|
4628
|
-
} else {
|
|
4629
|
-
fastify.log.warn("⚠️ Helmet disabled - security headers not applied");
|
|
4630
|
-
}
|
|
4631
|
-
if (config.cors !== false) {
|
|
4632
|
-
const cors = await loadPlugin("cors");
|
|
4633
|
-
const corsOptions = config.cors ?? {};
|
|
4634
|
-
if (config.preset === "production" && (!corsOptions || !("origin" in corsOptions))) {
|
|
4635
|
-
throw new Error(
|
|
4636
|
-
"CORS origin must be explicitly configured in production.\nSet cors.origin to allowed domains or set cors: false to disable.\nExample: cors: { origin: ['https://yourdomain.com'] }\nDocs: https://github.com/classytic/arc#security"
|
|
4637
|
-
);
|
|
4638
|
-
}
|
|
4639
|
-
await fastify.register(cors, corsOptions);
|
|
4640
|
-
fastify.log.info("✅ CORS enabled");
|
|
4641
|
-
} else {
|
|
4642
|
-
fastify.log.warn("⚠️ CORS disabled");
|
|
4643
|
-
}
|
|
4644
|
-
if (config.rateLimit !== false) {
|
|
4645
|
-
const rateLimit = await loadPlugin("rateLimit");
|
|
4646
|
-
await fastify.register(rateLimit, config.rateLimit ?? { max: 100, timeWindow: "1 minute" });
|
|
4647
|
-
fastify.log.info("✅ Rate limiting enabled");
|
|
4648
|
-
} else {
|
|
4649
|
-
fastify.log.warn("⚠️ Rate limiting disabled");
|
|
4650
|
-
}
|
|
4651
|
-
if (config.underPressure !== false) {
|
|
4652
|
-
const underPressure = await loadPlugin("underPressure");
|
|
4653
|
-
await fastify.register(underPressure, config.underPressure ?? { exposeStatusRoute: true });
|
|
4654
|
-
fastify.log.info("✅ Health monitoring (under-pressure) enabled");
|
|
4655
|
-
} else {
|
|
4656
|
-
fastify.log.info("ℹ️ Health monitoring disabled");
|
|
4657
|
-
}
|
|
4658
|
-
if (config.sensible !== false) {
|
|
4659
|
-
const sensible = await loadPlugin("sensible");
|
|
4660
|
-
await fastify.register(sensible);
|
|
4661
|
-
fastify.log.info("✅ Sensible (HTTP helpers) enabled");
|
|
4662
|
-
}
|
|
4663
|
-
if (config.multipart !== false) {
|
|
4664
|
-
const multipart = await loadPlugin("multipart", fastify.log);
|
|
4665
|
-
if (multipart) {
|
|
4666
|
-
const multipartDefaults = {
|
|
4667
|
-
limits: {
|
|
4668
|
-
fileSize: 10 * 1024 * 1024,
|
|
4669
|
-
// 10MB
|
|
4670
|
-
files: 10
|
|
4671
|
-
}
|
|
4672
|
-
};
|
|
4673
|
-
await fastify.register(multipart, { ...multipartDefaults, ...config.multipart });
|
|
4674
|
-
fastify.log.info("✅ Multipart (file uploads) enabled");
|
|
4675
|
-
}
|
|
4676
|
-
}
|
|
4677
|
-
if (config.rawBody !== false) {
|
|
4678
|
-
const rawBody = await loadPlugin("rawBody", fastify.log);
|
|
4679
|
-
if (rawBody) {
|
|
4680
|
-
const rawBodyDefaults = {
|
|
4681
|
-
field: "rawBody",
|
|
4682
|
-
global: false,
|
|
4683
|
-
encoding: "utf8",
|
|
4684
|
-
runFirst: true
|
|
4685
|
-
};
|
|
4686
|
-
await fastify.register(rawBody, { ...rawBodyDefaults, ...config.rawBody });
|
|
4687
|
-
fastify.log.info("✅ Raw body parsing enabled");
|
|
4688
|
-
}
|
|
4689
|
-
}
|
|
4690
|
-
const { arcCorePlugin: arcCorePlugin2 } = await Promise.resolve().then(() => (init_plugins(), plugins_exports));
|
|
4691
|
-
await fastify.register(arcCorePlugin2, {
|
|
4692
|
-
emitEvents: config.arcPlugins?.emitEvents !== false
|
|
4693
|
-
});
|
|
4694
|
-
if (config.arcPlugins?.requestId !== false) {
|
|
4695
|
-
const { requestIdPlugin: requestIdPlugin2 } = await Promise.resolve().then(() => (init_plugins(), plugins_exports));
|
|
4696
|
-
await fastify.register(requestIdPlugin2);
|
|
4697
|
-
fastify.log.info("✅ Arc requestId plugin enabled");
|
|
4698
|
-
}
|
|
4699
|
-
if (config.arcPlugins?.health !== false) {
|
|
4700
|
-
const { healthPlugin: healthPlugin2 } = await Promise.resolve().then(() => (init_plugins(), plugins_exports));
|
|
4701
|
-
await fastify.register(healthPlugin2);
|
|
4702
|
-
fastify.log.info("✅ Arc health plugin enabled");
|
|
4703
|
-
}
|
|
4704
|
-
if (config.arcPlugins?.gracefulShutdown !== false) {
|
|
4705
|
-
const { gracefulShutdownPlugin: gracefulShutdownPlugin2 } = await Promise.resolve().then(() => (init_plugins(), plugins_exports));
|
|
4706
|
-
await fastify.register(gracefulShutdownPlugin2);
|
|
4707
|
-
fastify.log.info("✅ Arc gracefulShutdown plugin enabled");
|
|
4708
|
-
}
|
|
4709
|
-
if (!isAuthDisabled) {
|
|
4710
|
-
if (hasCustomPlugin) {
|
|
4711
|
-
const pluginFn = authConfig.plugin;
|
|
4712
|
-
await pluginFn(fastify);
|
|
4713
|
-
fastify.log.info("✅ Custom authentication plugin enabled");
|
|
4714
|
-
} else {
|
|
4715
|
-
const { authPlugin: authPlugin2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
|
|
4716
|
-
const { plugin: _, ...authOpts } = typeof authConfig === "object" ? authConfig : {};
|
|
4717
|
-
await fastify.register(authPlugin2, authOpts);
|
|
4718
|
-
fastify.log.info("✅ Arc authentication plugin enabled");
|
|
4719
|
-
}
|
|
4720
|
-
} else {
|
|
4721
|
-
fastify.log.info("ℹ️ Authentication disabled");
|
|
4722
|
-
}
|
|
4723
|
-
if (config.plugins) {
|
|
4724
|
-
await config.plugins(fastify);
|
|
4725
|
-
fastify.log.info("✅ Custom plugins registered");
|
|
4726
|
-
}
|
|
4727
|
-
fastify.log.info(
|
|
4728
|
-
`🚀 Arc application created successfully (preset: ${config.preset ?? "custom"}, security: helmet=${config.helmet !== false}, cors=${config.cors !== false}, rateLimit=${config.rateLimit !== false})`
|
|
4729
|
-
);
|
|
4730
|
-
return fastify;
|
|
4731
|
-
}
|
|
4732
|
-
var ArcFactory = {
|
|
4733
|
-
/**
|
|
4734
|
-
* Create production app with strict security
|
|
4735
|
-
*/
|
|
4736
|
-
async production(options) {
|
|
4737
|
-
return createApp({ ...options, preset: "production" });
|
|
4738
|
-
},
|
|
4739
|
-
/**
|
|
4740
|
-
* Create development app with relaxed security
|
|
4741
|
-
*/
|
|
4742
|
-
async development(options) {
|
|
4743
|
-
return createApp({ ...options, preset: "development" });
|
|
4744
|
-
},
|
|
4745
|
-
/**
|
|
4746
|
-
* Create testing app with minimal setup
|
|
4747
|
-
*/
|
|
4748
|
-
async testing(options) {
|
|
4749
|
-
return createApp({ ...options, preset: "testing" });
|
|
4750
|
-
}
|
|
4751
|
-
};
|
|
4752
|
-
|
|
4753
|
-
// src/index.ts
|
|
4754
|
-
var version = "1.0.0";
|
|
4755
|
-
|
|
4756
|
-
export { ArcError, ArcFactory, BaseController, ForbiddenError, MongooseAdapter, NotFoundError, PrismaAdapter, ResourceDefinition, UnauthorizedError, ValidationError, allOf, allowPublic, anyOf, assertValidConfig, createApp, createMongooseAdapter, createPrismaAdapter, defineResource, denyAll, eventPlugin, formatValidationErrors, gracefulShutdown_default as gracefulShutdownPlugin, health_default as healthPlugin, hookSystem, requestId_default as requestIdPlugin, requireAuth, requireOwnership, requireRoles, resourceRegistry, validateResourceConfig, version, when };
|