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,14 +1,22 @@
|
|
|
1
|
-
import { GraphNode, createNotModelError
|
|
1
|
+
import { GraphNode, createNotModelError } from './GraphNode.js';
|
|
2
2
|
import { GraphOptions } from './GraphOptions.js';
|
|
3
3
|
import { GraphPlan } from './GraphPlan.js';
|
|
4
|
-
import { GraphKeys
|
|
4
|
+
import { GraphKeys } from './graphKeys.js';
|
|
5
5
|
import { GraphSchedule } from './GraphSchedule.js';
|
|
6
6
|
import { GraphWriter } from './GraphWriter.js';
|
|
7
|
-
import { loadCurrentGraph } from './GraphLoader.js';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
import { getReadKeys, loadCurrentGraph } from './GraphLoader.js';
|
|
8
|
+
import {
|
|
9
|
+
checkChangedIds,
|
|
10
|
+
checkGraph,
|
|
11
|
+
checkJoinTables,
|
|
12
|
+
checkKeyRelates,
|
|
13
|
+
checkMissingModels,
|
|
14
|
+
checkPlacements,
|
|
15
|
+
checkReadKeys,
|
|
16
|
+
checkReferenceCycles,
|
|
17
|
+
checkReferences,
|
|
18
|
+
} from './graphChecks.js';
|
|
19
|
+
import { asArray, isObject } from '../../utils/objectUtils.js';
|
|
12
20
|
|
|
13
21
|
export class GraphUpsert {
|
|
14
22
|
constructor({ rootModelClass, objects, upsertOptions, insertOnly = false }) {
|
|
@@ -48,79 +56,55 @@ export class GraphUpsert {
|
|
|
48
56
|
GraphNode.linkReferences(nodes);
|
|
49
57
|
const restoreMarkers = GraphNode.removeMarkers(nodes);
|
|
50
58
|
|
|
51
|
-
// A `#ref` to a model with an id is that model, also when its relations
|
|
52
|
-
// are loaded and upserted. Models inserted in a pass have their ids in the
|
|
53
|
-
// next ones:
|
|
54
|
-
const identifyReferences = (nodes) => {
|
|
55
|
-
for (const { obj, referencedNode } of nodes) {
|
|
56
|
-
if (referencedNode?.obj.$hasId() && !obj.$hasId()) {
|
|
57
|
-
obj.$id(referencedNode.obj.$id());
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
59
|
for (const node of nodes) {
|
|
63
60
|
if (node.parent && node.isDbReference) {
|
|
64
61
|
node.relation.setRelateProp(node.obj, asArray(node.dbReference));
|
|
65
62
|
}
|
|
66
63
|
}
|
|
67
64
|
|
|
68
|
-
|
|
65
|
+
// A `#ref` to a model with an id is that model, also when its relations
|
|
66
|
+
// are loaded and upserted:
|
|
67
|
+
for (const { obj, referencedNode } of nodes) {
|
|
68
|
+
if (referencedNode?.obj.$hasId() && !obj.$hasId()) {
|
|
69
|
+
obj.$id(referencedNode.obj.$id());
|
|
70
|
+
}
|
|
71
|
+
}
|
|
69
72
|
|
|
70
73
|
try {
|
|
71
|
-
|
|
74
|
+
const plan = new GraphPlan({ nodes, graphOptions });
|
|
75
|
+
checkGraph(builder, plan);
|
|
72
76
|
checkPlacements(builder, nodes);
|
|
73
77
|
checkReferenceCycles(nodes);
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
const isUpsert = !graphOptions.isInsertOnly();
|
|
79
|
+
const load = async (readKeys) => {
|
|
80
|
+
if (isUpsert) {
|
|
81
|
+
await loadCurrentGraph(builder, plan, readKeys);
|
|
82
|
+
}
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
for (let passNodes = nodes; passNodes.length > 0;) {
|
|
82
|
-
const plan = new GraphPlan({
|
|
83
|
-
nodes: passNodes,
|
|
84
|
-
graphOptions,
|
|
85
|
-
keptNodes,
|
|
86
|
-
laterNodes,
|
|
87
|
-
});
|
|
88
|
-
const movedNodes = graphOptions.isInsertOnly() ? [] : await loadCurrentGraph(builder, plan);
|
|
84
|
+
plan.plan();
|
|
85
|
+
return new GraphKeys(plan);
|
|
86
|
+
};
|
|
89
87
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
checkChangedIds(builder, plan);
|
|
95
|
-
const [nextLaterNodes, keys] = deferLaterNodes(plan);
|
|
88
|
+
// The relations of rows whose keys change are read again at the keys that
|
|
89
|
+
// the graph gives them, and the graph is planned with them:
|
|
90
|
+
let keys = await load();
|
|
91
|
+
const readKeys = isUpsert ? getReadKeys(plan, keys) : null;
|
|
96
92
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Models that `noInsert` or `noRelate` ignore aren't written, but the
|
|
103
|
-
// ones marked with `#delete` or `#unrelate` keep their values:
|
|
104
|
-
for (const node of plan.activeNodes) {
|
|
105
|
-
if (plan.actions.get(node) !== 'ignore' || node.toBeUnrelatedOrDeleted) {
|
|
106
|
-
writtenNodes.add(node);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
93
|
+
if (readKeys) {
|
|
94
|
+
keys = await load(readKeys);
|
|
95
|
+
}
|
|
109
96
|
|
|
110
|
-
|
|
111
|
-
checkJoinTables(builder, plan);
|
|
112
|
-
await new GraphWriter(new GraphSchedule(plan, keys)).write(builder);
|
|
113
|
-
keptNodes = plan.getNextKeptNodes();
|
|
97
|
+
checkMissingModels(builder, plan);
|
|
114
98
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
...new Set(
|
|
118
|
-
[...movedNodes, ...nextLaterNodes].flatMap((node) => [node, ...node.descendants]),
|
|
119
|
-
),
|
|
120
|
-
];
|
|
121
|
-
laterNodes = new Set(nextLaterNodes);
|
|
122
|
-
identifyReferences(passNodes);
|
|
99
|
+
if (readKeys) {
|
|
100
|
+
checkReadKeys(keys, readKeys);
|
|
123
101
|
}
|
|
102
|
+
|
|
103
|
+
checkChangedIds(builder, plan);
|
|
104
|
+
checkKeyRelates(builder, plan);
|
|
105
|
+
checkReferences(builder, plan);
|
|
106
|
+
checkJoinTables(builder, plan);
|
|
107
|
+
await new GraphWriter(new GraphSchedule(plan, keys)).write(builder);
|
|
124
108
|
} catch (err) {
|
|
125
109
|
// The input can be retried as it was:
|
|
126
110
|
restoreMarkers();
|
|
@@ -135,490 +119,6 @@ export class GraphUpsert {
|
|
|
135
119
|
}
|
|
136
120
|
}
|
|
137
121
|
|
|
138
|
-
// Leave the writes that need keys or `#ref{}` values that only a later pass
|
|
139
|
-
// gives to that pass, with their branches, and return them with the keys of
|
|
140
|
-
// the rest.
|
|
141
|
-
function deferLaterNodes(plan) {
|
|
142
|
-
const laterNodes = [];
|
|
143
|
-
let keys = new GraphKeys(plan);
|
|
144
|
-
|
|
145
|
-
for (let nodes = keys.getLaterNodes(); nodes.length > 0; nodes = keys.getLaterNodes()) {
|
|
146
|
-
laterNodes.push(...nodes);
|
|
147
|
-
plan.defer([...plan.deferredNodes, ...nodes.flatMap((node) => [node, ...node.descendants])]);
|
|
148
|
-
keys = new GraphKeys(plan);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return [laterNodes, keys];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Check the models with ids that are neither at their place nor related, once
|
|
155
|
-
// the current graph of a pass is loaded.
|
|
156
|
-
function checkMissingModels(builder, plan) {
|
|
157
|
-
for (const node of plan.nodes) {
|
|
158
|
-
if (plan.intentFor(node) === 'missing') {
|
|
159
|
-
const id = node.obj.$id();
|
|
160
|
-
|
|
161
|
-
throw node.modelClass.createNotFoundError(builder.context(), {
|
|
162
|
-
message: node.isMissing
|
|
163
|
-
? `model (id=${id}) does not exist. If you want to insert it with an id, use the insertMissing option`
|
|
164
|
-
: node.isRoot
|
|
165
|
-
? `root model (id=${id}) does not exist. If you want to insert it with an id, use the insertMissing option`
|
|
166
|
-
: `model (id=${id}) is not a child of model (id=${node.parent.obj.$id()}). If you want to relate it, use the relate option. If you want to insert it with an id, use the insertMissing option`,
|
|
167
|
-
dataPath: node.dataPath,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// The graph writes keys of rows that can't change if they're part of the ids
|
|
174
|
-
// of the rows: the foreign key of a row that a HasMany or HasOne relate moves
|
|
175
|
-
// to another owner, and the foreign key of an existing row that its own
|
|
176
|
-
// BelongsToOne relate or insert points to another row, or that `null`,
|
|
177
|
-
// `#unrelate` or `#delete` clears. Keys that aren't known before the writes
|
|
178
|
-
// are compared only where they surely change, as new models get new keys.
|
|
179
|
-
function checkChangedIds(builder, plan) {
|
|
180
|
-
const { actions } = plan;
|
|
181
|
-
const newKey = {};
|
|
182
|
-
const getKey = (node, prop, i) =>
|
|
183
|
-
actions.get(node) === 'insert' && !node.obj.$hasId()
|
|
184
|
-
? newKey
|
|
185
|
-
: (prop.getProp(node.obj, i) ?? (node.existing && prop.getProp(node.existing.obj, i)));
|
|
186
|
-
|
|
187
|
-
const checkKey = (row, relation, rowProp, getNewKey) => {
|
|
188
|
-
const idProps = row.modelClass.getIdPropertyArray();
|
|
189
|
-
const isChanged =
|
|
190
|
-
actions.get(row) !== 'insert' &&
|
|
191
|
-
rowProp.props.some((prop, i) => {
|
|
192
|
-
const key = row.obj[prop];
|
|
193
|
-
const changedKey = getNewKey(i);
|
|
194
|
-
return (
|
|
195
|
-
idProps.includes(prop) &&
|
|
196
|
-
key !== undefined &&
|
|
197
|
-
changedKey !== undefined &&
|
|
198
|
-
(changedKey === newKey || !nonStrictEquals(key, changedKey))
|
|
199
|
-
);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
if (isChanged) {
|
|
203
|
-
throw builder.modelClass().createValidationError({
|
|
204
|
-
type: ValidationErrorType.InvalidGraph,
|
|
205
|
-
message: `the graph changes the key of the ${row.modelClass.name} at ${formatPath(row.dataPathKey)} through ${relation.name}, which is part of its id`,
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
for (const node of plan.activeNodes) {
|
|
211
|
-
const { relation, parent } = node;
|
|
212
|
-
const action = actions.get(node);
|
|
213
|
-
|
|
214
|
-
if (relation?.keySide === 'related' && action === 'relate') {
|
|
215
|
-
checkKey(node.referencedNode || node, relation, relation.relatedProp, (i) =>
|
|
216
|
-
getKey(parent, relation.ownerProp, i),
|
|
217
|
-
);
|
|
218
|
-
} else if (relation?.keySide === 'owner' && relation.isOneToOne()) {
|
|
219
|
-
if (node.toBeUnrelatedOrDeleted) {
|
|
220
|
-
if (node.current) {
|
|
221
|
-
checkKey(parent, relation, relation.ownerProp, () => null);
|
|
222
|
-
}
|
|
223
|
-
} else if (action === 'relate' || action === 'insert') {
|
|
224
|
-
checkKey(parent, relation, relation.ownerProp, (i) =>
|
|
225
|
-
getKey(node.referencedNode || node, relation.relatedProp, i),
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// `null` clears the key:
|
|
231
|
-
const nullNames = node.modelClass.getRelationNames().filter((name) => node.obj[name] === null);
|
|
232
|
-
|
|
233
|
-
for (const name of nullNames) {
|
|
234
|
-
const nullRelation = node.modelClass.getRelation(name);
|
|
235
|
-
|
|
236
|
-
if (nullRelation.keySide === 'owner' && nullRelation.isOneToOne()) {
|
|
237
|
-
checkKey(node, nullRelation, nullRelation.ownerProp, () => null);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Check the whole graph before anything is written.
|
|
244
|
-
function checkGraph(builder, plan) {
|
|
245
|
-
const { nodes, graphOptions } = plan;
|
|
246
|
-
const modelClass = builder.modelClass();
|
|
247
|
-
const createInvalidGraphError = (message) =>
|
|
248
|
-
modelClass.createValidationError({ type: ValidationErrorType.InvalidGraph, message });
|
|
249
|
-
|
|
250
|
-
const allowedExpression = builder.allowedGraphExpression();
|
|
251
|
-
const rootObjs = nodes.filter((node) => node.isRoot).map((node) => node.obj);
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
allowedExpression &&
|
|
255
|
-
!allowedExpression.isSubExpression(RelationExpression.fromModelGraph(rootObjs))
|
|
256
|
-
) {
|
|
257
|
-
throw modelClass.createValidationError({
|
|
258
|
-
type: ValidationErrorType.UnallowedRelation,
|
|
259
|
-
message: 'trying to upsert an unallowed relation',
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
!graphOptions.allowRefs() &&
|
|
265
|
-
nodes.some((node) => node.isReference || node.propertyReferences.length > 0)
|
|
266
|
-
) {
|
|
267
|
-
throw createInvalidGraphError(
|
|
268
|
-
'#ref references are not allowed in a graph by default. see the allowRefs insert/upsert graph option',
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// `#unrelate` and `#delete` markers come from the graph data, which may be
|
|
273
|
-
// untrusted input. Like `#ref`, they have to be allowed explicitly (#2410).
|
|
274
|
-
// Root models can't be marked even with `allowMarkers`, so report that first.
|
|
275
|
-
// An insert has nothing to unrelate or delete, so `insertGraph` rejects them.
|
|
276
|
-
const markedNode = nodes.find((node) => node.toBeUnrelatedOrDeleted);
|
|
277
|
-
|
|
278
|
-
if (markedNode) {
|
|
279
|
-
const { graphUnrelateProp, graphDeleteProp } = markedNode.modelClass;
|
|
280
|
-
const markers = `${graphUnrelateProp} and ${graphDeleteProp}`;
|
|
281
|
-
|
|
282
|
-
if (nodes.some((node) => node.isRoot && node.toBeUnrelatedOrDeleted)) {
|
|
283
|
-
throw createInvalidGraphError(
|
|
284
|
-
`${markers} can only be used for related models, not for root models`,
|
|
285
|
-
);
|
|
286
|
-
} else if (!graphOptions.allowMarkers()) {
|
|
287
|
-
throw createInvalidGraphError(
|
|
288
|
-
graphOptions.isInsertOnly()
|
|
289
|
-
? `${markers} can only be used in upsertGraph, not in insertGraph`
|
|
290
|
-
: `${markers} markers are not allowed in a graph by default. see the allowMarkers upsertGraph option`,
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (graphOptions.isInsertOnly()) {
|
|
296
|
-
const relatedNodes = nodes.filter((node) => plan.actionFor(node) === 'relate');
|
|
297
|
-
|
|
298
|
-
if (relatedNodes.some((node) => node.relation.keySide === 'related')) {
|
|
299
|
-
throw new Error(
|
|
300
|
-
'You cannot relate HasManyRelation or HasOneRelation using insertGraph, because those require update operations. Consider using upsertGraph instead.',
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// `upsertGraph()` upserts the branches below related models, but
|
|
305
|
-
// `insertGraph()` leaves them out, which would drop the new models there
|
|
306
|
-
// (#217). Models with ids below related models are taken as fetched models
|
|
307
|
-
// with loaded relations, and are ignored.
|
|
308
|
-
for (const node of relatedNodes) {
|
|
309
|
-
const newNode = node.descendants.find(
|
|
310
|
-
(descendant) => !descendant.hasId && plan.actionFor(descendant) === 'insert',
|
|
311
|
-
);
|
|
312
|
-
|
|
313
|
-
if (newNode) {
|
|
314
|
-
throw createInvalidGraphError(
|
|
315
|
-
`insertGraph cannot insert the new model at ${formatPath(newNode.dataPathKey)} below a related model. Insert it with a separate query or use upsertGraph instead`,
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// One row can't get two values for one column, and the slot of a
|
|
323
|
-
// HasOneRelation can't get two rows. A `#ref` or `#dbRef` model is a place of
|
|
324
|
-
// the row that it refers to. The keys that relations give a row are the values
|
|
325
|
-
// of the key props across them, or stand for these where the graph doesn't
|
|
326
|
-
// give them, e.g. for new models.
|
|
327
|
-
function checkPlacements(builder, nodes) {
|
|
328
|
-
const newRowIds = new Map();
|
|
329
|
-
const getRowId = (node) => {
|
|
330
|
-
const target = node.referencedNode || node;
|
|
331
|
-
|
|
332
|
-
if (target.obj.$hasId()) {
|
|
333
|
-
return target.rowKey;
|
|
334
|
-
} else {
|
|
335
|
-
if (!newRowIds.has(target)) {
|
|
336
|
-
newRowIds.set(target, `new:${newRowIds.size}`);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return newRowIds.get(target);
|
|
340
|
-
}
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
// The values that the graph gives each row, by prop, and the model classes
|
|
344
|
-
// and relations through which the graph reaches the rows:
|
|
345
|
-
const rows = new Map();
|
|
346
|
-
const rowTables = new Map();
|
|
347
|
-
const rowRelations = new Map();
|
|
348
|
-
const setValue = (rowId, name, prop, value, path) => {
|
|
349
|
-
const values = rows.get(rowId) || new Map();
|
|
350
|
-
const other = values.get(prop);
|
|
351
|
-
rows.set(rowId, values);
|
|
352
|
-
|
|
353
|
-
if (!other) {
|
|
354
|
-
values.set(prop, { value, path });
|
|
355
|
-
} else if (!nonStrictEquals(other.value, value)) {
|
|
356
|
-
throw builder.modelClass().createValidationError({
|
|
357
|
-
type: ValidationErrorType.InvalidGraph,
|
|
358
|
-
message: `the graph gives ${prop} of one ${name} two values, at ${formatPath(other.path)} and at ${formatPath(path)}`,
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
const setNodeValue = (node, prop, value, path) =>
|
|
363
|
-
setValue(getRowId(node), node.modelClass.name, prop, value, path);
|
|
364
|
-
|
|
365
|
-
// The join row of the pair of models that `node` and its parent are, the
|
|
366
|
-
// same through both relations of the join table:
|
|
367
|
-
const getJoinRowId = (node) =>
|
|
368
|
-
[
|
|
369
|
-
node.relation.joinTable,
|
|
370
|
-
...[
|
|
371
|
-
[node.relation.joinTableOwnerProp.cols, getRowId(node.parent)],
|
|
372
|
-
[node.relation.joinTableRelatedProp.cols, getRowId(node)],
|
|
373
|
-
]
|
|
374
|
-
.sort(([cols1], [cols2]) => cols1.join().localeCompare(cols2.join()))
|
|
375
|
-
.map(([cols, rowId]) => `${cols}=${rowId}`),
|
|
376
|
-
].join(':');
|
|
377
|
-
|
|
378
|
-
const getPath = (node, ...keys) => [node.dataPathKey, ...keys].filter(Boolean).join('.');
|
|
379
|
-
const getRelations = (node) =>
|
|
380
|
-
node.modelClass
|
|
381
|
-
.getRelationNames()
|
|
382
|
-
.filter((name) => node.obj[name] !== undefined)
|
|
383
|
-
.map((name) => node.modelClass.getRelation(name));
|
|
384
|
-
const isOwnerKey = (relation) => relation.keySide === 'owner' && relation.isOneToOne();
|
|
385
|
-
const isSlot = (relation) => relation.keySide === 'related' && relation.isOneToOne();
|
|
386
|
-
|
|
387
|
-
// The props that the models give, except for the ids and the keys that the
|
|
388
|
-
// relations of the same model set, which follow these relations:
|
|
389
|
-
for (const node of nodes) {
|
|
390
|
-
const { modelClass, obj, relation } = node;
|
|
391
|
-
const skippedProps = [
|
|
392
|
-
...modelClass.getRelationNames(),
|
|
393
|
-
...modelClass.getIdPropertyArray(),
|
|
394
|
-
...(relation?.keySide === 'related' ? relation.relatedProp.props : []),
|
|
395
|
-
// The extras of join rows belong to the join rows:
|
|
396
|
-
...(relation?.keySide === 'join' ? relation.joinTableExtras.map((e) => e.aliasProp) : []),
|
|
397
|
-
...getRelations(node)
|
|
398
|
-
.filter(isOwnerKey)
|
|
399
|
-
.flatMap((relation) => relation.ownerProp.props),
|
|
400
|
-
];
|
|
401
|
-
|
|
402
|
-
const rowId = getRowId(node);
|
|
403
|
-
rowTables.set(rowId, modelClass.getTableName());
|
|
404
|
-
rowRelations.set(rowId, (rowRelations.get(rowId) || new Set()).add(relation));
|
|
405
|
-
|
|
406
|
-
if (!node.toBeUnrelatedOrDeleted) {
|
|
407
|
-
for (const prop of Object.keys(obj)) {
|
|
408
|
-
if (obj[prop] !== undefined && !isInternalProp(prop) && !skippedProps.includes(prop)) {
|
|
409
|
-
setNodeValue(node, prop, obj[prop], getPath(node, prop));
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// A pair of models has one join row, which gets the extras of all places
|
|
415
|
-
// of the pair:
|
|
416
|
-
if (relation?.keySide === 'join' && !node.toBeUnrelatedOrDeleted) {
|
|
417
|
-
for (const { aliasProp } of relation.joinTableExtras) {
|
|
418
|
-
if (obj[aliasProp] !== undefined) {
|
|
419
|
-
const path = getPath(node, aliasProp);
|
|
420
|
-
setValue(
|
|
421
|
-
getJoinRowId(node),
|
|
422
|
-
`join row of ${relation.name}`,
|
|
423
|
-
aliasProp,
|
|
424
|
-
obj[aliasProp],
|
|
425
|
-
path,
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// The value of the key prop `i` of `prop` that `node` gives across a
|
|
433
|
-
// relation, from wherever the graph gives the row, or one that stands for it:
|
|
434
|
-
const getKey = (node, prop, i) =>
|
|
435
|
-
prop.getProp(node.obj, i) ??
|
|
436
|
-
rows.get(getRowId(node))?.get(prop.props[i])?.value ?? {
|
|
437
|
-
row: getRowId(node),
|
|
438
|
-
prop: prop.props[i],
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
// The keys that the relations give the rows, the rows that the graph puts
|
|
442
|
-
// in the slots of HasOneRelations, and whether the rows are deleted:
|
|
443
|
-
const slots = [];
|
|
444
|
-
for (const node of nodes) {
|
|
445
|
-
const { relation, parent } = node;
|
|
446
|
-
const path = getPath(node);
|
|
447
|
-
const isUnrelated = node.toBeUnrelated;
|
|
448
|
-
const isDeleted = node.toBeDeleted && !isUnrelated;
|
|
449
|
-
|
|
450
|
-
if (relation?.keySide === 'related') {
|
|
451
|
-
// A row that lists itself on the same column restates its own key:
|
|
452
|
-
const isSelfLink =
|
|
453
|
-
getRowId(node) === getRowId(parent) &&
|
|
454
|
-
hasSameColumns(relation.relatedProp, relation.ownerProp);
|
|
455
|
-
|
|
456
|
-
if (!isDeleted && !isSelfLink) {
|
|
457
|
-
relation.relatedProp.forEach((i) => {
|
|
458
|
-
const key = isUnrelated ? null : getKey(parent, relation.ownerProp, i);
|
|
459
|
-
setNodeValue(node, relation.relatedProp.props[i], key, path);
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (isSlot(relation)) {
|
|
464
|
-
const row = node.toBeUnrelatedOrDeleted ? null : getRowId(node);
|
|
465
|
-
slots.push({ owner: parent, relation, row, path });
|
|
466
|
-
}
|
|
467
|
-
} else if (relation && isOwnerKey(relation)) {
|
|
468
|
-
relation.ownerProp.forEach((i) => {
|
|
469
|
-
const key = node.toBeUnrelatedOrDeleted ? null : getKey(node, relation.relatedProp, i);
|
|
470
|
-
setNodeValue(parent, relation.ownerProp.props[i], key, path);
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
setNodeValue(node, '#delete', isDeleted, path);
|
|
475
|
-
|
|
476
|
-
// All places of a pair of models keep or remove its join row:
|
|
477
|
-
if (relation?.keySide === 'join') {
|
|
478
|
-
const name = `join row of ${relation.name}`;
|
|
479
|
-
setValue(getJoinRowId(node), name, '#unrelate', node.toBeUnrelatedOrDeleted, path);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// `null` clears the key or the slot:
|
|
483
|
-
for (const nullRelation of getRelations(node).filter(({ name }) => node.obj[name] === null)) {
|
|
484
|
-
if (isOwnerKey(nullRelation)) {
|
|
485
|
-
nullRelation.ownerProp.forEach((i) => {
|
|
486
|
-
setNodeValue(
|
|
487
|
-
node,
|
|
488
|
-
nullRelation.ownerProp.props[i],
|
|
489
|
-
null,
|
|
490
|
-
getPath(node, nullRelation.name),
|
|
491
|
-
);
|
|
492
|
-
});
|
|
493
|
-
} else if (isSlot(nullRelation)) {
|
|
494
|
-
const path = getPath(node, nullRelation.name);
|
|
495
|
-
slots.push({ owner: node, relation: nullRelation, row: null, path });
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// The rows of a table by the values that the graph gives the props of a key,
|
|
501
|
-
// with the values that stand for keys as they are and the others as strings,
|
|
502
|
-
// so that e.g. `1` and `'1'` are the same key:
|
|
503
|
-
const toIndexKey = (values) =>
|
|
504
|
-
JSON.stringify(values.map((value) => (isObject(value) ? value : String(value))));
|
|
505
|
-
const indexes = new Map();
|
|
506
|
-
const getRowsByKey = (table, props) => {
|
|
507
|
-
const indexName = `${table}:${props.join()}`;
|
|
508
|
-
let index = indexes.get(indexName);
|
|
509
|
-
|
|
510
|
-
if (!index) {
|
|
511
|
-
index = new Map();
|
|
512
|
-
indexes.set(indexName, index);
|
|
513
|
-
|
|
514
|
-
for (const [rowId, values] of rows) {
|
|
515
|
-
const keyValues = props.map((prop) => values.get(prop)?.value);
|
|
516
|
-
|
|
517
|
-
if (rowTables.get(rowId) === table && keyValues.every((value) => value != null)) {
|
|
518
|
-
const indexKey = toIndexKey(keyValues);
|
|
519
|
-
const rowIds = index.get(indexKey);
|
|
520
|
-
|
|
521
|
-
if (rowIds) {
|
|
522
|
-
rowIds.push(rowId);
|
|
523
|
-
} else {
|
|
524
|
-
index.set(indexKey, [rowId]);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
return index;
|
|
531
|
-
};
|
|
532
|
-
|
|
533
|
-
// Any other row that the graph gives the key of the owner of a slot is a
|
|
534
|
-
// second row in it, unless the relation filters its rows and the graph
|
|
535
|
-
// doesn't reach the row through it:
|
|
536
|
-
for (const { owner, relation, row, path } of slots) {
|
|
537
|
-
const { relatedModelClass, relatedProp, ownerProp } = relation;
|
|
538
|
-
const ownerKey = ownerProp.props.map((_, i) => getKey(owner, ownerProp, i));
|
|
539
|
-
const rowsByKey = getRowsByKey(relatedModelClass.getTableName(), relatedProp.props);
|
|
540
|
-
const otherRowId = (rowsByKey.get(toIndexKey(ownerKey)) || []).find(
|
|
541
|
-
(rowId) => rowId !== row && (!relation.modify || rowRelations.get(rowId).has(relation)),
|
|
542
|
-
);
|
|
543
|
-
|
|
544
|
-
if (otherRowId) {
|
|
545
|
-
const other = rows.get(otherRowId).get(relatedProp.props[0]);
|
|
546
|
-
throw builder.modelClass().createValidationError({
|
|
547
|
-
type: ValidationErrorType.InvalidGraph,
|
|
548
|
-
message: `the graph puts two ${relatedModelClass.name} rows in ${relation.name} of one ${owner.modelClass.name}, at ${formatPath(path)} and at ${formatPath(other.path)}`,
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// References need the ids of the models they point to, which the database
|
|
555
|
-
// only gives the models that the graph writes in the same pass or an earlier
|
|
556
|
-
// one. The ids of new models that the graph doesn't write, e.g. as `noInsert`
|
|
557
|
-
// skips them, are never known. Everything else reads the models as the graph
|
|
558
|
-
// gives them. The references of models that aren't written are never resolved.
|
|
559
|
-
function checkReferences(builder, plan, writtenNodes) {
|
|
560
|
-
const isReadable = (node, refPath) =>
|
|
561
|
-
writtenNodes.has(node) ||
|
|
562
|
-
node.obj.$hasId() ||
|
|
563
|
-
(!!refPath && !node.modelClass.getIdPropertyArray().includes(refPath[0]));
|
|
564
|
-
|
|
565
|
-
for (const node of plan.activeNodes.filter((node) => writtenNodes.has(node))) {
|
|
566
|
-
const { referencedNode, propertyReferences } = node;
|
|
567
|
-
|
|
568
|
-
if (
|
|
569
|
-
(referencedNode && !isReadable(referencedNode)) ||
|
|
570
|
-
propertyReferences.some(({ referencedNode, refPath }) => !isReadable(referencedNode, refPath))
|
|
571
|
-
) {
|
|
572
|
-
throw builder.modelClass().createValidationError({
|
|
573
|
-
type: ValidationErrorType.InvalidGraph,
|
|
574
|
-
message: `the reference at ${formatPath(node.dataPathKey)} points to a model that the graph doesn't write before it`,
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// A `#ref{}` waits for the model it reads if the value only exists once that
|
|
581
|
-
// model is written. These waits follow from the graph alone, so their cycles
|
|
582
|
-
// are found across all passes, before the first write.
|
|
583
|
-
function checkReferenceCycles(nodes) {
|
|
584
|
-
const getWaitedNodes = (node) =>
|
|
585
|
-
node.propertyReferences
|
|
586
|
-
.filter((reference) => reference.referencedNode !== node && readsWrittenValue(reference))
|
|
587
|
-
.map(({ referencedNode }) => referencedNode);
|
|
588
|
-
const visitingNodes = new Set();
|
|
589
|
-
const visitedNodes = new Set();
|
|
590
|
-
|
|
591
|
-
const visit = (node) => {
|
|
592
|
-
if (visitingNodes.has(node)) {
|
|
593
|
-
throw new Error('the object graph contains cyclic references');
|
|
594
|
-
} else if (!visitedNodes.has(node)) {
|
|
595
|
-
visitingNodes.add(node);
|
|
596
|
-
getWaitedNodes(node).forEach(visit);
|
|
597
|
-
visitingNodes.delete(node);
|
|
598
|
-
visitedNodes.add(node);
|
|
599
|
-
}
|
|
600
|
-
};
|
|
601
|
-
|
|
602
|
-
nodes.forEach(visit);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// Inserting or relating a model of a many-to-many relation inserts a join row,
|
|
606
|
-
// which would be a new related row if the join table is also the related table
|
|
607
|
-
// (#133).
|
|
608
|
-
function checkJoinTables(builder, plan) {
|
|
609
|
-
for (const node of plan.activeNodes) {
|
|
610
|
-
const action = plan.actions.get(node);
|
|
611
|
-
|
|
612
|
-
if (
|
|
613
|
-
node.relation?.keySide === 'join' &&
|
|
614
|
-
!node.current &&
|
|
615
|
-
(action === 'insert' || action === 'relate')
|
|
616
|
-
) {
|
|
617
|
-
node.relation.assertCanModifyJoinTable(builder, action);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
122
|
function isOnConflictOperation(operation) {
|
|
623
123
|
return ['onConflict', 'ignore', 'merge'].includes(operation.name);
|
|
624
124
|
}
|