objection 4.0.0-rc.1 → 4.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/queryBuilder/graph/GraphLoader.js +18 -8
- package/lib/queryBuilder/graph/GraphNode.js +6 -0
- package/lib/queryBuilder/graph/GraphPlan.js +68 -32
- package/lib/queryBuilder/graph/GraphSchedule.js +513 -0
- package/lib/queryBuilder/graph/GraphUpsert.js +69 -14
- package/lib/queryBuilder/graph/GraphWriter.js +39 -794
- package/lib/queryBuilder/graph/graphKeys.js +929 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { asArray, chunk, groupBy
|
|
1
|
+
import { asArray, chunk, groupBy } from '../../utils/objectUtils.js';
|
|
2
2
|
import { getMaxRows } from '../../utils/knexUtils.js';
|
|
3
3
|
import { FetchStrategy } from './GraphOptions.js';
|
|
4
4
|
import { GraphNode } from './GraphNode.js';
|
|
@@ -25,7 +25,10 @@ export async function loadCurrentGraph(builder, plan) {
|
|
|
25
25
|
fetchedNodes.add(node);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
// The roots of a later pass were moved by an earlier one:
|
|
29
|
+
const isMovedNode = (node) => areRelated && isMoved(node) && passNodes.has(node.parent);
|
|
30
|
+
|
|
31
|
+
for (const [isMovedRow, groupNodes] of groupBy(nodes, isMovedNode)) {
|
|
29
32
|
for (const nodesOfClass of groupBy(groupNodes, (node) => node.modelClass).values()) {
|
|
30
33
|
const models = await fetchModels(builder, plan.graphOptions, nodesOfClass, {
|
|
31
34
|
withRelations: !isMovedRow,
|
|
@@ -84,8 +87,13 @@ export async function loadCurrentGraph(builder, plan) {
|
|
|
84
87
|
});
|
|
85
88
|
};
|
|
86
89
|
|
|
90
|
+
// The roots of the pass are at their places, except for the relates that an
|
|
91
|
+
// earlier pass left to this one:
|
|
87
92
|
await fetch(
|
|
88
|
-
plan.nodes.filter(
|
|
93
|
+
plan.nodes.filter(
|
|
94
|
+
(node) =>
|
|
95
|
+
!passNodes.has(node.parent) && node.hasId && !(plan.laterNodes.has(node) && !node.current),
|
|
96
|
+
),
|
|
89
97
|
{ areRelated: false },
|
|
90
98
|
);
|
|
91
99
|
|
|
@@ -159,11 +167,12 @@ function hasRelations(obj) {
|
|
|
159
167
|
// their ids, in as many queries as the number of their ids requires.
|
|
160
168
|
async function fetchModels(builder, graphOptions, nodes, options) {
|
|
161
169
|
const [{ modelClass }] = nodes;
|
|
162
|
-
const
|
|
170
|
+
const nodesByRow = [...groupBy(nodes, (node) => node.obj.$idKey()).values()];
|
|
163
171
|
const maxIds = getMaxRows(builder.unsafeKnex(), modelClass.getIdColumnArray().length);
|
|
164
172
|
const models = [];
|
|
165
173
|
|
|
166
|
-
|
|
174
|
+
// Fetch each row once, with the relations of all its places:
|
|
175
|
+
for (const chunkNodes of chunk(nodesByRow, maxIds)) {
|
|
167
176
|
models.push(...(await fetchModelsOfNodes(builder, graphOptions, chunkNodes, options)));
|
|
168
177
|
}
|
|
169
178
|
|
|
@@ -173,11 +182,12 @@ async function fetchModels(builder, graphOptions, nodes, options) {
|
|
|
173
182
|
function fetchModelsOfNodes(
|
|
174
183
|
builder,
|
|
175
184
|
graphOptions,
|
|
176
|
-
|
|
185
|
+
nodesByRow,
|
|
177
186
|
{ withRelations = false, onlyIds = false },
|
|
178
187
|
) {
|
|
179
|
-
const [{ modelClass }] =
|
|
180
|
-
const ids =
|
|
188
|
+
const [[{ modelClass }]] = nodesByRow;
|
|
189
|
+
const ids = nodesByRow.map(([node]) => node.obj.$id());
|
|
190
|
+
const nodes = nodesByRow.flat();
|
|
181
191
|
const expression = RelationExpression.fromModelGraph(
|
|
182
192
|
withRelations ? nodes.map((node) => node.obj) : [],
|
|
183
193
|
);
|
|
@@ -233,3 +233,9 @@ export function createNotModelError(modelClass, value) {
|
|
|
233
233
|
message: `expected value "${value}" to be an instance of ${modelClass.name}`,
|
|
234
234
|
});
|
|
235
235
|
}
|
|
236
|
+
|
|
237
|
+
// Name the place of a model in error messages, by its data path.
|
|
238
|
+
export function formatPath(dataPathKey) {
|
|
239
|
+
const path = dataPathKey.replace(/^\./, '');
|
|
240
|
+
return path ? `'${path}'` : 'the root';
|
|
241
|
+
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
import { hasSameColumns, hasSameKey } from './graphKeys.js';
|
|
2
|
+
|
|
1
3
|
// What a graph insert or upsert does with each model of the graph and each row
|
|
2
4
|
// of the current graph: The rules that decide it, and the pruning of branches
|
|
3
5
|
// that nothing is written in.
|
|
4
6
|
export class GraphPlan {
|
|
5
|
-
constructor({ nodes, graphOptions, keptNodes = new Map() }) {
|
|
7
|
+
constructor({ nodes, graphOptions, keptNodes = new Map(), laterNodes = new Set() }) {
|
|
6
8
|
this.nodes = nodes;
|
|
9
|
+
// The nodes that an earlier pass left to this one before writing them:
|
|
10
|
+
this.laterNodes = laterNodes;
|
|
7
11
|
this.currentNodes = [];
|
|
8
12
|
this.deferredNodes = new Set();
|
|
9
|
-
// The nodes that earlier passes of the graph decided to keep
|
|
13
|
+
// The nodes that earlier passes of the graph decided to keep or inserted,
|
|
14
|
+
// by row:
|
|
10
15
|
this.earlierKeptNodes = keptNodes;
|
|
11
16
|
this.graphOptions = graphOptions;
|
|
12
17
|
this.actions = null;
|
|
@@ -79,6 +84,20 @@ export class GraphPlan {
|
|
|
79
84
|
: 'missing';
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
// Return true if the model of the `#ref` model `node` writes what `node`
|
|
88
|
+
// would write to the row: The model is inserted, or it's written and its path
|
|
89
|
+
// allows any write that the path of `node` allows.
|
|
90
|
+
isCoveredByModel(node) {
|
|
91
|
+
const { actions, graphOptions } = this;
|
|
92
|
+
const { referencedNode } = node;
|
|
93
|
+
const action = actions.get(referencedNode);
|
|
94
|
+
const allowsWrite = (node) => graphOptions.shouldPatch(node) || graphOptions.shouldUpdate(node);
|
|
95
|
+
return (
|
|
96
|
+
action === 'insert' ||
|
|
97
|
+
(['relate', 'patch'].includes(action) && (allowsWrite(referencedNode) || !allowsWrite(node)))
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
82
101
|
// Return true if the graph writes the branch below `node`: below models at
|
|
83
102
|
// their place, inserted models, related models with fetched rows and `#ref`
|
|
84
103
|
// models, but not below removed or ignored ones.
|
|
@@ -146,11 +165,11 @@ export class GraphPlan {
|
|
|
146
165
|
} else if (this._hasIgnoredOtherModel(currentNode)) {
|
|
147
166
|
return 'keep';
|
|
148
167
|
} else {
|
|
149
|
-
const
|
|
168
|
+
const keptNodes = this.getKeptNodes(currentNode);
|
|
150
169
|
|
|
151
|
-
if (keptNode
|
|
170
|
+
if (keptNodes.some((keptNode) => !this._isUnrelatedWhenKept(currentNode, keptNode))) {
|
|
152
171
|
return 'keep';
|
|
153
|
-
} else if (
|
|
172
|
+
} else if (keptNodes.length > 0 || has('unrelate')) {
|
|
154
173
|
// Rows that the graph keeps in another place must not be deleted
|
|
155
174
|
// (#214). They're unrelated instead, which replaces the delete, so
|
|
156
175
|
// `noDelete` disables it too, unless the `unrelate` option asks for it.
|
|
@@ -317,17 +336,29 @@ export class GraphPlan {
|
|
|
317
336
|
);
|
|
318
337
|
}
|
|
319
338
|
|
|
320
|
-
// A row that the graph keeps in another place
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
339
|
+
// A row that the graph keeps in another place, or that an earlier pass
|
|
340
|
+
// inserted, is unrelated here, unless one of these places sets the same key
|
|
341
|
+
// column, by relating, patching or inserting it through such a relation, or
|
|
342
|
+
// through its own BelongsToOneRelation, e.g. `parent` for `children`: Then
|
|
343
|
+
// that place writes the key, which may not be nullable. A has-one row that
|
|
344
|
+
// its own BelongsToOneRelation or a later pass keeps is still unrelated if
|
|
345
|
+
// this pass fills its slot with another row, as the foreign key may be
|
|
346
|
+
// unique (#398).
|
|
325
347
|
_isUnrelatedWhenKept(currentNode, keptNode) {
|
|
326
348
|
const { relation } = currentNode;
|
|
349
|
+
const isKeptInRelation = hasSameKey(relation, keptNode.relation);
|
|
350
|
+
const isKeptByOwner =
|
|
351
|
+
!isKeptInRelation &&
|
|
352
|
+
keptNode.activeChildren.some(
|
|
353
|
+
(child) =>
|
|
354
|
+
child.relation.keySide === 'owner' &&
|
|
355
|
+
this.actionFor(child) !== 'ignore' &&
|
|
356
|
+
hasSameColumns(child.relation.ownerProp, relation.relatedProp),
|
|
357
|
+
);
|
|
327
358
|
return (
|
|
328
359
|
relation.keySide !== 'related' ||
|
|
329
|
-
!
|
|
330
|
-
(this.deferredNodes.has(keptNode) && this._hasNewRowInSlot(currentNode))
|
|
360
|
+
!(isKeptInRelation || isKeptByOwner) ||
|
|
361
|
+
((isKeptByOwner || this.deferredNodes.has(keptNode)) && this._hasNewRowInSlot(currentNode))
|
|
331
362
|
);
|
|
332
363
|
}
|
|
333
364
|
|
|
@@ -345,14 +376,30 @@ export class GraphPlan {
|
|
|
345
376
|
);
|
|
346
377
|
}
|
|
347
378
|
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
return this.keptNodes.get(
|
|
379
|
+
// The nodes that keep the row of `node` in other places of the graph, by
|
|
380
|
+
// relating or patching it there, or that an earlier pass inserted.
|
|
381
|
+
getKeptNodes(node) {
|
|
382
|
+
return this.keptNodes.get(node.rowKey) || [];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// The nodes that keep their rows in the next passes, once this one is
|
|
386
|
+
// written: the ones that it decided to keep before its writes, and the ones
|
|
387
|
+
// that it inserts.
|
|
388
|
+
getNextKeptNodes() {
|
|
389
|
+
const keptNodes = new Map(this.keptNodes);
|
|
390
|
+
|
|
391
|
+
for (const node of this.activeNodes) {
|
|
392
|
+
if (this.actions.get(node) === 'insert') {
|
|
393
|
+
addKeptNode(keptNodes, node);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return keptNodes;
|
|
352
398
|
}
|
|
353
399
|
|
|
354
|
-
// The nodes that keep their rows
|
|
355
|
-
// decided to keep.
|
|
400
|
+
// The nodes that keep their rows by relating or patching them, by row, also
|
|
401
|
+
// the ones that earlier passes decided to keep or inserted. A row may be kept
|
|
402
|
+
// in several places.
|
|
356
403
|
get keptNodes() {
|
|
357
404
|
if (!this._keptNodesByRowKey) {
|
|
358
405
|
this._keptNodesByRowKey = new Map(this.earlierKeptNodes);
|
|
@@ -361,7 +408,7 @@ export class GraphPlan {
|
|
|
361
408
|
const action = this.actionFor(node);
|
|
362
409
|
|
|
363
410
|
if (node.obj.$hasId() && (action === 'relate' || action === 'patch')) {
|
|
364
|
-
this._keptNodesByRowKey
|
|
411
|
+
addKeptNode(this._keptNodesByRowKey, node);
|
|
365
412
|
}
|
|
366
413
|
}
|
|
367
414
|
}
|
|
@@ -370,19 +417,8 @@ export class GraphPlan {
|
|
|
370
417
|
}
|
|
371
418
|
}
|
|
372
419
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
function hasSameKey(relation, otherRelation) {
|
|
376
|
-
const { relatedProp } = relation;
|
|
377
|
-
const otherProp = otherRelation?.keySide === 'related' ? otherRelation.relatedProp : null;
|
|
378
|
-
return (
|
|
379
|
-
!!otherProp &&
|
|
380
|
-
relation.relatedModelClass.getTableName() === otherRelation.relatedModelClass.getTableName() &&
|
|
381
|
-
relatedProp.size === otherProp.size &&
|
|
382
|
-
relatedProp.props.every(
|
|
383
|
-
(_, i) => relatedProp.propDescription(i) === otherProp.propDescription(i),
|
|
384
|
-
)
|
|
385
|
-
);
|
|
420
|
+
function addKeptNode(keptNodes, node) {
|
|
421
|
+
keptNodes.set(node.rowKey, [...(keptNodes.get(node.rowKey) || []), node]);
|
|
386
422
|
}
|
|
387
423
|
|
|
388
424
|
// For `HasMany` and `HasOne` relations, the relate prop is the id of the
|