objection 4.0.0-rc.2 → 4.0.0-rc.3
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/lib/queryBuilder/graph/GraphLoader.js +201 -126
- package/lib/queryBuilder/graph/GraphNode.js +10 -3
- package/lib/queryBuilder/graph/GraphOptions.js +24 -49
- package/lib/queryBuilder/graph/GraphPlan.js +92 -108
- package/lib/queryBuilder/graph/GraphSchedule.js +27 -18
- package/lib/queryBuilder/graph/GraphUpsert.js +46 -548
- package/lib/queryBuilder/graph/GraphWriter.js +257 -14
- package/lib/queryBuilder/graph/graphChecks.js +497 -0
- package/lib/queryBuilder/graph/graphKeys.js +109 -404
- package/package.json +2 -2
- package/typings/objection/index.d.ts +437 -85
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { nonStrictEquals } from '../../utils/objectUtils.js';
|
|
2
2
|
import { ValidationErrorType } from '../../model/ValidationError.js';
|
|
3
3
|
import { formatPath } from './GraphNode.js';
|
|
4
4
|
|
|
5
5
|
const UNRESOLVED = Symbol('unresolved');
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// common current value of the rows its places read. Each place writes the
|
|
16
|
-
// columns that it sets, and reads the others, from their slots.
|
|
7
|
+
// The keys of the rows of a planned graph, solved as slots:
|
|
8
|
+
// - row: a model with an id, existing or inserted, or the one that a `#ref` model refers to.
|
|
9
|
+
// - cell: one key column of a row, or one JSON path of a column.
|
|
10
|
+
// - place: puts a row into a relation, setting its cells or its owner's to cells it reads.
|
|
11
|
+
// - slot: the cells that places join, with one value from its anchors, which agree.
|
|
12
|
+
// - anchor: a cell that no place sets, or a value that a model gives a cell its place doesn't set.
|
|
13
|
+
// - clear: empties the key of an unrelated row, or of the owner of a removed BelongsToOne row.
|
|
14
|
+
// A slot without anchors takes the current value of the cells that its places read.
|
|
17
15
|
export class GraphKeys {
|
|
18
16
|
constructor(plan) {
|
|
19
17
|
this.plan = plan;
|
|
@@ -23,18 +21,15 @@ export class GraphKeys {
|
|
|
23
21
|
this.cells = new Map();
|
|
24
22
|
// The props that each node only reads, and that other nodes write:
|
|
25
23
|
this.readProps = new Map();
|
|
26
|
-
|
|
27
|
-
// The columns that each place sets or reads:
|
|
24
|
+
// The cells that each place sets or reads:
|
|
28
25
|
this.placeCells = new Map();
|
|
29
|
-
// The rows that
|
|
26
|
+
// The rows that the graph inserts, and the ones that it inserted already:
|
|
30
27
|
this.insertRows = new Set();
|
|
31
28
|
this.insertedRows = new Set();
|
|
32
|
-
// The rows that only a later pass inserts:
|
|
33
|
-
this.laterRows = new Set();
|
|
34
29
|
// The nodes whose `#ref{}` values are resolved:
|
|
35
30
|
this.resolvedNodes = new Set();
|
|
36
31
|
|
|
37
|
-
// The rows
|
|
32
|
+
// 1. The rows, decided before the inserts give new rows their ids:
|
|
38
33
|
for (const node of plan.activeNodes) {
|
|
39
34
|
const row = this._getRow(node);
|
|
40
35
|
|
|
@@ -43,8 +38,7 @@ export class GraphKeys {
|
|
|
43
38
|
}
|
|
44
39
|
}
|
|
45
40
|
|
|
46
|
-
// The
|
|
47
|
-
// `hasWholeReference()`:
|
|
41
|
+
// 2. The places whose cells a whole `#ref{}` gives, see `hasWholeReference()`:
|
|
48
42
|
this.wholeReferences = new Set(
|
|
49
43
|
plan.activeNodes.flatMap((node) =>
|
|
50
44
|
node.propertyReferences
|
|
@@ -53,22 +47,12 @@ export class GraphKeys {
|
|
|
53
47
|
),
|
|
54
48
|
);
|
|
55
49
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (plan.actionFor(node) === 'insert' && !this.insertRows.has(row)) {
|
|
60
|
-
this.laterRows.add(row);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Places that the graph ignores, and many-to-many places, only read:
|
|
65
|
-
const deferredNodes = new Set(plan.deferredNodes);
|
|
66
|
-
|
|
67
|
-
for (const node of [...plan.activeNodes, ...deferredNodes]) {
|
|
50
|
+
// 3. The places. The ones that the graph ignores, and many-to-many places,
|
|
51
|
+
// only read:
|
|
52
|
+
for (const node of plan.activeNodes) {
|
|
68
53
|
const { parent, relation } = node;
|
|
69
54
|
|
|
70
55
|
if (parent && !node.toBeUnrelatedOrDeleted) {
|
|
71
|
-
const isDeferred = deferredNodes.has(node) && !this._isMovedWithRow(node);
|
|
72
56
|
const [target, targetProp, source, sourceProp] =
|
|
73
57
|
relation.keySide === 'related'
|
|
74
58
|
? [node, relation.relatedProp, parent, relation.ownerProp]
|
|
@@ -76,13 +60,9 @@ export class GraphKeys {
|
|
|
76
60
|
? [parent, relation.ownerProp, node, relation.relatedProp]
|
|
77
61
|
: [];
|
|
78
62
|
|
|
79
|
-
if (
|
|
80
|
-
target
|
|
81
|
-
|
|
82
|
-
(isDeferred || plan.actions.get(node) !== 'ignore')
|
|
83
|
-
) {
|
|
84
|
-
this._addPlace(node, target, targetProp, source, sourceProp, isDeferred);
|
|
85
|
-
} else if (!isDeferred && (relation.keySide !== 'owner' || hasOwnerKey(relation))) {
|
|
63
|
+
if (target && !isPlacingReference(plan, target) && plan.actions.get(node) !== 'ignore') {
|
|
64
|
+
this._addPlace(node, target, targetProp, source, sourceProp);
|
|
65
|
+
} else if (relation.keySide !== 'owner' || hasOwnerKey(relation)) {
|
|
86
66
|
// Arrays of keys are written by `setKeyArrays()`. Join rows read the
|
|
87
67
|
// keys of both sides, and wait for them like the other places:
|
|
88
68
|
const cells = [
|
|
@@ -101,10 +81,8 @@ export class GraphKeys {
|
|
|
101
81
|
}
|
|
102
82
|
}
|
|
103
83
|
|
|
104
|
-
// The
|
|
105
|
-
//
|
|
106
|
-
// BelongsToOne rows. The places of a node read them as `null` if another
|
|
107
|
-
// node clears them:
|
|
84
|
+
// 4. The clears, with the nodes that clear the cells. The places of a node
|
|
85
|
+
// read them as `null` if another node clears them:
|
|
108
86
|
this.clearingNodes = new Map();
|
|
109
87
|
const addClearingNode = (row, prop, node) =>
|
|
110
88
|
prop.props.forEach((_, i) => {
|
|
@@ -121,90 +99,29 @@ export class GraphKeys {
|
|
|
121
99
|
}
|
|
122
100
|
|
|
123
101
|
for (const node of plan.activeNodes) {
|
|
124
|
-
for (const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
hasOwnerKey(relation) &&
|
|
129
|
-
(node.obj[relation.name] === null || currentChild.node?.toBeUnrelatedOrDeleted) &&
|
|
130
|
-
plan.actions.get(currentChild) !== 'keep'
|
|
131
|
-
) {
|
|
132
|
-
addClearingNode(this._getRow(node), relation.ownerProp, node);
|
|
133
|
-
}
|
|
102
|
+
for (const relation of plan.getRemovedOwnerRelations(node)) {
|
|
103
|
+
addClearingNode(this._getRow(node), relation.ownerProp, node);
|
|
134
104
|
}
|
|
135
105
|
}
|
|
136
106
|
|
|
107
|
+
// 5. The anchors of each slot must agree, and no clear may place rows:
|
|
137
108
|
this._checkSlots();
|
|
138
109
|
}
|
|
139
110
|
|
|
140
|
-
//
|
|
141
|
-
// that the owner's to-many relations in the graph are keyed on: The move
|
|
142
|
-
// writes it, as its key is known, so that the later pass reads these
|
|
143
|
-
// relations at the new key.
|
|
144
|
-
_isMovedWithRow(node) {
|
|
145
|
-
const { parent, relation } = node;
|
|
146
|
-
return (
|
|
147
|
-
hasOwnerKey(relation) &&
|
|
148
|
-
this.activeNodes.has(parent) &&
|
|
149
|
-
node.obj.$hasId() &&
|
|
150
|
-
!node.toBeUnrelatedOrDeleted &&
|
|
151
|
-
hasRelationsOnKey(parent, relation.ownerProp)
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Return the nodes whose writes need values that only a later pass gives:
|
|
156
|
-
// the ones whose places set columns of slots without anchors that a later
|
|
157
|
-
// pass sets, or with anchors of rows that a later pass inserts, and the ones
|
|
158
|
-
// with `#ref{}` values of models that a later pass writes. The owners of
|
|
159
|
-
// BelongsToOne places get their keys patched in there.
|
|
160
|
-
getLaterNodes() {
|
|
161
|
-
const { activeNodes, deferredNodes } = this.plan;
|
|
162
|
-
const nodes = new Set();
|
|
163
|
-
|
|
164
|
-
for (const slot of this._getSlots()) {
|
|
165
|
-
const anchorCells = this._getAnchorCells(slot);
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
anchorCells.length === 0
|
|
169
|
-
? slot.cells.some((cell) => cell.isPending)
|
|
170
|
-
: anchorCells.some((cell) => this.laterRows.has(cell.row))
|
|
171
|
-
) {
|
|
172
|
-
for (const cell of slot.cells) {
|
|
173
|
-
for (const node of cell.placeNodes) {
|
|
174
|
-
nodes.add(node);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
for (const node of activeNodes) {
|
|
181
|
-
if (
|
|
182
|
-
node.propertyReferences.some(
|
|
183
|
-
(reference) =>
|
|
184
|
-
deferredNodes.has(reference.referencedNode) && readsWrittenValue(reference),
|
|
185
|
-
)
|
|
186
|
-
) {
|
|
187
|
-
nodes.add(node);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return [...nodes];
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Bind the columns of the rows of the inserted `nodes` to their values.
|
|
111
|
+
// Bind the cells of the rows of the inserted `nodes` to their values.
|
|
195
112
|
setInserted(nodes) {
|
|
196
113
|
for (const node of nodes) {
|
|
197
114
|
this.insertedRows.add(this.rows.get(node));
|
|
198
115
|
}
|
|
199
116
|
}
|
|
200
117
|
|
|
201
|
-
// Fill the
|
|
118
|
+
// Fill the cells that the place of `node` sets or reads with the values of
|
|
202
119
|
// their slots, as far as they're known.
|
|
203
120
|
fill(node) {
|
|
204
121
|
for (const cell of this.placeCells.get(node) || []) {
|
|
205
122
|
const value = this._getSlotValue(cell);
|
|
206
123
|
|
|
207
|
-
// A place that only reads a
|
|
124
|
+
// A place that only reads a cell that other nodes write leaves the
|
|
208
125
|
// write to them:
|
|
209
126
|
if (
|
|
210
127
|
!cell.setNodes.has(node) &&
|
|
@@ -236,25 +153,56 @@ export class GraphKeys {
|
|
|
236
153
|
.map(({ prop, index }) => prop.props[index]);
|
|
237
154
|
}
|
|
238
155
|
|
|
156
|
+
// Return true if a place other than the places of the row of `node` through
|
|
157
|
+
// its BelongsToOne `relation` sets the key of the relation, e.g. the place
|
|
158
|
+
// that moves the row.
|
|
159
|
+
isSetByOtherPlaces(node, relation) {
|
|
160
|
+
const row = this._getRow(node);
|
|
161
|
+
return relation.ownerProp.props.some((_, i) =>
|
|
162
|
+
[...(this.cells.get(this._getKey(row, relation.ownerProp, i))?.placeNodes || [])].some(
|
|
163
|
+
(other) => !(other.relation === relation && this._getRow(other.parent) === row),
|
|
164
|
+
),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
239
168
|
// Return the values of the key columns `prop` of the row of `node`.
|
|
240
169
|
getProps(node, prop) {
|
|
241
170
|
return this._getCells(node, prop).map((cell) => this._getSlotValue(cell));
|
|
242
171
|
}
|
|
243
172
|
|
|
173
|
+
// Return the values of the key columns `prop` of the row of `node` at which
|
|
174
|
+
// its relations are found before the writes: the current values of the rows
|
|
175
|
+
// that give the final ones, as rows follow a key that changes with them, or
|
|
176
|
+
// without such rows, the current value of the row itself. Clears don't count,
|
|
177
|
+
// as the removals of the relations that are read cause them. Keys of new rows
|
|
178
|
+
// have no related rows yet.
|
|
179
|
+
getReadProps(node, prop) {
|
|
180
|
+
return this._getCells(node, prop).map((cell) => {
|
|
181
|
+
const values = this._getAnchorCells(cell.slot).map((anchor) =>
|
|
182
|
+
this.insertRows.has(anchor.row)
|
|
183
|
+
? null
|
|
184
|
+
: (this._getStoredValue(anchor) ?? this._getAnchorValue(anchor)),
|
|
185
|
+
);
|
|
186
|
+
const value = this._getSlotValue(cell);
|
|
187
|
+
return values.length > 0
|
|
188
|
+
? (values.find((other) => nonStrictEquals(other, value)) ?? values[0])
|
|
189
|
+
: this._getStoredValue(cell);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
244
193
|
// Return true if `node` only reads its key column `prop`, which other nodes
|
|
245
194
|
// write, so that it leaves the write to them.
|
|
246
195
|
isOnlyRead(node, prop) {
|
|
247
196
|
return !!this.readProps.get(node)?.has(prop);
|
|
248
197
|
}
|
|
249
198
|
|
|
250
|
-
// Return true if a place of `node`
|
|
251
|
-
// of its row:
|
|
199
|
+
// Return true if a place of `node` sets the key column `prop` of its row:
|
|
252
200
|
isSet(node, prop, index) {
|
|
253
201
|
return !!this.cells.get(this._getKey(this._getRow(node), prop, index))?.setNodes.has(node);
|
|
254
202
|
}
|
|
255
203
|
|
|
256
204
|
// Return the nodes whose inserts or `#ref{}` values bind the slots of the
|
|
257
|
-
//
|
|
205
|
+
// cells that the place of `node` sets or reads.
|
|
258
206
|
waitsFor(node) {
|
|
259
207
|
const waits = [];
|
|
260
208
|
|
|
@@ -281,18 +229,14 @@ export class GraphKeys {
|
|
|
281
229
|
}
|
|
282
230
|
|
|
283
231
|
// The place of `node` gives `target` the key of `source`, for each column of
|
|
284
|
-
// the relation
|
|
285
|
-
_addPlace(node, target, targetProp, source, sourceProp
|
|
232
|
+
// the relation:
|
|
233
|
+
_addPlace(node, target, targetProp, source, sourceProp) {
|
|
286
234
|
const targetCells = this._getCells(target, targetProp);
|
|
287
235
|
const sourceCells = this._getCells(source, sourceProp);
|
|
288
236
|
|
|
289
237
|
// A row below itself, e.g. in its own `siblings`, restates its own key and
|
|
290
238
|
// writes nothing:
|
|
291
|
-
if (
|
|
292
|
-
targetCells.forEach((cell, i) => {
|
|
293
|
-
cell.isPending ||= cell !== sourceCells[i];
|
|
294
|
-
});
|
|
295
|
-
} else if (targetCells.some((cell, i) => cell !== sourceCells[i])) {
|
|
239
|
+
if (targetCells.some((cell, i) => cell !== sourceCells[i])) {
|
|
296
240
|
this._addCells(target, targetCells);
|
|
297
241
|
this._addCells(source, sourceCells);
|
|
298
242
|
|
|
@@ -310,7 +254,7 @@ export class GraphKeys {
|
|
|
310
254
|
}
|
|
311
255
|
}
|
|
312
256
|
|
|
313
|
-
// The key of a
|
|
257
|
+
// The key of a cell of the row of `node` at its place: the row of its
|
|
314
258
|
// parent and the relation.
|
|
315
259
|
_getPlaceKey(node, prop) {
|
|
316
260
|
const { parent, relation } = node;
|
|
@@ -336,8 +280,7 @@ export class GraphKeys {
|
|
|
336
280
|
return prop.props.map((_, i) => this._getCell(node, prop, i));
|
|
337
281
|
}
|
|
338
282
|
|
|
339
|
-
// The row of `node
|
|
340
|
-
// passes wrote:
|
|
283
|
+
// The row of `node`:
|
|
341
284
|
_getRow(node) {
|
|
342
285
|
let row = this.rows.get(node);
|
|
343
286
|
|
|
@@ -365,14 +308,7 @@ export class GraphKeys {
|
|
|
365
308
|
}
|
|
366
309
|
|
|
367
310
|
_getKey(row, prop, index) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (!descriptions) {
|
|
371
|
-
descriptions = prop.props.map((_, i) => prop.propDescription(i));
|
|
372
|
-
this.descriptions.set(prop, descriptions);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return `${row}|${descriptions[index]}`;
|
|
311
|
+
return `${row}|${prop.propDescription(index)}`;
|
|
376
312
|
}
|
|
377
313
|
|
|
378
314
|
_getCell(node, prop, index) {
|
|
@@ -391,7 +327,6 @@ export class GraphKeys {
|
|
|
391
327
|
setNodes: new Set(),
|
|
392
328
|
placeNodes: new Set(),
|
|
393
329
|
readerNodes: new Set(),
|
|
394
|
-
isPending: false,
|
|
395
330
|
slot: null,
|
|
396
331
|
};
|
|
397
332
|
cell.slot = { cells: [cell] };
|
|
@@ -402,15 +337,15 @@ export class GraphKeys {
|
|
|
402
337
|
}
|
|
403
338
|
|
|
404
339
|
// The cells that give a slot its value, known once the slots are built: the
|
|
405
|
-
// ones that no place
|
|
406
|
-
//
|
|
340
|
+
// ones that no place sets, and the ones that a node gives whose own place
|
|
341
|
+
// doesn't set them.
|
|
407
342
|
_getAnchorCells(slot) {
|
|
408
|
-
return (slot.anchorCells ??= slot.cells.filter(
|
|
409
|
-
cell.setNodes.size === 0
|
|
343
|
+
return (slot.anchorCells ??= slot.cells.filter(
|
|
344
|
+
(cell) => cell.setNodes.size === 0 || this._getGivenValues(cell).value !== undefined,
|
|
410
345
|
));
|
|
411
346
|
}
|
|
412
347
|
|
|
413
|
-
// The values that the nodes of a row give a
|
|
348
|
+
// The values that the nodes of a row give a cell that their own places
|
|
414
349
|
// don't set, as the graph gives them: `value` from any write, `writtenValue`
|
|
415
350
|
// from a patch or relate. A `#ref{}` is `UNRESOLVED` until it's resolved.
|
|
416
351
|
_getGivenValues(cell) {
|
|
@@ -461,7 +396,7 @@ export class GraphKeys {
|
|
|
461
396
|
return cell.givenValues;
|
|
462
397
|
}
|
|
463
398
|
|
|
464
|
-
// Return true if a
|
|
399
|
+
// Return true if a cell of `node` holds a `#ref{}` that isn't resolved yet:
|
|
465
400
|
_hasReference(node, prop, index) {
|
|
466
401
|
return (
|
|
467
402
|
!this.resolvedNodes.has(node) &&
|
|
@@ -469,8 +404,8 @@ export class GraphKeys {
|
|
|
469
404
|
);
|
|
470
405
|
}
|
|
471
406
|
|
|
472
|
-
// The value of an anchor cell: of a row that
|
|
473
|
-
// inserted, with `null` for the
|
|
407
|
+
// The value of an anchor cell: of a row that the graph inserts once it's
|
|
408
|
+
// inserted, with `null` for the cells the graph doesn't give, a value that
|
|
474
409
|
// a patch or relate writes, which wins over the inserted one, an id, or the
|
|
475
410
|
// current value.
|
|
476
411
|
_getAnchorValue(cell) {
|
|
@@ -531,7 +466,7 @@ export class GraphKeys {
|
|
|
531
466
|
}
|
|
532
467
|
|
|
533
468
|
// The value of the slot of `cell`, from its anchors, which agree, or without
|
|
534
|
-
// any, from the current values of the
|
|
469
|
+
// any, from the current values of the cells its places read. `undefined`
|
|
535
470
|
// until the inserts and `#ref{}` values that bind it are written.
|
|
536
471
|
_getSlotValue({ slot }) {
|
|
537
472
|
if (!('value' in slot)) {
|
|
@@ -554,14 +489,16 @@ export class GraphKeys {
|
|
|
554
489
|
return slot.value;
|
|
555
490
|
}
|
|
556
491
|
|
|
557
|
-
// The current value of
|
|
558
|
-
// owners from earlier passes, what these passes wrote into their models.
|
|
492
|
+
// The current value of `cell`, `null` once it's cleared:
|
|
559
493
|
_getCurrentValue(cell) {
|
|
560
|
-
|
|
494
|
+
return this._isCleared(cell) ? null : this._getStoredValue(cell);
|
|
495
|
+
}
|
|
561
496
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
497
|
+
// The value of `cell` in its row before any clears: of its
|
|
498
|
+
// current row, or what an ignored node gives it, as the graph doesn't write
|
|
499
|
+
// it there:
|
|
500
|
+
_getStoredValue(cell) {
|
|
501
|
+
const { row, prop, index } = cell;
|
|
565
502
|
|
|
566
503
|
for (const node of this.nodesByRow.get(row)) {
|
|
567
504
|
const value =
|
|
@@ -577,36 +514,46 @@ export class GraphKeys {
|
|
|
577
514
|
}
|
|
578
515
|
}
|
|
579
516
|
|
|
580
|
-
// Return true if another node than the ones that read
|
|
581
|
-
// clears it in this pass:
|
|
517
|
+
// Return true if another node than the ones that read `cell` clears it:
|
|
582
518
|
_isCleared(cell) {
|
|
583
519
|
const clearingNodes = this.clearingNodes.get(cell.key);
|
|
584
520
|
return (cell.isCleared ??=
|
|
585
521
|
!!clearingNodes && [...clearingNodes].some((node) => !cell.readerNodes.has(node)));
|
|
586
522
|
}
|
|
587
523
|
|
|
588
|
-
_getSlots() {
|
|
589
|
-
return new Set([...this.cells.values()].map((cell) => cell.slot));
|
|
590
|
-
}
|
|
591
|
-
|
|
592
524
|
// Throw for slots whose anchors, or whose rows without any, give different
|
|
593
525
|
// values that are known before the writes.
|
|
594
526
|
_checkSlots() {
|
|
595
|
-
|
|
527
|
+
const slots = new Set([...this.cells.values()].map((cell) => cell.slot));
|
|
528
|
+
|
|
529
|
+
for (const slot of [...slots].filter(({ cells }) => cells.length > 1)) {
|
|
596
530
|
const anchorCells = this._getAnchorCells(slot);
|
|
531
|
+
const readCells = slot.cells.filter((cell) => cell.readerNodes.size > 0);
|
|
532
|
+
const placingCell = slot.cells.find((cell) => cell.setNodes.size > 0);
|
|
533
|
+
// A key that one node clears can't place the rows of another node:
|
|
534
|
+
const clearedCell = (anchorCells.length > 0 ? anchorCells : readCells).find((cell) =>
|
|
535
|
+
this._isCleared(cell),
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
if (clearedCell && placingCell) {
|
|
539
|
+
const { prop, index, node } = clearedCell;
|
|
540
|
+
throw node.modelClass.createValidationError({
|
|
541
|
+
type: ValidationErrorType.InvalidGraph,
|
|
542
|
+
message: `the graph clears ${prop.props[index]} of the ${node.modelClass.name} at ${formatPath(node.dataPathKey)}, which places rows at ${formatPath([...placingCell.placeNodes][0].dataPathKey)}`,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
597
546
|
const known = (
|
|
598
547
|
anchorCells.length > 0
|
|
599
548
|
? anchorCells.map((cell) => [cell, this._getKnownKey(cell)])
|
|
600
|
-
:
|
|
601
|
-
.filter((cell) => cell.readerNodes.size > 0)
|
|
602
|
-
.map((cell) => [cell, this._getCurrentValue(cell)])
|
|
549
|
+
: readCells.map((cell) => [cell, this._getCurrentValue(cell)])
|
|
603
550
|
).filter(([, value]) => value !== undefined);
|
|
604
551
|
const [cell, value] = known[0] || [];
|
|
605
552
|
const [otherCell] = known.find(([, other]) => String(other) !== String(value)) || [];
|
|
606
553
|
|
|
607
554
|
if (otherCell) {
|
|
608
555
|
// Name the column that the places set, e.g. not the id they read it from:
|
|
609
|
-
const { prop, index, node } =
|
|
556
|
+
const { prop, index, node } = placingCell || cell;
|
|
610
557
|
throw node.modelClass.createValidationError({
|
|
611
558
|
type: ValidationErrorType.InvalidGraph,
|
|
612
559
|
message: `the graph gives ${prop.props[index]} of one ${node.modelClass.name} two values, at ${formatPath(cell.node.dataPathKey)} and at ${formatPath(otherCell.node.dataPathKey)}`,
|
|
@@ -631,180 +578,6 @@ function joinCells(cell, otherCell) {
|
|
|
631
578
|
}
|
|
632
579
|
}
|
|
633
580
|
|
|
634
|
-
// Copy the keys and `#ref{}` values that `node` waits for into its model.
|
|
635
|
-
export function resolveDependencies(schedule, node) {
|
|
636
|
-
for (const dependency of schedule.dependencies.get(node) || []) {
|
|
637
|
-
if (!schedule.deferredDependencies.has(dependency)) {
|
|
638
|
-
if (dependency.relation) {
|
|
639
|
-
// Hooks see the props of the models that `#ref` models refer to:
|
|
640
|
-
resolveReference(dependency.related);
|
|
641
|
-
resolveReference(dependency.dependent);
|
|
642
|
-
} else if (dependency.reference) {
|
|
643
|
-
resolvePropertyReference(dependency.dependent, dependency.reference);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
schedule.keys.resolvedNodes.add(node);
|
|
649
|
-
schedule.keys.fill(node);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// Copy the keys and `#ref{}` values that `node` waits for into its model,
|
|
653
|
-
// after those that the graph fills into the models that it reads them from,
|
|
654
|
-
// e.g. the new key of an owner whose BelongsToOne row is replaced. Rows that
|
|
655
|
-
// move first only wait for rows that exist, see `GraphSchedule#_waitsForInserts()`.
|
|
656
|
-
export function resolveFilledDependencies(schedule, node, visitedNodes = new Set()) {
|
|
657
|
-
visitedNodes.add(node);
|
|
658
|
-
|
|
659
|
-
for (const dependency of schedule.dependencies.get(node) || []) {
|
|
660
|
-
const referencedNode = dependency.node.referencedNode || dependency.node;
|
|
661
|
-
|
|
662
|
-
if (
|
|
663
|
-
!visitedNodes.has(referencedNode) &&
|
|
664
|
-
(dependency.isKeyWait || schedule.readsFilledProps(dependency))
|
|
665
|
-
) {
|
|
666
|
-
resolveFilledDependencies(schedule, referencedNode, visitedNodes);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
resolveDependencies(schedule, node);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// Change the arrays of keys that `node` holds for the relations that the graph
|
|
674
|
-
// gives it: Remove the keys of the current related rows that it deletes or
|
|
675
|
-
// unrelates, and add the keys of the related models that it writes. Other
|
|
676
|
-
// keys stay, e.g. of rows that the relation's filter leaves out. Relations
|
|
677
|
-
// that the graph leaves out keep their arrays (#1202). Return the props that
|
|
678
|
-
// change.
|
|
679
|
-
export function setKeyArrays(plan, node) {
|
|
680
|
-
const { actions, graphOptions } = plan;
|
|
681
|
-
const { existing } = node;
|
|
682
|
-
const props = [];
|
|
683
|
-
|
|
684
|
-
for (const name of node.modelClass.getRelationNames()) {
|
|
685
|
-
const relation = node.obj[name] !== undefined && node.modelClass.getRelation(name);
|
|
686
|
-
|
|
687
|
-
if (relation && hasOwnerKeyArray(relation)) {
|
|
688
|
-
const { ownerProp, relatedProp } = relation;
|
|
689
|
-
const getKey = (related) => relatedProp.getProp(related.obj, 0);
|
|
690
|
-
const keys = relation.getKeys(existing ? existing.obj : node.obj);
|
|
691
|
-
const removed = (existing?.activeChildren || []).filter(
|
|
692
|
-
(child) =>
|
|
693
|
-
child.relation.name === name && ['delete', 'unrelate'].includes(actions.get(child)),
|
|
694
|
-
);
|
|
695
|
-
const added = node.activeChildren.filter(
|
|
696
|
-
(child) =>
|
|
697
|
-
child.relation.name === name &&
|
|
698
|
-
!child.toBeUnrelatedOrDeleted &&
|
|
699
|
-
actions.get(child) !== 'ignore',
|
|
700
|
-
);
|
|
701
|
-
|
|
702
|
-
for (const related of added) {
|
|
703
|
-
resolveReference(related);
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
const newKeys = relation.changeKeys(keys, {
|
|
707
|
-
add: added.map(getKey),
|
|
708
|
-
remove: removed.map(getKey),
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
if (!existing || graphOptions.shouldUpdate(node)) {
|
|
712
|
-
// Inserts and updates write the whole row:
|
|
713
|
-
ownerProp.setProp(node.obj, 0, newKeys);
|
|
714
|
-
props.push(ownerProp.props[0]);
|
|
715
|
-
} else if (!jsonEquals(keys, newKeys)) {
|
|
716
|
-
const patch = relation.createKeysPatch(existing.obj, newKeys);
|
|
717
|
-
Object.assign(node.obj, patch);
|
|
718
|
-
props.push(...Object.keys(patch));
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
return props;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// Return the keys of `node` that point to other BelongsToOne rows than before,
|
|
727
|
-
// which the graph inserts or relates, and which `GraphKeys` filled in for the
|
|
728
|
-
// places of this pass.
|
|
729
|
-
export function getChangedKeysOfRelatedRows(plan, keys, node) {
|
|
730
|
-
const { actions } = plan;
|
|
731
|
-
const props = [];
|
|
732
|
-
|
|
733
|
-
for (const child of node.activeChildren) {
|
|
734
|
-
const { relation } = child;
|
|
735
|
-
|
|
736
|
-
// Ignored models don't replace the current one, and models marked with
|
|
737
|
-
// `#unrelate` or `#delete` are handled below:
|
|
738
|
-
if (hasOwnerKey(relation) && actions.get(child) !== 'ignore') {
|
|
739
|
-
relation.ownerProp.forEach((i) => {
|
|
740
|
-
const currentValue = node.existing && relation.ownerProp.getProp(node.existing.obj, i);
|
|
741
|
-
const value = relation.ownerProp.getProp(node.obj, i);
|
|
742
|
-
|
|
743
|
-
if (
|
|
744
|
-
value !== undefined &&
|
|
745
|
-
currentValue != value &&
|
|
746
|
-
keys.isSet(node, relation.ownerProp, i)
|
|
747
|
-
) {
|
|
748
|
-
props.push(relation.ownerProp.props[i]);
|
|
749
|
-
}
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
return props;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Clear the keys of `node` that point to BelongsToOne rows that are removed,
|
|
758
|
-
// and return the props that change.
|
|
759
|
-
export function clearKeysOfRemovedRows(plan, node) {
|
|
760
|
-
const { actions } = plan;
|
|
761
|
-
const props = [];
|
|
762
|
-
|
|
763
|
-
for (const currentChild of node.existing?.activeChildren || []) {
|
|
764
|
-
const { relation } = currentChild;
|
|
765
|
-
|
|
766
|
-
if (
|
|
767
|
-
hasOwnerKey(relation) &&
|
|
768
|
-
(node.obj[relation.name] === null || currentChild.node?.toBeUnrelatedOrDeleted) &&
|
|
769
|
-
actions.get(currentChild) !== 'keep'
|
|
770
|
-
) {
|
|
771
|
-
relation.ownerProp.forEach((i) => {
|
|
772
|
-
if (relation.ownerProp.getProp(node.existing.obj, i) != null) {
|
|
773
|
-
relation.ownerProp.setProp(node.obj, i, null);
|
|
774
|
-
props.push(relation.ownerProp.props[i]);
|
|
775
|
-
}
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
return props;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
// Give a `#ref` model the props of the model it refers to that it doesn't have,
|
|
784
|
-
// and the `filledProps` again, as they may have changed since. Its own keys
|
|
785
|
-
// stay its own, and the join table extras of the place of the model belong to
|
|
786
|
-
// the join row there.
|
|
787
|
-
export function resolveReference(node, filledProps = []) {
|
|
788
|
-
const { referencedNode, relation } = node;
|
|
789
|
-
|
|
790
|
-
if (referencedNode) {
|
|
791
|
-
const ownKeys = relation?.keySide === 'related' ? relation.relatedProp.props : [];
|
|
792
|
-
const modelExtras =
|
|
793
|
-
referencedNode.relation?.keySide === 'join'
|
|
794
|
-
? referencedNode.relation.joinTableExtras.map((extra) => extra.aliasProp)
|
|
795
|
-
: [];
|
|
796
|
-
|
|
797
|
-
for (const prop of difference(Object.keys(referencedNode.obj), modelExtras)) {
|
|
798
|
-
if (
|
|
799
|
-
!Object.hasOwn(node.obj, prop) ||
|
|
800
|
-
(filledProps.includes(prop) && !ownKeys.includes(prop))
|
|
801
|
-
) {
|
|
802
|
-
node.obj[prop] = referencedNode.obj[prop];
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
581
|
// Return true if `node` is a `#ref` model in a many-to-many or BelongsToOne
|
|
809
582
|
// relation whose model writes its row, so that it only places the row there,
|
|
810
583
|
// and neither sets nor gives the keys of that row.
|
|
@@ -819,7 +592,7 @@ function isPlacingReference(plan, node) {
|
|
|
819
592
|
|
|
820
593
|
// Return true if `node` gives `prop` a whole `#ref{}` value that wins over the
|
|
821
594
|
// key that its place in its parent sets, or that any place sets when `node` is
|
|
822
|
-
// inserted (Vincit#1378). The
|
|
595
|
+
// inserted (Vincit#1378). The cell then keeps that value for all its readers.
|
|
823
596
|
function hasWholeReference(plan, node, prop) {
|
|
824
597
|
return (
|
|
825
598
|
(plan.actions.get(node) === 'insert' ||
|
|
@@ -830,83 +603,15 @@ function hasWholeReference(plan, node, prop) {
|
|
|
830
603
|
);
|
|
831
604
|
}
|
|
832
605
|
|
|
833
|
-
//
|
|
834
|
-
function
|
|
835
|
-
|
|
836
|
-
set(node.obj, path, isWholeValue ? value : get(node.obj, path).replace(match, value));
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// Fetch the keys that the relations of the inserted nodes need, but that the
|
|
840
|
-
// database generated and the insert didn't return, like a uuid that isn't the
|
|
841
|
-
// id (Vincit#853).
|
|
842
|
-
export async function fetchGeneratedKeys(builder, modelClass, nodes) {
|
|
843
|
-
const keyProps = (node) => [
|
|
844
|
-
...(node.relation && node.relation.keySide !== 'related' ? [node.relation.relatedProp] : []),
|
|
845
|
-
...node.activeChildren
|
|
846
|
-
.filter((child) => child.relation.keySide !== 'owner')
|
|
847
|
-
.map((child) => child.relation.ownerProp),
|
|
848
|
-
];
|
|
849
|
-
const missingNodes = nodes.filter((node) =>
|
|
850
|
-
keyProps(node).some((prop) => prop.getProps(node.obj).some((value) => value === undefined)),
|
|
851
|
-
);
|
|
852
|
-
|
|
853
|
-
if (missingNodes.length > 0 && missingNodes.every((node) => node.obj.$hasId())) {
|
|
854
|
-
// SQL Server returns columns selected twice as arrays, so the ids are
|
|
855
|
-
// part of the set:
|
|
856
|
-
const columns = new Set([
|
|
857
|
-
...modelClass.getIdColumnArray(),
|
|
858
|
-
...missingNodes.flatMap((node) => keyProps(node)).flatMap((prop) => prop.cols),
|
|
859
|
-
]);
|
|
860
|
-
const rows = await modelClass
|
|
861
|
-
.query()
|
|
862
|
-
.childQueryOf(builder, { fork: true, isInternalQuery: true })
|
|
863
|
-
.findByIds(missingNodes.map((node) => node.obj.$id()))
|
|
864
|
-
.select([...columns]);
|
|
865
|
-
const rowsByKey = new Map(rows.map((row) => [row.$idKey(), row]));
|
|
866
|
-
|
|
867
|
-
for (const node of missingNodes) {
|
|
868
|
-
const row = rowsByKey.get(node.obj.$idKey());
|
|
869
|
-
|
|
870
|
-
for (const prop of keyProps(node)) {
|
|
871
|
-
prop.forEach((i) => {
|
|
872
|
-
if (prop.getProp(node.obj, i) === undefined) {
|
|
873
|
-
prop.setProp(node.obj, i, prop.getProp(row, i));
|
|
874
|
-
}
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// Return true if a `#ref{}` reads a value that only exists once the model it
|
|
882
|
-
// refers to is written: the id of a new model, or another `#ref{}`.
|
|
883
|
-
export function readsWrittenValue({ referencedNode, refPath: [prop] }) {
|
|
884
|
-
return (
|
|
885
|
-
(!referencedNode.obj.$hasId() &&
|
|
886
|
-
referencedNode.modelClass.getIdPropertyArray().includes(prop)) ||
|
|
887
|
-
referencedNode.propertyReferences.some(({ path }) => path[0] === prop)
|
|
888
|
-
);
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
// Return true if the graph gives `node` to-many relations keyed on `prop`, e.g.
|
|
892
|
-
// `siblings` on `ownerId`.
|
|
893
|
-
function hasRelationsOnKey(node, prop) {
|
|
894
|
-
const { modelClass, obj } = node;
|
|
895
|
-
return modelClass
|
|
896
|
-
.getRelationNames()
|
|
897
|
-
.filter((name) => obj[name] !== undefined)
|
|
898
|
-
.map((name) => modelClass.getRelation(name))
|
|
899
|
-
.some((relation) => relation.keySide === 'related' && hasSameColumns(relation.ownerProp, prop));
|
|
606
|
+
// Return true if the values of two keys differ, with missing values as `null`.
|
|
607
|
+
export function isOtherKey(values, otherValues) {
|
|
608
|
+
return values.some((value, i) => !nonStrictEquals(value ?? null, otherValues[i] ?? null));
|
|
900
609
|
}
|
|
901
610
|
|
|
902
611
|
export function hasOwnerKey(relation) {
|
|
903
612
|
return relation.keySide === 'owner' && relation.isOneToOne();
|
|
904
613
|
}
|
|
905
614
|
|
|
906
|
-
function hasOwnerKeyArray(relation) {
|
|
907
|
-
return relation.keySide === 'owner' && !relation.isOneToOne();
|
|
908
|
-
}
|
|
909
|
-
|
|
910
615
|
// Return true if both relations keep the keys of their related rows in the same
|
|
911
616
|
// columns, also the same JSON paths of them.
|
|
912
617
|
export function hasSameKey(relation, otherRelation) {
|