@classytic/mongokit 3.6.1 → 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.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { $ as ReadPreferenceType, A as InferDocument, At as LookupOptions, B as NonNullableFields, C as EventPayload, Ct as WithPlugins, D as GroupResult, Dt as IRequestContext, E as FieldRules, Et as IControllerResponse, F as KeysetPaginationResult, G as PaginationConfig, H as OffsetPaginationOptions, I as Logger, J as Plugin, K as PaginationResult, L as LookupPopulateOptions, M as JsonSchema, N as KeysOfType, O as HookMode, Ot as IResponseFormatter, P as KeysetPaginationOptions, Q as ReadOptions, R as LookupPopulateResult, S as EventHandlers, St as ValidatorDefinition, T as FieldPreset, Tt as IController, U as OffsetPaginationResult, V as ObjectId, W as OperationOptions, X as PluginType, Y as PluginFunction, Z as PopulateSpec, _ as CrudSchemas, _t as UpdateOptions, a as AnyDocument, at as RequiredBy, b as DeleteResult, bt as ValidationChainOptions, c as CacheOperationOptions, ct as SessionOptions, d as CacheableOptions, dt as SoftDeleteRepository, et as RepositoryContext, f as CascadeOptions, ft as SortDirection, g as CreateOptions, gt as UpdateManyResult, h as CreateInput, ht as UpdateInput, i as AllPluginMethods, it as RepositoryOptions, j as InferRawDoc, k as HttpError, kt as LookupBuilder, l as CacheOptions, lt as SoftDeleteFilterMode, m as CollationOptions, mt as Strict, n as AggregatePaginationOptions, nt as RepositoryInstance, o as AnyModel, ot as SchemaBuilderOptions, p as CascadeRelation, pt as SortSpec, q as PartialBy, r as AggregatePaginationResult, rt as RepositoryOperation, s as CacheAdapter, st as SelectSpec, t as AggregateOptions, tt as RepositoryEvent, u as CacheStats, ut as SoftDeleteOptions, v as DecodedCursor, vt as UpdateWithValidationResult, w as EventPhase, wt as WithTransactionOptions, x as DocField, xt as ValidationResult, y as DeepPartial, yt as UserContext, z as MinMaxResult } from "./types-DiQfDesD.mjs";
2
2
  import { t as index_d_exports } from "./index-BSRSsRRx.mjs";
3
3
  import { PaginationEngine } from "./pagination/PaginationEngine.mjs";
4
- import { A as dateSequentialId, B as AuditEntry, C as elasticSearchPlugin, D as PrefixedIdOptions, E as IdGenerator, F as CacheMethods, G as AuditTrailOptions, H as AuditQueryOptions, I as cachePlugin, J as auditLogPlugin, K as AuditTrailQuery, L as BatchOperationsMethods, M as prefixedId, N as sequentialId, O as SequentialIdOptions, P as cascadePlugin, R as BulkWriteResult, S as ElasticSearchOptions, T as DateSequentialIdOptions, U as AuditQueryResult, V as AuditOperation, W as AuditTrailMethods, X as aggregateHelpersPlugin, Y as AggregateHelpersMethods, _ as MongoOperationsMethods, a as uniqueField, b as methodRegistryPlugin, c as SubdocumentMethods, d as softDeletePlugin, f as ObservabilityOptions, g as multiTenantPlugin, h as MultiTenantOptions, i as requireField, j as getNextSequence, k as customIdPlugin, l as subdocumentPlugin, m as observabilityPlugin, n as blockIf, o as validationChainPlugin, p as OperationMetric, q as auditTrailPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as SoftDeleteMethods, v as mongoOperationsPlugin, w as CustomIdOptions, x as fieldFilterPlugin, z as batchOperationsPlugin } from "./validation-chain.plugin-mI2Z19lM.mjs";
4
+ import { A as dateSequentialId, B as AuditEntry, C as elasticSearchPlugin, D as PrefixedIdOptions, E as IdGenerator, F as CacheMethods, G as AuditTrailOptions, H as AuditQueryOptions, I as cachePlugin, J as auditLogPlugin, K as AuditTrailQuery, L as BatchOperationsMethods, M as prefixedId, N as sequentialId, O as SequentialIdOptions, P as cascadePlugin, R as BulkWriteResult, S as ElasticSearchOptions, T as DateSequentialIdOptions, U as AuditQueryResult, V as AuditOperation, W as AuditTrailMethods, X as aggregateHelpersPlugin, Y as AggregateHelpersMethods, _ as MongoOperationsMethods, a as uniqueField, b as methodRegistryPlugin, c as SubdocumentMethods, d as softDeletePlugin, f as ObservabilityOptions, g as multiTenantPlugin, h as MultiTenantOptions, i as requireField, j as getNextSequence, k as customIdPlugin, l as subdocumentPlugin, m as observabilityPlugin, n as blockIf, o as validationChainPlugin, p as OperationMetric, q as auditTrailPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as SoftDeleteMethods, v as mongoOperationsPlugin, w as CustomIdOptions, x as fieldFilterPlugin, z as batchOperationsPlugin } from "./validation-chain.plugin-BsFzKVnO.mjs";
5
5
  import { n as SchemaPathsLike, t as FieldType$1 } from "./coercion-B19MD38s.mjs";
