@classytic/mongokit 3.0.6 → 3.1.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/README.md +4 -2
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +40 -9
- package/dist/{index-CkwbNdpJ.d.ts → index-3Nkm_Brq.d.ts} +170 -3
- package/dist/index.d.ts +997 -8
- package/dist/index.js +1202 -197
- package/dist/{queryParser-Do3SgsyJ.d.ts → mongooseToJsonSchema-CUQma8QK.d.ts} +8 -111
- package/dist/pagination/PaginationEngine.d.ts +1 -1
- package/dist/plugins/index.d.ts +1 -1
- package/dist/{types-DDDYo18H.d.ts → types-CrSoCuWu.d.ts} +11 -31
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +104 -417
- package/package.json +1 -1
package/dist/utils/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import 'mongoose';
|
|
2
2
|
|
|
3
3
|
// src/utils/field-selection.ts
|
|
4
4
|
function getFieldsForUser(user, preset) {
|
|
@@ -45,339 +45,15 @@ function createFieldPreset(config) {
|
|
|
45
45
|
admin: config.admin || []
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
var QueryParser = class {
|
|
49
|
-
options;
|
|
50
|
-
operators = {
|
|
51
|
-
eq: "$eq",
|
|
52
|
-
ne: "$ne",
|
|
53
|
-
gt: "$gt",
|
|
54
|
-
gte: "$gte",
|
|
55
|
-
lt: "$lt",
|
|
56
|
-
lte: "$lte",
|
|
57
|
-
in: "$in",
|
|
58
|
-
nin: "$nin",
|
|
59
|
-
like: "$regex",
|
|
60
|
-
contains: "$regex",
|
|
61
|
-
regex: "$regex",
|
|
62
|
-
exists: "$exists",
|
|
63
|
-
size: "$size",
|
|
64
|
-
type: "$type"
|
|
65
|
-
};
|
|
66
|
-
/**
|
|
67
|
-
* Dangerous MongoDB operators that should never be accepted from user input
|
|
68
|
-
* Security: Prevent NoSQL injection attacks
|
|
69
|
-
*/
|
|
70
|
-
dangerousOperators;
|
|
71
|
-
/**
|
|
72
|
-
* Regex pattern characters that can cause catastrophic backtracking (ReDoS)
|
|
73
|
-
*/
|
|
74
|
-
dangerousRegexPatterns = /(\{[0-9,]+\}|\*\+|\+\+|\?\+|(\([^)]*\))\1|\(\?[^)]*\)|[\[\]].*[\[\]])/;
|
|
75
|
-
constructor(options = {}) {
|
|
76
|
-
this.options = {
|
|
77
|
-
maxRegexLength: options.maxRegexLength ?? 500,
|
|
78
|
-
maxSearchLength: options.maxSearchLength ?? 200,
|
|
79
|
-
maxFilterDepth: options.maxFilterDepth ?? 10,
|
|
80
|
-
additionalDangerousOperators: options.additionalDangerousOperators ?? []
|
|
81
|
-
};
|
|
82
|
-
this.dangerousOperators = [
|
|
83
|
-
"$where",
|
|
84
|
-
"$function",
|
|
85
|
-
"$accumulator",
|
|
86
|
-
"$expr",
|
|
87
|
-
...this.options.additionalDangerousOperators
|
|
88
|
-
];
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Parse query parameters into MongoDB query format
|
|
92
|
-
*/
|
|
93
|
-
parseQuery(query) {
|
|
94
|
-
const {
|
|
95
|
-
page,
|
|
96
|
-
limit = 20,
|
|
97
|
-
sort = "-createdAt",
|
|
98
|
-
populate,
|
|
99
|
-
search,
|
|
100
|
-
after,
|
|
101
|
-
cursor,
|
|
102
|
-
...filters
|
|
103
|
-
} = query || {};
|
|
104
|
-
const parsed = {
|
|
105
|
-
filters: this._parseFilters(filters),
|
|
106
|
-
limit: parseInt(String(limit), 10),
|
|
107
|
-
sort: this._parseSort(sort),
|
|
108
|
-
populate,
|
|
109
|
-
search: this._sanitizeSearch(search)
|
|
110
|
-
};
|
|
111
|
-
if (after || cursor) {
|
|
112
|
-
parsed.after = after || cursor;
|
|
113
|
-
} else if (page !== void 0) {
|
|
114
|
-
parsed.page = parseInt(String(page), 10);
|
|
115
|
-
} else {
|
|
116
|
-
parsed.page = 1;
|
|
117
|
-
}
|
|
118
|
-
const orGroup = this._parseOr(query);
|
|
119
|
-
if (orGroup) {
|
|
120
|
-
parsed.filters = { ...parsed.filters, $or: orGroup };
|
|
121
|
-
}
|
|
122
|
-
parsed.filters = this._enhanceWithBetween(parsed.filters);
|
|
123
|
-
return parsed;
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Parse sort parameter
|
|
127
|
-
* Converts string like '-createdAt' to { createdAt: -1 }
|
|
128
|
-
* Handles multiple sorts: '-createdAt,name' → { createdAt: -1, name: 1 }
|
|
129
|
-
*/
|
|
130
|
-
_parseSort(sort) {
|
|
131
|
-
if (!sort) return void 0;
|
|
132
|
-
if (typeof sort === "object") return sort;
|
|
133
|
-
const sortObj = {};
|
|
134
|
-
const fields = sort.split(",").map((s) => s.trim());
|
|
135
|
-
for (const field of fields) {
|
|
136
|
-
if (field.startsWith("-")) {
|
|
137
|
-
sortObj[field.substring(1)] = -1;
|
|
138
|
-
} else {
|
|
139
|
-
sortObj[field] = 1;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return sortObj;
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Parse standard filter parameter (filter[field]=value)
|
|
146
|
-
*/
|
|
147
|
-
_parseFilters(filters) {
|
|
148
|
-
const parsedFilters = {};
|
|
149
|
-
const regexFields = {};
|
|
150
|
-
for (const [key, value] of Object.entries(filters)) {
|
|
151
|
-
if (this.dangerousOperators.includes(key) || key.startsWith("$") && !["$or", "$and"].includes(key)) {
|
|
152
|
-
console.warn(`[mongokit] Blocked dangerous operator: ${key}`);
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
if (["page", "limit", "sort", "populate", "search", "select", "lean", "includeDeleted"].includes(key)) {
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
const operatorMatch = key.match(/^(.+)\[(.+)\]$/);
|
|
159
|
-
if (operatorMatch) {
|
|
160
|
-
const [, , operator] = operatorMatch;
|
|
161
|
-
if (this.dangerousOperators.includes("$" + operator)) {
|
|
162
|
-
console.warn(`[mongokit] Blocked dangerous operator: ${operator}`);
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
this._handleOperatorSyntax(parsedFilters, regexFields, operatorMatch, value);
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
169
|
-
this._handleBracketSyntax(key, value, parsedFilters);
|
|
170
|
-
} else {
|
|
171
|
-
parsedFilters[key] = this._convertValue(value);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return parsedFilters;
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Handle operator syntax: field[operator]=value
|
|
178
|
-
*/
|
|
179
|
-
_handleOperatorSyntax(filters, regexFields, operatorMatch, value) {
|
|
180
|
-
const [, field, operator] = operatorMatch;
|
|
181
|
-
if (operator.toLowerCase() === "options" && regexFields[field]) {
|
|
182
|
-
const fieldValue = filters[field];
|
|
183
|
-
if (typeof fieldValue === "object" && fieldValue !== null && "$regex" in fieldValue) {
|
|
184
|
-
fieldValue.$options = value;
|
|
185
|
-
}
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
if (operator.toLowerCase() === "contains" || operator.toLowerCase() === "like") {
|
|
189
|
-
const safeRegex = this._createSafeRegex(value);
|
|
190
|
-
if (safeRegex) {
|
|
191
|
-
filters[field] = { $regex: safeRegex };
|
|
192
|
-
regexFields[field] = true;
|
|
193
|
-
}
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
const mongoOperator = this._toMongoOperator(operator);
|
|
197
|
-
if (this.dangerousOperators.includes(mongoOperator)) {
|
|
198
|
-
console.warn(`[mongokit] Blocked dangerous operator in field[${operator}]: ${mongoOperator}`);
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
if (mongoOperator === "$eq") {
|
|
202
|
-
filters[field] = value;
|
|
203
|
-
} else if (mongoOperator === "$regex") {
|
|
204
|
-
filters[field] = { $regex: value };
|
|
205
|
-
regexFields[field] = true;
|
|
206
|
-
} else {
|
|
207
|
-
if (typeof filters[field] !== "object" || filters[field] === null || Array.isArray(filters[field])) {
|
|
208
|
-
filters[field] = {};
|
|
209
|
-
}
|
|
210
|
-
let processedValue;
|
|
211
|
-
const op = operator.toLowerCase();
|
|
212
|
-
if (["gt", "gte", "lt", "lte", "size"].includes(op)) {
|
|
213
|
-
processedValue = parseFloat(String(value));
|
|
214
|
-
if (isNaN(processedValue)) return;
|
|
215
|
-
} else if (op === "in" || op === "nin") {
|
|
216
|
-
processedValue = Array.isArray(value) ? value : String(value).split(",").map((v) => v.trim());
|
|
217
|
-
} else {
|
|
218
|
-
processedValue = this._convertValue(value);
|
|
219
|
-
}
|
|
220
|
-
filters[field][mongoOperator] = processedValue;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Handle bracket syntax with object value
|
|
225
|
-
*/
|
|
226
|
-
_handleBracketSyntax(field, operators, parsedFilters) {
|
|
227
|
-
if (!parsedFilters[field]) {
|
|
228
|
-
parsedFilters[field] = {};
|
|
229
|
-
}
|
|
230
|
-
for (const [operator, value] of Object.entries(operators)) {
|
|
231
|
-
if (operator === "between") {
|
|
232
|
-
parsedFilters[field].between = value;
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
if (this.operators[operator]) {
|
|
236
|
-
const mongoOperator = this.operators[operator];
|
|
237
|
-
let processedValue;
|
|
238
|
-
if (["gt", "gte", "lt", "lte", "size"].includes(operator)) {
|
|
239
|
-
processedValue = parseFloat(String(value));
|
|
240
|
-
if (isNaN(processedValue)) continue;
|
|
241
|
-
} else if (operator === "in" || operator === "nin") {
|
|
242
|
-
processedValue = Array.isArray(value) ? value : String(value).split(",").map((v) => v.trim());
|
|
243
|
-
} else if (operator === "like" || operator === "contains") {
|
|
244
|
-
const safeRegex = this._createSafeRegex(value);
|
|
245
|
-
if (!safeRegex) continue;
|
|
246
|
-
processedValue = safeRegex;
|
|
247
|
-
} else {
|
|
248
|
-
processedValue = this._convertValue(value);
|
|
249
|
-
}
|
|
250
|
-
parsedFilters[field][mongoOperator] = processedValue;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Convert operator to MongoDB format
|
|
256
|
-
*/
|
|
257
|
-
_toMongoOperator(operator) {
|
|
258
|
-
const op = operator.toLowerCase();
|
|
259
|
-
return op.startsWith("$") ? op : "$" + op;
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Create a safe regex pattern with protection against ReDoS attacks
|
|
263
|
-
* @param pattern - The pattern string from user input
|
|
264
|
-
* @param flags - Regex flags (default: 'i' for case-insensitive)
|
|
265
|
-
* @returns A safe RegExp or null if pattern is invalid/dangerous
|
|
266
|
-
*/
|
|
267
|
-
_createSafeRegex(pattern, flags = "i") {
|
|
268
|
-
if (pattern === null || pattern === void 0) {
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
271
|
-
const patternStr = String(pattern);
|
|
272
|
-
if (patternStr.length > this.options.maxRegexLength) {
|
|
273
|
-
console.warn(`[mongokit] Regex pattern too long (${patternStr.length} > ${this.options.maxRegexLength}), truncating`);
|
|
274
|
-
return new RegExp(this._escapeRegex(patternStr.substring(0, this.options.maxRegexLength)), flags);
|
|
275
|
-
}
|
|
276
|
-
if (this.dangerousRegexPatterns.test(patternStr)) {
|
|
277
|
-
console.warn("[mongokit] Potentially dangerous regex pattern detected, escaping");
|
|
278
|
-
return new RegExp(this._escapeRegex(patternStr), flags);
|
|
279
|
-
}
|
|
280
|
-
try {
|
|
281
|
-
return new RegExp(patternStr, flags);
|
|
282
|
-
} catch {
|
|
283
|
-
return new RegExp(this._escapeRegex(patternStr), flags);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Escape special regex characters for literal matching
|
|
288
|
-
*/
|
|
289
|
-
_escapeRegex(str) {
|
|
290
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Sanitize text search query for MongoDB $text search
|
|
294
|
-
* @param search - Raw search input from user
|
|
295
|
-
* @returns Sanitized search string or undefined
|
|
296
|
-
*/
|
|
297
|
-
_sanitizeSearch(search) {
|
|
298
|
-
if (search === null || search === void 0 || search === "") {
|
|
299
|
-
return void 0;
|
|
300
|
-
}
|
|
301
|
-
let searchStr = String(search).trim();
|
|
302
|
-
if (!searchStr) {
|
|
303
|
-
return void 0;
|
|
304
|
-
}
|
|
305
|
-
if (searchStr.length > this.options.maxSearchLength) {
|
|
306
|
-
console.warn(`[mongokit] Search query too long (${searchStr.length} > ${this.options.maxSearchLength}), truncating`);
|
|
307
|
-
searchStr = searchStr.substring(0, this.options.maxSearchLength);
|
|
308
|
-
}
|
|
309
|
-
return searchStr;
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Convert values based on operator type
|
|
313
|
-
*/
|
|
314
|
-
_convertValue(value) {
|
|
315
|
-
if (value === null || value === void 0) return value;
|
|
316
|
-
if (Array.isArray(value)) return value.map((v) => this._convertValue(v));
|
|
317
|
-
if (typeof value === "object") return value;
|
|
318
|
-
const stringValue = String(value);
|
|
319
|
-
if (stringValue === "true") return true;
|
|
320
|
-
if (stringValue === "false") return false;
|
|
321
|
-
if (mongoose2.Types.ObjectId.isValid(stringValue) && stringValue.length === 24) {
|
|
322
|
-
return stringValue;
|
|
323
|
-
}
|
|
324
|
-
return stringValue;
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Parse $or conditions
|
|
328
|
-
*/
|
|
329
|
-
_parseOr(query) {
|
|
330
|
-
const orArray = [];
|
|
331
|
-
const raw = query?.or || query?.OR || query?.$or;
|
|
332
|
-
if (!raw) return void 0;
|
|
333
|
-
const items = Array.isArray(raw) ? raw : typeof raw === "object" ? Object.values(raw) : [];
|
|
334
|
-
for (const item of items) {
|
|
335
|
-
if (typeof item === "object" && item) {
|
|
336
|
-
orArray.push(this._parseFilters(item));
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
return orArray.length ? orArray : void 0;
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Enhance filters with between operator
|
|
343
|
-
*/
|
|
344
|
-
_enhanceWithBetween(filters) {
|
|
345
|
-
const output = { ...filters };
|
|
346
|
-
for (const [key, value] of Object.entries(filters || {})) {
|
|
347
|
-
if (value && typeof value === "object" && "between" in value) {
|
|
348
|
-
const between = value.between;
|
|
349
|
-
const [from, to] = String(between).split(",").map((s) => s.trim());
|
|
350
|
-
const fromDate = from ? new Date(from) : void 0;
|
|
351
|
-
const toDate = to ? new Date(to) : void 0;
|
|
352
|
-
const range = {};
|
|
353
|
-
if (fromDate && !isNaN(fromDate.getTime())) range.$gte = fromDate;
|
|
354
|
-
if (toDate && !isNaN(toDate.getTime())) range.$lte = toDate;
|
|
355
|
-
output[key] = range;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
return output;
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
var defaultQueryParser = new QueryParser();
|
|
362
|
-
var queryParser_default = defaultQueryParser;
|
|
363
|
-
function isMongooseSchema(value) {
|
|
364
|
-
return value instanceof mongoose2.Schema;
|
|
365
|
-
}
|
|
366
|
-
function isPlainObject(value) {
|
|
367
|
-
return Object.prototype.toString.call(value) === "[object Object]";
|
|
368
|
-
}
|
|
369
|
-
function isObjectIdType(t) {
|
|
370
|
-
return t === mongoose2.Schema.Types.ObjectId || t === mongoose2.Types.ObjectId;
|
|
371
|
-
}
|
|
372
48
|
function buildCrudSchemasFromMongooseSchema(mongooseSchema, options = {}) {
|
|
373
|
-
const
|
|
374
|
-
const jsonCreate = buildJsonSchemaForCreate(tree, options);
|
|
49
|
+
const jsonCreate = buildJsonSchemaFromPaths(mongooseSchema, options);
|
|
375
50
|
const jsonUpdate = buildJsonSchemaForUpdate(jsonCreate, options);
|
|
376
51
|
const jsonParams = {
|
|
377
52
|
type: "object",
|
|
378
53
|
properties: { id: { type: "string", pattern: "^[0-9a-fA-F]{24}$" } },
|
|
379
54
|
required: ["id"]
|
|
380
55
|
};
|
|
56
|
+
const tree = mongooseSchema?.obj || {};
|
|
381
57
|
const jsonQuery = buildJsonSchemaForQuery(tree, options);
|
|
382
58
|
return { createBody: jsonCreate, updateBody: jsonUpdate, params: jsonParams, listQuery: jsonQuery };
|
|
383
59
|
}
|
|
@@ -431,88 +107,37 @@ function validateUpdateBody(body = {}, options = {}) {
|
|
|
431
107
|
violations
|
|
432
108
|
};
|
|
433
109
|
}
|
|
434
|
-
function
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
if (
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
110
|
+
function buildJsonSchemaFromPaths(mongooseSchema, options) {
|
|
111
|
+
const properties = {};
|
|
112
|
+
const required = [];
|
|
113
|
+
const paths = mongooseSchema.paths;
|
|
114
|
+
const rootFields = /* @__PURE__ */ new Map();
|
|
115
|
+
for (const [path, schemaType] of Object.entries(paths)) {
|
|
116
|
+
if (path === "_id" || path === "__v") continue;
|
|
117
|
+
const parts = path.split(".");
|
|
118
|
+
const rootField = parts[0];
|
|
119
|
+
if (!rootFields.has(rootField)) {
|
|
120
|
+
rootFields.set(rootField, []);
|
|
121
|
+
}
|
|
122
|
+
rootFields.get(rootField).push({ path, schemaType });
|
|
123
|
+
}
|
|
124
|
+
for (const [rootField, fieldPaths] of rootFields.entries()) {
|
|
125
|
+
if (fieldPaths.length === 1 && fieldPaths[0].path === rootField) {
|
|
126
|
+
const schemaType = fieldPaths[0].schemaType;
|
|
127
|
+
properties[rootField] = schemaTypeToJsonSchema(schemaType);
|
|
128
|
+
if (schemaType.isRequired) {
|
|
129
|
+
required.push(rootField);
|
|
450
130
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (typedDef.type === Date) {
|
|
457
|
-
const mode = options?.dateAs || "datetime";
|
|
458
|
-
return mode === "date" ? { type: "string", format: "date" } : { type: "string", format: "date-time" };
|
|
459
|
-
}
|
|
460
|
-
if (typedDef.type === Map || typedDef.type === mongoose2.Schema.Types.Map) {
|
|
461
|
-
const ofSchema = jsonTypeFor(typedDef.of || String, options, seen);
|
|
462
|
-
return { type: "object", additionalProperties: ofSchema };
|
|
463
|
-
}
|
|
464
|
-
if (typedDef.type === mongoose2.Schema.Types.Mixed) {
|
|
465
|
-
return { type: "object", additionalProperties: true };
|
|
466
|
-
}
|
|
467
|
-
if (isObjectIdType(typedDef.type)) {
|
|
468
|
-
return { type: "string", pattern: "^[0-9a-fA-F]{24}$" };
|
|
469
|
-
}
|
|
470
|
-
if (isMongooseSchema(typedDef.type)) {
|
|
471
|
-
const obj = typedDef.type.obj;
|
|
472
|
-
if (obj && typeof obj === "object") {
|
|
473
|
-
if (seen.has(obj)) return { type: "object", additionalProperties: true };
|
|
474
|
-
seen.add(obj);
|
|
475
|
-
return convertTreeToJsonSchema(obj, options, seen);
|
|
131
|
+
} else {
|
|
132
|
+
const nestedSchema = buildNestedJsonSchema(fieldPaths, rootField);
|
|
133
|
+
properties[rootField] = nestedSchema.schema;
|
|
134
|
+
if (nestedSchema.required) {
|
|
135
|
+
required.push(rootField);
|
|
476
136
|
}
|
|
477
137
|
}
|
|
478
138
|
}
|
|
479
|
-
if (def === String) return { type: "string" };
|
|
480
|
-
if (def === Number) return { type: "number" };
|
|
481
|
-
if (def === Boolean) return { type: "boolean" };
|
|
482
|
-
if (def === Date) {
|
|
483
|
-
const mode = options?.dateAs || "datetime";
|
|
484
|
-
return mode === "date" ? { type: "string", format: "date" } : { type: "string", format: "date-time" };
|
|
485
|
-
}
|
|
486
|
-
if (isObjectIdType(def)) return { type: "string", pattern: "^[0-9a-fA-F]{24}$" };
|
|
487
|
-
if (isPlainObject(def)) {
|
|
488
|
-
if (seen.has(def)) return { type: "object", additionalProperties: true };
|
|
489
|
-
seen.add(def);
|
|
490
|
-
return convertTreeToJsonSchema(def, options, seen);
|
|
491
|
-
}
|
|
492
|
-
return {};
|
|
493
|
-
}
|
|
494
|
-
function convertTreeToJsonSchema(tree, options, seen = /* @__PURE__ */ new WeakSet()) {
|
|
495
|
-
if (!tree || typeof tree !== "object") {
|
|
496
|
-
return { type: "object", properties: {} };
|
|
497
|
-
}
|
|
498
|
-
if (seen.has(tree)) {
|
|
499
|
-
return { type: "object", additionalProperties: true };
|
|
500
|
-
}
|
|
501
|
-
seen.add(tree);
|
|
502
|
-
const properties = {};
|
|
503
|
-
const required = [];
|
|
504
|
-
for (const [key, val] of Object.entries(tree || {})) {
|
|
505
|
-
if (key === "__v" || key === "_id" || key === "id") continue;
|
|
506
|
-
const cfg = isPlainObject(val) && "type" in val ? val : { };
|
|
507
|
-
properties[key] = jsonTypeFor(val, options, seen);
|
|
508
|
-
if (cfg.required === true) required.push(key);
|
|
509
|
-
}
|
|
510
139
|
const schema = { type: "object", properties };
|
|
511
140
|
if (required.length) schema.required = required;
|
|
512
|
-
return schema;
|
|
513
|
-
}
|
|
514
|
-
function buildJsonSchemaForCreate(tree, options) {
|
|
515
|
-
const base = convertTreeToJsonSchema(tree, options, /* @__PURE__ */ new WeakSet());
|
|
516
141
|
const fieldsToOmit = /* @__PURE__ */ new Set(["createdAt", "updatedAt", "__v"]);
|
|
517
142
|
(options?.create?.omitFields || []).forEach((f) => fieldsToOmit.add(f));
|
|
518
143
|
const fieldRules = options?.fieldRules || {};
|
|
@@ -522,37 +147,96 @@ function buildJsonSchemaForCreate(tree, options) {
|
|
|
522
147
|
}
|
|
523
148
|
});
|
|
524
149
|
fieldsToOmit.forEach((field) => {
|
|
525
|
-
if (
|
|
526
|
-
delete
|
|
150
|
+
if (schema.properties?.[field]) {
|
|
151
|
+
delete schema.properties[field];
|
|
527
152
|
}
|
|
528
|
-
if (
|
|
529
|
-
|
|
153
|
+
if (schema.required) {
|
|
154
|
+
schema.required = schema.required.filter((k) => k !== field);
|
|
530
155
|
}
|
|
531
156
|
});
|
|
532
157
|
const reqOv = options?.create?.requiredOverrides || {};
|
|
533
158
|
const optOv = options?.create?.optionalOverrides || {};
|
|
534
|
-
|
|
159
|
+
schema.required = schema.required || [];
|
|
535
160
|
for (const [k, v] of Object.entries(reqOv)) {
|
|
536
|
-
if (v && !
|
|
161
|
+
if (v && !schema.required.includes(k)) schema.required.push(k);
|
|
537
162
|
}
|
|
538
163
|
for (const [k, v] of Object.entries(optOv)) {
|
|
539
|
-
if (v &&
|
|
164
|
+
if (v && schema.required) schema.required = schema.required.filter((x) => x !== k);
|
|
540
165
|
}
|
|
541
166
|
Object.entries(fieldRules).forEach(([field, rules]) => {
|
|
542
|
-
if (rules.optional &&
|
|
543
|
-
|
|
167
|
+
if (rules.optional && schema.required) {
|
|
168
|
+
schema.required = schema.required.filter((x) => x !== field);
|
|
544
169
|
}
|
|
545
170
|
});
|
|
546
171
|
const schemaOverrides = options?.create?.schemaOverrides || {};
|
|
547
172
|
for (const [k, override] of Object.entries(schemaOverrides)) {
|
|
548
|
-
if (
|
|
549
|
-
|
|
173
|
+
if (schema.properties?.[k]) {
|
|
174
|
+
schema.properties[k] = override;
|
|
550
175
|
}
|
|
551
176
|
}
|
|
552
177
|
if (options?.strictAdditionalProperties === true) {
|
|
553
|
-
|
|
178
|
+
schema.additionalProperties = false;
|
|
179
|
+
}
|
|
180
|
+
return schema;
|
|
181
|
+
}
|
|
182
|
+
function buildNestedJsonSchema(fieldPaths, rootField) {
|
|
183
|
+
const properties = {};
|
|
184
|
+
const required = [];
|
|
185
|
+
let hasRequiredFields = false;
|
|
186
|
+
for (const { path, schemaType } of fieldPaths) {
|
|
187
|
+
const relativePath = path.substring(rootField.length + 1);
|
|
188
|
+
const parts = relativePath.split(".");
|
|
189
|
+
if (parts.length === 1) {
|
|
190
|
+
properties[parts[0]] = schemaTypeToJsonSchema(schemaType);
|
|
191
|
+
if (schemaType.isRequired) {
|
|
192
|
+
required.push(parts[0]);
|
|
193
|
+
hasRequiredFields = true;
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
const fieldName = parts[0];
|
|
197
|
+
if (!properties[fieldName]) {
|
|
198
|
+
properties[fieldName] = { type: "object", properties: {} };
|
|
199
|
+
}
|
|
200
|
+
const nestedObj = properties[fieldName];
|
|
201
|
+
if (!nestedObj.properties) nestedObj.properties = {};
|
|
202
|
+
const deepPath = parts.slice(1).join(".");
|
|
203
|
+
nestedObj.properties[deepPath] = schemaTypeToJsonSchema(schemaType);
|
|
204
|
+
}
|
|
554
205
|
}
|
|
555
|
-
|
|
206
|
+
const schema = { type: "object", properties };
|
|
207
|
+
if (required.length) schema.required = required;
|
|
208
|
+
return { schema, required: hasRequiredFields };
|
|
209
|
+
}
|
|
210
|
+
function schemaTypeToJsonSchema(schemaType) {
|
|
211
|
+
const result = {};
|
|
212
|
+
const instance = schemaType.instance;
|
|
213
|
+
const options = schemaType.options || {};
|
|
214
|
+
if (instance === "String") {
|
|
215
|
+
result.type = "string";
|
|
216
|
+
if (typeof options.minlength === "number") result.minLength = options.minlength;
|
|
217
|
+
if (typeof options.maxlength === "number") result.maxLength = options.maxlength;
|
|
218
|
+
if (options.match instanceof RegExp) result.pattern = options.match.source;
|
|
219
|
+
if (options.enum && Array.isArray(options.enum)) result.enum = options.enum;
|
|
220
|
+
} else if (instance === "Number") {
|
|
221
|
+
result.type = "number";
|
|
222
|
+
if (typeof options.min === "number") result.minimum = options.min;
|
|
223
|
+
if (typeof options.max === "number") result.maximum = options.max;
|
|
224
|
+
} else if (instance === "Boolean") {
|
|
225
|
+
result.type = "boolean";
|
|
226
|
+
} else if (instance === "Date") {
|
|
227
|
+
result.type = "string";
|
|
228
|
+
result.format = "date-time";
|
|
229
|
+
} else if (instance === "ObjectId" || instance === "ObjectID") {
|
|
230
|
+
result.type = "string";
|
|
231
|
+
result.pattern = "^[0-9a-fA-F]{24}$";
|
|
232
|
+
} else if (instance === "Array") {
|
|
233
|
+
result.type = "array";
|
|
234
|
+
result.items = { type: "string" };
|
|
235
|
+
} else {
|
|
236
|
+
result.type = "object";
|
|
237
|
+
result.additionalProperties = true;
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
556
240
|
}
|
|
557
241
|
function buildJsonSchemaForUpdate(createJson, options) {
|
|
558
242
|
const clone = JSON.parse(JSON.stringify(createJson));
|
|
@@ -573,6 +257,9 @@ function buildJsonSchemaForUpdate(createJson, options) {
|
|
|
573
257
|
if (options?.strictAdditionalProperties === true) {
|
|
574
258
|
clone.additionalProperties = false;
|
|
575
259
|
}
|
|
260
|
+
if (options?.update?.requireAtLeastOne === true) {
|
|
261
|
+
clone.minProperties = 1;
|
|
262
|
+
}
|
|
576
263
|
return clone;
|
|
577
264
|
}
|
|
578
265
|
function buildJsonSchemaForQuery(_tree, options) {
|
|
@@ -708,4 +395,4 @@ function listPattern(prefix, model) {
|
|
|
708
395
|
return `${prefix}:list:${model}:*`;
|
|
709
396
|
}
|
|
710
397
|
|
|
711
|
-
export {
|
|
398
|
+
export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getSystemManagedFields, isFieldUpdateAllowed, listPattern, listQueryKey, modelPattern, validateUpdateBody, versionKey };
|
package/package.json
CHANGED