objection 4.0.0-rc.2 → 4.0.0-rc.4

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.
@@ -1,40 +1,37 @@
1
- import { difference, get, jsonEquals, set } from '../../utils/objectUtils.js';
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
- // 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.
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;
20
18
  this.activeNodes = new Set(plan.activeNodes);
21
19
  this.rows = new Map();
22
20
  this.nodesByRow = new Map();
23
- this.cells = new Map();
21
+ // The cells in their order, and those of each row by their props:
22
+ this.cells = [];
23
+ this.cellsByRow = new Map();
24
24
  // The props that each node only reads, and that other nodes write:
25
25
  this.readProps = new Map();
26
- this.descriptions = new Map();
27
- // The columns that each place sets or reads:
26
+ // The cells that each place sets or reads:
28
27
  this.placeCells = new Map();
29
- // The rows that this pass inserts, and the ones that it inserted already:
28
+ // The rows that the graph inserts, and the ones that it inserted already:
30
29
  this.insertRows = new Set();
31
30
  this.insertedRows = new Set();
32
- // The rows that only a later pass inserts:
33
- this.laterRows = new Set();
34
31
  // The nodes whose `#ref{}` values are resolved:
35
32
  this.resolvedNodes = new Set();
36
33
 
37
- // The rows are decided before the inserts give new rows their ids:
34
+ // 1. The rows, decided before the inserts give new rows their ids:
38
35
  for (const node of plan.activeNodes) {
39
36
  const row = this._getRow(node);
40
37
 
@@ -43,8 +40,7 @@ export class GraphKeys {
43
40
  }
44
41
  }
45
42
 
