@warlock.js/cascade 5.20.0 → 5.22.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 (172) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/cjs/index.cjs +2460 -1049
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/contracts/query-builder.contract.d.mts +30 -3
  5. package/esm/contracts/query-builder.contract.d.mts.map +1 -1
  6. package/esm/data-source/data-source-registry.d.mts +33 -1
  7. package/esm/data-source/data-source-registry.d.mts.map +1 -1
  8. package/esm/data-source/data-source-registry.mjs +75 -5
  9. package/esm/data-source/data-source-registry.mjs.map +1 -1
  10. package/esm/database-dirty-tracker.d.mts +5 -0
  11. package/esm/database-dirty-tracker.d.mts.map +1 -1
  12. package/esm/database-dirty-tracker.mjs +16 -0
  13. package/esm/database-dirty-tracker.mjs.map +1 -1
  14. package/esm/drivers/mongodb/mongodb-driver.d.mts +12 -0
  15. package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
  16. package/esm/drivers/mongodb/mongodb-driver.mjs +47 -16
  17. package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
  18. package/esm/drivers/mongodb/mongodb-migration-driver.d.mts +1 -1
  19. package/esm/drivers/mongodb/mongodb-migration-driver.d.mts.map +1 -1
  20. package/esm/drivers/mongodb/mongodb-migration-driver.mjs +15 -18
  21. package/esm/drivers/mongodb/mongodb-migration-driver.mjs.map +1 -1
  22. package/esm/drivers/mongodb/mongodb-query-builder.d.mts +13 -3
  23. package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
  24. package/esm/drivers/mongodb/mongodb-query-builder.mjs +72 -19
  25. package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
  26. package/esm/drivers/mongodb/mongodb-query-parser.d.mts +6 -0
  27. package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
  28. package/esm/drivers/mongodb/mongodb-query-parser.mjs +128 -30
  29. package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
  30. package/esm/drivers/postgres/index.d.mts +2 -3
  31. package/esm/drivers/postgres/index.mjs +1 -2
  32. package/esm/drivers/postgres/postgres-blueprint.mjs +4 -4
  33. package/esm/drivers/postgres/postgres-blueprint.mjs.map +1 -1
  34. package/esm/drivers/postgres/postgres-dialect.d.mts +5 -1
  35. package/esm/drivers/postgres/postgres-dialect.d.mts.map +1 -1
  36. package/esm/drivers/postgres/postgres-dialect.mjs +38 -8
  37. package/esm/drivers/postgres/postgres-dialect.mjs.map +1 -1
  38. package/esm/drivers/postgres/postgres-driver.d.mts +52 -29
  39. package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
  40. package/esm/drivers/postgres/postgres-driver.mjs +193 -57
  41. package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
  42. package/esm/drivers/postgres/postgres-migration-driver.d.mts +2 -1
  43. package/esm/drivers/postgres/postgres-migration-driver.d.mts.map +1 -1
  44. package/esm/drivers/postgres/postgres-migration-driver.mjs +13 -6
  45. package/esm/drivers/postgres/postgres-migration-driver.mjs.map +1 -1
  46. package/esm/drivers/postgres/postgres-query-builder.d.mts +18 -7
  47. package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
  48. package/esm/drivers/postgres/postgres-query-builder.mjs +146 -31
  49. package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
  50. package/esm/drivers/postgres/postgres-query-parser.d.mts +23 -1
  51. package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
  52. package/esm/drivers/postgres/postgres-query-parser.mjs +160 -43
  53. package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
  54. package/esm/drivers/postgres/postgres-sql-serializer.mjs +3 -2
  55. package/esm/drivers/postgres/postgres-sql-serializer.mjs.map +1 -1
  56. package/esm/drivers/postgres/postgres-ttl-purge.mjs +98 -0
  57. package/esm/drivers/postgres/postgres-ttl-purge.mjs.map +1 -0
  58. package/esm/drivers/postgres/postgres-type-converters.mjs +57 -0
  59. package/esm/drivers/postgres/postgres-type-converters.mjs.map +1 -0
  60. package/esm/events/model-events.d.mts +1 -1
  61. package/esm/events/model-events.d.mts.map +1 -1
  62. package/esm/events/model-events.mjs.map +1 -1
  63. package/esm/index.d.mts +5 -6
  64. package/esm/index.mjs +4 -5
  65. package/esm/migration/column-builder.d.mts.map +1 -1
  66. package/esm/migration/column-builder.mjs +2 -2
  67. package/esm/migration/column-builder.mjs.map +1 -1
  68. package/esm/migration/index.d.mts +2 -2
  69. package/esm/migration/index.mjs +1 -1
  70. package/esm/migration/migration-order.mjs +5 -2
  71. package/esm/migration/migration-order.mjs.map +1 -1
  72. package/esm/migration/migration-runner.d.mts +50 -5
  73. package/esm/migration/migration-runner.d.mts.map +1 -1
  74. package/esm/migration/migration-runner.mjs +369 -146
  75. package/esm/migration/migration-runner.mjs.map +1 -1
  76. package/esm/migration/migration.d.mts +36 -3
  77. package/esm/migration/migration.d.mts.map +1 -1
  78. package/esm/migration/migration.mjs +62 -5
  79. package/esm/migration/migration.mjs.map +1 -1
  80. package/esm/model/methods/accessor-methods.mjs +1 -3
  81. package/esm/model/methods/accessor-methods.mjs.map +1 -1
  82. package/esm/model/methods/delete-methods.mjs +6 -2
  83. package/esm/model/methods/delete-methods.mjs.map +1 -1
  84. package/esm/model/methods/hydration-methods.mjs +16 -2
  85. package/esm/model/methods/hydration-methods.mjs.map +1 -1
  86. package/esm/model/methods/instance-event-methods.mjs +2 -1
  87. package/esm/model/methods/instance-event-methods.mjs.map +1 -1
  88. package/esm/model/methods/meta-methods.mjs +53 -16
  89. package/esm/model/methods/meta-methods.mjs.map +1 -1
  90. package/esm/model/methods/query-methods.mjs +86 -15
  91. package/esm/model/methods/query-methods.mjs.map +1 -1
  92. package/esm/model/methods/static-event-methods.mjs +16 -1
  93. package/esm/model/methods/static-event-methods.mjs.map +1 -1
  94. package/esm/model/methods/write-methods.d.mts.map +1 -1
  95. package/esm/model/methods/write-methods.mjs +50 -50
  96. package/esm/model/methods/write-methods.mjs.map +1 -1
  97. package/esm/model/model.d.mts +16 -1
  98. package/esm/model/model.d.mts.map +1 -1
  99. package/esm/model/model.mjs +39 -5
  100. package/esm/model/model.mjs.map +1 -1
  101. package/esm/model/register-model.d.mts +6 -1
  102. package/esm/model/register-model.d.mts.map +1 -1
  103. package/esm/model/register-model.mjs +16 -1
  104. package/esm/model/register-model.mjs.map +1 -1
  105. package/esm/query-builder/query-builder.d.mts +21 -2
  106. package/esm/query-builder/query-builder.d.mts.map +1 -1
  107. package/esm/query-builder/query-builder.mjs +97 -20
  108. package/esm/query-builder/query-builder.mjs.map +1 -1
  109. package/esm/relations/key-conventions.mjs +13 -3
  110. package/esm/relations/key-conventions.mjs.map +1 -1
  111. package/esm/relations/pivot-operations.mjs +1 -1
  112. package/esm/relations/relation-loader.d.mts +15 -5
  113. package/esm/relations/relation-loader.d.mts.map +1 -1
  114. package/esm/relations/relation-loader.mjs +51 -16
  115. package/esm/relations/relation-loader.mjs.map +1 -1
  116. package/esm/remover/database-remover.d.mts.map +1 -1
  117. package/esm/remover/database-remover.mjs +22 -8
  118. package/esm/remover/database-remover.mjs.map +1 -1
  119. package/esm/restorer/database-restorer.d.mts +11 -2
  120. package/esm/restorer/database-restorer.d.mts.map +1 -1
  121. package/esm/restorer/database-restorer.mjs +45 -13
  122. package/esm/restorer/database-restorer.mjs.map +1 -1
  123. package/esm/sync/model-events.mjs +63 -3
  124. package/esm/sync/model-events.mjs.map +1 -1
  125. package/esm/sync/model-sync-operation.d.mts.map +1 -1
  126. package/esm/sync/model-sync-operation.mjs +3 -2
  127. package/esm/sync/model-sync-operation.mjs.map +1 -1
  128. package/esm/sync/sync-fanout.mjs +67 -0
  129. package/esm/sync/sync-fanout.mjs.map +1 -0
  130. package/esm/sync/sync-manager.d.mts +3 -42
  131. package/esm/sync/sync-manager.d.mts.map +1 -1
  132. package/esm/sync/sync-manager.mjs +37 -126
  133. package/esm/sync/sync-manager.mjs.map +1 -1
  134. package/esm/utils/connect-to-database.d.mts.map +1 -1
  135. package/esm/utils/connect-to-database.mjs +4 -4
  136. package/esm/utils/connect-to-database.mjs.map +1 -1
  137. package/esm/utils/database-writer.utils.d.mts +1 -1
  138. package/esm/utils/database-writer.utils.d.mts.map +1 -1
  139. package/esm/utils/database-writer.utils.mjs +1 -0
  140. package/esm/utils/database-writer.utils.mjs.map +1 -1
  141. package/esm/utils/define-model.d.mts.map +1 -1
  142. package/esm/utils/define-model.mjs +2 -10
  143. package/esm/utils/define-model.mjs.map +1 -1
  144. package/esm/utils/is-valid-date-value.mjs +3 -2
  145. package/esm/utils/is-valid-date-value.mjs.map +1 -1
  146. package/esm/utils/once-connected.d.mts.map +1 -1
  147. package/esm/utils/once-connected.mjs +12 -2
  148. package/esm/utils/once-connected.mjs.map +1 -1
  149. package/esm/validation/mutators/embed-mutator.mjs +19 -11
  150. package/esm/validation/mutators/embed-mutator.mjs.map +1 -1
  151. package/esm/validation/plugins/embed-validator-plugin.d.mts +2 -2
  152. package/esm/validation/plugins/embed-validator-plugin.d.mts.map +1 -1
  153. package/esm/validation/plugins/embed-validator-plugin.mjs +8 -2
  154. package/esm/validation/plugins/embed-validator-plugin.mjs.map +1 -1
  155. package/esm/validation/rules/database-model-rule.mjs +6 -5
  156. package/esm/validation/rules/database-model-rule.mjs.map +1 -1
  157. package/esm/validation/rules/exists-rule.mjs +6 -4
  158. package/esm/validation/rules/exists-rule.mjs.map +1 -1
  159. package/esm/validation/rules/unique-rule.mjs +5 -2
  160. package/esm/validation/rules/unique-rule.mjs.map +1 -1
  161. package/esm/validation/validators/embed-validator.d.mts.map +1 -1
  162. package/esm/validation/validators/embed-validator.mjs +1 -1
  163. package/esm/validation/validators/embed-validator.mjs.map +1 -1
  164. package/esm/writer/database-writer.d.mts +27 -0
  165. package/esm/writer/database-writer.d.mts.map +1 -1
  166. package/esm/writer/database-writer.mjs +85 -18
  167. package/esm/writer/database-writer.mjs.map +1 -1
  168. package/package.json +8 -7
  169. package/esm/drivers/postgres/postgres-sync-adapter.d.mts +0 -79
  170. package/esm/drivers/postgres/postgres-sync-adapter.d.mts.map +0 -1
  171. package/esm/drivers/postgres/postgres-sync-adapter.mjs +0 -162
  172. package/esm/drivers/postgres/postgres-sync-adapter.mjs.map +0 -1
