@warlock.js/cascade 5.23.2 → 5.25.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 (72) hide show
  1. package/CHANGELOG.md +352 -329
  2. package/README.md +101 -101
  3. package/bin/cascade.js +0 -0
  4. package/cjs/index.cjs +208 -69
  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/data-source/data-source-registry.d.mts +8 -6
  10. package/esm/data-source/data-source-registry.d.mts.map +1 -1
  11. package/esm/data-source/data-source-registry.mjs +7 -1
  12. package/esm/data-source/data-source-registry.mjs.map +1 -1
  13. package/esm/drivers/mongodb/mongodb-driver.d.mts +13 -0
  14. package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
  15. package/esm/drivers/mongodb/mongodb-driver.mjs +71 -2
  16. package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
  17. package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
  18. package/esm/drivers/mongodb/mongodb-query-builder.mjs +2 -1
  19. package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
  20. package/esm/drivers/postgres/postgres-driver.d.mts +9 -0
  21. package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
  22. package/esm/drivers/postgres/postgres-driver.mjs +61 -5
  23. package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
  24. package/esm/drivers/postgres/postgres-query-parser.d.mts +1 -4
  25. package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
  26. package/esm/drivers/postgres/postgres-query-parser.mjs +4 -5
  27. package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
  28. package/esm/drivers/postgres/types.d.mts +5 -0
  29. package/esm/drivers/postgres/types.d.mts.map +1 -1
  30. package/esm/errors/unsafe-filter.error.mjs.map +1 -1
  31. package/esm/errors/unsafe-raw-expression.error.mjs.map +1 -1
  32. package/esm/errors/unsupported-lock-for-update.error.d.mts +11 -0
  33. package/esm/errors/unsupported-lock-for-update.error.d.mts.map +1 -0
  34. package/esm/errors/unsupported-lock-for-update.error.mjs +16 -0
  35. package/esm/errors/unsupported-lock-for-update.error.mjs.map +1 -0
  36. package/esm/events/model-events.mjs.map +1 -1
  37. package/esm/index.d.mts +3 -2
  38. package/esm/index.mjs +2 -1
  39. package/esm/migration/migration-runner.mjs.map +1 -1
  40. package/esm/migration/sql-grammar.mjs.map +1 -1
  41. package/esm/model/generate-model-id.mjs +23 -0
  42. package/esm/model/generate-model-id.mjs.map +1 -0
  43. package/esm/model/methods/delete-methods.mjs.map +1 -1
  44. package/esm/model/methods/meta-methods.mjs.map +1 -1
  45. package/esm/model/methods/query-methods.mjs +25 -2
  46. package/esm/model/methods/query-methods.mjs.map +1 -1
  47. package/esm/model/methods/write-methods.mjs.map +1 -1
  48. package/esm/query-builder/query-builder.mjs +1 -1
  49. package/esm/query-builder/query-builder.mjs.map +1 -1
  50. package/esm/utils/sanitize-filter.mjs.map +1 -1
  51. package/esm/validation/plugins/database-rules-plugin.mjs.map +1 -1
  52. package/esm/writer/database-writer.d.mts +0 -33
  53. package/esm/writer/database-writer.d.mts.map +1 -1
  54. package/esm/writer/database-writer.mjs +4 -52
  55. package/esm/writer/database-writer.mjs.map +1 -1
  56. package/llms-full.txt +2835 -2783
  57. package/package.json +5 -5
  58. package/skills/aggregate-data/SKILL.md +165 -165
  59. package/skills/alter-migration/SKILL.md +531 -531
  60. package/skills/configure-delete-strategy/SKILL.md +130 -130
  61. package/skills/define-model/SKILL.md +205 -205
  62. package/skills/define-relations/SKILL.md +171 -171
  63. package/skills/manage-data-sources/SKILL.md +177 -140
  64. package/skills/manage-transactions/SKILL.md +233 -208
  65. package/skills/paginate-results/SKILL.md +122 -122
  66. package/skills/perform-atomic-ops/SKILL.md +156 -156
  67. package/skills/query-data/SKILL.md +213 -213
  68. package/skills/run-cascade-cli/SKILL.md +125 -125
  69. package/skills/search-by-vector/SKILL.md +127 -127
  70. package/skills/subscribe-to-model-events/SKILL.md +148 -148
  71. package/skills/track-changes/SKILL.md +127 -127
  72. package/skills/write-migration/SKILL.md +206 -206
