@warlock.js/cascade 4.14.0 → 4.16.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 (64) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/cjs/index.cjs +494 -72
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/contracts/query-builder.contract.d.mts +7 -3
  5. package/esm/contracts/query-builder.contract.d.mts.map +1 -1
  6. package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
  7. package/esm/drivers/mongodb/mongodb-query-builder.mjs +4 -3
  8. package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
  9. package/esm/drivers/mongodb/mongodb-query-parser.d.mts +21 -0
  10. package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
  11. package/esm/drivers/mongodb/mongodb-query-parser.mjs +55 -22
  12. package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
  13. package/esm/errors/unsafe-filter.error.d.mts +33 -0
  14. package/esm/errors/unsafe-filter.error.d.mts.map +1 -0
  15. package/esm/errors/unsafe-filter.error.mjs +40 -0
  16. package/esm/errors/unsafe-filter.error.mjs.map +1 -0
  17. package/esm/errors/unsafe-raw-expression.error.d.mts +23 -0
  18. package/esm/errors/unsafe-raw-expression.error.d.mts.map +1 -0
  19. package/esm/errors/unsafe-raw-expression.error.mjs +28 -0
  20. package/esm/errors/unsafe-raw-expression.error.mjs.map +1 -0
  21. package/esm/index.d.mts +5 -1
  22. package/esm/index.mjs +5 -1
  23. package/esm/model/methods/accessor-methods.mjs +39 -1
  24. package/esm/model/methods/accessor-methods.mjs.map +1 -1
  25. package/esm/model/methods/delete-methods.mjs +3 -2
  26. package/esm/model/methods/delete-methods.mjs.map +1 -1
  27. package/esm/model/methods/query-methods.mjs +7 -4
  28. package/esm/model/methods/query-methods.mjs.map +1 -1
  29. package/esm/model/methods/serialization-methods.mjs +49 -4
  30. package/esm/model/methods/serialization-methods.mjs.map +1 -1
  31. package/esm/model/methods/write-methods.d.mts.map +1 -1
  32. package/esm/model/methods/write-methods.mjs +2 -4
  33. package/esm/model/methods/write-methods.mjs.map +1 -1
  34. package/esm/model/model.d.mts +47 -1
  35. package/esm/model/model.d.mts.map +1 -1
  36. package/esm/model/model.mjs +61 -2
  37. package/esm/model/model.mjs.map +1 -1
  38. package/esm/model/model.types.d.mts +1 -1
  39. package/esm/query-builder/query-builder.d.mts +13 -1
  40. package/esm/query-builder/query-builder.d.mts.map +1 -1
  41. package/esm/query-builder/query-builder.mjs +28 -11
  42. package/esm/query-builder/query-builder.mjs.map +1 -1
  43. package/esm/remover/database-remover.d.mts.map +1 -1
  44. package/esm/remover/database-remover.mjs +1 -1
  45. package/esm/remover/database-remover.mjs.map +1 -1
  46. package/esm/utils/escape-regex.d.mts +67 -0
  47. package/esm/utils/escape-regex.d.mts.map +1 -0
  48. package/esm/utils/escape-regex.mjs +76 -0
  49. package/esm/utils/escape-regex.mjs.map +1 -0
  50. package/esm/utils/sanitize-filter.d.mts +26 -0
  51. package/esm/utils/sanitize-filter.d.mts.map +1 -0
  52. package/esm/utils/sanitize-filter.mjs +76 -0
  53. package/esm/utils/sanitize-filter.mjs.map +1 -0
  54. package/esm/writer/database-writer.d.mts +12 -0
  55. package/esm/writer/database-writer.d.mts.map +1 -1
  56. package/esm/writer/database-writer.mjs +26 -6
  57. package/esm/writer/database-writer.mjs.map +1 -1
  58. package/llms-full.txt +59 -4
  59. package/llms.txt +3 -3
  60. package/package.json +24 -24
  61. package/skills/README.md +3 -3
  62. package/skills/define-model/SKILL.md +17 -1
  63. package/skills/perform-atomic-ops/SKILL.md +4 -1
  64. package/skills/query-data/SKILL.md +38 -2
package/cjs/index.cjs CHANGED
@@ -411,6 +411,70 @@ var TransactionRollbackError = class TransactionRollbackError extends Error {
411
411
  }
412
412
  };
413
413
 
414
+ //#endregion
415
+ //#region ../cascade/src/errors/unsafe-filter.error.ts
416
+ /**
417
+ * Error thrown when a filter value in an equality position contains
418
+ * MongoDB operator keys (keys starting with `$`, e.g. `$ne`, `$gt`, `$where`).
419
+ *
420
+ * This blocks NoSQL operator injection: request-controlled payloads such as
421
+ * `{ password: { $ne: null } }` must not be able to turn an equality match
422
+ * into an operator query. Use the explicit operator APIs instead:
423
+ * - `where(field, operator, value)` for comparisons
424
+ * - `whereIn` / `whereNull` / `whereBetween` / … for specific operators
425
+ * - `whereRaw({ ... })` (object form) for intentional raw driver filters
426
+ */
427
+ var UnsafeFilterError = class UnsafeFilterError extends Error {
428
+ /**
429
+ * The field whose value contained the rejected operator key (if applicable).
430
+ */
431
+ field;
432
+ /**
433
+ * The `$`-prefixed key that triggered the rejection.
434
+ */
435
+ operatorKey;
436
+ /**
437
+ * Creates a new UnsafeFilterError.
438
+ *
439
+ * @param message - Descriptive error message
440
+ * @param field - Optional field name whose value was rejected
441
+ * @param operatorKey - Optional `$`-prefixed key that was rejected
442
+ */
443
+ constructor(message, field, operatorKey) {
444
+ super(message);
445
+ this.name = "UnsafeFilterError";
446
+ this.field = field;
447
+ this.operatorKey = operatorKey;
448
+ if (Error.captureStackTrace) Error.captureStackTrace(this, UnsafeFilterError);
449
+ }
450
+ };
451
+
452
+ //#endregion
453
+ //#region ../cascade/src/errors/unsafe-raw-expression.error.ts
454
+ /**
455
+ * Error thrown when a string expression is passed to `whereRaw()` /
456
+ * `orWhereRaw()` on the MongoDB driver.
457
+ *
458
+ * String raw expressions used to compile to `{ $where: "<js>" }`, which MongoDB
459
+ * evaluates as JavaScript inside the server for every scanned document — a
460
+ * server-side JS injection sink when any part of the string is
461
+ * request-controlled, and an unindexed full-scan DoS vector even when trusted.
462
+ *
463
+ * Use the object form instead, e.g. `whereRaw({ $expr: { $gt: ["$stock", "$reserved"] } })`.
464
+ */
465
+ var UnsafeRawExpressionError = class UnsafeRawExpressionError extends Error {
466
+ /**
467
+ * Creates a new UnsafeRawExpressionError.
468
+ *
469
+ * @param message - Descriptive error message
470
+ */
471
+ constructor(message) {
472
+ super(message);
473
+ this.name = "UnsafeRawExpressionError";
474
+ if (Error.captureStackTrace) Error.captureStackTrace(this, UnsafeRawExpressionError);
475
+ }
476
+ };
477
+
414
478
  //#endregion
