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.
@@ -1,19 +1,17 @@
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;
@@ -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
- this.descriptions = new Map();
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 this pass inserts, and the ones that it inserted already:
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 are decided before the inserts give new rows their ids:
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 columns that a whole `#ref{}` gives the rows at their places, see
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
- 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]) {
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
- !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))) {
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 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:
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 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
- }
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
- // 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.
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 columns that the place of `node` sets or reads with the values of
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 column that other nodes write leaves the
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` in this pass sets the key column `prop`
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
- // columns that the place of `node` sets or reads.
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. A place of a later pass only marks the columns it sets there:
285
- _addPlace(node, target, targetProp, source, sourceProp, isDeferred) {
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 (isDeferred) {
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 column of the row of `node` at its place: the row of its
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`, also of the owners of a later pass, which earlier
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
- 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]}`;
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 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.
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((cell) =>
409
- cell.setNodes.size === 0 ? !cell.isPending : this._getGivenValues(cell).value !== undefined,
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 column that their own places
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 column of `node` holds a `#ref{}` that isn't resolved yet:
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 this pass inserts once it's
473
- // inserted, with `null` for the columns the graph doesn't give, a value that
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 columns its places read. `undefined`
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 the column of `cell`: of its current row, or for the
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
- const { row, prop, index } = cell;
494
+ return this._isCleared(cell) ? null : this._getStoredValue(cell);
495
+ }
561
496
 
562
- if (this._isCleared(cell)) {
563
- return null;
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 the column of `cell`
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
- for (const slot of [...this._getSlots()].filter(({ cells }) => cells.length > 1)) {
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
- : slot.cells
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 } = slot.cells.find((cell) => cell.setNodes.size > 0) || cell;
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 column then keeps that value for all its readers.
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
- // 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));
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) {