46
- // The columns that a whole `#ref{}` gives the rows at their places, see
47
- // `hasWholeReference()`:
43
+ // 2. The places whose cells a whole `#ref{}` gives, see `hasWholeReference()`:
48
44
  this.wholeReferences = new Set(
49
45
  plan.activeNodes.flatMap((node) =>
50
46
  node.propertyReferences
@@ -53,22 +49,12 @@ export class GraphKeys {
53
49
  ),
54
50
  );
55
51
 
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]) {
52
+ // 3. The places. The ones that the graph ignores, and many-to-many places,
53
+ // only read:
54
+ for (const node of plan.activeNodes) {
68
55
  const { parent, relation } = node;
69
56
 
70
57
  if (parent && !node.toBeUnrelatedOrDeleted) {
71
- const isDeferred = deferredNodes.has(node) && !this._isMovedWithRow(node);
72
58
  const [target, targetProp, source, sourceProp] =
73
59
  relation.keySide === 'related'
74
60
  ? [node, relation.relatedProp, parent, relation.ownerProp]
@@ -76,13 +62,9 @@ export class GraphKeys {
76
62
  ? [parent, relation.ownerProp, node, relation.relatedProp]
77
63
  : [];
78
64
 
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))) {
65
+ if (target && !isPlacingReference(plan, target) && plan.actions.get(node) !== 'ignore') {
66
+ this._addPlace(node, target, targetProp, source, sourceProp);
67
+ } else if (relation.keySide !== 'owner' || hasOwnerKey(relation)) {
86
68
  // Arrays of keys are written by `setKeyArrays()`. Join rows read the
87
69
  // keys of both sides, and wait for them like the other places:
88
70
  const cells = [
@@ -101,10 +83,8 @@ export class GraphKeys {
101
83
  }
102
84
  }
103
85
 
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:
86
+ // 4. The clears, with the nodes that clear the cells. The places of a node
87
+ // read them as `null` if another node clears them:
108
88
  this.clearingNodes = new Map();
109
89
  const addClearingNode = (row, prop, node) =>
110
90
  prop.props.forEach((_, i) => {
@@ -121,90 +101,29 @@ export class GraphKeys {
121
101
  }
122
102
 
123
103
  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
- }
104
+ for (const relation of plan.getRemovedOwnerRelations(node)) {
105
+ addClearingNode(this._getRow(node), relation.ownerProp, node);
134
106
  }
135
107
  }
136
108
 
109
+ // 5. The anchors of each slot must agree, and no clear may place rows:
137
110
  this._checkSlots();
138
111
  }
139
112
 
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.
113
+ // Bind the cells of the rows of the inserted `nodes` to their values.
195
114
  setInserted(nodes) {
196
115
  for (const node of nodes) {
197
116
  this.insertedRows.add(this.rows.get(node));
198
117
  }
199
118
  }
200
119
 
201
- // Fill the columns that the place of `node` sets or reads with the values of
120
+ // Fill the cells that the place of `node` sets or reads with the values of
202
121
  // their slots, as far as they're known.
203
122
  fill(node) {
204
123
  for (const cell of this.placeCells.get(node) || []) {
205
124
  const value = this._getSlotValue(cell);
206
125
 
207
- // A place that only reads a column that other nodes write leaves the
126
+ // A place that only reads a cell that other nodes write leaves the
208
127
  // write to them:
209
128
  if (
210
129
  !cell.setNodes.has(node) &&
@@ -236,27 +155,81 @@ export class GraphKeys {
236
155
  .map(({ prop, index }) => prop.props[index]);
237
156
  }
238
157
 
158
+ // Return the cells of the row of `node` that the graph sets, unless `node`
159
+ // only reads them:
160
+ getSetCells(node) {
161
+ const row = this._getRow(node);
162
+ return [...(this.placeCells.get(node) || [])].filter(
163
+ (cell) =>
164
+ cell.row === row &&
165
+ cell.setNodes.size > 0 &&
166
+ !this.isOnlyRead(node, cell.prop.props[cell.index]),
167
+ );
168
+ }
169
+
170
+ // Return true if a place other than the places of the row of `node` through
171
+ // its BelongsToOne `relation` sets the key of the relation, e.g. the place
172
+ // that moves the row.
173
+ isSetByOtherPlaces(node, relation) {
174
+ const row = this._getRow(node);
175
+ const cells = this.cellsByRow.get(row);
176
+ return relation.ownerProp.props.some((_, i) =>
177
+ [...(cells?.get(relation.ownerProp.propDescription(i))?.placeNodes || [])].some(
178
+ (other) => !(other.relation === relation && this._getRow(other.parent) === row),
179
+ ),
180
+ );
181
+ }
182
+
239
183
  // Return the values of the key columns `prop` of the row of `node`.
240
184
  getProps(node, prop) {
241
185
  return this._getCells(node, prop).map((cell) => this._getSlotValue(cell));
242
186
  }
243
187
 
188
+ // Return the values of the key columns `prop` of the row of `node` at which
189
+ // its relations are found before the writes: the current values of the rows
190
+ // that give the final ones, as rows follow a key that changes with them, or
191
+ // without such rows, the current value of the row itself. Clears don't count,
192
+ // as the removals of the relations that are read cause them. Keys of new rows
193
+ // have no related rows yet.
194
+ getReadProps(node, prop) {
195
+ return this._getCells(node, prop).map((cell) => {
196
+ const values = this._getAnchorCells(cell.slot).map((anchor) =>
197
+ this.insertRows.has(anchor.row)
198
+ ? null
199
+ : (this._getStoredValue(anchor) ?? this._getAnchorValue(anchor)),
200
+ );
201
+ const value = this._getSlotValue(cell);
202
+ return values.length > 0
203
+ ? (values.find((other) => nonStrictEquals(other, value)) ?? values[0])
204
+ : this._getStoredValue(cell);
205
+ });
206
+ }
207
+
244
208
  // Return true if `node` only reads its key column `prop`, which other nodes
245
209
  // write, so that it leaves the write to them.
246
210
  isOnlyRead(node, prop) {
247
211
  return !!this.readProps.get(node)?.has(prop);
248
212
  }
249
213
 
250
- // Return true if a place of `node` in this pass sets the key column `prop`
251
- // of its row:
214
+ // Return true if a place of `node` sets the key column `prop` of its row:
252
215
  isSet(node, prop, index) {
253
- return !!this.cells.get(this._getKey(this._getRow(node), prop, index))?.setNodes.has(node);
216
+ const cell = this.cellsByRow.get(this._getRow(node))?.get(prop.propDescription(index));
217
+ return !!cell?.setNodes.has(node);
254
218
  }
255
219
 
256
- // Return the nodes whose inserts or `#ref{}` values bind the slots of the
257
- // columns that the place of `node` sets or reads.
220
+ // Return the nodes whose inserts, `#ref{}` values or changes bind the slots
221
+ // of the cells that the place of `node` sets or reads, and whether they
222
+ // change them.
258
223
  waitsFor(node) {
224
+ const { relation, parent } = node;
259
225
  const waits = [];
226
+ const addWaits = (cell, nodes, isKeyChange) => {
227
+ for (const other of nodes) {
228
+ if (this.rows.get(other) !== this.rows.get(node)) {
229
+ waits.push({ node: other, prop: cell.prop.props[cell.index], isKeyChange });
230
+ }
231
+ }
232
+ };
260
233
 
261
234
  for (const cell of this.placeCells.get(node) || []) {
262
235
  for (const anchorCell of this._getAnchorCells(cell.slot)) {
@@ -268,11 +241,21 @@ export class GraphKeys {
268
241
  (this.insertRows.has(row) && this.plan.actions.get(other) === 'insert') ||
269
242
  this._hasReference(other, prop, index),
270
243
  );
244
+ addWaits(cell, anchorCell.waitedNodes, false);
271
245
 
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
- }
246
+ // Only the places that set a cell write the changes of its slot:
247
+ if (cell.setNodes.has(node)) {
248
+ addWaits(cell, [this._getChangingNode(anchorCell)].filter(Boolean), true);
249
+ }
250
+ }
251
+ }
252
+
253
+ // Join rows are inserted after all rows, but read the keys of their owners,
254
+ // which patches may change:
255
+ if (relation?.keySide === 'join') {
256
+ for (const cell of this._getCells(parent, relation.ownerProp)) {
257
+ for (const anchorCell of this._getAnchorCells(cell.slot)) {
258
+ addWaits(cell, [this._getChangingNode(anchorCell)].filter(Boolean), true);
276
259
  }
277
260
  }
278
261
  }
@@ -280,19 +263,30 @@ export class GraphKeys {
280
263
  return waits;
281
264
  }
282
265
 
266
+ // Return the patch or relate that changes the value of an anchor cell, or
267
+ // may change it, when its row isn't loaded. A foreign key may reference the
268
+ // cell, so the rows that read the new value wait for it (#428). Ids don't
269
+ // change, see `checkChangedIds()`, and rows without ids are found by the
270
+ // values that the graph gives them.
271
+ _getChangingNode(cell) {
272
+ const { writtenValue, writingNode } = this._getGivenValues(cell);
273
+ return writingNode &&
274
+ !cell.isId &&
275
+ (this.insertRows.has(cell.row) || (writingNode.referencedNode || writingNode).obj.$hasId()) &&
276
+ !nonStrictEquals(writtenValue, this._getStoredValue(cell))
277
+ ? writingNode
278
+ : null;
279
+ }
280
+
283
281
  // 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) {
282
+ // the relation:
283
+ _addPlace(node, target, targetProp, source, sourceProp) {
286
284
  const targetCells = this._getCells(target, targetProp);
287
285
  const sourceCells = this._getCells(source, sourceProp);
288
286
 
289
287
  // A row below itself, e.g. in its own `siblings`, restates its own key and
290
288
  // 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])) {
289
+ if (targetCells.some((cell, i) => cell !== sourceCells[i])) {
296
290
  this._addCells(target, targetCells);
297
291
  this._addCells(source, sourceCells);
298
292
 
@@ -300,8 +294,11 @@ export class GraphKeys {
300
294
  sourceCells[i].readerNodes.add(source);
301
295
 
302
296
  // 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]))) {
297
+ // place lists the row again. Most graphs have none, so skip the keys:
298
+ if (
299
+ this.wholeReferences.size === 0 ||
300
+ !this.wholeReferences.has(this._getPlaceKey(target, cell.prop.props[cell.index]))
301
+ ) {
305
302
  cell.setNodes.add(target);
306
303
  cell.placeNodes.add(node);
307
304
  joinCells(cell, sourceCells[i]);
@@ -310,7 +307,7 @@ export class GraphKeys {
310
307
  }
311
308
  }
312
309
 
313
- // The key of a column of the row of `node` at its place: the row of its
310
+ // The key of a cell of the row of `node` at its place: the row of its
314
311
  // parent and the relation.
315
312
  _getPlaceKey(node, prop) {
316
313
  const { parent, relation } = node;
@@ -336,8 +333,7 @@ export class GraphKeys {
336
333
  return prop.props.map((_, i) => this._getCell(node, prop, i));
337
334
  }
338
335
 
339
- // The row of `node`, also of the owners of a later pass, which earlier
340
- // passes wrote:
336
+ // The row of `node`:
341
337
  _getRow(node) {
342
338
  let row = this.rows.get(node);
343
339
 
@@ -365,24 +361,24 @@ export class GraphKeys {
365
361
  }
366
362
 
367
363
  _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]}`;
364
+ return `${row}|${prop.propDescription(index)}`;
376
365
  }
377
366
 
378
367
  _getCell(node, prop, index) {
379
368
  const row = this._getRow(node);
380
- const key = this._getKey(row, prop, index);
381
- let cell = this.cells.get(key);
369
+ const description = prop.propDescription(index);
370
+ let cells = this.cellsByRow.get(row);
371
+
372
+ if (!cells) {
373
+ cells = new Map();
374
+ this.cellsByRow.set(row, cells);
375
+ }
376
+
377
+ let cell = cells.get(description);
382
378
 
383
379
  if (!cell) {
384
380
  cell = {
385
- key,
381
+ key: this._getKey(row, prop, index),
386
382
  row,
387
383
  node,
388
384
  prop,
@@ -391,26 +387,26 @@ export class GraphKeys {
391
387
  setNodes: new Set(),
392
388
  placeNodes: new Set(),
393
389
  readerNodes: new Set(),
394
- isPending: false,
395
390
  slot: null,
396
391
  };
397
392
  cell.slot = { cells: [cell] };
398
- this.cells.set(key, cell);
393
+ this.cells.push(cell);
394
+ cells.set(description, cell);
399
395
  }
400
396
 
401
397
  return cell;
402
398
  }
403
399
 
404
400
  // 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.
401
+ // ones that no place sets, and the ones that a node gives whose own place
402
+ // doesn't set them.
407
403
  _getAnchorCells(slot) {
408
- return (slot.anchorCells ??= slot.cells.filter((cell) =>
409
- cell.setNodes.size === 0 ? !cell.isPending : this._getGivenValues(cell).value !== undefined,
404
+ return (slot.anchorCells ??= slot.cells.filter(
405
+ (cell) => cell.setNodes.size === 0 || this._getGivenValues(cell).value !== undefined,
410
406
  ));
411
407
  }
412
408
 
413
- // The values that the nodes of a row give a column that their own places
409
+ // The values that the nodes of a row give a cell that their own places
414
410
  // don't set, as the graph gives them: `value` from any write, `writtenValue`
415
411
  // from a patch or relate. A `#ref{}` is `UNRESOLVED` until it's resolved.
416
412
  _getGivenValues(cell) {
@@ -441,8 +437,13 @@ export class GraphKeys {
441
437
  givenValues.value = value;
442
438
  }
443
439
 
444
- if (givenValues.writtenValue === undefined && action !== 'insert') {
440
+ if (
441
+ givenValues.writtenValue === undefined &&
442
+ action !== 'insert' &&
443
+ value !== undefined
444
+ ) {
445
445
  givenValues.writtenValue = value;
446
+ givenValues.writingNode = node;
446
447
  }
447
448
 
448
449
  if (givenValues.writtenValue !== undefined) {
@@ -461,7 +462,7 @@ export class GraphKeys {
461
462
  return cell.givenValues;
462
463
  }
463
464
 
464
- // Return true if a column of `node` holds a `#ref{}` that isn't resolved yet:
465
+ // Return true if a cell of `node` holds a `#ref{}` that isn't resolved yet:
465
466
  _hasReference(node, prop, index) {
466
467
  return (
467
468
  !this.resolvedNodes.has(node) &&
@@ -469,8 +470,8 @@ export class GraphKeys {
469
470
  );
470
471
  }
471
472
 
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
473
+ // The value of an anchor cell: of a row that the graph inserts once it's
474
+ // inserted, with `null` for the cells the graph doesn't give, a value that
474
475
  // a patch or relate writes, which wins over the inserted one, an id, or the
475
476
  // current value.
476
477
  _getAnchorValue(cell) {
@@ -531,7 +532,7 @@ export class GraphKeys {
531
532
  }
532
533
 
533
534
  // 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
+ // any, from the current values of the cells its places read. `undefined`
535
536
  // until the inserts and `#ref{}` values that bind it are written.
536
537
  _getSlotValue({ slot }) {
537
538
  if (!('value' in slot)) {
@@ -554,14 +555,16 @@ export class GraphKeys {
554
555
  return slot.value;
555
556
  }
556
557
 
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.
558
+ // The current value of `cell`, `null` once it's cleared:
559
559
  _getCurrentValue(cell) {
560
- const { row, prop, index } = cell;
560
+ return this._isCleared(cell) ? null : this._getStoredValue(cell);
561
+ }
561
562
 
562
- if (this._isCleared(cell)) {
563
- return null;
564
- }
563
+ // The value of `cell` in its row before any clears: of its
564
+ // current row, or what an ignored node gives it, as the graph doesn't write
565
+ // it there:
566
+ _getStoredValue(cell) {
567
+ const { row, prop, index } = cell;
565
568
 
566
569
  for (const node of this.nodesByRow.get(row)) {
567
570
  const value =
@@ -577,36 +580,46 @@ export class GraphKeys {
577
580
  }
578
581
  }
579
582
 
580
- // Return true if another node than the ones that read the column of `cell`
581
- // clears it in this pass:
583
+ // Return true if another node than the ones that read `cell` clears it:
582
584
  _isCleared(cell) {
583
585
  const clearingNodes = this.clearingNodes.get(cell.key);
584
586
  return (cell.isCleared ??=
585
587
  !!clearingNodes && [...clearingNodes].some((node) => !cell.readerNodes.has(node)));
586
588
  }
587
589
 
588
- _getSlots() {
589
- return new Set([...this.cells.values()].map((cell) => cell.slot));
590
- }
591
-
592
590
  // Throw for slots whose anchors, or whose rows without any, give different
593
591
  // values that are known before the writes.
594
592
  _checkSlots() {
595
- for (const slot of [...this._getSlots()].filter(({ cells }) => cells.length > 1)) {
593
+ const slots = new Set(this.cells.map((cell) => cell.slot));
594
+
595
+ for (const slot of [...slots].filter(({ cells }) => cells.length > 1)) {
596
596
  const anchorCells = this._getAnchorCells(slot);
597
+ const readCells = slot.cells.filter((cell) => cell.readerNodes.size > 0);
598
+ const placingCell = slot.cells.find((cell) => cell.setNodes.size > 0);
599
+ // A key that one node clears can't place the rows of another node:
600
+ const clearedCell = (anchorCells.length > 0 ? anchorCells : readCells).find((cell) =>
601
+ this._isCleared(cell),
602
+ );
603
+
604
+ if (clearedCell && placingCell) {
605
+ const { prop, index, node } = clearedCell;
606
+ throw node.modelClass.createValidationError({
607
+ type: ValidationErrorType.InvalidGraph,
608
+ 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)}`,
609
+ });
610
+ }
611
+
597
612
  const known = (
598
613
  anchorCells.length > 0
599
614
  ? anchorCells.map((cell) => [cell, this._getKnownKey(cell)])
600
- : slot.cells
601
- .filter((cell) => cell.readerNodes.size > 0)
602
- .map((cell) => [cell, this._getCurrentValue(cell)])
615
+ : readCells.map((cell) => [cell, this._getCurrentValue(cell)])
603
616
  ).filter(([, value]) => value !== undefined);
604
617
  const [cell, value] = known[0] || [];
605
618
  const [otherCell] = known.find(([, other]) => String(other) !== String(value)) || [];
606
619
 
607
620
  if (otherCell) {
608
621
  // 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;
622
+ const { prop, index, node } = placingCell || cell;
610
623
  throw node.modelClass.createValidationError({
611
624
  type: ValidationErrorType.InvalidGraph,
612
625
  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 +644,6 @@ function joinCells(cell, otherCell) {
631
644
  }
632
645
  }
633
646
 
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
647
  // Return true if `node` is a `#ref` model in a many-to-many or BelongsToOne
809
648
  // relation whose model writes its row, so that it only places the row there,
810
649
  // and neither sets nor gives the keys of that row.
@@ -819,7 +658,7 @@ function isPlacingReference(plan, node) {
819
658
 
820
659
  // Return true if `node` gives `prop` a whole `#ref{}` value that wins over the
821
660
  // 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.
661
+ // inserted (Vincit#1378). The cell then keeps that value for all its readers.
823
662
  function hasWholeReference(plan, node, prop) {
824
663
  return (
825
664
  (plan.actions.get(node) === 'insert' ||
@@ -830,83 +669,15 @@ function hasWholeReference(plan, node, prop) {
830
669
  );
831
670
  }
832
671
 
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));
672
+ // Return true if the values of two keys differ, with missing values as `null`.
673
+ export function isOtherKey(values, otherValues) {
674
+ return values.some((value, i) => !nonStrictEquals(value ?? null, otherValues[i] ?? null));
900
675
  }
901
676
 
902
677
  export function hasOwnerKey(relation) {
903
678
  return relation.keySide === 'owner' && relation.isOneToOne();
904
679
  }
905
680
 
906
- function hasOwnerKeyArray(relation) {
907
- return relation.keySide === 'owner' && !relation.isOneToOne();
908
- }
909
-
910
681
  // Return true if both relations keep the keys of their related rows in the same
911
682
  // columns, also the same JSON paths of them.
912
683
  export function hasSameKey(relation, otherRelation) {