6
6
  import { n as SchemaIndexes } from "./indexes-qIBCZ3mY.mjs";
7
7
  import { a as isFieldUpdateAllowed, c as configureLogger, d as getFieldsForUser, f as getMongooseProjection, i as getSystemManagedFields, l as createFieldPreset, m as parseDuplicateKeyError, n as buildCrudSchemasFromMongooseSchema, o as validateUpdateBody, p as createError, r as getImmutableFields, s as createMemoryCache, t as buildCrudSchemasFromModel, u as filterResponseData } from "./mongooseToJsonSchema-D2YjHo7W.mjs";
package/dist/index.mjs CHANGED
@@ -2,12 +2,12 @@ import { a as warn, n as parseDuplicateKeyError, r as configureLogger, t as crea
2
2
  import { y as LookupBuilder } from "./update-AVfKWNGt.mjs";
3
3
  import { t as actions_exports } from "./actions/index.mjs";
4
4
  import { t as PaginationEngine } from "./PaginationEngine-CunXkR9p.mjs";
5
- import { A as auditTrailPlugin, C as HOOK_PRIORITY, D as AggregationBuilder, E as withTransaction, M as aggregateHelpersPlugin, O as batchOperationsPlugin, S as cachePlugin, T as isTransactionUnsupported, _ as dateSequentialId, a as uniqueField, b as sequentialId, c as subdocumentPlugin, d as multiTenantPlugin, f as mongoOperationsPlugin, g as customIdPlugin, h as elasticSearchPlugin, i as requireField, j as auditLogPlugin, k as AuditTrailQuery, l as softDeletePlugin, m as fieldFilterPlugin, n as blockIf, o as validationChainPlugin, p as methodRegistryPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as observabilityPlugin, v as getNextSequence, w as Repository, x as cascadePlugin, y as prefixedId } from "./validation-chain.plugin-DoOEyM_g.mjs";
5
+ import { A as auditTrailPlugin, C as HOOK_PRIORITY, D as AggregationBuilder, E as withTransaction, M as aggregateHelpersPlugin, O as batchOperationsPlugin, S as cachePlugin, T as isTransactionUnsupported, _ as dateSequentialId, a as uniqueField, b as sequentialId, c as subdocumentPlugin, d as multiTenantPlugin, f as mongoOperationsPlugin, g as customIdPlugin, h as elasticSearchPlugin, i as requireField, j as auditLogPlugin, k as AuditTrailQuery, l as softDeletePlugin, m as fieldFilterPlugin, n as blockIf, o as validationChainPlugin, p as methodRegistryPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as observabilityPlugin, v as getNextSequence, w as Repository, x as cascadePlugin, y as prefixedId } from "./validation-chain.plugin-7A5qGEQq.mjs";
6
6
  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-8Cyu3NFJ.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-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 };
