@spinajs/orm 2.0.480 → 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.
- package/README.md +72 -3
- package/lib/cjs/bootstrap.js +10 -0
- package/lib/cjs/bootstrap.js.map +1 -1
- package/lib/cjs/builders.d.ts +115 -11
- package/lib/cjs/builders.d.ts.map +1 -1
- package/lib/cjs/builders.js +342 -283
- package/lib/cjs/builders.js.map +1 -1
- package/lib/cjs/converters.d.ts +9 -2
- package/lib/cjs/converters.d.ts.map +1 -1
- package/lib/cjs/converters.js +48 -8
- package/lib/cjs/converters.js.map +1 -1
- package/lib/cjs/decorators.d.ts +26 -3
- package/lib/cjs/decorators.d.ts.map +1 -1
- package/lib/cjs/decorators.js +95 -42
- package/lib/cjs/decorators.js.map +1 -1
- package/lib/cjs/dehydrators.js +1 -1
- package/lib/cjs/dehydrators.js.map +1 -1
- package/lib/cjs/descriptor.d.ts +1 -0
- package/lib/cjs/descriptor.d.ts.map +1 -1
- package/lib/cjs/descriptor.js +16 -40
- package/lib/cjs/descriptor.js.map +1 -1
- package/lib/cjs/driver.d.ts +136 -7
- package/lib/cjs/driver.d.ts.map +1 -1
- package/lib/cjs/driver.js +279 -0
- package/lib/cjs/driver.js.map +1 -1
- package/lib/cjs/existsRelationHandlers.d.ts +29 -0
- package/lib/cjs/existsRelationHandlers.d.ts.map +1 -0
- package/lib/cjs/existsRelationHandlers.js +104 -0
- package/lib/cjs/existsRelationHandlers.js.map +1 -0
- package/lib/cjs/fp.d.ts.map +1 -1
- package/lib/cjs/fp.js +10 -15
- package/lib/cjs/fp.js.map +1 -1
- package/lib/cjs/hydrators.d.ts.map +1 -1
- package/lib/cjs/hydrators.js +29 -2
- package/lib/cjs/hydrators.js.map +1 -1
- package/lib/cjs/identity-map.d.ts +60 -0
- package/lib/cjs/identity-map.d.ts.map +1 -0
- package/lib/cjs/identity-map.js +135 -0
- package/lib/cjs/identity-map.js.map +1 -0
- package/lib/cjs/index.d.ts +12 -0
- package/lib/cjs/index.d.ts.map +1 -1
- package/lib/cjs/index.js +12 -0
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/interfaces.d.ts +295 -9
- package/lib/cjs/interfaces.d.ts.map +1 -1
- package/lib/cjs/interfaces.js +54 -1
- package/lib/cjs/interfaces.js.map +1 -1
- package/lib/cjs/metadata.d.ts.map +1 -1
- package/lib/cjs/metadata.js +4 -1
- package/lib/cjs/metadata.js.map +1 -1
- package/lib/cjs/metrics.d.ts +53 -0
- package/lib/cjs/metrics.d.ts.map +1 -0
- package/lib/cjs/metrics.js +82 -0
- package/lib/cjs/metrics.js.map +1 -0
- package/lib/cjs/middlewares.d.ts +13 -1
- package/lib/cjs/middlewares.d.ts.map +1 -1
- package/lib/cjs/middlewares.js +55 -15
- package/lib/cjs/middlewares.js.map +1 -1
- package/lib/cjs/model.d.ts +129 -13
- package/lib/cjs/model.d.ts.map +1 -1
- package/lib/cjs/model.js +449 -93
- package/lib/cjs/model.js.map +1 -1
- package/lib/cjs/orm.d.ts.map +1 -1
- package/lib/cjs/orm.js +12 -3
- package/lib/cjs/orm.js.map +1 -1
- package/lib/cjs/orphan.d.ts +27 -0
- package/lib/cjs/orphan.d.ts.map +1 -0
- package/lib/cjs/orphan.js +55 -0
- package/lib/cjs/orphan.js.map +1 -0
- package/lib/cjs/primary-keys.d.ts +60 -0
- package/lib/cjs/primary-keys.d.ts.map +1 -0
- package/lib/cjs/primary-keys.js +243 -0
- package/lib/cjs/primary-keys.js.map +1 -0
- package/lib/cjs/relation-objects.d.ts +101 -9
- package/lib/cjs/relation-objects.d.ts.map +1 -1
- package/lib/cjs/relation-objects.js +194 -43
- package/lib/cjs/relation-objects.js.map +1 -1
- package/lib/cjs/relations.d.ts +7 -1
- package/lib/cjs/relations.d.ts.map +1 -1
- package/lib/cjs/relations.js +16 -1
- package/lib/cjs/relations.js.map +1 -1
- package/lib/cjs/resilience.d.ts +50 -0
- package/lib/cjs/resilience.d.ts.map +1 -0
- package/lib/cjs/resilience.js +70 -0
- package/lib/cjs/resilience.js.map +1 -0
- package/lib/cjs/snapshot.d.ts +58 -0
- package/lib/cjs/snapshot.d.ts.map +1 -0
- package/lib/cjs/snapshot.js +125 -0
- package/lib/cjs/snapshot.js.map +1 -0
- package/lib/cjs/statements.d.ts +11 -1
- package/lib/cjs/statements.d.ts.map +1 -1
- package/lib/cjs/statements.js +13 -2
- package/lib/cjs/statements.js.map +1 -1
- package/lib/cjs/subject-builder.d.ts +79 -0
- package/lib/cjs/subject-builder.d.ts.map +1 -0
- package/lib/cjs/subject-builder.js +296 -0
- package/lib/cjs/subject-builder.js.map +1 -0
- package/lib/cjs/subject-executor.d.ts +122 -0
- package/lib/cjs/subject-executor.d.ts.map +1 -0
- package/lib/cjs/subject-executor.js +319 -0
- package/lib/cjs/subject-executor.js.map +1 -0
- package/lib/cjs/subject-sorter.d.ts +70 -0
- package/lib/cjs/subject-sorter.d.ts.map +1 -0
- package/lib/cjs/subject-sorter.js +173 -0
- package/lib/cjs/subject-sorter.js.map +1 -0
- package/lib/cjs/subject.d.ts +100 -0
- package/lib/cjs/subject.d.ts.map +1 -0
- package/lib/cjs/subject.js +89 -0
- package/lib/cjs/subject.js.map +1 -0
- package/lib/cjs/unit-of-work.d.ts +52 -0
- package/lib/cjs/unit-of-work.d.ts.map +1 -0
- package/lib/cjs/unit-of-work.js +170 -0
- package/lib/cjs/unit-of-work.js.map +1 -0
- package/lib/mjs/bootstrap.js +10 -0
- package/lib/mjs/bootstrap.js.map +1 -1
- package/lib/mjs/builders.d.ts +115 -11
- package/lib/mjs/builders.d.ts.map +1 -1
- package/lib/mjs/builders.js +344 -285
- package/lib/mjs/builders.js.map +1 -1
- package/lib/mjs/converters.d.ts +9 -2
- package/lib/mjs/converters.d.ts.map +1 -1
- package/lib/mjs/converters.js +48 -8
- package/lib/mjs/converters.js.map +1 -1
- package/lib/mjs/decorators.d.ts +26 -3
- package/lib/mjs/decorators.d.ts.map +1 -1
- package/lib/mjs/decorators.js +96 -44
- package/lib/mjs/decorators.js.map +1 -1
- package/lib/mjs/dehydrators.js +1 -1
- package/lib/mjs/dehydrators.js.map +1 -1
- package/lib/mjs/descriptor.d.ts +1 -0
- package/lib/mjs/descriptor.d.ts.map +1 -1
- package/lib/mjs/descriptor.js +17 -39
- package/lib/mjs/descriptor.js.map +1 -1
- package/lib/mjs/driver.d.ts +136 -7
- package/lib/mjs/driver.d.ts.map +1 -1
- package/lib/mjs/driver.js +279 -0
- package/lib/mjs/driver.js.map +1 -1
- package/lib/mjs/existsRelationHandlers.d.ts +29 -0
- package/lib/mjs/existsRelationHandlers.d.ts.map +1 -0
- package/lib/mjs/existsRelationHandlers.js +100 -0
- package/lib/mjs/existsRelationHandlers.js.map +1 -0
- package/lib/mjs/fp.d.ts.map +1 -1
- package/lib/mjs/fp.js +10 -15
- package/lib/mjs/fp.js.map +1 -1
- package/lib/mjs/hydrators.d.ts.map +1 -1
- package/lib/mjs/hydrators.js +29 -2
- package/lib/mjs/hydrators.js.map +1 -1
- package/lib/mjs/identity-map.d.ts +60 -0
- package/lib/mjs/identity-map.d.ts.map +1 -0
- package/lib/mjs/identity-map.js +130 -0
- package/lib/mjs/identity-map.js.map +1 -0
- package/lib/mjs/index.d.ts +12 -0
- package/lib/mjs/index.d.ts.map +1 -1
- package/lib/mjs/index.js +12 -0
- package/lib/mjs/index.js.map +1 -1
- package/lib/mjs/interfaces.d.ts +295 -9
- package/lib/mjs/interfaces.d.ts.map +1 -1
- package/lib/mjs/interfaces.js +53 -0
- package/lib/mjs/interfaces.js.map +1 -1
- package/lib/mjs/metadata.d.ts.map +1 -1
- package/lib/mjs/metadata.js +4 -1
- package/lib/mjs/metadata.js.map +1 -1
- package/lib/mjs/metrics.d.ts +53 -0
- package/lib/mjs/metrics.d.ts.map +1 -0
- package/lib/mjs/metrics.js +76 -0
- package/lib/mjs/metrics.js.map +1 -0
- package/lib/mjs/middlewares.d.ts +13 -1
- package/lib/mjs/middlewares.d.ts.map +1 -1
- package/lib/mjs/middlewares.js +55 -15
- package/lib/mjs/middlewares.js.map +1 -1
- package/lib/mjs/model.d.ts +129 -13
- package/lib/mjs/model.d.ts.map +1 -1
- package/lib/mjs/model.js +451 -95
- package/lib/mjs/model.js.map +1 -1
- package/lib/mjs/orm.d.ts.map +1 -1
- package/lib/mjs/orm.js +12 -3
- package/lib/mjs/orm.js.map +1 -1
- package/lib/mjs/orphan.d.ts +27 -0
- package/lib/mjs/orphan.d.ts.map +1 -0
- package/lib/mjs/orphan.js +52 -0
- package/lib/mjs/orphan.js.map +1 -0
- package/lib/mjs/primary-keys.d.ts +60 -0
- package/lib/mjs/primary-keys.d.ts.map +1 -0
- package/lib/mjs/primary-keys.js +223 -0
- package/lib/mjs/primary-keys.js.map +1 -0
- package/lib/mjs/relation-objects.d.ts +101 -9
- package/lib/mjs/relation-objects.d.ts.map +1 -1
- package/lib/mjs/relation-objects.js +194 -43
- package/lib/mjs/relation-objects.js.map +1 -1
- package/lib/mjs/relations.d.ts +7 -1
- package/lib/mjs/relations.d.ts.map +1 -1
- package/lib/mjs/relations.js +16 -1
- package/lib/mjs/relations.js.map +1 -1
- package/lib/mjs/resilience.d.ts +50 -0
- package/lib/mjs/resilience.d.ts.map +1 -0
- package/lib/mjs/resilience.js +64 -0
- package/lib/mjs/resilience.js.map +1 -0
- package/lib/mjs/snapshot.d.ts +58 -0
- package/lib/mjs/snapshot.d.ts.map +1 -0
- package/lib/mjs/snapshot.js +115 -0
- package/lib/mjs/snapshot.js.map +1 -0
- package/lib/mjs/statements.d.ts +11 -1
- package/lib/mjs/statements.d.ts.map +1 -1
- package/lib/mjs/statements.js +14 -3
- package/lib/mjs/statements.js.map +1 -1
- package/lib/mjs/subject-builder.d.ts +79 -0
- package/lib/mjs/subject-builder.d.ts.map +1 -0
- package/lib/mjs/subject-builder.js +292 -0
- package/lib/mjs/subject-builder.js.map +1 -0
- package/lib/mjs/subject-executor.d.ts +122 -0
- package/lib/mjs/subject-executor.d.ts.map +1 -0
- package/lib/mjs/subject-executor.js +312 -0
- package/lib/mjs/subject-executor.js.map +1 -0
- package/lib/mjs/subject-sorter.d.ts +70 -0
- package/lib/mjs/subject-sorter.d.ts.map +1 -0
- package/lib/mjs/subject-sorter.js +168 -0
- package/lib/mjs/subject-sorter.js.map +1 -0
- package/lib/mjs/subject.d.ts +100 -0
- package/lib/mjs/subject.d.ts.map +1 -0
- package/lib/mjs/subject.js +84 -0
- package/lib/mjs/subject.js.map +1 -0
- package/lib/mjs/unit-of-work.d.ts +52 -0
- package/lib/mjs/unit-of-work.d.ts.map +1 -0
- package/lib/mjs/unit-of-work.js +166 -0
- package/lib/mjs/unit-of-work.js.map +1 -0
- package/lib/tsconfig.cjs.tsbuildinfo +1 -1
- package/lib/tsconfig.mjs.tsbuildinfo +1 -1
- 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
|
-
|
|
48
|
-
|
|
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
|
|
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
|
|
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] =
|
|
125
|
+
rel[r.ForeignKey] = scalar;
|
|
98
126
|
break;
|
|
99
127
|
case interfaces_js_1.RelationType.Many:
|
|
100
|
-
rel.forEach((rVal) => (rVal[r.ForeignKey] =
|
|
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 [
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
407
|
-
|
|
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
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (
|
|
442
|
-
|
|
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
|
-
|
|
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
|
-
|
|
560
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
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
|
-
|
|
649
|
-
|
|
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
|
-
|
|
731
|
-
|
|
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
|
|
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 (
|
|
738
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
808
|
-
|
|
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
|
-
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
|
853
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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__');
|