@spinajs/orm 2.0.481 → 2.0.482

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 (228) hide show
  1. package/README.md +72 -3
  2. package/lib/cjs/bootstrap.js +10 -0
  3. package/lib/cjs/bootstrap.js.map +1 -1
  4. package/lib/cjs/builders.d.ts +115 -11
  5. package/lib/cjs/builders.d.ts.map +1 -1
  6. package/lib/cjs/builders.js +342 -283
  7. package/lib/cjs/builders.js.map +1 -1
  8. package/lib/cjs/converters.d.ts +9 -2
  9. package/lib/cjs/converters.d.ts.map +1 -1
  10. package/lib/cjs/converters.js +48 -8
  11. package/lib/cjs/converters.js.map +1 -1
  12. package/lib/cjs/decorators.d.ts +26 -3
  13. package/lib/cjs/decorators.d.ts.map +1 -1
  14. package/lib/cjs/decorators.js +95 -42
  15. package/lib/cjs/decorators.js.map +1 -1
  16. package/lib/cjs/dehydrators.js +1 -1
  17. package/lib/cjs/dehydrators.js.map +1 -1
  18. package/lib/cjs/descriptor.d.ts +1 -0
  19. package/lib/cjs/descriptor.d.ts.map +1 -1
  20. package/lib/cjs/descriptor.js +16 -40
  21. package/lib/cjs/descriptor.js.map +1 -1
  22. package/lib/cjs/driver.d.ts +136 -7
  23. package/lib/cjs/driver.d.ts.map +1 -1
  24. package/lib/cjs/driver.js +279 -0
  25. package/lib/cjs/driver.js.map +1 -1
  26. package/lib/cjs/existsRelationHandlers.d.ts +29 -0
  27. package/lib/cjs/existsRelationHandlers.d.ts.map +1 -0
  28. package/lib/cjs/existsRelationHandlers.js +104 -0
  29. package/lib/cjs/existsRelationHandlers.js.map +1 -0
  30. package/lib/cjs/fp.d.ts.map +1 -1
  31. package/lib/cjs/fp.js +10 -15
  32. package/lib/cjs/fp.js.map +1 -1
  33. package/lib/cjs/hydrators.d.ts.map +1 -1
  34. package/lib/cjs/hydrators.js +29 -2
  35. package/lib/cjs/hydrators.js.map +1 -1
  36. package/lib/cjs/identity-map.d.ts +60 -0
  37. package/lib/cjs/identity-map.d.ts.map +1 -0
  38. package/lib/cjs/identity-map.js +135 -0
  39. package/lib/cjs/identity-map.js.map +1 -0
  40. package/lib/cjs/index.d.ts +12 -0
  41. package/lib/cjs/index.d.ts.map +1 -1
  42. package/lib/cjs/index.js +12 -0
  43. package/lib/cjs/index.js.map +1 -1
  44. package/lib/cjs/interfaces.d.ts +295 -9
  45. package/lib/cjs/interfaces.d.ts.map +1 -1
  46. package/lib/cjs/interfaces.js +54 -1
  47. package/lib/cjs/interfaces.js.map +1 -1
  48. package/lib/cjs/metadata.d.ts.map +1 -1
  49. package/lib/cjs/metadata.js +4 -1
  50. package/lib/cjs/metadata.js.map +1 -1
  51. package/lib/cjs/metrics.d.ts +53 -0
  52. package/lib/cjs/metrics.d.ts.map +1 -0
  53. package/lib/cjs/metrics.js +82 -0
  54. package/lib/cjs/metrics.js.map +1 -0
  55. package/lib/cjs/middlewares.d.ts +13 -1
  56. package/lib/cjs/middlewares.d.ts.map +1 -1
  57. package/lib/cjs/middlewares.js +55 -15
  58. package/lib/cjs/middlewares.js.map +1 -1
  59. package/lib/cjs/model.d.ts +129 -13
  60. package/lib/cjs/model.d.ts.map +1 -1
  61. package/lib/cjs/model.js +449 -93
  62. package/lib/cjs/model.js.map +1 -1
  63. package/lib/cjs/orm.d.ts.map +1 -1
  64. package/lib/cjs/orm.js +12 -3
  65. package/lib/cjs/orm.js.map +1 -1
  66. package/lib/cjs/orphan.d.ts +27 -0
  67. package/lib/cjs/orphan.d.ts.map +1 -0
  68. package/lib/cjs/orphan.js +55 -0
  69. package/lib/cjs/orphan.js.map +1 -0
  70. package/lib/cjs/primary-keys.d.ts +60 -0
  71. package/lib/cjs/primary-keys.d.ts.map +1 -0
  72. package/lib/cjs/primary-keys.js +243 -0
  73. package/lib/cjs/primary-keys.js.map +1 -0
  74. package/lib/cjs/relation-objects.d.ts +101 -9
  75. package/lib/cjs/relation-objects.d.ts.map +1 -1
  76. package/lib/cjs/relation-objects.js +194 -43
  77. package/lib/cjs/relation-objects.js.map +1 -1
  78. package/lib/cjs/relations.d.ts +7 -1
  79. package/lib/cjs/relations.d.ts.map +1 -1
  80. package/lib/cjs/relations.js +16 -1
  81. package/lib/cjs/relations.js.map +1 -1
  82. package/lib/cjs/resilience.d.ts +50 -0
  83. package/lib/cjs/resilience.d.ts.map +1 -0
  84. package/lib/cjs/resilience.js +70 -0
  85. package/lib/cjs/resilience.js.map +1 -0
  86. package/lib/cjs/snapshot.d.ts +58 -0
  87. package/lib/cjs/snapshot.d.ts.map +1 -0
  88. package/lib/cjs/snapshot.js +125 -0
  89. package/lib/cjs/snapshot.js.map +1 -0
  90. package/lib/cjs/statements.d.ts +11 -1
  91. package/lib/cjs/statements.d.ts.map +1 -1
  92. package/lib/cjs/statements.js +13 -2
  93. package/lib/cjs/statements.js.map +1 -1
  94. package/lib/cjs/subject-builder.d.ts +79 -0
  95. package/lib/cjs/subject-builder.d.ts.map +1 -0
  96. package/lib/cjs/subject-builder.js +296 -0
  97. package/lib/cjs/subject-builder.js.map +1 -0
  98. package/lib/cjs/subject-executor.d.ts +122 -0
  99. package/lib/cjs/subject-executor.d.ts.map +1 -0
  100. package/lib/cjs/subject-executor.js +319 -0
  101. package/lib/cjs/subject-executor.js.map +1 -0
  102. package/lib/cjs/subject-sorter.d.ts +70 -0
  103. package/lib/cjs/subject-sorter.d.ts.map +1 -0
  104. package/lib/cjs/subject-sorter.js +173 -0
  105. package/lib/cjs/subject-sorter.js.map +1 -0
  106. package/lib/cjs/subject.d.ts +100 -0
  107. package/lib/cjs/subject.d.ts.map +1 -0
  108. package/lib/cjs/subject.js +89 -0
  109. package/lib/cjs/subject.js.map +1 -0
  110. package/lib/cjs/unit-of-work.d.ts +52 -0
  111. package/lib/cjs/unit-of-work.d.ts.map +1 -0
  112. package/lib/cjs/unit-of-work.js +170 -0
  113. package/lib/cjs/unit-of-work.js.map +1 -0
  114. package/lib/mjs/bootstrap.js +10 -0
  115. package/lib/mjs/bootstrap.js.map +1 -1
  116. package/lib/mjs/builders.d.ts +115 -11
  117. package/lib/mjs/builders.d.ts.map +1 -1
  118. package/lib/mjs/builders.js +344 -285
  119. package/lib/mjs/builders.js.map +1 -1
  120. package/lib/mjs/converters.d.ts +9 -2
  121. package/lib/mjs/converters.d.ts.map +1 -1
  122. package/lib/mjs/converters.js +48 -8
  123. package/lib/mjs/converters.js.map +1 -1
  124. package/lib/mjs/decorators.d.ts +26 -3
  125. package/lib/mjs/decorators.d.ts.map +1 -1
  126. package/lib/mjs/decorators.js +96 -44
  127. package/lib/mjs/decorators.js.map +1 -1
  128. package/lib/mjs/dehydrators.js +1 -1
  129. package/lib/mjs/dehydrators.js.map +1 -1
  130. package/lib/mjs/descriptor.d.ts +1 -0
  131. package/lib/mjs/descriptor.d.ts.map +1 -1
  132. package/lib/mjs/descriptor.js +17 -39
  133. package/lib/mjs/descriptor.js.map +1 -1
  134. package/lib/mjs/driver.d.ts +136 -7
  135. package/lib/mjs/driver.d.ts.map +1 -1
  136. package/lib/mjs/driver.js +279 -0
  137. package/lib/mjs/driver.js.map +1 -1
  138. package/lib/mjs/existsRelationHandlers.d.ts +29 -0
  139. package/lib/mjs/existsRelationHandlers.d.ts.map +1 -0
  140. package/lib/mjs/existsRelationHandlers.js +100 -0
  141. package/lib/mjs/existsRelationHandlers.js.map +1 -0
  142. package/lib/mjs/fp.d.ts.map +1 -1
  143. package/lib/mjs/fp.js +10 -15
  144. package/lib/mjs/fp.js.map +1 -1
  145. package/lib/mjs/hydrators.d.ts.map +1 -1
  146. package/lib/mjs/hydrators.js +29 -2
  147. package/lib/mjs/hydrators.js.map +1 -1
  148. package/lib/mjs/identity-map.d.ts +60 -0
  149. package/lib/mjs/identity-map.d.ts.map +1 -0
  150. package/lib/mjs/identity-map.js +130 -0
  151. package/lib/mjs/identity-map.js.map +1 -0
  152. package/lib/mjs/index.d.ts +12 -0
  153. package/lib/mjs/index.d.ts.map +1 -1
  154. package/lib/mjs/index.js +12 -0
  155. package/lib/mjs/index.js.map +1 -1
  156. package/lib/mjs/interfaces.d.ts +295 -9
  157. package/lib/mjs/interfaces.d.ts.map +1 -1
  158. package/lib/mjs/interfaces.js +53 -0
  159. package/lib/mjs/interfaces.js.map +1 -1
  160. package/lib/mjs/metadata.d.ts.map +1 -1
  161. package/lib/mjs/metadata.js +4 -1
  162. package/lib/mjs/metadata.js.map +1 -1
  163. package/lib/mjs/metrics.d.ts +53 -0
  164. package/lib/mjs/metrics.d.ts.map +1 -0
  165. package/lib/mjs/metrics.js +76 -0
  166. package/lib/mjs/metrics.js.map +1 -0
  167. package/lib/mjs/middlewares.d.ts +13 -1
  168. package/lib/mjs/middlewares.d.ts.map +1 -1
  169. package/lib/mjs/middlewares.js +55 -15
  170. package/lib/mjs/middlewares.js.map +1 -1
  171. package/lib/mjs/model.d.ts +129 -13
  172. package/lib/mjs/model.d.ts.map +1 -1
  173. package/lib/mjs/model.js +451 -95
  174. package/lib/mjs/model.js.map +1 -1
  175. package/lib/mjs/orm.d.ts.map +1 -1
  176. package/lib/mjs/orm.js +12 -3
  177. package/lib/mjs/orm.js.map +1 -1
  178. package/lib/mjs/orphan.d.ts +27 -0
  179. package/lib/mjs/orphan.d.ts.map +1 -0
  180. package/lib/mjs/orphan.js +52 -0
  181. package/lib/mjs/orphan.js.map +1 -0
  182. package/lib/mjs/primary-keys.d.ts +60 -0
  183. package/lib/mjs/primary-keys.d.ts.map +1 -0
  184. package/lib/mjs/primary-keys.js +223 -0
  185. package/lib/mjs/primary-keys.js.map +1 -0
  186. package/lib/mjs/relation-objects.d.ts +101 -9
  187. package/lib/mjs/relation-objects.d.ts.map +1 -1
  188. package/lib/mjs/relation-objects.js +194 -43
  189. package/lib/mjs/relation-objects.js.map +1 -1
  190. package/lib/mjs/relations.d.ts +7 -1
  191. package/lib/mjs/relations.d.ts.map +1 -1
  192. package/lib/mjs/relations.js +16 -1
  193. package/lib/mjs/relations.js.map +1 -1
  194. package/lib/mjs/resilience.d.ts +50 -0
  195. package/lib/mjs/resilience.d.ts.map +1 -0
  196. package/lib/mjs/resilience.js +64 -0
  197. package/lib/mjs/resilience.js.map +1 -0
  198. package/lib/mjs/snapshot.d.ts +58 -0
  199. package/lib/mjs/snapshot.d.ts.map +1 -0
  200. package/lib/mjs/snapshot.js +115 -0
  201. package/lib/mjs/snapshot.js.map +1 -0
  202. package/lib/mjs/statements.d.ts +11 -1
  203. package/lib/mjs/statements.d.ts.map +1 -1
  204. package/lib/mjs/statements.js +14 -3
  205. package/lib/mjs/statements.js.map +1 -1
  206. package/lib/mjs/subject-builder.d.ts +79 -0
  207. package/lib/mjs/subject-builder.d.ts.map +1 -0
  208. package/lib/mjs/subject-builder.js +292 -0
  209. package/lib/mjs/subject-builder.js.map +1 -0
  210. package/lib/mjs/subject-executor.d.ts +122 -0
  211. package/lib/mjs/subject-executor.d.ts.map +1 -0
  212. package/lib/mjs/subject-executor.js +312 -0
  213. package/lib/mjs/subject-executor.js.map +1 -0
  214. package/lib/mjs/subject-sorter.d.ts +70 -0
  215. package/lib/mjs/subject-sorter.d.ts.map +1 -0
  216. package/lib/mjs/subject-sorter.js +168 -0
  217. package/lib/mjs/subject-sorter.js.map +1 -0
  218. package/lib/mjs/subject.d.ts +100 -0
  219. package/lib/mjs/subject.d.ts.map +1 -0
  220. package/lib/mjs/subject.js +84 -0
  221. package/lib/mjs/subject.js.map +1 -0
  222. package/lib/mjs/unit-of-work.d.ts +52 -0
  223. package/lib/mjs/unit-of-work.d.ts.map +1 -0
  224. package/lib/mjs/unit-of-work.js +166 -0
  225. package/lib/mjs/unit-of-work.js.map +1 -0
  226. package/lib/tsconfig.cjs.tsbuildinfo +1 -1
  227. package/lib/tsconfig.mjs.tsbuildinfo +1 -1
  228. package/package.json +9 -7
