@spinajs/orm 2.0.481 → 2.0.482
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 {
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
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
|
|
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] =
|
|
118
|
+
rel[r.ForeignKey] = scalar;
|
|
91
119
|
break;
|
|
92
120
|
case RelationType.Many:
|
|
93
|
-
rel.forEach((rVal) => (rVal[r.ForeignKey] =
|
|
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 [
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
400
|
-
|
|
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
|
-
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if (
|
|
435
|
-
|
|
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
|
-
|
|
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
|
-
|
|
552
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
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
|
-
|
|
640
|
-
|
|
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
|
-
|
|
722
|
-
|
|
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
|
|
1062
|
+
setPkValue(v, description, pkValueOf(response.Returning[idx], description));
|
|
725
1063
|
}
|
|
726
1064
|
});
|
|
727
1065
|
}
|
|
728
|
-
else if (
|
|
729
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
799
|
-
query
|
|
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
|
-
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
|
844
|
-
query
|
|
1197
|
+
if (hasPk(description) && pk !== null) {
|
|
1198
|
+
wherePk(query, description, pk);
|
|
845
1199
|
}
|
|
846
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|