@classytic/arc 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +900 -0
- package/bin/arc.js +344 -0
- package/dist/adapters/index.d.ts +237 -0
- package/dist/adapters/index.js +668 -0
- package/dist/arcCorePlugin-DTPWXcZN.d.ts +273 -0
- package/dist/audit/index.d.ts +195 -0
- package/dist/audit/index.js +319 -0
- package/dist/auth/index.d.ts +47 -0
- package/dist/auth/index.js +174 -0
- package/dist/cli/commands/docs.d.ts +11 -0
- package/dist/cli/commands/docs.js +474 -0
- package/dist/cli/commands/introspect.d.ts +8 -0
- package/dist/cli/commands/introspect.js +338 -0
- package/dist/cli/index.d.ts +43 -0
- package/dist/cli/index.js +520 -0
- package/dist/createApp-pzUAkzbz.d.ts +77 -0
- package/dist/docs/index.d.ts +166 -0
- package/dist/docs/index.js +650 -0
- package/dist/errors-8WIxGS_6.d.ts +122 -0
- package/dist/events/index.d.ts +117 -0
- package/dist/events/index.js +89 -0
- package/dist/factory/index.d.ts +38 -0
- package/dist/factory/index.js +1664 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +199 -0
- package/dist/idempotency/index.d.ts +323 -0
- package/dist/idempotency/index.js +500 -0
- package/dist/index-DkAW8BXh.d.ts +1302 -0
- package/dist/index.d.ts +331 -0
- package/dist/index.js +4734 -0
- package/dist/migrations/index.d.ts +185 -0
- package/dist/migrations/index.js +274 -0
- package/dist/org/index.d.ts +129 -0
- package/dist/org/index.js +220 -0
- package/dist/permissions/index.d.ts +144 -0
- package/dist/permissions/index.js +100 -0
- package/dist/plugins/index.d.ts +46 -0
- package/dist/plugins/index.js +1069 -0
- package/dist/policies/index.d.ts +398 -0
- package/dist/policies/index.js +196 -0
- package/dist/presets/index.d.ts +336 -0
- package/dist/presets/index.js +382 -0
- package/dist/presets/multiTenant.d.ts +39 -0
- package/dist/presets/multiTenant.js +112 -0
- package/dist/registry/index.d.ts +16 -0
- package/dist/registry/index.js +253 -0
- package/dist/testing/index.d.ts +618 -0
- package/dist/testing/index.js +48032 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.js +8 -0
- package/dist/types-0IPhH_NR.d.ts +143 -0
- package/dist/types-B99TBmFV.d.ts +76 -0
- package/dist/utils/index.d.ts +655 -0
- package/dist/utils/index.js +905 -0
- package/package.json +227 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
// src/adapters/types.ts
|
|
2
|
+
function isMongooseModel(value) {
|
|
3
|
+
return typeof value === "function" && value.prototype && "modelName" in value && "schema" in value;
|
|
4
|
+
}
|
|
5
|
+
function isRepository(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && "getAll" in value && "getById" in value && "create" in value && "update" in value && "delete" in value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// src/adapters/mongoose.ts
|
|
10
|
+
var MongooseAdapter = class {
|
|
11
|
+
type = "mongoose";
|
|
12
|
+
name;
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
model;
|
|
15
|
+
repository;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
if (!isMongooseModel(options.model)) {
|
|
18
|
+
throw new TypeError(
|
|
19
|
+
"MongooseAdapter: Invalid model. Expected Mongoose Model instance.\nUsage: createMongooseAdapter({ model: YourModel, repository: yourRepo })"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
if (!isRepository(options.repository)) {
|
|
23
|
+
throw new TypeError(
|
|
24
|
+
"MongooseAdapter: Invalid repository. Expected CrudRepository instance.\nUsage: createMongooseAdapter({ model: YourModel, repository: yourRepo })"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
this.model = options.model;
|
|
28
|
+
this.repository = options.repository;
|
|
29
|
+
this.name = `MongooseAdapter<${options.model.modelName}>`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get schema metadata from Mongoose model
|
|
33
|
+
*/
|
|
34
|
+
getSchemaMetadata() {
|
|
35
|
+
const schema = this.model.schema;
|
|
36
|
+
const paths = schema.paths;
|
|
37
|
+
const fields = {};
|
|
38
|
+
for (const [fieldName, schemaType] of Object.entries(paths)) {
|
|
39
|
+
if (fieldName.startsWith("_") && fieldName !== "_id") continue;
|
|
40
|
+
const typeInfo = schemaType;
|
|
41
|
+
const mongooseType = typeInfo.instance || "Mixed";
|
|
42
|
+
const typeMap = {
|
|
43
|
+
String: "string",
|
|
44
|
+
Number: "number",
|
|
45
|
+
Boolean: "boolean",
|
|
46
|
+
Date: "date",
|
|
47
|
+
ObjectID: "objectId",
|
|
48
|
+
Array: "array",
|
|
49
|
+
Mixed: "object",
|
|
50
|
+
Buffer: "object",
|
|
51
|
+
Embedded: "object"
|
|
52
|
+
};
|
|
53
|
+
fields[fieldName] = {
|
|
54
|
+
type: typeMap[mongooseType] ?? "object",
|
|
55
|
+
required: !!typeInfo.isRequired,
|
|
56
|
+
ref: typeInfo.options?.ref
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
name: this.model.modelName,
|
|
61
|
+
fields,
|
|
62
|
+
relations: this.extractRelations(paths)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Generate OpenAPI schemas from Mongoose model
|
|
67
|
+
*/
|
|
68
|
+
generateSchemas(schemaOptions) {
|
|
69
|
+
try {
|
|
70
|
+
const schema = this.model.schema;
|
|
71
|
+
const paths = schema.paths;
|
|
72
|
+
const properties = {};
|
|
73
|
+
const required = [];
|
|
74
|
+
const fieldRules = schemaOptions?.fieldRules || {};
|
|
75
|
+
const blockedFields = new Set(
|
|
76
|
+
Object.entries(fieldRules).filter(([, rules]) => rules.systemManaged || rules.hidden).map(([field]) => field)
|
|
77
|
+
);
|
|
78
|
+
for (const [fieldName, schemaType] of Object.entries(paths)) {
|
|
79
|
+
if (fieldName.startsWith("__")) continue;
|
|
80
|
+
if (blockedFields.has(fieldName)) continue;
|
|
81
|
+
const typeInfo = schemaType;
|
|
82
|
+
properties[fieldName] = this.mongooseTypeToOpenApi(typeInfo);
|
|
83
|
+
if (typeInfo.isRequired) {
|
|
84
|
+
required.push(fieldName);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const baseSchema = {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties,
|
|
90
|
+
required: required.length > 0 ? required : void 0
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
create: {
|
|
94
|
+
body: {
|
|
95
|
+
...baseSchema,
|
|
96
|
+
// Remove system-managed fields from create
|
|
97
|
+
properties: Object.fromEntries(
|
|
98
|
+
Object.entries(properties).filter(
|
|
99
|
+
([field]) => !["_id", "createdAt", "updatedAt", "deletedAt"].includes(field)
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
update: {
|
|
105
|
+
body: {
|
|
106
|
+
...baseSchema,
|
|
107
|
+
// All fields optional for PATCH
|
|
108
|
+
required: void 0,
|
|
109
|
+
properties: Object.fromEntries(
|
|
110
|
+
Object.entries(properties).filter(
|
|
111
|
+
([field]) => !["_id", "createdAt", "updatedAt", "deletedAt"].includes(field)
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
response: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Extract relation metadata
|
|
127
|
+
*/
|
|
128
|
+
extractRelations(paths) {
|
|
129
|
+
const relations = {};
|
|
130
|
+
for (const [fieldName, schemaType] of Object.entries(paths)) {
|
|
131
|
+
const ref = schemaType.options?.ref;
|
|
132
|
+
if (ref) {
|
|
133
|
+
relations[fieldName] = {
|
|
134
|
+
type: "one-to-one",
|
|
135
|
+
// Mongoose refs are typically one-to-one
|
|
136
|
+
target: ref,
|
|
137
|
+
foreignKey: fieldName
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return Object.keys(relations).length > 0 ? relations : void 0;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Convert Mongoose type to OpenAPI type
|
|
145
|
+
*/
|
|
146
|
+
mongooseTypeToOpenApi(typeInfo) {
|
|
147
|
+
const instance = typeInfo.instance;
|
|
148
|
+
const options = typeInfo.options || {};
|
|
149
|
+
const baseType = {};
|
|
150
|
+
switch (instance) {
|
|
151
|
+
case "String":
|
|
152
|
+
baseType.type = "string";
|
|
153
|
+
if (options.enum) baseType.enum = options.enum;
|
|
154
|
+
if (options.minlength) baseType.minLength = options.minlength;
|
|
155
|
+
if (options.maxlength) baseType.maxLength = options.maxlength;
|
|
156
|
+
break;
|
|
157
|
+
case "Number":
|
|
158
|
+
baseType.type = "number";
|
|
159
|
+
if (options.min !== void 0) baseType.minimum = options.min;
|
|
160
|
+
if (options.max !== void 0) baseType.maximum = options.max;
|
|
161
|
+
break;
|
|
162
|
+
case "Boolean":
|
|
163
|
+
baseType.type = "boolean";
|
|
164
|
+
break;
|
|
165
|
+
case "Date":
|
|
166
|
+
baseType.type = "string";
|
|
167
|
+
baseType.format = "date-time";
|
|
168
|
+
break;
|
|
169
|
+
case "ObjectID":
|
|
170
|
+
baseType.type = "string";
|
|
171
|
+
baseType.pattern = "^[a-f\\d]{24}$";
|
|
172
|
+
break;
|
|
173
|
+
case "Array":
|
|
174
|
+
baseType.type = "array";
|
|
175
|
+
baseType.items = { type: "string" };
|
|
176
|
+
break;
|
|
177
|
+
default:
|
|
178
|
+
baseType.type = "object";
|
|
179
|
+
}
|
|
180
|
+
return baseType;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
function createMongooseAdapter(options) {
|
|
184
|
+
return new MongooseAdapter(options);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/adapters/prisma.ts
|
|
188
|
+
var PrismaQueryParser = class {
|
|
189
|
+
maxLimit;
|
|
190
|
+
defaultLimit;
|
|
191
|
+
softDeleteEnabled;
|
|
192
|
+
softDeleteField;
|
|
193
|
+
/** Map Arc operators to Prisma operators */
|
|
194
|
+
operatorMap = {
|
|
195
|
+
$eq: "equals",
|
|
196
|
+
$ne: "not",
|
|
197
|
+
$gt: "gt",
|
|
198
|
+
$gte: "gte",
|
|
199
|
+
$lt: "lt",
|
|
200
|
+
$lte: "lte",
|
|
201
|
+
$in: "in",
|
|
202
|
+
$nin: "notIn",
|
|
203
|
+
$regex: "contains",
|
|
204
|
+
$exists: void 0
|
|
205
|
+
// Handled specially
|
|
206
|
+
};
|
|
207
|
+
constructor(options = {}) {
|
|
208
|
+
this.maxLimit = options.maxLimit ?? 1e3;
|
|
209
|
+
this.defaultLimit = options.defaultLimit ?? 20;
|
|
210
|
+
this.softDeleteEnabled = options.softDeleteEnabled ?? true;
|
|
211
|
+
this.softDeleteField = options.softDeleteField ?? "deletedAt";
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Parse URL query parameters (delegates to ArcQueryParser format)
|
|
215
|
+
*/
|
|
216
|
+
parse(query) {
|
|
217
|
+
const q = query ?? {};
|
|
218
|
+
const page = this.parseNumber(q.page, 1);
|
|
219
|
+
const limit = Math.min(this.parseNumber(q.limit, this.defaultLimit), this.maxLimit);
|
|
220
|
+
return {
|
|
221
|
+
filters: this.parseFilters(q),
|
|
222
|
+
limit,
|
|
223
|
+
page,
|
|
224
|
+
sort: this.parseSort(q.sort),
|
|
225
|
+
search: q.search,
|
|
226
|
+
select: this.parseSelect(q.select)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Convert ParsedQuery to Prisma query options
|
|
231
|
+
*/
|
|
232
|
+
toPrismaQuery(parsed, policyFilters) {
|
|
233
|
+
const where = {};
|
|
234
|
+
if (parsed.filters) {
|
|
235
|
+
Object.assign(where, this.translateFilters(parsed.filters));
|
|
236
|
+
}
|
|
237
|
+
if (policyFilters) {
|
|
238
|
+
Object.assign(where, this.translateFilters(policyFilters));
|
|
239
|
+
}
|
|
240
|
+
if (this.softDeleteEnabled) {
|
|
241
|
+
where[this.softDeleteField] = null;
|
|
242
|
+
}
|
|
243
|
+
const orderBy = parsed.sort ? Object.entries(parsed.sort).map(([field, dir]) => ({
|
|
244
|
+
[field]: dir === 1 ? "asc" : "desc"
|
|
245
|
+
})) : void 0;
|
|
246
|
+
const take = parsed.limit ?? this.defaultLimit;
|
|
247
|
+
const skip = parsed.page ? (parsed.page - 1) * take : 0;
|
|
248
|
+
const select = parsed.select ? Object.fromEntries(
|
|
249
|
+
Object.entries(parsed.select).filter(([, v]) => v === 1).map(([k]) => [k, true])
|
|
250
|
+
) : void 0;
|
|
251
|
+
return {
|
|
252
|
+
where: Object.keys(where).length > 0 ? where : void 0,
|
|
253
|
+
orderBy: orderBy && orderBy.length > 0 ? orderBy : void 0,
|
|
254
|
+
take,
|
|
255
|
+
skip,
|
|
256
|
+
select: select && Object.keys(select).length > 0 ? select : void 0
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Translate Arc/MongoDB-style filters to Prisma where clause
|
|
261
|
+
*/
|
|
262
|
+
translateFilters(filters) {
|
|
263
|
+
const result = {};
|
|
264
|
+
for (const [field, value] of Object.entries(filters)) {
|
|
265
|
+
if (value === null || value === void 0) continue;
|
|
266
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
267
|
+
const prismaCondition = {};
|
|
268
|
+
for (const [op, opValue] of Object.entries(value)) {
|
|
269
|
+
if (op === "$exists") {
|
|
270
|
+
result[field] = opValue ? { not: null } : null;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const prismaOp = this.operatorMap[op];
|
|
274
|
+
if (prismaOp) {
|
|
275
|
+
prismaCondition[prismaOp] = opValue;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (Object.keys(prismaCondition).length > 0) {
|
|
279
|
+
result[field] = prismaCondition;
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
result[field] = value;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
parseNumber(value, defaultValue) {
|
|
288
|
+
if (value === void 0 || value === null) return defaultValue;
|
|
289
|
+
const num = parseInt(String(value), 10);
|
|
290
|
+
return Number.isNaN(num) ? defaultValue : Math.max(1, num);
|
|
291
|
+
}
|
|
292
|
+
parseSort(value) {
|
|
293
|
+
if (!value) return void 0;
|
|
294
|
+
const sortStr = String(value);
|
|
295
|
+
const result = {};
|
|
296
|
+
for (const field of sortStr.split(",")) {
|
|
297
|
+
const trimmed = field.trim();
|
|
298
|
+
if (!trimmed || !/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) continue;
|
|
299
|
+
if (trimmed.startsWith("-")) {
|
|
300
|
+
result[trimmed.slice(1)] = -1;
|
|
301
|
+
} else {
|
|
302
|
+
result[trimmed] = 1;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
306
|
+
}
|
|
307
|
+
parseSelect(value) {
|
|
308
|
+
if (!value) return void 0;
|
|
309
|
+
const result = {};
|
|
310
|
+
for (const field of String(value).split(",")) {
|
|
311
|
+
const trimmed = field.trim();
|
|
312
|
+
if (!trimmed || !/^-?[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) continue;
|
|
313
|
+
result[trimmed.startsWith("-") ? trimmed.slice(1) : trimmed] = trimmed.startsWith("-") ? 0 : 1;
|
|
314
|
+
}
|
|
315
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
316
|
+
}
|
|
317
|
+
parseFilters(query) {
|
|
318
|
+
const reserved = /* @__PURE__ */ new Set(["page", "limit", "sort", "search", "select", "populate", "after", "cursor"]);
|
|
319
|
+
const filters = {};
|
|
320
|
+
const operators = {
|
|
321
|
+
eq: "$eq",
|
|
322
|
+
ne: "$ne",
|
|
323
|
+
gt: "$gt",
|
|
324
|
+
gte: "$gte",
|
|
325
|
+
lt: "$lt",
|
|
326
|
+
lte: "$lte",
|
|
327
|
+
in: "$in",
|
|
328
|
+
nin: "$nin",
|
|
329
|
+
like: "$regex",
|
|
330
|
+
contains: "$regex",
|
|
331
|
+
exists: "$exists"
|
|
332
|
+
};
|
|
333
|
+
for (const [key, value] of Object.entries(query)) {
|
|
334
|
+
if (reserved.has(key) || value === void 0 || value === null) continue;
|
|
335
|
+
const match = key.match(/^([a-zA-Z_][a-zA-Z0-9_.]*)(?:\[([a-z]+)\])?$/);
|
|
336
|
+
if (!match) continue;
|
|
337
|
+
const [, fieldName, operator] = match;
|
|
338
|
+
if (!fieldName) continue;
|
|
339
|
+
if (operator && operators[operator]) {
|
|
340
|
+
if (!filters[fieldName]) filters[fieldName] = {};
|
|
341
|
+
filters[fieldName][operators[operator]] = this.coerceValue(value, operator);
|
|
342
|
+
} else if (!operator) {
|
|
343
|
+
filters[fieldName] = this.coerceValue(value);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return filters;
|
|
347
|
+
}
|
|
348
|
+
coerceValue(value, operator) {
|
|
349
|
+
if (operator === "in" || operator === "nin") {
|
|
350
|
+
if (Array.isArray(value)) return value.map((v) => this.coerceValue(v));
|
|
351
|
+
if (typeof value === "string" && value.includes(",")) {
|
|
352
|
+
return value.split(",").map((v) => this.coerceValue(v.trim()));
|
|
353
|
+
}
|
|
354
|
+
return [this.coerceValue(value)];
|
|
355
|
+
}
|
|
356
|
+
if (operator === "exists") {
|
|
357
|
+
return String(value).toLowerCase() === "true" || value === "1";
|
|
358
|
+
}
|
|
359
|
+
if (value === "true") return true;
|
|
360
|
+
if (value === "false") return false;
|
|
361
|
+
if (value === "null") return null;
|
|
362
|
+
if (typeof value === "string") {
|
|
363
|
+
const num = Number(value);
|
|
364
|
+
if (!Number.isNaN(num) && value.trim() !== "") return num;
|
|
365
|
+
}
|
|
366
|
+
return value;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
var PrismaAdapter = class {
|
|
370
|
+
type = "prisma";
|
|
371
|
+
name;
|
|
372
|
+
repository;
|
|
373
|
+
queryParser;
|
|
374
|
+
client;
|
|
375
|
+
modelName;
|
|
376
|
+
dmmf;
|
|
377
|
+
softDeleteEnabled;
|
|
378
|
+
softDeleteField;
|
|
379
|
+
constructor(options) {
|
|
380
|
+
this.client = options.client;
|
|
381
|
+
this.modelName = options.modelName;
|
|
382
|
+
this.repository = options.repository;
|
|
383
|
+
this.dmmf = options.dmmf;
|
|
384
|
+
this.name = `prisma:${options.modelName}`;
|
|
385
|
+
this.softDeleteEnabled = options.softDeleteEnabled ?? true;
|
|
386
|
+
this.softDeleteField = options.softDeleteField ?? "deletedAt";
|
|
387
|
+
this.queryParser = options.queryParser ?? new PrismaQueryParser({
|
|
388
|
+
softDeleteEnabled: this.softDeleteEnabled,
|
|
389
|
+
softDeleteField: this.softDeleteField
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Parse URL query parameters and convert to Prisma query options
|
|
394
|
+
*/
|
|
395
|
+
parseQuery(query, policyFilters) {
|
|
396
|
+
const parsed = this.queryParser.parse(query);
|
|
397
|
+
return this.queryParser.toPrismaQuery(parsed, policyFilters);
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Apply policy filters to existing Prisma where clause
|
|
401
|
+
* Used for multi-tenant, ownership, and other security filters
|
|
402
|
+
*/
|
|
403
|
+
applyPolicyFilters(where, policyFilters) {
|
|
404
|
+
return { ...where, ...policyFilters };
|
|
405
|
+
}
|
|
406
|
+
generateSchemas(options) {
|
|
407
|
+
if (!this.dmmf) return null;
|
|
408
|
+
try {
|
|
409
|
+
const model = this.dmmf.datamodel?.models?.find(
|
|
410
|
+
(m) => m.name.toLowerCase() === this.modelName.toLowerCase()
|
|
411
|
+
);
|
|
412
|
+
if (!model) return null;
|
|
413
|
+
const entitySchema = this.buildEntitySchema(model, options);
|
|
414
|
+
const createBodySchema = this.buildCreateSchema(model, options);
|
|
415
|
+
const updateBodySchema = this.buildUpdateSchema(model, options);
|
|
416
|
+
return {
|
|
417
|
+
entity: entitySchema,
|
|
418
|
+
createBody: createBodySchema,
|
|
419
|
+
updateBody: updateBodySchema,
|
|
420
|
+
params: {
|
|
421
|
+
type: "object",
|
|
422
|
+
properties: {
|
|
423
|
+
id: { type: "string" }
|
|
424
|
+
},
|
|
425
|
+
required: ["id"]
|
|
426
|
+
},
|
|
427
|
+
listQuery: {
|
|
428
|
+
type: "object",
|
|
429
|
+
properties: {
|
|
430
|
+
page: { type: "number", minimum: 1, description: "Page number for pagination" },
|
|
431
|
+
limit: { type: "number", minimum: 1, maximum: 100, description: "Items per page" },
|
|
432
|
+
sort: { type: "string", description: 'Sort field (e.g., "name", "-createdAt")' }
|
|
433
|
+
// Note: Actual filtering requires custom query parser implementation
|
|
434
|
+
// This is placeholder documentation only
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
} catch {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
getSchemaMetadata() {
|
|
443
|
+
if (!this.dmmf) return null;
|
|
444
|
+
try {
|
|
445
|
+
const model = this.dmmf.datamodel?.models?.find(
|
|
446
|
+
(m) => m.name.toLowerCase() === this.modelName.toLowerCase()
|
|
447
|
+
);
|
|
448
|
+
if (!model) return null;
|
|
449
|
+
const fields = {};
|
|
450
|
+
for (const field of model.fields) {
|
|
451
|
+
fields[field.name] = this.convertPrismaFieldToMetadata(field);
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
name: model.name,
|
|
455
|
+
fields,
|
|
456
|
+
indexes: model.uniqueIndexes?.map((idx) => ({
|
|
457
|
+
fields: idx.fields,
|
|
458
|
+
unique: true
|
|
459
|
+
}))
|
|
460
|
+
};
|
|
461
|
+
} catch (err) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async validate(data) {
|
|
466
|
+
if (!data || typeof data !== "object") {
|
|
467
|
+
return {
|
|
468
|
+
valid: false,
|
|
469
|
+
errors: [{ field: "root", message: "Data must be an object" }]
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
if (this.dmmf) {
|
|
473
|
+
try {
|
|
474
|
+
const model = this.dmmf.datamodel?.models?.find(
|
|
475
|
+
(m) => m.name.toLowerCase() === this.modelName.toLowerCase()
|
|
476
|
+
);
|
|
477
|
+
if (model) {
|
|
478
|
+
const requiredFields = model.fields.filter(
|
|
479
|
+
(f) => f.isRequired && !f.hasDefaultValue && !f.isGenerated
|
|
480
|
+
);
|
|
481
|
+
const errors = [];
|
|
482
|
+
for (const field of requiredFields) {
|
|
483
|
+
if (!(field.name in data)) {
|
|
484
|
+
errors.push({
|
|
485
|
+
field: field.name,
|
|
486
|
+
message: `${field.name} is required`
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (errors.length > 0) {
|
|
491
|
+
return { valid: false, errors };
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} catch (err) {
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return { valid: true };
|
|
498
|
+
}
|
|
499
|
+
async healthCheck() {
|
|
500
|
+
try {
|
|
501
|
+
const delegateName = this.modelName.charAt(0).toLowerCase() + this.modelName.slice(1);
|
|
502
|
+
const delegate = this.client[delegateName];
|
|
503
|
+
if (!delegate) {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
await delegate.findMany({ take: 1 });
|
|
507
|
+
return true;
|
|
508
|
+
} catch (err) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async close() {
|
|
513
|
+
try {
|
|
514
|
+
await this.client.$disconnect();
|
|
515
|
+
} catch (err) {
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// ============================================================================
|
|
519
|
+
// Private Helper Methods
|
|
520
|
+
// ============================================================================
|
|
521
|
+
buildEntitySchema(model, options) {
|
|
522
|
+
const properties = {};
|
|
523
|
+
const required = [];
|
|
524
|
+
for (const field of model.fields) {
|
|
525
|
+
if (this.shouldSkipField(field, options)) continue;
|
|
526
|
+
properties[field.name] = this.convertPrismaFieldToJsonSchema(field);
|
|
527
|
+
if (field.isRequired && !field.hasDefaultValue) {
|
|
528
|
+
required.push(field.name);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
type: "object",
|
|
533
|
+
properties,
|
|
534
|
+
...required.length > 0 && { required }
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
buildCreateSchema(model, options) {
|
|
538
|
+
const properties = {};
|
|
539
|
+
const required = [];
|
|
540
|
+
for (const field of model.fields) {
|
|
541
|
+
if (field.isGenerated || field.relationName) continue;
|
|
542
|
+
if (this.shouldSkipField(field, options)) continue;
|
|
543
|
+
properties[field.name] = this.convertPrismaFieldToJsonSchema(field);
|
|
544
|
+
if (field.isRequired && !field.hasDefaultValue) {
|
|
545
|
+
required.push(field.name);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
type: "object",
|
|
550
|
+
properties,
|
|
551
|
+
...required.length > 0 && { required }
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
buildUpdateSchema(model, options) {
|
|
555
|
+
const properties = {};
|
|
556
|
+
for (const field of model.fields) {
|
|
557
|
+
if (field.isGenerated || field.isId || field.relationName) continue;
|
|
558
|
+
if (this.shouldSkipField(field, options)) continue;
|
|
559
|
+
properties[field.name] = this.convertPrismaFieldToJsonSchema(field);
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
type: "object",
|
|
563
|
+
properties
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
shouldSkipField(field, options) {
|
|
567
|
+
if (options?.excludeFields?.includes(field.name)) {
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
if (field.name.startsWith("_")) {
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
convertPrismaFieldToJsonSchema(field) {
|
|
576
|
+
const schema = {};
|
|
577
|
+
switch (field.type) {
|
|
578
|
+
case "String":
|
|
579
|
+
schema.type = "string";
|
|
580
|
+
break;
|
|
581
|
+
case "Int":
|
|
582
|
+
case "BigInt":
|
|
583
|
+
schema.type = "integer";
|
|
584
|
+
break;
|
|
585
|
+
case "Float":
|
|
586
|
+
case "Decimal":
|
|
587
|
+
schema.type = "number";
|
|
588
|
+
break;
|
|
589
|
+
case "Boolean":
|
|
590
|
+
schema.type = "boolean";
|
|
591
|
+
break;
|
|
592
|
+
case "DateTime":
|
|
593
|
+
schema.type = "string";
|
|
594
|
+
schema.format = "date-time";
|
|
595
|
+
break;
|
|
596
|
+
case "Json":
|
|
597
|
+
schema.type = "object";
|
|
598
|
+
break;
|
|
599
|
+
default:
|
|
600
|
+
if (field.kind === "enum") {
|
|
601
|
+
schema.type = "string";
|
|
602
|
+
if (this.dmmf?.datamodel?.enums) {
|
|
603
|
+
const enumDef = this.dmmf.datamodel.enums.find((e) => e.name === field.type);
|
|
604
|
+
if (enumDef) {
|
|
605
|
+
schema.enum = enumDef.values.map((v) => v.name);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
schema.type = "string";
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (field.isList) {
|
|
613
|
+
return {
|
|
614
|
+
type: "array",
|
|
615
|
+
items: schema
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
if (field.documentation) {
|
|
619
|
+
schema.description = field.documentation;
|
|
620
|
+
}
|
|
621
|
+
return schema;
|
|
622
|
+
}
|
|
623
|
+
convertPrismaFieldToMetadata(field) {
|
|
624
|
+
const metadata = {
|
|
625
|
+
type: this.mapPrismaTypeToMetadataType(field.type, field.kind),
|
|
626
|
+
required: field.isRequired,
|
|
627
|
+
array: field.isList
|
|
628
|
+
};
|
|
629
|
+
if (field.isUnique) {
|
|
630
|
+
metadata.unique = true;
|
|
631
|
+
}
|
|
632
|
+
if (field.hasDefaultValue) {
|
|
633
|
+
metadata.default = field.default;
|
|
634
|
+
}
|
|
635
|
+
if (field.documentation) {
|
|
636
|
+
metadata.description = field.documentation;
|
|
637
|
+
}
|
|
638
|
+
if (field.relationName) {
|
|
639
|
+
metadata.ref = field.type;
|
|
640
|
+
}
|
|
641
|
+
return metadata;
|
|
642
|
+
}
|
|
643
|
+
mapPrismaTypeToMetadataType(type, kind) {
|
|
644
|
+
if (kind === "enum") return "enum";
|
|
645
|
+
switch (type) {
|
|
646
|
+
case "String":
|
|
647
|
+
return "string";
|
|
648
|
+
case "Int":
|
|
649
|
+
case "BigInt":
|
|
650
|
+
case "Float":
|
|
651
|
+
case "Decimal":
|
|
652
|
+
return "number";
|
|
653
|
+
case "Boolean":
|
|
654
|
+
return "boolean";
|
|
655
|
+
case "DateTime":
|
|
656
|
+
return "date";
|
|
657
|
+
case "Json":
|
|
658
|
+
return "object";
|
|
659
|
+
default:
|
|
660
|
+
return "string";
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
function createPrismaAdapter(options) {
|
|
665
|
+
return new PrismaAdapter(options);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export { MongooseAdapter, PrismaAdapter, PrismaQueryParser, createMongooseAdapter, createPrismaAdapter };
|