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.
- package/lib/queryBuilder/InternalOptions.js +4 -0
- package/lib/queryBuilder/graph/GraphLoader.js +201 -126
- package/lib/queryBuilder/graph/GraphNode.js +18 -3
- package/lib/queryBuilder/graph/GraphOptions.js +24 -49
- package/lib/queryBuilder/graph/GraphPlan.js +105 -108
- package/lib/queryBuilder/graph/GraphSchedule.js +102 -55
- package/lib/queryBuilder/graph/GraphUpsert.js +48 -548
- package/lib/queryBuilder/graph/GraphWriter.js +355 -28
- package/lib/queryBuilder/graph/graphChecks.js +534 -0
- package/lib/queryBuilder/graph/graphKeys.js +189 -418
- package/lib/queryBuilder/jsonDialects/JsonDialect.js +6 -0
- package/lib/queryBuilder/operations/UpdateOperation.js +5 -2
- package/lib/queryBuilder/operations/fieldExpressions.js +10 -7
- package/lib/relations/RelationProperty.js +6 -0
- package/package.json +3 -2
- package/typings/objection/index.d.ts +516 -163
|
@@ -1,40 +1,37 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { nonStrictEquals } from '../../utils/objectUtils.js';
|
|
2
2
|
import { ValidationErrorType } from '../../model/ValidationError.js';
|
|
3
3
|
import { formatPath } from './GraphNode.js';
|
|
4
4
|
|
|
5
5
|
const UNRESOLVED = Symbol('unresolved');
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// common current value of the rows its places read. Each place writes the
|
|
16
|
-
// columns that it sets, and reads the others, from their slots.
|
|
7
|
+
// The keys of the rows of a planned graph, solved as slots:
|
|
8
|
+
// - row: a model with an id, existing or inserted, or the one that a `#ref` model refers to.
|
|
9
|
+
// - cell: one key column of a row, or one JSON path of a column.
|
|
10
|
+
// - place: puts a row into a relation, setting its cells or its owner's to cells it reads.
|
|
11
|
+
// - slot: the cells that places join, with one value from its anchors, which agree.
|
|
12
|
+
// - anchor: a cell that no place sets, or a value that a model gives a cell its place doesn't set.
|
|
13
|
+
// - clear: empties the key of an unrelated row, or of the owner of a removed BelongsToOne row.
|
|
14
|
+
// A slot without anchors takes the current value of the cells that its places read.
|
|
17
15
|
export class GraphKeys {
|
|
18
16
|
constructor(plan) {
|
|
19
17
|
this.plan = plan;
|
|
20
18
|
this.activeNodes = new Set(plan.activeNodes);
|
|
21
19
|
this.rows = new Map();
|
|
22
20
|
this.nodesByRow = new Map();
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (plan.actionFor(node) === 'insert' && !this.insertRows.has(row)) {
|
|
60
|
-
this.laterRows.add(row);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Places that the graph ignores, and many-to-many places, only read:
|
|
65
|
-
const deferredNodes = new Set(plan.deferredNodes);
|
|
66
|
-
|
|
67
|
-
for (const node of [...plan.activeNodes, ...deferredNodes]) {
|
|
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
|
-
|
|
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
|
|
105
|
-
//
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
hasOwnerKey(relation) &&
|
|
129
|
-
(node.obj[relation.name] === null || currentChild.node?.toBeUnrelatedOrDeleted) &&
|
|
130
|
-
plan.actions.get(currentChild) !== 'keep'
|
|
131
|
-
) {
|
|
132
|
-
addClearingNode(this._getRow(node), relation.ownerProp, node);
|
|
133
|
-
}
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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`
|
|
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
|
-
|
|
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
|
|
257
|
-
//
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
285
|
-
_addPlace(node, target, targetProp, source, sourceProp
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
381
|
-
let
|
|
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.
|
|
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
|
|
406
|
-
//
|
|
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(
|
|
409
|
-
cell.setNodes.size === 0
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
473
|
-
// inserted, with `null` for the
|
|
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
|
|
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
|
|
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
|
-
|
|
560
|
+
return this._isCleared(cell) ? null : this._getStoredValue(cell);
|
|
561
|
+
}
|
|
561
562
|
|
|
562
|
-
|
|
563
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
:
|
|
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 } =
|
|
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
|
|
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
|
-
//
|
|
834
|
-
function
|
|
835
|
-
|
|
836
|
-
set(node.obj, path, isWholeValue ? value : get(node.obj, path).replace(match, value));
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// Fetch the keys that the relations of the inserted nodes need, but that the
|
|
840
|
-
// database generated and the insert didn't return, like a uuid that isn't the
|
|
841
|
-
// id (Vincit#853).
|
|
842
|
-
export async function fetchGeneratedKeys(builder, modelClass, nodes) {
|
|
843
|
-
const keyProps = (node) => [
|
|
844
|
-
...(node.relation && node.relation.keySide !== 'related' ? [node.relation.relatedProp] : []),
|
|
845
|
-
...node.activeChildren
|
|
846
|
-
.filter((child) => child.relation.keySide !== 'owner')
|
|
847
|
-
.map((child) => child.relation.ownerProp),
|
|
848
|
-
];
|
|
849
|
-
const missingNodes = nodes.filter((node) =>
|
|
850
|
-
keyProps(node).some((prop) => prop.getProps(node.obj).some((value) => value === undefined)),
|
|
851
|
-
);
|
|
852
|
-
|
|
853
|
-
if (missingNodes.length > 0 && missingNodes.every((node) => node.obj.$hasId())) {
|
|
854
|
-
// SQL Server returns columns selected twice as arrays, so the ids are
|
|
855
|
-
// part of the set:
|
|
856
|
-
const columns = new Set([
|
|
857
|
-
...modelClass.getIdColumnArray(),
|
|
858
|
-
...missingNodes.flatMap((node) => keyProps(node)).flatMap((prop) => prop.cols),
|
|
859
|
-
]);
|
|
860
|
-
const rows = await modelClass
|
|
861
|
-
.query()
|
|
862
|
-
.childQueryOf(builder, { fork: true, isInternalQuery: true })
|
|
863
|
-
.findByIds(missingNodes.map((node) => node.obj.$id()))
|
|
864
|
-
.select([...columns]);
|
|
865
|
-
const rowsByKey = new Map(rows.map((row) => [row.$idKey(), row]));
|
|
866
|
-
|
|
867
|
-
for (const node of missingNodes) {
|
|
868
|
-
const row = rowsByKey.get(node.obj.$idKey());
|
|
869
|
-
|
|
870
|
-
for (const prop of keyProps(node)) {
|
|
871
|
-
prop.forEach((i) => {
|
|
872
|
-
if (prop.getProp(node.obj, i) === undefined) {
|
|
873
|
-
prop.setProp(node.obj, i, prop.getProp(row, i));
|
|
874
|
-
}
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// Return true if a `#ref{}` reads a value that only exists once the model it
|
|
882
|
-
// refers to is written: the id of a new model, or another `#ref{}`.
|
|
883
|
-
export function readsWrittenValue({ referencedNode, refPath: [prop] }) {
|
|
884
|
-
return (
|
|
885
|
-
(!referencedNode.obj.$hasId() &&
|
|
886
|
-
referencedNode.modelClass.getIdPropertyArray().includes(prop)) ||
|
|
887
|
-
referencedNode.propertyReferences.some(({ path }) => path[0] === prop)
|
|
888
|
-
);
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
// Return true if the graph gives `node` to-many relations keyed on `prop`, e.g.
|
|
892
|
-
// `siblings` on `ownerId`.
|
|
893
|
-
function hasRelationsOnKey(node, prop) {
|
|
894
|
-
const { modelClass, obj } = node;
|
|
895
|
-
return modelClass
|
|
896
|
-
.getRelationNames()
|
|
897
|
-
.filter((name) => obj[name] !== undefined)
|
|
898
|
-
.map((name) => modelClass.getRelation(name))
|
|
899
|
-
.some((relation) => relation.keySide === 'related' && hasSameColumns(relation.ownerProp, prop));
|
|
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) {
|