@classytic/arc 2.11.3 → 2.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -18
- package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
- package/dist/EventTransport-CT_52aWU.d.mts +34 -0
- package/dist/EventTransport-DLWoUMHy.mjs +103 -0
- package/dist/{QueryCache-DOBNHBE0.d.mts → QueryCache-D41bfdBB.d.mts} +1 -1
- package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/audit.d.mts +199 -0
- package/dist/auth/audit.mjs +288 -0
- package/dist/auth/index.d.mts +5 -5
- package/dist/auth/index.mjs +117 -191
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
- package/dist/buildHandler-olo-gt94.mjs +610 -0
- package/dist/cache/index.d.mts +3 -3
- package/dist/cache/index.mjs +3 -3
- package/dist/cli/commands/describe.d.mts +89 -13
- package/dist/cli/commands/describe.mjs +56 -2
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +147 -48
- package/dist/cli/commands/init.d.mts +13 -0
- package/dist/cli/commands/init.mjs +237 -112
- package/dist/cli/commands/introspect.mjs +8 -1
- package/dist/context/index.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -5
- package/dist/core-D72ia0EH.mjs +1399 -0
- package/dist/{createActionRouter-u3ql2EDo.mjs → createActionRouter-CEvzKcy8.mjs} +7 -20
- package/dist/createAggregationRouter-CyecOxnO.mjs +114 -0
- package/dist/{createApp-BFxtdKy6.mjs → createApp-XX2-N0Yd.mjs} +31 -27
- package/dist/defineEvent-D5h7EvAx.mjs +188 -0
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
- package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
- package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
- package/dist/errors-j4aJm1Wg.mjs +184 -0
- package/dist/{eventPlugin-KrFIQ097.mjs → eventPlugin-CaKTYkYM.mjs} +35 -137
- package/dist/{eventPlugin-CUNjYYRY.d.mts → eventPlugin-qXpqTebY.d.mts} +57 -7
- package/dist/events/index.d.mts +164 -5
- package/dist/events/index.mjs +133 -209
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +204 -31
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +2 -2
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-C8Y0XLAu.d.mts → fields-COhcH3fk.d.mts} +23 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/index.mjs +1 -20
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +1 -1
- package/dist/{index-BYCqHCVu.d.mts → index-BTqLEvhu.d.mts} +164 -4
- package/dist/{index-6u4_Gg6G.d.mts → index-BtW7qYwa.d.mts} +661 -281
- package/dist/{index-BdXnTPRj.d.mts → index-Ds61mrJE.d.mts} +50 -4
- package/dist/{index-DdQ3O9Pg.d.mts → index-Dz5IKsrE.d.mts} +360 -219
- package/dist/index.d.mts +6 -7
- package/dist/index.mjs +9 -10
- package/dist/integrations/event-gateway.d.mts +2 -2
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +60 -11
- package/dist/integrations/streamline.mjs +75 -85
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +2 -8
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +2 -2
- package/dist/migrations/index.d.mts +23 -3
- package/dist/migrations/index.mjs +0 -7
- package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
- package/dist/{openapi-BGUn7Ki1.mjs → openapi-CiOMVW1p.mjs} +143 -13
- package/dist/org/index.d.mts +2 -2
- package/dist/org/index.mjs +1 -1
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +3 -3
- package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
- package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +18 -33
- package/dist/plugins/index.mjs +33 -13
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +5 -5
- package/dist/presets/filesUpload.mjs +6 -9
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +2 -2
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +6 -8
- package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
- package/dist/{queryCachePlugin-BUXBSm4F.d.mts → queryCachePlugin-CqMdLI2-.d.mts} +2 -2
- package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
- package/dist/{redis-Cm1gnRDf.d.mts → redis-DiMkdHEl.d.mts} +1 -1
- package/dist/redis-stream-D6HzR1Z_.d.mts +232 -0
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
- package/dist/{resourceToTools-ByZpgjeH.mjs → resourceToTools-C5coh64w.mjs} +224 -71
- package/dist/{routerShared-BqLRb5l7.mjs → routerShared-D6_fEGHh.mjs} +40 -36
- package/dist/{schemaIR-BlG9bY7v.mjs → schemaIR-7Vl611Qs.mjs} +1 -1
- package/dist/schemas/index.d.mts +100 -30
- package/dist/schemas/index.mjs +86 -29
- package/dist/scim/index.d.mts +264 -0
- package/dist/scim/index.mjs +963 -0
- package/dist/scope/index.d.mts +3 -3
- package/dist/scope/index.mjs +4 -4
- package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
- package/dist/{store-helpers-BhrzxvyQ.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
- package/dist/testing/index.d.mts +2 -8
- package/dist/testing/index.mjs +16 -24
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-BH7dEGvU.d.mts → types-BvqwCCSx.d.mts} +77 -29
- package/dist/{types-tgR4Pt8F.d.mts → types-CTYvcwHe.d.mts} +195 -1
- package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
- package/dist/{types-9beEMe25.d.mts → types-DQHFc8PM.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +5 -5
- package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
- package/dist/{versioning-M9lNLhO8.d.mts → versioning-DTTvc80y.d.mts} +1 -1
- package/package.json +24 -34
- package/skills/arc/SKILL.md +521 -785
- package/skills/arc/references/agent-auth.md +238 -0
- package/skills/arc/references/api-reference.md +187 -0
- package/skills/arc/references/auth.md +354 -7
- package/skills/arc/references/enterprise-auth.md +94 -0
- package/skills/arc/references/events.md +8 -6
- package/skills/arc/references/mcp.md +2 -2
- package/skills/arc/references/multi-tenancy.md +11 -2
- package/skills/arc/references/production.md +10 -9
- package/skills/arc/references/scim.md +247 -0
- package/skills/arc/references/testing.md +1 -1
- package/skills/arc-code-review/SKILL.md +141 -0
- package/skills/arc-code-review/references/anti-patterns.md +911 -0
- package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
- package/skills/arc-code-review/references/migration-recipes.md +700 -0
- package/skills/arc-code-review/references/mongokit-migration.md +386 -0
- package/skills/arc-code-review/references/scaffolding.md +230 -0
- package/skills/arc-code-review/references/severity.md +127 -0
- package/dist/EventTransport-CfVEGaEl.d.mts +0 -293
- package/dist/adapters/index.d.mts +0 -3
- package/dist/adapters/index.mjs +0 -2
- package/dist/adapters-D0tT2Tyo.mjs +0 -949
- package/dist/auth/mongoose.d.mts +0 -191
- package/dist/auth/mongoose.mjs +0 -73
- package/dist/core-DnUsRpuX.mjs +0 -1049
- package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
- package/dist/errorHandler-Co3lnVmJ.d.mts +0 -114
- package/dist/errors-D5c-5BJL.mjs +0 -232
- package/dist/index-BbMrcvGp.d.mts +0 -362
- package/dist/redis-stream-CM8TXTix.d.mts +0 -110
- /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
- /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
- /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
- /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
- /package/dist/{elevation-s5ykdNHr.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
- /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BD5nw6St.d.mts} +0 -0
- /package/dist/{interface-CkkWm5uR.d.mts → interface-DfLGcus7.d.mts} +0 -0
- /package/dist/{interface-Da0r7Lna.d.mts → interface-beEtJyWM.d.mts} +0 -0
- /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
- /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
- /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
- /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
- /package/dist/{pluralize-BneOJkpi.mjs → pluralize-DQgqgifU.mjs} +0 -0
- /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
- /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
- /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
- /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-C4Le_UB3.d.mts} +0 -0
- /package/dist/{storage-BwGQXUpd.d.mts → storage-Dfzt4VTl.d.mts} +0 -0
- /package/dist/{tracing-DokiEsuz.d.mts → tracing-QJVprktp.d.mts} +0 -0
- /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
- /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
- /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
- /package/dist/{websocket-CyJ1VIFI.d.mts → websocket-ChC2rqe1.d.mts} +0 -0
|
@@ -1,949 +0,0 @@
|
|
|
1
|
-
import { h as SYSTEM_FIELDS, m as RESERVED_QUERY_PARAMS } from "./constants-BhY1OHoH.mjs";
|
|
2
|
-
//#region src/adapters/field-rule-helpers.ts
|
|
3
|
-
/**
|
|
4
|
-
* Merge constraint-style `fieldRules` into an `OpenApiSchemas` bag in place.
|
|
5
|
-
*
|
|
6
|
-
* Operates on the three schema slots that carry property maps — `createBody`,
|
|
7
|
-
* `updateBody`, `response`. `listQuery` and `params` are skipped (their
|
|
8
|
-
* constraint vocabulary is owned by the kit's query parser).
|
|
9
|
-
*
|
|
10
|
-
* Existing constraints on a property always win — the merge only fills in
|
|
11
|
-
* gaps. Adapters that already walk `fieldRules` during base-schema assembly
|
|
12
|
-
* can call this helper for free (the checks are no-ops when constraints
|
|
13
|
-
* already exist).
|
|
14
|
-
*/
|
|
15
|
-
function mergeFieldRuleConstraints(schemas, schemaOptions) {
|
|
16
|
-
if (!schemas || typeof schemas !== "object") return;
|
|
17
|
-
const rules = schemaOptions?.fieldRules;
|
|
18
|
-
if (!rules || Object.keys(rules).length === 0) return;
|
|
19
|
-
for (const slot of [
|
|
20
|
-
"createBody",
|
|
21
|
-
"updateBody",
|
|
22
|
-
"response"
|
|
23
|
-
]) {
|
|
24
|
-
const slotSchema = schemas[slot];
|
|
25
|
-
if (!slotSchema || typeof slotSchema !== "object") continue;
|
|
26
|
-
const properties = slotSchema.properties;
|
|
27
|
-
if (!properties) continue;
|
|
28
|
-
for (const [field, rule] of Object.entries(rules)) {
|
|
29
|
-
const prop = properties[field];
|
|
30
|
-
if (!prop || typeof prop !== "object") continue;
|
|
31
|
-
if (rule.minLength != null && prop.minLength == null) prop.minLength = rule.minLength;
|
|
32
|
-
if (rule.maxLength != null && prop.maxLength == null) prop.maxLength = rule.maxLength;
|
|
33
|
-
if (rule.min != null && prop.minimum == null) prop.minimum = rule.min;
|
|
34
|
-
if (rule.max != null && prop.maximum == null) prop.maximum = rule.max;
|
|
35
|
-
if (rule.pattern != null && prop.pattern == null) prop.pattern = rule.pattern;
|
|
36
|
-
if (rule.enum != null && prop.enum == null) prop.enum = rule.enum;
|
|
37
|
-
if (rule.description != null && prop.description == null) prop.description = rule.description;
|
|
38
|
-
if (rule.nullable === true) applyNullable(prop);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Widen a JSON Schema property to also accept `null`.
|
|
44
|
-
*
|
|
45
|
-
* Handles the three ways a property can be typed:
|
|
46
|
-
* - `type: 'string'` → `type: ['string', 'null']`
|
|
47
|
-
* - `type: [...]` → append `'null'` if missing
|
|
48
|
-
* - `anyOf: [...]` → append `{ type: 'null' }` branch if missing
|
|
49
|
-
*
|
|
50
|
-
* **Enum interaction:** when the widened prop also carries `enum: [...]`,
|
|
51
|
-
* `null` is appended to the enum list too. AJV's `enum` keyword rejects
|
|
52
|
-
* values not in the list regardless of the widened `type`, so
|
|
53
|
-
* `{ type: ['string','null'], enum: ['a','b'] }` alone would still reject
|
|
54
|
-
* `null`. The fix is `enum: ['a','b', null]`. (The `anyOf` branch dodges
|
|
55
|
-
* this entirely — each branch scopes its own enum.)
|
|
56
|
-
*
|
|
57
|
-
* No-op when the schema already admits null (don't double-wrap) or has
|
|
58
|
-
* no `type` / `anyOf` anchor to widen (e.g. Mixed — already accepts null).
|
|
59
|
-
*
|
|
60
|
-
* Mutates in place — callers already treat the slot schema as owned.
|
|
61
|
-
* Exported so adapters that walk `fieldRules` inline (mongoose fallback,
|
|
62
|
-
* drizzle post-process) can reuse the same widening logic.
|
|
63
|
-
*/
|
|
64
|
-
function applyNullable(prop) {
|
|
65
|
-
if (Array.isArray(prop.anyOf)) {
|
|
66
|
-
if (!prop.anyOf.some((b) => b && typeof b === "object" && (b.type === "null" || b.const === null))) prop.anyOf.push({ type: "null" });
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
if (Array.isArray(prop.type)) {
|
|
70
|
-
if (!prop.type.includes("null")) prop.type.push("null");
|
|
71
|
-
widenEnumToIncludeNull(prop);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (typeof prop.type === "string") {
|
|
75
|
-
prop.type = [prop.type, "null"];
|
|
76
|
-
widenEnumToIncludeNull(prop);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Append `null` to `enum` when present. Required because AJV's `enum`
|
|
82
|
-
* keyword is independent of `type` — a value must appear in the enum
|
|
83
|
-
* array verbatim even if the widened type says null is allowed.
|
|
84
|
-
*/
|
|
85
|
-
function widenEnumToIncludeNull(prop) {
|
|
86
|
-
if (!Array.isArray(prop.enum)) return;
|
|
87
|
-
if (prop.enum.includes(null)) return;
|
|
88
|
-
prop.enum = [...prop.enum, null];
|
|
89
|
-
}
|
|
90
|
-
//#endregion
|
|
91
|
-
//#region src/adapters/types.ts
|
|
92
|
-
/**
|
|
93
|
-
* Check if value is a Mongoose model
|
|
94
|
-
*/
|
|
95
|
-
function isMongooseModel(value) {
|
|
96
|
-
return typeof value === "function" && value.prototype && "modelName" in value && "schema" in value;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Check if value is a repository
|
|
100
|
-
*/
|
|
101
|
-
function isRepository(value) {
|
|
102
|
-
return typeof value === "object" && value !== null && "getAll" in value && "getById" in value && "create" in value && "update" in value && "delete" in value;
|
|
103
|
-
}
|
|
104
|
-
//#endregion
|
|
105
|
-
//#region src/adapters/drizzle.ts
|
|
106
|
-
const DRIZZLE_COLUMNS_SYMBOL = Symbol.for("drizzle:Columns");
|
|
107
|
-
function getColumns(table) {
|
|
108
|
-
const cols = table[DRIZZLE_COLUMNS_SYMBOL];
|
|
109
|
-
if (!cols || typeof cols !== "object") return {};
|
|
110
|
-
return cols;
|
|
111
|
-
}
|
|
112
|
-
function columnToJsonSchema(column) {
|
|
113
|
-
const { dataType, columnType, enumValues, length } = column;
|
|
114
|
-
if (dataType === "date") return {
|
|
115
|
-
type: "string",
|
|
116
|
-
format: "date-time"
|
|
117
|
-
};
|
|
118
|
-
if (dataType === "boolean") return { type: "boolean" };
|
|
119
|
-
if (dataType === "json") return {
|
|
120
|
-
type: "object",
|
|
121
|
-
additionalProperties: true
|
|
122
|
-
};
|
|
123
|
-
if (dataType === "buffer") return {
|
|
124
|
-
type: "string",
|
|
125
|
-
contentEncoding: "base64"
|
|
126
|
-
};
|
|
127
|
-
if (dataType === "number" || dataType === "bigint") return { type: columnType === "SQLiteInteger" ? "integer" : "number" };
|
|
128
|
-
if (dataType === "string") {
|
|
129
|
-
const result = { type: "string" };
|
|
130
|
-
if (Array.isArray(enumValues) && enumValues.length > 0) result.enum = [...enumValues];
|
|
131
|
-
if (typeof length === "number" && length > 0) result.maxLength = length;
|
|
132
|
-
return result;
|
|
133
|
-
}
|
|
134
|
-
return {};
|
|
135
|
-
}
|
|
136
|
-
function columnToFieldMetadata(column) {
|
|
137
|
-
const { dataType, enumValues } = column;
|
|
138
|
-
const meta = {
|
|
139
|
-
type: (dataType && {
|
|
140
|
-
number: "number",
|
|
141
|
-
bigint: "number",
|
|
142
|
-
string: "string",
|
|
143
|
-
date: "date",
|
|
144
|
-
boolean: "boolean",
|
|
145
|
-
json: "object",
|
|
146
|
-
buffer: "object"
|
|
147
|
-
}[dataType]) ?? (enumValues?.length ? "enum" : "object"),
|
|
148
|
-
required: !!column.notNull && !column.hasDefault
|
|
149
|
-
};
|
|
150
|
-
if (enumValues?.length) meta.enum = [...enumValues];
|
|
151
|
-
if (typeof column.length === "number") meta.maxLength = column.length;
|
|
152
|
-
return meta;
|
|
153
|
-
}
|
|
154
|
-
var DrizzleAdapter = class {
|
|
155
|
-
type = "drizzle";
|
|
156
|
-
name;
|
|
157
|
-
table;
|
|
158
|
-
repository;
|
|
159
|
-
schemaGenerator;
|
|
160
|
-
constructor(options) {
|
|
161
|
-
if (!options.table || typeof options.table !== "object") throw new TypeError("DrizzleAdapter: Invalid table. Expected a Drizzle table created with sqliteTable / pgTable / mysqlTable.");
|
|
162
|
-
if (!isRepository(options.repository)) throw new TypeError("DrizzleAdapter: Invalid repository. Expected an object implementing MinimalRepo (getAll / getById / create / update / delete).");
|
|
163
|
-
this.table = options.table;
|
|
164
|
-
this.repository = options.repository;
|
|
165
|
-
this.schemaGenerator = options.schemaGenerator;
|
|
166
|
-
this.name = options.name ?? "DrizzleAdapter";
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* Introspect Drizzle columns into arc's schema metadata shape.
|
|
170
|
-
*/
|
|
171
|
-
getSchemaMetadata() {
|
|
172
|
-
const columns = getColumns(this.table);
|
|
173
|
-
const fields = {};
|
|
174
|
-
const indexes = [];
|
|
175
|
-
for (const [name, column] of Object.entries(columns)) {
|
|
176
|
-
fields[name] = columnToFieldMetadata(column);
|
|
177
|
-
if (column.primary) indexes.push({
|
|
178
|
-
fields: [name],
|
|
179
|
-
unique: true
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
return {
|
|
183
|
-
name: this.name,
|
|
184
|
-
fields,
|
|
185
|
-
...indexes.length > 0 ? { indexes } : {}
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Generate OpenAPI schemas. Delegates to the user-provided
|
|
190
|
-
* `schemaGenerator` when available (strongly recommended — that's where
|
|
191
|
-
* field rules, omit lists, and param-type narrowing live). The built-in
|
|
192
|
-
* fallback emits a permissive entity + CRUD body shape so routes still
|
|
193
|
-
* register when no generator is provided.
|
|
194
|
-
*
|
|
195
|
-
* After the kit generator runs, arc merges constraint-style field rules
|
|
196
|
-
* (`minLength`, `maxLength`, `min`, `max`, `pattern`, `enum`, `description`)
|
|
197
|
-
* into the resulting property schemas so sqlitekit / pgkit behave
|
|
198
|
-
* identically to mongoose here — rule-driven AJV constraints apply
|
|
199
|
-
* regardless of backend.
|
|
200
|
-
*/
|
|
201
|
-
generateSchemas(schemaOptions, context) {
|
|
202
|
-
try {
|
|
203
|
-
if (this.schemaGenerator) {
|
|
204
|
-
const generated = this.schemaGenerator(this.table, schemaOptions, context);
|
|
205
|
-
mergeFieldRuleConstraints(generated, schemaOptions);
|
|
206
|
-
return generated;
|
|
207
|
-
}
|
|
208
|
-
const columns = getColumns(this.table);
|
|
209
|
-
if (Object.keys(columns).length === 0) return null;
|
|
210
|
-
const entityProperties = {};
|
|
211
|
-
const inputProperties = {};
|
|
212
|
-
const inputRequired = [];
|
|
213
|
-
const updateProperties = {};
|
|
214
|
-
const fieldRules = schemaOptions?.fieldRules ?? {};
|
|
215
|
-
const readonlySet = new Set(schemaOptions?.readonlyFields ?? []);
|
|
216
|
-
const optionalSet = new Set(schemaOptions?.optionalFields ?? []);
|
|
217
|
-
const blocked = new Set([
|
|
218
|
-
...Object.entries(fieldRules).filter(([, rules]) => rules.systemManaged || rules.hidden).map(([field]) => field),
|
|
219
|
-
...schemaOptions?.excludeFields ?? [],
|
|
220
|
-
...schemaOptions?.hiddenFields ?? []
|
|
221
|
-
]);
|
|
222
|
-
for (const [fieldName, column] of Object.entries(columns)) {
|
|
223
|
-
const schema = columnToJsonSchema(column);
|
|
224
|
-
entityProperties[fieldName] = schema;
|
|
225
|
-
if (blocked.has(fieldName)) continue;
|
|
226
|
-
if (column.primary && column.columnType === "SQLiteInteger") continue;
|
|
227
|
-
if (!readonlySet.has(fieldName)) {
|
|
228
|
-
inputProperties[fieldName] = schema;
|
|
229
|
-
if (!!column.notNull && !column.hasDefault && !optionalSet.has(fieldName)) inputRequired.push(fieldName);
|
|
230
|
-
updateProperties[fieldName] = schema;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return {
|
|
234
|
-
createBody: {
|
|
235
|
-
type: "object",
|
|
236
|
-
properties: inputProperties,
|
|
237
|
-
required: inputRequired.length > 0 ? inputRequired : void 0,
|
|
238
|
-
additionalProperties: true
|
|
239
|
-
},
|
|
240
|
-
updateBody: {
|
|
241
|
-
type: "object",
|
|
242
|
-
properties: updateProperties,
|
|
243
|
-
additionalProperties: true
|
|
244
|
-
},
|
|
245
|
-
response: {
|
|
246
|
-
type: "object",
|
|
247
|
-
properties: entityProperties,
|
|
248
|
-
additionalProperties: true
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
} catch {
|
|
252
|
-
return null;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
async healthCheck() {
|
|
256
|
-
return typeof this.repository.getAll === "function";
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
/**
|
|
260
|
-
* Factory — preferred construction style for symmetry with
|
|
261
|
-
* `createMongooseAdapter` / `createPrismaAdapter`.
|
|
262
|
-
*/
|
|
263
|
-
function createDrizzleAdapter(options) {
|
|
264
|
-
return new DrizzleAdapter(options);
|
|
265
|
-
}
|
|
266
|
-
//#endregion
|
|
267
|
-
//#region src/adapters/mongoose.ts
|
|
268
|
-
/**
|
|
269
|
-
* Mongoose data adapter with proper type safety
|
|
270
|
-
*
|
|
271
|
-
* @typeParam TDoc - The document type
|
|
272
|
-
*/
|
|
273
|
-
var MongooseAdapter = class {
|
|
274
|
-
type = "mongoose";
|
|
275
|
-
name;
|
|
276
|
-
model;
|
|
277
|
-
repository;
|
|
278
|
-
schemaGenerator;
|
|
279
|
-
constructor(options) {
|
|
280
|
-
if (!isMongooseModel(options.model)) throw new TypeError("MongooseAdapter: Invalid model. Expected Mongoose Model instance.\nUsage: createMongooseAdapter({ model: YourModel, repository: yourRepo })");
|
|
281
|
-
if (!isRepository(options.repository)) throw new TypeError("MongooseAdapter: Invalid repository. Expected StandardRepo instance.\nUsage: createMongooseAdapter({ model: YourModel, repository: yourRepo })");
|
|
282
|
-
this.model = options.model;
|
|
283
|
-
this.repository = options.repository;
|
|
284
|
-
this.schemaGenerator = options.schemaGenerator;
|
|
285
|
-
this.name = `MongooseAdapter<${options.model.modelName}>`;
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Get schema metadata from Mongoose model
|
|
289
|
-
*/
|
|
290
|
-
getSchemaMetadata() {
|
|
291
|
-
const paths = this.model.schema.paths;
|
|
292
|
-
const fields = {};
|
|
293
|
-
for (const [fieldName, schemaType] of Object.entries(paths)) {
|
|
294
|
-
if (fieldName.startsWith("_") && fieldName !== "_id") continue;
|
|
295
|
-
const typeInfo = schemaType;
|
|
296
|
-
fields[fieldName] = {
|
|
297
|
-
type: {
|
|
298
|
-
String: "string",
|
|
299
|
-
Number: "number",
|
|
300
|
-
Boolean: "boolean",
|
|
301
|
-
Date: "date",
|
|
302
|
-
ObjectID: "objectId",
|
|
303
|
-
ObjectId: "objectId",
|
|
304
|
-
Array: "array",
|
|
305
|
-
Mixed: "object",
|
|
306
|
-
Buffer: "object",
|
|
307
|
-
Embedded: "object"
|
|
308
|
-
}[typeInfo.instance || "Mixed"] ?? "object",
|
|
309
|
-
required: !!typeInfo.isRequired,
|
|
310
|
-
ref: typeInfo.options?.ref
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
return {
|
|
314
|
-
name: this.model.modelName,
|
|
315
|
-
fields,
|
|
316
|
-
relations: this.extractRelations(paths)
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Generate OpenAPI schemas from Mongoose model.
|
|
321
|
-
*
|
|
322
|
-
* If a `schemaGenerator` plugin was provided (e.g. MongoKit's buildCrudSchemasFromModel),
|
|
323
|
-
* it is used instead of the built-in basic conversion.
|
|
324
|
-
*/
|
|
325
|
-
generateSchemas(schemaOptions, context) {
|
|
326
|
-
try {
|
|
327
|
-
if (this.schemaGenerator) {
|
|
328
|
-
const generated = this.schemaGenerator(this.model, schemaOptions, context);
|
|
329
|
-
mergeFieldRuleConstraints(generated, schemaOptions);
|
|
330
|
-
return generated;
|
|
331
|
-
}
|
|
332
|
-
const paths = this.model.schema.paths;
|
|
333
|
-
const properties = {};
|
|
334
|
-
const required = [];
|
|
335
|
-
const fieldRules = schemaOptions?.fieldRules || {};
|
|
336
|
-
const blockedFields = new Set([
|
|
337
|
-
...Object.entries(fieldRules).filter(([, rules]) => rules.systemManaged || rules.hidden).map(([field]) => field),
|
|
338
|
-
...schemaOptions?.excludeFields ?? [],
|
|
339
|
-
...schemaOptions?.hiddenFields ?? []
|
|
340
|
-
]);
|
|
341
|
-
const readonlySet = new Set(schemaOptions?.readonlyFields ?? []);
|
|
342
|
-
const optionalSet = new Set(schemaOptions?.optionalFields ?? []);
|
|
343
|
-
for (const [fieldName, schemaType] of Object.entries(paths)) {
|
|
344
|
-
if (fieldName.startsWith("__")) continue;
|
|
345
|
-
if (blockedFields.has(fieldName)) continue;
|
|
346
|
-
const typeInfo = schemaType;
|
|
347
|
-
properties[fieldName] = this.mongooseTypeToOpenApi(typeInfo);
|
|
348
|
-
const rule = fieldRules[fieldName];
|
|
349
|
-
if (rule) {
|
|
350
|
-
const prop = properties[fieldName];
|
|
351
|
-
if (rule.minLength != null && prop.minLength == null) prop.minLength = rule.minLength;
|
|
352
|
-
if (rule.maxLength != null && prop.maxLength == null) prop.maxLength = rule.maxLength;
|
|
353
|
-
if (rule.min != null && prop.minimum == null) prop.minimum = rule.min;
|
|
354
|
-
if (rule.max != null && prop.maximum == null) prop.maximum = rule.max;
|
|
355
|
-
if (rule.pattern != null && prop.pattern == null) prop.pattern = rule.pattern;
|
|
356
|
-
if (rule.enum != null && prop.enum == null) prop.enum = rule.enum;
|
|
357
|
-
if (rule.description != null && prop.description == null) prop.description = rule.description;
|
|
358
|
-
if (rule.nullable === true) applyNullable(prop);
|
|
359
|
-
}
|
|
360
|
-
if (typeInfo.isRequired && !optionalSet.has(fieldName) && !fieldRules[fieldName]?.optional) required.push(fieldName);
|
|
361
|
-
}
|
|
362
|
-
const readonlyForInput = new Set([...readonlySet]);
|
|
363
|
-
for (const [field, rules] of Object.entries(fieldRules)) if (rules.immutable || rules.immutableAfterCreate) readonlyForInput.add(field);
|
|
364
|
-
const inputBlockedSet = new Set([...SYSTEM_FIELDS, ...readonlyForInput]);
|
|
365
|
-
const inputProperties = Object.fromEntries(Object.entries(properties).filter(([field]) => !inputBlockedSet.has(field)));
|
|
366
|
-
const inputRequired = required.filter((field) => !inputBlockedSet.has(field) && !blockedFields.has(field));
|
|
367
|
-
const immutableSet = /* @__PURE__ */ new Set();
|
|
368
|
-
for (const [field, rules] of Object.entries(fieldRules)) if (rules.immutable || rules.immutableAfterCreate) immutableSet.add(field);
|
|
369
|
-
const updateProperties = Object.fromEntries(Object.entries(inputProperties).filter(([field]) => !immutableSet.has(field)));
|
|
370
|
-
return {
|
|
371
|
-
createBody: {
|
|
372
|
-
type: "object",
|
|
373
|
-
properties: inputProperties,
|
|
374
|
-
required: inputRequired.length > 0 ? inputRequired : void 0,
|
|
375
|
-
additionalProperties: true
|
|
376
|
-
},
|
|
377
|
-
updateBody: {
|
|
378
|
-
type: "object",
|
|
379
|
-
properties: updateProperties,
|
|
380
|
-
additionalProperties: true
|
|
381
|
-
},
|
|
382
|
-
response: {
|
|
383
|
-
type: "object",
|
|
384
|
-
properties,
|
|
385
|
-
additionalProperties: true
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
} catch {
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
/**
|
|
393
|
-
* Extract relation metadata
|
|
394
|
-
*/
|
|
395
|
-
extractRelations(paths) {
|
|
396
|
-
const relations = {};
|
|
397
|
-
for (const [fieldName, schemaType] of Object.entries(paths)) {
|
|
398
|
-
const ref = schemaType.options?.ref;
|
|
399
|
-
if (ref) relations[fieldName] = {
|
|
400
|
-
type: "one-to-one",
|
|
401
|
-
target: ref,
|
|
402
|
-
foreignKey: fieldName
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
return Object.keys(relations).length > 0 ? relations : void 0;
|
|
406
|
-
}
|
|
407
|
-
/**
|
|
408
|
-
* Convert Mongoose type to OpenAPI type
|
|
409
|
-
*/
|
|
410
|
-
mongooseTypeToOpenApi(typeInfo) {
|
|
411
|
-
const instance = typeInfo.instance;
|
|
412
|
-
const options = typeInfo.options || {};
|
|
413
|
-
const baseType = {};
|
|
414
|
-
switch (instance) {
|
|
415
|
-
case "String":
|
|
416
|
-
baseType.type = "string";
|
|
417
|
-
if (options.enum) baseType.enum = options.enum;
|
|
418
|
-
if (options.minlength) baseType.minLength = options.minlength;
|
|
419
|
-
if (options.maxlength) baseType.maxLength = options.maxlength;
|
|
420
|
-
break;
|
|
421
|
-
case "Number":
|
|
422
|
-
baseType.type = "number";
|
|
423
|
-
if (options.min !== void 0) baseType.minimum = options.min;
|
|
424
|
-
if (options.max !== void 0) baseType.maximum = options.max;
|
|
425
|
-
break;
|
|
426
|
-
case "Boolean":
|
|
427
|
-
baseType.type = "boolean";
|
|
428
|
-
break;
|
|
429
|
-
case "Date":
|
|
430
|
-
baseType.type = "string";
|
|
431
|
-
break;
|
|
432
|
-
case "ObjectID":
|
|
433
|
-
case "ObjectId":
|
|
434
|
-
baseType.type = "string";
|
|
435
|
-
baseType.pattern = "^[a-f\\d]{24}$";
|
|
436
|
-
break;
|
|
437
|
-
case "Array": {
|
|
438
|
-
baseType.type = "array";
|
|
439
|
-
const ti = typeInfo;
|
|
440
|
-
if (ti.$isMongooseDocumentArray && ti.schema) {
|
|
441
|
-
const subSchema = ti.schema;
|
|
442
|
-
const subProps = {};
|
|
443
|
-
const subRequired = [];
|
|
444
|
-
for (const [subField, subType] of Object.entries(subSchema.paths)) {
|
|
445
|
-
if (subField.startsWith("_")) continue;
|
|
446
|
-
subProps[subField] = this.mongooseTypeToOpenApi(subType);
|
|
447
|
-
if (subType.isRequired) subRequired.push(subField);
|
|
448
|
-
}
|
|
449
|
-
baseType.items = {
|
|
450
|
-
type: "object",
|
|
451
|
-
properties: subProps,
|
|
452
|
-
...subRequired.length > 0 ? { required: subRequired } : {},
|
|
453
|
-
additionalProperties: true
|
|
454
|
-
};
|
|
455
|
-
} else if (ti.embeddedSchemaType?.instance) baseType.items = this.mongooseTypeToOpenApi(ti.embeddedSchemaType);
|
|
456
|
-
else baseType.items = {};
|
|
457
|
-
break;
|
|
458
|
-
}
|
|
459
|
-
case "Mixed": break;
|
|
460
|
-
case "Map":
|
|
461
|
-
baseType.type = "object";
|
|
462
|
-
baseType.additionalProperties = true;
|
|
463
|
-
break;
|
|
464
|
-
case "Embedded":
|
|
465
|
-
case "SubDocument":
|
|
466
|
-
baseType.type = "object";
|
|
467
|
-
baseType.additionalProperties = true;
|
|
468
|
-
break;
|
|
469
|
-
case "Buffer":
|
|
470
|
-
baseType.type = "string";
|
|
471
|
-
baseType.format = "binary";
|
|
472
|
-
break;
|
|
473
|
-
case "Decimal128":
|
|
474
|
-
baseType.type = "string";
|
|
475
|
-
baseType.description = "Decimal128 (high-precision number as string)";
|
|
476
|
-
break;
|
|
477
|
-
case "UUID":
|
|
478
|
-
baseType.type = "string";
|
|
479
|
-
baseType.format = "uuid";
|
|
480
|
-
break;
|
|
481
|
-
default:
|
|
482
|
-
baseType.type = "object";
|
|
483
|
-
baseType.additionalProperties = true;
|
|
484
|
-
}
|
|
485
|
-
if (options.default === null) {
|
|
486
|
-
applyNullable(baseType);
|
|
487
|
-
baseType.default = null;
|
|
488
|
-
}
|
|
489
|
-
return baseType;
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
function createMongooseAdapter(modelOrOptions, repository) {
|
|
493
|
-
if (isMongooseModel(modelOrOptions)) {
|
|
494
|
-
if (!repository) throw new TypeError("createMongooseAdapter: repository is required when using 2-arg form.\nUsage: createMongooseAdapter(Model, repository)");
|
|
495
|
-
return new MongooseAdapter({
|
|
496
|
-
model: modelOrOptions,
|
|
497
|
-
repository
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
return new MongooseAdapter(modelOrOptions);
|
|
501
|
-
}
|
|
502
|
-
//#endregion
|
|
503
|
-
//#region src/adapters/prisma.ts
|
|
504
|
-
/**
|
|
505
|
-
* Prisma Query Parser - Converts URL parameters to Prisma query format
|
|
506
|
-
*
|
|
507
|
-
* Translates Arc's query format to Prisma's where/orderBy/take/skip structure.
|
|
508
|
-
*
|
|
509
|
-
* @example
|
|
510
|
-
* ```typescript
|
|
511
|
-
* const parser = new PrismaQueryParser();
|
|
512
|
-
*
|
|
513
|
-
* // URL: ?status=active&price[gte]=100&sort=-createdAt&page=2&limit=10
|
|
514
|
-
* const prismaQuery = parser.toPrismaQuery(parsedQuery);
|
|
515
|
-
* // Returns:
|
|
516
|
-
* // {
|
|
517
|
-
* // where: { status: 'active', price: { gte: 100 }, deletedAt: null },
|
|
518
|
-
* // orderBy: { createdAt: 'desc' },
|
|
519
|
-
* // take: 10,
|
|
520
|
-
* // skip: 10,
|
|
521
|
-
* // }
|
|
522
|
-
* ```
|
|
523
|
-
*/
|
|
524
|
-
var PrismaQueryParser = class {
|
|
525
|
-
maxLimit;
|
|
526
|
-
defaultLimit;
|
|
527
|
-
softDeleteEnabled;
|
|
528
|
-
softDeleteField;
|
|
529
|
-
/** Map Arc operators to Prisma operators */
|
|
530
|
-
operatorMap = {
|
|
531
|
-
$eq: "equals",
|
|
532
|
-
$ne: "not",
|
|
533
|
-
$gt: "gt",
|
|
534
|
-
$gte: "gte",
|
|
535
|
-
$lt: "lt",
|
|
536
|
-
$lte: "lte",
|
|
537
|
-
$in: "in",
|
|
538
|
-
$nin: "notIn",
|
|
539
|
-
$regex: "contains",
|
|
540
|
-
$exists: void 0
|
|
541
|
-
};
|
|
542
|
-
constructor(options = {}) {
|
|
543
|
-
this.maxLimit = options.maxLimit ?? 1e3;
|
|
544
|
-
this.defaultLimit = options.defaultLimit ?? 20;
|
|
545
|
-
this.softDeleteEnabled = options.softDeleteEnabled ?? true;
|
|
546
|
-
this.softDeleteField = options.softDeleteField ?? "deletedAt";
|
|
547
|
-
}
|
|
548
|
-
/**
|
|
549
|
-
* Parse URL query parameters (delegates to ArcQueryParser format)
|
|
550
|
-
*/
|
|
551
|
-
parse(query) {
|
|
552
|
-
const q = query ?? {};
|
|
553
|
-
const page = this.parseNumber(q.page, 1);
|
|
554
|
-
const limit = Math.min(this.parseNumber(q.limit, this.defaultLimit), this.maxLimit);
|
|
555
|
-
return {
|
|
556
|
-
filters: this.parseFilters(q),
|
|
557
|
-
limit,
|
|
558
|
-
page,
|
|
559
|
-
sort: this.parseSort(q.sort),
|
|
560
|
-
search: q.search,
|
|
561
|
-
select: this.parseSelect(q.select)
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
/**
|
|
565
|
-
* Convert ParsedQuery to Prisma query options
|
|
566
|
-
*/
|
|
567
|
-
toPrismaQuery(parsed, policyFilters) {
|
|
568
|
-
const where = {};
|
|
569
|
-
if (parsed.filters) Object.assign(where, this.translateFilters(parsed.filters));
|
|
570
|
-
if (policyFilters) Object.assign(where, this.translateFilters(policyFilters));
|
|
571
|
-
if (this.softDeleteEnabled) where[this.softDeleteField] = null;
|
|
572
|
-
const orderBy = parsed.sort ? Object.entries(parsed.sort).map(([field, dir]) => ({ [field]: dir === 1 ? "asc" : "desc" })) : void 0;
|
|
573
|
-
const take = parsed.limit ?? this.defaultLimit;
|
|
574
|
-
const skip = parsed.page ? (parsed.page - 1) * take : 0;
|
|
575
|
-
const select = parsed.select ? Object.fromEntries(Object.entries(parsed.select).filter(([, v]) => v === 1).map(([k]) => [k, true])) : void 0;
|
|
576
|
-
return {
|
|
577
|
-
where: Object.keys(where).length > 0 ? where : void 0,
|
|
578
|
-
orderBy: orderBy && orderBy.length > 0 ? orderBy : void 0,
|
|
579
|
-
take,
|
|
580
|
-
skip,
|
|
581
|
-
select: select && Object.keys(select).length > 0 ? select : void 0
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Translate Arc/MongoDB-style filters to Prisma where clause
|
|
586
|
-
*/
|
|
587
|
-
translateFilters(filters) {
|
|
588
|
-
const result = {};
|
|
589
|
-
for (const [field, value] of Object.entries(filters)) {
|
|
590
|
-
if (value === null || value === void 0) continue;
|
|
591
|
-
if (typeof value === "object" && !Array.isArray(value)) {
|
|
592
|
-
const prismaCondition = {};
|
|
593
|
-
for (const [op, opValue] of Object.entries(value)) {
|
|
594
|
-
if (op === "$exists") {
|
|
595
|
-
result[field] = opValue ? { not: null } : null;
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
const prismaOp = this.operatorMap[op];
|
|
599
|
-
if (prismaOp) prismaCondition[prismaOp] = opValue;
|
|
600
|
-
}
|
|
601
|
-
if (Object.keys(prismaCondition).length > 0) result[field] = prismaCondition;
|
|
602
|
-
} else result[field] = value;
|
|
603
|
-
}
|
|
604
|
-
return result;
|
|
605
|
-
}
|
|
606
|
-
parseNumber(value, defaultValue) {
|
|
607
|
-
if (value === void 0 || value === null) return defaultValue;
|
|
608
|
-
const num = parseInt(String(value), 10);
|
|
609
|
-
return Number.isNaN(num) ? defaultValue : Math.max(1, num);
|
|
610
|
-
}
|
|
611
|
-
parseSort(value) {
|
|
612
|
-
if (!value) return void 0;
|
|
613
|
-
const sortStr = String(value);
|
|
614
|
-
const result = {};
|
|
615
|
-
for (const field of sortStr.split(",")) {
|
|
616
|
-
const trimmed = field.trim();
|
|
617
|
-
if (!trimmed || !/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) continue;
|
|
618
|
-
if (trimmed.startsWith("-")) result[trimmed.slice(1)] = -1;
|
|
619
|
-
else result[trimmed] = 1;
|
|
620
|
-
}
|
|
621
|
-
return Object.keys(result).length > 0 ? result : void 0;
|
|
622
|
-
}
|
|
623
|
-
parseSelect(value) {
|
|
624
|
-
if (!value) return void 0;
|
|
625
|
-
const result = {};
|
|
626
|
-
for (const field of String(value).split(",")) {
|
|
627
|
-
const trimmed = field.trim();
|
|
628
|
-
if (!trimmed || !/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) continue;
|
|
629
|
-
result[trimmed.startsWith("-") ? trimmed.slice(1) : trimmed] = trimmed.startsWith("-") ? 0 : 1;
|
|
630
|
-
}
|
|
631
|
-
return Object.keys(result).length > 0 ? result : void 0;
|
|
632
|
-
}
|
|
633
|
-
parseFilters(query) {
|
|
634
|
-
const filters = {};
|
|
635
|
-
const operators = {
|
|
636
|
-
eq: "$eq",
|
|
637
|
-
ne: "$ne",
|
|
638
|
-
gt: "$gt",
|
|
639
|
-
gte: "$gte",
|
|
640
|
-
lt: "$lt",
|
|
641
|
-
lte: "$lte",
|
|
642
|
-
in: "$in",
|
|
643
|
-
nin: "$nin",
|
|
644
|
-
like: "$regex",
|
|
645
|
-
contains: "$regex",
|
|
646
|
-
exists: "$exists"
|
|
647
|
-
};
|
|
648
|
-
for (const [key, value] of Object.entries(query)) {
|
|
649
|
-
if (RESERVED_QUERY_PARAMS.has(key) || value === void 0 || value === null) continue;
|
|
650
|
-
const match = key.match(/^([a-zA-Z_][a-zA-Z0-9_.]*)(?:\[([a-z]+)\])?$/);
|
|
651
|
-
if (!match) continue;
|
|
652
|
-
const [, fieldName, operator] = match;
|
|
653
|
-
if (!fieldName) continue;
|
|
654
|
-
if (operator && operators[operator]) {
|
|
655
|
-
if (!filters[fieldName]) filters[fieldName] = {};
|
|
656
|
-
filters[fieldName][operators[operator]] = this.coerceValue(value, operator);
|
|
657
|
-
} else if (!operator) filters[fieldName] = this.coerceValue(value);
|
|
658
|
-
}
|
|
659
|
-
return filters;
|
|
660
|
-
}
|
|
661
|
-
coerceValue(value, operator) {
|
|
662
|
-
if (operator === "in" || operator === "nin") {
|
|
663
|
-
if (Array.isArray(value)) return value.map((v) => this.coerceValue(v));
|
|
664
|
-
if (typeof value === "string" && value.includes(",")) return value.split(",").map((v) => this.coerceValue(v.trim()));
|
|
665
|
-
return [this.coerceValue(value)];
|
|
666
|
-
}
|
|
667
|
-
if (operator === "exists") return String(value).toLowerCase() === "true" || value === "1";
|
|
668
|
-
if (value === "true") return true;
|
|
669
|
-
if (value === "false") return false;
|
|
670
|
-
if (value === "null") return null;
|
|
671
|
-
if (typeof value === "string") {
|
|
672
|
-
const num = Number(value);
|
|
673
|
-
if (!Number.isNaN(num) && value.trim() !== "") return num;
|
|
674
|
-
}
|
|
675
|
-
return value;
|
|
676
|
-
}
|
|
677
|
-
};
|
|
678
|
-
var PrismaAdapter = class {
|
|
679
|
-
type = "prisma";
|
|
680
|
-
name;
|
|
681
|
-
repository;
|
|
682
|
-
queryParser;
|
|
683
|
-
client;
|
|
684
|
-
modelName;
|
|
685
|
-
dmmf;
|
|
686
|
-
softDeleteEnabled;
|
|
687
|
-
softDeleteField;
|
|
688
|
-
constructor(options) {
|
|
689
|
-
this.client = options.client;
|
|
690
|
-
this.modelName = options.modelName;
|
|
691
|
-
this.repository = options.repository;
|
|
692
|
-
this.dmmf = options.dmmf;
|
|
693
|
-
this.name = `prisma:${options.modelName}`;
|
|
694
|
-
this.softDeleteEnabled = options.softDeleteEnabled ?? true;
|
|
695
|
-
this.softDeleteField = options.softDeleteField ?? "deletedAt";
|
|
696
|
-
this.queryParser = options.queryParser ?? new PrismaQueryParser({
|
|
697
|
-
softDeleteEnabled: this.softDeleteEnabled,
|
|
698
|
-
softDeleteField: this.softDeleteField
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Parse URL query parameters and convert to Prisma query options
|
|
703
|
-
*/
|
|
704
|
-
parseQuery(query, policyFilters) {
|
|
705
|
-
const parsed = this.queryParser.parse(query);
|
|
706
|
-
return this.queryParser.toPrismaQuery(parsed, policyFilters);
|
|
707
|
-
}
|
|
708
|
-
/**
|
|
709
|
-
* Apply policy filters to existing Prisma where clause
|
|
710
|
-
* Used for multi-tenant, ownership, and other security filters
|
|
711
|
-
*/
|
|
712
|
-
applyPolicyFilters(where, policyFilters) {
|
|
713
|
-
return {
|
|
714
|
-
...where,
|
|
715
|
-
...policyFilters
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
generateSchemas(options) {
|
|
719
|
-
if (!this.dmmf) return null;
|
|
720
|
-
try {
|
|
721
|
-
const model = this.dmmf.datamodel?.models?.find((m) => m.name.toLowerCase() === this.modelName.toLowerCase());
|
|
722
|
-
if (!model) return null;
|
|
723
|
-
return {
|
|
724
|
-
entity: this.buildEntitySchema(model, options),
|
|
725
|
-
createBody: this.buildCreateSchema(model, options),
|
|
726
|
-
updateBody: this.buildUpdateSchema(model, options),
|
|
727
|
-
params: {
|
|
728
|
-
type: "object",
|
|
729
|
-
properties: { id: { type: "string" } },
|
|
730
|
-
required: ["id"]
|
|
731
|
-
},
|
|
732
|
-
listQuery: {
|
|
733
|
-
type: "object",
|
|
734
|
-
properties: {
|
|
735
|
-
page: {
|
|
736
|
-
type: "number",
|
|
737
|
-
minimum: 1,
|
|
738
|
-
description: "Page number for pagination"
|
|
739
|
-
},
|
|
740
|
-
limit: {
|
|
741
|
-
type: "number",
|
|
742
|
-
minimum: 1,
|
|
743
|
-
maximum: 100,
|
|
744
|
-
description: "Items per page"
|
|
745
|
-
},
|
|
746
|
-
sort: {
|
|
747
|
-
type: "string",
|
|
748
|
-
description: "Sort field (e.g., \"name\", \"-createdAt\")"
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
};
|
|
753
|
-
} catch {
|
|
754
|
-
return null;
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
getSchemaMetadata() {
|
|
758
|
-
if (!this.dmmf) return null;
|
|
759
|
-
try {
|
|
760
|
-
const model = this.dmmf.datamodel?.models?.find((m) => m.name.toLowerCase() === this.modelName.toLowerCase());
|
|
761
|
-
if (!model) return null;
|
|
762
|
-
const fields = {};
|
|
763
|
-
for (const field of model.fields) fields[field.name] = this.convertPrismaFieldToMetadata(field);
|
|
764
|
-
return {
|
|
765
|
-
name: model.name,
|
|
766
|
-
fields,
|
|
767
|
-
indexes: model.uniqueIndexes?.map((idx) => ({
|
|
768
|
-
fields: idx.fields,
|
|
769
|
-
unique: true
|
|
770
|
-
}))
|
|
771
|
-
};
|
|
772
|
-
} catch (_err) {
|
|
773
|
-
return null;
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
async validate(data) {
|
|
777
|
-
if (!data || typeof data !== "object") return {
|
|
778
|
-
valid: false,
|
|
779
|
-
errors: [{
|
|
780
|
-
field: "root",
|
|
781
|
-
message: "Data must be an object"
|
|
782
|
-
}]
|
|
783
|
-
};
|
|
784
|
-
if (this.dmmf) try {
|
|
785
|
-
const model = this.dmmf.datamodel?.models?.find((m) => m.name.toLowerCase() === this.modelName.toLowerCase());
|
|
786
|
-
if (model) {
|
|
787
|
-
const requiredFields = model.fields.filter((f) => f.isRequired && !f.hasDefaultValue && !f.isGenerated);
|
|
788
|
-
const errors = [];
|
|
789
|
-
for (const field of requiredFields) if (!(field.name in data)) errors.push({
|
|
790
|
-
field: field.name,
|
|
791
|
-
message: `${field.name} is required`
|
|
792
|
-
});
|
|
793
|
-
if (errors.length > 0) return {
|
|
794
|
-
valid: false,
|
|
795
|
-
errors
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
} catch (_err) {}
|
|
799
|
-
return { valid: true };
|
|
800
|
-
}
|
|
801
|
-
async healthCheck() {
|
|
802
|
-
try {
|
|
803
|
-
const delegateName = this.modelName.charAt(0).toLowerCase() + this.modelName.slice(1);
|
|
804
|
-
const delegate = this.client[delegateName];
|
|
805
|
-
if (!delegate) return false;
|
|
806
|
-
await delegate.findMany({ take: 1 });
|
|
807
|
-
return true;
|
|
808
|
-
} catch (_err) {
|
|
809
|
-
return false;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
async close() {
|
|
813
|
-
try {
|
|
814
|
-
await this.client.$disconnect();
|
|
815
|
-
} catch (_err) {}
|
|
816
|
-
}
|
|
817
|
-
buildEntitySchema(model, options) {
|
|
818
|
-
const properties = {};
|
|
819
|
-
const required = [];
|
|
820
|
-
for (const field of model.fields) {
|
|
821
|
-
if (this.shouldSkipField(field, options)) continue;
|
|
822
|
-
properties[field.name] = this.convertPrismaFieldToJsonSchema(field);
|
|
823
|
-
if (field.isRequired && !field.hasDefaultValue) required.push(field.name);
|
|
824
|
-
}
|
|
825
|
-
return {
|
|
826
|
-
type: "object",
|
|
827
|
-
properties,
|
|
828
|
-
...required.length > 0 && { required }
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
buildCreateSchema(model, options) {
|
|
832
|
-
const properties = {};
|
|
833
|
-
const required = [];
|
|
834
|
-
for (const field of model.fields) {
|
|
835
|
-
if (field.isGenerated || field.relationName) continue;
|
|
836
|
-
if (this.shouldSkipField(field, options)) continue;
|
|
837
|
-
properties[field.name] = this.convertPrismaFieldToJsonSchema(field);
|
|
838
|
-
if (field.isRequired && !field.hasDefaultValue) required.push(field.name);
|
|
839
|
-
}
|
|
840
|
-
return {
|
|
841
|
-
type: "object",
|
|
842
|
-
properties,
|
|
843
|
-
...required.length > 0 && { required }
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
buildUpdateSchema(model, options) {
|
|
847
|
-
const properties = {};
|
|
848
|
-
for (const field of model.fields) {
|
|
849
|
-
if (field.isGenerated || field.isId || field.relationName) continue;
|
|
850
|
-
if (this.shouldSkipField(field, options)) continue;
|
|
851
|
-
properties[field.name] = this.convertPrismaFieldToJsonSchema(field);
|
|
852
|
-
}
|
|
853
|
-
return {
|
|
854
|
-
type: "object",
|
|
855
|
-
properties
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
shouldSkipField(field, options) {
|
|
859
|
-
if (options?.excludeFields?.includes(field.name)) return true;
|
|
860
|
-
if (field.name.startsWith("_")) return true;
|
|
861
|
-
return false;
|
|
862
|
-
}
|
|
863
|
-
convertPrismaFieldToJsonSchema(field) {
|
|
864
|
-
const schema = {};
|
|
865
|
-
switch (field.type) {
|
|
866
|
-
case "String":
|
|
867
|
-
schema.type = "string";
|
|
868
|
-
break;
|
|
869
|
-
case "Int":
|
|
870
|
-
case "BigInt":
|
|
871
|
-
schema.type = "integer";
|
|
872
|
-
break;
|
|
873
|
-
case "Float":
|
|
874
|
-
case "Decimal":
|
|
875
|
-
schema.type = "number";
|
|
876
|
-
break;
|
|
877
|
-
case "Boolean":
|
|
878
|
-
schema.type = "boolean";
|
|
879
|
-
break;
|
|
880
|
-
case "DateTime":
|
|
881
|
-
schema.type = "string";
|
|
882
|
-
schema.format = "date-time";
|
|
883
|
-
break;
|
|
884
|
-
case "Json":
|
|
885
|
-
schema.type = "object";
|
|
886
|
-
break;
|
|
887
|
-
default: if (field.kind === "enum") {
|
|
888
|
-
schema.type = "string";
|
|
889
|
-
if (this.dmmf?.datamodel?.enums) {
|
|
890
|
-
const enumDef = this.dmmf.datamodel.enums.find((e) => e.name === field.type);
|
|
891
|
-
if (enumDef) schema.enum = enumDef.values.map((v) => v.name);
|
|
892
|
-
}
|
|
893
|
-
} else schema.type = "string";
|
|
894
|
-
}
|
|
895
|
-
if (field.isList) return {
|
|
896
|
-
type: "array",
|
|
897
|
-
items: schema
|
|
898
|
-
};
|
|
899
|
-
if (field.documentation) schema.description = field.documentation;
|
|
900
|
-
return schema;
|
|
901
|
-
}
|
|
902
|
-
convertPrismaFieldToMetadata(field) {
|
|
903
|
-
const metadata = {
|
|
904
|
-
type: this.mapPrismaTypeToMetadataType(field.type, field.kind),
|
|
905
|
-
required: field.isRequired,
|
|
906
|
-
array: field.isList
|
|
907
|
-
};
|
|
908
|
-
if (field.isUnique) metadata.unique = true;
|
|
909
|
-
if (field.hasDefaultValue) metadata.default = field.default;
|
|
910
|
-
if (field.documentation) metadata.description = field.documentation;
|
|
911
|
-
if (field.relationName) metadata.ref = field.type;
|
|
912
|
-
return metadata;
|
|
913
|
-
}
|
|
914
|
-
mapPrismaTypeToMetadataType(type, kind) {
|
|
915
|
-
if (kind === "enum") return "enum";
|
|
916
|
-
switch (type) {
|
|
917
|
-
case "String": return "string";
|
|
918
|
-
case "Int":
|
|
919
|
-
case "BigInt":
|
|
920
|
-
case "Float":
|
|
921
|
-
case "Decimal": return "number";
|
|
922
|
-
case "Boolean": return "boolean";
|
|
923
|
-
case "DateTime": return "date";
|
|
924
|
-
case "Json": return "object";
|
|
925
|
-
default: return "string";
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
};
|
|
929
|
-
/**
|
|
930
|
-
* Factory function to create Prisma adapter
|
|
931
|
-
*
|
|
932
|
-
* @example
|
|
933
|
-
* import { PrismaClient } from '@prisma/client';
|
|
934
|
-
* import { createPrismaAdapter } from '@classytic/arc';
|
|
935
|
-
*
|
|
936
|
-
* const prisma = new PrismaClient();
|
|
937
|
-
*
|
|
938
|
-
* const userAdapter = createPrismaAdapter({
|
|
939
|
-
* client: prisma,
|
|
940
|
-
* modelName: 'user',
|
|
941
|
-
* repository: userRepository,
|
|
942
|
-
* dmmf: Prisma.dmmf, // Optional: for schema generation
|
|
943
|
-
* });
|
|
944
|
-
*/
|
|
945
|
-
function createPrismaAdapter(options) {
|
|
946
|
-
return new PrismaAdapter(options);
|
|
947
|
-
}
|
|
948
|
-
//#endregion
|
|
949
|
-
export { createMongooseAdapter as a, MongooseAdapter as i, PrismaQueryParser as n, DrizzleAdapter as o, createPrismaAdapter as r, createDrizzleAdapter as s, PrismaAdapter as t };
|