package/README.md CHANGED
@@ -1,102 +1,102 @@
1
- # cascade
2
-
3
- A powerful, model-first TypeScript ORM for **MongoDB and PostgreSQL** on `Node.js`.
4
-
5
- ## Features
6
-
7
- Cascade is a unified ORM for MongoDB and PostgreSQL — one model-first API runs against both engines. Define a model once (its `@warlock.js/seal` schema doubles as the TypeScript type, the runtime validator, and the DB shape) and query straight off the class. It's fast, type-safe, and built for real-world apps.
8
-
9
- Some but not all of the features:
10
-
11
-
12
- - **Easy to use:** `Cascade` is very easy to use, it's just a wrapper around MongoDB driver.
13
- - **Supports multiple connections:** You can perform multiple connections to different MongoDB connections and use each one of them separately.
14
- - **Supports multiple databases:** `Cascade` supports multiple databases, you can connect to multiple databases at the same time.
15
- - **Powerful Aggregate framework:** `Cascade` has a powerful aggregate framework that helps you to perform complex queries.
16
- - **Basic CRUD operations:** `Cascade` supports basic CRUD operations, you can perform create, read, update and delete operations.
17
- - **Events Driven:** `Cascade` is events driven, you can listen to events and perform actions, for example before creating, updating or deleting a document.
18
- - **Powerful Models:** `Cascade` has a powerful models system, a Model is a collection manager document based, it manages a collection's document easily with many utilities.
19
- - **Learning curve:** `Cascade` has a very small learning curve, you can learn it in few minutes.
20
- - **Pagination support:** `Cascade` supports pagination, you can paginate your results easily.
21
- - **Output formatting:** `Cascade` supports output formatting, you can format your output easily when model is sent as a response.
22
- - **Auto incremented id:** `Cascade` supports auto incremented id, you can use it as a primary key for your documents.
23
- - **Random or sequential id:** `Cascade` supports random or sequential id.
24
- - **Recycle Bin:** Reduce collection documents by removing the document entirely from the collection, but move it to a separate collection trash.
25
- - **Migration system:** `Cascade` has a migration system, you can create migrations and run them easily.
26
- - **Data casting:** You can cast your data to a specific type or using custom casting.
27
- - **Embedded documents:** `Cascade` supports single and multiple embedded documents, you can embed documents inside other documents.
28
- - **Syncing Models**: Auto update documents when model's data is updated or deleted.
29
-
30
-
31
- ## Server-only
32
-
33
- `@warlock.js/cascade`'s entire runtime surface is server-only — its `package.json` declares `"warlock": { "environment": "server" }`. `@warlock.js/web`'s build treats this as a boundary: app client code cannot value-import this package (type-only imports are still allowed), while server loaders/controllers/modules can import it freely.
34
-
35
- ## Installation
36
-
37
- ```bash
38
- npm i @warlock.js/cascade
39
- ```
40
-
41
- Using yarn:
42
-
43
- ```bash
44
- yarn add @warlock.js/cascade
45
- ```
46
-
47
- Using pnpm:
48
-
49
- ```bash
50
- pnpm add @warlock.js/cascade
51
- ```
52
-
53
-
54
- ## Peek inside cascade
55
-
56
- Here is a simple example of defining a User model:
57
-
58
- ```ts title="src/models/user.ts"
59
- import { Model, RegisterModel } from "@warlock.js/cascade";
60
-
61
- @RegisterModel()
62
- export class User extends Model {
63
- /**
64
- * The table / collection name.
65
- * Must be defined explicitly.
66
- */
67
- public static table = "users";
68
- }
69
- ```
70
-
71
- A quick example of creating a user:
72
-
73
- ```ts title="src/controllers/users.ts"
74
- import { User } from "src/models/user";
75
-
76
- export async function createUser() {
77
- const user = await User.create({
78
- name: "Hasan Zohdy",
79
- email: "hassanzohdy@gmail.com",
80
- });
81
-
82
- console.log(user.data);
83
- }
84
- ```
85
-
86
- Outputs something similar to:
87
-
88
- ```json
89
- {
90
- "id": 1231412,
91
- "_id": "fagtrw43qwedasjoijwq",
92
- "name": "Hasan Zohdy",
93
- "email": "hassanzohdy@gmail.com",
94
- "createdAt": "2023-06-01 00:00:00",
95
- "updatedAt": "2023-06-01 00:00:00"
96
- }
97
- ```
98
-
99
-
100
- ## Documentation
101
-
1
+ # cascade
2
+
3
+ A powerful, model-first TypeScript ORM for **MongoDB and PostgreSQL** on `Node.js`.
4
+
5
+ ## Features
6
+
7
+ Cascade is a unified ORM for MongoDB and PostgreSQL — one model-first API runs against both engines. Define a model once (its `@warlock.js/seal` schema doubles as the TypeScript type, the runtime validator, and the DB shape) and query straight off the class. It's fast, type-safe, and built for real-world apps.
8
+
9
+ Some but not all of the features:
10
+
11
+
12
+ - **Easy to use:** `Cascade` is very easy to use, it's just a wrapper around MongoDB driver.
13
+ - **Supports multiple connections:** You can perform multiple connections to different MongoDB connections and use each one of them separately.
14
+ - **Supports multiple databases:** `Cascade` supports multiple databases, you can connect to multiple databases at the same time.
15
+ - **Powerful Aggregate framework:** `Cascade` has a powerful aggregate framework that helps you to perform complex queries.
16
+ - **Basic CRUD operations:** `Cascade` supports basic CRUD operations, you can perform create, read, update and delete operations.
17
+ - **Events Driven:** `Cascade` is events driven, you can listen to events and perform actions, for example before creating, updating or deleting a document.
18
+ - **Powerful Models:** `Cascade` has a powerful models system, a Model is a collection manager document based, it manages a collection's document easily with many utilities.
19
+ - **Learning curve:** `Cascade` has a very small learning curve, you can learn it in few minutes.
20
+ - **Pagination support:** `Cascade` supports pagination, you can paginate your results easily.
21
+ - **Output formatting:** `Cascade` supports output formatting, you can format your output easily when model is sent as a response.
22
+ - **Auto incremented id:** `Cascade` supports auto incremented id, you can use it as a primary key for your documents.
23
+ - **Random or sequential id:** `Cascade` supports random or sequential id.
24
+ - **Recycle Bin:** Reduce collection documents by removing the document entirely from the collection, but move it to a separate collection trash.
25
+ - **Migration system:** `Cascade` has a migration system, you can create migrations and run them easily.
26
+ - **Data casting:** You can cast your data to a specific type or using custom casting.
27
+ - **Embedded documents:** `Cascade` supports single and multiple embedded documents, you can embed documents inside other documents.
28
+ - **Syncing Models**: Auto update documents when model's data is updated or deleted.
29
+
30
+
31
+ ## Server-only
32
+
33
+ `@warlock.js/cascade`'s entire runtime surface is server-only — its `package.json` declares `"warlock": { "environment": "server" }`. `@warlock.js/web`'s build treats this as a boundary: app client code cannot value-import this package (type-only imports are still allowed), while server loaders/controllers/modules can import it freely.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ npm i @warlock.js/cascade
39
+ ```
40
+
41
+ Using yarn:
42
+
43
+ ```bash
44
+ yarn add @warlock.js/cascade
45
+ ```
46
+
47
+ Using pnpm:
48
+
49
+ ```bash
50
+ pnpm add @warlock.js/cascade
51
+ ```
52
+
53
+
54
+ ## Peek inside cascade
55
+
56
+ Here is a simple example of defining a User model:
57
+
58
+ ```ts title="src/models/user.ts"
59
+ import { Model, RegisterModel } from "@warlock.js/cascade";
60
+
61
+ @RegisterModel()
62
+ export class User extends Model {
63
+ /**
64
+ * The table / collection name.
65
+ * Must be defined explicitly.
66
+ */
67
+ public static table = "users";
68
+ }
69
+ ```
70
+
71
+ A quick example of creating a user:
72
+
73
+ ```ts title="src/controllers/users.ts"
74
+ import { User } from "src/models/user";
75
+
76
+ export async function createUser() {
77
+ const user = await User.create({
78
+ name: "Hasan Zohdy",
79
+ email: "hassanzohdy@gmail.com",
80
+ });
81
+
82
+ console.log(user.data);
83
+ }
84
+ ```
85
+
86
+ Outputs something similar to:
87
+
88
+ ```json
89
+ {
90
+ "id": 1231412,
91
+ "_id": "fagtrw43qwedasjoijwq",
92
+ "name": "Hasan Zohdy",
93
+ "email": "hassanzohdy@gmail.com",
94
+ "createdAt": "2023-06-01 00:00:00",
95
+ "updatedAt": "2023-06-01 00:00:00"
96
+ }
97
+ ```
98
+
99
+
100
+ ## Documentation
101
+
102
102
  To see the full documentation, please visit [Cascade Documentation](https://warlock.js.org/docs/cascade/getting-started/introduction)
package/bin/cascade.js CHANGED
File without changes
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
  /**
@@ -4589,6 +4630,7 @@ var DatabaseWriter = class {
4589
4630
  else {
4590
4631
  const validated = result.data;
4591
4632
  for (const column of Object.keys(dataToValidate)) if (column in validated) this.model.set(column, validated[column]);
4633
+ else if (dataToValidate[column] === null && column in validationSchema.schema) this.model.set(column, null);
4592
4634
  else this.unvalidatedColumns.add(column);
4593
4635
  }
4594
4636
  if (!options.skipEvents) await this.model.emitEvent("validated", { result });
@@ -4707,15 +4749,8 @@ var DatabaseWriter = class {
4707
4749
  */
4708
4750
  async generateNextId() {
4709
4751
  if (!this.ctor.autoGenerateId || this.model.get("id")) return;
4710
- const idGenerator = this.dataSource.idGenerator;
4711
- if (!idGenerator) return;
4712
- const initialId = this.resolveInitialId();
4713
- const incrementIdBy = this.resolveIncrementBy();
4714
- const id = await idGenerator.generateNextId({
4715
- table: this.table,
4716
- initialId,
4717
- incrementIdBy
4718
- });
4752
+ const id = await generateModelId(this.ctor);
4753
+ if (id === void 0) return;
4719
4754
  this.model.set("id", id);
4720
4755
  }
4721
4756
  /**
@@ -4762,49 +4797,6 @@ var DatabaseWriter = class {
4762
4797
  return operations;
4763
4798
  }
4764
4799
  /**
4765
- * Resolve the initial ID from model configuration.
4766
- *
4767
- * Priority:
4768
- * 1. Model.initialId (explicit value)
4769
- * 2. Model.randomInitialId (random or function)
4770
- * 3. Default: 1
4771
- *
4772
- * @returns The initial ID value
4773
- * @private
4774
- */
4775
- resolveInitialId() {
4776
- if (this.ctor.initialId) return this.ctor.initialId;
4777
- if (this.ctor.randomInitialId) return typeof this.ctor.randomInitialId === "function" ? this.ctor.randomInitialId() : this.randomInt(1e4, 499999);
4778
- return 1;
4779
- }
4780
- /**
4781
- * Resolve the increment value from model configuration.
4782
- *
4783
- * Priority:
4784
- * 1. Model.incrementIdBy (explicit value)
4785
- * 2. Model.randomIncrement (random or function)
4786
- * 3. Default: 1
4787
- *
4788
- * @returns The increment value
4789
- * @private
4790
- */
4791
- resolveIncrementBy() {
4792
- if (this.ctor.randomIncrement) return typeof this.ctor.randomIncrement === "function" ? this.ctor.randomIncrement() : this.randomInt(1, 10);
4793
- if (this.ctor.incrementIdBy) return this.ctor.incrementIdBy;
4794
- return 1;
4795
- }
4796
- /**
4797
- * Generate a random integer between min and max (inclusive).
4798
- *
4799
- * @param min - Minimum value
4800
- * @param max - Maximum value
4801
- * @returns Random integer
4802
- * @private
4803
- */
4804
- randomInt(min, max) {
4805
- return Math.floor(Math.random() * (max - min + 1)) + min;
4806
- }
4807
- /**
4808
4800
  * Trigger sync operations after successful save.
4809
4801
  *
4810
4802
  * Emits a model.updated event that ModelSyncOperation listens to.
@@ -5318,6 +5310,26 @@ function toDriverOptions(options) {
5318
5310
  return rest;
5319
5311
  }
5320
5312
  /**
5313
+ * Add the same generated `id` that normal Mongo inserts receive to an upsert's
5314
+ * insert branch. `$setOnInsert` keeps an existing document's id immutable.
5315
+ */
5316
+ async function withAutoGeneratedUpsertId(ModelClass, filter, operations, options) {
5317
+ if (!options?.upsert || !ModelClass.autoGenerateId || ModelClass.getDriver().name !== "mongodb" || Array.isArray(operations)) return operations;
5318
+ const filterId = filter.id;
5319
+ const filterConstrainsId = Object.hasOwn(filter, "id") && (filterId === null || typeof filterId !== "object" || !Array.isArray(filterId) && Object.hasOwn(filterId, "$eq"));
5320
+ const updateSuppliesId = Object.hasOwn(operations.$set ?? {}, "id") || Object.hasOwn(operations.$setOnInsert ?? {}, "id");
5321
+ if (filterConstrainsId || updateSuppliesId) return operations;
5322
+ const id = await generateModelId(ModelClass);
5323
+ if (id === void 0) return operations;
5324
+ return {
5325
+ ...operations,
5326
+ $setOnInsert: {
5327
+ ...operations.$setOnInsert,
5328
+ id
5329
+ }
5330
+ };
5331
+ }
5332
+ /**
5321
5333
  * Pin a driver-level write to the rows the model's global scopes (tenant, soft
5322
5334
  * delete) allow. The driver write APIs take a raw filter and never see scopes,
5323
5335
  * so the target ids are resolved through the scoped query builder and added to
@@ -5351,7 +5363,8 @@ async function performAtomic(ModelClass, filter, operations, options) {
5351
5363
  const resolved = resolveFilter(filter, options);
5352
5364
  const scoped = options?.upsert ? await scopeUpsertFilter(ModelClass, resolved) : await scopeWriteFilter(ModelClass, resolved, false);
5353
5365
  if (!scoped) return 0;
5354
- const result = await ModelClass.getDriver().atomic(ModelClass.table, scoped, operations, toDriverOptions(options));
5366
+ const upsertOperations = await withAutoGeneratedUpsertId(ModelClass, scoped, operations, options);
5367
+ const result = await ModelClass.getDriver().atomic(ModelClass.table, scoped, upsertOperations, toDriverOptions(options));
5355
5368
  return result.modifiedCount + (result.upsertedCount ?? 0);
5356
5369
  }
5357
5370
  /**
@@ -5388,7 +5401,8 @@ async function findOneAndUpdateRecord(ModelClass, filter, update, options) {
5388
5401
  const resolved = resolveFilter(filter, options);
5389
5402
  const scoped = options?.upsert ? await scopeUpsertFilter(ModelClass, resolved) : await scopeWriteFilter(ModelClass, resolved, true);
5390
5403
  if (!scoped) return null;
5391
- const result = await ModelClass.getDriver().findOneAndUpdate(ModelClass.table, scoped, update, toDriverOptions(options));
5404
+ const upsertUpdate = await withAutoGeneratedUpsertId(ModelClass, scoped, update, options);
5405
+ const result = await ModelClass.getDriver().findOneAndUpdate(ModelClass.table, scoped, upsertUpdate, toDriverOptions(options));
5392
5406
  if (!result) return null;
5393
5407
  return ModelClass.hydrate(result);
5394
5408
  }
@@ -10465,7 +10479,7 @@ var QueryBuilder = class QueryBuilder {
10465
10479
  }
10466
10480
  /** Raw GROUP BY expression. */
10467
10481
  groupByRaw(expression, bindings) {
10468
- this.addOperation("groupBy", {
10482
+ this.addOperation("groupByRaw", {
10469
10483
  expression,
10470
10484
  bindings: bindings ?? []
10471
10485
  });
@@ -13582,7 +13596,7 @@ var MongoQueryBuilder = class MongoQueryBuilder extends QueryBuilder {
13582
13596
  * keeps a queue-claim pattern from silently running unlocked.
13583
13597
  */
13584
13598
  lockForUpdate() {
13585
- 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).");
13599
+ throw new UnsupportedLockForUpdateError();
13586
13600
  }
13587
13601
  /**
13588
13602
  * Executes a callback with the query builder without breaking the chain.
@@ -14389,6 +14403,7 @@ var MongoDbDriver = class {
14389
14403
  transactionOptions;
14390
14404
  idGeneratorInstance;
14391
14405
  _blueprint;
14406
+ monitoredCommands = /* @__PURE__ */ new Map();
14392
14407
  get blueprint() {
14393
14408
  if (!this._blueprint) this._blueprint = new MongoDBBlueprint(this.database);
14394
14409
  return this._blueprint;
@@ -14516,13 +14531,18 @@ var MongoDbDriver = class {
14516
14531
  _warlock_js_logger.log.warn("database.mongodb", "connection", "Disconnected from database");
14517
14532
  }
14518
14533
  });
14534
+ client.on("commandStarted", (event) => this.commandStarted(event));
14535
+ client.on("commandSucceeded", (event) => this.commandSucceeded(event));
14536
+ client.on("commandFailed", (event) => this.commandFailed(event));
14519
14537
  if (this.config.logging) {
14520
14538
  const ignoredCommands = [
14521
14539
  "isMaster",
14522
14540
  "hello",
14523
14541
  "ping",
14524
14542
  "saslStart",
14525
- "saslContinue"
14543
+ "saslContinue",
14544
+ "endSessions",
14545
+ "buildInfo"
14526
14546
  ];
14527
14547
  client.on("commandStarted", (event) => {
14528
14548
  if (ignoredCommands.includes(event.commandName)) return;
@@ -14579,6 +14599,10 @@ var MongoDbDriver = class {
14579
14599
  on(event, listener) {
14580
14600
  this.events.on(event, listener);
14581
14601
  }
14602
+ /** Remove a previously registered driver event listener. */
14603
+ off(event, listener) {
14604
+ this.events.off(event, listener);
14605
+ }
14582
14606
  /**
14583
14607
  * Insert a single document into the given collection.
14584
14608
  */
@@ -14895,7 +14919,7 @@ var MongoDbDriver = class {
14895
14919
  */
14896
14920
  buildClientOptions() {
14897
14921
  const baseOptions = { ...this.config.clientOptions ?? {} };
14898
- if (this.config.logging) baseOptions.monitorCommands = true;
14922
+ if (baseOptions.monitorCommands === void 0) baseOptions.monitorCommands = true;
14899
14923
  if (this.config.username && !baseOptions.auth) baseOptions.auth = {
14900
14924
  username: this.config.username,
14901
14925
  password: this.config.password
@@ -14909,6 +14933,65 @@ var MongoDbDriver = class {
14909
14933
  emit(event, ...args) {
14910
14934
  this.events.emit(event, ...args);
14911
14935
  }
14936
+ commandStarted(event) {
14937
+ if (this.ignoredCommand(event.commandName) || this.events.listenerCount("query") === 0) return;
14938
+ const started = performance.now();
14939
+ const { lsid: _lsid, $clusterTime: _clusterTime, $db: _database, ...pipeline } = event.command;
14940
+ const collection = event.command[event.commandName];
14941
+ this.monitoredCommands.set(event.requestId, {
14942
+ connection: this.name,
14943
+ driver: "mongodb",
14944
+ command: event.commandName,
14945
+ collection: typeof collection === "string" ? collection : void 0,
14946
+ pipeline,
14947
+ started,
14948
+ startedAt: performance.timeOrigin + started
14949
+ });
14950
+ }
14951
+ commandSucceeded(event) {
14952
+ const command = this.monitoredCommands.get(event.requestId);
14953
+ if (!command) return;
14954
+ this.monitoredCommands.delete(event.requestId);
14955
+ const { started, ...query } = command;
14956
+ const rowCount = event.reply?.n ?? event.reply?.cursor?.firstBatch?.length;
14957
+ this.emitQueryEvent({
14958
+ ...query,
14959
+ durationMs: performance.now() - started,
14960
+ ...typeof rowCount === "number" ? { rowCount } : {}
14961
+ });
14962
+ }
14963
+ commandFailed(event) {
14964
+ const command = this.monitoredCommands.get(event.requestId);
14965
+ if (!command) return;
14966
+ this.monitoredCommands.delete(event.requestId);
14967
+ const { started, ...query } = command;
14968
+ this.emitQueryEvent({
14969
+ ...query,
14970
+ durationMs: performance.now() - started,
14971
+ error: event.failure
14972
+ });
14973
+ }
14974
+ /**
14975
+ * Emit a `query` event. It runs inside the MongoDB client's own monitoring
14976
+ * callback, and an observer must never break the client, so a throwing
14977
+ * listener is swallowed here.
14978
+ */
14979
+ emitQueryEvent(event) {
14980
+ try {
14981
+ this.emit("query", event);
14982
+ } catch {}
14983
+ }
14984
+ ignoredCommand(command) {
14985
+ return [
14986
+ "isMaster",
14987
+ "hello",
14988
+ "ping",
14989
+ "saslStart",
14990
+ "saslContinue",
14991
+ "endSessions",
14992
+ "buildInfo"
14993
+ ].includes(command);
14994
+ }
14912
14995
  /**
14913
14996
  * Ensure MongoDB is running as a replica set (required for transactions).
14914
14997
  *
@@ -17314,12 +17397,11 @@ var PostgresQueryParser = class PostgresQueryParser {
17314
17397
  const columns = Array.isArray(fields) ? fields : [fields];
17315
17398
  this.groupColumns.push(...columns);
17316
17399
  }
17317
- /**
17318
- * Process a raw GROUP BY expression (emitted verbatim, no quoting). Used by
17319
- * `groupByDate` to add a `date_trunc(...)` bucket to the GROUP BY clause.
17320
- */
17400
+ /** Process a raw GROUP BY expression, binding `?` placeholders positionally. */
17321
17401
  processGroupByRaw(data) {
17322
- const expression = data.expression;
17402
+ let expression = data.expression;
17403
+ const bindings = data.bindings ?? [];
17404
+ for (const binding of bindings) expression = expression.replace("?", this.addParam(binding));
17323
17405
  this.groupRawExpressions.push(expression);
17324
17406
  }
17325
17407
  /**
@@ -19322,6 +19404,13 @@ var PostgresDriver = class {
19322
19404
  if (!this._eventListeners.has(event)) this._eventListeners.set(event, /* @__PURE__ */ new Set());
19323
19405
  this._eventListeners.get(event).add(listener);
19324
19406
  }
19407
+ /** Remove a previously registered driver event listener. */
19408
+ off(event, listener) {
19409
+ const listeners = this._eventListeners.get(event);
19410
+ if (!listeners) return;
19411
+ listeners.delete(listener);
19412
+ if (listeners.size === 0) this._eventListeners.delete(event);
19413
+ }
19325
19414
  /**
19326
19415
  * Serialize data for storage in PostgreSQL.
19327
19416
  *
@@ -19814,14 +19903,22 @@ var PostgresDriver = class {
19814
19903
  * @returns Transaction contract with commit/rollback methods
19815
19904
  */
19816
19905
  async beginTransaction(options) {
19906
+ const settings = Object.entries(options?.settings ?? {});
19907
+ for (const [name] of settings) if (!/^[A-Za-z_][A-Za-z0-9_.]*$/.test(name)) throw new Error(`Invalid PostgreSQL transaction setting name: "${name}"`);
19817
19908
  const client = await this.pool.connect();
19909
+ let transactionStarted = false;
19818
19910
  try {
19819
19911
  let beginSql = "BEGIN";
19820
19912
  if (options?.isolationLevel) beginSql += ` ISOLATION LEVEL ${options.isolationLevel.toUpperCase()}`;
19821
19913
  if (options?.readOnly) beginSql += " READ ONLY";
19822
19914
  if (options?.deferrable) beginSql += " DEFERRABLE";
19823
- await client.query(beginSql);
19915
+ await this.executePostgresQuery(() => client.query(beginSql), beginSql);
19916
+ transactionStarted = true;
19917
+ 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)]);
19824
19918
  } catch (error) {
19919
+ if (transactionStarted) try {
19920
+ await this.executePostgresQuery(() => client.query("ROLLBACK"), "ROLLBACK");
19921
+ } catch {}
19825
19922
  client.release(error instanceof Error ? error : new Error(String(error)));
19826
19923
  throw error;
19827
19924
  }
@@ -19829,7 +19926,7 @@ var PostgresDriver = class {
19829
19926
  context: client,
19830
19927
  commit: async () => {
19831
19928
  try {
19832
- await client.query("COMMIT");
19929
+ await this.executePostgresQuery(() => client.query("COMMIT"), "COMMIT");
19833
19930
  } catch (error) {
19834
19931
  client.release(error instanceof Error ? error : new Error(String(error)));
19835
19932
  throw error;
@@ -19838,7 +19935,7 @@ var PostgresDriver = class {
19838
19935
  },
19839
19936
  rollback: async () => {
19840
19937
  try {
19841
- await client.query("ROLLBACK");
19938
+ await this.executePostgresQuery(() => client.query("ROLLBACK"), "ROLLBACK");
19842
19939
  } catch (error) {
19843
19940
  client.release(error instanceof Error ? error : new Error(String(error)));
19844
19941
  throw error;
@@ -20071,8 +20168,8 @@ var PostgresDriver = class {
20071
20168
  paramCount: params.length
20072
20169
  }
20073
20170
  });
20074
- if (txClient) result = await txClient.query(sql, params);
20075
- else result = await this.pool.query(sql, params);
20171
+ if (txClient) result = await this.executePostgresQuery(() => txClient.query(sql, params), sql, params);
20172
+ else result = await this.executePostgresQuery(() => this.pool.query(sql, params), sql, params);
20076
20173
  if (this.config.logging) {
20077
20174
  const duration = (performance.now() - startTime).toFixed(2);
20078
20175
  _warlock_js_logger.log.success({
@@ -20105,6 +20202,47 @@ var PostgresDriver = class {
20105
20202
  throw error;
20106
20203
  }
20107
20204
  }
20205
+ /** Execute SQL and emit its public query event when the event is observed. */
20206
+ async executePostgresQuery(execute, sql, bindings = []) {
20207
+ if (!this._eventListeners.get("query")?.size) return execute();
20208
+ const started = performance.now();
20209
+ const startedAt = performance.timeOrigin + started;
20210
+ let result;
20211
+ try {
20212
+ result = await execute();
20213
+ } catch (error) {
20214
+ this.emitQueryEvent({
20215
+ connection: this.name,
20216
+ driver: "postgres",
20217
+ sql,
20218
+ bindings,
20219
+ durationMs: performance.now() - started,
20220
+ startedAt,
20221
+ error
20222
+ });
20223
+ throw error;
20224
+ }
20225
+ const event = {
20226
+ connection: this.name,
20227
+ driver: "postgres",
20228
+ sql,
20229
+ bindings,
20230
+ durationMs: performance.now() - started,
20231
+ startedAt
20232
+ };
20233
+ if (typeof result.rowCount === "number") event.rowCount = result.rowCount;
20234
+ this.emitQueryEvent(event);
20235
+ return result;
20236
+ }
20237
+ /**
20238
+ * Emit a `query` event. An observer is never allowed to fail the query it
20239
+ * observes, so a throwing listener is swallowed here.
20240
+ */
20241
+ emitQueryEvent(event) {
20242
+ try {
20243
+ this.emit("query", event);
20244
+ } catch {}
20245
+ }
20108
20246
  /**
20109
20247
  * Emit an event to all registered listeners.
20110
20248
  *
@@ -25312,6 +25450,7 @@ exports.UndefinedWhereValueError = UndefinedWhereValueError;
25312
25450
  exports.UnsafeFilterError = UnsafeFilterError;
25313
25451
  exports.UnsafeRawExpressionError = UnsafeRawExpressionError;
25314
25452
  exports.UnsupportedLeanOperationError = UnsupportedLeanOperationError;
25453
+ exports.UnsupportedLockForUpdateError = UnsupportedLockForUpdateError;
25315
25454
  exports.UnsupportedQueryOperationError = UnsupportedQueryOperationError;
25316
25455
  exports.UnsupportedUpdateOperationError = UnsupportedUpdateOperationError;
25317
25456
  exports.afterCommit = afterCommit;