@classytic/mongokit 3.6.2 → 3.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { isGeoOperator, parseGeoFilter } from "./query/primitives/geo.mjs";
|
|
|
7
7
|
import { i as getMongooseProjection, n as filterResponseData, r as getFieldsForUser, t as createFieldPreset } from "./field-selection-C5o7jyyA.mjs";
|
|
8
8
|
import { buildFieldTypeMap, coerceFieldValue, coerceHeuristic } from "./query/primitives/coercion.mjs";
|
|
9
9
|
import { extractSchemaIndexes } from "./query/primitives/indexes.mjs";
|
|
10
|
-
import { a as isFieldUpdateAllowed, i as getSystemManagedFields, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-
|
|
10
|
+
import { a as isFieldUpdateAllowed, i as getSystemManagedFields, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel } from "./mongooseToJsonSchema-MlaD3C0B.mjs";
|
|
11
11
|
//#region src/query/QueryParser.ts
|
|
12
12
|
/**
|
|
13
13
|
* Modern Query Parser
|
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
import { n as isObjectIdInstance } from "./id-resolution-LZ-cCwR5.mjs";
|
|
2
|
+
import mongoose from "mongoose";
|
|
3
|
+
//#region src/utils/memory-cache.ts
|
|
4
|
+
/**
|
|
5
|
+
* Creates an in-memory cache adapter
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Automatic TTL expiration
|
|
9
|
+
* - Pattern-based clearing (simple glob with *)
|
|
10
|
+
* - Max entries limit to prevent memory leaks
|
|
11
|
+
*
|
|
12
|
+
* @param maxEntries - Maximum cache entries before oldest are evicted (default: 1000)
|
|
13
|
+
*/
|
|
14
|
+
function createMemoryCache(maxEntries = 1e3) {
|
|
15
|
+
const cache = /* @__PURE__ */ new Map();
|
|
16
|
+
let lastCleanup = Date.now();
|
|
17
|
+
const CLEANUP_INTERVAL_MS = 6e4;
|
|
18
|
+
function cleanupIfNeeded() {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
if (now - lastCleanup < CLEANUP_INTERVAL_MS) return;
|
|
21
|
+
lastCleanup = now;
|
|
22
|
+
for (const [key, entry] of cache) if (entry.expiresAt < now) cache.delete(key);
|
|
23
|
+
}
|
|
24
|
+
function evictOldest() {
|
|
25
|
+
while (cache.size >= maxEntries) {
|
|
26
|
+
const firstKey = cache.keys().next().value;
|
|
27
|
+
if (firstKey) cache.delete(firstKey);
|
|
28
|
+
else break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
async get(key) {
|
|
33
|
+
const entry = cache.get(key);
|
|
34
|
+
if (!entry) return null;
|
|
35
|
+
if (entry.expiresAt < Date.now()) {
|
|
36
|
+
cache.delete(key);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
cache.delete(key);
|
|
40
|
+
cache.set(key, entry);
|
|
41
|
+
return entry.value;
|
|
42
|
+
},
|
|
43
|
+
async set(key, value, ttl) {
|
|
44
|
+
cache.delete(key);
|
|
45
|
+
if (cache.size >= maxEntries) {
|
|
46
|
+
cleanupIfNeeded();
|
|
47
|
+
evictOldest();
|
|
48
|
+
}
|
|
49
|
+
cache.set(key, {
|
|
50
|
+
value,
|
|
51
|
+
expiresAt: Date.now() + ttl * 1e3
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
async del(key) {
|
|
55
|
+
cache.delete(key);
|
|
56
|
+
},
|
|
57
|
+
async clear(pattern) {
|
|
58
|
+
if (!pattern) {
|
|
59
|
+
cache.clear();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
63
|
+
const regex = new RegExp(`^${escaped}$`);
|
|
64
|
+
for (const key of cache.keys()) if (regex.test(key)) cache.delete(key);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/utils/mongooseToJsonSchema.ts
|
|
70
|
+
/**
|
|
71
|
+
* Mongoose to JSON Schema Converter with Field Rules
|
|
72
|
+
*
|
|
73
|
+
* Generates Fastify JSON schemas from Mongoose models with declarative field rules.
|
|
74
|
+
*
|
|
75
|
+
* Field Rules (options.fieldRules):
|
|
76
|
+
* - immutable: Field cannot be updated (omitted from update schema)
|
|
77
|
+
* - immutableAfterCreate: Alias for immutable
|
|
78
|
+
* - systemManaged: System-only field (omitted from create/update)
|
|
79
|
+
* - optional: Remove from required array
|
|
80
|
+
*
|
|
81
|
+
* Additional Options:
|
|
82
|
+
* - strictAdditionalProperties: Set to true to add "additionalProperties: false" to schemas
|
|
83
|
+
* This makes Fastify reject unknown fields at validation level (default: false for backward compatibility)
|
|
84
|
+
* - update.requireAtLeastOne: Set to true to add "minProperties: 1" to update schema
|
|
85
|
+
* This prevents empty update payloads (default: false)
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* buildCrudSchemasFromModel(Model, {
|
|
89
|
+
* strictAdditionalProperties: true, // Reject unknown fields
|
|
90
|
+
* fieldRules: {
|
|
91
|
+
* organizationId: { immutable: true },
|
|
92
|
+
* status: { systemManaged: true },
|
|
93
|
+
* },
|
|
94
|
+
* create: { omitFields: ['verifiedAt'] },
|
|
95
|
+
* update: {
|
|
96
|
+
* omitFields: ['customerId'],
|
|
97
|
+
* requireAtLeastOne: true // Reject empty updates
|
|
98
|
+
* }
|
|
99
|
+
* })
|
|
100
|
+
*/
|
|
101
|
+
function isMongooseSchema(value) {
|
|
102
|
+
return value instanceof mongoose.Schema;
|
|
103
|
+
}
|
|
104
|
+
function isPlainObject(value) {
|
|
105
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
106
|
+
}
|
|
107
|
+
function isObjectIdType(t) {
|
|
108
|
+
return t === mongoose.Schema.Types.ObjectId || t === mongoose.Types.ObjectId;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Build CRUD schemas from Mongoose schema
|
|
112
|
+
*/
|
|
113
|
+
function buildCrudSchemasFromMongooseSchema(mongooseSchema, options = {}) {
|
|
114
|
+
const jsonCreate = buildJsonSchemaFromPaths(mongooseSchema, options);
|
|
115
|
+
return {
|
|
116
|
+
createBody: jsonCreate,
|
|
117
|
+
updateBody: buildJsonSchemaForUpdate(jsonCreate, options),
|
|
118
|
+
params: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: { id: {
|
|
121
|
+
type: "string",
|
|
122
|
+
pattern: "^[0-9a-fA-F]{24}$"
|
|
123
|
+
} },
|
|
124
|
+
required: ["id"]
|
|
125
|
+
},
|
|
126
|
+
listQuery: buildJsonSchemaForQuery(mongooseSchema?.obj || {}, options)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Build CRUD schemas from Mongoose model
|
|
131
|
+
*/
|
|
132
|
+
function buildCrudSchemasFromModel(mongooseModel, options = {}) {
|
|
133
|
+
if (!mongooseModel?.schema) throw new Error("Invalid mongoose model");
|
|
134
|
+
return buildCrudSchemasFromMongooseSchema(mongooseModel.schema, options);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Collect fields to omit from a generated schema based on field rules and
|
|
138
|
+
* explicit omit lists. This is the single source of truth for "which fields
|
|
139
|
+
* should NOT appear in the create/update body" — called by both the
|
|
140
|
+
* path-based and tree-based schema builders.
|
|
141
|
+
*/
|
|
142
|
+
function collectFieldsToOmit(options, purpose) {
|
|
143
|
+
const result = new Set([
|
|
144
|
+
"createdAt",
|
|
145
|
+
"updatedAt",
|
|
146
|
+
"__v"
|
|
147
|
+
]);
|
|
148
|
+
const rules = options?.fieldRules || {};
|
|
149
|
+
for (const [field, rule] of Object.entries(rules)) {
|
|
150
|
+
if (rule.systemManaged) result.add(field);
|
|
151
|
+
if (purpose === "update" && (rule.immutable || rule.immutableAfterCreate)) result.add(field);
|
|
152
|
+
}
|
|
153
|
+
const explicit = purpose === "create" ? options?.create?.omitFields : options?.update?.omitFields;
|
|
154
|
+
if (explicit) for (const f of explicit) result.add(f);
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Apply omissions + optional-overrides to a built JSON schema in-place.
|
|
159
|
+
* Removes properties and adjusts the `required` array.
|
|
160
|
+
*/
|
|
161
|
+
function applyFieldRules(schema, fieldsToOmit, options) {
|
|
162
|
+
for (const field of fieldsToOmit) {
|
|
163
|
+
if (schema.properties?.[field]) delete schema.properties[field];
|
|
164
|
+
if (schema.required) schema.required = schema.required.filter((k) => k !== field);
|
|
165
|
+
}
|
|
166
|
+
const rules = options?.fieldRules || {};
|
|
167
|
+
for (const [field, rule] of Object.entries(rules)) if (rule.optional && schema.required) schema.required = schema.required.filter((k) => k !== field);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Get fields that are immutable (cannot be updated).
|
|
171
|
+
* Delegates to `collectFieldsToOmit` internally.
|
|
172
|
+
*/
|
|
173
|
+
function getImmutableFields(options = {}) {
|
|
174
|
+
const immutable = [];
|
|
175
|
+
const rules = options?.fieldRules || {};
|
|
176
|
+
for (const [field, rule] of Object.entries(rules)) if (rule.immutable || rule.immutableAfterCreate) immutable.push(field);
|
|
177
|
+
for (const f of options?.update?.omitFields || []) if (!immutable.includes(f)) immutable.push(f);
|
|
178
|
+
return immutable;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get fields that are system-managed (cannot be set by users).
|
|
182
|
+
*/
|
|
183
|
+
function getSystemManagedFields(options = {}) {
|
|
184
|
+
const systemManaged = [];
|
|
185
|
+
const rules = options?.fieldRules || {};
|
|
186
|
+
for (const [field, rule] of Object.entries(rules)) if (rule.systemManaged) systemManaged.push(field);
|
|
187
|
+
return systemManaged;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Check if field is allowed in update
|
|
191
|
+
*/
|
|
192
|
+
function isFieldUpdateAllowed(fieldName, options = {}) {
|
|
193
|
+
const immutableFields = getImmutableFields(options);
|
|
194
|
+
const systemManagedFields = getSystemManagedFields(options);
|
|
195
|
+
return !immutableFields.includes(fieldName) && !systemManagedFields.includes(fieldName);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Validate update body against field rules
|
|
199
|
+
*/
|
|
200
|
+
function validateUpdateBody(body = {}, options = {}) {
|
|
201
|
+
const violations = [];
|
|
202
|
+
const immutableFields = getImmutableFields(options);
|
|
203
|
+
const systemManagedFields = getSystemManagedFields(options);
|
|
204
|
+
Object.keys(body).forEach((field) => {
|
|
205
|
+
if (immutableFields.includes(field)) violations.push({
|
|
206
|
+
field,
|
|
207
|
+
reason: "Field is immutable"
|
|
208
|
+
});
|
|
209
|
+
else if (systemManagedFields.includes(field)) violations.push({
|
|
210
|
+
field,
|
|
211
|
+
reason: "Field is system-managed"
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
valid: violations.length === 0,
|
|
216
|
+
violations
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Build JSON schema from Mongoose schema.paths (accurate type information)
|
|
221
|
+
*/
|
|
222
|
+
function buildJsonSchemaFromPaths(mongooseSchema, options) {
|
|
223
|
+
const properties = {};
|
|
224
|
+
const required = [];
|
|
225
|
+
const paths = mongooseSchema.paths;
|
|
226
|
+
const softRequiredSet = new Set(options?.softRequiredFields ?? []);
|
|
227
|
+
const isSoftRequired = (name, schemaType) => {
|
|
228
|
+
if (softRequiredSet.has(name)) return true;
|
|
229
|
+
return schemaType?.options?.softRequired === true;
|
|
230
|
+
};
|
|
231
|
+
const rootFields = /* @__PURE__ */ new Map();
|
|
232
|
+
for (const [path, schemaType] of Object.entries(paths)) {
|
|
233
|
+
if (path === "__v") continue;
|
|
234
|
+
if (path.includes("$*")) continue;
|
|
235
|
+
if (path === "_id") {
|
|
236
|
+
const instance = schemaType.instance;
|
|
237
|
+
if (!instance || isObjectIdInstance(instance)) continue;
|
|
238
|
+
}
|
|
239
|
+
const rootField = path.split(".")[0];
|
|
240
|
+
if (!rootFields.has(rootField)) rootFields.set(rootField, []);
|
|
241
|
+
rootFields.get(rootField)?.push({
|
|
242
|
+
path,
|
|
243
|
+
schemaType
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
for (const [rootField, fieldPaths] of rootFields.entries()) if (fieldPaths.length === 1 && fieldPaths[0].path === rootField) {
|
|
247
|
+
const schemaType = fieldPaths[0].schemaType;
|
|
248
|
+
properties[rootField] = schemaTypeToJsonSchema(schemaType);
|
|
249
|
+
if (schemaType.isRequired && !isSoftRequired(rootField, schemaType)) required.push(rootField);
|
|
250
|
+
} else {
|
|
251
|
+
const nestedSchema = buildNestedJsonSchema(fieldPaths, rootField);
|
|
252
|
+
properties[rootField] = nestedSchema.schema;
|
|
253
|
+
if (nestedSchema.required) required.push(rootField);
|
|
254
|
+
}
|
|
255
|
+
const schema = {
|
|
256
|
+
type: "object",
|
|
257
|
+
properties
|
|
258
|
+
};
|
|
259
|
+
if (required.length) schema.required = required;
|
|
260
|
+
applyFieldRules(schema, collectFieldsToOmit(options, "create"), options);
|
|
261
|
+
const reqOv = options?.create?.requiredOverrides || {};
|
|
262
|
+
const optOv = options?.create?.optionalOverrides || {};
|
|
263
|
+
schema.required = schema.required || [];
|
|
264
|
+
for (const [k, v] of Object.entries(reqOv)) if (v && !schema.required.includes(k)) schema.required.push(k);
|
|
265
|
+
for (const [k, v] of Object.entries(optOv)) if (v && schema.required) schema.required = schema.required.filter((x) => x !== k);
|
|
266
|
+
const schemaOverrides = options?.create?.schemaOverrides || {};
|
|
267
|
+
for (const [k, override] of Object.entries(schemaOverrides)) if (schema.properties?.[k]) schema.properties[k] = override;
|
|
268
|
+
if (options?.strictAdditionalProperties === true) schema.additionalProperties = false;
|
|
269
|
+
return schema;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Build nested JSON schema from dot-notation paths
|
|
273
|
+
*/
|
|
274
|
+
function buildNestedJsonSchema(fieldPaths, rootField) {
|
|
275
|
+
const properties = {};
|
|
276
|
+
const required = [];
|
|
277
|
+
let hasRequiredFields = false;
|
|
278
|
+
for (const { path, schemaType } of fieldPaths) {
|
|
279
|
+
const parts = path.substring(rootField.length + 1).split(".");
|
|
280
|
+
if (parts.length === 1) {
|
|
281
|
+
properties[parts[0]] = schemaTypeToJsonSchema(schemaType);
|
|
282
|
+
if (schemaType.isRequired) {
|
|
283
|
+
required.push(parts[0]);
|
|
284
|
+
hasRequiredFields = true;
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
const fieldName = parts[0];
|
|
288
|
+
if (!properties[fieldName]) properties[fieldName] = {
|
|
289
|
+
type: "object",
|
|
290
|
+
properties: {}
|
|
291
|
+
};
|
|
292
|
+
const nestedObj = properties[fieldName];
|
|
293
|
+
if (!nestedObj.properties) nestedObj.properties = {};
|
|
294
|
+
const deepPath = parts.slice(1).join(".");
|
|
295
|
+
nestedObj.properties[deepPath] = schemaTypeToJsonSchema(schemaType);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const schema = {
|
|
299
|
+
type: "object",
|
|
300
|
+
properties
|
|
301
|
+
};
|
|
302
|
+
if (required.length) schema.required = required;
|
|
303
|
+
return {
|
|
304
|
+
schema,
|
|
305
|
+
required: hasRequiredFields
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Introspect the element type of a Mongoose array SchemaType and produce a
|
|
310
|
+
* JSON Schema `items` clause that matches what clients will actually POST.
|
|
311
|
+
*
|
|
312
|
+
* Falls through four tiers so we cover every Mongoose array shape:
|
|
313
|
+
*
|
|
314
|
+
* 1. DocumentArray / explicit subschema — `schemaType.schema.paths` exists.
|
|
315
|
+
* Recurse into the inner paths so `[{ name, url }]` yields
|
|
316
|
+
* `items: { type: 'object', properties: { name, url }, required: [...] }`.
|
|
317
|
+
* 2. Legacy Mongoose (v6/v7) — `schemaType.caster` exposes the casted
|
|
318
|
+
* element's `instance` ('String', 'Number', 'ObjectId', …).
|
|
319
|
+
* 3. Modern Mongoose (v8+/v9) — `caster` is undefined for
|
|
320
|
+
* `[{ type: String }]`; the declaration lives on
|
|
321
|
+
* `schemaType.options.type[0]`. Hand it to `jsonTypeFor` which already
|
|
322
|
+
* handles bare constructors, `{ type: Fn }` shorthand, and Mixed.
|
|
323
|
+
* 4. Everything else (Mixed arrays, unknown casters) — a permissive
|
|
324
|
+
* `{ type: 'object', additionalProperties: true }` so we never block a
|
|
325
|
+
* valid payload with the old `{ type: 'string' }` default.
|
|
326
|
+
*/
|
|
327
|
+
function introspectArrayItems(schemaType) {
|
|
328
|
+
if (hasInnerSchema(schemaType)) return subSchemaToJsonSchema(schemaType.schema);
|
|
329
|
+
const caster = schemaType?.caster;
|
|
330
|
+
if (caster && typeof caster === "object" && "instance" in caster && caster.instance) return schemaTypeToJsonSchema(caster);
|
|
331
|
+
const declaredType = schemaType?.options?.type;
|
|
332
|
+
if (Array.isArray(declaredType) && declaredType.length > 0) {
|
|
333
|
+
const inner = declaredType[0];
|
|
334
|
+
if (inner === mongoose.Schema.Types.Mixed) return {
|
|
335
|
+
type: "object",
|
|
336
|
+
additionalProperties: true
|
|
337
|
+
};
|
|
338
|
+
return jsonTypeFor(inner, {}, /* @__PURE__ */ new WeakSet());
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
type: "object",
|
|
342
|
+
additionalProperties: true
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Convert a Mongoose sub-Schema (as found on a DocumentArray's `.schema`)
|
|
347
|
+
* into a JSON Schema object clause. Unlike `buildJsonSchemaFromPaths`, this
|
|
348
|
+
* does NOT apply create/update field rules — subdocument shape should faithfully
|
|
349
|
+
* reflect the declared schema, not the top-level CRUD-mode omissions.
|
|
350
|
+
*/
|
|
351
|
+
function subSchemaToJsonSchema(subSchema) {
|
|
352
|
+
const properties = {};
|
|
353
|
+
const required = [];
|
|
354
|
+
const paths = subSchema.paths ?? {};
|
|
355
|
+
for (const [path, st] of Object.entries(paths)) {
|
|
356
|
+
if (path === "__v") continue;
|
|
357
|
+
if (path === "_id") {
|
|
358
|
+
const instance = st.instance;
|
|
359
|
+
if (!instance || isObjectIdInstance(instance)) continue;
|
|
360
|
+
}
|
|
361
|
+
properties[path] = schemaTypeToJsonSchema(st);
|
|
362
|
+
if (st.isRequired) required.push(path);
|
|
363
|
+
}
|
|
364
|
+
const result = {
|
|
365
|
+
type: "object",
|
|
366
|
+
properties
|
|
367
|
+
};
|
|
368
|
+
if (required.length) result.required = required;
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Convert Mongoose SchemaType to JSON Schema
|
|
373
|
+
*/
|
|
374
|
+
function schemaTypeToJsonSchema(schemaType) {
|
|
375
|
+
if (typeof schemaType?.jsonSchema === "function") try {
|
|
376
|
+
const custom = schemaType.jsonSchema();
|
|
377
|
+
if (custom && typeof custom === "object") return custom;
|
|
378
|
+
} catch {}
|
|
379
|
+
const result = {};
|
|
380
|
+
const instance = schemaType.instance;
|
|
381
|
+
const options = schemaType.options || {};
|
|
382
|
+
if (instance === "String") {
|
|
383
|
+
result.type = "string";
|
|
384
|
+
const minLen = typeof options.minlength === "number" ? options.minlength : typeof options.minLength === "number" ? options.minLength : void 0;
|
|
385
|
+
const maxLen = typeof options.maxlength === "number" ? options.maxlength : typeof options.maxLength === "number" ? options.maxLength : void 0;
|
|
386
|
+
if (minLen !== void 0) result.minLength = minLen;
|
|
387
|
+
if (maxLen !== void 0) result.maxLength = maxLen;
|
|
388
|
+
if (options.match instanceof RegExp) result.pattern = options.match.source;
|
|
389
|
+
else if (typeof options.match === "string") result.pattern = options.match;
|
|
390
|
+
const enumValues = Array.isArray(options.enum) ? options.enum : options.enum && Array.isArray(options.enum.values) ? options.enum.values : void 0;
|
|
391
|
+
if (enumValues) result.enum = enumValues;
|
|
392
|
+
} else if (instance === "Number") {
|
|
393
|
+
result.type = "number";
|
|
394
|
+
if (typeof options.min === "number") result.minimum = options.min;
|
|
395
|
+
if (typeof options.max === "number") result.maximum = options.max;
|
|
396
|
+
} else if (instance === "Boolean") result.type = "boolean";
|
|
397
|
+
else if (instance === "Date") {
|
|
398
|
+
result.type = "string";
|
|
399
|
+
result.format = "date-time";
|
|
400
|
+
} else if (isObjectIdInstance(instance)) {
|
|
401
|
+
result.type = "string";
|
|
402
|
+
result.pattern = "^[0-9a-fA-F]{24}$";
|
|
403
|
+
if (typeof options.ref === "string" && options.ref.length > 0) result["x-ref"] = options.ref;
|
|
404
|
+
} else if (instance === "Array") {
|
|
405
|
+
result.type = "array";
|
|
406
|
+
result.items = introspectArrayItems(schemaType);
|
|
407
|
+
} else if (instance === "Map") {
|
|
408
|
+
result.type = "object";
|
|
409
|
+
const ofDef = options.of;
|
|
410
|
+
if (ofDef !== void 0 && ofDef !== null) result.additionalProperties = jsonTypeFor(ofDef, {}, /* @__PURE__ */ new WeakSet());
|
|
411
|
+
else result.additionalProperties = true;
|
|
412
|
+
} else if (hasInnerSchema(schemaType)) return subSchemaToJsonSchema(schemaType.schema);
|
|
413
|
+
else {
|
|
414
|
+
result.type = "object";
|
|
415
|
+
result.additionalProperties = true;
|
|
416
|
+
}
|
|
417
|
+
if (options.default === null && typeof result.type === "string") {
|
|
418
|
+
result.type = [result.type, "null"];
|
|
419
|
+
result.default = null;
|
|
420
|
+
}
|
|
421
|
+
if (typeof options.description === "string" && options.description.length > 0) result.description = options.description;
|
|
422
|
+
if (typeof options.title === "string" && options.title.length > 0) result.title = options.title;
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
function hasInnerSchema(schemaType) {
|
|
426
|
+
const inner = schemaType?.schema;
|
|
427
|
+
return !!inner && typeof inner === "object" && "paths" in inner && typeof inner.paths === "object";
|
|
428
|
+
}
|
|
429
|
+
function jsonTypeFor(def, options, seen) {
|
|
430
|
+
if (Array.isArray(def)) {
|
|
431
|
+
if (def[0] === mongoose.Schema.Types.Mixed) return {
|
|
432
|
+
type: "array",
|
|
433
|
+
items: {
|
|
434
|
+
type: "object",
|
|
435
|
+
additionalProperties: true
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
return {
|
|
439
|
+
type: "array",
|
|
440
|
+
items: jsonTypeFor(def[0] ?? String, options, seen)
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
if (isPlainObject(def) && "type" in def) {
|
|
444
|
+
const typedDef = def;
|
|
445
|
+
if (typedDef.enum && Array.isArray(typedDef.enum) && typedDef.enum.length) return {
|
|
446
|
+
type: "string",
|
|
447
|
+
enum: typedDef.enum.map(String)
|
|
448
|
+
};
|
|
449
|
+
if (Array.isArray(typedDef.type)) {
|
|
450
|
+
const inner = typedDef.type[0] !== void 0 ? typedDef.type[0] : String;
|
|
451
|
+
if (inner === mongoose.Schema.Types.Mixed) return {
|
|
452
|
+
type: "array",
|
|
453
|
+
items: {
|
|
454
|
+
type: "object",
|
|
455
|
+
additionalProperties: true
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
return {
|
|
459
|
+
type: "array",
|
|
460
|
+
items: jsonTypeFor(inner, options, seen)
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const validators = {};
|
|
464
|
+
if (typedDef.type === String) {
|
|
465
|
+
validators.type = "string";
|
|
466
|
+
if (typeof typedDef.minlength === "number") validators.minLength = typedDef.minlength;
|
|
467
|
+
if (typeof typedDef.maxlength === "number") validators.maxLength = typedDef.maxlength;
|
|
468
|
+
if (typedDef.match instanceof RegExp) validators.pattern = typedDef.match.source;
|
|
469
|
+
if (typeof typedDef.lowercase === "boolean" && typedDef.lowercase) validators.pattern = validators.pattern ? `(?=.*[a-z])${validators.pattern}` : "^[a-z]*$";
|
|
470
|
+
if (typeof typedDef.uppercase === "boolean" && typedDef.uppercase) validators.pattern = validators.pattern ? `(?=.*[A-Z])${validators.pattern}` : "^[A-Z]*$";
|
|
471
|
+
if (typeof typedDef.trim === "boolean") {}
|
|
472
|
+
return validators;
|
|
473
|
+
}
|
|
474
|
+
if (typedDef.type === Number) {
|
|
475
|
+
validators.type = "number";
|
|
476
|
+
if (typeof typedDef.min === "number") validators.minimum = typedDef.min;
|
|
477
|
+
if (typeof typedDef.max === "number") validators.maximum = typedDef.max;
|
|
478
|
+
return validators;
|
|
479
|
+
}
|
|
480
|
+
if (typedDef.type === Boolean) return { type: "boolean" };
|
|
481
|
+
if (typedDef.type === Date) return (options?.dateAs || "datetime") === "date" ? {
|
|
482
|
+
type: "string",
|
|
483
|
+
format: "date"
|
|
484
|
+
} : {
|
|
485
|
+
type: "string",
|
|
486
|
+
format: "date-time"
|
|
487
|
+
};
|
|
488
|
+
if (typedDef.type === Map || typedDef.type === mongoose.Schema.Types.Map) return {
|
|
489
|
+
type: "object",
|
|
490
|
+
additionalProperties: jsonTypeFor(typedDef.of || String, options, seen)
|
|
491
|
+
};
|
|
492
|
+
if (typedDef.type === mongoose.Schema.Types.Mixed) return {
|
|
493
|
+
type: "object",
|
|
494
|
+
additionalProperties: true
|
|
495
|
+
};
|
|
496
|
+
if (typedDef.type === Object) {
|
|
497
|
+
if (isPlainObject(typedDef) && Object.keys(typedDef).some((k) => k !== "type")) {
|
|
498
|
+
const nested = {};
|
|
499
|
+
for (const [k, v] of Object.entries(typedDef)) if (k !== "type") nested[k] = v;
|
|
500
|
+
if (Object.keys(nested).length > 0) {
|
|
501
|
+
if (seen.has(nested)) return {
|
|
502
|
+
type: "object",
|
|
503
|
+
additionalProperties: true
|
|
504
|
+
};
|
|
505
|
+
seen.add(nested);
|
|
506
|
+
return convertTreeToJsonSchema(nested, options, seen);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
type: "object",
|
|
511
|
+
additionalProperties: true
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
if (isObjectIdType(typedDef.type)) return {
|
|
515
|
+
type: "string",
|
|
516
|
+
pattern: "^[0-9a-fA-F]{24}$"
|
|
517
|
+
};
|
|
518
|
+
if (isMongooseSchema(typedDef.type)) {
|
|
519
|
+
const obj = typedDef.type.obj;
|
|
520
|
+
if (obj && typeof obj === "object") {
|
|
521
|
+
if (seen.has(obj)) return {
|
|
522
|
+
type: "object",
|
|
523
|
+
additionalProperties: true
|
|
524
|
+
};
|
|
525
|
+
seen.add(obj);
|
|
526
|
+
return convertTreeToJsonSchema(obj, options, seen);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (def === String) return { type: "string" };
|
|
531
|
+
if (def === Number) return { type: "number" };
|
|
532
|
+
if (def === Boolean) return { type: "boolean" };
|
|
533
|
+
if (def === Date) return (options?.dateAs || "datetime") === "date" ? {
|
|
534
|
+
type: "string",
|
|
535
|
+
format: "date"
|
|
536
|
+
} : {
|
|
537
|
+
type: "string",
|
|
538
|
+
format: "date-time"
|
|
539
|
+
};
|
|
540
|
+
if (isObjectIdType(def)) return {
|
|
541
|
+
type: "string",
|
|
542
|
+
pattern: "^[0-9a-fA-F]{24}$"
|
|
543
|
+
};
|
|
544
|
+
if (isMongooseSchema(def)) {
|
|
545
|
+
if (seen.has(def)) return {
|
|
546
|
+
type: "object",
|
|
547
|
+
additionalProperties: true
|
|
548
|
+
};
|
|
549
|
+
seen.add(def);
|
|
550
|
+
return subSchemaToJsonSchema(def);
|
|
551
|
+
}
|
|
552
|
+
if (isPlainObject(def)) {
|
|
553
|
+
if (seen.has(def)) return {
|
|
554
|
+
type: "object",
|
|
555
|
+
additionalProperties: true
|
|
556
|
+
};
|
|
557
|
+
seen.add(def);
|
|
558
|
+
return convertTreeToJsonSchema(def, options, seen);
|
|
559
|
+
}
|
|
560
|
+
return {};
|
|
561
|
+
}
|
|
562
|
+
function convertTreeToJsonSchema(tree, options, seen = /* @__PURE__ */ new WeakSet()) {
|
|
563
|
+
if (!tree || typeof tree !== "object") return {
|
|
564
|
+
type: "object",
|
|
565
|
+
properties: {}
|
|
566
|
+
};
|
|
567
|
+
if (seen.has(tree)) return {
|
|
568
|
+
type: "object",
|
|
569
|
+
additionalProperties: true
|
|
570
|
+
};
|
|
571
|
+
seen.add(tree);
|
|
572
|
+
const properties = {};
|
|
573
|
+
const required = [];
|
|
574
|
+
for (const [key, val] of Object.entries(tree || {})) {
|
|
575
|
+
if (key === "__v" || key === "_id" || key === "id") continue;
|
|
576
|
+
const cfg = isPlainObject(val) && "type" in val ? val : { type: val };
|
|
577
|
+
properties[key] = jsonTypeFor(val, options, seen);
|
|
578
|
+
if (cfg.required === true) required.push(key);
|
|
579
|
+
}
|
|
580
|
+
const schema = {
|
|
581
|
+
type: "object",
|
|
582
|
+
properties
|
|
583
|
+
};
|
|
584
|
+
if (required.length) schema.required = required;
|
|
585
|
+
return schema;
|
|
586
|
+
}
|
|
587
|
+
function buildJsonSchemaForUpdate(createJson, options) {
|
|
588
|
+
const clone = JSON.parse(JSON.stringify(createJson));
|
|
589
|
+
delete clone.required;
|
|
590
|
+
applyFieldRules(clone, collectFieldsToOmit(options, "update"), options);
|
|
591
|
+
if (options?.strictAdditionalProperties === true) clone.additionalProperties = false;
|
|
592
|
+
if (options?.update?.requireAtLeastOne === true) clone.minProperties = 1;
|
|
593
|
+
return clone;
|
|
594
|
+
}
|
|
595
|
+
function buildJsonSchemaForQuery(_tree, options) {
|
|
596
|
+
const basePagination = {
|
|
597
|
+
type: "object",
|
|
598
|
+
properties: {
|
|
599
|
+
page: {
|
|
600
|
+
type: "integer",
|
|
601
|
+
minimum: 1,
|
|
602
|
+
default: 1
|
|
603
|
+
},
|
|
604
|
+
limit: {
|
|
605
|
+
type: "integer",
|
|
606
|
+
minimum: 1,
|
|
607
|
+
default: 20
|
|
608
|
+
},
|
|
609
|
+
sort: { type: "string" },
|
|
610
|
+
populate: { type: "string" },
|
|
611
|
+
search: { type: "string" },
|
|
612
|
+
select: { type: "string" },
|
|
613
|
+
after: { type: "string" },
|
|
614
|
+
lean: {
|
|
615
|
+
type: "boolean",
|
|
616
|
+
default: false
|
|
617
|
+
},
|
|
618
|
+
includeDeleted: {
|
|
619
|
+
type: "boolean",
|
|
620
|
+
default: false
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
additionalProperties: true
|
|
624
|
+
};
|
|
625
|
+
const filterable = options?.query?.filterableFields || {};
|
|
626
|
+
for (const [k, v] of Object.entries(filterable)) if (basePagination.properties) basePagination.properties[k] = v && typeof v === "object" && "type" in v ? v : { type: "string" };
|
|
627
|
+
return basePagination;
|
|
628
|
+
}
|
|
629
|
+
//#endregion
|
|
630
|
+
export { isFieldUpdateAllowed as a, getSystemManagedFields as i, buildCrudSchemasFromMongooseSchema as n, validateUpdateBody as o, getImmutableFields as r, createMemoryCache as s, buildCrudSchemasFromModel as t };
|
package/dist/utils/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as configureLogger, t as createError } from "../error-Bpbi_NKo.mjs";
|
|
2
2
|
import { n as isObjectIdInstance, r as isValidIdForType, t as getSchemaIdType } from "../id-resolution-LZ-cCwR5.mjs";
|
|
3
3
|
import { a as byIdKey, c as listQueryKey, i as getMongooseProjection, l as modelPattern, n as filterResponseData, o as byQueryKey, r as getFieldsForUser, s as listPattern, t as createFieldPreset, u as versionKey } from "../field-selection-C5o7jyyA.mjs";
|
|
4
|
-
import { a as isFieldUpdateAllowed, i as getSystemManagedFields, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-
|
|
4
|
+
import { a as isFieldUpdateAllowed, i as getSystemManagedFields, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel } from "../mongooseToJsonSchema-MlaD3C0B.mjs";
|
|
5
5
|
export { buildCrudSchemasFromModel, buildCrudSchemasFromMongooseSchema, byIdKey, byQueryKey, configureLogger, createError, createFieldPreset, createMemoryCache, filterResponseData, getFieldsForUser, getImmutableFields, getMongooseProjection, getSchemaIdType, getSystemManagedFields, isFieldUpdateAllowed, isObjectIdInstance, isValidIdForType, listPattern, listQueryKey, modelPattern, validateUpdateBody, versionKey };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/mongokit",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.3",
|
|
4
4
|
"description": "Production-grade MongoDB repositories with zero dependencies - smart pagination, events, and plugins",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -122,6 +122,8 @@
|
|
|
122
122
|
"@biomejs/biome": "^2.4.10",
|
|
123
123
|
"@types/node": "^22.0.0",
|
|
124
124
|
"@vitest/coverage-v8": "^3.2.4",
|
|
125
|
+
"ajv": "^8.18.0",
|
|
126
|
+
"ajv-formats": "^3.0.1",
|
|
125
127
|
"knip": "^6.3.0",
|
|
126
128
|
"mongodb-memory-server": "^10.4.3",
|
|
127
129
|
"mongoose": "^9.4.1",
|
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
import { n as isObjectIdInstance } from "./id-resolution-LZ-cCwR5.mjs";
|
|
2
|
-
import "mongoose";
|
|
3
|
-
//#region src/utils/memory-cache.ts
|
|
4
|
-
/**
|
|
5
|
-
* Creates an in-memory cache adapter
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - Automatic TTL expiration
|
|
9
|
-
* - Pattern-based clearing (simple glob with *)
|
|
10
|
-
* - Max entries limit to prevent memory leaks
|
|
11
|
-
*
|
|
12
|
-
* @param maxEntries - Maximum cache entries before oldest are evicted (default: 1000)
|
|
13
|
-
*/
|
|
14
|
-
function createMemoryCache(maxEntries = 1e3) {
|
|
15
|
-
const cache = /* @__PURE__ */ new Map();
|
|
16
|
-
let lastCleanup = Date.now();
|
|
17
|
-
const CLEANUP_INTERVAL_MS = 6e4;
|
|
18
|
-
function cleanupIfNeeded() {
|
|
19
|
-
const now = Date.now();
|
|
20
|
-
if (now - lastCleanup < CLEANUP_INTERVAL_MS) return;
|
|
21
|
-
lastCleanup = now;
|
|
22
|
-
for (const [key, entry] of cache) if (entry.expiresAt < now) cache.delete(key);
|
|
23
|
-
}
|
|
24
|
-
function evictOldest() {
|
|
25
|
-
while (cache.size >= maxEntries) {
|
|
26
|
-
const firstKey = cache.keys().next().value;
|
|
27
|
-
if (firstKey) cache.delete(firstKey);
|
|
28
|
-
else break;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return {
|
|
32
|
-
async get(key) {
|
|
33
|
-
const entry = cache.get(key);
|
|
34
|
-
if (!entry) return null;
|
|
35
|
-
if (entry.expiresAt < Date.now()) {
|
|
36
|
-
cache.delete(key);
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
cache.delete(key);
|
|
40
|
-
cache.set(key, entry);
|
|
41
|
-
return entry.value;
|
|
42
|
-
},
|
|
43
|
-
async set(key, value, ttl) {
|
|
44
|
-
cache.delete(key);
|
|
45
|
-
if (cache.size >= maxEntries) {
|
|
46
|
-
cleanupIfNeeded();
|
|
47
|
-
evictOldest();
|
|
48
|
-
}
|
|
49
|
-
cache.set(key, {
|
|
50
|
-
value,
|
|
51
|
-
expiresAt: Date.now() + ttl * 1e3
|
|
52
|
-
});
|
|
53
|
-
},
|
|
54
|
-
async del(key) {
|
|
55
|
-
cache.delete(key);
|
|
56
|
-
},
|
|
57
|
-
async clear(pattern) {
|
|
58
|
-
if (!pattern) {
|
|
59
|
-
cache.clear();
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
63
|
-
const regex = new RegExp(`^${escaped}$`);
|
|
64
|
-
for (const key of cache.keys()) if (regex.test(key)) cache.delete(key);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
//#endregion
|
|
69
|
-
//#region src/utils/mongooseToJsonSchema.ts
|
|
70
|
-
/**
|
|
71
|
-
* Build CRUD schemas from Mongoose schema
|
|
72
|
-
*/
|
|
73
|
-
function buildCrudSchemasFromMongooseSchema(mongooseSchema, options = {}) {
|
|
74
|
-
const jsonCreate = buildJsonSchemaFromPaths(mongooseSchema, options);
|
|
75
|
-
return {
|
|
76
|
-
createBody: jsonCreate,
|
|
77
|
-
updateBody: buildJsonSchemaForUpdate(jsonCreate, options),
|
|
78
|
-
params: {
|
|
79
|
-
type: "object",
|
|
80
|
-
properties: { id: {
|
|
81
|
-
type: "string",
|
|
82
|
-
pattern: "^[0-9a-fA-F]{24}$"
|
|
83
|
-
} },
|
|
84
|
-
required: ["id"]
|
|
85
|
-
},
|
|
86
|
-
listQuery: buildJsonSchemaForQuery(mongooseSchema?.obj || {}, options)
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Build CRUD schemas from Mongoose model
|
|
91
|
-
*/
|
|
92
|
-
function buildCrudSchemasFromModel(mongooseModel, options = {}) {
|
|
93
|
-
if (!mongooseModel?.schema) throw new Error("Invalid mongoose model");
|
|
94
|
-
return buildCrudSchemasFromMongooseSchema(mongooseModel.schema, options);
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Collect fields to omit from a generated schema based on field rules and
|
|
98
|
-
* explicit omit lists. This is the single source of truth for "which fields
|
|
99
|
-
* should NOT appear in the create/update body" — called by both the
|
|
100
|
-
* path-based and tree-based schema builders.
|
|
101
|
-
*/
|
|
102
|
-
function collectFieldsToOmit(options, purpose) {
|
|
103
|
-
const result = new Set([
|
|
104
|
-
"createdAt",
|
|
105
|
-
"updatedAt",
|
|
106
|
-
"__v"
|
|
107
|
-
]);
|
|
108
|
-
const rules = options?.fieldRules || {};
|
|
109
|
-
for (const [field, rule] of Object.entries(rules)) {
|
|
110
|
-
if (rule.systemManaged) result.add(field);
|
|
111
|
-
if (purpose === "update" && (rule.immutable || rule.immutableAfterCreate)) result.add(field);
|
|
112
|
-
}
|
|
113
|
-
const explicit = purpose === "create" ? options?.create?.omitFields : options?.update?.omitFields;
|
|
114
|
-
if (explicit) for (const f of explicit) result.add(f);
|
|
115
|
-
return result;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Apply omissions + optional-overrides to a built JSON schema in-place.
|
|
119
|
-
* Removes properties and adjusts the `required` array.
|
|
120
|
-
*/
|
|
121
|
-
function applyFieldRules(schema, fieldsToOmit, options) {
|
|
122
|
-
for (const field of fieldsToOmit) {
|
|
123
|
-
if (schema.properties?.[field]) delete schema.properties[field];
|
|
124
|
-
if (schema.required) schema.required = schema.required.filter((k) => k !== field);
|
|
125
|
-
}
|
|
126
|
-
const rules = options?.fieldRules || {};
|
|
127
|
-
for (const [field, rule] of Object.entries(rules)) if (rule.optional && schema.required) schema.required = schema.required.filter((k) => k !== field);
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Get fields that are immutable (cannot be updated).
|
|
131
|
-
* Delegates to `collectFieldsToOmit` internally.
|
|
132
|
-
*/
|
|
133
|
-
function getImmutableFields(options = {}) {
|
|
134
|
-
const immutable = [];
|
|
135
|
-
const rules = options?.fieldRules || {};
|
|
136
|
-
for (const [field, rule] of Object.entries(rules)) if (rule.immutable || rule.immutableAfterCreate) immutable.push(field);
|
|
137
|
-
for (const f of options?.update?.omitFields || []) if (!immutable.includes(f)) immutable.push(f);
|
|
138
|
-
return immutable;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Get fields that are system-managed (cannot be set by users).
|
|
142
|
-
*/
|
|
143
|
-
function getSystemManagedFields(options = {}) {
|
|
144
|
-
const systemManaged = [];
|
|
145
|
-
const rules = options?.fieldRules || {};
|
|
146
|
-
for (const [field, rule] of Object.entries(rules)) if (rule.systemManaged) systemManaged.push(field);
|
|
147
|
-
return systemManaged;
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Check if field is allowed in update
|
|
151
|
-
*/
|
|
152
|
-
function isFieldUpdateAllowed(fieldName, options = {}) {
|
|
153
|
-
const immutableFields = getImmutableFields(options);
|
|
154
|
-
const systemManagedFields = getSystemManagedFields(options);
|
|
155
|
-
return !immutableFields.includes(fieldName) && !systemManagedFields.includes(fieldName);
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Validate update body against field rules
|
|
159
|
-
*/
|
|
160
|
-
function validateUpdateBody(body = {}, options = {}) {
|
|
161
|
-
const violations = [];
|
|
162
|
-
const immutableFields = getImmutableFields(options);
|
|
163
|
-
const systemManagedFields = getSystemManagedFields(options);
|
|
164
|
-
Object.keys(body).forEach((field) => {
|
|
165
|
-
if (immutableFields.includes(field)) violations.push({
|
|
166
|
-
field,
|
|
167
|
-
reason: "Field is immutable"
|
|
168
|
-
});
|
|
169
|
-
else if (systemManagedFields.includes(field)) violations.push({
|
|
170
|
-
field,
|
|
171
|
-
reason: "Field is system-managed"
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
return {
|
|
175
|
-
valid: violations.length === 0,
|
|
176
|
-
violations
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Build JSON schema from Mongoose schema.paths (accurate type information)
|
|
181
|
-
*/
|
|
182
|
-
function buildJsonSchemaFromPaths(mongooseSchema, options) {
|
|
183
|
-
const properties = {};
|
|
184
|
-
const required = [];
|
|
185
|
-
const paths = mongooseSchema.paths;
|
|
186
|
-
const softRequiredSet = new Set(options?.softRequiredFields ?? []);
|
|
187
|
-
const isSoftRequired = (name, schemaType) => {
|
|
188
|
-
if (softRequiredSet.has(name)) return true;
|
|
189
|
-
return schemaType?.options?.softRequired === true;
|
|
190
|
-
};
|
|
191
|
-
const rootFields = /* @__PURE__ */ new Map();
|
|
192
|
-
for (const [path, schemaType] of Object.entries(paths)) {
|
|
193
|
-
if (path === "__v") continue;
|
|
194
|
-
if (path === "_id") {
|
|
195
|
-
const instance = schemaType.instance;
|
|
196
|
-
if (!instance || isObjectIdInstance(instance)) continue;
|
|
197
|
-
}
|
|
198
|
-
const rootField = path.split(".")[0];
|
|
199
|
-
if (!rootFields.has(rootField)) rootFields.set(rootField, []);
|
|
200
|
-
rootFields.get(rootField)?.push({
|
|
201
|
-
path,
|
|
202
|
-
schemaType
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
for (const [rootField, fieldPaths] of rootFields.entries()) if (fieldPaths.length === 1 && fieldPaths[0].path === rootField) {
|
|
206
|
-
const schemaType = fieldPaths[0].schemaType;
|
|
207
|
-
properties[rootField] = schemaTypeToJsonSchema(schemaType);
|
|
208
|
-
if (schemaType.isRequired && !isSoftRequired(rootField, schemaType)) required.push(rootField);
|
|
209
|
-
} else {
|
|
210
|
-
const nestedSchema = buildNestedJsonSchema(fieldPaths, rootField);
|
|
211
|
-
properties[rootField] = nestedSchema.schema;
|
|
212
|
-
if (nestedSchema.required) required.push(rootField);
|
|
213
|
-
}
|
|
214
|
-
const schema = {
|
|
215
|
-
type: "object",
|
|
216
|
-
properties
|
|
217
|
-
};
|
|
218
|
-
if (required.length) schema.required = required;
|
|
219
|
-
applyFieldRules(schema, collectFieldsToOmit(options, "create"), options);
|
|
220
|
-
const reqOv = options?.create?.requiredOverrides || {};
|
|
221
|
-
const optOv = options?.create?.optionalOverrides || {};
|
|
222
|
-
schema.required = schema.required || [];
|
|
223
|
-
for (const [k, v] of Object.entries(reqOv)) if (v && !schema.required.includes(k)) schema.required.push(k);
|
|
224
|
-
for (const [k, v] of Object.entries(optOv)) if (v && schema.required) schema.required = schema.required.filter((x) => x !== k);
|
|
225
|
-
const schemaOverrides = options?.create?.schemaOverrides || {};
|
|
226
|
-
for (const [k, override] of Object.entries(schemaOverrides)) if (schema.properties?.[k]) schema.properties[k] = override;
|
|
227
|
-
if (options?.strictAdditionalProperties === true) schema.additionalProperties = false;
|
|
228
|
-
return schema;
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Build nested JSON schema from dot-notation paths
|
|
232
|
-
*/
|
|
233
|
-
function buildNestedJsonSchema(fieldPaths, rootField) {
|
|
234
|
-
const properties = {};
|
|
235
|
-
const required = [];
|
|
236
|
-
let hasRequiredFields = false;
|
|
237
|
-
for (const { path, schemaType } of fieldPaths) {
|
|
238
|
-
const parts = path.substring(rootField.length + 1).split(".");
|
|
239
|
-
if (parts.length === 1) {
|
|
240
|
-
properties[parts[0]] = schemaTypeToJsonSchema(schemaType);
|
|
241
|
-
if (schemaType.isRequired) {
|
|
242
|
-
required.push(parts[0]);
|
|
243
|
-
hasRequiredFields = true;
|
|
244
|
-
}
|
|
245
|
-
} else {
|
|
246
|
-
const fieldName = parts[0];
|
|
247
|
-
if (!properties[fieldName]) properties[fieldName] = {
|
|
248
|
-
type: "object",
|
|
249
|
-
properties: {}
|
|
250
|
-
};
|
|
251
|
-
const nestedObj = properties[fieldName];
|
|
252
|
-
if (!nestedObj.properties) nestedObj.properties = {};
|
|
253
|
-
const deepPath = parts.slice(1).join(".");
|
|
254
|
-
nestedObj.properties[deepPath] = schemaTypeToJsonSchema(schemaType);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
const schema = {
|
|
258
|
-
type: "object",
|
|
259
|
-
properties
|
|
260
|
-
};
|
|
261
|
-
if (required.length) schema.required = required;
|
|
262
|
-
return {
|
|
263
|
-
schema,
|
|
264
|
-
required: hasRequiredFields
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Convert Mongoose SchemaType to JSON Schema
|
|
269
|
-
*/
|
|
270
|
-
function schemaTypeToJsonSchema(schemaType) {
|
|
271
|
-
const result = {};
|
|
272
|
-
const instance = schemaType.instance;
|
|
273
|
-
const options = schemaType.options || {};
|
|
274
|
-
if (instance === "String") {
|
|
275
|
-
result.type = "string";
|
|
276
|
-
if (typeof options.minlength === "number") result.minLength = options.minlength;
|
|
277
|
-
if (typeof options.maxlength === "number") result.maxLength = options.maxlength;
|
|
278
|
-
if (options.match instanceof RegExp) result.pattern = options.match.source;
|
|
279
|
-
if (options.enum && Array.isArray(options.enum)) result.enum = options.enum;
|
|
280
|
-
} else if (instance === "Number") {
|
|
281
|
-
result.type = "number";
|
|
282
|
-
if (typeof options.min === "number") result.minimum = options.min;
|
|
283
|
-
if (typeof options.max === "number") result.maximum = options.max;
|
|
284
|
-
} else if (instance === "Boolean") result.type = "boolean";
|
|
285
|
-
else if (instance === "Date") {
|
|
286
|
-
result.type = "string";
|
|
287
|
-
result.format = "date-time";
|
|
288
|
-
} else if (isObjectIdInstance(instance)) {
|
|
289
|
-
result.type = "string";
|
|
290
|
-
result.pattern = "^[0-9a-fA-F]{24}$";
|
|
291
|
-
} else if (instance === "Array") {
|
|
292
|
-
result.type = "array";
|
|
293
|
-
result.items = { type: "string" };
|
|
294
|
-
} else {
|
|
295
|
-
result.type = "object";
|
|
296
|
-
result.additionalProperties = true;
|
|
297
|
-
}
|
|
298
|
-
return result;
|
|
299
|
-
}
|
|
300
|
-
function buildJsonSchemaForUpdate(createJson, options) {
|
|
301
|
-
const clone = JSON.parse(JSON.stringify(createJson));
|
|
302
|
-
delete clone.required;
|
|
303
|
-
applyFieldRules(clone, collectFieldsToOmit(options, "update"), options);
|
|
304
|
-
if (options?.strictAdditionalProperties === true) clone.additionalProperties = false;
|
|
305
|
-
if (options?.update?.requireAtLeastOne === true) clone.minProperties = 1;
|
|
306
|
-
return clone;
|
|
307
|
-
}
|
|
308
|
-
function buildJsonSchemaForQuery(_tree, options) {
|
|
309
|
-
const basePagination = {
|
|
310
|
-
type: "object",
|
|
311
|
-
properties: {
|
|
312
|
-
page: { type: "string" },
|
|
313
|
-
limit: { type: "string" },
|
|
314
|
-
sort: { type: "string" },
|
|
315
|
-
populate: { type: "string" },
|
|
316
|
-
search: { type: "string" },
|
|
317
|
-
select: { type: "string" },
|
|
318
|
-
lean: { type: "string" },
|
|
319
|
-
includeDeleted: { type: "string" }
|
|
320
|
-
},
|
|
321
|
-
additionalProperties: true
|
|
322
|
-
};
|
|
323
|
-
const filterable = options?.query?.filterableFields || {};
|
|
324
|
-
for (const [k, v] of Object.entries(filterable)) if (basePagination.properties) basePagination.properties[k] = v && typeof v === "object" && "type" in v ? v : { type: "string" };
|
|
325
|
-
return basePagination;
|
|
326
|
-
}
|
|
327
|
-
//#endregion
|
|
328
|
-
export { isFieldUpdateAllowed as a, getSystemManagedFields as i, buildCrudSchemasFromMongooseSchema as n, validateUpdateBody as o, getImmutableFields as r, createMemoryCache as s, buildCrudSchemasFromModel as t };
|