@classytic/mongokit 3.39.0 → 3.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/{PaginationEngine-DQ7beTCR.mjs → PaginationEngine-Dvl8jcRY.mjs} +33 -17
  2. package/dist/{Repository-D7fDpyFx.mjs → Repository-BMgPSkqZ.mjs} +279 -625
  3. package/dist/{Repository-IxI983_m.d.mts → Repository-CR8Y9g2E.d.mts} +57 -32
  4. package/dist/actions/index.mjs +1 -1
  5. package/dist/ai/index.d.mts +1 -1
  6. package/dist/better-auth/index.mjs +1 -1
  7. package/dist/{create-repository-DOe9E_5F.mjs → create-repository-BzBUMvd1.mjs} +2 -2
  8. package/dist/{create-repository-CRXjDa3N.d.mts → create-repository-CAle1MSU.d.mts} +4 -4
  9. package/dist/idempotency/index.d.mts +23 -0
  10. package/dist/idempotency/index.mjs +134 -0
  11. package/dist/index.d.mts +58 -32
  12. package/dist/index.mjs +285 -675
  13. package/dist/indexes/verify-indexes.d.mts +14 -1
  14. package/dist/indexes/verify-indexes.mjs +54 -7
  15. package/dist/{multi-tenant.plugin-B-tzNeYL.d.mts → multi-tenant.plugin-CBgCo9im.d.mts} +18 -2
  16. package/dist/outbox/index.d.mts +1 -1
  17. package/dist/outbox/index.mjs +6 -4
  18. package/dist/pagination/PaginationEngine.mjs +1 -1
  19. package/dist/{plugin-options-BuvtOSZf.d.mts → plugin-options-Zk46lD6F.d.mts} +15 -1
  20. package/dist/plugins/index.d.mts +8 -3
  21. package/dist/plugins/index.mjs +98 -25
  22. package/dist/{repository-ZbLtL7Nk.d.mts → repository-odl03582.d.mts} +20 -2
  23. package/dist/sync/index.d.mts +40 -0
  24. package/dist/sync/index.mjs +334 -0
  25. package/dist/tenant/index.mjs +2 -0
  26. package/dist/testkit/index.d.mts +2 -2
  27. package/dist/testkit/index.mjs +1 -1
  28. package/dist/{timestamp.plugin-B5eiaItQ.mjs → timestamp.plugin-DF0gJB6p.mjs} +86 -2
  29. package/dist/{to-plain-B8ufk3Qz.d.mts → to-plain-k7SjNlSS.d.mts} +2 -2
  30. package/dist/transaction-B5L9glO0.mjs +533 -0
  31. package/dist/{update-c_01A0if.mjs → update-DG0fCwRJ.mjs} +168 -11
  32. package/dist/utils/index.d.mts +1 -1
  33. package/dist/{validation-chain.plugin-Bju1D1Sn.mjs → validation-chain.plugin-BLY2gLEk.mjs} +192 -279
  34. package/dist/{validation-chain.plugin-DvrLJIbJ.d.mts → validation-chain.plugin-DYp9E7K2.d.mts} +25 -3
  35. package/package.json +13 -4
@@ -95,6 +95,122 @@ async function upsert(Model, query, data, options = {}) {
95
95
  });
96
96
  }
97
97
  //#endregion
