@spinajs/orm 2.0.481 → 2.0.484

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