@warlock.js/cascade 5.24.0 → 5.26.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 (93) hide show
  1. package/CHANGELOG.md +30 -5
  2. package/README.md +101 -101
  3. package/bin/cascade.js +0 -0
  4. package/cjs/index.cjs +307 -88
  5. package/cjs/index.cjs.map +1 -1
  6. package/esm/contracts/database-driver.contract.d.mts +22 -1
  7. package/esm/contracts/database-driver.contract.d.mts.map +1 -1
  8. package/esm/contracts/index.d.mts +1 -1
  9. package/esm/contracts/query-builder.contract.d.mts +7 -0
  10. package/esm/contracts/query-builder.contract.d.mts.map +1 -1
  11. package/esm/data-source/data-source-registry.d.mts +8 -6
  12. package/esm/data-source/data-source-registry.d.mts.map +1 -1
  13. package/esm/data-source/data-source-registry.mjs +7 -1
  14. package/esm/data-source/data-source-registry.mjs.map +1 -1
  15. package/esm/drivers/mongodb/mongodb-driver.d.mts +13 -0
  16. package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
  17. package/esm/drivers/mongodb/mongodb-driver.mjs +71 -2
  18. package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
  19. package/esm/drivers/mongodb/mongodb-query-builder.d.mts +5 -0
  20. package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
  21. package/esm/drivers/mongodb/mongodb-query-builder.mjs +11 -1
  22. package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
  23. package/esm/drivers/postgres/postgres-driver.d.mts +9 -0
  24. package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
  25. package/esm/drivers/postgres/postgres-driver.mjs +55 -7
  26. package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
  27. package/esm/drivers/postgres/postgres-query-builder.d.mts +5 -0
  28. package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
  29. package/esm/drivers/postgres/postgres-query-builder.mjs +15 -0
  30. package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
  31. package/esm/drivers/postgres/postgres-query-parser.d.mts +1 -4
  32. package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
  33. package/esm/drivers/postgres/postgres-query-parser.mjs +4 -5
  34. package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
  35. package/esm/errors/unsafe-filter.error.mjs.map +1 -1
  36. package/esm/errors/unsafe-raw-expression.error.mjs.map +1 -1
  37. package/esm/errors/unsupported-lock-for-update.error.d.mts +11 -0
  38. package/esm/errors/unsupported-lock-for-update.error.d.mts.map +1 -0
  39. package/esm/errors/unsupported-lock-for-update.error.mjs +16 -0
  40. package/esm/errors/unsupported-lock-for-update.error.mjs.map +1 -0
  41. package/esm/events/model-events.mjs.map +1 -1
  42. package/esm/index.d.mts +4 -3
  43. package/esm/index.mjs +2 -1
  44. package/esm/migration/index.d.mts +1 -1
  45. package/esm/migration/migration-order.mjs +6 -3
  46. package/esm/migration/migration-order.mjs.map +1 -1
  47. package/esm/migration/migration-runner.d.mts +5 -3
  48. package/esm/migration/migration-runner.d.mts.map +1 -1
  49. package/esm/migration/migration-runner.mjs +3 -2
  50. package/esm/migration/migration-runner.mjs.map +1 -1
  51. package/esm/migration/migration.d.mts +11 -1
  52. package/esm/migration/migration.d.mts.map +1 -1
  53. package/esm/migration/migration.mjs +45 -2
  54. package/esm/migration/migration.mjs.map +1 -1
  55. package/esm/migration/sql-grammar.mjs.map +1 -1
  56. package/esm/model/generate-model-id.mjs +23 -0
  57. package/esm/model/generate-model-id.mjs.map +1 -0
  58. package/esm/model/methods/delete-methods.mjs.map +1 -1
  59. package/esm/model/methods/meta-methods.mjs.map +1 -1
  60. package/esm/model/methods/query-methods.mjs +38 -11
  61. package/esm/model/methods/query-methods.mjs.map +1 -1
  62. package/esm/model/methods/write-methods.mjs.map +1 -1
  63. package/esm/model/model.d.mts +2 -2
  64. package/esm/model/model.d.mts.map +1 -1
  65. package/esm/model/model.mjs +1 -1
  66. package/esm/model/model.mjs.map +1 -1
  67. package/esm/query-builder/query-builder.d.mts +5 -0
  68. package/esm/query-builder/query-builder.d.mts.map +1 -1
  69. package/esm/query-builder/query-builder.mjs +16 -1
  70. package/esm/query-builder/query-builder.mjs.map +1 -1
  71. package/esm/utils/sanitize-filter.mjs.map +1 -1
  72. package/esm/validation/plugins/database-rules-plugin.mjs.map +1 -1
  73. package/esm/writer/database-writer.d.mts +0 -33
  74. package/esm/writer/database-writer.d.mts.map +1 -1
  75. package/esm/writer/database-writer.mjs +3 -52
  76. package/esm/writer/database-writer.mjs.map +1 -1
  77. package/llms-full.txt +2911 -2864
  78. package/package.json +5 -5
  79. package/skills/aggregate-data/SKILL.md +165 -165
  80. package/skills/alter-migration/SKILL.md +531 -531
  81. package/skills/configure-delete-strategy/SKILL.md +130 -130
  82. package/skills/define-model/SKILL.md +205 -205
  83. package/skills/define-relations/SKILL.md +171 -171
  84. package/skills/manage-data-sources/SKILL.md +177 -140
  85. package/skills/manage-transactions/SKILL.md +215 -205
  86. package/skills/paginate-results/SKILL.md +122 -122
  87. package/skills/perform-atomic-ops/SKILL.md +156 -156
  88. package/skills/query-data/SKILL.md +213 -213
  89. package/skills/run-cascade-cli/SKILL.md +125 -125
  90. package/skills/search-by-vector/SKILL.md +127 -127
  91. package/skills/subscribe-to-model-events/SKILL.md +148 -148
  92. package/skills/track-changes/SKILL.md +127 -127
  93. package/skills/write-migration/SKILL.md +206 -206