package/lib/cjs/model.js CHANGED
@@ -13,11 +13,14 @@ const hydrators_js_1 = require("./hydrators.js");
13
13
  const exceptions_js_1 = require("./exceptions.js");
14
14
  const dehydrators_js_1 = require("./dehydrators.js");
15
15
  const relation_objects_js_1 = require("./relation-objects.js");
16
+ const snapshot_js_1 = require("./snapshot.js");
17
+ const unit_of_work_js_1 = require("./unit-of-work.js");
16
18
  const di_1 = require("@spinajs/di");
17
19
  const luxon_1 = require("luxon");
18
20
  const lodash_1 = __importDefault(require("lodash"));
19
21
  const uuid_1 = require("uuid");
20
22
  const descriptor_js_1 = require("./descriptor.js");
23
+ const primary_keys_js_1 = require("./primary-keys.js");
21
24
  const MODEL_PROXY_HANDLER = {
22
25
  set: (target, p, value) => {
23
26
  if (target[p] !== value) {
@@ -44,8 +47,16 @@ function updateModelDescriptor(targetOrForward, callback) {
44
47
  if (!target) {
45
48
  return;
46
49
  }
47
- const metadata = Reflect.getMetadata(symbols_js_1.MODEL_DESCTRIPTION_SYMBOL, target);
48
- callback(metadata[target.name]);
50
+ // Must go through getInheritedDescriptor like every other write of this
51
+ // symbol - it hands back the class's OWN descriptor, so mutating it here
52
+ // cannot leak into the base class ( eg. assigning the driver to a subclass )
53
+ const descriptor = (0, di_1.getInheritedDescriptor)(target, symbols_js_1.MODEL_DESCTRIPTION_SYMBOL, descriptor_js_1.createDefaultModelDescriptor);
54
+ // Name is this class's own, never inherited from the base. Matters when this
55
+ // is the FIRST access for the class: the descriptor is created by collapsing
56
+ // the chain, and the merger keeps the ancestor's non-empty Name over the
57
+ // default '' ( eg. a subclass would be stored under its parent's name )
58
+ descriptor.Name = target.name;
59
+ callback(descriptor);
49
60
  }
50
61
  class ModelBase {
51
62
  /**
@@ -80,24 +91,41 @@ class ModelBase {
80
91
  }
81
92
  return this._container;
82
93
  }
94
+ /**
95
+ * Primary key column names of this model. One element for the common single-column case.
96
+ */
83
97
  get PrimaryKeyName() {
84
98
  return this.ModelDescriptor.PrimaryKey;
85
99
  }
100
+ /**
101
+ * Primary key value: a scalar for a single-column key, a tuple in key order for a composite key.
102
+ */
86
103
  get PrimaryKeyValue() {
87
- return this[this.PrimaryKeyName];
104
+ return (0, primary_keys_js_1.pkValueOf)(this, this.ModelDescriptor);
88
105
  }
106
+ /**
107
+ * Accepts a scalar for a single-column key, and an array in key order or an object keyed by
108
+ * column name for a composite key. Cascades the new value into loaded relations exactly as
109
+ * before, using the single-column relation key ( relations join on one column pair ).
110
+ */
89
111
  set PrimaryKeyValue(newVal) {
90
- this[this.PrimaryKeyName] = newVal;
112
+ (0, primary_keys_js_1.setPkValue)(this, this.ModelDescriptor, newVal);
91
113
  this.ModelDescriptor.Relations.forEach((r) => {
92
114
  const rel = this[r.Name];
93
115
  if (!rel)
94
116
  return;
117
+ // A relation's ForeignKey names ONE column, so it can only carry a single-column key.
118
+ // Cascading a composite parent key into children is not expressible and is skipped.
119
+ if ((0, primary_keys_js_1.isCompositePk)(this.ModelDescriptor)) {
120
+ return;
121
+ }
122
+ const scalar = (0, primary_keys_js_1.pkValueOf)(this, this.ModelDescriptor);
95
123
  switch (r.Type) {
96
124
  case interfaces_js_1.RelationType.One:
97
- rel[r.ForeignKey] = newVal;
125
+ rel[r.ForeignKey] = scalar;
98
126
  break;
99
127
  case interfaces_js_1.RelationType.Many:
100
- rel.forEach((rVal) => (rVal[r.ForeignKey] = newVal));
128
+ rel.forEach((rVal) => (rVal[r.ForeignKey] = scalar));
101
129
  break;
102
130
  case interfaces_js_1.RelationType.ManyToMany:
103
131
  // TODO: rethink this
@@ -105,6 +133,122 @@ class ModelBase {
105
133
  }
106
134
  });
107
135
  }
136
+ /**
137
+ * The diff baseline for this instance, or `null` when it has never been hydrated from
138
+ * the database. Read-only from the outside: mutate it only through `takeSnapshot()`,
139
+ * `snapshotRelation()` and `clearSnapshot()`.
140
+ */
141
+ get Snapshot() {
142
+ return this.__snapshot__;
143
+ }
144
+ /**
145
+ * Captures the current value of every column as the diff baseline, discarding any
146
+ * previous baseline and any relation keys recorded against it.
147
+ *
148
+ * Values are copied, never aliased — see `snapshotValue`. An aliased snapshot makes
149
+ * every diff empty and `save()` a silent no-op.
150
+ */
151
+ takeSnapshot() {
152
+ const snapshot = (0, snapshot_js_1.createSnapshot)();
153
+ for (const c of this.ModelDescriptor?.Columns ?? []) {
154
+ snapshot.Columns.set(c.Name, (0, snapshot_js_1.snapshotValue)(this[c.Name], c.Converter));
155
+ }
156
+ this.__snapshot__ = snapshot;
157
+ }
158
+ /**
159
+ * Records the primary keys of the members currently in relation `name` as that
160
+ * relation's baseline. A no-op when the model has no snapshot — an unhydrated model
161
+ * has nothing to diff against and its relations are all "new".
162
+ *
163
+ * @param name - relation property name, as declared on the model descriptor
164
+ */
165
+ snapshotRelation(name) {
166
+ if (!this.__snapshot__) {
167
+ return;
168
+ }
169
+ const relation = this[name];
170
+ if (relation === null || relation === undefined) {
171
+ return;
172
+ }
173
+ if (relation instanceof relation_objects_js_1.SingleRelation) {
174
+ const value = relation.Value;
175
+ this.__snapshot__.Relations.set(name, value ? [value.PrimaryKeyValue] : []);
176
+ return;
177
+ }
178
+ if (typeof relation[Symbol.iterator] === 'function') {
179
+ this.__snapshot__.Relations.set(name, [...relation].map((m) => m.PrimaryKeyValue));
180
+ }
181
+ }
182
+ /**
183
+ * Discards the diff baseline. After this the model is treated as brand new by `save()`.
184
+ */
185
+ clearSnapshot() {
186
+ this.__snapshot__ = null;
187
+ }
188
+ /**
189
+ * Names of the columns whose current value differs from the snapshot.
190
+ *
191
+ * With no snapshot every column is reported as changed, which is the right answer for a
192
+ * model that is about to be inserted.
193
+ *
194
+ * This is deliberately independent of `__dirty_props__`: the proxy records a property as
195
+ * dirty on any write, including one that puts the original value back, so the snapshot
196
+ * diff is the more precise answer and the one the UPDATE payload is built from.
197
+ */
198
+ changedColumns() {
199
+ const columns = this.ModelDescriptor?.Columns ?? [];
200
+ if (!this.__snapshot__) {
201
+ return columns.map((c) => c.Name);
202
+ }
203
+ const snapshot = this.__snapshot__;
204
+ return columns.filter((c) => !(0, snapshot_js_1.snapshotEquals)(snapshot.Columns.get(c.Name), this[c.Name], c.Converter)).map((c) => c.Name);
205
+ }
206
+ /**
207
+ * Foreign-key columns a relation object reported as rewritten, via {@link markDirty}.
208
+ *
209
+ * These are the only columns the snapshot diff cannot see. `SingleRelation.attach()` stores
210
+ * the new target on the relation wrapper and marks the foreign key dirty, but never writes
211
+ * the column on the model — the value is materialised from the relation later, by
212
+ * `StandardModelToSqlConverter`. So `changedColumns()` compares the column against its
213
+ * snapshot, finds it untouched, and reports no change even though the relation was
214
+ * re-pointed. Detaching a relation and calling `update()` would then emit nothing at all.
215
+ *
216
+ * Restricted to declared relation foreign keys on purpose. `__dirty_props__` also collects
217
+ * ordinary property writes, and folding those in wholesale would undo the precision the
218
+ * diff exists for: a column written A -> B -> A is in `__dirty_props__` but has no net
219
+ * change, and must not be written back.
220
+ */
221
+ relationDirtyColumns() {
222
+ const descriptor = this.ModelDescriptor;
223
+ if (!descriptor) {
224
+ return [];
225
+ }
226
+ const relationKeys = new Set();
227
+ for (const [, relation] of descriptor.Relations) {
228
+ if (relation.ForeignKey) {
229
+ relationKeys.add(relation.ForeignKey);
230
+ }
231
+ }
232
+ return this.__dirty_props__.filter((p) => relationKeys.has(p));
233
+ }
234
+ /**
235
+ * Records `prop` as changed and marks the model dirty.
236
+ *
237
+ * This is the supported way for relation objects to report that they rewrote one of the
238
+ * owner's foreign keys. It replaces the `(owner as any).__dirty_props__.push(...)` casts
239
+ * that reached into a private field from outside the class ( A6 ).
240
+ *
241
+ * The push comes before `IsDirty = true` so the method stays correct even if the
242
+ * `IsDirty` setter ever starts clearing `__dirty_props__` on a truthy assignment too.
243
+ *
244
+ * @param prop - column name
245
+ */
246
+ markDirty(prop) {
247
+ if (!this.__dirty_props__.includes(prop)) {
248
+ this.__dirty_props__.push(prop);
249
+ }
250
+ this.IsDirty = true;
251
+ }
108
252
  valueOf() {
109
253
  return this.PrimaryKeyValue;
110
254
  }
@@ -281,6 +425,11 @@ class ModelBase {
281
425
  static whereNotExists(_qOrR, _func) {
282
426
  throw new Error('Not implemented');
283
427
  }
428
+ /**
429
+ * Runs `_callback` inside a transaction on this model's connection. The transaction commits
430
+ * when the callback resolves and rolls back when it throws — see `OrmDriver.transaction`.
431
+ * Resolves with whatever the callback returned.
432
+ */
284
433
  static transaction(_callback) {
285
434
  throw new Error('Not implemented');
286
435
  }
@@ -290,6 +439,13 @@ class ModelBase {
290
439
  * prop to track model props that changeded since last update
291
440
  */
292
441
  this.__dirty_props__ = [];
442
+ /**
443
+ * Diff baseline, captured when this instance was hydrated from a database row.
444
+ * `null` means "this model has never been in the database", which is what `save()`
445
+ * uses to classify it as an INSERT — not the presence of a primary key, because
446
+ * `setDefaults()` pre-fills @Uuid keys on construction.
447
+ */
448
+ this.__snapshot__ = null;
293
449
  /**
294
450
  * List of hidden properties from JSON / dehydrations
295
451
  * eg. password field of user
@@ -318,26 +474,36 @@ class ModelBase {
318
474
  attach(data) {
319
475
  // TODO: refactor this, to not check every time for relation
320
476
  // do this as map or smth
321
- for (const [_, v] of this.ModelDescriptor.Relations.entries()) {
322
- if (v.TargetModel.name === data.constructor.name) {
323
- // TODO: refactor this, so we dont update foreign key
324
- // instead we must use belongsTo relation on data model to update
325
- //(data as any)[v.ForeignKey] = this.PrimaryKeyValue;
326
- switch (v.Type) {
327
- case interfaces_js_1.RelationType.One:
328
- this[v.Name].attach(data);
329
- this.__dirty_props__.push(v.ForeignKey);
330
- break;
331
- case interfaces_js_1.RelationType.Many:
332
- // attach to related model too
333
- const rel = [...data.ModelDescriptor.Relations.entries()].find((e) => e[1].ForeignKey === v.ForeignKey);
334
- if (rel) {
335
- data[rel[0]].Value = this;
336
- }
337
- case interfaces_js_1.RelationType.ManyToMany:
338
- this[v.Name].push(data);
339
- break;
477
+ for (const [, v] of this.ModelDescriptor.Relations.entries()) {
478
+ // Constructor identity, not class name. Name matching pushed the row into *every*
479
+ // relation whose target happened to share a name, so a model with two relations to
480
+ // the same target received it twice ( B26 ), and it breaks under minification ( A9 ).
481
+ if (v.TargetModel !== data.constructor) {
482
+ continue;
483
+ }
484
+ switch (v.Type) {
485
+ case interfaces_js_1.RelationType.One:
486
+ this[v.Name].attach(data);
487
+ this.markDirty(v.ForeignKey);
488
+ break;
489
+ case interfaces_js_1.RelationType.Many: {
490
+ // Set the child's back-reference to this owner, when the child declares one.
491
+ const rel = [...data.ModelDescriptor.Relations.entries()].find((e) => e[1].ForeignKey === v.ForeignKey);
492
+ if (rel) {
493
+ data[rel[0]].Value = this;
494
+ }
495
+ this[v.Name].push(data);
496
+ break;
340
497
  }
498
+ case interfaces_js_1.RelationType.ManyToMany:
499
+ // No back-reference: the link lives in the junction table, not on the target row.
500
+ // The `push` is written out again rather than shared with the Many case above —
501
+ // the two are only coincidentally similar, and the missing `break` that used to
502
+ // join them was one reordering away from silently breaking.
503
+ this[v.Name].push(data);
504
+ break;
505
+ default:
506
+ break;
341
507
  }
342
508
  }
343
509
  this.IsDirty = true;
@@ -374,10 +540,14 @@ class ModelBase {
374
540
  * deletes enitt from db. If model have SoftDelete decorator, model is marked as deleted
375
541
  */
376
542
  async destroy() {
377
- if (!this.PrimaryKeyValue) {
543
+ // A composite key is a tuple, and a tuple is ALWAYS truthy - so the old `!pk` guard
544
+ // would happily issue a DELETE for a model whose key columns are still unset.
545
+ const pk = this.PrimaryKeyValue;
546
+ const missing = Array.isArray(pk) ? pk.some((v) => v === null || v === undefined) : !pk;
547
+ if (missing) {
378
548
  return;
379
549
  }
380
- const result = await this.constructor.destroy(this.PrimaryKeyValue);
550
+ const result = await this.constructor.destroy(pk);
381
551
  this.IsDirty = false;
382
552
  return result;
383
553
  }
@@ -392,33 +562,64 @@ class ModelBase {
392
562
  throw new exceptions_js_1.OrmException('archived at column not exists in model');
393
563
  }
394
564
  const { query } = this.createUpdateQuery();
395
- return await query.update(this.toSql()).where(this.PrimaryKeyName, this.PrimaryKeyValue);
565
+ query.update(this.toSql());
566
+ (0, primary_keys_js_1.wherePk)(query, this.ModelDescriptor, this.PrimaryKeyValue);
567
+ return await query;
396
568
  }
569
+ /**
570
+ * Writes the columns that differ from the snapshot.
571
+ *
572
+ * The change set comes from `changedColumns()` — the snapshot diff — and not from the
573
+ * proxy's `__dirty_props__`, which records a property as dirty on ANY write including one
574
+ * that puts the original value straight back. `save()` has always used the diff, and
575
+ * `changedColumns()` documents it as the more precise answer; this path used the imprecise
576
+ * one, so the same edit produced a different UPDATE depending on which method you called.
577
+ *
578
+ * A model with no snapshot ( never hydrated ) reports every column as changed, which is the
579
+ * right answer: there is no baseline to be more precise than.
580
+ *
581
+ * @param data - optional patch hydrated onto the model first
582
+ */
397
583
  async update(data) {
398
- const { query } = this.createUpdateQuery();
399
- let result = {
584
+ const result = {
400
585
  RowsAffected: 0,
401
586
  LastInsertId: 0,
402
587
  };
403
588
  if (data) {
404
589
  this.hydrate(data);
405
590
  }
406
- // if no changes, return without update
407
- if (this.IsDirty === false) {
591
+ const keyColumns = this.ModelDescriptor.PrimaryKey ?? [];
592
+ const changed = lodash_1.default.union(this.changedColumns(), this.relationDirtyColumns()).filter((c) => !keyColumns.includes(c));
593
+ // Nothing to write. Checked against the diff, so re-assigning a column its current value
594
+ // no longer produces an UPDATE that sets it to what it already was.
595
+ if (changed.length === 0) {
596
+ this.IsDirty = false;
408
597
  return result;
409
598
  }
410
- if (this.ModelDescriptor.Timestamps.UpdatedAt) {
411
- this[this.ModelDescriptor.Timestamps.UpdatedAt] = luxon_1.DateTime.now();
599
+ const updatedAt = this.ModelDescriptor.Timestamps.UpdatedAt;
600
+ if (updatedAt) {
601
+ this[updatedAt] = luxon_1.DateTime.now();
602
+ if (!changed.includes(updatedAt)) {
603
+ changed.push(updatedAt);
604
+ }
412
605
  }
413
- result = await query.update(this.toSql(true)).where(this.PrimaryKeyName, this.PrimaryKeyValue);
606
+ const { query } = this.createUpdateQuery();
607
+ query.update(lodash_1.default.pick(this.toSql(), changed));
608
+ (0, primary_keys_js_1.wherePk)(query, this.ModelDescriptor, this.PrimaryKeyValue);
609
+ const updateResult = await query;
414
610
  this.IsDirty = false;
415
- return result;
611
+ this.takeSnapshot();
612
+ return updateResult;
416
613
  }
417
614
  /**
418
615
  * Save all changes to db. It creates new entry id db or updates existing one if
419
616
  * primary key exists
420
617
  */
421
618
  async insert(insertBehaviour = interfaces_js_1.InsertBehaviour.None) {
619
+ // Both run BEFORE the query is built, so an `assigned` key that was never supplied fails
620
+ // without touching the database.
621
+ (0, primary_keys_js_1.generateClientSideKeys)(this, this.ModelDescriptor);
622
+ (0, primary_keys_js_1.assertAssignedKeys)(this, this.ModelDescriptor);
422
623
  const { query, description } = this.createInsertQuery();
423
624
  const sResponseMapper = query.Container.resolve(interfaces_js_1.ServerResponseMapper);
424
625
  switch (insertBehaviour) {
@@ -432,14 +633,27 @@ class ModelBase {
432
633
  query.orReplace();
433
634
  break;
434
635
  }
636
+ // Only an `auto` key needs the database to tell us what it became. Asking for RETURNING
637
+ // where the dialect supports it beats reading an identity counter, and is the shape
638
+ // orm-uow needs to backfill cascaded children.
639
+ const needsKeyBack = (0, primary_keys_js_1.pkColumns)(description).some((c) => (0, primary_keys_js_1.pkGeneration)(description, c) === 'auto');
640
+ if (needsKeyBack && insertBehaviour !== interfaces_js_1.InsertBehaviour.InsertOrUpdate && query.Driver.supportedFeatures().insertReturning) {
641
+ query.returning((0, primary_keys_js_1.pkColumns)(description));
642
+ }
435
643
  query.middleware({
436
644
  afterQuery: (data) => {
437
- const response = sResponseMapper.read(data, this.PrimaryKeyName);
438
- // if already exists do not overwrite
439
- // sometimes we have models with primary key as string etc
440
- // and not autoincrement
441
- if (!this.PrimaryKeyValue) {
442
- this.PrimaryKeyValue = response.LastInsertId;
645
+ const response = sResponseMapper.read(data, (0, primary_keys_js_1.pkColumns)(description));
646
+ if ((response.Returning ?? []).length !== 0) {
647
+ (0, primary_keys_js_1.setPkValue)(this, description, (0, primary_keys_js_1.pkValueOf)(response.Returning[0], description));
648
+ }
649
+ else if (needsKeyBack) {
650
+ // Do not overwrite a key the caller already supplied ( uuid / assigned strategies ).
651
+ // Same tuple-truthiness trap as destroy().
652
+ const current = this.PrimaryKeyValue;
653
+ const missing = Array.isArray(current) ? current.some((v) => v === null || v === undefined) : !current;
654
+ if (missing) {
655
+ this.PrimaryKeyValue = response.LastInsertId;
656
+ }
443
657
  }
444
658
  return data;
445
659
  },
@@ -463,6 +677,26 @@ class ModelBase {
463
677
  }
464
678
  return await this.insert();
465
679
  }
680
+ /**
681
+ * Persists this model and everything reachable from it in one transaction.
682
+ *
683
+ * The graph is diffed against the snapshots taken when it was loaded, sorted so that a
684
+ * parent is inserted before any child that references it, and executed as inserts, then
685
+ * updates restricted to the columns that actually changed, then junction rows, then the
686
+ * orphan policy of every relation that lost a member.
687
+ *
688
+ * A relation that was never populated is invisible: `Items: OrderItem[] = []` on a freshly
689
+ * constructed model deletes nothing. That is the deliberate divergence from TypeORM.
690
+ *
691
+ * @param options - `{ reload: true }` to diff against current database state instead of the
692
+ * hydration snapshot; `{ chunk: n }` to bound batched statement size.
693
+ */
694
+ async save(options) {
695
+ // `UnitOfWork` is referenced only inside this body, never at module-evaluation time, so
696
+ // the model.ts -> unit-of-work.ts -> ... -> model.ts cycle stays safe under ESM. Do not
697
+ // move this into a field initializer or an extends clause.
698
+ return await unit_of_work_js_1.UnitOfWork.save(this, options);
699
+ }
466
700
  /**
467
701
  * Gets model data from database and returns as fresh instance.
468
702
  *
@@ -489,6 +723,7 @@ class ModelBase {
489
723
  for (const c of this.ModelDescriptor.Columns) {
490
724
  this[c.Name] = model[c.Name];
491
725
  }
726
+ this.IsDirty = false;
492
727
  }
493
728
  toJSON() {
494
729
  return this.dehydrate();
@@ -505,6 +740,9 @@ class ModelBase {
505
740
  this[c.Name] = c.DefaultValue;
506
741
  }
507
742
  });
743
+ // `uuid` primary keys are generated at construction so the value is available to callers
744
+ // and to cascaded children before the row ever reaches the database.
745
+ (0, primary_keys_js_1.generateClientSideKeys)(this, this.ModelDescriptor);
508
746
  if (this.ModelDescriptor.Timestamps.CreatedAt) {
509
747
  this[this.ModelDescriptor.Timestamps.CreatedAt] = luxon_1.DateTime.now();
510
748
  }
@@ -539,9 +777,60 @@ class ModelBase {
539
777
  }
540
778
  }
541
779
  exports.ModelBase = ModelBase;
780
+ /**
781
+ * Decides whether a multi-row insert's generated keys can be read off `LastInsertId + index` on a
782
+ * dialect that has no RETURNING.
783
+ *
784
+ * The premise is a documented MySQL guarantee, not a guess. InnoDB splits inserts into *simple*
785
+ * ones — row count known before execution, which is exactly what `INSERT INTO t (…) VALUES (…),
786
+ * (…)` is, and the only shape {@link InsertQueryBuilder} can build — and *bulk* ones
787
+ * (`INSERT … SELECT`), where it is not. For a simple insert InnoDB reserves one contiguous block
788
+ * of N auto-increment values under a short mutex it releases immediately, so the k-th row of the
789
+ * statement gets `LAST_INSERT_ID() + k`. This holds under `innodb_autoinc_lock_mode = 2`, the
790
+ * MySQL 8 default, and was verified against a live server. The "values may not be contiguous"
791
+ * caveat in the MySQL manual is about bulk inserts and about mixed-mode inserts.
792
+ *
793
+ * The guards below rule out every case where the mapping stops being positional.
794
+ */
795
+ function _canBackfillContiguousKeys(description, rows, response, features, insertBehaviour) {
796
+ // Only a dialect whose reported id is the FIRST of the block can be walked forwards. MSSQL's
797
+ // SCOPE_IDENTITY() and SQLite's last_insert_rowid() report the LAST one.
798
+ if (!features.insertIdIsFirstOfBatch) {
799
+ return false;
800
+ }
801
+ // One database-generated identity column, or there is no counter to walk. A composite key has
802
+ // no single identity column, and uuid / assigned keys are already set by this point.
803
+ const keys = (0, primary_keys_js_1.pkColumns)(description);
804
+ if (keys.length !== 1 || (0, primary_keys_js_1.pkGeneration)(description, keys[0]) !== 'auto') {
805
+ return false;
806
+ }
807
+ // INSERT IGNORE / REPLACE / ON DUPLICATE KEY UPDATE are mixed-mode: rows can be skipped,
808
+ // replaced or updated rather than inserted, so the k-th allocated id stops belonging to the
809
+ // k-th input row. ( The array path rejects these outright today; this keeps the invariant
810
+ // local to the decision rather than depending on a check three hundred lines away. )
811
+ if (insertBehaviour !== interfaces_js_1.InsertBehaviour.None) {
812
+ return false;
813
+ }
814
+ // No identity value reported at all.
815
+ if (typeof response.LastInsertId !== 'number' || !Number.isFinite(response.LastInsertId) || response.LastInsertId <= 0) {
816
+ return false;
817
+ }
818
+ // The server must confirm it inserted exactly one row per row we sent. Anything else means
819
+ // rows were skipped or the statement did something other than a plain multi-row insert.
820
+ if (response.RowsAffected !== rows.length) {
821
+ return false;
822
+ }
823
+ // A batch where SOME rows carry an explicit key is a mixed-mode insert: InnoDB allocates
824
+ // auto-increment values only for the rows that omitted one, so index arithmetic would both
825
+ // mis-key the generated rows and overwrite the supplied ones.
826
+ return rows.every((v) => v instanceof ModelBase && (v.PrimaryKeyValue === null || v.PrimaryKeyValue === undefined));
827
+ }
542
828
  function _preparePkWhere(description, query, model) {
543
- if (description.PrimaryKey) {
544
- query.where(description.PrimaryKey, model.PrimaryKeyValue);
829
+ // NOTE: `if (description.PrimaryKey)` used to be false for the '' default. An empty ARRAY is
830
+ // truthy, so this must be an explicit length check or no-primary-key models would stop
831
+ // falling back to their unique columns.
832
+ if ((0, primary_keys_js_1.hasPk)(description)) {
833
+ (0, primary_keys_js_1.wherePk)(query, description, model.PrimaryKeyValue);
545
834
  }
546
835
  else {
547
836
  const unique = description.Columns.filter((x) => x.Unique);
@@ -556,20 +845,20 @@ function _preparePkWhere(description, query, model) {
556
845
  }
557
846
  }
558
847
  function _prepareOrderBy(description, query, order) {
559
- if (description.PrimaryKey) {
560
- query.order(description.PrimaryKey, order ?? enums_js_1.SortOrder.DESC);
848
+ // orderByPk emits one ORDER BY term per key column and reports whether the model has a
849
+ // primary key at all - see the note in _preparePkWhere on why a length check is required.
850
+ if ((0, primary_keys_js_1.orderByPk)(query, description, order ?? enums_js_1.SortOrder.DESC)) {
851
+ return;
561
852
  }
562
- else {
563
- const unique = description.Columns.filter((c) => c.Unique);
564
- if (unique.length !== 0) {
565
- unique.forEach((c) => query.order(c.Name, order ?? enums_js_1.SortOrder.DESC));
566
- }
567
- else if (description.Timestamps?.CreatedAt) {
568
- query.order(description.Timestamps.CreatedAt, order ?? enums_js_1.SortOrder.DESC);
569
- }
570
- else if (description.Timestamps?.UpdatedAt) {
571
- query.order(description.Timestamps.UpdatedAt, order ?? enums_js_1.SortOrder.DESC);
572
- }
853
+ const unique = description.Columns.filter((c) => c.Unique);
854
+ if (unique.length !== 0) {
855
+ unique.forEach((c) => query.order(c.Name, order ?? enums_js_1.SortOrder.DESC));
856
+ }
857
+ else if (description.Timestamps?.CreatedAt) {
858
+ query.order(description.Timestamps.CreatedAt, order ?? enums_js_1.SortOrder.DESC);
859
+ }
860
+ else if (description.Timestamps?.UpdatedAt) {
861
+ query.order(description.Timestamps.UpdatedAt, order ?? enums_js_1.SortOrder.DESC);
573
862
  }
574
863
  }
575
864
  class HistoricalModel {
@@ -610,6 +899,8 @@ exports.MODEL_STATIC_MIXINS = {
610
899
  }
611
900
  return driver;
612
901
  },
902
+ // Every branch now returns a builder or throws, so the `| undefined` is gone — it only ever
903
+ // described the unimplemented ManyToMany case. This matches ModelBase.populate's static stub.
613
904
  populate(relation, owner) {
614
905
  //TODO: fix cast
615
906
  const modelDescriptor = this.getModelDescriptor();
@@ -630,7 +921,7 @@ exports.MODEL_STATIC_MIXINS = {
630
921
  async afterHydration(_relationData) { },
631
922
  };
632
923
  switch (relationDescriptor.Type) {
633
- case interfaces_js_1.RelationType.One:
924
+ case interfaces_js_1.RelationType.One: {
634
925
  const { query: JoinQuery } = (0, builders_js_1.createQuery)(relationDescriptor.SourceModel, builders_js_1.SelectQueryBuilder);
635
926
  // NOTE: we could use simple right join, but we use LEFT JOIN
636
927
  // becouse sqlite does not support right join
@@ -640,19 +931,52 @@ exports.MODEL_STATIC_MIXINS = {
640
931
  joinModel: relationDescriptor.TargetModel,
641
932
  queryCallback: function () {
642
933
  this.select(new builders_js_1.RawQuery(`\`${this.TableAlias}\`.*`));
643
- }
934
+ },
935
+ // Both were omitted, so the join compiled to `$source$.undefined`. A belongsTo
936
+ // joins the owner's ForeignKey to the target's PrimaryKey — the same derivation
937
+ // SelectQueryBuilder uses for RelationType.One.
938
+ sourceTablePrimaryKey: relationDescriptor.ForeignKey,
939
+ joinTableForeignKey: relationDescriptor.PrimaryKey,
644
940
  });
645
- JoinQuery.where(relationDescriptor.SourceModel.getModelDescriptor().PrimaryKey, owner);
941
+ // `wherePk`, not `where(descriptor.PrimaryKey, ...)`: PrimaryKey is a string[] since
942
+ // composite keys landed, and passing the array as a column name compiled to
943
+ // `column 0 not exists in model ...`.
944
+ (0, primary_keys_js_1.wherePk)(JoinQuery, relationDescriptor.SourceModel.getModelDescriptor(), owner instanceof ModelBase ? owner.PrimaryKeyValue : owner);
646
945
  JoinQuery.middleware(hydrateMiddleware);
647
946
  return JoinQuery;
648
- case interfaces_js_1.RelationType.ManyToMany:
649
- break;
947
+ }
948
+ case interfaces_js_1.RelationType.ManyToMany: {
949
+ // Was a bare `break`, so this returned undefined and every caller crashed on the
950
+ // result. Read the junction table, join the target, and project the target's columns
951
+ // — the same shape ManyToManyRelation.compile() builds. A sub-query would be more
952
+ // direct but `whereIn` takes value arrays only.
953
+ if (!relationDescriptor.JunctionModel) {
954
+ throw new exceptions_js_1.OrmException(`relation ${relation} on ${modelDescriptor.Name} has no junction model`);
955
+ }
956
+ const { query: junctionQuery } = (0, builders_js_1.createQuery)(relationDescriptor.JunctionModel, builders_js_1.SelectQueryBuilder);
957
+ junctionQuery.clearColumns();
958
+ junctionQuery.leftJoin({
959
+ joinModel: relationDescriptor.TargetModel,
960
+ queryCallback: function () {
961
+ this.select(new builders_js_1.RawQuery(`\`${this.TableAlias}\`.*`));
962
+ },
963
+ sourceTablePrimaryKey: relationDescriptor.JunctionModelTargetModelFKey_Name,
964
+ joinTableForeignKey: relationDescriptor.ForeignKey,
965
+ });
966
+ junctionQuery.where(relationDescriptor.JunctionModelSourceModelFKey_Name, owner instanceof ModelBase ? owner.PrimaryKeyValue : owner);
967
+ junctionQuery.middleware(hydrateMiddleware);
968
+ return junctionQuery;
969
+ }
970
+ case interfaces_js_1.RelationType.Virtual:
650
971
  case interfaces_js_1.RelationType.Query:
651
972
  throw new exceptions_js_1.OrmException(`Query population for relation type ${interfaces_js_1.RelationType[relationDescriptor.Type]} is not supported yet`);
652
- case interfaces_js_1.RelationType.Many:
973
+ case interfaces_js_1.RelationType.Many: {
653
974
  const { query } = (0, builders_js_1.createQuery)(relationDescriptor.TargetModel, builders_js_1.SelectQueryBuilder);
654
975
  query.where(relationDescriptor.ForeignKey, owner instanceof ModelBase ? owner.PrimaryKeyValue : owner);
655
976
  return query;
977
+ }
978
+ default:
979
+ throw new exceptions_js_1.OrmException(`unknown relation type ${relationDescriptor.Type} for relation ${relation} on ${modelDescriptor.Name}`);
656
980
  }
657
981
  },
658
982
  query() {
@@ -698,6 +1022,12 @@ exports.MODEL_STATIC_MIXINS = {
698
1022
  if (insertBehaviour !== interfaces_js_1.InsertBehaviour.None) {
699
1023
  throw new exceptions_js_1.OrmException(`insert behaviour is not supported with arrays`);
700
1024
  }
1025
+ // Run the key strategies over every element BEFORE any SQL is built, so a missing
1026
+ // `assigned` key fails with a clear message instead of a NOT NULL violation.
1027
+ data.forEach((d) => {
1028
+ (0, primary_keys_js_1.generateClientSideKeys)(d, description);
1029
+ (0, primary_keys_js_1.assertAssignedKeys)(d, description);
1030
+ });
701
1031
  query.values(data.map((d) => {
702
1032
  if (d instanceof ModelBase) {
703
1033
  return d.toSql();
@@ -717,6 +1047,8 @@ exports.MODEL_STATIC_MIXINS = {
717
1047
  query.orReplace();
718
1048
  break;
719
1049
  }
1050
+ (0, primary_keys_js_1.generateClientSideKeys)(data, description);
1051
+ (0, primary_keys_js_1.assertAssignedKeys)(data, description);
720
1052
  if (data instanceof ModelBase) {
721
1053
  query.values(data.toSql());
722
1054
  }
@@ -724,19 +1056,40 @@ exports.MODEL_STATIC_MIXINS = {
724
1056
  query.values(converter.toSql(data, description));
725
1057
  }
726
1058
  }
1059
+ const autoKey = (0, primary_keys_js_1.pkColumns)(description).some((c) => (0, primary_keys_js_1.pkGeneration)(description, c) === 'auto');
1060
+ if (autoKey && query.Driver.supportedFeatures().insertReturning) {
1061
+ query.returning((0, primary_keys_js_1.pkColumns)(description));
1062
+ }
727
1063
  const iMidleware = {
728
1064
  afterQuery: (result) => {
729
- const response = sResponseMapper.read(result);
730
- if (Array.isArray(data)) {
731
- data.forEach((v, idx) => {
1065
+ const response = sResponseMapper.read(result, (0, primary_keys_js_1.pkColumns)(description));
1066
+ const rows = Array.isArray(data) ? data : [data];
1067
+ if ((response.Returning ?? []).length === rows.length) {
1068
+ // Authoritative: the database told us every key it assigned, in insert order.
1069
+ rows.forEach((v, idx) => {
732
1070
  if (v instanceof ModelBase) {
733
- v.PrimaryKeyValue = v.PrimaryKeyValue ?? response.LastInsertId + idx;
1071
+ (0, primary_keys_js_1.setPkValue)(v, description, (0, primary_keys_js_1.pkValueOf)(response.Returning[idx], description));
734
1072
  }
735
1073
  });
736
1074
  }
737
- else if (data instanceof ModelBase) {
738
- data.PrimaryKeyValue = data.PrimaryKeyValue ?? response.LastInsertId;
1075
+ else if (autoKey && rows.length === 1) {
1076
+ // One row, one identity value - safe.
1077
+ const v = rows[0];
1078
+ if (v instanceof ModelBase && !v.PrimaryKeyValue) {
1079
+ v.PrimaryKeyValue = response.LastInsertId;
1080
+ }
1081
+ }
1082
+ else if (_canBackfillContiguousKeys(description, rows, response, query.Driver.supportedFeatures(), insertBehaviour)) {
1083
+ // Multi-row `INSERT ... VALUES` on a dialect with no RETURNING whose identity value is
1084
+ // the first of the statement's contiguous block. See _canBackfillContiguousKeys.
1085
+ rows.forEach((v, idx) => {
1086
+ v.PrimaryKeyValue = response.LastInsertId + idx;
1087
+ });
739
1088
  }
1089
+ // Anything else — a dialect whose insert id names the last row, a batch that mixed
1090
+ // supplied and generated keys, a statement the server did not insert one row per input
1091
+ // row for — cannot be mapped positionally, so nothing is assigned. Callers needing the
1092
+ // keys there must re-select or insert the models one at a time.
740
1093
  return result;
741
1094
  },
742
1095
  modelCreation: () => null,
@@ -747,16 +1100,14 @@ exports.MODEL_STATIC_MIXINS = {
747
1100
  },
748
1101
  async find(pks) {
749
1102
  const { query, description } = (0, builders_js_1.createQuery)(this, builders_js_1.SelectQueryBuilder);
750
- const pkey = description.PrimaryKey;
751
1103
  query.select('*');
752
- query.whereIn(pkey, pks);
1104
+ (0, primary_keys_js_1.whereAnyPk)(query, description, pks);
753
1105
  return await query;
754
1106
  },
755
1107
  async findOrFail(pks) {
756
1108
  const { query, description, model } = (0, builders_js_1.createQuery)(this, builders_js_1.SelectQueryBuilder);
757
- const pkey = description.PrimaryKey;
758
1109
  query.select('*');
759
- query.whereIn(pkey, pks);
1110
+ (0, primary_keys_js_1.whereAnyPk)(query, description, pks);
760
1111
  const result = await query;
761
1112
  if (result.length !== pks.length) {
762
1113
  throw new Error(`could not find all results for model ${model.name}`);
@@ -765,22 +1116,23 @@ exports.MODEL_STATIC_MIXINS = {
765
1116
  },
766
1117
  async get(pk) {
767
1118
  const { query, description } = (0, builders_js_1.createQuery)(this, builders_js_1.SelectQueryBuilder);
768
- const pkey = description.PrimaryKey;
769
1119
  query.select('*');
770
- query.where(pkey, pk);
1120
+ (0, primary_keys_js_1.wherePk)(query, description, pk);
771
1121
  _prepareOrderBy(description, query);
772
1122
  return (await query.first());
773
1123
  },
774
1124
  async getOrFail(pk) {
775
1125
  const { query, description } = (0, builders_js_1.createQuery)(this, builders_js_1.SelectQueryBuilder);
776
- const pkey = description.PrimaryKey;
777
1126
  query.select('*');
778
- query.where(pkey, pk);
1127
+ (0, primary_keys_js_1.wherePk)(query, description, pk);
779
1128
  _prepareOrderBy(description, query);
780
1129
  return (await query.firstOrFail());
781
1130
  },
782
1131
  destroy(pks) {
783
1132
  const description = (0, builders_js_1._descriptor)(this);
1133
+ if (pks === undefined || pks === null) {
1134
+ throw new exceptions_js_1.OrmException('Cannot destroy without primary keys ( unbounded DELETE/UPDATE ). Use truncate() to clear the whole table.');
1135
+ }
784
1136
  const data = Array.isArray(pks) ? pks : [pks];
785
1137
  if (data.length === 0) {
786
1138
  throw new exceptions_js_1.OrmException('Cannot delete empty array of primary keys');
@@ -792,7 +1144,7 @@ exports.MODEL_STATIC_MIXINS = {
792
1144
  });
793
1145
  }
794
1146
  if (pks) {
795
- query.whereIn(description.PrimaryKey, data);
1147
+ (0, primary_keys_js_1.whereAnyPk)(query, description, data);
796
1148
  }
797
1149
  return query;
798
1150
  },
@@ -804,8 +1156,8 @@ exports.MODEL_STATIC_MIXINS = {
804
1156
  async getOrCreate(pk, data) {
805
1157
  const { query, description } = (0, builders_js_1.createQuery)(this, builders_js_1.SelectQueryBuilder);
806
1158
  // pk constrain
807
- if (description.PrimaryKey && pk !== null) {
808
- query.where(description.PrimaryKey, pk);
1159
+ if ((0, primary_keys_js_1.hasPk)(description) && pk !== null) {
1160
+ (0, primary_keys_js_1.wherePk)(query, description, pk);
809
1161
  }
810
1162
  // check for all unique columns ( unique constrain )
811
1163
  description.Columns.filter((c) => c.Unique).forEach((c) => {
@@ -834,13 +1186,15 @@ exports.MODEL_STATIC_MIXINS = {
834
1186
  let entity = (await query.first());
835
1187
  if (!entity) {
836
1188
  const toHydrate = data ?? {};
837
- const primaryKey = description.Columns.find((c) => c.PrimaryKey);
838
- // remove primary key from data to hydrate
839
- // we dont want to set primary key on new model if not exists
840
- // and autoincrement is set
841
- if (primaryKey?.AutoIncrement) {
842
- delete toHydrate[description.PrimaryKey];
843
- }
1189
+ // Do not carry an auto-increment key into a brand new model; the engine assigns it.
1190
+ // Every key column is checked, not just the first, so a composite key with an
1191
+ // auto-increment member is stripped correctly.
1192
+ (0, primary_keys_js_1.pkColumns)(description).forEach((name) => {
1193
+ const col = description.Columns.find((c) => c.Name === name);
1194
+ if (col?.AutoIncrement) {
1195
+ delete toHydrate[name];
1196
+ }
1197
+ });
844
1198
  entity = new (Function.prototype.bind.apply(this))(toHydrate);
845
1199
  return entity;
846
1200
  }
@@ -849,10 +1203,12 @@ exports.MODEL_STATIC_MIXINS = {
849
1203
  async exists(pk) {
850
1204
  const { query, description } = (0, builders_js_1.createQuery)(this, builders_js_1.SelectQueryBuilder);
851
1205
  // pk constrain
852
- if (description.PrimaryKey && pk !== null) {
853
- query.where(description.PrimaryKey, pk);
1206
+ if ((0, primary_keys_js_1.hasPk)(description) && pk !== null) {
1207
+ (0, primary_keys_js_1.wherePk)(query, description, pk);
854
1208
  }
855
- const result = await query.clearColumns().select(description.PrimaryKey).first();
1209
+ const q = query.clearColumns();
1210
+ (0, primary_keys_js_1.pkColumns)(description).forEach((c) => q.select(c));
1211
+ const result = await q.first();
856
1212
  if (result) {
857
1213
  return true;
858
1214
  }
@@ -916,7 +1272,8 @@ exports.MODEL_STATIC_MIXINS = {
916
1272
  if (callback) {
917
1273
  callback(query);
918
1274
  }
919
- return await (await query.asRaw()).count;
1275
+ const row = await query.takeFirst().asRaw();
1276
+ return row?.count ?? 0;
920
1277
  },
921
1278
  async transaction(callback) {
922
1279
  const driver = this.getModelDescriptor();
@@ -924,8 +1281,7 @@ exports.MODEL_STATIC_MIXINS = {
924
1281
  }
925
1282
  };
926
1283
  const _modelProxyFactory = (_c, model) => {
927
- const mInstance = new model();
928
- return new Proxy(mInstance, MODEL_PROXY_HANDLER);
1284
+ return new model();
929
1285
  };
930
1286
  exports._modelProxyFactory = _modelProxyFactory;
931
1287
  di_1.DI.register(exports._modelProxyFactory).as('__orm_model_factory__');