98
+ //#region src/filter/cast-schema.ts
99
+ /** Operators whose operand is a single scalar to cast. */
100
+ const SCALAR_OPS = /* @__PURE__ */ new Set([
101
+ "$eq",
102
+ "$ne",
103
+ "$gt",
104
+ "$gte",
105
+ "$lt",
106
+ "$lte"
107
+ ]);
108
+ /** Operators whose operand is an array of scalars to cast element-wise. */
109
+ const ARRAY_OPS = /* @__PURE__ */ new Set(["$in", "$nin"]);
110
+ /** Logical operators whose operand is an ARRAY of sub-filters. */
111
+ const LOGICAL_ARRAY_OPS$1 = /* @__PURE__ */ new Set([
112
+ "$and",
113
+ "$or",
114
+ "$nor"
115
+ ]);
116
+ /** Logical operators whose operand is a SINGLE nested sub-filter. */
117
+ const LOGICAL_OBJECT_OPS = /* @__PURE__ */ new Set(["$not"]);
118
+ function isPlainObject(value) {
119
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
120
+ }
121
+ /**
122
+ * Cast one value through a schema path. Returns the original value when the
123
+ * path is unknown, the value is nullish, or the cast throws — never raises.
124
+ */
125
+ function castValue(schema, field, value) {
126
+ if (value === null || value === void 0) return value;
127
+ if (typeof value === "object" && !(value instanceof Date)) return value;
128
+ let schemaType;
129
+ try {
130
+ schemaType = schema.path(field);
131
+ } catch {
132
+ return value;
133
+ }
134
+ if (!schemaType || typeof schemaType.cast !== "function") return value;
135
+ try {
136
+ return schemaType.cast(value);
137
+ } catch {
138
+ return value;
139
+ }
140
+ }
141
+ /**
142
+ * Cast the operand(s) of a field-level operator object, leaving operators we
143
+ * don't understand (`$regex`, `$exists`, `$size`, `$elemMatch`, …) alone.
144
+ */
145
+ function castOperatorObject(schema, field, ops) {
146
+ let changed = false;
147
+ const out = {};
148
+ for (const [op, operand] of Object.entries(ops)) if (SCALAR_OPS.has(op)) {
149
+ const next = castValue(schema, field, operand);
150
+ out[op] = next;
151
+ if (next !== operand) changed = true;
152
+ } else if (ARRAY_OPS.has(op) && Array.isArray(operand)) {
153
+ const arr = operand.map((entry) => castValue(schema, field, entry));
154
+ out[op] = arr;
155
+ if (arr.some((v, i) => v !== operand[i])) changed = true;
156
+ } else out[op] = operand;
157
+ return changed ? out : ops;
158
+ }
159
+ /**
160
+ * Cast a record-shape Mongo filter against a Mongoose schema so it is safe to
161
+ * hand to a `$match` stage. Recurses `$and` / `$or` / `$nor` / `$not`, since a
162
+ * policy/tenant scope conjoined with a caller filter nests one level down.
163
+ *
164
+ * Filter IR nodes and non-objects pass through untouched — IR values are built
165
+ * in code (already correctly typed) and carry no schema paths to resolve.
166
+ *
167
+ * @example
168
+ * ```ts
169
+ * // organizationId is an ObjectId column; arc supplies the scope id as a string
170
+ * castFilterToSchema({ organizationId: '6a6a…' }, Order.schema)
171
+ * // → { organizationId: ObjectId('6a6a…') }
172
+ * ```
173
+ */
174
+ function castFilterToSchema(filter, schema) {
175
+ if (!isPlainObject(filter)) return filter;
176
+ if (typeof filter.op === "string") return filter;
177
+ let changed = false;
178
+ const out = {};
179
+ for (const [key, value] of Object.entries(filter)) {
180
+ if (LOGICAL_ARRAY_OPS$1.has(key) && Array.isArray(value)) {
181
+ const arr = value.map((entry) => castFilterToSchema(entry, schema));
182
+ out[key] = arr;
183
+ if (arr.some((v, i) => v !== value[i])) changed = true;
184
+ continue;
185
+ }
186
+ if (LOGICAL_OBJECT_OPS.has(key) && isPlainObject(value)) {
187
+ const nested = castFilterToSchema(value, schema);
188
+ out[key] = nested;
189
+ if (nested !== value) changed = true;
190
+ continue;
191
+ }
192
+ if (key.startsWith("$")) {
193
+ out[key] = value;
194
+ continue;
195
+ }
196
+ if (isPlainObject(value)) {
197
+ const keys = Object.keys(value);
198
+ if (keys.length > 0 && keys.every((k) => k.startsWith("$"))) {
199
+ const nextOps = castOperatorObject(schema, key, value);
200
+ out[key] = nextOps;
201
+ if (nextOps !== value) changed = true;
202
+ continue;
203
+ }
204
+ out[key] = value;
205
+ continue;
206
+ }
207
+ const next = castValue(schema, key, value);
208
+ out[key] = next;
209
+ if (next !== value) changed = true;
210
+ }
211
+ return changed ? out : filter;
212
+ }
213
+ //#endregion
98
214
  //#region src/filter/compile.ts
99
215
  /**
100
216
  * Compile a Filter IR node to a MongoDB query object. Returns `{}`
@@ -114,6 +230,12 @@ async function upsert(Model, query, data, options = {}) {
114
230
  * handles them"). This set covers every operator the arc aggregation
115
231
  * guard accepts in `parseDateRange` and `hasFilterOnField`.
116
232
  */