@@ -1,9 +1,10 @@
1
- import { getModelUpdatedEvent } from "../sync/model-events.mjs";
1
+ import { afterCommit } from "../transactions/after-commit.mjs";
2
+ import { triggerModelEvent } from "../sync/model-events.mjs";
2
3
  import { mergeDriverFields } from "../model/methods/accessor-methods.mjs";
4
+ import { enqueueSyncFanout } from "../sync/sync-fanout.mjs";
3
5
  import { DatabaseWriterValidationError } from "../validation/database-writer-validation-error.mjs";
4
6
  import "../validation/index.mjs";
5
7
  import { when } from "@mongez/reinforcements";
6
- import events from "@mongez/events";
7
8
  import { getSealConfig, v } from "@warlock.js/seal";
8
9
 
9
10
  //#region ../cascade/src/writer/database-writer.ts
@@ -44,6 +45,8 @@ import { getSealConfig, v } from "@warlock.js/seal";
44
45
  var DatabaseWriter = class {
45
46
  /** The model instance being persisted */
46
47
  model;
48
+ /** Dirty columns an update validation dropped (strip mode); never written. */
49
+ unvalidatedColumns = /* @__PURE__ */ new Set();
47
50
  /** Model constructor reference */
48
51
  ctor;
49
52
  /** Data source containing driver and ID generator */
@@ -100,10 +103,12 @@ var DatabaseWriter = class {
100
103
  options,
101
104
  mode: isInsert ? "insert" : "update"
102
105
  });
103
- await this.validateAndCast(isInsert, options);
104
106
  let result;
105
107
  if (isInsert) result = await this.performInsert(options);
106
- else result = await this.performUpdate(options);
108
+ else {
109
+ await this.validateAndCast(false, options);
110
+ result = await this.performUpdate(options);
111
+ }
107
112
  const changedFields = isInsert ? [] : this.model.getDirtyColumns();
108
113
  this.model.dirtyTracker.reset();
109
114
  this.model.isNew = false;
@@ -111,7 +116,7 @@ var DatabaseWriter = class {
111
116
  await this.model.emitEvent("saved");
112
117
  await this.model.emitEvent(isInsert ? "created" : "updated");
113
118
  }