415
479
  //#region ../cascade/src/database-dirty-tracker.ts
416
480
  function canBeFlatten(object) {
@@ -3069,7 +3133,45 @@ function unsetFields(model, ...fields) {
3069
3133
  model.dirtyTracker.unset(fields);
3070
3134
  return model;
3071
3135
  }
3136
+ /**
3137
+ * Identity columns that a mass-assignment payload may never carry into an
3138
+ * already-persisted model. `merge()` is the method request bodies reach
3139
+ * (`model.merge(req.body)` / `save({ merge })`), and the primary key is what
3140
+ * the UPDATE filter is built from — letting it through means a payload of
3141
+ * `{ id: "<victim-id>" }` rewrites which document the save targets. Both the
3142
+ * configured primary key and the two framework identity columns are dropped,
3143
+ * because `id` and `_id` are managed by the writer/driver either way.
3144
+ *
3145
+ * A NEW model is untouched: creating with an explicit id is a legitimate flow,
3146
+ * and the writer itself merges the driver-returned document (`_id`, defaults)
3147
+ * back onto the instance before it is marked persisted.
3148
+ */
3149
+ function stripIdentityColumns(model, values) {
3150
+ if (model.isNew) return values;
3151
+ const identityColumns = new Set([
3152
+ model.getPrimaryKey(),
3153
+ "id",
3154
+ "_id"
3155
+ ]);
3156
+ const blockedKeys = Object.keys(values).filter((key) => identityColumns.has(key));
3157
+ if (blockedKeys.length === 0) return values;
3158
+ const safeValues = { ...values };
3159
+ for (const key of blockedKeys) delete safeValues[key];
3160
+ return safeValues;
3161
+ }
3072
3162
  function mergeFields(model, values) {
3163
+ return mergeDriverFields(model, stripIdentityColumns(model, values));
3164
+ }
3165
+ /**
3166
+ * Merge a document the DRIVER produced (generated `_id`, `RETURNING *`, DB
3167
+ * defaults) back onto the model, identity columns included.
3168
+ *
3169
+ * Framework-internal counterpart of {@link mergeFields}: the write pipeline is
3170
+ * the one caller allowed to set identity columns on an already-persisted
3171
+ * instance, because the values come from the database rather than from a
3172
+ * request. Never route caller-supplied data through this.
3173
+ */
3174
+ function mergeDriverFields(model, values) {
3073
3175
  model.data = (0, _mongez_reinforcements.merge)(model.data, values);
3074
3176
  model.dirtyTracker.mergeChanges(values);
3075
3177
  return model;
@@ -3152,7 +3254,7 @@ var DatabaseRemover = class {
3152
3254
  async destroy(options = {}) {
3153
3255
  const strategy = options.strategy ?? this.ctor.deleteStrategy ?? this.dataSource.defaultDeleteStrategy ?? "permanent";
3154
3256
  if (this.model.isNew) throw new Error(`Cannot destroy ${this.ctor.name} instance that hasn't been saved to the database.`);
3155
- const primaryKeyValue = this.model.get(this.primaryKey);
3257
+ const primaryKeyValue = this.model.trustedPrimaryKey;
3156
3258
  if (!primaryKeyValue) throw new Error(`Cannot destroy ${this.ctor.name} instance: primary key (${this.primaryKey}) is missing.`);
3157
3259
  if (!options.skipEvents) await this.model.emitEvent("deleting", {
3158
3260
  strategy,
@@ -3258,16 +3360,88 @@ var DatabaseRemover = class {
3258
3360
  }
3259
3361
  };
3260
3362
 
3363
+ //#endregion
3364
+ //#region ../cascade/src/utils/sanitize-filter.ts
3365
+ /**
3366
+ * Filter sanitization for equality-position values.
3367
+ *
3368
+ * `where({ field: value })`, `where(field, value)` and the filter-accepting
3369
+ * model statics (`first`, `findAll`, `deleteMany`, …) express *equality*
3370
+ * matches. If a request-controlled value such as `{ $ne: null }` is passed
3371
+ * through verbatim, MongoDB reinterprets it as an operator query — the classic
3372
+ * NoSQL operator-injection / auth-bypass primitive. These helpers reject any
3373
+ * `$`-prefixed key found in an equality position instead of forwarding it.
3374
+ *
3375
+ * Explicit operator APIs (`where(field, operator, value)`, `whereIn`,
3376
+ * `whereNull`, object-form `whereRaw({ ... })`, …) are intentionally NOT
3377
+ * routed through this check.
3378
+ */
3379
+ /**
3380
+ * Returns true for plain objects only (`{}` / `Object.create(null)`).
3381
+ * BSON values such as `Date`, `RegExp`, `ObjectId` or `Buffer` are class
3382
+ * instances whose keys are not Mongo operators, so they are never traversed.
3383
+ */
3384
+ const isPlainObject = (value) => {
3385
+ if (value === null || typeof value !== "object") return false;
3386
+ const prototype = Object.getPrototypeOf(value);
3387
+ return prototype === Object.prototype || prototype === null;
3388
+ };
3389
+ const rejectOperatorKey = (field, key) => {
3390
+ throw new UnsafeFilterError(`Unsafe filter: value for field "${field}" contains the reserved MongoDB operator key "${key}". Equality filters must not carry "$"-prefixed keys. Use the explicit operator API instead — e.g. where(field, operator, value), whereIn(), whereNull(), or the object form of whereRaw().`, field, key);
3391
+ };
3392
+ const assertNoOperatorKeys = (value, field) => {
3393
+ if (Array.isArray(value)) {
3394
+ for (const item of value) assertNoOperatorKeys(item, field);
3395
+ return;
3396
+ }
3397
+ if (!isPlainObject(value)) return;
3398
+ for (const [key, nested] of Object.entries(value)) {
3399
+ if (key.startsWith("$")) rejectOperatorKey(field, key);
3400
+ assertNoOperatorKeys(nested, field);
3401
+ }
3402
+ };
3403
+ /**
3404
+ * Assert a single equality-position value carries no `$`-prefixed keys.
3405
+ * Scalars, `Date`s and other non-plain objects pass through untouched;
3406
+ * plain objects/arrays are checked recursively.
3407
+ *
3408
+ * @param value - The equality value to check
3409
+ * @param field - The field name, used for the error message
3410
+ * @returns The value, unchanged
3411
+ * @throws UnsafeFilterError when a `$`-prefixed key is found
3412
+ */
3413
+ function sanitizeFilterValue(value, field) {
3414
+ assertNoOperatorKeys(value, field);
3415
+ return value;
3416
+ }
3417
+ /**
3418
+ * Assert a `{ field: value }` equality filter carries no `$`-prefixed keys —
3419
+ * neither as top-level field names (`{ $where: … }`) nor inside any value
3420
+ * (`{ password: { $ne: null } }`). Dotted field paths ("profile.name") and
3421
+ * plain nested documents remain valid.
3422
+ *
3423
+ * @param filter - The filter object to check
3424
+ * @returns The filter, unchanged
3425
+ * @throws UnsafeFilterError when a `$`-prefixed key is found
3426
+ */
3427
+ function sanitizeFilter(filter) {
3428
+ for (const [field, value] of Object.entries(filter)) {
3429
+ if (field.startsWith("$")) rejectOperatorKey(field, field);
3430
+ assertNoOperatorKeys(value, field);
3431
+ }
3432
+ return filter;
3433
+ }
3434
+
3261
3435
  //#endregion
3262
3436
  //#region ../cascade/src/model/methods/delete-methods.ts
3263
3437
  async function destroyModel(model, options) {
3264
3438
  return new DatabaseRemover(model).destroy(options);
3265
3439
  }
3266
3440
  async function deleteRecords(ModelClass, filter) {
3267
- return ModelClass.getDriver().deleteMany(ModelClass.table, filter);
3441
+ return ModelClass.getDriver().deleteMany(ModelClass.table, filter ? sanitizeFilter(filter) : filter);
3268
3442
  }
3269
3443
  async function deleteOneRecord(ModelClass, filter) {
3270
- return ModelClass.getDriver().delete(ModelClass.table, filter);
3444
+ return ModelClass.getDriver().delete(ModelClass.table, filter ? sanitizeFilter(filter) : filter);
3271
3445
  }
3272
3446
 
3273
3447
  //#endregion
@@ -3936,7 +4110,7 @@ var DatabaseWriter = class {
3936
4110
  if (updatedAtColumn) dataToInsert[updatedAtColumn] = /* @__PURE__ */ new Date();
3937
4111
  if (!options.skipEvents) await this.model.emitEvent("creating");
3938
4112
  const result = await this.driver.insert(this.table, dataToInsert);
3939
- this.model.merge(result.document);
4113
+ mergeDriverFields(this.model, result.document);
3940
4114
  this.model.dirtyTracker.reset();
3941
4115
  return result;
3942
4116
  }
@@ -3952,13 +4126,27 @@ var DatabaseWriter = class {
3952
4126
  const updatedAtColumn = this.ctor.updatedAtColumn;
3953
4127
  if (updatedAtColumn) this.model.set(updatedAtColumn, /* @__PURE__ */ new Date());
3954
4128
  if (options.replace) {
3955
- const document = await this.driver.replace(this.table, { [this.primaryKey]: this.model.get(this.primaryKey) }, this.model.data);
4129
+ const document = await this.driver.replace(this.table, this.buildPrimaryKeyFilter(), this.model.data);
3956
4130
  if (document) this.model.replaceData(document);
3957
4131
  return { modifiedCount: document ? 1 : 0 };
3958
4132
  }
3959
4133
  const operations = this.buildUpdateOperations();
3960
- const filter = { [this.primaryKey]: this.model.get(this.primaryKey) };
3961
- return await this.driver.update(this.table, filter, operations);
4134
+ if (Object.keys(operations).length === 0) return { modifiedCount: 0 };
4135
+ return await this.driver.update(this.table, this.buildPrimaryKeyFilter(), operations);
4136
+ }
4137
+ /**
4138
+ * Build the filter that pins a write to the row this model was loaded from.
4139
+ *
4140
+ * It reads `model.trustedPrimaryKey` — the value captured when the instance
4141
+ * became persisted — NOT the current value in `model.data`. The current value
4142
+ * is reachable by mass assignment (`model.merge(req.body)`), so deriving the
4143
+ * filter from it let a request body redirect the UPDATE to another document.
4144
+ *
4145
+ * @returns Filter matching the originally loaded record
4146
+ * @private
4147
+ */
4148
+ buildPrimaryKeyFilter() {
4149
+ return { [this.primaryKey]: this.model.trustedPrimaryKey };
3962
4150
  }
3963
4151
  /**
3964
4152
  * Generate ID for the model if auto-generation is enabled.
@@ -4001,7 +4189,12 @@ var DatabaseWriter = class {
4001
4189
  */
4002
4190
  buildUpdateOperations() {
4003
4191
  const operations = {};
4004
- const dirtyColumns = this.model.getDirtyColumns();
4192
+ const identityColumns = new Set([
4193
+ this.primaryKey,
4194
+ "id",
4195
+ "_id"
4196
+ ]);
4197
+ const dirtyColumns = this.model.getDirtyColumns().filter((column) => !identityColumns.has(column));
4005
4198
  if (dirtyColumns.length > 0) {
4006
4199
  operations.$set = {};
4007
4200
  for (const column of dirtyColumns) {
@@ -4009,7 +4202,7 @@ var DatabaseWriter = class {
4009
4202
  operations.$set[column] = this.model.get(column);
4010
4203
  }
4011
4204
  }
4012
- const removedColumns = this.model.getRemovedColumns();
4205
+ const removedColumns = this.model.getRemovedColumns().filter((column) => !identityColumns.has(column));
4013
4206
  if (removedColumns.length > 0) {
4014
4207
  operations.$unset = {};
4015
4208
  for (const column of removedColumns) operations.$unset[column] = 1;
@@ -4376,6 +4569,69 @@ async function detachPivotRelation(model, relation, ids) {
4376
4569
  return createPivotOperations(model, relation).detach(ids);
4377
4570
  }
4378
4571
 
4572
+ //#endregion
4573
+ //#region ../cascade/src/model/methods/serialization-methods.ts
4574
+ /**
4575
+ * Field names that commonly hold credentials/secrets. Used only to emit a
4576
+ * one-time warning when such a field exists in a model's schema but is not
4577
+ * covered by `hidden`/`resource`/`toJsonColumns` — the fail-open default
4578
+ * (`toJSON()` returns the raw document) would leak it.
4579
+ */
4580
+ const SENSITIVE_FIELD_PATTERN = /^(password|passwordHash|secret|token|apiKey|api_key)$/i;
4581
+ const warnedModelClasses = /* @__PURE__ */ new WeakSet();
4582
+ /**
4583
+ * Warn once per model class when its schema declares a sensitive-looking field
4584
+ * that serialization would expose: no `hidden` entry for it, no `resource`
4585
+ * class, and no `toJsonColumns` allow-list excluding it.
4586
+ *
4587
+ * A warning rather than an error — the fail-open `toJSON()` default is kept
4588
+ * for compatibility, but it should at least be loud.
4589
+ */
4590
+ function warnUndeclaredSensitiveFields(ModelClass) {
4591
+ if (warnedModelClasses.has(ModelClass)) return;
4592
+ warnedModelClasses.add(ModelClass);
4593
+ const schema = ModelClass.schema;
4594
+ if (!schema) return;
4595
+ const hidden = ModelClass.hidden ?? [];
4596
+ const toJsonColumns = ModelClass.toJsonColumns;
4597
+ const exposed = Object.keys(schema.schema).filter((field) => {
4598
+ if (!SENSITIVE_FIELD_PATTERN.test(field)) return false;
4599
+ if (hidden.includes(field)) return false;
4600
+ if (ModelClass.resource) return false;
4601
+ if (toJsonColumns && toJsonColumns.length > 0 && !toJsonColumns.includes(field)) return false;
4602
+ return true;
4603
+ });
4604
+ if (exposed.length === 0) return;
4605
+ console.warn(`[cascade] Model "${ModelClass.name}" (table "${ModelClass.table}") has sensitive-looking schema field(s) ${exposed.map((field) => `"${field}"`).join(", ")} that toJSON()/JSON.stringify() will include in output. Add them to \`static hidden = [...]\` (or configure \`resource\`/\`toJsonColumns\`) so they cannot leak via res.json(model).`);
4606
+ }
4607
+ /**
4608
+ * Return `data` without the model's `hidden` top-level fields.
4609
+ * Returns `data` untouched (same reference) when nothing is hidden.
4610
+ */
4611
+ function stripHiddenFields(data, hidden) {
4612
+ if (hidden.length === 0) return data;
4613
+ const output = { ...data };
4614
+ for (const field of hidden) delete output[field];
4615
+ return output;
4616
+ }
4617
+ function modelToJSON(model) {
4618
+ const ModelClass = model.self();
4619
+ const resource = ModelClass.resource;
4620
+ const hidden = ModelClass.hidden ?? [];
4621
+ warnUndeclaredSensitiveFields(ModelClass);
4622
+ if (!resource) {
4623
+ const toJsonColumns = ModelClass.toJsonColumns;
4624
+ if (toJsonColumns && toJsonColumns.length > 0) return stripHiddenFields(model.only(toJsonColumns), hidden);
4625
+ return stripHiddenFields(model.data, hidden);
4626
+ }
4627
+ const resourceColumns = ModelClass.resourceColumns;
4628
+ let data = resourceColumns !== void 0 && resourceColumns.length > 0 ? model.only(resourceColumns) : { ...model.data };
4629
+ for (const [relationName, relatedModel] of model.loadedRelations) if (Array.isArray(relatedModel)) data[relationName] = relatedModel.map((m) => m instanceof Object && typeof m.toJSON === "function" ? m.toJSON() : m);
4630
+ else if (relatedModel instanceof Object && typeof relatedModel.toJSON === "function") data[relationName] = relatedModel.toJSON();
4631
+ else data[relationName] = relatedModel;
4632
+ return new resource(stripHiddenFields(data, hidden)).toJSON();
4633
+ }
4634
+
4379
4635
  //#endregion
4380
4636
  //#region ../cascade/src/model/methods/query-methods.ts
4381
4637
  function buildQuery(ModelClass, BaseModel) {
@@ -4448,7 +4704,7 @@ function decreaseField(ModelClass, filter, field, amount) {
4448
4704
  return ModelClass.query().where(filter).decrement(field, amount);
4449
4705
  }
4450
4706
  async function performAtomic(ModelClass, filter, operations) {
4451
- return (await ModelClass.getDriver().atomic(ModelClass.table, filter, operations)).modifiedCount;
4707
+ return (await ModelClass.getDriver().atomic(ModelClass.table, sanitizeFilter(filter), operations)).modifiedCount;
4452
4708
  }
4453
4709
  async function updateById(ModelClass, id, data) {
4454
4710
  return (await ModelClass.getDriver().update(ModelClass.table, { [ModelClass.primaryKey]: id }, { $set: data })).modifiedCount;
@@ -4458,17 +4714,17 @@ async function findAndUpdateRecords(ModelClass, filter, update) {
4458
4714
  return await ModelClass.query().where(filter).get();
4459
4715
  }
4460
4716
  async function findOneAndUpdateRecord(ModelClass, filter, update) {
4461
- const result = await ModelClass.getDriver().findOneAndUpdate(ModelClass.table, filter, update);
4717
+ const result = await ModelClass.getDriver().findOneAndUpdate(ModelClass.table, sanitizeFilter(filter), update);
4462
4718
  if (!result) return null;
4463
4719
  return new ModelClass(result);
4464
4720
  }
4465
4721
  async function findAndReplaceRecord(ModelClass, filter, document) {
4466
- const result = await ModelClass.getDriver().replace(ModelClass.table, filter, document);
4722
+ const result = await ModelClass.getDriver().replace(ModelClass.table, sanitizeFilter(filter), document);
4467
4723
  if (!result) return null;
4468
4724
  return new ModelClass(result);
4469
4725
  }
4470
4726
  async function findOneAndDeleteRecord(ModelClass, filter, options) {
4471
- const result = await ModelClass.getDriver().findOneAndDelete(ModelClass.table, filter, options);
4727
+ const result = await ModelClass.getDriver().findOneAndDelete(ModelClass.table, sanitizeFilter(filter), options);
4472
4728
  if (!result) return null;
4473
4729
  const model = ModelClass.hydrate(result);
4474
4730
  model.dirtyTracker.reset();
@@ -4490,6 +4746,7 @@ function resolveDataSource(ModelClass) {
4490
4746
  if (Object.keys(mergedDefaults).length > 0) ModelClass.applyModelDefaults(mergedDefaults);
4491
4747
  ModelClass._defaultsApplied = true;
4492
4748
  }
4749
+ warnUndeclaredSensitiveFields(ModelClass);
4493
4750
  return dataSource;
4494
4751
  }
4495
4752
 
@@ -4851,24 +5108,6 @@ function removeLocalModelScope(ModelClass, name) {
4851
5108
  ownScopeMap(ModelClass, "localScopes").delete(name);
4852
5109
  }
4853
5110
 
4854
- //#endregion
4855
- //#region ../cascade/src/model/methods/serialization-methods.ts
4856
- function modelToJSON(model) {
4857
- const ModelClass = model.self();
4858
- const resource = ModelClass.resource;
4859
- if (!resource) {
4860
- const toJsonColumns = ModelClass.toJsonColumns;
4861
- if (toJsonColumns && toJsonColumns.length > 0) return model.only(toJsonColumns);
4862
- return model.data;
4863
- }
4864
- const resourceColumns = ModelClass.resourceColumns;
4865
- let data = resourceColumns !== void 0 && resourceColumns.length > 0 ? model.only(resourceColumns) : { ...model.data };
4866
- for (const [relationName, relatedModel] of model.loadedRelations) if (Array.isArray(relatedModel)) data[relationName] = relatedModel.map((m) => m instanceof Object && typeof m.toJSON === "function" ? m.toJSON() : m);
4867
- else if (relatedModel instanceof Object && typeof relatedModel.toJSON === "function") data[relationName] = relatedModel.toJSON();
4868
- else data[relationName] = relatedModel;
4869
- return new resource(data).toJSON();
4870
- }
4871
-
4872
5111
  //#endregion
4873
5112
  //#region ../cascade/src/model/methods/static-event-methods.ts
4874
5113
  /**
@@ -5064,10 +5303,7 @@ async function createManyBulk(ModelClass, chunks) {
5064
5303
  const results = await driver.insertMany(table, preparedDocuments);
5065
5304
  models.forEach((model, index) => {
5066
5305
  const result = results[index];
5067
- if (result) {
5068
- const returnedDocument = "document" in result ? result.document : result;
5069
- model.merge(returnedDocument);
5070
- }
5306
+ if (result) mergeDriverFields(model, "document" in result ? result.document : result);
5071
5307
  model.dirtyTracker.reset();
5072
5308
  model.isNew = false;
5073
5309
  });
@@ -5203,6 +5439,25 @@ var Model = class Model {
5203
5439
  */
5204
5440
  static toJsonColumns;
5205
5441
  /**
5442
+ * Top-level fields that are ALWAYS stripped from `toJSON()` output —
5443
+ * regardless of `resource`, `resourceColumns` or `toJsonColumns`.
5444
+ *
5445
+ * `toJSON()` is invoked implicitly by `JSON.stringify(model)` (e.g.
5446
+ * `res.json(user)`), and with no `resource`/`toJsonColumns` configured it
5447
+ * returns the entire raw document. Declare credential/PII columns here so
5448
+ * they can never leak through serialization, whatever else is configured.
5449
+ *
5450
+ * @default []
5451
+ *
5452
+ * @example
5453
+ * ```typescript
5454
+ * class User extends Model {
5455
+ * public static hidden = ["password", "resetToken"];
5456
+ * }
5457
+ * ```
5458
+ */
5459
+ static hidden = [];
5460
+ /**
5206
5461
  * Data source reference for this model.
5207
5462
  *
5208
5463
  * Can be:
@@ -5483,6 +5738,16 @@ var Model = class Model {
5483
5738
  * ```
5484
5739
  */
5485
5740
  static relations = {};
5741
+ /** Backing field for {@link isNew}. */
5742
+ newRecord = true;
5743
+ /**
5744
+ * The primary key value captured at the moment this instance became a
5745
+ * persisted record — the ONLY value an UPDATE/REPLACE filter may be built
5746
+ * from. See {@link trustedPrimaryKey}.
5747
+ */
5748
+ capturedPrimaryKey = void 0;
5749
+ /** Whether {@link capturedPrimaryKey} holds a captured value. */
5750
+ primaryKeyCaptured = false;
5486
5751
  /**
5487
5752
  * Flag indicating whether this model instance represents a new (unsaved) record.
5488
5753
  *
@@ -5490,8 +5755,38 @@ var Model = class Model {
5490
5755
  * - `false`: The model represents an existing database record
5491
5756
  *
5492
5757
  * This flag is used by the writer to determine whether to perform an insert or update.
5758
+ *
5759
+ * Setting it to `false` (hydration from the database, or the writer marking a
5760
+ * freshly inserted row as persisted) is also the moment the instance captures
5761
+ * its trusted primary key — see {@link trustedPrimaryKey}.
5493
5762
  */
5494
- isNew = true;
5763
+ get isNew() {
5764
+ return this.newRecord;
5765
+ }
5766
+ set isNew(value) {
5767
+ this.newRecord = value;
5768
+ if (value) {
5769
+ this.capturedPrimaryKey = void 0;
5770
+ this.primaryKeyCaptured = false;
5771
+ return;
5772
+ }
5773
+ this.capturedPrimaryKey = this.get(this.self().primaryKey);
5774
+ this.primaryKeyCaptured = true;
5775
+ }
5776
+ /**
5777
+ * The primary key value an UPDATE/REPLACE filter must be built from.
5778
+ *
5779
+ * For a persisted record this is the value the row was loaded with, captured
5780
+ * when `isNew` flipped to `false` — NOT the current value in `data`. Deriving
5781
+ * the filter from post-`merge()` state let a request body carrying
5782
+ * `{ id: "<victim-id>" }` redirect the write to a different document
5783
+ * (`model.merge(req.body); await model.save()`), so the two must never be the
5784
+ * same read. For a model that was never persisted it falls back to the
5785
+ * current value, which is what an insert would use anyway.
5786
+ */
5787
+ get trustedPrimaryKey() {
5788
+ return this.primaryKeyCaptured ? this.capturedPrimaryKey : this.get(this.self().primaryKey);
5789
+ }
5495
5790
  /**
5496
5791
  * The raw mutable data backing this model instance.
5497
5792
  *
@@ -8384,7 +8679,7 @@ var QueryBuilder = class QueryBuilder {
8384
8679
  const sub = this.subQuery();
8385
8680
  args[0](sub);
8386
8681
  this.addOperation("where", { nested: sub.operations });
8387
- } else if (args.length === 1 && typeof args[0] === "object" && args[0] !== null) for (const [key, value] of Object.entries(args[0])) this.addOperation("where", {
8682
+ } else if (args.length === 1 && typeof args[0] === "object" && args[0] !== null) for (const [key, value] of Object.entries(sanitizeFilter(args[0]))) this.addOperation("where", {
8388
8683
  field: key,
8389
8684
  operator: "=",
8390
8685
  value
@@ -8392,12 +8687,12 @@ var QueryBuilder = class QueryBuilder {
8392
8687
  else if (args.length === 2) this.addOperation("where", {
8393
8688
  field: args[0],
8394
8689
  operator: "=",
8395
- value: args[1]
8690
+ value: sanitizeFilterValue(args[1], String(args[0]))
8396
8691
  });
8397
8692
  else this.addOperation("where", {
8398
8693
  field: args[0],
8399
8694
  operator: args[1],
8400
- value: args[2]
8695
+ value: args[1] === "=" ? sanitizeFilterValue(args[2], String(args[0])) : args[2]
8401
8696
  });
8402
8697
  return this;
8403
8698
  }
@@ -8406,7 +8701,7 @@ var QueryBuilder = class QueryBuilder {
8406
8701
  const sub = this.subQuery();
8407
8702
  args[0](sub);
8408
8703
  this.addOperation("orWhere", { nested: sub.operations });
8409
- } else if (args.length === 1 && typeof args[0] === "object" && args[0] !== null) for (const [key, value] of Object.entries(args[0])) this.addOperation("orWhere", {
8704
+ } else if (args.length === 1 && typeof args[0] === "object" && args[0] !== null) for (const [key, value] of Object.entries(sanitizeFilter(args[0]))) this.addOperation("orWhere", {
8410
8705
  field: key,
8411
8706
  operator: "=",
8412
8707
  value
@@ -8414,12 +8709,12 @@ var QueryBuilder = class QueryBuilder {
8414
8709
  else if (args.length === 2) this.addOperation("orWhere", {
8415
8710
  field: args[0],
8416
8711
  operator: "=",
8417
- value: args[1]
8712
+ value: sanitizeFilterValue(args[1], String(args[0]))
8418
8713
  });
8419
8714
  else this.addOperation("orWhere", {
8420
8715
  field: args[0],
8421
8716
  operator: args[1],
8422
- value: args[2]
8717
+ value: args[1] === "=" ? sanitizeFilterValue(args[2], String(args[0])) : args[2]
8423
8718
  });
8424
8719
  return this;
8425
8720
  }
@@ -8529,25 +8824,40 @@ var QueryBuilder = class QueryBuilder {
8529
8824
  }
8530
8825
  /**
8531
8826
  * LIKE pattern match (AND).
8827
+ *
8828
+ * A string is a LIKE pattern (`%` wildcard) and is matched literally
8829
+ * otherwise — regex metacharacters in it are escaped by the driver, so
8830
+ * search input cannot alter the query. Pass an explicit `RegExp` to opt into
8831
+ * raw pattern semantics; never build that `RegExp` from user input.
8832
+ *
8532
8833
  * @example q.whereLike("email", "%@gmail.com")
8533
8834
  */
8534
8835
  whereLike(field, pattern) {
8535
- const patternStr = pattern instanceof RegExp ? pattern.source : pattern;
8536
8836
  this.addOperation("whereLike", {
8537
8837
  field,
8538
- pattern: patternStr
8838
+ ...this.likePatternData(pattern)
8539
8839
  });
8540
8840
  return this;
8541
8841
  }
8542
- /** NOT LIKE pattern match. */
8842
+ /** NOT LIKE pattern match. @see whereLike for the escaping rules. */
8543
8843
  whereNotLike(field, pattern) {
8544
- const patternStr = pattern instanceof RegExp ? pattern.source : pattern;
8545
8844
  this.addOperation("whereNotLike", {
8546
8845
  field,
8547
- pattern: patternStr
8846
+ ...this.likePatternData(pattern)
8548
8847
  });
8549
8848
  return this;
8550
8849
  }
8850
+ /**
8851
+ * Flatten a LIKE argument into operation data, keeping the distinction the
8852
+ * drivers need: an explicit `RegExp` (developer-authored) stays a pattern,
8853
+ * a string (potentially request input) is a literal.
8854
+ */
8855
+ likePatternData(pattern) {
8856
+ return pattern instanceof RegExp ? {
8857
+ pattern: pattern.source,
8858
+ isRegExp: true
8859
+ } : { pattern };
8860
+ }
8551
8861
  /** Starts with a prefix. */
8552
8862
  whereStartsWith(field, value) {
8553
8863
  return this.whereLike(field, `${value}%`);
@@ -9610,6 +9920,80 @@ var MongoQueryOperations = class {
9610
9920
  }
9611
9921
  };
9612
9922
 
9923
+ //#endregion
9924
+ //#region ../cascade/src/utils/escape-regex.ts
9925
+ /**
9926
+ * Regex escaping for pattern-matching helpers.
9927
+ *
9928
+ * `whereLike` / `whereNotLike` / `whereStartsWith` / `whereEndsWith` /
9929
+ * `whereSearch` compile their argument into a MongoDB `$regex`. The argument is
9930
+ * exactly what a search box hands over (`whereSearch("name", req.query.q)`), so
9931
+ * passing it through unescaped gave the caller control of the regex itself:
9932
+ *
9933
+ * - **injection** — `.*` / `|` / `^` change the intended match semantics, and a
9934
+ * boolean-oracle probe (`^a`, `^b`, …) reads values back one character at a
9935
+ * time from a field the endpoint never meant to expose;
9936
+ * - **ReDoS** — nested quantifiers (`(a+)+`) backtrack catastrophically inside
9937
+ * `mongod`, against every document the query scans.
9938
+ *
9939
+ * A string value is therefore treated as a LITERAL. Only an explicit `RegExp`
9940
+ * argument — which cannot come from JSON, so it is developer-authored — reaches
9941
+ * the regex engine as a pattern.
9942
+ */
9943
+ /** Characters that carry meaning to the regex engine and must be neutralized. */
9944
+ const REGEX_METACHARACTERS = /[.*+?^${}()|[\]\\]/g;
9945
+ /**
9946
+ * Escape every regex metacharacter in a string so it matches itself.
9947
+ *
9948
+ * @param value - The literal text to match
9949
+ * @returns Regex source matching `value` verbatim
9950
+ *
9951
+ * @example
9952
+ * ```typescript
9953
+ * escapeRegex("(a+)+$"); // "\\(a\\+\\)\\+\\$"
9954
+ * ```
9955
+ */
9956
+ function escapeRegex(value) {
9957
+ return value.replace(REGEX_METACHARACTERS, "\\$&");
9958
+ }
9959
+ /**
9960
+ * Compile a `whereLike` pattern into regex source.
9961
+ *
9962
+ * The pattern is escaped first, so nothing the caller typed can act as a regex
9963
+ * operator; the SQL `LIKE` wildcard `%` is then translated to `.*` — the one
9964
+ * wildcard the API documents (`whereLike("email", "%@gmail.com")`). Runs of `%`
9965
+ * collapse into a single `.*`, since `%%%%…` compiles to nothing but extra
9966
+ * backtracking work.
9967
+ *
9968
+ * The result stays unanchored: MongoDB's `whereLike` matches a substring
9969
+ * (`whereLike("name", "ar")` finds "Carol"), which is the documented behavior of
9970
+ * this driver and is unchanged by the escaping.
9971
+ *
9972
+ * @param pattern - The user-supplied LIKE pattern
9973
+ * @returns Regex source matching the pattern literally, `%` aside
9974
+ *
9975
+ * @example
9976
+ * ```typescript
9977
+ * likePatternToRegexSource("%o'brien%"); // ".*o'brien.*"
9978
+ * likePatternToRegexSource("a.b"); // "a\\.b" (a literal dot)
9979
+ * ```
9980
+ */
9981
+ function likePatternToRegexSource(pattern) {
9982
+ return escapeRegex(pattern).replace(/%+/g, ".*");
9983
+ }
9984
+ /**
9985
+ * Resolve a `whereLike`-style argument to regex source.
9986
+ *
9987
+ * An explicit `RegExp` is developer-authored and passes through as-is; a string
9988
+ * is user-shaped input and is treated as a literal LIKE pattern.
9989
+ *
9990
+ * @param pattern - A `RegExp` (trusted, used verbatim) or a string (escaped)
9991
+ * @returns Regex source ready for `$regex`
9992
+ */
9993
+ function resolveLikePattern(pattern) {
9994
+ return pattern instanceof RegExp ? pattern.source : likePatternToRegexSource(pattern);
9995
+ }
9996
+
9613
9997
  //#endregion
9614
9998
  //#region ../cascade/src/drivers/mongodb/mongodb-query-parser.ts
9615
9999
  /**
@@ -10078,34 +10462,28 @@ var MongoQueryParser = class {
10078
10462
  $gte: op.data.range[0],
10079
10463
  $lte: op.data.range[1]
10080
10464
  } } };
10081
- case "whereLike": {
10082
- const pattern = typeof op.data.pattern === "string" ? op.data.pattern : op.data.pattern.source;
10083
- return { [field]: {
10084
- $regex: pattern,
10085
- $options: "i"
10086
- } };
10087
- }
10088
- case "whereNotLike": {
10089
- const notPattern = typeof op.data.pattern === "string" ? op.data.pattern : op.data.pattern.source;
10090
- return { [field]: { $not: {
10091
- $regex: notPattern,
10092
- $options: "i"
10093
- } } };
10094
- }
10465
+ case "whereLike": return { [field]: {
10466
+ $regex: this.buildLikeRegexSource(op.data),
10467
+ $options: "i"
10468
+ } };
10469
+ case "whereNotLike": return { [field]: { $not: {
10470
+ $regex: this.buildLikeRegexSource(op.data),
10471
+ $options: "i"
10472
+ } } };
10095
10473
  case "whereStartsWith": return { [field]: {
10096
- $regex: `^${op.data.value}`,
10474
+ $regex: `^${escapeRegex(String(op.data.value))}`,
10097
10475
  $options: "i"
10098
10476
  } };
10099
10477
  case "whereNotStartsWith": return { [field]: { $not: {
10100
- $regex: `^${op.data.value}`,
10478
+ $regex: `^${escapeRegex(String(op.data.value))}`,
10101
10479
  $options: "i"
10102
10480
  } } };
10103
10481
  case "whereEndsWith": return { [field]: {
10104
- $regex: `${op.data.value}$`,
10482
+ $regex: `${escapeRegex(String(op.data.value))}$`,
10105
10483
  $options: "i"
10106
10484
  } };
10107
10485
  case "whereNotEndsWith": return { [field]: { $not: {
10108
- $regex: `${op.data.value}$`,
10486
+ $regex: `${escapeRegex(String(op.data.value))}$`,
10109
10487
  $options: "i"
10110
10488
  } } };
10111
10489
  case "whereExists": return { [field]: { $exists: true } };
@@ -10139,7 +10517,7 @@ var MongoQueryParser = class {
10139
10517
  case "whereFullText":
10140
10518
  case "orWhereFullText": return { $text: { $search: op.data.query } };
10141
10519
  case "whereSearch": return { [op.data.field]: {
10142
- $regex: op.data.query,
10520
+ $regex: escapeRegex(String(op.data.query)),
10143
10521
  $options: "i"
10144
10522
  } };
10145
10523
  case "where:not":
@@ -10158,6 +10536,22 @@ var MongoQueryParser = class {
10158
10536
  }
10159
10537
  }
10160
10538
  /**
10539
+ * Resolve the `$regex` source for a whereLike/whereNotLike operation.
10540
+ *
10541
+ * A `RegExp` argument is developer-authored and used verbatim; a string is
10542
+ * treated as a literal LIKE pattern (`%` aside) so that search input cannot
10543
+ * inject regex operators or a catastrophically backtracking pattern. The base
10544
+ * (driver-agnostic) query builder flattens a `RegExp` to its source string and
10545
+ * flags it with `isRegExp`, which is honored here for the same reason.
10546
+ *
10547
+ * @param data - The operation data (`{ pattern, isRegExp? }`)
10548
+ * @returns Regex source for `$regex`
10549
+ */
10550
+ buildLikeRegexSource(data) {
10551
+ if (data.isRegExp === true && typeof data.pattern === "string") return data.pattern;
10552
+ return resolveLikePattern(data.pattern);
10553
+ }
10554
+ /**
10161
10555
  * Build a condition based on the operator.
10162
10556
  *
10163
10557
  * @param field - The field name
@@ -10192,11 +10586,32 @@ var MongoQueryParser = class {
10192
10586
  "<=": "$lte"
10193
10587
  }[operator] || "$eq";
10194
10588
  }
10195
- resolveRawExpression(expression, bindings) {
10196
- if (typeof expression === "string") return { $where: this.bindRawString(expression, bindings) };
10197
- if (typeof expression === "object" && expression !== null) return expression;
10589
+ resolveRawExpression(expression, _bindings) {
10590
+ if (typeof expression === "string") throw new UnsafeRawExpressionError("whereRaw()/orWhereRaw() string expressions are not supported on the MongoDB driver: they would compile to a {$where: \"<js>\"} filter, which executes JavaScript on the database server. Use the object form instead, e.g. whereRaw({ $expr: { $gt: [\"$stock\", \"$reserved\"] } }).");
10591
+ if (typeof expression === "object" && expression !== null) {
10592
+ this.assertNoJsExecutionOperators(expression);
10593
+ return expression;
10594
+ }
10198
10595
  return null;
10199
10596
  }
10597
+ /**
10598
+ * The object form of whereRaw() is forwarded to MongoDB verbatim, so it must
10599
+ * not smuggle in the operators that execute JavaScript inside mongod
10600
+ * ($where, and $function/$accumulator which can nest under $expr).
10601
+ * Aggregation operators like $expr/$gt remain valid — that is the object
10602
+ * form's purpose.
10603
+ */
10604
+ assertNoJsExecutionOperators(value) {
10605
+ if (Array.isArray(value)) {
10606
+ for (const item of value) this.assertNoJsExecutionOperators(item);
10607
+ return;
10608
+ }
10609
+ if (typeof value !== "object" || value === null) return;
10610
+ for (const [key, nested] of Object.entries(value)) {
10611
+ if (key === "$where" || key === "$function" || key === "$accumulator") throw new UnsafeRawExpressionError(`whereRaw()/orWhereRaw() expressions must not contain "${key}": it executes JavaScript on the database server. Express the condition with query operators instead, e.g. whereRaw({ $expr: { $gt: ["$stock", "$reserved"] } }).`);
10612
+ this.assertNoJsExecutionOperators(nested);
10613
+ }
10614
+ }
10200
10615
  bindRawString(expression, bindings) {
10201
10616
  if (!bindings || bindings.length === 0) return expression;
10202
10617
  let index = 0;
@@ -11403,16 +11818,16 @@ var MongoQueryBuilder = class MongoQueryBuilder extends QueryBuilder {
11403
11818
  */
11404
11819
  addWhereClause(prefix, args) {
11405
11820
  if (args.length === 1) if (typeof args[0] === "function") this.operationsHelper.addMatchOperation(`${prefix}:callback`, args[0]);
11406
- else this.operationsHelper.addMatchOperation(`${prefix}:object`, args[0]);
11821
+ else this.operationsHelper.addMatchOperation(`${prefix}:object`, sanitizeFilter(args[0]));
11407
11822
  else if (args.length === 2) this.operationsHelper.addMatchOperation(prefix, {
11408
11823
  field: args[0],
11409
11824
  operator: "=",
11410
- value: args[1]
11825
+ value: sanitizeFilterValue(args[1], String(args[0]))
11411
11826
  });
11412
11827
  else if (args.length === 3) this.operationsHelper.addMatchOperation(prefix, {
11413
11828
  field: args[0],
11414
11829
  operator: args[1],
11415
- value: args[2]
11830
+ value: args[1] === "=" ? sanitizeFilterValue(args[2], String(args[0])) : args[2]
11416
11831
  });
11417
11832
  }
11418
11833
  /**
@@ -22603,6 +23018,8 @@ exports.RelationLoader = RelationLoader;
22603
23018
  exports.SyncContextManager = SyncContextManager;
22604
23019
  exports.SyncManager = SyncManager;
22605
23020
  exports.TransactionRollbackError = TransactionRollbackError;
23021
+ exports.UnsafeFilterError = UnsafeFilterError;
23022
+ exports.UnsafeRawExpressionError = UnsafeRawExpressionError;
22606
23023
  exports.arrayBigInt = arrayBigInt;
22607
23024
  exports.arrayBoolean = arrayBoolean;
22608
23025
  exports.arrayDate = arrayDate;
@@ -22634,6 +23051,7 @@ exports.defineModel = defineModel;
22634
23051
  exports.double = double;
22635
23052
  exports.dropAllTables = dropAllTables;
22636
23053
  exports.enumCol = enumCol;
23054
+ exports.escapeRegex = escapeRegex;
22637
23055
  exports.exportMigrationsSQL = exportMigrationsSQL;
22638
23056
  exports.float = float;
22639
23057
  exports.freshMigrate = freshMigrate;
@@ -22649,6 +23067,7 @@ exports.isAggregateExpression = isAggregateExpression;
22649
23067
  exports.isColumnExpression = isColumnExpression;
22650
23068
  exports.isMongoDBDriverLoaded = isMongoDBDriverLoaded;
22651
23069
  exports.json = json;
23070
+ exports.likePatternToRegexSource = likePatternToRegexSource;
22652
23071
  exports.lineString = lineString;
22653
23072
  exports.listExecutedMigrations = listExecutedMigrations;
22654
23073
  exports.listPendingMigrations = listPendingMigrations;
@@ -22665,10 +23084,13 @@ exports.point = point;
22665
23084
  exports.polygon = polygon;
22666
23085
  exports.registerModelInRegistry = registerModelInRegistry;
22667
23086
  exports.removeModelFromRegistery = removeModelFromRegistery;
23087
+ exports.resolveLikePattern = resolveLikePattern;
22668
23088
  exports.resolveModelClass = resolveModelClass;
22669
23089
  exports.resolveModelName = resolveModelName;
22670
23090
  exports.rollbackMigrations = rollbackMigrations;
22671
23091
  exports.runMigrations = runMigrations;
23092
+ exports.sanitizeFilter = sanitizeFilter;
23093
+ exports.sanitizeFilterValue = sanitizeFilterValue;
22672
23094
  exports.setCol = setCol;
22673
23095
  exports.smallInt = smallInt;
22674
23096
  exports.smallInteger = smallInteger;