@@ -1,2 +1,2 @@
1
- import { A as dateSequentialId, B as AuditEntry, C as elasticSearchPlugin, D as PrefixedIdOptions, E as IdGenerator, F as CacheMethods, G as AuditTrailOptions, H as AuditQueryOptions, I as cachePlugin, J as auditLogPlugin, K as AuditTrailQuery, L as BatchOperationsMethods, M as prefixedId, N as sequentialId, O as SequentialIdOptions, P as cascadePlugin, S as ElasticSearchOptions, T as DateSequentialIdOptions, U as AuditQueryResult, V as AuditOperation, W as AuditTrailMethods, X as aggregateHelpersPlugin, Y as AggregateHelpersMethods, _ as MongoOperationsMethods, a as uniqueField, b as methodRegistryPlugin, c as SubdocumentMethods, d as softDeletePlugin, f as ObservabilityOptions, g as multiTenantPlugin, h as MultiTenantOptions, i as requireField, j as getNextSequence, k as customIdPlugin, l as subdocumentPlugin, m as observabilityPlugin, n as blockIf, o as validationChainPlugin, p as OperationMetric, q as auditTrailPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as SoftDeleteMethods, v as mongoOperationsPlugin, w as CustomIdOptions, x as fieldFilterPlugin, y as MethodRegistryRepository, z as batchOperationsPlugin } from "../validation-chain.plugin-mI2Z19lM.mjs";
1
+ import { A as dateSequentialId, B as AuditEntry, C as elasticSearchPlugin, D as PrefixedIdOptions, E as IdGenerator, F as CacheMethods, G as AuditTrailOptions, H as AuditQueryOptions, I as cachePlugin, J as auditLogPlugin, K as AuditTrailQuery, L as BatchOperationsMethods, M as prefixedId, N as sequentialId, O as SequentialIdOptions, P as cascadePlugin, S as ElasticSearchOptions, T as DateSequentialIdOptions, U as AuditQueryResult, V as AuditOperation, W as AuditTrailMethods, X as aggregateHelpersPlugin, Y as AggregateHelpersMethods, _ as MongoOperationsMethods, a as uniqueField, b as methodRegistryPlugin, c as SubdocumentMethods, d as softDeletePlugin, f as ObservabilityOptions, g as multiTenantPlugin, h as MultiTenantOptions, i as requireField, j as getNextSequence, k as customIdPlugin, l as subdocumentPlugin, m as observabilityPlugin, n as blockIf, o as validationChainPlugin, p as OperationMetric, q as auditTrailPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as SoftDeleteMethods, v as mongoOperationsPlugin, w as CustomIdOptions, x as fieldFilterPlugin, y as MethodRegistryRepository, z as batchOperationsPlugin } from "../validation-chain.plugin-BsFzKVnO.mjs";
2
2
  export { type AggregateHelpersMethods, type AuditEntry, type AuditOperation, type AuditQueryOptions, type AuditQueryResult, type AuditTrailMethods, type AuditTrailOptions, AuditTrailQuery, type BatchOperationsMethods, type CacheMethods, type CustomIdOptions, type DateSequentialIdOptions, type ElasticSearchOptions, type IdGenerator, type MethodRegistryRepository, type MongoOperationsMethods, type MultiTenantOptions, type ObservabilityOptions, type OperationMetric, type PrefixedIdOptions, type SequentialIdOptions, type SoftDeleteMethods, type SubdocumentMethods, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, getNextSequence, immutableField, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
@@ -1,2 +1,2 @@
1
- import { A as auditTrailPlugin, M as aggregateHelpersPlugin, O as batchOperationsPlugin, S as cachePlugin, _ as dateSequentialId, a as uniqueField, b as sequentialId, c as subdocumentPlugin, d as multiTenantPlugin, f as mongoOperationsPlugin, g as customIdPlugin, h as elasticSearchPlugin, i as requireField, j as auditLogPlugin, k as AuditTrailQuery, l as softDeletePlugin, m as fieldFilterPlugin, n as blockIf, o as validationChainPlugin, p as methodRegistryPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as observabilityPlugin, v as getNextSequence, x as cascadePlugin, y as prefixedId } from "../validation-chain.plugin-DoOEyM_g.mjs";
1
+ import { A as auditTrailPlugin, M as aggregateHelpersPlugin, O as batchOperationsPlugin, S as cachePlugin, _ as dateSequentialId, a as uniqueField, b as sequentialId, c as subdocumentPlugin, d as multiTenantPlugin, f as mongoOperationsPlugin, g as customIdPlugin, h as elasticSearchPlugin, i as requireField, j as auditLogPlugin, k as AuditTrailQuery, l as softDeletePlugin, m as fieldFilterPlugin, n as blockIf, o as validationChainPlugin, p as methodRegistryPlugin, r as immutableField, s as timestampPlugin, t as autoInject, u as observabilityPlugin, v as getNextSequence, x as cascadePlugin, y as prefixedId } from "../validation-chain.plugin-7A5qGEQq.mjs";
2
2
  export { AuditTrailQuery, aggregateHelpersPlugin, auditLogPlugin, auditTrailPlugin, autoInject, batchOperationsPlugin, blockIf, cachePlugin, cascadePlugin, customIdPlugin, dateSequentialId, elasticSearchPlugin, fieldFilterPlugin, getNextSequence, immutableField, methodRegistryPlugin, mongoOperationsPlugin, multiTenantPlugin, observabilityPlugin, prefixedId, requireField, sequentialId, softDeletePlugin, subdocumentPlugin, timestampPlugin, uniqueField, validationChainPlugin };
@@ -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-8Cyu3NFJ.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-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 };
@@ -3154,6 +3154,11 @@ function getCounterModel(connection) {
3154
3154
  *
3155
3155
  * @param counterKey - Unique key identifying this counter (e.g., "Invoice" or "Invoice:2026-02")
3156
3156
  * @param increment - Value to increment by (default: 1)
3157
+ * @param connection - Mongoose connection to use (defaults to the default connection)
3158
+ * @param session - Optional client session. When provided, the counter bump
3159
+ * participates in the caller's transaction — if the surrounding transaction
3160
+ * aborts, the counter does NOT advance. Without a session, the bump commits
3161
+ * immediately and is not rolled back if a sibling write fails.
3157
3162
  * @returns The next sequence number (after increment)
3158
3163
  *
3159
3164
  * @example
@@ -3163,11 +3168,19 @@ function getCounterModel(connection) {
3163
3168
  * @example Batch increment for createMany
3164
3169
  * const startSeq = await getNextSequence('invoices', 5);
3165
3170
  * // If current was 10, returns 15 (you use 11, 12, 13, 14, 15)
3171
+ *
3172
+ * @example Transactional counter bump
3173
+ * await withTransaction(conn, async (session) => {
3174
+ * const seq = await getNextSequence('invoices', 1, conn, session);
3175
+ * await invoiceRepo.create({ invoiceNumber: `INV-${seq}` }, { session });
3176
+ * // if this throws, the counter is rolled back with the insert
3177
+ * });
3166
3178
  */
3167
- async function getNextSequence(counterKey, increment = 1, connection) {
3179
+ async function getNextSequence(counterKey, increment = 1, connection, session) {
3168
3180
  const result = await getCounterModel(connection).findOneAndUpdate({ _id: counterKey }, { $inc: { seq: increment } }, {
3169
3181
  upsert: true,
3170
- returnDocument: "after"
3182
+ returnDocument: "after",
3183
+ session
3171
3184
  });
3172
3185
  if (!result) throw new Error(`Failed to increment counter '${counterKey}'`);
3173
3186
  return result.seq;
@@ -3190,7 +3203,7 @@ function sequentialId(options) {
3190
3203
  const { prefix, model, padding = 4, separator = "-", counterKey } = options;
3191
3204
  const key = counterKey || model.modelName;
3192
3205
  return async (context) => {
3193
- const seq = await getNextSequence(key, 1, context._counterConnection);
3206
+ const seq = await getNextSequence(key, 1, context._counterConnection, context.session);
3194
3207
  return `${prefix}${separator}${String(seq).padStart(padding, "0")}`;
3195
3208
  };
3196
3209
  }
@@ -3238,7 +3251,7 @@ function dateSequentialId(options) {
3238
3251
  counterKey = `${model.modelName}:${year}-${month}`;
3239
3252
  break;
3240
3253
  }
3241
- const seq = await getNextSequence(counterKey, 1, context._counterConnection);
3254
+ const seq = await getNextSequence(counterKey, 1, context._counterConnection, context.session);
3242
3255
  return `${prefix}${separator}${datePart}${separator}${String(seq).padStart(padding, "0")}`;
3243
3256
  };
3244
3257
  }
@@ -432,6 +432,11 @@ interface CustomIdOptions {
432
432
  *
433
433
  * @param counterKey - Unique key identifying this counter (e.g., "Invoice" or "Invoice:2026-02")
434
434
  * @param increment - Value to increment by (default: 1)
435
+ * @param connection - Mongoose connection to use (defaults to the default connection)
436
+ * @param session - Optional client session. When provided, the counter bump
437
+ * participates in the caller's transaction — if the surrounding transaction
438
+ * aborts, the counter does NOT advance. Without a session, the bump commits
439
+ * immediately and is not rolled back if a sibling write fails.
435
440
  * @returns The next sequence number (after increment)
436
441
  *
437
442
  * @example
@@ -441,8 +446,15 @@ interface CustomIdOptions {
441
446
  * @example Batch increment for createMany
442
447
  * const startSeq = await getNextSequence('invoices', 5);
443
448
  * // If current was 10, returns 15 (you use 11, 12, 13, 14, 15)
449
+ *
450
+ * @example Transactional counter bump
451
+ * await withTransaction(conn, async (session) => {
452
+ * const seq = await getNextSequence('invoices', 1, conn, session);
453
+ * await invoiceRepo.create({ invoiceNumber: `INV-${seq}` }, { session });
454
+ * // if this throws, the counter is rolled back with the insert
455
+ * });
444
456
  */
445
- declare function getNextSequence(counterKey: string, increment?: number, connection?: mongoose.Connection): Promise<number>;
457
+ declare function getNextSequence(counterKey: string, increment?: number, connection?: mongoose.Connection, session?: mongoose.ClientSession): Promise<number>;
446
458
  interface SequentialIdOptions {
447
459
  /** Prefix string (e.g., 'INV', 'ORD') */
448
460
  prefix: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/mongokit",
3
- "version": "3.6.1",
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,
@@ -118,13 +118,17 @@
118
118
  "release:major": "npm version major && npm run release"
119
119
  },
120
120
  "devDependencies": {
121
+ "@arethetypeswrong/cli": "^0.18.2",
121
122
  "@biomejs/biome": "^2.4.10",
122
123
  "@types/node": "^22.0.0",
123
124
  "@vitest/coverage-v8": "^3.2.4",
125
+ "ajv": "^8.18.0",
126
+ "ajv-formats": "^3.0.1",
124
127
  "knip": "^6.3.0",
125
128
  "mongodb-memory-server": "^10.4.3",
126
129
  "mongoose": "^9.4.1",
127
- "tsdown": "^0.21.7",
130
+ "publint": "^0.3.18",
131
+ "tsdown": "^0.21.8",
128
132
  "typescript": "^5.7.0",
129
133
  "vitest": "^3.2.4"
130
134
  }
@@ -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 };