objection 4.0.0-rc.1 → 4.0.0-rc.2
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 +18 -8
- package/lib/queryBuilder/graph/GraphNode.js +6 -0
- package/lib/queryBuilder/graph/GraphPlan.js +68 -32
- package/lib/queryBuilder/graph/GraphSchedule.js +513 -0
- package/lib/queryBuilder/graph/GraphUpsert.js +69 -14
- package/lib/queryBuilder/graph/GraphWriter.js +39 -794
- package/lib/queryBuilder/graph/graphKeys.js +929 -0
- package/package.json +1 -1
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
import { difference, get, jsonEquals, set } from '../../utils/objectUtils.js';
|
|
2
|
+
import { ValidationErrorType } from '../../model/ValidationError.js';
|
|
3
|
+
import { formatPath } from './GraphNode.js';
|
|
4
|
+
|
|
5
|
+
const UNRESOLVED = Symbol('unresolved');
|
|
6
|
+
|
|
7
|
+
// Resolve the keys and `#ref{}` values of the models of a planned graph.
|
|
8
|
+
|
|
9
|
+
// The keys of the rows of a planned graph, as slots: Each place of this pass
|
|
10
|
+
// that puts a row into a relation through a key says that a key column of the
|
|
11
|
+
// row, or of its owner for a BelongsToOne row, equals a column of the other
|
|
12
|
+
// side. These columns form a slot. Its anchors give it its value: the columns
|
|
13
|
+
// that no place of this pass sets, and the values that a node gives whose own
|
|
14
|
+
// place doesn't set them. Anchors have to agree. A slot without any takes the
|
|
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.
|
|
17
|
+
export class GraphKeys {
|
|
18
|
+
constructor(plan) {
|
|
19
|
+
this.plan = plan;
|
|
20
|
+
this.activeNodes = new Set(plan.activeNodes);
|
|
21
|
+
this.rows = new Map();
|
|
22
|
+
this.nodesByRow = new Map();
|
|
23
|
+
this.cells = new Map();
|
|
24
|
+
// The props that each node only reads, and that other nodes write:
|
|
25
|
+
this.readProps = new Map();
|
|
26
|
+
this.descriptions = new Map();
|
|
27
|
+
// The columns that each place sets or reads:
|
|
28
|
+
this.placeCells = new Map();
|
|
29
|
+
// The rows that this pass inserts, and the ones that it inserted already:
|
|
30
|
+
this.insertRows = new Set();
|
|
31
|
+
this.insertedRows = new Set();
|
|
32
|
+
// The rows that only a later pass inserts:
|
|
33
|
+
this.laterRows = new Set();
|
|
34
|
+
// The nodes whose `#ref{}` values are resolved:
|
|
35
|
+
this.resolvedNodes = new Set();
|
|
36
|
+
|
|
37
|
+
// The rows are decided before the inserts give new rows their ids:
|
|
38
|
+
for (const node of plan.activeNodes) {
|
|
39
|
+
const row = this._getRow(node);
|
|
40
|
+
|
|
41
|
+
if (plan.actions.get(node) === 'insert') {
|
|
42
|
+
this.insertRows.add(row);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The columns that a whole `#ref{}` gives the rows at their places, see
|
|
47
|
+
// `hasWholeReference()`:
|
|
48
|
+
this.wholeReferences = new Set(
|
|
49
|
+
plan.activeNodes.flatMap((node) =>
|
|
50
|
+
node.propertyReferences
|
|
51
|
+
.filter(({ path }) => hasWholeReference(plan, node, path[0]))
|
|
52
|
+
.map(({ path }) => this._getPlaceKey(node, path[0])),
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
for (const node of plan.deferredNodes) {
|
|
57
|
+
const row = this._getRow(node);
|
|
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]) {
|
|
68
|
+
const { parent, relation } = node;
|
|
69
|
+
|
|
70
|
+
if (parent && !node.toBeUnrelatedOrDeleted) {
|
|
71
|
+
const isDeferred = deferredNodes.has(node) && !this._isMovedWithRow(node);
|
|
72
|
+
const [target, targetProp, source, sourceProp] =
|
|
73
|
+
relation.keySide === 'related'
|
|
74
|
+
? [node, relation.relatedProp, parent, relation.ownerProp]
|
|
75
|
+
: hasOwnerKey(relation)
|
|
76
|
+
? [parent, relation.ownerProp, node, relation.relatedProp]
|
|
77
|
+
: [];
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
target &&
|
|
81
|
+
!isPlacingReference(plan, target) &&
|
|
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))) {
|
|
86
|
+
// Arrays of keys are written by `setKeyArrays()`. Join rows read the
|
|
87
|
+
// keys of both sides, and wait for them like the other places:
|
|
88
|
+
const cells = [
|
|
89
|
+
...this._addCells(parent, this._getCells(parent, relation.ownerProp)),
|
|
90
|
+
...(relation.keySide === 'join'
|
|
91
|
+
? this._addCells(node, this._getCells(node, relation.relatedProp))
|
|
92
|
+
: []),
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
if (relation.keySide === 'join') {
|
|
96
|
+
for (const cell of cells) {
|
|
97
|
+
cell.placeNodes.add(node);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The key columns that the plan clears in this pass, with the nodes that
|
|
105
|
+
// clear them: rows that relations leave out, and the keys of removed
|
|
106
|
+
// BelongsToOne rows. The places of a node read them as `null` if another
|
|
107
|
+
// node clears them:
|
|
108
|
+
this.clearingNodes = new Map();
|
|
109
|
+
const addClearingNode = (row, prop, node) =>
|
|
110
|
+
prop.props.forEach((_, i) => {
|
|
111
|
+
const key = this._getKey(row, prop, i);
|
|
112
|
+
this.clearingNodes.set(key, (this.clearingNodes.get(key) || new Set()).add(node));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
for (const currentNode of plan.activeCurrentNodes) {
|
|
116
|
+
const { relation, parent } = currentNode;
|
|
117
|
+
|
|
118
|
+
if (relation?.keySide === 'related' && plan.actions.get(currentNode) === 'unrelate') {
|
|
119
|
+
addClearingNode(currentNode.rowKey, relation.relatedProp, parent.node);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const node of plan.activeNodes) {
|
|
124
|
+
for (const currentChild of node.existing?.activeChildren || []) {
|
|
125
|
+
const { relation } = currentChild;
|
|
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
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this._checkSlots();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Return true if the BelongsToOne row of `node` gives its moved owner a key
|
|
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.
|
|
195
|
+
setInserted(nodes) {
|
|
196
|
+
for (const node of nodes) {
|
|
197
|
+
this.insertedRows.add(this.rows.get(node));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Fill the columns that the place of `node` sets or reads with the values of
|
|
202
|
+
// their slots, as far as they're known.
|
|
203
|
+
fill(node) {
|
|
204
|
+
for (const cell of this.placeCells.get(node) || []) {
|
|
205
|
+
const value = this._getSlotValue(cell);
|
|
206
|
+
|
|
207
|
+
// A place that only reads a column that other nodes write leaves the
|
|
208
|
+
// write to them:
|
|
209
|
+
if (
|
|
210
|
+
!cell.setNodes.has(node) &&
|
|
211
|
+
cell.prop.getProp(node.obj, cell.index) === undefined &&
|
|
212
|
+
(cell.setNodes.size > 0 ||
|
|
213
|
+
this._isCleared(cell) ||
|
|
214
|
+
this._getGivenValues(cell).value !== undefined)
|
|
215
|
+
) {
|
|
216
|
+
let readProps = this.readProps.get(node);
|
|
217
|
+
|
|
218
|
+
if (!readProps) {
|
|
219
|
+
readProps = new Set();
|
|
220
|
+
this.readProps.set(node, readProps);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
readProps.add(cell.prop.props[cell.index]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (value !== undefined) {
|
|
227
|
+
cell.prop.setProp(node.obj, cell.index, value);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Return the props of `node` that the places of its row set.
|
|
233
|
+
getFilledProps(node) {
|
|
234
|
+
return [...(this.placeCells.get(node) || [])]
|
|
235
|
+
.filter(({ setNodes }) => setNodes.size > 0)
|
|
236
|
+
.map(({ prop, index }) => prop.props[index]);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Return the values of the key columns `prop` of the row of `node`.
|
|
240
|
+
getProps(node, prop) {
|
|
241
|
+
return this._getCells(node, prop).map((cell) => this._getSlotValue(cell));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Return true if `node` only reads its key column `prop`, which other nodes
|
|
245
|
+
// write, so that it leaves the write to them.
|
|
246
|
+
isOnlyRead(node, prop) {
|
|
247
|
+
return !!this.readProps.get(node)?.has(prop);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Return true if a place of `node` in this pass sets the key column `prop`
|
|
251
|
+
// of its row:
|
|
252
|
+
isSet(node, prop, index) {
|
|
253
|
+
return !!this.cells.get(this._getKey(this._getRow(node), prop, index))?.setNodes.has(node);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Return the nodes whose inserts or `#ref{}` values bind the slots of the
|
|
257
|
+
// columns that the place of `node` sets or reads.
|
|
258
|
+
waitsFor(node) {
|
|
259
|
+
const waits = [];
|
|
260
|
+
|
|
261
|
+
for (const cell of this.placeCells.get(node) || []) {
|
|
262
|
+
for (const anchorCell of this._getAnchorCells(cell.slot)) {
|
|
263
|
+
const { row, prop, index } = anchorCell;
|
|
264
|
+
anchorCell.waitedNodes ??= this.nodesByRow
|
|
265
|
+
.get(row)
|
|
266
|
+
.filter(
|
|
267
|
+
(other) =>
|
|
268
|
+
(this.insertRows.has(row) && this.plan.actions.get(other) === 'insert') ||
|
|
269
|
+
this._hasReference(other, prop, index),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
for (const other of anchorCell.waitedNodes) {
|
|
273
|
+
if (this.rows.get(other) !== this.rows.get(node)) {
|
|
274
|
+
waits.push({ node: other, prop: cell.prop.props[cell.index] });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return waits;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// The place of `node` gives `target` the key of `source`, for each column of
|
|
284
|
+
// the relation. A place of a later pass only marks the columns it sets there:
|
|
285
|
+
_addPlace(node, target, targetProp, source, sourceProp, isDeferred) {
|
|
286
|
+
const targetCells = this._getCells(target, targetProp);
|
|
287
|
+
const sourceCells = this._getCells(source, sourceProp);
|
|
288
|
+
|
|
289
|
+
// A row below itself, e.g. in its own `siblings`, restates its own key and
|
|
290
|
+
// writes nothing:
|
|
291
|
+
if (isDeferred) {
|
|
292
|
+
targetCells.forEach((cell, i) => {
|
|
293
|
+
cell.isPending ||= cell !== sourceCells[i];
|
|
294
|
+
});
|
|
295
|
+
} else if (targetCells.some((cell, i) => cell !== sourceCells[i])) {
|
|
296
|
+
this._addCells(target, targetCells);
|
|
297
|
+
this._addCells(source, sourceCells);
|
|
298
|
+
|
|
299
|
+
targetCells.forEach((cell, i) => {
|
|
300
|
+
sourceCells[i].readerNodes.add(source);
|
|
301
|
+
|
|
302
|
+
// A whole `#ref{}` value wins over the key of its place, also where the
|
|
303
|
+
// place lists the row again:
|
|
304
|
+
if (!this.wholeReferences.has(this._getPlaceKey(target, cell.prop.props[cell.index]))) {
|
|
305
|
+
cell.setNodes.add(target);
|
|
306
|
+
cell.placeNodes.add(node);
|
|
307
|
+
joinCells(cell, sourceCells[i]);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// The key of a column of the row of `node` at its place: the row of its
|
|
314
|
+
// parent and the relation.
|
|
315
|
+
_getPlaceKey(node, prop) {
|
|
316
|
+
const { parent, relation } = node;
|
|
317
|
+
return `${this._getRow(node)}|${parent ? this._getRow(parent) : ''}|${relation?.name}|${prop}`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
_addCells(node, cells) {
|
|
321
|
+
let placeCells = this.placeCells.get(node);
|
|
322
|
+
|
|
323
|
+
if (!placeCells) {
|
|
324
|
+
placeCells = new Set();
|
|
325
|
+
this.placeCells.set(node, placeCells);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
for (const cell of cells) {
|
|
329
|
+
placeCells.add(cell);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return cells;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
_getCells(node, prop) {
|
|
336
|
+
return prop.props.map((_, i) => this._getCell(node, prop, i));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// The row of `node`, also of the owners of a later pass, which earlier
|
|
340
|
+
// passes wrote:
|
|
341
|
+
_getRow(node) {
|
|
342
|
+
let row = this.rows.get(node);
|
|
343
|
+
|
|
344
|
+
if (row === undefined) {
|
|
345
|
+
// A `#ref` model has the row of the model it refers to, and a new model
|
|
346
|
+
// a row of its own:
|
|
347
|
+
const { referencedNode } = node;
|
|
348
|
+
row = referencedNode
|
|
349
|
+
? this._getRow(referencedNode)
|
|
350
|
+
: node.obj.$hasId()
|
|
351
|
+
? node.rowKey
|
|
352
|
+
: `new:${this.rows.size}`;
|
|
353
|
+
this.rows.set(node, row);
|
|
354
|
+
|
|
355
|
+
const nodes = this.nodesByRow.get(row);
|
|
356
|
+
|
|
357
|
+
if (nodes) {
|
|
358
|
+
nodes.push(node);
|
|
359
|
+
} else {
|
|
360
|
+
this.nodesByRow.set(row, [node]);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return row;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
_getKey(row, prop, index) {
|
|
368
|
+
let descriptions = this.descriptions.get(prop);
|
|
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]}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
_getCell(node, prop, index) {
|
|
379
|
+
const row = this._getRow(node);
|
|
380
|
+
const key = this._getKey(row, prop, index);
|
|
381
|
+
let cell = this.cells.get(key);
|
|
382
|
+
|
|
383
|
+
if (!cell) {
|
|
384
|
+
cell = {
|
|
385
|
+
key,
|
|
386
|
+
row,
|
|
387
|
+
node,
|
|
388
|
+
prop,
|
|
389
|
+
index,
|
|
390
|
+
isId: node.modelClass.getIdPropertyArray().includes(prop.props[index]),
|
|
391
|
+
setNodes: new Set(),
|
|
392
|
+
placeNodes: new Set(),
|
|
393
|
+
readerNodes: new Set(),
|
|
394
|
+
isPending: false,
|
|
395
|
+
slot: null,
|
|
396
|
+
};
|
|
397
|
+
cell.slot = { cells: [cell] };
|
|
398
|
+
this.cells.set(key, cell);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return cell;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// The cells that give a slot its value, known once the slots are built: the
|
|
405
|
+
// ones that no place of this pass sets, unless a place of a later pass sets
|
|
406
|
+
// them, and the ones that a node gives whose own place doesn't set them.
|
|
407
|
+
_getAnchorCells(slot) {
|
|
408
|
+
return (slot.anchorCells ??= slot.cells.filter((cell) =>
|
|
409
|
+
cell.setNodes.size === 0 ? !cell.isPending : this._getGivenValues(cell).value !== undefined,
|
|
410
|
+
));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// The values that the nodes of a row give a column that their own places
|
|
414
|
+
// don't set, as the graph gives them: `value` from any write, `writtenValue`
|
|
415
|
+
// from a patch or relate. A `#ref{}` is `UNRESOLVED` until it's resolved.
|
|
416
|
+
_getGivenValues(cell) {
|
|
417
|
+
if (!cell.givenValues) {
|
|
418
|
+
const { row, prop, index, setNodes } = cell;
|
|
419
|
+
const { actions, graphOptions } = this.plan;
|
|
420
|
+
const givenValues = {};
|
|
421
|
+
|
|
422
|
+
for (const node of this.nodesByRow.get(row)) {
|
|
423
|
+
const action = actions.get(node);
|
|
424
|
+
|
|
425
|
+
if (
|
|
426
|
+
!setNodes.has(node) &&
|
|
427
|
+
!isPlacingReference(this.plan, node) &&
|
|
428
|
+
// A whole `#ref{}` doesn't win over the key of another place:
|
|
429
|
+
!(setNodes.size > 0 && hasWholeReference(this.plan, node, prop.props[index])) &&
|
|
430
|
+
['insert', 'patch', 'relate'].includes(action) &&
|
|
431
|
+
!node.toBeUnrelatedOrDeleted &&
|
|
432
|
+
(action === 'insert' || graphOptions.shouldPatch(node) || graphOptions.shouldUpdate(node))
|
|
433
|
+
) {
|
|
434
|
+
const value = this._hasReference(node, prop, index)
|
|
435
|
+
? UNRESOLVED
|
|
436
|
+
: (prop.getProp(node.obj, index) ??
|
|
437
|
+
(node.referencedNode && !setNodes.has(node.referencedNode)
|
|
438
|
+
? prop.getProp(node.referencedNode.obj, index)
|
|
439
|
+
: undefined));
|
|
440
|
+
if (givenValues.value === undefined) {
|
|
441
|
+
givenValues.value = value;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (givenValues.writtenValue === undefined && action !== 'insert') {
|
|
445
|
+
givenValues.writtenValue = value;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (givenValues.writtenValue !== undefined) {
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (givenValues.value === UNRESOLVED || givenValues.writtenValue === UNRESOLVED) {
|
|
455
|
+
return givenValues;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
cell.givenValues = givenValues;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return cell.givenValues;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Return true if a column of `node` holds a `#ref{}` that isn't resolved yet:
|
|
465
|
+
_hasReference(node, prop, index) {
|
|
466
|
+
return (
|
|
467
|
+
!this.resolvedNodes.has(node) &&
|
|
468
|
+
node.propertyReferences.some(({ path }) => path[0] === prop.props[index])
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// The value of an anchor cell: of a row that this pass inserts once it's
|
|
473
|
+
// inserted, with `null` for the columns the graph doesn't give, a value that
|
|
474
|
+
// a patch or relate writes, which wins over the inserted one, an id, or the
|
|
475
|
+
// current value.
|
|
476
|
+
_getAnchorValue(cell) {
|
|
477
|
+
const { row, prop, index } = cell;
|
|
478
|
+
const nodes = this.nodesByRow.get(row);
|
|
479
|
+
const [node] = nodes;
|
|
480
|
+
const getValue = (obj) => (obj ? prop.getProp(obj, index) : undefined);
|
|
481
|
+
const { writtenValue } = this._getGivenValues(cell);
|
|
482
|
+
|
|
483
|
+
if (this.insertRows.has(row)) {
|
|
484
|
+
const insertNode = nodes.find((node) => this.plan.actions.get(node) === 'insert');
|
|
485
|
+
return writtenValue ?? getValue(insertNode.obj) ?? null;
|
|
486
|
+
} else if (writtenValue !== undefined) {
|
|
487
|
+
return writtenValue;
|
|
488
|
+
} else if (cell.isId) {
|
|
489
|
+
return getValue(node.obj) ?? getValue(node.existing?.obj);
|
|
490
|
+
} else {
|
|
491
|
+
// A related row that isn't loaded is found by the key that its
|
|
492
|
+
// BelongsToOne place reads, so that is its key:
|
|
493
|
+
const relateNode = nodes.find(
|
|
494
|
+
(node) =>
|
|
495
|
+
this.plan.actions.get(node) === 'relate' &&
|
|
496
|
+
hasOwnerKey(node.relation) &&
|
|
497
|
+
node.relation.relatedProp.props.includes(prop.props[index]),
|
|
498
|
+
);
|
|
499
|
+
const currentValue = this._getCurrentValue(cell);
|
|
500
|
+
return currentValue === undefined && relateNode ? getValue(relateNode.obj) : currentValue;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Return true if an anchor cell waits for an insert or a `#ref{}` value:
|
|
505
|
+
_isWaiting(cell) {
|
|
506
|
+
return (
|
|
507
|
+
(this.insertRows.has(cell.row) && !this.insertedRows.has(cell.row)) ||
|
|
508
|
+
this._getGivenValues(cell).writtenValue === UNRESOLVED
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// The value of an anchor cell that is known before the writes, as a key to
|
|
513
|
+
// compare: the given value, or for the id of a new row the row itself, which
|
|
514
|
+
// differs from all other values.
|
|
515
|
+
_getKnownKey(cell) {
|
|
516
|
+
const { row, prop, index } = cell;
|
|
517
|
+
|
|
518
|
+
if (this.insertRows.has(row)) {
|
|
519
|
+
const { value } = this._getGivenValues(cell);
|
|
520
|
+
return value === UNRESOLVED
|
|
521
|
+
? undefined
|
|
522
|
+
: value !== undefined
|
|
523
|
+
? `=${value}`
|
|
524
|
+
: cell.isId
|
|
525
|
+
? row
|
|
526
|
+
: undefined;
|
|
527
|
+
} else {
|
|
528
|
+
const value = this._isWaiting(cell) ? undefined : this._getAnchorValue(cell);
|
|
529
|
+
return value !== undefined ? `=${value}` : undefined;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// The value of the slot of `cell`, from its anchors, which agree, or without
|
|
534
|
+
// any, from the current values of the columns its places read. `undefined`
|
|
535
|
+
// until the inserts and `#ref{}` values that bind it are written.
|
|
536
|
+
_getSlotValue({ slot }) {
|
|
537
|
+
if (!('value' in slot)) {
|
|
538
|
+
const cells = this._getAnchorCells(slot);
|
|
539
|
+
|
|
540
|
+
if (cells.length === 0) {
|
|
541
|
+
// Without anchors, the rows that the places read their keys from give it:
|
|
542
|
+
slot.value = slot.cells
|
|
543
|
+
.filter((cell) => cell.readerNodes.size > 0)
|
|
544
|
+
.map((cell) => this._getCurrentValue(cell))
|
|
545
|
+
.find((value) => value !== undefined);
|
|
546
|
+
} else if (!cells.some((cell) => this._isWaiting(cell))) {
|
|
547
|
+
// Once nothing waits, the value doesn't change anymore:
|
|
548
|
+
slot.value = cells
|
|
549
|
+
.map((cell) => this._getAnchorValue(cell))
|
|
550
|
+
.find((value) => value !== undefined);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return slot.value;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// The current value of the column of `cell`: of its current row, or for the
|
|
558
|
+
// owners from earlier passes, what these passes wrote into their models.
|
|
559
|
+
_getCurrentValue(cell) {
|
|
560
|
+
const { row, prop, index } = cell;
|
|
561
|
+
|
|
562
|
+
if (this._isCleared(cell)) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
for (const node of this.nodesByRow.get(row)) {
|
|
567
|
+
const value =
|
|
568
|
+
!this.activeNodes.has(node) && prop.getProp(node.obj, index) !== undefined
|
|
569
|
+
? prop.getProp(node.obj, index)
|
|
570
|
+
: node.existing
|
|
571
|
+
? prop.getProp(node.existing.obj, index)
|
|
572
|
+
: undefined;
|
|
573
|
+
|
|
574
|
+
if (value !== undefined) {
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Return true if another node than the ones that read the column of `cell`
|
|
581
|
+
// clears it in this pass:
|
|
582
|
+
_isCleared(cell) {
|
|
583
|
+
const clearingNodes = this.clearingNodes.get(cell.key);
|
|
584
|
+
return (cell.isCleared ??=
|
|
585
|
+
!!clearingNodes && [...clearingNodes].some((node) => !cell.readerNodes.has(node)));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
_getSlots() {
|
|
589
|
+
return new Set([...this.cells.values()].map((cell) => cell.slot));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Throw for slots whose anchors, or whose rows without any, give different
|
|
593
|
+
// values that are known before the writes.
|
|
594
|
+
_checkSlots() {
|
|
595
|
+
for (const slot of [...this._getSlots()].filter(({ cells }) => cells.length > 1)) {
|
|
596
|
+
const anchorCells = this._getAnchorCells(slot);
|
|
597
|
+
const known = (
|
|
598
|
+
anchorCells.length > 0
|
|
599
|
+
? anchorCells.map((cell) => [cell, this._getKnownKey(cell)])
|
|
600
|
+
: slot.cells
|
|
601
|
+
.filter((cell) => cell.readerNodes.size > 0)
|
|
602
|
+
.map((cell) => [cell, this._getCurrentValue(cell)])
|
|
603
|
+
).filter(([, value]) => value !== undefined);
|
|
604
|
+
const [cell, value] = known[0] || [];
|
|
605
|
+
const [otherCell] = known.find(([, other]) => String(other) !== String(value)) || [];
|
|
606
|
+
|
|
607
|
+
if (otherCell) {
|
|
608
|
+
// Name the column that the places set, e.g. not the id they read it from:
|
|
609
|
+
const { prop, index, node } = slot.cells.find((cell) => cell.setNodes.size > 0) || cell;
|
|
610
|
+
throw node.modelClass.createValidationError({
|
|
611
|
+
type: ValidationErrorType.InvalidGraph,
|
|
612
|
+
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)}`,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Join the slots of both cells, moving the cells of the smaller one:
|
|
620
|
+
function joinCells(cell, otherCell) {
|
|
621
|
+
const [slot, otherSlot] =
|
|
622
|
+
cell.slot.cells.length >= otherCell.slot.cells.length
|
|
623
|
+
? [cell.slot, otherCell.slot]
|
|
624
|
+
: [otherCell.slot, cell.slot];
|
|
625
|
+
|
|
626
|
+
if (slot !== otherSlot) {
|
|
627
|
+
for (const other of otherSlot.cells) {
|
|
628
|
+
other.slot = slot;
|
|
629
|
+
slot.cells.push(other);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
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
|
+
// Return true if `node` is a `#ref` model in a many-to-many or BelongsToOne
|
|
809
|
+
// relation whose model writes its row, so that it only places the row there,
|
|
810
|
+
// and neither sets nor gives the keys of that row.
|
|
811
|
+
function isPlacingReference(plan, node) {
|
|
812
|
+
const { referencedNode, relation } = node;
|
|
813
|
+
return (
|
|
814
|
+
!!referencedNode &&
|
|
815
|
+
(relation.keySide === 'join' || hasOwnerKey(relation)) &&
|
|
816
|
+
plan.isCoveredByModel(node)
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Return true if `node` gives `prop` a whole `#ref{}` value that wins over the
|
|
821
|
+
// key that its place in its parent sets, or that any place sets when `node` is
|
|
822
|
+
// inserted (Vincit#1378). The column then keeps that value for all its readers.
|
|
823
|
+
function hasWholeReference(plan, node, prop) {
|
|
824
|
+
return (
|
|
825
|
+
(plan.actions.get(node) === 'insert' ||
|
|
826
|
+
(node.relation?.keySide === 'related' && node.relation.relatedProp.props.includes(prop))) &&
|
|
827
|
+
node.propertyReferences.some(
|
|
828
|
+
({ path, isWholeValue }) => isWholeValue && path.length === 1 && path[0] === prop,
|
|
829
|
+
)
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Replace a `#ref{}` in a prop of `node` with the value it refers to:
|
|
834
|
+
function resolvePropertyReference(node, { path, match, refPath, isWholeValue, referencedNode }) {
|
|
835
|
+
const value = get(referencedNode.obj, refPath);
|
|
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));
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
export function hasOwnerKey(relation) {
|
|
903
|
+
return relation.keySide === 'owner' && relation.isOneToOne();
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function hasOwnerKeyArray(relation) {
|
|
907
|
+
return relation.keySide === 'owner' && !relation.isOneToOne();
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Return true if both relations keep the keys of their related rows in the same
|
|
911
|
+
// columns, also the same JSON paths of them.
|
|
912
|
+
export function hasSameKey(relation, otherRelation) {
|
|
913
|
+
const { relatedProp } = relation;
|
|
914
|
+
const otherProp = otherRelation?.keySide === 'related' ? otherRelation.relatedProp : null;
|
|
915
|
+
return (
|
|
916
|
+
!!otherProp &&
|
|
917
|
+
relation.relatedModelClass.getTableName() === otherRelation.relatedModelClass.getTableName() &&
|
|
918
|
+
hasSameColumns(relatedProp, otherProp)
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Return true if both props are the same columns, also the same JSON paths of
|
|
923
|
+
// them.
|
|
924
|
+
export function hasSameColumns(prop, otherProp) {
|
|
925
|
+
return (
|
|
926
|
+
prop.size === otherProp.size &&
|
|
927
|
+
prop.props.every((_, i) => prop.propDescription(i) === otherProp.propDescription(i))
|
|
928
|
+
);
|
|
929
|
+
}
|