114
- if (!options.skipSync && !isInsert) this.triggerSync(changedFields);
119
+ if (!options.skipSync && !isInsert) afterCommit(() => this.triggerSync(changedFields));
115
120
  return {
116
121
  success: true,
117
122
  document: this.model.data,
@@ -143,16 +148,16 @@ var DatabaseWriter = class {
143
148
  ...when(this.ctor.updatedAtColumn, () => ({ [this.ctor.updatedAtColumn]: v.date().optional() })),
144
149
  ...when(this.ctor.deletedAtColumn, () => ({ [this.ctor.deletedAtColumn]: v.date().optional() }))
145
150
  };
146
- const validationSchema = isInsert ? this.schema.clone().extend(systemColumns) : this.schema.clone(Object.keys(this.model.data)).extend(systemColumns);
151
+ const dataToValidate = isInsert ? this.model.data : Object.fromEntries(this.model.getDirtyColumns().filter((column) => column in this.model.data).map((column) => [column, this.model.data[column]]));
152
+ const validationSchema = isInsert ? this.schema.clone().extend(systemColumns) : this.schema.clone(Object.keys(dataToValidate)).extend(systemColumns);
147
153
  if (this.strictMode === "strip") validationSchema.stripUnknown();
148
154
  else if (this.strictMode === "fail") validationSchema.allowUnknown(false);
149
155
  else if (this.strictMode === "allow") validationSchema.allowUnknown(true);
150
- const result = await v.validate(validationSchema, this.model.data, {
156
+ const result = await v.validate(validationSchema, dataToValidate, {
151
157
  context: { model: this.model },
152
158
  ...getSealConfig()
153
159
  });
154
160
  if (!result.isValid) {
155
- console.trace(result.errors);
156
161
  const error = new DatabaseWriterValidationError(`[${this.model.constructor.name} Model] ${isInsert ? "Insert" : "Update"} Validation failed`, result.errors);
157
162
  if (!options.skipEvents) await this.model.emitEvent("validated", {
158
163
  result,
@@ -160,10 +165,25 @@ var DatabaseWriter = class {
160
165
  });
161
166
  throw error;
162
167
  }
163
- this.model.replaceData(result.data);
168
+ if (isInsert) this.model.replaceData(result.data);
169
+ else {
170
+ const validated = result.data;
171
+ for (const column of Object.keys(dataToValidate)) if (column in validated) this.model.set(column, validated[column]);
172
+ else this.unvalidatedColumns.add(column);
173
+ }
164
174
  if (!options.skipEvents) await this.model.emitEvent("validated", { result });
165
175
  }
166
176
  /**
177
+ * Validate, cast and id-generate a not-yet-persisted model for `upsert()`.
178
+ *
179
+ * Runs the insert-mode pipeline (schema, casts, strictMode, Mongo `id`) but
180
+ * does not write; the caller sends the resulting data through the driver.
181
+ */
182
+ async prepareForUpsert() {
183
+ await this.validateAndCast(true, {});
184
+ await this.generateNextId();
185
+ }
186
+ /**
167
187
  * Perform an insert operation.
168
188
  *
169
189
  * @param options - Save options
@@ -171,6 +191,30 @@ var DatabaseWriter = class {
171
191
  * @private
172
192
  */
173
193
  async performInsert(options) {
194
+ const dataToInsert = await this.prepareForInsert(options);
195
+ let result;
196
+ try {
197
+ result = await this.driver.insert(this.table, dataToInsert);
198
+ } catch (error) {
199
+ throw this.mapUniqueViolation(error, true);
200
+ }
201
+ mergeDriverFields(this.model, result.document);
202
+ this.model.dirtyTracker.reset();
203
+ return result;
204
+ }
205
+ /**
206
+ * Run the insert pipeline (validation, casting, id generation, timestamps,
207
+ * `validating`/`validated`/`creating` hooks) WITHOUT writing.
208
+ *
209
+ * `save()` uses this for its insert path, so bulk inserts and single inserts
210
+ * cannot drift. The caller is responsible for sending the returned document
211
+ * to the driver.
212
+ *
213
+ * @param options - Save options (e.g. `skipEvents`, `skipValidation`)
214
+ * @returns The prepared document to insert
215
+ */
216
+ async prepareForInsert(options = {}) {
217
+ await this.validateAndCast(true, options);
174
218
  await this.generateNextId();
175
219
  const dataToInsert = this.model.data;
176
220
  const createdAtColumn = this.ctor.createdAtColumn;
@@ -178,10 +222,7 @@ var DatabaseWriter = class {
178
222
  const updatedAtColumn = this.ctor.updatedAtColumn;
179
223
  if (updatedAtColumn) dataToInsert[updatedAtColumn] = /* @__PURE__ */ new Date();
180
224
  if (!options.skipEvents) await this.model.emitEvent("creating");
181
- const result = await this.driver.insert(this.table, dataToInsert);
182
- mergeDriverFields(this.model, result.document);
183
- this.model.dirtyTracker.reset();
184
- return result;
225
+ return dataToInsert;
185
226
  }
186
227
  /**
187
228
  * Perform an update operation.
@@ -201,7 +242,29 @@ var DatabaseWriter = class {
201
242
  }
202
243
  const operations = this.buildUpdateOperations();
203
244
  if (Object.keys(operations).length === 0) return { modifiedCount: 0 };
204
- return await this.driver.update(this.table, this.buildPrimaryKeyFilter(), operations);
245
+ try {
246
+ return await this.driver.update(this.table, this.buildPrimaryKeyFilter(), operations);
247
+ } catch (error) {
248
+ throw this.mapUniqueViolation(error, false);
249
+ }
250
+ }
251
+ /**
252
+ * Map a driver unique-constraint violation (Postgres 23505, MongoDB 11000)
253
+ * to a field validation error, so a lost `unique` race never surfaces as a
254
+ * 500. Any other error is returned untouched.
255
+ */
256
+ mapUniqueViolation(error, isInsert) {
257
+ const err = error;
258
+ if (err?.code !== "23505" && err?.code !== 11e3 && err?.code !== "11000") return error;
259
+ let field;
260
+ if (err.keyPattern) field = Object.keys(err.keyPattern)[0];
261
+ else field = /Key \(([^)]+)\)=/.exec(err.detail ?? "")?.[1]?.split(",")[0]?.trim();
262
+ if (!field) field = /index: (\S+?)_-?1\b/.exec(err.message ?? "")?.[1];
263
+ return new DatabaseWriterValidationError(`[${this.model.constructor.name} Model] ${isInsert ? "Insert" : "Update"} Validation failed`, [{
264
+ type: "unique",
265
+ error: `The ${field ?? "value"} must be unique`,
266
+ input: field ?? "id"
267
+ }]);
205
268
  }
206
269
  /**
207
270
  * Build the filter that pins a write to the row this model was loaded from.
@@ -263,7 +326,7 @@ var DatabaseWriter = class {
263
326
  "id",
264
327
  "_id"
265
328
  ]);
266
- const dirtyColumns = this.model.getDirtyColumns().filter((column) => !identityColumns.has(column));
329
+ const dirtyColumns = this.model.getDirtyColumns().filter((column) => !identityColumns.has(column) && !this.unvalidatedColumns.has(column));
267
330
  if (dirtyColumns.length > 0) {
268
331
  operations.$set = {};
269
332
  for (const column of dirtyColumns) {
@@ -306,8 +369,8 @@ var DatabaseWriter = class {
306
369
  * @private
307
370
  */
308
371
  resolveIncrementBy() {
309
- if (this.ctor.incrementIdBy) return this.ctor.incrementIdBy;
310
372
  if (this.ctor.randomIncrement) return typeof this.ctor.randomIncrement === "function" ? this.ctor.randomIncrement() : this.randomInt(1, 10);
373
+ if (this.ctor.incrementIdBy) return this.ctor.incrementIdBy;
311
374
  return 1;
312
375
  }
313
376
  /**
@@ -330,8 +393,12 @@ var DatabaseWriter = class {
330
393
  * @param changedFields - Fields that were changed (for filtering)
331
394
  * @private
332
395
  */
333
- async triggerSync(changedFields) {
334
- await events.triggerAll(getModelUpdatedEvent(this.ctor), this.model, changedFields);
396
+ triggerSync(changedFields) {
397
+ enqueueSyncFanout({
398
+ sourceModel: this.ctor.name,
399
+ operation: "update",
400
+ execute: () => triggerModelEvent(this.ctor, "updated", this.model, changedFields)
401
+ });
335
402
  }
336
403
  };
337
404
 
@@ -1 +1 @@
1
- {"version":3,"file":"database-writer.mjs","names":[],"sources":["../../../../../../../cascade/src/writer/database-writer.ts"],"sourcesContent":["import events from \"@mongez/events\";\r\nimport { when } from \"@mongez/reinforcements\";\r\nimport { getSealConfig, v, type ObjectValidator } from \"@warlock.js/seal\";\r\nimport type {\r\n DriverContract,\r\n InsertResult,\r\n UpdateOperations,\r\n UpdateResult,\r\n} from \"../contracts/database-driver.contract\";\r\nimport type {\r\n WriterContract,\r\n WriterOptions,\r\n WriterResult,\r\n} from \"../contracts/database-writer.contract\";\r\nimport { mergeDriverFields } from \"../model/methods/accessor-methods\";\r\nimport type { ChildModel, Model } from \"../model/model\";\r\nimport { getModelUpdatedEvent } from \"../sync/model-events\";\r\nimport type { StrictMode } from \"../types\";\r\nimport { DatabaseWriterValidationError } from \"../validation\";\r\nimport type { DataSource } from \"./../data-source/data-source\";\r\n\r\n/**\r\n * Database writer service that orchestrates model persistence.\r\n *\r\n * Handles the complete save pipeline:\r\n * 1. Check for changes (skip if no changes and not new)\r\n * 2. Emit `saving` event (for data enrichment)\r\n * 3. Emit `validating` event\r\n * 4. Validate and cast data via @warlock.js/seal schema\r\n * 5. Emit `validated` event\r\n * 6. Generate ID (for new NoSQL records)\r\n * 7. Emit `creating`/`updating` events\r\n * 8. Execute insert or update via driver\r\n * 9. Merge returned data into model\r\n * 10. Reset dirty tracker and update `isNew` flag\r\n * 11. Emit `saved` and `created`/`updated` events\r\n *\r\n * @example\r\n * ```typescript\r\n * const user = new User({ name: \"Alice\", email: \"alice@example.com\" });\r\n * const writer = new DatabaseWriter(user);\r\n * await writer.save();\r\n *\r\n * console.log(user.get(\"id\")); // 1 (auto-generated)\r\n * console.log(user.get(\"_id\")); // ObjectId(\"...\")\r\n *\r\n * // Update existing record\r\n * user.set(\"name\", \"Alice Smith\");\r\n * await writer.save();\r\n * // Only updates the \"name\" field (partial update)\r\n *\r\n * // Silent save (no events)\r\n * await writer.save({ skipEvents: true });\r\n * ```\r\n */\r\nexport class DatabaseWriter implements WriterContract {\r\n /** The model instance being persisted */\r\n private readonly model: Model;\r\n\r\n /** Model constructor reference */\r\n private readonly ctor: ChildModel<Model>;\r\n\r\n /** Data source containing driver and ID generator */\r\n private readonly dataSource: DataSource;\r\n\r\n /** Database driver for executing queries */\r\n private readonly driver: DriverContract;\r\n\r\n /** Table/collection name */\r\n private readonly table: string;\r\n\r\n /** Primary key field name */\r\n private readonly primaryKey: string;\r\n\r\n /** Validation schema (if defined) */\r\n private readonly schema?: ObjectValidator;\r\n\r\n /** Strict mode configuration */\r\n private readonly strictMode: StrictMode;\r\n\r\n /**\r\n * Create a new writer instance for a model.\r\n *\r\n * @param model - The model instance to persist\r\n *\r\n * @example\r\n * ```typescript\r\n * const user = new User({ name: \"Alice\" });\r\n * const writer = new DatabaseWriter(user);\r\n * await writer.save();\r\n * ```\r\n */\r\n public constructor(model: Model) {\r\n this.model = model;\r\n this.ctor = model.constructor as ChildModel<Model>;\r\n this.dataSource = this.ctor.getDataSource();\r\n this.driver = this.dataSource.driver;\r\n this.table = this.ctor.table;\r\n this.primaryKey = this.ctor.primaryKey;\r\n this.schema = this.ctor.schema;\r\n this.strictMode = this.ctor.strictMode;\r\n }\r\n\r\n /**\r\n * Save the model instance to the database.\r\n *\r\n * @param options - Save options\r\n * @returns Result with success status, document, and metadata\r\n * @throws {ValidationError} If validation fails\r\n */\r\n public async save(options: WriterOptions = {}): Promise<WriterResult> {\r\n const isInsert = this.model.isNew;\r\n\r\n // 1. Check if model has changes (skip if no changes and not new)\r\n if (!isInsert && !this.model.hasChanges()) {\r\n return {\r\n success: true,\r\n document: this.model.data,\r\n isNew: false,\r\n modifiedCount: 0,\r\n };\r\n }\r\n\r\n // 2. Emit saving event (before validation for data enrichment)\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"saving\", {\r\n isInsert,\r\n options,\r\n mode: isInsert ? \"insert\" : \"update\",\r\n });\r\n }\r\n\r\n // 3. Validate and cast data\r\n await this.validateAndCast(isInsert, options);\r\n\r\n // 4. Execute insert or update\r\n let result: InsertResult | UpdateResult;\r\n\r\n if (isInsert) {\r\n result = await this.performInsert(options);\r\n } else {\r\n result = await this.performUpdate(options);\r\n }\r\n\r\n // 5. Reset dirty tracker and update isNew flag\r\n const changedFields = isInsert ? [] : this.model.getDirtyColumns();\r\n this.model.dirtyTracker.reset();\r\n this.model.isNew = false;\r\n\r\n // 6. Emit post-save events\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"saved\");\r\n await this.model.emitEvent(isInsert ? \"created\" : \"updated\");\r\n }\r\n\r\n // 7. Trigger sync operations (fire-and-forget, non-blocking)\r\n if (!options.skipSync && !isInsert) {\r\n void this.triggerSync(changedFields);\r\n }\r\n\r\n return {\r\n success: true,\r\n document: this.model.data,\r\n isNew: isInsert,\r\n modifiedCount: isInsert\r\n ? undefined\r\n : (result as UpdateResult).modifiedCount,\r\n };\r\n }\r\n\r\n /**\r\n * Validate and cast model data using the schema.\r\n *\r\n * Updates the model's data in-place with validated/casted values.\r\n *\r\n * @param isInsert - Whether this is an insert operation\r\n * @param options - Save options\r\n * @throws {ValidationError} If validation fails\r\n * @private\r\n */\r\n private async validateAndCast(\r\n isInsert: boolean,\r\n options: WriterOptions,\r\n ): Promise<void> {\r\n // Emit validating event\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"validating\", {\r\n isInsert,\r\n options,\r\n mode: isInsert ? \"insert\" : \"update\",\r\n });\r\n }\r\n\r\n // Skip validation if requested or no schema defined\r\n if (options.skipValidation || !this.schema) {\r\n return;\r\n }\r\n\r\n // Whitelist the framework-managed system columns so a model carrying them\r\n // (id / _id / timestamps / soft-delete deletedAt) validates cleanly instead\r\n // of being stripped (strictMode \"strip\") or rejected (strictMode \"fail\").\r\n // `when(...)` adds each timestamp/soft-delete column only when configured\r\n // (truthy) — the lazy factory means a disabled column (`false`) never even\r\n // builds a bogus schema key.\r\n //\r\n // The whitelist must apply to BOTH insert and update: on insert a caller may\r\n // supply a backdated createdAt (e.g. data migrations / imports), and without\r\n // the whitelist strictMode \"strip\" silently drops it before it reaches the\r\n // writer, while \"fail\" rejects the whole insert.\r\n const systemColumns = {\r\n id: v.scalar().optional(),\r\n _id: v.any().optional(),\r\n ...when(this.ctor.createdAtColumn, () => ({\r\n [this.ctor.createdAtColumn as string]: v.date().optional(),\r\n })),\r\n ...when(this.ctor.updatedAtColumn, () => ({\r\n [this.ctor.updatedAtColumn as string]: v.date().optional(),\r\n })),\r\n ...when(this.ctor.deletedAtColumn, () => ({\r\n [this.ctor.deletedAtColumn as string]: v.date().optional(),\r\n })),\r\n };\r\n\r\n // Clone full schema for insert, partial (dirty keys only) for updates.\r\n const validationSchema = isInsert\r\n ? this.schema.clone().extend(systemColumns)\r\n : this.schema.clone(Object.keys(this.model.data)).extend(systemColumns);\r\n\r\n // Apply strict mode\r\n if (this.strictMode === \"strip\") {\r\n validationSchema.stripUnknown();\r\n } else if (this.strictMode === \"fail\") {\r\n validationSchema.allowUnknown(false);\r\n } else if (this.strictMode === \"allow\") {\r\n validationSchema.allowUnknown(true);\r\n }\r\n\r\n // Run validation\r\n const result = await v.validate(validationSchema, this.model.data, {\r\n context: {\r\n model: this.model,\r\n },\r\n ...getSealConfig(),\r\n });\r\n\r\n if (!result.isValid) {\r\n console.trace(result.errors);\r\n\r\n const error = new DatabaseWriterValidationError(\r\n `[${this.model.constructor.name} Model] ${isInsert ? \"Insert\" : \"Update\"} Validation failed`,\r\n result.errors,\r\n );\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"validated\", { result, error });\r\n }\r\n throw error;\r\n }\r\n\r\n // Update model data with validated/casted data\r\n this.model.replaceData(result.data);\r\n\r\n // Emit validated event\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"validated\", { result });\r\n }\r\n }\r\n\r\n /**\r\n * Perform an insert operation.\r\n *\r\n * @param options - Save options\r\n * @returns Insert result\r\n * @private\r\n */\r\n private async performInsert(options: WriterOptions): Promise<InsertResult> {\r\n // Generate ID if needed (NoSQL only)\r\n await this.generateNextId();\r\n\r\n // Get data to insert (already validated and casted)\r\n const dataToInsert = this.model.data;\r\n\r\n // Add createdAt and updatedAt to the data (using resolved column names)\r\n // The column names are already resolved through the hierarchy:\r\n // Model static property > Database config > Driver defaults > undefined\r\n //\r\n // Only stamp createdAt when the caller did NOT supply one, so a backdated\r\n // value (e.g. data migrations / imports) is honored instead of overwritten.\r\n // This mirrors the upsert path's guard. updatedAt is always stamped to\r\n // reflect the moment the record is persisted.\r\n const createdAtColumn = this.ctor.createdAtColumn;\r\n\r\n if (createdAtColumn && dataToInsert[createdAtColumn] == null) {\r\n dataToInsert[createdAtColumn] = new Date();\r\n }\r\n\r\n const updatedAtColumn = this.ctor.updatedAtColumn;\r\n if (updatedAtColumn) {\r\n dataToInsert[updatedAtColumn] = new Date();\r\n }\r\n\r\n // Emit creating event\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"creating\");\r\n }\r\n\r\n // INSERT: use full validated data\r\n const result = await this.driver.insert(this.table, dataToInsert);\r\n\r\n // Merge returned data (e.g., generated _id, timestamps)\r\n // Note: We use merge here because the result might not include all fields\r\n // (e.g., our generated 'id' field), and we don't want to lose them\r\n mergeDriverFields(this.model, result.document as Record<string, unknown>);\r\n\r\n // Reset dirty tracker immediately after merge to prevent\r\n // database-generated fields (like _id) from being marked as dirty\r\n this.model.dirtyTracker.reset();\r\n\r\n return result;\r\n }\r\n\r\n /**\r\n * Perform an update operation.\r\n *\r\n * @param options - Save options\r\n * @returns Update result\r\n * @private\r\n */\r\n private async performUpdate(options: WriterOptions): Promise<UpdateResult> {\r\n // Emit updating event\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"updating\");\r\n }\r\n\r\n // Update the updatedAt timestamp (using resolved column name)\r\n const updatedAtColumn = this.ctor.updatedAtColumn;\r\n if (updatedAtColumn) {\r\n this.model.set(updatedAtColumn, new Date());\r\n }\r\n\r\n if (options.replace) {\r\n const document = await this.driver.replace(\r\n this.table,\r\n this.buildPrimaryKeyFilter(),\r\n this.model.data,\r\n );\r\n\r\n if (document) {\r\n this.model.replaceData(document as Record<string, unknown>);\r\n }\r\n\r\n return { modifiedCount: document ? 1 : 0 };\r\n }\r\n\r\n // Build operations from dirty tracker\r\n const operations = this.buildUpdateOperations();\r\n\r\n // Nothing left to write once identity columns are excluded — don't hand the\r\n // driver an empty update document (MongoDB rejects one).\r\n if (Object.keys(operations).length === 0) {\r\n return { modifiedCount: 0 };\r\n }\r\n\r\n // Execute update with operations\r\n return await this.driver.update(\r\n this.table,\r\n this.buildPrimaryKeyFilter(),\r\n operations,\r\n );\r\n }\r\n\r\n /**\r\n * Build the filter that pins a write to the row this model was loaded from.\r\n *\r\n * It reads `model.trustedPrimaryKey` — the value captured when the instance\r\n * became persisted — NOT the current value in `model.data`. The current value\r\n * is reachable by mass assignment (`model.merge(req.body)`), so deriving the\r\n * filter from it let a request body redirect the UPDATE to another document.\r\n *\r\n * @returns Filter matching the originally loaded record\r\n * @private\r\n */\r\n private buildPrimaryKeyFilter(): Record<string, unknown> {\r\n return { [this.primaryKey]: this.model.trustedPrimaryKey };\r\n }\r\n\r\n /**\r\n * Generate ID for the model if auto-generation is enabled.\r\n *\r\n * @private\r\n */\r\n public async generateNextId(): Promise<void> {\r\n if (!this.ctor.autoGenerateId || this.model.get(\"id\")) {\r\n return;\r\n }\r\n\r\n const idGenerator = this.dataSource.idGenerator;\r\n if (!idGenerator) {\r\n return;\r\n }\r\n\r\n // Resolve ID generation options from model configuration\r\n const initialId = this.resolveInitialId();\r\n\r\n const incrementIdBy = this.resolveIncrementBy();\r\n\r\n const id = await idGenerator.generateNextId({\r\n table: this.table,\r\n initialId,\r\n incrementIdBy,\r\n });\r\n\r\n this.model.set(\"id\", id);\r\n }\r\n\r\n /**\r\n * Build update operations from the model's dirty tracker.\r\n *\r\n * Handles both modified fields ($set) and removed fields ($unset).\r\n *\r\n * @returns Update operations for the driver\r\n * @private\r\n *\r\n * @example\r\n * ```typescript\r\n * // Model with changes\r\n * user.set(\"name\", \"Alice\");\r\n * user.unset(\"tempField\");\r\n *\r\n * const operations = this.buildUpdateOperations();\r\n * // {\r\n * // $set: { name: \"Alice\" },\r\n * // $unset: { tempField: 1 }\r\n * // }\r\n * ```\r\n */\r\n private buildUpdateOperations(): UpdateOperations {\r\n const operations: UpdateOperations = {};\r\n\r\n // Identity columns are never written by an update. The filter pins the row\r\n // by its captured primary key, so a dirty `id`/`_id` can only come from\r\n // mass assignment or an explicit set() — either way, rewriting the key of\r\n // an existing row corrupts identity (and `_id` is immutable in MongoDB).\r\n // Changing a primary key is a deliberate operation: use the atomic/raw APIs.\r\n const identityColumns = new Set([this.primaryKey, \"id\", \"_id\"]);\r\n\r\n // Get dirty columns (modified fields)\r\n const dirtyColumns = this.model\r\n .getDirtyColumns()\r\n .filter(column => !identityColumns.has(column));\r\n\r\n if (dirtyColumns.length > 0) {\r\n operations.$set = {};\r\n for (const column of dirtyColumns) {\r\n const value = this.model.get(column);\r\n if (value === undefined) continue;\r\n\r\n operations.$set[column] = this.model.get(column);\r\n }\r\n }\r\n\r\n // Get removed columns\r\n const removedColumns = this.model\r\n .getRemovedColumns()\r\n .filter(column => !identityColumns.has(column));\r\n\r\n if (removedColumns.length > 0) {\r\n operations.$unset = {};\r\n for (const column of removedColumns) {\r\n operations.$unset[column] = 1;\r\n }\r\n }\r\n\r\n return operations;\r\n }\r\n\r\n /**\r\n * Resolve the initial ID from model configuration.\r\n *\r\n * Priority:\r\n * 1. Model.initialId (explicit value)\r\n * 2. Model.randomInitialId (random or function)\r\n * 3. Default: 1\r\n *\r\n * @returns The initial ID value\r\n * @private\r\n */\r\n private resolveInitialId(): number {\r\n if (this.ctor.initialId) {\r\n return this.ctor.initialId;\r\n }\r\n\r\n if (this.ctor.randomInitialId) {\r\n return typeof this.ctor.randomInitialId === \"function\"\r\n ? this.ctor.randomInitialId()\r\n : this.randomInt(10000, 499999);\r\n }\r\n\r\n return 1; // Default initial ID\r\n }\r\n\r\n /**\r\n * Resolve the increment value from model configuration.\r\n *\r\n * Priority:\r\n * 1. Model.incrementIdBy (explicit value)\r\n * 2. Model.randomIncrement (random or function)\r\n * 3. Default: 1\r\n *\r\n * @returns The increment value\r\n * @private\r\n */\r\n private resolveIncrementBy(): number {\r\n if (this.ctor.incrementIdBy) {\r\n return this.ctor.incrementIdBy;\r\n }\r\n\r\n if (this.ctor.randomIncrement) {\r\n return typeof this.ctor.randomIncrement === \"function\"\r\n ? this.ctor.randomIncrement()\r\n : this.randomInt(1, 10);\r\n }\r\n\r\n return 1; // Default increment\r\n }\r\n\r\n /**\r\n * Generate a random integer between min and max (inclusive).\r\n *\r\n * @param min - Minimum value\r\n * @param max - Maximum value\r\n * @returns Random integer\r\n * @private\r\n */\r\n private randomInt(min: number, max: number): number {\r\n return Math.floor(Math.random() * (max - min + 1)) + min;\r\n }\r\n\r\n /**\r\n * Trigger sync operations after successful save.\r\n *\r\n * Emits a model.updated event that ModelSyncOperation listens to.\r\n * The sync is handled by registered sync operations, not directly here.\r\n *\r\n * @param changedFields - Fields that were changed (for filtering)\r\n * @private\r\n */\r\n private async triggerSync(changedFields: string[]): Promise<void> {\r\n // Emit model.updated event - ModelSyncOperation listens to these\r\n await events.triggerAll(\r\n getModelUpdatedEvent(this.ctor),\r\n this.model,\r\n changedFields,\r\n );\r\n }\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"database-writer.mjs","names":[],"sources":["../../../../../../../cascade/src/writer/database-writer.ts"],"sourcesContent":["import { when } from \"@mongez/reinforcements\";\nimport { getSealConfig, v, type ObjectValidator } from \"@warlock.js/seal\";\nimport type {\n DriverContract,\n InsertResult,\n UpdateOperations,\n UpdateResult,\n} from \"../contracts/database-driver.contract\";\nimport type {\n WriterContract,\n WriterOptions,\n WriterResult,\n} from \"../contracts/database-writer.contract\";\nimport { mergeDriverFields } from \"../model/methods/accessor-methods\";\nimport type { ChildModel, Model } from \"../model/model\";\nimport { enqueueSyncFanout } from \"../sync/sync-fanout\";\nimport { triggerModelEvent } from \"../sync/model-events\";\nimport type { StrictMode } from \"../types\";\nimport { DatabaseWriterValidationError } from \"../validation\";\nimport { afterCommit } from \"../transactions/after-commit\";\nimport type { DataSource } from \"./../data-source/data-source\";\n\n/**\n * Database writer service that orchestrates model persistence.\n *\n * Handles the complete save pipeline:\n * 1. Check for changes (skip if no changes and not new)\n * 2. Emit `saving` event (for data enrichment)\n * 3. Emit `validating` event\n * 4. Validate and cast data via @warlock.js/seal schema\n * 5. Emit `validated` event\n * 6. Generate ID (for new NoSQL records)\n * 7. Emit `creating`/`updating` events\n * 8. Execute insert or update via driver\n * 9. Merge returned data into model\n * 10. Reset dirty tracker and update `isNew` flag\n * 11. Emit `saved` and `created`/`updated` events\n *\n * @example\n * ```typescript\n * const user = new User({ name: \"Alice\", email: \"alice@example.com\" });\n * const writer = new DatabaseWriter(user);\n * await writer.save();\n *\n * console.log(user.get(\"id\")); // 1 (auto-generated)\n * console.log(user.get(\"_id\")); // ObjectId(\"...\")\n *\n * // Update existing record\n * user.set(\"name\", \"Alice Smith\");\n * await writer.save();\n * // Only updates the \"name\" field (partial update)\n *\n * // Silent save (no events)\n * await writer.save({ skipEvents: true });\n * ```\n */\nexport class DatabaseWriter implements WriterContract {\n /** The model instance being persisted */\n private readonly model: Model;\n\n /** Dirty columns an update validation dropped (strip mode); never written. */\n private readonly unvalidatedColumns = new Set<string>();\n\n /** Model constructor reference */\n private readonly ctor: ChildModel<Model>;\n\n /** Data source containing driver and ID generator */\n private readonly dataSource: DataSource;\n\n /** Database driver for executing queries */\n private readonly driver: DriverContract;\n\n /** Table/collection name */\n private readonly table: string;\n\n /** Primary key field name */\n private readonly primaryKey: string;\n\n /** Validation schema (if defined) */\n private readonly schema?: ObjectValidator;\n\n /** Strict mode configuration */\n private readonly strictMode: StrictMode;\n\n /**\n * Create a new writer instance for a model.\n *\n * @param model - The model instance to persist\n *\n * @example\n * ```typescript\n * const user = new User({ name: \"Alice\" });\n * const writer = new DatabaseWriter(user);\n * await writer.save();\n * ```\n */\n public constructor(model: Model) {\n this.model = model;\n this.ctor = model.constructor as ChildModel<Model>;\n this.dataSource = this.ctor.getDataSource();\n this.driver = this.dataSource.driver;\n this.table = this.ctor.table;\n this.primaryKey = this.ctor.primaryKey;\n this.schema = this.ctor.schema;\n this.strictMode = this.ctor.strictMode;\n }\n\n /**\n * Save the model instance to the database.\n *\n * @param options - Save options\n * @returns Result with success status, document, and metadata\n * @throws {ValidationError} If validation fails\n */\n public async save(options: WriterOptions = {}): Promise<WriterResult> {\n const isInsert = this.model.isNew;\n\n // 1. Check if model has changes (skip if no changes and not new)\n if (!isInsert && !this.model.hasChanges()) {\n return {\n success: true,\n document: this.model.data,\n isNew: false,\n modifiedCount: 0,\n };\n }\n\n // 2. Emit saving event (before validation for data enrichment)\n if (!options.skipEvents) {\n await this.model.emitEvent(\"saving\", {\n isInsert,\n options,\n mode: isInsert ? \"insert\" : \"update\",\n });\n }\n\n // 3-4. Execute insert or update (validation happens inside each path)\n let result: InsertResult | UpdateResult;\n\n if (isInsert) {\n result = await this.performInsert(options);\n } else {\n await this.validateAndCast(false, options);\n result = await this.performUpdate(options);\n }\n\n // 5. Reset dirty tracker and update isNew flag\n const changedFields = isInsert ? [] : this.model.getDirtyColumns();\n this.model.dirtyTracker.reset();\n this.model.isNew = false;\n\n // 6. Emit post-save events\n if (!options.skipEvents) {\n await this.model.emitEvent(\"saved\");\n await this.model.emitEvent(isInsert ? \"created\" : \"updated\");\n }\n\n // 7. Trigger sync operations after COMMIT (inside a transaction they are\n // queued, so a rolled-back write never fans out). Failures are logged.\n if (!options.skipSync && !isInsert) {\n afterCommit(() => this.triggerSync(changedFields));\n }\n\n return {\n success: true,\n document: this.model.data,\n isNew: isInsert,\n modifiedCount: isInsert\n ? undefined\n : (result as UpdateResult).modifiedCount,\n };\n }\n\n /**\n * Validate and cast model data using the schema.\n *\n * Updates the model's data in-place with validated/casted values.\n *\n * @param isInsert - Whether this is an insert operation\n * @param options - Save options\n * @throws {ValidationError} If validation fails\n * @private\n */\n private async validateAndCast(\n isInsert: boolean,\n options: WriterOptions,\n ): Promise<void> {\n // Emit validating event\n if (!options.skipEvents) {\n await this.model.emitEvent(\"validating\", {\n isInsert,\n options,\n mode: isInsert ? \"insert\" : \"update\",\n });\n }\n\n // Skip validation if requested or no schema defined\n if (options.skipValidation || !this.schema) {\n return;\n }\n\n // Whitelist the framework-managed system columns so a model carrying them\n // (id / _id / timestamps / soft-delete deletedAt) validates cleanly instead\n // of being stripped (strictMode \"strip\") or rejected (strictMode \"fail\").\n // `when(...)` adds each timestamp/soft-delete column only when configured\n // (truthy) — the lazy factory means a disabled column (`false`) never even\n // builds a bogus schema key.\n //\n // The whitelist must apply to BOTH insert and update: on insert a caller may\n // supply a backdated createdAt (e.g. data migrations / imports), and without\n // the whitelist strictMode \"strip\" silently drops it before it reaches the\n // writer, while \"fail\" rejects the whole insert.\n const systemColumns = {\n id: v.scalar().optional(),\n _id: v.any().optional(),\n ...when(this.ctor.createdAtColumn, () => ({\n [this.ctor.createdAtColumn as string]: v.date().optional(),\n })),\n ...when(this.ctor.updatedAtColumn, () => ({\n [this.ctor.updatedAtColumn as string]: v.date().optional(),\n })),\n ...when(this.ctor.deletedAtColumn, () => ({\n [this.ctor.deletedAtColumn as string]: v.date().optional(),\n })),\n };\n\n // Clone full schema for insert, partial (dirty keys only) for updates.\n //\n // An update validates ONLY the dirty columns and merges back only those\n // keys: columns the schema does not list (migration-added, withCount, ...)\n // are never touched, so they can no longer be turned into `$unset`.\n const dataToValidate: Record<string, unknown> = isInsert\n ? this.model.data\n : Object.fromEntries(\n this.model\n .getDirtyColumns()\n .filter(column => column in this.model.data)\n .map(column => [column, this.model.data[column]]),\n );\n\n const validationSchema = isInsert\n ? this.schema.clone().extend(systemColumns)\n : this.schema.clone(Object.keys(dataToValidate)).extend(systemColumns);\n\n // Apply strict mode\n if (this.strictMode === \"strip\") {\n validationSchema.stripUnknown();\n } else if (this.strictMode === \"fail\") {\n validationSchema.allowUnknown(false);\n } else if (this.strictMode === \"allow\") {\n validationSchema.allowUnknown(true);\n }\n\n // Run validation\n const result = await v.validate(validationSchema, dataToValidate, {\n context: {\n model: this.model,\n },\n ...getSealConfig(),\n });\n\n if (!result.isValid) {\n const error = new DatabaseWriterValidationError(\n `[${this.model.constructor.name} Model] ${isInsert ? \"Insert\" : \"Update\"} Validation failed`,\n result.errors,\n );\n if (!options.skipEvents) {\n await this.model.emitEvent(\"validated\", { result, error });\n }\n throw error;\n }\n\n // Update model data with validated/casted data\n if (isInsert) {\n this.model.replaceData(result.data);\n } else {\n const validated = result.data as Record<string, unknown>;\n\n for (const column of Object.keys(dataToValidate)) {\n if (column in validated) {\n this.model.set(column, validated[column]);\n } else {\n // Dropped by strictMode \"strip\": never write an unvalidated column.\n this.unvalidatedColumns.add(column);\n }\n }\n }\n\n // Emit validated event\n if (!options.skipEvents) {\n await this.model.emitEvent(\"validated\", { result });\n }\n }\n\n /**\n * Validate, cast and id-generate a not-yet-persisted model for `upsert()`.\n *\n * Runs the insert-mode pipeline (schema, casts, strictMode, Mongo `id`) but\n * does not write; the caller sends the resulting data through the driver.\n */\n public async prepareForUpsert(): Promise<void> {\n await this.validateAndCast(true, {});\n await this.generateNextId();\n }\n\n /**\n * Perform an insert operation.\n *\n * @param options - Save options\n * @returns Insert result\n * @private\n */\n private async performInsert(options: WriterOptions): Promise<InsertResult> {\n const dataToInsert = await this.prepareForInsert(options);\n\n // INSERT: use full validated data\n let result: InsertResult;\n\n try {\n result = await this.driver.insert(this.table, dataToInsert);\n } catch (error) {\n throw this.mapUniqueViolation(error, true);\n }\n\n // Merge returned data (e.g., generated _id, timestamps)\n // Note: We use merge here because the result might not include all fields\n // (e.g., our generated 'id' field), and we don't want to lose them\n mergeDriverFields(this.model, result.document as Record<string, unknown>);\n\n // Reset dirty tracker immediately after merge to prevent\n // database-generated fields (like _id) from being marked as dirty\n this.model.dirtyTracker.reset();\n\n return result;\n }\n\n /**\n * Run the insert pipeline (validation, casting, id generation, timestamps,\n * `validating`/`validated`/`creating` hooks) WITHOUT writing.\n *\n * `save()` uses this for its insert path, so bulk inserts and single inserts\n * cannot drift. The caller is responsible for sending the returned document\n * to the driver.\n *\n * @param options - Save options (e.g. `skipEvents`, `skipValidation`)\n * @returns The prepared document to insert\n */\n public async prepareForInsert(options: WriterOptions = {}): Promise<Record<string, unknown>> {\n // Validate and cast data\n await this.validateAndCast(true, options);\n\n // Generate ID if needed (NoSQL only)\n await this.generateNextId();\n\n // Get data to insert (already validated and casted)\n const dataToInsert = this.model.data;\n\n // Add createdAt and updatedAt to the data (using resolved column names)\n // The column names are already resolved through the hierarchy:\n // Model static property > Database config > Driver defaults > undefined\n //\n // Only stamp createdAt when the caller did NOT supply one, so a backdated\n // value (e.g. data migrations / imports) is honored instead of overwritten.\n // This mirrors the upsert path's guard. updatedAt is always stamped to\n // reflect the moment the record is persisted.\n const createdAtColumn = this.ctor.createdAtColumn;\n\n if (createdAtColumn && dataToInsert[createdAtColumn] == null) {\n dataToInsert[createdAtColumn] = new Date();\n }\n\n const updatedAtColumn = this.ctor.updatedAtColumn;\n if (updatedAtColumn) {\n dataToInsert[updatedAtColumn] = new Date();\n }\n\n // Emit creating event\n if (!options.skipEvents) {\n await this.model.emitEvent(\"creating\");\n }\n\n return dataToInsert;\n }\n\n /**\n * Perform an update operation.\n *\n * @param options - Save options\n * @returns Update result\n * @private\n */\n private async performUpdate(options: WriterOptions): Promise<UpdateResult> {\n // Emit updating event\n if (!options.skipEvents) {\n await this.model.emitEvent(\"updating\");\n }\n\n // Update the updatedAt timestamp (using resolved column name)\n const updatedAtColumn = this.ctor.updatedAtColumn;\n if (updatedAtColumn) {\n this.model.set(updatedAtColumn, new Date());\n }\n\n if (options.replace) {\n const document = await this.driver.replace(\n this.table,\n this.buildPrimaryKeyFilter(),\n this.model.data,\n );\n\n if (document) {\n this.model.replaceData(document as Record<string, unknown>);\n }\n\n return { modifiedCount: document ? 1 : 0 };\n }\n\n // Build operations from dirty tracker\n const operations = this.buildUpdateOperations();\n\n // Nothing left to write once identity columns are excluded — don't hand the\n // driver an empty update document (MongoDB rejects one).\n if (Object.keys(operations).length === 0) {\n return { modifiedCount: 0 };\n }\n\n // Execute update with operations\n try {\n return await this.driver.update(\n this.table,\n this.buildPrimaryKeyFilter(),\n operations,\n );\n } catch (error) {\n throw this.mapUniqueViolation(error, false);\n }\n }\n\n /**\n * Map a driver unique-constraint violation (Postgres 23505, MongoDB 11000)\n * to a field validation error, so a lost `unique` race never surfaces as a\n * 500. Any other error is returned untouched.\n */\n private mapUniqueViolation(error: unknown, isInsert: boolean): unknown {\n const err = error as\n | {\n code?: string | number;\n detail?: string;\n keyPattern?: Record<string, unknown>;\n message?: string;\n }\n | undefined;\n\n if (err?.code !== \"23505\" && err?.code !== 11000 && err?.code !== \"11000\") {\n return error;\n }\n\n let field: string | undefined;\n\n if (err.keyPattern) {\n field = Object.keys(err.keyPattern)[0];\n } else {\n // Postgres: \"Key (email)=(x) already exists.\"\n field = /Key \\(([^)]+)\\)=/.exec(err.detail ?? \"\")?.[1]?.split(\",\")[0]?.trim();\n }\n\n if (!field) {\n // Mongo: \"index: email_1 dup key\"\n field = /index: (\\S+?)_-?1\\b/.exec(err.message ?? \"\")?.[1];\n }\n\n return new DatabaseWriterValidationError(\n `[${this.model.constructor.name} Model] ${isInsert ? \"Insert\" : \"Update\"} Validation failed`,\n [\n {\n type: \"unique\",\n error: `The ${field ?? \"value\"} must be unique`,\n input: field ?? \"id\",\n },\n ],\n );\n }\n\n /**\n * Build the filter that pins a write to the row this model was loaded from.\n *\n * It reads `model.trustedPrimaryKey` — the value captured when the instance\n * became persisted — NOT the current value in `model.data`. The current value\n * is reachable by mass assignment (`model.merge(req.body)`), so deriving the\n * filter from it let a request body redirect the UPDATE to another document.\n *\n * @returns Filter matching the originally loaded record\n * @private\n */\n private buildPrimaryKeyFilter(): Record<string, unknown> {\n return { [this.primaryKey]: this.model.trustedPrimaryKey };\n }\n\n /**\n * Generate ID for the model if auto-generation is enabled.\n *\n * @private\n */\n public async generateNextId(): Promise<void> {\n if (!this.ctor.autoGenerateId || this.model.get(\"id\")) {\n return;\n }\n\n const idGenerator = this.dataSource.idGenerator;\n if (!idGenerator) {\n return;\n }\n\n // Resolve ID generation options from model configuration\n const initialId = this.resolveInitialId();\n\n const incrementIdBy = this.resolveIncrementBy();\n\n const id = await idGenerator.generateNextId({\n table: this.table,\n initialId,\n incrementIdBy,\n });\n\n this.model.set(\"id\", id);\n }\n\n /**\n * Build update operations from the model's dirty tracker.\n *\n * Handles both modified fields ($set) and removed fields ($unset).\n *\n * @returns Update operations for the driver\n * @private\n *\n * @example\n * ```typescript\n * // Model with changes\n * user.set(\"name\", \"Alice\");\n * user.unset(\"tempField\");\n *\n * const operations = this.buildUpdateOperations();\n * // {\n * // $set: { name: \"Alice\" },\n * // $unset: { tempField: 1 }\n * // }\n * ```\n */\n private buildUpdateOperations(): UpdateOperations {\n const operations: UpdateOperations = {};\n\n // Identity columns are never written by an update. The filter pins the row\n // by its captured primary key, so a dirty `id`/`_id` can only come from\n // mass assignment or an explicit set() — either way, rewriting the key of\n // an existing row corrupts identity (and `_id` is immutable in MongoDB).\n // Changing a primary key is a deliberate operation: use the atomic/raw APIs.\n const identityColumns = new Set([this.primaryKey, \"id\", \"_id\"]);\n\n // Get dirty columns (modified fields)\n const dirtyColumns = this.model\n .getDirtyColumns()\n .filter(column => !identityColumns.has(column) && !this.unvalidatedColumns.has(column));\n\n if (dirtyColumns.length > 0) {\n operations.$set = {};\n for (const column of dirtyColumns) {\n const value = this.model.get(column);\n if (value === undefined) continue;\n\n operations.$set[column] = this.model.get(column);\n }\n }\n\n // Get removed columns\n const removedColumns = this.model\n .getRemovedColumns()\n .filter(column => !identityColumns.has(column));\n\n if (removedColumns.length > 0) {\n operations.$unset = {};\n for (const column of removedColumns) {\n operations.$unset[column] = 1;\n }\n }\n\n return operations;\n }\n\n /**\n * Resolve the initial ID from model configuration.\n *\n * Priority:\n * 1. Model.initialId (explicit value)\n * 2. Model.randomInitialId (random or function)\n * 3. Default: 1\n *\n * @returns The initial ID value\n * @private\n */\n private resolveInitialId(): number {\n if (this.ctor.initialId) {\n return this.ctor.initialId;\n }\n\n if (this.ctor.randomInitialId) {\n return typeof this.ctor.randomInitialId === \"function\"\n ? this.ctor.randomInitialId()\n : this.randomInt(10000, 499999);\n }\n\n return 1; // Default initial ID\n }\n\n /**\n * Resolve the increment value from model configuration.\n *\n * Priority:\n * 1. Model.incrementIdBy (explicit value)\n * 2. Model.randomIncrement (random or function)\n * 3. Default: 1\n *\n * @returns The increment value\n * @private\n */\n private resolveIncrementBy(): number {\n if (this.ctor.randomIncrement) {\n return typeof this.ctor.randomIncrement === \"function\"\n ? this.ctor.randomIncrement()\n : this.randomInt(1, 10);\n }\n\n if (this.ctor.incrementIdBy) {\n return this.ctor.incrementIdBy;\n }\n\n return 1; // Default increment\n }\n\n /**\n * Generate a random integer between min and max (inclusive).\n *\n * @param min - Minimum value\n * @param max - Maximum value\n * @returns Random integer\n * @private\n */\n private randomInt(min: number, max: number): number {\n return Math.floor(Math.random() * (max - min + 1)) + min;\n }\n\n /**\n * Trigger sync operations after successful save.\n *\n * Emits a model.updated event that ModelSyncOperation listens to.\n * The sync is handled by registered sync operations, not directly here.\n *\n * @param changedFields - Fields that were changed (for filtering)\n * @private\n */\n private triggerSync(changedFields: string[]): void {\n enqueueSyncFanout({\n sourceModel: this.ctor.name,\n operation: \"update\",\n execute: () => triggerModelEvent(this.ctor, \"updated\", this.model, changedFields),\n });\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -8,14 +8,15 @@
8
8
  "cascade": "bin/cascade.js"
9
9
  },
10
10
  "dependencies": {
11
- "@mongez/copper": "^2.1.2",
12
- "@mongez/dotenv": "^1.3.2",
13
- "@mongez/events": "^2.2.7",
11
+ "@mongez/copper": "^2.1.3",
12
+ "@mongez/dotenv": "^1.3.3",
13
+ "@mongez/events": "^2.2.8",
14
14
  "@mongez/reinforcements": "^4.0.2",
15
15
  "@mongez/supportive-is": "^2.1.5",
16
- "@warlock.js/context": "5.20.0",
17
- "@warlock.js/logger": "5.20.0",
18
- "@warlock.js/seal": "5.20.0",
16
+ "@warlock.js/context": "5.22.0",
17
+ "@warlock.js/logger": "5.22.0",
18
+ "@warlock.js/scheduler": "5.22.0",
19
+ "@warlock.js/seal": "5.22.0",
19
20
  "citty": "^0.2.2",
20
21
  "fast-glob": "^3.3.3"
21
22
  },
@@ -51,7 +52,7 @@
51
52
  ],
52
53
  "author": "hassanzohdy",
53
54
  "license": "MIT",
54
- "version": "5.20.0",
55
+ "version": "5.22.0",
55
56
  "main": "./cjs/index.cjs",
56
57
  "module": "./esm/index.mjs",
57
58
  "types": "./esm/index.d.mts",
@@ -1,79 +0,0 @@
1
- import { SyncAdapterContract, SyncInstruction } from "../../contracts/sync-adapter.contract.mjs";
2
- import { PostgresDriver } from "./postgres-driver.mjs";
3
-
4
- //#region ../cascade/src/drivers/postgres/postgres-sync-adapter.d.ts
5
- /**
6
- * PostgreSQL Sync Adapter.
7
- *
8
- * Handles batch updates for embedded/denormalized data stored
9
- * in JSONB columns. In a normalized SQL world, this is less common
10
- * than in MongoDB, but still useful for JSONB documents.
11
- *
12
- * @example
13
- * ```typescript
14
- * const syncAdapter = driver.syncAdapter();
15
- *
16
- * // Update embedded user data in posts
17
- * await syncAdapter.executeBatch([
18
- * {
19
- * targetTable: 'posts',
20
- * filter: { 'author.id': 123 },
21
- * update: { 'author.name': 'New Name' },
22
- * // ... other fields
23
- * }
24
- * ]);
25
- * ```
26
- */
27
- declare class PostgresSyncAdapter implements SyncAdapterContract {
28
- private readonly driver;
29
- /**
30
- * Create a new sync adapter.
31
- *
32
- * @param driver - The PostgreSQL driver instance
33
- */
34
- constructor(driver: PostgresDriver);
35
- /**
36
- * Execute a batch of sync instructions.
37
- *
38
- * @param instructions - Array of sync instructions
39
- * @returns Total number of affected rows
40
- */
41
- executeBatch(instructions: SyncInstruction[]): Promise<number>;
42
- /**
43
- * Execute a single sync instruction.
44
- *
45
- * @param instruction - Sync instruction
46
- * @returns Number of affected rows
47
- */
48
- executeOne(instruction: SyncInstruction): Promise<number>;
49
- /**
50
- * Execute an array update instruction with positional operators.
51
- *
52
- * @param instruction - Sync instruction with array update info
53
- * @returns Number of affected rows
54
- */
55
- executeArrayUpdate(instruction: SyncInstruction): Promise<number>;
56
- /**
57
- * Execute an update on JSONB fields.
58
- *
59
- * @param table - Table name
60
- * @param filter - Row filter
61
- * @param update - Fields to update
62
- * @returns Number of affected rows
63
- */
64
- private executeJsonbUpdate;
65
- /**
66
- * Execute an update on elements within a JSONB array.
67
- *
68
- * @param table - Table name
69
- * @param filter - Row filter
70
- * @param arrayField - JSONB array column
71
- * @param arrayFilter - Filter to match array elements
72
- * @param update - Fields to update on matched elements
73
- * @returns Number of affected rows
74
- */
75
- private executeArrayElementUpdate;
76
- }
77
- //#endregion
78
- export { PostgresSyncAdapter };
79
- //# sourceMappingURL=postgres-sync-adapter.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"postgres-sync-adapter.d.mts","names":[],"sources":["../../../../../../../../cascade/src/drivers/postgres/postgres-sync-adapter.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;cAkCa,mBAAA,YAA+B,mBAAA;EAAA,iBAMN,MAAA;EAiEtB;;;AAmFyB;;cApJH,MAAA,EAAQ,cAAA;;;;;;;EAQ/B,YAAA,CAAa,YAAA,EAAc,eAAA,KAAoB,OAAA;;;;;;;EAoB/C,UAAA,CAAW,WAAA,EAAa,eAAA,GAAkB,OAAA;;;;;;;EAW1C,kBAAA,CAAmB,WAAA,EAAa,eAAA,GAAkB,OAAA;;;;;;;;;UA0BjD,kBAAA;;;;;;;;;;;UAmFA,yBAAA;AAAA"}
@@ -1,162 +0,0 @@
1
- //#region ../cascade/src/drivers/postgres/postgres-sync-adapter.ts
2
- /**
3
- * PostgreSQL Sync Adapter.
4
- *
5
- * Handles batch updates for embedded/denormalized data stored
6
- * in JSONB columns. In a normalized SQL world, this is less common
7
- * than in MongoDB, but still useful for JSONB documents.
8
- *
9
- * @example
10
- * ```typescript
11
- * const syncAdapter = driver.syncAdapter();
12
- *
13
- * // Update embedded user data in posts
14
- * await syncAdapter.executeBatch([
15
- * {
16
- * targetTable: 'posts',
17
- * filter: { 'author.id': 123 },
18
- * update: { 'author.name': 'New Name' },
19
- * // ... other fields
20
- * }
21
- * ]);
22
- * ```
23
- */
24
- var PostgresSyncAdapter = class {
25
- driver;
26
- /**
27
- * Create a new sync adapter.
28
- *
29
- * @param driver - The PostgreSQL driver instance
30
- */
31
- constructor(driver) {
32
- this.driver = driver;
33
- }
34
- /**
35
- * Execute a batch of sync instructions.
36
- *
37
- * @param instructions - Array of sync instructions
38
- * @returns Total number of affected rows
39
- */
40
- async executeBatch(instructions) {
41
- let totalAffected = 0;
42
- for (const instruction of instructions) if (instruction.isArrayUpdate) totalAffected += await this.executeArrayUpdate(instruction);
43
- else totalAffected += await this.executeOne(instruction);
44
- return totalAffected;
45
- }
46
- /**
47
- * Execute a single sync instruction.
48
- *
49
- * @param instruction - Sync instruction
50
- * @returns Number of affected rows
51
- */
52
- async executeOne(instruction) {
53
- const { targetTable, filter, update } = instruction;
54
- return this.executeJsonbUpdate(targetTable, filter, update);
55
- }
56
- /**
57
- * Execute an array update instruction with positional operators.
58
- *
59
- * @param instruction - Sync instruction with array update info
60
- * @returns Number of affected rows
61
- */
62
- async executeArrayUpdate(instruction) {
63
- const { targetTable, filter, update, arrayField, identifierField, identifierValue } = instruction;
64
- if (!arrayField || !identifierField || identifierValue === void 0) return this.executeOne(instruction);
65
- return this.executeArrayElementUpdate(targetTable, filter, arrayField, { [identifierField]: identifierValue }, update);
66
- }
67
- /**
68
- * Execute an update on JSONB fields.
69
- *
70
- * @param table - Table name
71
- * @param filter - Row filter
72
- * @param update - Fields to update
73
- * @returns Number of affected rows
74
- */
75
- async executeJsonbUpdate(table, filter, update) {
76
- const quotedTable = this.driver.dialect.quoteIdentifier(table);
77
- const setClauses = [];
78
- const params = [];
79
- let paramIndex = 1;
80
- for (const [path, value] of Object.entries(update)) if (path.includes(".")) {
81
- const [column, ...pathParts] = path.split(".");
82
- const quotedColumn = this.driver.dialect.quoteIdentifier(column ?? path);
83
- const jsonPath = `{${pathParts.join(",")}}`;
84
- const placeholder = this.driver.dialect.placeholder(paramIndex++);
85
- params.push(JSON.stringify(value));
86
- setClauses.push(`${quotedColumn} = jsonb_set(COALESCE(${quotedColumn}, '{}'::jsonb), '${jsonPath}', ${placeholder}::jsonb)`);
87
- } else {
88
- const quotedColumn = this.driver.dialect.quoteIdentifier(path);
89
- const placeholder = this.driver.dialect.placeholder(paramIndex++);
90
- params.push(value);
91
- setClauses.push(`${quotedColumn} = ${placeholder}`);
92
- }
93
- const whereClauses = [];
94
- for (const [key, value] of Object.entries(filter)) if (key.includes(".")) {
95
- const [column, ...pathParts] = key.split(".");
96
- const quotedColumn = this.driver.dialect.quoteIdentifier(column ?? key);
97
- const intermediateExpr = pathParts.slice(0, -1).map((p) => `->'${p}'`).join("");
98
- const finalExpr = `->>'${pathParts[pathParts.length - 1]}'`;
99
- const placeholder = this.driver.dialect.placeholder(paramIndex++);
100
- params.push(value);
101
- whereClauses.push(`${quotedColumn}${intermediateExpr}${finalExpr} = ${placeholder}`);
102
- } else {
103
- const quotedKey = this.driver.dialect.quoteIdentifier(key);
104
- const placeholder = this.driver.dialect.placeholder(paramIndex++);
105
- params.push(value);
106
- whereClauses.push(`${quotedKey} = ${placeholder}`);
107
- }
108
- const sql = `UPDATE ${quotedTable} SET ${setClauses.join(", ")} WHERE ${whereClauses.join(" AND ")}`;
109
- return (await this.driver.query(sql, params)).rowCount ?? 0;
110
- }
111
- /**
112
- * Execute an update on elements within a JSONB array.
113
- *
114
- * @param table - Table name
115
- * @param filter - Row filter
116
- * @param arrayField - JSONB array column
117
- * @param arrayFilter - Filter to match array elements
118
- * @param update - Fields to update on matched elements
119
- * @returns Number of affected rows
120
- */
121
- async executeArrayElementUpdate(table, filter, arrayField, arrayFilter, update) {
122
- const quotedTable = this.driver.dialect.quoteIdentifier(table);
123
- const quotedArrayField = this.driver.dialect.quoteIdentifier(arrayField);
124
- const params = [];
125
- let paramIndex = 1;
126
- const sql = `
127
- WITH updated AS (
128
- SELECT ctid, (
129
- SELECT jsonb_agg(
130
- CASE
131
- WHEN ${Object.entries(arrayFilter).map(([key, value]) => {
132
- params.push(value);
133
- return `elem->>'${key}' = ${this.driver.dialect.placeholder(paramIndex++)}`;
134
- }).join(" AND ")}
135
- THEN elem || jsonb_build_object(${Object.entries(update).map(([key, value]) => {
136
- params.push(JSON.stringify(value));
137
- return `'${key}', ${this.driver.dialect.placeholder(paramIndex++)}::jsonb`;
138
- }).join(", ")})
139
- ELSE elem
140
- END
141
- )
142
- FROM jsonb_array_elements(${quotedArrayField}) elem
143
- ) AS new_array
144
- FROM ${quotedTable}
145
- WHERE ${Object.entries(filter).map(([key, value]) => {
146
- const quotedKey = this.driver.dialect.quoteIdentifier(key);
147
- params.push(value);
148
- return `${quotedKey} = ${this.driver.dialect.placeholder(paramIndex++)}`;
149
- }).join(" AND ")}
150
- )
151
- UPDATE ${quotedTable} t
152
- SET ${quotedArrayField} = u.new_array
153
- FROM updated u
154
- WHERE t.ctid = u.ctid
155
- `;
156
- return (await this.driver.query(sql, params)).rowCount ?? 0;
157
- }
158
- };
159
-
160
- //#endregion
161
- export { PostgresSyncAdapter };
162
- //# sourceMappingURL=postgres-sync-adapter.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"postgres-sync-adapter.mjs","names":[],"sources":["../../../../../../../../cascade/src/drivers/postgres/postgres-sync-adapter.ts"],"sourcesContent":["/**\r\n * PostgreSQL Sync Adapter\r\n *\r\n * Implements the SyncAdapterContract for batch update operations\r\n * on embedded/denormalized data in PostgreSQL.\r\n *\r\n * @module cascade/drivers/postgres\r\n */\r\n\r\nimport type { SyncAdapterContract, SyncInstruction } from \"../../contracts/sync-adapter.contract\";\r\nimport type { PostgresDriver } from \"./postgres-driver\";\r\n\r\n/**\r\n * PostgreSQL Sync Adapter.\r\n *\r\n * Handles batch updates for embedded/denormalized data stored\r\n * in JSONB columns. In a normalized SQL world, this is less common\r\n * than in MongoDB, but still useful for JSONB documents.\r\n *\r\n * @example\r\n * ```typescript\r\n * const syncAdapter = driver.syncAdapter();\r\n *\r\n * // Update embedded user data in posts\r\n * await syncAdapter.executeBatch([\r\n * {\r\n * targetTable: 'posts',\r\n * filter: { 'author.id': 123 },\r\n * update: { 'author.name': 'New Name' },\r\n * // ... other fields\r\n * }\r\n * ]);\r\n * ```\r\n */\r\nexport class PostgresSyncAdapter implements SyncAdapterContract {\r\n /**\r\n * Create a new sync adapter.\r\n *\r\n * @param driver - The PostgreSQL driver instance\r\n */\r\n public constructor(private readonly driver: PostgresDriver) {}\r\n\r\n /**\r\n * Execute a batch of sync instructions.\r\n *\r\n * @param instructions - Array of sync instructions\r\n * @returns Total number of affected rows\r\n */\r\n public async executeBatch(instructions: SyncInstruction[]): Promise<number> {\r\n let totalAffected = 0;\r\n\r\n for (const instruction of instructions) {\r\n if (instruction.isArrayUpdate) {\r\n totalAffected += await this.executeArrayUpdate(instruction);\r\n } else {\r\n totalAffected += await this.executeOne(instruction);\r\n }\r\n }\r\n\r\n return totalAffected;\r\n }\r\n\r\n /**\r\n * Execute a single sync instruction.\r\n *\r\n * @param instruction - Sync instruction\r\n * @returns Number of affected rows\r\n */\r\n public async executeOne(instruction: SyncInstruction): Promise<number> {\r\n const { targetTable, filter, update } = instruction;\r\n return this.executeJsonbUpdate(targetTable, filter, update);\r\n }\r\n\r\n /**\r\n * Execute an array update instruction with positional operators.\r\n *\r\n * @param instruction - Sync instruction with array update info\r\n * @returns Number of affected rows\r\n */\r\n public async executeArrayUpdate(instruction: SyncInstruction): Promise<number> {\r\n const { targetTable, filter, update, arrayField, identifierField, identifierValue } =\r\n instruction;\r\n\r\n if (!arrayField || !identifierField || identifierValue === undefined) {\r\n // Fall back to regular update if array info is missing\r\n return this.executeOne(instruction);\r\n }\r\n\r\n return this.executeArrayElementUpdate(\r\n targetTable,\r\n filter,\r\n arrayField,\r\n { [identifierField]: identifierValue },\r\n update,\r\n );\r\n }\r\n\r\n /**\r\n * Execute an update on JSONB fields.\r\n *\r\n * @param table - Table name\r\n * @param filter - Row filter\r\n * @param update - Fields to update\r\n * @returns Number of affected rows\r\n */\r\n private async executeJsonbUpdate(\r\n table: string,\r\n filter: Record<string, unknown>,\r\n update: Record<string, unknown>,\r\n ): Promise<number> {\r\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\r\n const setClauses: string[] = [];\r\n const params: unknown[] = [];\r\n let paramIndex = 1;\r\n\r\n // Build SET clauses for each update field\r\n for (const [path, value] of Object.entries(update)) {\r\n if (path.includes(\".\")) {\r\n // JSONB path update: \"column.nested.field\" -> jsonb_set\r\n const [column, ...pathParts] = path.split(\".\");\r\n\r\n // The `includes(\".\")` check above guarantees a non-empty first segment,\r\n // but the compiler does not carry it here. Falling back to the whole\r\n // path keeps a REAL identifier in the SQL — quoting `undefined` would\r\n // emit a valid query against a column that does not exist.\r\n const quotedColumn = this.driver.dialect.quoteIdentifier(column ?? path);\r\n const jsonPath = `{${pathParts.join(\",\")}}`;\r\n const placeholder = this.driver.dialect.placeholder(paramIndex++);\r\n params.push(JSON.stringify(value));\r\n\r\n setClauses.push(\r\n `${quotedColumn} = jsonb_set(COALESCE(${quotedColumn}, '{}'::jsonb), '${jsonPath}', ${placeholder}::jsonb)`,\r\n );\r\n } else {\r\n // Regular column update\r\n const quotedColumn = this.driver.dialect.quoteIdentifier(path);\r\n const placeholder = this.driver.dialect.placeholder(paramIndex++);\r\n params.push(value);\r\n\r\n setClauses.push(`${quotedColumn} = ${placeholder}`);\r\n }\r\n }\r\n\r\n // Build WHERE clause\r\n const whereClauses: string[] = [];\r\n for (const [key, value] of Object.entries(filter)) {\r\n if (key.includes(\".\")) {\r\n // JSONB path filter: intermediate segments use -> (jsonb), final uses ->> (text).\r\n // e.g. \"column.a\" → \"column\"->>'a', \"column.a.b\" → \"column\"->'a'->>'b'\r\n const [column, ...pathParts] = key.split(\".\");\r\n\r\n // Same reasoning as the jsonb_set path above: fall back to the whole\r\n // key so the emitted SQL always names a real column.\r\n const quotedColumn = this.driver.dialect.quoteIdentifier(column ?? key);\r\n const intermediateExpr = pathParts\r\n .slice(0, -1)\r\n .map((p) => `->'${p}'`)\r\n .join(\"\");\r\n const finalExpr = `->>'${pathParts[pathParts.length - 1]}'`;\r\n const placeholder = this.driver.dialect.placeholder(paramIndex++);\r\n params.push(value);\r\n\r\n whereClauses.push(`${quotedColumn}${intermediateExpr}${finalExpr} = ${placeholder}`);\r\n } else {\r\n const quotedKey = this.driver.dialect.quoteIdentifier(key);\r\n const placeholder = this.driver.dialect.placeholder(paramIndex++);\r\n params.push(value);\r\n\r\n whereClauses.push(`${quotedKey} = ${placeholder}`);\r\n }\r\n }\r\n\r\n const sql = `UPDATE ${quotedTable} SET ${setClauses.join(\", \")} WHERE ${whereClauses.join(\" AND \")}`;\r\n\r\n const result = await this.driver.query(sql, params);\r\n return result.rowCount ?? 0;\r\n }\r\n\r\n /**\r\n * Execute an update on elements within a JSONB array.\r\n *\r\n * @param table - Table name\r\n * @param filter - Row filter\r\n * @param arrayField - JSONB array column\r\n * @param arrayFilter - Filter to match array elements\r\n * @param update - Fields to update on matched elements\r\n * @returns Number of affected rows\r\n */\r\n private async executeArrayElementUpdate(\r\n table: string,\r\n filter: Record<string, unknown>,\r\n arrayField: string,\r\n arrayFilter: Record<string, unknown>,\r\n update: Record<string, unknown>,\r\n ): Promise<number> {\r\n const quotedTable = this.driver.dialect.quoteIdentifier(table);\r\n const quotedArrayField = this.driver.dialect.quoteIdentifier(arrayField);\r\n\r\n const params: unknown[] = [];\r\n let paramIndex = 1;\r\n\r\n // Build array element filter condition\r\n const arrayFilterCondition = Object.entries(arrayFilter)\r\n .map(([key, value]) => {\r\n params.push(value);\r\n return `elem->>'${key}' = ${this.driver.dialect.placeholder(paramIndex++)}`;\r\n })\r\n .join(\" AND \");\r\n\r\n // Build update expression for matched elements\r\n const updateExpr = Object.entries(update)\r\n .map(([key, value]) => {\r\n params.push(JSON.stringify(value));\r\n return `'${key}', ${this.driver.dialect.placeholder(paramIndex++)}::jsonb`;\r\n })\r\n .join(\", \");\r\n\r\n // Build WHERE clause for row filter\r\n const whereClauses = Object.entries(filter).map(([key, value]) => {\r\n const quotedKey = this.driver.dialect.quoteIdentifier(key);\r\n params.push(value);\r\n return `${quotedKey} = ${this.driver.dialect.placeholder(paramIndex++)}`;\r\n });\r\n\r\n // Use a CTE to update array elements.\r\n // ctid is PostgreSQL's built-in physical row identifier, available on every\r\n // table regardless of the primary key column name.\r\n const sql = `\r\n WITH updated AS (\r\n SELECT ctid, (\r\n SELECT jsonb_agg(\r\n CASE\r\n WHEN ${arrayFilterCondition}\r\n THEN elem || jsonb_build_object(${updateExpr})\r\n ELSE elem\r\n END\r\n )\r\n FROM jsonb_array_elements(${quotedArrayField}) elem\r\n ) AS new_array\r\n FROM ${quotedTable}\r\n WHERE ${whereClauses.join(\" AND \")}\r\n )\r\n UPDATE ${quotedTable} t\r\n SET ${quotedArrayField} = u.new_array\r\n FROM updated u\r\n WHERE t.ctid = u.ctid\r\n `;\r\n\r\n const result = await this.driver.query(sql, params);\r\n return result.rowCount ?? 0;\r\n }\r\n}\r\n"],"mappings":"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"}