package/cjs/index.cjs CHANGED
@@ -317,7 +317,7 @@ var DataSourceRegistry = class {
317
317
  defaultSource;
318
318
  defaultIsExplicit = false;
319
319
  events = new node_events.EventEmitter();
320
- /** Replaced sources; their forwarded driver events are ignored (drivers have no `off`). */
320
+ /** Replaced sources; their forwarded driver events are ignored. */
321
321
  detached = /* @__PURE__ */ new WeakSet();
322
322
  /** In-flight `getOrRegister` factories, so concurrent callers share one creation. */
323
323
  pending = /* @__PURE__ */ new Map();
@@ -368,6 +368,12 @@ var DataSourceRegistry = class {
368
368
  source.driver.on("disconnected", () => {
369
369
  if (!this.detached.has(source)) this.events.emit("disconnected", source);
370
370
  });
371
+ source.driver.on("query", (event) => {
372
+ if (!this.detached.has(source)) this.events.emit("query", {
373
+ ...event,
374
+ connection: source.name
375
+ });
376
+ });
371
377
  return source;
372
378
  }
373
379
  /**
@@ -670,6 +676,20 @@ var UnsupportedLeanOperationError = class UnsupportedLeanOperationError extends
670
676
  }
671
677
  };
672
678
 
679
+ //#endregion
680
+ //#region ../cascade/src/errors/unsupported-lock-for-update.error.ts
681
+ /**
682
+ * Error thrown when `lockForUpdate()` is requested on MongoDB, whose document
683
+ * model has no row-level `SELECT ... FOR UPDATE` equivalent.
684
+ */
685
+ var UnsupportedLockForUpdateError = class UnsupportedLockForUpdateError extends Error {
686
+ constructor() {
687
+ super("lockForUpdate() is unsupported on MongoDB. MongoDB has no row-level SELECT locking. Use a conditional atomic update instead, e.g. Model.atomic({ id, leaseUntil: { $lt: now } }, { $set: { status: \"processing\" } }, { trustedFilter: true }).");
688
+ this.name = "UnsupportedLockForUpdateError";
689
+ if (Error.captureStackTrace) Error.captureStackTrace(this, UnsupportedLockForUpdateError);
690
+ }
691
+ };
692
+
673
693
  //#endregion
674
694
  //#region ../cascade/src/errors/unsupported-query-operation.error.ts
675
695
  /**
@@ -4045,6 +4065,27 @@ function offModelEvent(model, event, listener) {
4045
4065
  model.events.off(event, listener);
4046
4066
  }
4047
4067
 
4068
+ //#endregion
4069
+ //#region ../cascade/src/model/generate-model-id.ts
4070
+ /**
4071
+ * Generate the next model id using the model's configured sequence options.
4072
+ *
4073
+ * This is shared by normal inserts and MongoDB upserts so both write paths
4074
+ * resolve random and configured sequence settings identically.
4075
+ */
4076
+ async function generateModelId(ModelClass) {
4077
+ const idGenerator = ModelClass.getDataSource().idGenerator;
4078
+ if (!idGenerator) return;
4079
+ const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
4080
+ const initialId = ModelClass.initialId ? ModelClass.initialId : ModelClass.randomInitialId ? typeof ModelClass.randomInitialId === "function" ? ModelClass.randomInitialId() : randomInt(1e4, 499999) : 1;
4081
+ const incrementIdBy = ModelClass.randomIncrement ? typeof ModelClass.randomIncrement === "function" ? ModelClass.randomIncrement() : randomInt(1, 10) : ModelClass.incrementIdBy || 1;
4082
+ return await idGenerator.generateNextId({
4083
+ table: ModelClass.table,
4084
+ initialId,
4085
+ incrementIdBy
4086
+ });
4087
+ }
4088
+
4048
4089
  //#endregion
4049
4090
  //#region ../cascade/src/validation/database-writer-validation-error.ts
4050
4091
  /**
@@ -4708,15 +4749,8 @@ var DatabaseWriter = class {
4708
4749
  */
4709
4750
  async generateNextId() {
4710
4751
  if (!this.ctor.autoGenerateId || this.model.get("id")) return;
4711
- const idGenerator = this.dataSource.idGenerator;
4712
- if (!idGenerator) return;
4713
- const initialId = this.resolveInitialId();
4714
- const incrementIdBy = this.resolveIncrementBy();
4715
- const id = await idGenerator.generateNextId({
4716
- table: this.table,
4717
- initialId,
4718
- incrementIdBy
4719
- });
4752
+ const id = await generateModelId(this.ctor);
4753
+ if (id === void 0) return;
4720
4754
  this.model.set("id", id);
4721
4755
  }
4722
4756
  /**
@@ -4763,49 +4797,6 @@ var DatabaseWriter = class {
4763
4797
  return operations;
4764
4798
  }
4765
4799
  /**
4766
- * Resolve the initial ID from model configuration.
4767
- *
4768
- * Priority:
4769
- * 1. Model.initialId (explicit value)
4770
- * 2. Model.randomInitialId (random or function)
4771
- * 3. Default: 1
4772
- *
4773
- * @returns The initial ID value
4774
- * @private
4775
- */
4776
- resolveInitialId() {
4777
- if (this.ctor.initialId) return this.ctor.initialId;
4778
- if (this.ctor.randomInitialId) return typeof this.ctor.randomInitialId === "function" ? this.ctor.randomInitialId() : this.randomInt(1e4, 499999);
4779
- return 1;
4780
- }
4781
- /**
4782
- * Resolve the increment value from model configuration.
4783
- *
4784
- * Priority:
4785
- * 1. Model.incrementIdBy (explicit value)
4786
- * 2. Model.randomIncrement (random or function)
4787
- * 3. Default: 1
4788
- *
4789
- * @returns The increment value
4790
- * @private
4791
- */
4792
- resolveIncrementBy() {
4793
- if (this.ctor.randomIncrement) return typeof this.ctor.randomIncrement === "function" ? this.ctor.randomIncrement() : this.randomInt(1, 10);
4794
- if (this.ctor.incrementIdBy) return this.ctor.incrementIdBy;
4795
- return 1;
4796
- }
4797
- /**
4798
- * Generate a random integer between min and max (inclusive).
4799
- *
4800
- * @param min - Minimum value
4801
- * @param max - Maximum value
4802
- * @returns Random integer
4803
- * @private
4804
- */
4805
- randomInt(min, max) {
4806
- return Math.floor(Math.random() * (max - min + 1)) + min;
4807
- }
4808
- /**
4809
4800
  * Trigger sync operations after successful save.
4810
4801
  *
4811
4802
  * Emits a model.updated event that ModelSyncOperation listens to.
@@ -5318,15 +5309,39 @@ function toDriverOptions(options) {
5318
5309
  const { trustedFilter: _trustedFilter, ...rest } = options;
5319
5310
  return rest;
5320
5311
  }
5312
+ function applyWriteFilter(query, filter, trustedFilter) {
5313
+ if (trustedFilter && typeof query.whereTrusted === "function") return query.whereTrusted(filter);
5314
+ return query.where(filter);
5315
+ }
5316
+ /**
5317
+ * Add the same generated `id` that normal Mongo inserts receive to an upsert's
5318
+ * insert branch. `$setOnInsert` keeps an existing document's id immutable.
5319
+ */
5320
+ async function withAutoGeneratedUpsertId(ModelClass, filter, operations, options) {
5321
+ if (!options?.upsert || !ModelClass.autoGenerateId || ModelClass.getDriver().name !== "mongodb" || Array.isArray(operations)) return operations;
5322
+ const filterId = filter.id;
5323
+ const filterConstrainsId = Object.hasOwn(filter, "id") && (filterId === null || typeof filterId !== "object" || !Array.isArray(filterId) && Object.hasOwn(filterId, "$eq"));
5324
+ const updateSuppliesId = Object.hasOwn(operations.$set ?? {}, "id") || Object.hasOwn(operations.$setOnInsert ?? {}, "id");
5325
+ if (filterConstrainsId || updateSuppliesId) return operations;
5326
+ const id = await generateModelId(ModelClass);
5327
+ if (id === void 0) return operations;
5328
+ return {
5329
+ ...operations,
5330
+ $setOnInsert: {
5331
+ ...operations.$setOnInsert,
5332
+ id
5333
+ }
5334
+ };
5335
+ }
5321
5336
  /**
5322
5337
  * Pin a driver-level write to the rows the model's global scopes (tenant, soft
5323
5338
  * delete) allow. The driver write APIs take a raw filter and never see scopes,
5324
5339
  * so the target ids are resolved through the scoped query builder and added to
5325
5340
  * the filter. `null` means no visible row matches: the write must be a no-op.
5326
5341
  */
5327
- async function scopeWriteFilter(ModelClass, filter, single) {
5342
+ async function scopeWriteFilter(ModelClass, filter, single, trustedFilter = false) {
5328
5343
  const primaryKey = ModelClass.primaryKey;
5329
- const ids = await ModelClass.query().where(filter).pluck(primaryKey);
5344
+ const ids = await applyWriteFilter(ModelClass.query(), filter, trustedFilter).pluck(primaryKey);
5330
5345
  if (ids.length === 0) return null;
5331
5346
  return {
5332
5347
  ...filter,
@@ -5338,10 +5353,10 @@ async function scopeWriteFilter(ModelClass, filter, single) {
5338
5353
  * the driver would insert or, worse, update a row a scope hides (another
5339
5354
  * tenant's, a soft-deleted one), so a hidden match is refused.
5340
5355
  */
5341
- async function scopeUpsertFilter(ModelClass, filter) {
5342
- const scoped = await scopeWriteFilter(ModelClass, filter, true);
5356
+ async function scopeUpsertFilter(ModelClass, filter, trustedFilter = false) {
5357
+ const scoped = await scopeWriteFilter(ModelClass, filter, true, trustedFilter);
5343
5358
  if (scoped) return scoped;
5344
- if (await ModelClass.query().withoutGlobalScopes().where(filter).exists()) throw new Error(`${ModelClass.name}: upsert matches a row hidden by a global scope (tenant or soft delete); refusing to touch it.`);
5359
+ if (await applyWriteFilter(ModelClass.query().withoutGlobalScopes(), filter, trustedFilter).exists()) throw new Error(`${ModelClass.name}: upsert matches a row hidden by a global scope (tenant or soft delete); refusing to touch it.`);
5345
5360
  return filter;
5346
5361
  }
5347
5362
  /**
@@ -5350,9 +5365,10 @@ async function scopeUpsertFilter(ModelClass, filter) {
5350
5365
  */
5351
5366
  async function performAtomic(ModelClass, filter, operations, options) {
5352
5367
  const resolved = resolveFilter(filter, options);
5353
- const scoped = options?.upsert ? await scopeUpsertFilter(ModelClass, resolved) : await scopeWriteFilter(ModelClass, resolved, false);
5368
+ const scoped = options?.upsert ? await scopeUpsertFilter(ModelClass, resolved, options?.trustedFilter) : await scopeWriteFilter(ModelClass, resolved, false, options?.trustedFilter);
5354
5369
  if (!scoped) return 0;
5355
- const result = await ModelClass.getDriver().atomic(ModelClass.table, scoped, operations, toDriverOptions(options));
5370
+ const upsertOperations = await withAutoGeneratedUpsertId(ModelClass, scoped, operations, options);
5371
+ const result = await ModelClass.getDriver().atomic(ModelClass.table, scoped, upsertOperations, toDriverOptions(options));
5356
5372
  return result.modifiedCount + (result.upsertedCount ?? 0);
5357
5373
  }
5358
5374
  /**
@@ -5369,8 +5385,8 @@ async function updateById(ModelClass, id, data) {
5369
5385
  }
5370
5386
  async function findAndUpdateRecords(ModelClass, filter, update, options) {
5371
5387
  const primaryKey = ModelClass.primaryKey;
5372
- const safeFilter = sanitizeFilter(filter);
5373
- const ids = await ModelClass.query().where(safeFilter).pluck(primaryKey);
5388
+ const safeFilter = resolveFilter(filter, options);
5389
+ const ids = await applyWriteFilter(ModelClass.query(), safeFilter, options?.trustedFilter ?? false).pluck(primaryKey);
5374
5390
  if (ids.length === 0) {
5375
5391
  if (!options?.upsert) return [];
5376
5392
  await performAtomic(ModelClass, safeFilter, update, {
@@ -5387,9 +5403,10 @@ async function findAndUpdateRecords(ModelClass, filter, update, options) {
5387
5403
  }
5388
5404
  async function findOneAndUpdateRecord(ModelClass, filter, update, options) {
5389
5405
  const resolved = resolveFilter(filter, options);
5390
- const scoped = options?.upsert ? await scopeUpsertFilter(ModelClass, resolved) : await scopeWriteFilter(ModelClass, resolved, true);
5406
+ const scoped = options?.upsert ? await scopeUpsertFilter(ModelClass, resolved, options?.trustedFilter) : await scopeWriteFilter(ModelClass, resolved, true, options?.trustedFilter);
5391
5407
  if (!scoped) return null;
5392
- const result = await ModelClass.getDriver().findOneAndUpdate(ModelClass.table, scoped, update, toDriverOptions(options));
5408
+ const upsertUpdate = await withAutoGeneratedUpsertId(ModelClass, scoped, update, options);
5409
+ const result = await ModelClass.getDriver().findOneAndUpdate(ModelClass.table, scoped, upsertUpdate, toDriverOptions(options));
5393
5410
  if (!result) return null;
5394
5411
  return ModelClass.hydrate(result);
5395
5412
  }
@@ -7283,7 +7300,7 @@ var Model = class Model {
7283
7300
  * Find one and update multiple records that matches the provided filter and return the updated record
7284
7301
  * @param filter - Filter conditions
7285
7302
  * @param update - Update operators, or an aggregation pipeline (MongoDB only)
7286
- * @param options - `upsert`, `arrayFilters` (MongoDB only)
7303
+ * @param options - `upsert`, `arrayFilters` (MongoDB only), `trustedFilter`
7287
7304
  * @returns The updated records
7288
7305
  */
7289
7306
  static async findAndUpdate(filter, update, options) {
@@ -9492,6 +9509,21 @@ var QueryBuilder = class QueryBuilder {
9492
9509
  }
9493
9510
  return this;
9494
9511
  }
9512
+ /**
9513
+ * Add a code-authored object filter without the `$`-operator safety guard.
9514
+ * Never use this with request input; callers must use where() for that.
9515
+ */
9516
+ whereTrusted(conditions) {
9517
+ for (const [field, value] of Object.entries(conditions)) {
9518
+ assertDefined(value, field);
9519
+ this.addOperation("where", {
9520
+ field,
9521
+ operator: "=",
9522
+ value
9523
+ });
9524
+ }
9525
+ return this;
9526
+ }
9495
9527
  orWhere(...args) {
9496
9528
  if (args.length === 1 && typeof args[0] === "function") {
9497
9529
  const sub = this.subQuery();
@@ -10466,7 +10498,7 @@ var QueryBuilder = class QueryBuilder {
10466
10498
  }
10467
10499
  /** Raw GROUP BY expression. */
10468
10500
  groupByRaw(expression, bindings) {
10469
- this.addOperation("groupBy", {
10501
+ this.addOperation("groupByRaw", {
10470
10502
  expression,
10471
10503
  bindings: bindings ?? []
10472
10504
  });
@@ -12362,6 +12394,15 @@ var MongoQueryBuilder = class MongoQueryBuilder extends QueryBuilder {
12362
12394
  this.addWhereClause("where", args);
12363
12395
  return this;
12364
12396
  }
12397
+ /**
12398
+ * Add a code-authored MongoDB filter without the `$`-operator safety guard.
12399
+ * Never use this with request input; callers must use where() for that.
12400
+ */
12401
+ whereTrusted(conditions) {
12402
+ for (const [field, value] of Object.entries(conditions)) assertDefined(value, field);
12403
+ this.operationsHelper.addMatchOperation("where:object", conditions);
12404
+ return this;
12405
+ }
12365
12406
  orWhere(...args) {
12366
12407
  this.addWhereClause("orWhere", args);
12367
12408
  return this;
@@ -13583,7 +13624,7 @@ var MongoQueryBuilder = class MongoQueryBuilder extends QueryBuilder {
13583
13624
  * keeps a queue-claim pattern from silently running unlocked.
13584
13625
  */
13585
13626
  lockForUpdate() {
13586
- throw new Error("lockForUpdate() is not supported by the MongoDB driver — MongoDB has no row-level SELECT locking. Use an atomic claim instead (e.g. findOneAndUpdate with a reservation filter).");
13627
+ throw new UnsupportedLockForUpdateError();
13587
13628
  }
13588
13629
  /**
13589
13630
  * Executes a callback with the query builder without breaking the chain.
@@ -14390,6 +14431,7 @@ var MongoDbDriver = class {
14390
14431
  transactionOptions;
14391
14432
  idGeneratorInstance;
14392
14433
  _blueprint;
14434
+ monitoredCommands = /* @__PURE__ */ new Map();
14393
14435
  get blueprint() {
14394
14436
  if (!this._blueprint) this._blueprint = new MongoDBBlueprint(this.database);
14395
14437
  return this._blueprint;
@@ -14517,13 +14559,18 @@ var MongoDbDriver = class {
14517
14559
  _warlock_js_logger.log.warn("database.mongodb", "connection", "Disconnected from database");
14518
14560
  }
14519
14561
  });
14562
+ client.on("commandStarted", (event) => this.commandStarted(event));
14563
+ client.on("commandSucceeded", (event) => this.commandSucceeded(event));
14564
+ client.on("commandFailed", (event) => this.commandFailed(event));
14520
14565
  if (this.config.logging) {
14521
14566
  const ignoredCommands = [
14522
14567
  "isMaster",
14523
14568
  "hello",
14524
14569
  "ping",
14525
14570
  "saslStart",
14526
- "saslContinue"
14571
+ "saslContinue",
14572
+ "endSessions",
14573
+ "buildInfo"
14527
14574
  ];
14528
14575
  client.on("commandStarted", (event) => {
14529
14576
  if (ignoredCommands.includes(event.commandName)) return;
@@ -14580,6 +14627,10 @@ var MongoDbDriver = class {
14580
14627
  on(event, listener) {
14581
14628
  this.events.on(event, listener);
14582
14629
  }
14630
+ /** Remove a previously registered driver event listener. */
14631
+ off(event, listener) {
14632
+ this.events.off(event, listener);
14633
+ }
14583
14634
  /**
14584
14635
  * Insert a single document into the given collection.
14585
14636
  */
@@ -14896,7 +14947,7 @@ var MongoDbDriver = class {
14896
14947
  */
14897
14948
  buildClientOptions() {
14898
14949
  const baseOptions = { ...this.config.clientOptions ?? {} };
14899
- if (this.config.logging) baseOptions.monitorCommands = true;
14950
+ if (baseOptions.monitorCommands === void 0) baseOptions.monitorCommands = true;
14900
14951
  if (this.config.username && !baseOptions.auth) baseOptions.auth = {
14901
14952
  username: this.config.username,
14902
14953
  password: this.config.password
@@ -14910,6 +14961,65 @@ var MongoDbDriver = class {
14910
14961
  emit(event, ...args) {
14911
14962
  this.events.emit(event, ...args);
14912
14963
  }
14964
+ commandStarted(event) {
14965
+ if (this.ignoredCommand(event.commandName) || this.events.listenerCount("query") === 0) return;
14966
+ const started = performance.now();
14967
+ const { lsid: _lsid, $clusterTime: _clusterTime, $db: _database, ...pipeline } = event.command;
14968
+ const collection = event.command[event.commandName];
14969
+ this.monitoredCommands.set(event.requestId, {
14970
+ connection: this.name,
14971
+ driver: "mongodb",
14972
+ command: event.commandName,
14973
+ collection: typeof collection === "string" ? collection : void 0,
14974
+ pipeline,
14975
+ started,
14976
+ startedAt: performance.timeOrigin + started
14977
+ });
14978
+ }
14979
+ commandSucceeded(event) {
14980
+ const command = this.monitoredCommands.get(event.requestId);
14981
+ if (!command) return;
14982
+ this.monitoredCommands.delete(event.requestId);
14983
+ const { started, ...query } = command;
14984
+ const rowCount = event.reply?.n ?? event.reply?.cursor?.firstBatch?.length;
14985
+ this.emitQueryEvent({
14986
+ ...query,
14987
+ durationMs: performance.now() - started,
14988
+ ...typeof rowCount === "number" ? { rowCount } : {}
14989
+ });
14990
+ }
14991
+ commandFailed(event) {
14992
+ const command = this.monitoredCommands.get(event.requestId);
14993
+ if (!command) return;
14994
+ this.monitoredCommands.delete(event.requestId);
14995
+ const { started, ...query } = command;
14996
+ this.emitQueryEvent({
14997
+ ...query,
14998
+ durationMs: performance.now() - started,
14999
+ error: event.failure
15000
+ });
15001
+ }
15002
+ /**
15003
+ * Emit a `query` event. It runs inside the MongoDB client's own monitoring
15004
+ * callback, and an observer must never break the client, so a throwing
15005
+ * listener is swallowed here.
15006
+ */
15007
+ emitQueryEvent(event) {
15008
+ try {
15009
+ this.emit("query", event);
15010
+ } catch {}
15011
+ }
15012
+ ignoredCommand(command) {
15013
+ return [
15014
+ "isMaster",
15015
+ "hello",
15016
+ "ping",
15017
+ "saslStart",
15018
+ "saslContinue",
15019
+ "endSessions",
15020
+ "buildInfo"
15021
+ ].includes(command);
15022
+ }
14913
15023
  /**
14914
15024
  * Ensure MongoDB is running as a replica set (required for transactions).
14915
15025
  *
@@ -17315,12 +17425,11 @@ var PostgresQueryParser = class PostgresQueryParser {
17315
17425
  const columns = Array.isArray(fields) ? fields : [fields];
17316
17426
  this.groupColumns.push(...columns);
17317
17427
  }
17318
- /**
17319
- * Process a raw GROUP BY expression (emitted verbatim, no quoting). Used by
17320
- * `groupByDate` to add a `date_trunc(...)` bucket to the GROUP BY clause.
17321
- */
17428
+ /** Process a raw GROUP BY expression, binding `?` placeholders positionally. */
17322
17429
  processGroupByRaw(data) {
17323
- const expression = data.expression;
17430
+ let expression = data.expression;
17431
+ const bindings = data.bindings ?? [];
17432
+ for (const binding of bindings) expression = expression.replace("?", this.addParam(binding));
17324
17433
  this.groupRawExpressions.push(expression);
17325
17434
  }
17326
17435
  /**
@@ -17394,6 +17503,13 @@ function parseAggregateValue(value) {
17394
17503
  function toParserOps(ops) {
17395
17504
  return ops;
17396
17505
  }
17506
+ function containsOperatorKey(value) {
17507
+ if (Array.isArray(value)) return value.some(containsOperatorKey);
17508
+ if (value === null || typeof value !== "object") return false;
17509
+ const prototype = Object.getPrototypeOf(value);
17510
+ if (prototype !== Object.prototype && prototype !== null) return false;
17511
+ return Object.entries(value).some(([key, nested]) => key.startsWith("$") || containsOperatorKey(nested));
17512
+ }
17397
17513
  /**
17398
17514
  * PostgreSQL Query Builder.
17399
17515
  *
@@ -17414,6 +17530,14 @@ var PostgresQueryBuilder = class PostgresQueryBuilder extends QueryBuilder {
17414
17530
  table;
17415
17531
  /** Data source backing this builder. */
17416
17532
  dataSource;
17533
+ /**
17534
+ * PostgreSQL has no parser for MongoDB-style object operators such as
17535
+ * `{ id: { $in: [1, 2] } }`; accepting one would bind the object as `=`.
17536
+ */
17537
+ whereTrusted(conditions) {
17538
+ if (containsOperatorKey(conditions)) throw new UnsupportedQueryOperationError("whereTrusted with MongoDB-style operators", "postgres", "Use the portable whereIn()/where(field, operator, value) APIs instead.");
17539
+ return super.whereTrusted(conditions);
17540
+ }
17417
17541
  /** Hydration callback for transforming result rows into model instances. */
17418
17542
  hydrateCallback;
17419
17543
  /** Invoked before query execution. */
@@ -19323,6 +19447,13 @@ var PostgresDriver = class {
19323
19447
  if (!this._eventListeners.has(event)) this._eventListeners.set(event, /* @__PURE__ */ new Set());
19324
19448
  this._eventListeners.get(event).add(listener);
19325
19449
  }
19450
+ /** Remove a previously registered driver event listener. */
19451
+ off(event, listener) {
19452
+ const listeners = this._eventListeners.get(event);
19453
+ if (!listeners) return;
19454
+ listeners.delete(listener);
19455
+ if (listeners.size === 0) this._eventListeners.delete(event);
19456
+ }
19326
19457
  /**
19327
19458
  * Serialize data for storage in PostgreSQL.
19328
19459
  *
@@ -19824,12 +19955,12 @@ var PostgresDriver = class {
19824
19955
  if (options?.isolationLevel) beginSql += ` ISOLATION LEVEL ${options.isolationLevel.toUpperCase()}`;
19825
19956
  if (options?.readOnly) beginSql += " READ ONLY";
19826
19957
  if (options?.deferrable) beginSql += " DEFERRABLE";
19827
- await client.query(beginSql);
19958
+ await this.executePostgresQuery(() => client.query(beginSql), beginSql);
19828
19959
  transactionStarted = true;
19829
- for (const [name, value] of settings) await client.query("SELECT set_config($1, $2, true)", [name, String(value)]);
19960
+ for (const [name, value] of settings) await this.executePostgresQuery(() => client.query("SELECT set_config($1, $2, true)", [name, String(value)]), "SELECT set_config($1, $2, true)", [name, String(value)]);
19830
19961
  } catch (error) {
19831
19962
  if (transactionStarted) try {
19832
- await client.query("ROLLBACK");
19963
+ await this.executePostgresQuery(() => client.query("ROLLBACK"), "ROLLBACK");
19833
19964
  } catch {}
19834
19965
  client.release(error instanceof Error ? error : new Error(String(error)));
19835
19966
  throw error;
@@ -19838,7 +19969,7 @@ var PostgresDriver = class {
19838
19969
  context: client,
19839
19970
  commit: async () => {
19840
19971
  try {
19841
- await client.query("COMMIT");
19972
+ await this.executePostgresQuery(() => client.query("COMMIT"), "COMMIT");
19842
19973
  } catch (error) {
19843
19974
  client.release(error instanceof Error ? error : new Error(String(error)));
19844
19975
  throw error;
@@ -19847,7 +19978,7 @@ var PostgresDriver = class {
19847
19978
  },
19848
19979
  rollback: async () => {
19849
19980
  try {
19850
- await client.query("ROLLBACK");
19981
+ await this.executePostgresQuery(() => client.query("ROLLBACK"), "ROLLBACK");
19851
19982
  } catch (error) {
19852
19983
  client.release(error instanceof Error ? error : new Error(String(error)));
19853
19984
  throw error;
@@ -20080,8 +20211,8 @@ var PostgresDriver = class {
20080
20211
  paramCount: params.length
20081
20212
  }
20082
20213
  });
20083
- if (txClient) result = await txClient.query(sql, params);
20084
- else result = await this.pool.query(sql, params);
20214
+ if (txClient) result = await this.executePostgresQuery(() => txClient.query(sql, params), sql, params);
20215
+ else result = await this.executePostgresQuery(() => this.pool.query(sql, params), sql, params);
20085
20216
  if (this.config.logging) {
20086
20217
  const duration = (performance.now() - startTime).toFixed(2);
20087
20218
  _warlock_js_logger.log.success({
@@ -20114,6 +20245,47 @@ var PostgresDriver = class {
20114
20245
  throw error;
20115
20246
  }
20116
20247
  }
20248
+ /** Execute SQL and emit its public query event when the event is observed. */
20249
+ async executePostgresQuery(execute, sql, bindings = []) {
20250
+ if (!this._eventListeners.get("query")?.size) return execute();
20251
+ const started = performance.now();
20252
+ const startedAt = performance.timeOrigin + started;
20253
+ let result;
20254
+ try {
20255
+ result = await execute();
20256
+ } catch (error) {
20257
+ this.emitQueryEvent({
20258
+ connection: this.name,
20259
+ driver: "postgres",
20260
+ sql,
20261
+ bindings,
20262
+ durationMs: performance.now() - started,
20263
+ startedAt,
20264
+ error
20265
+ });
20266
+ throw error;
20267
+ }
20268
+ const event = {
20269
+ connection: this.name,
20270
+ driver: "postgres",
20271
+ sql,
20272
+ bindings,
20273
+ durationMs: performance.now() - started,
20274
+ startedAt
20275
+ };
20276
+ if (typeof result.rowCount === "number") event.rowCount = result.rowCount;
20277
+ this.emitQueryEvent(event);
20278
+ return result;
20279
+ }
20280
+ /**
20281
+ * Emit a `query` event. An observer is never allowed to fail the query it
20282
+ * observes, so a throwing listener is swallowed here.
20283
+ */
20284
+ emitQueryEvent(event) {
20285
+ try {
20286
+ this.emit("query", event);
20287
+ } catch {}
20288
+ }
20117
20289
  /**
20118
20290
  * Emit an event to all registered listeners.
20119
20291
  *
@@ -22110,6 +22282,7 @@ var Migration = class {
22110
22282
  * @internal
22111
22283
  */
22112
22284
  async execute() {
22285
+ this.assertNoGeneratedIndexNameCollisions();
22113
22286
  for (const op of this.pendingOperations) await this.executeOperation(op);
22114
22287
  this.pendingOperations.length = 0;
22115
22288
  }
@@ -22146,6 +22319,7 @@ var Migration = class {
22146
22319
  * @internal
22147
22320
  */
22148
22321
  toSteps() {
22322
+ this.assertNoGeneratedIndexNameCollisions();
22149
22323
  const serializer = this.driver.driver.getSQLSerializer();
22150
22324
  const steps = [];
22151
22325
  let schemaRun = [];
@@ -22293,6 +22467,40 @@ var Migration = class {
22293
22467
  }
22294
22468
  }
22295
22469
  /**
22470
+ * Reject two unnamed indexes that a driver would give the same name before
22471
+ * either DDL command is sent. MongoDB's default names include directions;
22472
+ * PostgreSQL's include the table and columns.
22473
+ */
22474
+ assertNoGeneratedIndexNameCollisions() {
22475
+ const engine = this.databaseEngine;
22476
+ if (engine !== "mongodb" && engine !== "postgres") return;
22477
+ const unnamedIndexes = this.pendingOperations.flatMap((operation) => {
22478
+ if (operation.type === "createIndex") {
22479
+ const index = operation.payload;
22480
+ return index.name ? [] : [index];
22481
+ }
22482
+ if (operation.type === "createUniqueIndex") {
22483
+ const { columns, name } = operation.payload;
22484
+ return name ? [] : [{
22485
+ columns,
22486
+ unique: true
22487
+ }];
22488
+ }
22489
+ return [];
22490
+ });
22491
+ const seen = /* @__PURE__ */ new Map();
22492
+ for (const index of unnamedIndexes) {
22493
+ const generatedName = engine === "mongodb" ? index.columns.map((column, i) => `${column}_${index.directions?.[i] === "desc" ? -1 : 1}`).join("_") : `idx_${this.table}_${index.columns.join("_")}`;
22494
+ const first = seen.get(generatedName);
22495
+ if (first) throw new Error(`Migration queued two ${engine} indexes with the generated name "${generatedName}": ${this.describeIndex(first)} and ${this.describeIndex(index)}. Give one index an explicit name, for example { name: "..." }.`);
22496
+ seen.set(generatedName, index);
22497
+ }
22498
+ }
22499
+ describeIndex(index) {
22500
+ const partial = index.where ? JSON.stringify(index.where) : "none";
22501
+ return `columns [${index.columns.join(", ")}] (unique: ${!!index.unique}, partial: ${partial})`;
22502
+ }
22503
+ /**
22296
22504
  * Check if a table exists.
22297
22505
  *
22298
22506
  * Useful for conditional migrations and idempotent operations.
@@ -23999,13 +24207,18 @@ Migration.create = function createMigration(model, columns, options = {}) {
23999
24207
  */
24000
24208
  Migration.alter = function alterMigration(model, schema, options = {}) {
24001
24209
  const { order = 0, createdAt, transactional } = options;
24002
- return class AlterMigration extends Migration {
24210
+ let missingDownWarningEmitted = false;
24211
+ class AlterMigration extends Migration {
24003
24212
  static order = order;
24004
24213
  static createdAt = createdAt;
24005
24214
  static transactional = transactional;
24006
24215
  table = model.table;
24007
24216
  dataSource = model.dataSource;
24008
24217
  async up() {
24218
+ if (options.up && !options.down && !missingDownWarningEmitted) {
24219
+ missingDownWarningEmitted = true;
24220
+ _warlock_js_logger.log.warn("database", "migration", `Migration "${AlterMigration.migrationName ?? this.table}" defines a custom up() without a down() rollback.`);
24221
+ }
24009
24222
  if (schema.add) wireColumns(this, schema.add);
24010
24223
  if (schema.drop) for (const col of schema.drop) this.dropColumn(col);
24011
24224
  if (schema.rename) for (const [from, to] of Object.entries(schema.rename)) this.renameColumn(from, to);
@@ -24058,7 +24271,8 @@ Migration.alter = function alterMigration(model, schema, options = {}) {
24058
24271
  async down() {
24059
24272
  if (options.down) await options.down.call(this);
24060
24273
  }
24061
- };
24274
+ }
24275
+ return AlterMigration;
24062
24276
  };
24063
24277
  Migration.__declarativeFactoriesAttached = true;
24064
24278
 
@@ -24134,11 +24348,14 @@ function compareCreatedAt(a, b) {
24134
24348
  * Comparator for applying migrations — oldest first.
24135
24349
  *
24136
24350
  * Priority:
24137
- * 1. `order` override (lower = earlier, default 0)
24138
- * 2. `createdAt` timestamp (older = earlier)
24139
- * 3. Alphabetical by migration name (last resort)
24351
+ * 1. package origin before app origin
24352
+ * 2. `order` override (lower = earlier, default 0)
24353
+ * 3. `createdAt` timestamp (older = earlier)
24354
+ * 4. Alphabetical by migration name (last resort)
24140
24355
  */
24141
24356
  function sortMigrations(a, b) {
24357
+ const byOrigin = Number(a.origin === "app") - Number(b.origin === "app");
24358
+ if (byOrigin !== 0) return byOrigin;
24142
24359
  const byOrder = (a.order ?? 0) - (b.order ?? 0);
24143
24360
  if (byOrder !== 0) return byOrder;
24144
24361
  const byCreatedAt = compareCreatedAt(a.createdAt, b.createdAt);
@@ -24514,7 +24731,7 @@ var MigrationRunner = class {
24514
24731
  * The migration name is read from `MigrationClass.migrationName`.
24515
24732
  *
24516
24733
  * @param MigrationClass - Migration class (must have static `name` set)
24517
- * @param createdAt - Optional timestamp for ordering
24734
+ * @param origin - Registration source; defaults to `"app"` for direct registrations.
24518
24735
  * @returns This runner for chaining
24519
24736
  *
24520
24737
  * @example
@@ -24523,7 +24740,7 @@ var MigrationRunner = class {
24523
24740
  * runner.register(CreateUsersTable);
24524
24741
  * ```
24525
24742
  */
24526
- register(MigrationClass) {
24743
+ register(MigrationClass, origin = "app") {
24527
24744
  const name = MigrationClass.migrationName;
24528
24745
  if (!name) throw new Error("Migration class must have a static 'migrationName' property set. Set it in CLI after importing: MigrationClass.migrationName = \"filename\";");
24529
24746
  const existing = this.migrations.find((m) => m.migrationName === name);
@@ -24531,6 +24748,7 @@ var MigrationRunner = class {
24531
24748
  if (existing !== MigrationClass) throw new Error(`Duplicate migration name "${name}": two different migration classes resolve to the same name. Rename one of the files (or set a unique static 'migrationName').`);
24532
24749
  return this;
24533
24750
  }
24751
+ MigrationClass.origin = origin;
24534
24752
  this.migrations.push(MigrationClass);
24535
24753
  return this;
24536
24754
  }
@@ -25321,6 +25539,7 @@ exports.UndefinedWhereValueError = UndefinedWhereValueError;
25321
25539
  exports.UnsafeFilterError = UnsafeFilterError;
25322
25540
  exports.UnsafeRawExpressionError = UnsafeRawExpressionError;
25323
25541
  exports.UnsupportedLeanOperationError = UnsupportedLeanOperationError;
25542
+ exports.UnsupportedLockForUpdateError = UnsupportedLockForUpdateError;
25324
25543
  exports.UnsupportedQueryOperationError = UnsupportedQueryOperationError;
25325
25544
  exports.UnsupportedUpdateOperationError = UnsupportedUpdateOperationError;
25326
25545
  exports.afterCommit = afterCommit;