233
+ /**
234
+ * The query-shorthand vocabulary, shared with `compileFilterToMongoExpr` so
235
+ * both filter surfaces recognise the same operator names. Exported because a
236
+ * second private copy is how the two compilers drifted apart in the first
237
+ * place: one accepted `{ f: { eq: 'x' } }` and the other read it as a literal.
238
+ */
117
239
  const SHORTHAND_OPS = /* @__PURE__ */ new Set([
118
240
  "eq",
119
241
  "ne",
@@ -191,7 +313,10 @@ function schemaDateOracle(schema) {
191
313
  }
192
314
  function compileFilterToMongo(input, schema) {
193
315
  if (!input) return {};
194
- if (!isFilter(input)) return coerceFilterDates(expandShorthands(input), schema ? { isDateField: schemaDateOracle(schema) } : void 0);
316
+ if (!isFilter(input)) {
317
+ const expanded = expandShorthands(input);
318
+ return coerceFilterDates(schema ? castFilterToSchema(expanded, schema) : expanded, schema ? { isDateField: schemaDateOracle(schema) } : void 0);
319
+ }
195
320
  return compile(input);
196
321
  }
197
322
  function compile(filter) {
@@ -199,7 +324,7 @@ function compile(filter) {
199
324
  case "true": return {};
200
325
  case "false": return { $expr: false };
201
326
  case "eq": return { [filter.field]: filter.value };
202
- case "ne": return filter.value === null ? { [filter.field]: { $ne: null } } : { $and: [{ [filter.field]: { $ne: filter.value } }, { [filter.field]: { $ne: null } }] };
327
+ case "ne": return filter.value === null ? { [filter.field]: { $ne: null } } : { [filter.field]: { $nin: [filter.value, null] } };
203
328
  case "gt": return { [filter.field]: { $gt: filter.value } };
204
329
  case "gte": return { [filter.field]: { $gte: filter.value } };
205
330
  case "lt": return { [filter.field]: { $lt: filter.value } };
@@ -209,7 +334,7 @@ function compile(filter) {
209
334
  return { [filter.field]: { $in: [...filter.values] } };
210
335
  case "nin":
211
336
  if (filter.values.length === 0) return {};
212
- return { [filter.field]: { $nin: [...filter.values] } };
337
+ return { [filter.field]: { $nin: filter.values.includes(null) ? [...filter.values] : [...filter.values, null] } };
213
338
  case "exists": return filter.exists ? { [filter.field]: { $ne: null } } : { [filter.field]: null };
214
339
  case "like": return { [filter.field]: likeToRegex(filter.pattern, filter.caseSensitivity) };
215
340
  /**
@@ -233,10 +358,10 @@ function compile(filter) {
233
358
  $options: filter.flags
234
359
  } : { $regex: filter.pattern } };
235
360
  case "and": {
236
- if (filter.children.length === 0) return {};
237
- const parts = filter.children.map(compile);
361
+ const parts = filter.children.map(compile).filter((part) => Object.keys(part).length > 0);
362
+ if (parts.length === 0) return {};
238
363
  if (parts.length === 1) return parts[0];
239
- return { $and: parts };
364
+ return mergeConjunction(parts) ?? { $and: parts };
240
365
  }
241
366
  case "or": {
242
367
  if (filter.children.length === 0) return { $expr: false };
@@ -268,7 +393,7 @@ function compile(filter) {
268
393
  * same way sqlitekit's `LIKE` semantically does.
269
394
  */
270
395
  function likeToRegex(pattern, caseSensitivity) {
271
- let out = "^";
396
+ let out = "";
272
397
  for (let i = 0; i < pattern.length; i++) {
273
398
  const ch = pattern[i];
274
399
  if (ch === "\\" && i + 1 < pattern.length) {
@@ -283,12 +408,44 @@ function likeToRegex(pattern, caseSensitivity) {
283
408
  else if (ch === "_") out += ".";
284
409
  else out += escapeRegexChar(ch);
285
410
  }
286
- out += "$";
287
- return caseSensitivity === "sensitive" ? { $regex: out } : {
288
- $regex: out,
411
+ const source = `${out.startsWith(".*") ? out.slice(2) : `^${out}`}`;
412
+ const anchored = source.endsWith(".*") && !source.endsWith("\\.*") ? source.slice(0, -2) : `${source}$`;
413
+ return caseSensitivity === "sensitive" ? { $regex: anchored } : {
414
+ $regex: anchored,
289
415
  $options: "i"
290
416
  };
291
417
  }
418
+ /**
419
+ * Merge an AND of field conditions into one object (`{ price: { $gte, $lte } }`) when no field
420
+ * repeats an operator; `undefined` when a merge would change meaning, so the caller keeps `$and`.
421
+ */
422
+ function mergeConjunction(parts) {
423
+ const merged = {};
424
+ for (const part of parts) for (const [field, condition] of Object.entries(part)) {
425
+ if (field.startsWith("$")) return void 0;
426
+ if (!(field in merged)) {
427
+ merged[field] = condition;
428
+ continue;
429
+ }
430
+ const left = asOperators(merged[field]);
431
+ const right = asOperators(condition);
432
+ if (!left || !right || Object.keys(right).some((op) => op in left)) return void 0;
433
+ merged[field] = {
434
+ ...left,
435
+ ...right
436
+ };
437
+ }
438
+ return merged;
439
+ }
440
+ /** A field condition as an operator object — an equality value becomes `{ $eq }`. */
441
+ function asOperators(condition) {
442
+ if (condition instanceof RegExp || Array.isArray(condition)) return void 0;
443
+ if (condition !== null && typeof condition === "object" && !(condition instanceof Date)) {
444
+ const keys = Object.keys(condition);
445
+ return keys.length > 0 && keys.every((k) => k.startsWith("$")) ? condition : void 0;
446
+ }
447
+ return condition === null ? void 0 : { $eq: condition };
448
+ }
292
449
  function escapeRegexChar(ch) {
293
450
  return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
294
451
  }
@@ -1257,4 +1414,4 @@ async function pullFromArray(Model, id, field, value, options = {}) {
1257
1414
  return update(Model, id, { $pull: { [field]: value } }, options);
1258
1415
  }
1259
1416
  //#endregion
1260
- export { upsert as S, compileFilterToMongo as _, count as a, create_exports as b, getByQuery as c, deleteById as d, deleteByQuery as f, LookupBuilder as g, distinct as h, update_exports as i, getOrCreate as l, aggregate_exports as m, update as n, exists as o, delete_exports as p, updateByQuery as r, getById as s, findOneAndUpdate as t, read_exports as u, create as v, isCreateManyPartialError as x, createMany as y };
1417
+ export { isCreateManyPartialError as C, create_exports as S, SHORTHAND_OPS as _, count as a, create as b, getByQuery as c, deleteById as d, deleteByQuery as f, LookupBuilder as g, distinct as h, update_exports as i, getOrCreate as l, aggregate_exports as m, update as n, exists as o, delete_exports as p, updateByQuery as r, getById as s, findOneAndUpdate as t, read_exports as u, compileFilterToMongo as v, upsert as w, createMany as x, castFilterToSchema as y };
@@ -1,4 +1,4 @@
1
- import { _ as getMongooseProjection, a as createMemoryCache, b as createError, c as IdType, d as isObjectIdInstance, f as isValidIdForType, g as getFieldsForUser, h as filterResponseData, i as buildCrudSchemasFromMongooseSchema, l as getSchemaIdType, m as createFieldPreset, n as CrudSchemasFramework, o as LoggerConfig, p as toObjectId, r as buildCrudSchemasFromModel, s as configureLogger, t as toPlain, v as CreateErrorOptions, y as ParseDuplicateKeyOptions } from "../to-plain-B8ufk3Qz.mjs";
1
+ import { _ as getMongooseProjection, a as createMemoryCache, b as createError, c as IdType, d as isObjectIdInstance, f as isValidIdForType, g as getFieldsForUser, h as filterResponseData, i as buildCrudSchemasFromMongooseSchema, l as getSchemaIdType, m as createFieldPreset, n as CrudSchemasFramework, o as LoggerConfig, p as toObjectId, r as buildCrudSchemasFromModel, s as configureLogger, t as toPlain, v as CreateErrorOptions, y as ParseDuplicateKeyOptions } from "../to-plain-k7SjNlSS.mjs";
2
2
  import { ClientSession, Model } from "mongoose";
3
3
  //#region src/utils/id-chunks.d.ts
4
4
  /**