objection 4.0.0-rc.1 → 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.
- package/lib/queryBuilder/graph/GraphLoader.js +204 -119
- package/lib/queryBuilder/graph/GraphNode.js +16 -3
- package/lib/queryBuilder/graph/GraphOptions.js +24 -49
- package/lib/queryBuilder/graph/GraphPlan.js +120 -100
- package/lib/queryBuilder/graph/GraphSchedule.js +522 -0
- package/lib/queryBuilder/graph/GraphUpsert.js +47 -494
- package/lib/queryBuilder/graph/GraphWriter.js +250 -762
- package/lib/queryBuilder/graph/graphChecks.js +497 -0
- package/lib/queryBuilder/graph/graphKeys.js +634 -0
- package/package.json +2 -2
- package/typings/objection/index.d.ts +437 -85
|
@@ -1,57 +1,57 @@
|
|
|
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
|
+
import { isOtherKey } from './graphKeys.js';
|
|
4
5
|
import { GraphNode } from './GraphNode.js';
|
|
5
6
|
import { RelationExpression } from '../RelationExpression.js';
|
|
6
7
|
import { getDeclaredColumns } from '../operations/fieldExpressions.js';
|
|
7
8
|
import { getFieldExpressionColumn } from '../../utils/parseFieldExpression.js';
|
|
8
9
|
|
|
9
10
|
// Fetch the current state of the graph from the database and pair its rows with
|
|
10
|
-
// the models of the graph: First the roots
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export async function loadCurrentGraph(builder, plan) {
|
|
19
|
-
const passNodes = new Set(plan.nodes);
|
|
11
|
+
// the models of the graph, from scratch: First the roots, then the rows of the
|
|
12
|
+
// models that the graph relates from elsewhere, each with the relations that
|
|
13
|
+
// the graph gives them, level by level, as the rows of related models decide
|
|
14
|
+
// which models below them are related too. Models that `insertMissing` inserts
|
|
15
|
+
// if they don't exist are looked up too. The relations are read at their
|
|
16
|
+
// current keys, or at the ones of `readKeys`, see `getReadKeys()`.
|
|
17
|
+
export async function loadCurrentGraph(builder, plan, readKeys = null) {
|
|
18
|
+
const { graphOptions } = plan;
|
|
20
19
|
const fetchedNodes = new Set();
|
|
21
|
-
|
|
20
|
+
plan.reset();
|
|
22
21
|
|
|
23
22
|
const fetch = async (nodes, { areRelated }) => {
|
|
24
23
|
for (const node of nodes) {
|
|
25
24
|
fetchedNodes.add(node);
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
for (const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
27
|
+
for (const nodesOfClass of groupBy(nodes, (node) => node.modelClass).values()) {
|
|
28
|
+
const models = await fetchModels(builder, graphOptions, nodesOfClass);
|
|
29
|
+
const modelsByKey = new Map(models.map((model) => [model.$idKey(), model]));
|
|
30
|
+
const entries = [];
|
|
31
|
+
|
|
32
|
+
for (const node of nodesOfClass) {
|
|
33
|
+
const model = modelsByKey.get(node.obj.$idKey());
|
|
34
|
+
|
|
35
|
+
if (model) {
|
|
36
|
+
entries.push([node, model]);
|
|
37
|
+
} else if (areRelated) {
|
|
38
|
+
node.isMissing = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await readRelations(builder, graphOptions, entries, readKeys);
|
|
43
|
+
|
|
44
|
+
for (const [node, model] of entries) {
|
|
45
|
+
const currentNodes = GraphNode.buildGraph(node.modelClass, model, node.relationPath);
|
|
46
|
+
const [currentNode] = currentNodes;
|
|
47
|
+
plan.currentNodes.push(...currentNodes);
|
|
48
|
+
|
|
49
|
+
if (areRelated) {
|
|
50
|
+
node.row = currentNode;
|
|
51
|
+
currentNode.node = node;
|
|
52
|
+
pairChildren(node, currentNode);
|
|
53
|
+
} else {
|
|
54
|
+
pair(node, currentNode);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -68,13 +68,14 @@ export async function loadCurrentGraph(builder, plan) {
|
|
|
68
68
|
!fetchedNodes.has(node) &&
|
|
69
69
|
!node.current &&
|
|
70
70
|
node.obj.$hasId() &&
|
|
71
|
+
!hasIdReference(node) &&
|
|
71
72
|
plan.actionFor(node) === 'relate' &&
|
|
72
73
|
isFetched(node, rowKeys),
|
|
73
74
|
);
|
|
74
75
|
const nodeSet = new Set(nodes);
|
|
75
76
|
|
|
76
77
|
return nodes.filter((node) => {
|
|
77
|
-
for (let parent = node.parent;
|
|
78
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
78
79
|
if (nodeSet.has(parent) || !plan.writesBranchOf(parent)) {
|
|
79
80
|
return false;
|
|
80
81
|
}
|
|
@@ -84,16 +85,17 @@ export async function loadCurrentGraph(builder, plan) {
|
|
|
84
85
|
});
|
|
85
86
|
};
|
|
86
87
|
|
|
88
|
+
// The roots are at their places:
|
|
87
89
|
await fetch(
|
|
88
|
-
plan.nodes.filter((node) => !
|
|
90
|
+
plan.nodes.filter((node) => !node.parent && node.hasId && !hasIdReference(node)),
|
|
89
91
|
{ areRelated: false },
|
|
90
92
|
);
|
|
91
93
|
|
|
92
94
|
// The rows of the related models that the graph gives relations, or inserts
|
|
93
95
|
// with `insertMissing` if they don't exist:
|
|
94
96
|
const hasRelatedRow = (node, rowKeys) =>
|
|
95
|
-
|
|
96
|
-
(
|
|
97
|
+
node.relations.length > 0 ||
|
|
98
|
+
(graphOptions.has(node, 'insertMissing') && !rowKeys.has(node.rowKey));
|
|
97
99
|
|
|
98
100
|
for (
|
|
99
101
|
let nodes = findRelatedNodes(hasRelatedRow);
|
|
@@ -109,17 +111,100 @@ export async function loadCurrentGraph(builder, plan) {
|
|
|
109
111
|
const nodes = findRelatedNodes((node, rowKeys) => !node.isReference && !rowKeys.has(node.rowKey));
|
|
110
112
|
|
|
111
113
|
for (const nodesOfClass of groupBy(nodes, (node) => node.modelClass).values()) {
|
|
112
|
-
const models = await fetchModels(builder,
|
|
113
|
-
onlyIds: true,
|
|
114
|
-
});
|
|
114
|
+
const models = await fetchModels(builder, graphOptions, nodesOfClass, { onlyIds: true });
|
|
115
115
|
const idKeys = new Set(models.map((model) => model.$idKey()));
|
|
116
116
|
|
|
117
117
|
for (const node of nodesOfClass) {
|
|
118
118
|
node.isMissing = !idKeys.has(node.obj.$idKey());
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
}
|
|
121
122
|
|
|
122
|
-
|
|
123
|
+
// Return the keys at which the relations of the rows whose keys change are read
|
|
124
|
+
// once the graph is planned, by node and relation, or `null` if all are read at
|
|
125
|
+
// their current keys: where the rows that give the final keys are now, see
|
|
126
|
+
// `GraphKeys.getReadProps()`. A BelongsToOne relation is read at its final key
|
|
127
|
+
// only where another place moves the row on its column; otherwise its row is
|
|
128
|
+
// replaced.
|
|
129
|
+
export function getReadKeys(plan, keys) {
|
|
130
|
+
const readKeys = new Map();
|
|
131
|
+
let isChanged = false;
|
|
132
|
+
|
|
133
|
+
for (const node of plan.activeNodes) {
|
|
134
|
+
if (node.existing) {
|
|
135
|
+
for (const relation of node.relations) {
|
|
136
|
+
if (relation.keySide !== 'owner' || keys.isSetByOtherPlaces(node, relation)) {
|
|
137
|
+
const values = keys.getReadProps(node, relation.ownerProp);
|
|
138
|
+
const nodeKeys = readKeys.get(node) || new Map();
|
|
139
|
+
readKeys.set(node, nodeKeys.set(relation, values));
|
|
140
|
+
isChanged ||= isOtherKey(values, relation.ownerProp.getProps(node.existing.obj));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return isChanged ? readKeys : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Read the relations that the graph gives the models of `entries`, `[node,
|
|
150
|
+
// model]` pairs, into the models, level by level, at the keys of `readKeys` or
|
|
151
|
+
// at their current ones. The rows of a model class are read together, with the
|
|
152
|
+
// relations of all their places.
|
|
153
|
+
async function readRelations(builder, graphOptions, entries, readKeys) {
|
|
154
|
+
for (let level = entries; level.length > 0;) {
|
|
155
|
+
const nextLevel = [];
|
|
156
|
+
|
|
157
|
+
for (const levelEntries of groupBy(level, ([node]) => node.modelClass).values()) {
|
|
158
|
+
const [[{ modelClass }]] = levelEntries;
|
|
159
|
+
const nodes = levelEntries.map(([node]) => node);
|
|
160
|
+
const relations = [...new Set(nodes.flatMap((node) => node.relations))];
|
|
161
|
+
const models = [...new Set(levelEntries.map(([, model]) => model))];
|
|
162
|
+
// Set the read keys, and keep the current ones to put back:
|
|
163
|
+
const currentKeys = new Map(models.map((model) => [model, {}]));
|
|
164
|
+
|
|
165
|
+
for (const [node, model] of levelEntries) {
|
|
166
|
+
for (const [relation, values] of readKeys?.get(node) || []) {
|
|
167
|
+
relation.ownerProp.forEach((i) => {
|
|
168
|
+
const keys = currentKeys.get(model);
|
|
169
|
+
const prop = relation.ownerProp.props[i];
|
|
170
|
+
// Relations on the same column keep its first, current value:
|
|
171
|
+
if (!(prop in keys)) {
|
|
172
|
+
keys[prop] = relation.ownerProp.getProp(model, i);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
relation.ownerProp.setProp(model, i, values[i] ?? null);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (relations.length > 0) {
|
|
181
|
+
const expression = RelationExpression.create(
|
|
182
|
+
`[${relations.map((relation) => relation.name).join(', ')}]`,
|
|
183
|
+
);
|
|
184
|
+
const fetched = await createFetchQuery(builder, graphOptions, modelClass, nodes, expression)
|
|
185
|
+
.resolve(models)
|
|
186
|
+
.findOptions({ dontCallFindHooks: true });
|
|
187
|
+
|
|
188
|
+
models.forEach((model, i) => Object.assign(model, fetched[i], currentKeys.get(model)));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// The related rows that the graph gives relations are read next:
|
|
192
|
+
for (const [node, model] of levelEntries) {
|
|
193
|
+
for (const child of node.children) {
|
|
194
|
+
if (child.obj.$hasId() && child.relations.length > 0) {
|
|
195
|
+
const key = child.obj.$idKey();
|
|
196
|
+
const row = asArray(model[child.relation.name]).find((row) => row?.$idKey() === key);
|
|
197
|
+
|
|
198
|
+
if (row) {
|
|
199
|
+
nextLevel.push([child, row]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
level = nextLevel;
|
|
207
|
+
}
|
|
123
208
|
}
|
|
124
209
|
|
|
125
210
|
function pair(node, currentNode) {
|
|
@@ -133,72 +218,77 @@ function pair(node, currentNode) {
|
|
|
133
218
|
// them pair with the same model.
|
|
134
219
|
function pairChildren(node, currentNode) {
|
|
135
220
|
const childrenByRelation = groupBy(currentNode.children, (child) => child.relation.name);
|
|
221
|
+
// The current rows of a relation by id, once the graph pairs models in it:
|
|
222
|
+
const childrenByKey = new Map();
|
|
223
|
+
const getChildrenByKey = (name) => {
|
|
224
|
+
let children = childrenByKey.get(name);
|
|
225
|
+
|
|
226
|
+
if (!children) {
|
|
227
|
+
children = groupBy(childrenByRelation.get(name) || [], (child) => child.obj.$idKey());
|
|
228
|
+
childrenByKey.set(name, children);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return children;
|
|
232
|
+
};
|
|
136
233
|
|
|
137
234
|
for (const child of node.children) {
|
|
138
235
|
if (child.obj.$hasId()) {
|
|
139
|
-
const
|
|
236
|
+
const currentChildren = getChildrenByKey(child.relation.name).get(child.obj.$idKey());
|
|
140
237
|
|
|
141
|
-
for (const currentChild of
|
|
142
|
-
|
|
143
|
-
pair(child, currentChild);
|
|
144
|
-
}
|
|
238
|
+
for (const currentChild of currentChildren || []) {
|
|
239
|
+
pair(child, currentChild);
|
|
145
240
|
}
|
|
146
241
|
}
|
|
147
242
|
}
|
|
148
243
|
}
|
|
149
244
|
|
|
150
|
-
|
|
151
|
-
|
|
245
|
+
// Return true if an id of `node` is a `#ref{}`, which is only known once the
|
|
246
|
+
// model that it refers to is written, so that its row can't be looked up.
|
|
247
|
+
function hasIdReference(node) {
|
|
248
|
+
const idProps = node.modelClass.getIdPropertyArray();
|
|
249
|
+
return node.propertyReferences.some(({ path }) => idProps.includes(path[0]));
|
|
152
250
|
}
|
|
153
251
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Fetch the rows of `nodes`, with their relations unless they're moved, or only
|
|
159
|
-
// their ids, in as many queries as the number of their ids requires.
|
|
160
|
-
async function fetchModels(builder, graphOptions, nodes, options) {
|
|
252
|
+
// Fetch the rows of `nodes`, or only their ids, in as many queries as the
|
|
253
|
+
// number of their ids requires, each row once for all its places.
|
|
254
|
+
async function fetchModels(builder, graphOptions, nodes, { onlyIds = false } = {}) {
|
|
161
255
|
const [{ modelClass }] = nodes;
|
|
162
|
-
const
|
|
256
|
+
const nodesByRow = [...groupBy(nodes, (node) => node.obj.$idKey()).values()];
|
|
163
257
|
const maxIds = getMaxRows(builder.unsafeKnex(), modelClass.getIdColumnArray().length);
|
|
258
|
+
const expression = RelationExpression.create('[]');
|
|
164
259
|
const models = [];
|
|
165
260
|
|
|
166
|
-
for (const chunkNodes of chunk(
|
|
167
|
-
models.push(
|
|
261
|
+
for (const chunkNodes of chunk(nodesByRow, maxIds)) {
|
|
262
|
+
models.push(
|
|
263
|
+
...(await createFetchQuery(
|
|
264
|
+
builder,
|
|
265
|
+
graphOptions,
|
|
266
|
+
modelClass,
|
|
267
|
+
chunkNodes.flat(),
|
|
268
|
+
expression,
|
|
269
|
+
onlyIds,
|
|
270
|
+
).findByIds(chunkNodes.map(([node]) => node.obj.$id()))),
|
|
271
|
+
);
|
|
168
272
|
}
|
|
169
273
|
|
|
170
274
|
return models;
|
|
171
275
|
}
|
|
172
276
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
{ withRelations = false, onlyIds = false },
|
|
178
|
-
) {
|
|
179
|
-
const [{ modelClass }] = nodes;
|
|
180
|
-
const ids = nodes.map((node) => node.obj.$id());
|
|
181
|
-
const expression = RelationExpression.fromModelGraph(
|
|
182
|
-
withRelations ? nodes.map((node) => node.obj) : [],
|
|
183
|
-
);
|
|
184
|
-
|
|
277
|
+
// Create the query that fetches the rows of `nodes` with the relations of
|
|
278
|
+
// `expression`, locked like the upsert query asks for, and with the columns
|
|
279
|
+
// that the fetch strategy selects for them and their related models.
|
|
280
|
+
function createFetchQuery(builder, graphOptions, modelClass, nodes, expression, onlyIds = false) {
|
|
185
281
|
return modelClass
|
|
186
282
|
.query()
|
|
187
283
|
.childQueryOf(builder, { fork: true, isInternalQuery: true })
|
|
188
284
|
.modify((fetchBuilder) => {
|
|
189
|
-
// Lock the fetched rows like the upsert query asks for.
|
|
190
285
|
for (const method of ['forUpdate', 'forShare', 'forNoKeyUpdate', 'forKeyShare']) {
|
|
191
286
|
if (builder.has(method)) {
|
|
192
287
|
fetchBuilder[method]();
|
|
193
288
|
}
|
|
194
289
|
}
|
|
195
|
-
})
|
|
196
|
-
.modify((fetchBuilder) => {
|
|
197
|
-
const nodesByRelationPath = withRelations
|
|
198
|
-
? getNodesByRelationPath(nodes, expression)
|
|
199
|
-
: new Map([['', nodes]]);
|
|
200
290
|
|
|
201
|
-
for (const [relationPath, pathNodes] of
|
|
291
|
+
for (const [relationPath, pathNodes] of getNodesByRelationPath(nodes, expression)) {
|
|
202
292
|
const select = onlyIds
|
|
203
293
|
? (builder) => builder.select(builder.fullIdColumn())
|
|
204
294
|
: createSelect(pathNodes, graphOptions, !relationPath);
|
|
@@ -210,52 +300,39 @@ function fetchModelsOfNodes(
|
|
|
210
300
|
}
|
|
211
301
|
}
|
|
212
302
|
})
|
|
213
|
-
.findByIds(ids)
|
|
214
303
|
.withGraphFetched(expression)
|
|
215
304
|
.internalOptions({ keepImplicitJoinProps: true });
|
|
216
305
|
}
|
|
217
306
|
|
|
218
307
|
// Group the fetched nodes and their descendants by their relation paths below
|
|
219
|
-
// the fetched nodes
|
|
220
|
-
// are fetched too.
|
|
308
|
+
// the fetched nodes, for the paths of `expression`. Relations with nulls or
|
|
309
|
+
// empty arrays have no nodes, but are fetched too.
|
|
221
310
|
function getNodesByRelationPath(nodes, expression) {
|
|
222
|
-
const nodesByRelationPath = new Map();
|
|
223
|
-
|
|
224
|
-
for (const node of nodes) {
|
|
225
|
-
const depth = node.relationPath.length;
|
|
226
|
-
|
|
227
|
-
for (const descendant of [node, ...node.descendants]) {
|
|
228
|
-
const relationPath = descendant.relationPath.slice(depth).join('.');
|
|
229
|
-
const pathNodes = nodesByRelationPath.get(relationPath);
|
|
230
|
-
|
|
231
|
-
if (pathNodes) {
|
|
232
|
-
pathNodes.push(descendant);
|
|
233
|
-
} else {
|
|
234
|
-
nodesByRelationPath.set(relationPath, [descendant]);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
311
|
+
const nodesByRelationPath = new Map([['', []]]);
|
|
239
312
|
const addPaths = (expressionNode, path) => {
|
|
240
313
|
for (const name of expressionNode.$childNames) {
|
|
241
314
|
const relationPath = [...path, name];
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (!nodesByRelationPath.has(key)) {
|
|
245
|
-
nodesByRelationPath.set(key, []);
|
|
246
|
-
}
|
|
247
|
-
|
|
315
|
+
nodesByRelationPath.set(relationPath.join('.'), []);
|
|
248
316
|
addPaths(expressionNode[name], relationPath);
|
|
249
317
|
}
|
|
250
318
|
};
|
|
251
319
|
|
|
252
320
|
addPaths(expression.node, []);
|
|
321
|
+
|
|
322
|
+
for (const node of nodes) {
|
|
323
|
+
const depth = node.relationPath.length;
|
|
324
|
+
|
|
325
|
+
for (const descendant of [node, ...node.descendants]) {
|
|
326
|
+
nodesByRelationPath.get(descendant.relationPath.slice(depth).join('.'))?.push(descendant);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
253
330
|
return nodesByRelationPath;
|
|
254
331
|
}
|
|
255
332
|
|
|
256
333
|
function createSelect(nodes, graphOptions, isFetchRoot) {
|
|
257
334
|
if (graphOptions.isFetchStrategy(FetchStrategy.OnlyIdentifiers)) {
|
|
258
|
-
return (builder) => builder.select(builder
|
|
335
|
+
return (builder) => builder.select(getKeySelections(builder, nodes));
|
|
259
336
|
} else if (graphOptions.isFetchStrategy(FetchStrategy.OnlyNeeded)) {
|
|
260
337
|
return (builder) => builder.select(getInputColumnSelections(builder, nodes, isFetchRoot));
|
|
261
338
|
} else {
|
|
@@ -263,6 +340,22 @@ function createSelect(nodes, graphOptions, isFetchRoot) {
|
|
|
263
340
|
}
|
|
264
341
|
}
|
|
265
342
|
|
|
343
|
+
// Select the ids, and the keys of the relations that the graph gives, which
|
|
344
|
+
// `readRelations()` reads the relations at.
|
|
345
|
+
function getKeySelections(builder, nodes) {
|
|
346
|
+
const columns = new Set([
|
|
347
|
+
...asArray(builder.fullIdColumn()),
|
|
348
|
+
...nodes.flatMap((node) =>
|
|
349
|
+
node.relations.flatMap((relation) =>
|
|
350
|
+
relation.ownerProp.cols.map(
|
|
351
|
+
(column) => `${builder.tableRefFor(node.modelClass)}.${column}`,
|
|
352
|
+
),
|
|
353
|
+
),
|
|
354
|
+
),
|
|
355
|
+
]);
|
|
356
|
+
return [...columns];
|
|
357
|
+
}
|
|
358
|
+
|
|
266
359
|
// Select only the columns that the graph gives, and always the ids. Related
|
|
267
360
|
// models are fetched without the join tables of their relations, so their join
|
|
268
361
|
// table extras are left out at the root of the fetch.
|
|
@@ -292,13 +385,5 @@ function getInputColumnSelections(builder, nodes, isFetchRoot) {
|
|
|
292
385
|
}
|
|
293
386
|
}
|
|
294
387
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
for (const idColumn of asArray(builder.fullIdColumn())) {
|
|
298
|
-
if (!values.includes(idColumn)) {
|
|
299
|
-
values.push(idColumn);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return values;
|
|
388
|
+
return [...new Set([...selections.values(), ...getKeySelections(builder, nodes)])];
|
|
304
389
|
}
|
|
@@ -29,8 +29,7 @@ export class GraphNode {
|
|
|
29
29
|
// The node of a `#ref` model, and the `#ref{}` properties of this model:
|
|
30
30
|
this.referencedNode = null;
|
|
31
31
|
this.propertyReferences = [];
|
|
32
|
-
// The nodes whose `#ref
|
|
33
|
-
this.referencingNodes = [];
|
|
32
|
+
// The nodes whose `#ref{}` point to this node:
|
|
34
33
|
this.propertyReferencingNodes = [];
|
|
35
34
|
|
|
36
35
|
// How the graph and the current graph pair up, see `GraphLoader`: The
|
|
@@ -61,6 +60,15 @@ export class GraphNode {
|
|
|
61
60
|
return !this.parent;
|
|
62
61
|
}
|
|
63
62
|
|
|
63
|
+
// The relations that the graph gives the model of this node, in its order.
|
|
64
|
+
get relations() {
|
|
65
|
+
const { modelClass, obj } = this;
|
|
66
|
+
const names = modelClass.getRelationNames();
|
|
67
|
+
return Object.keys(obj)
|
|
68
|
+
.filter((name) => names.includes(name) && obj[name] !== undefined)
|
|
69
|
+
.map((name) => modelClass.getRelation(name));
|
|
70
|
+
}
|
|
71
|
+
|
|
64
72
|
// The current node of the row of this node, at its place or where a relate
|
|
65
73
|
// moves it from.
|
|
66
74
|
get existing() {
|
|
@@ -184,7 +192,6 @@ export class GraphNode {
|
|
|
184
192
|
for (const node of nodes) {
|
|
185
193
|
if (node.isReference) {
|
|
186
194
|
node.referencedNode = getNode(node.reference);
|
|
187
|
-
node.referencedNode.referencingNodes.push(node);
|
|
188
195
|
}
|
|
189
196
|
|
|
190
197
|
forEachPropertyReference(node.obj, ({ path, match, uid, refPath, isWholeValue }) => {
|
|
@@ -233,3 +240,9 @@ export function createNotModelError(modelClass, value) {
|
|
|
233
240
|
message: `expected value "${value}" to be an instance of ${modelClass.name}`,
|
|
234
241
|
});
|
|
235
242
|
}
|
|
243
|
+
|
|
244
|
+
// Name the place of a model in error messages, by its data path.
|
|
245
|
+
export function formatPath(dataPathKey) {
|
|
246
|
+
const path = dataPathKey.replace(/^\./, '');
|
|
247
|
+
return path ? `'${path}'` : 'the root';
|
|
248
|
+
}
|
|
@@ -1,44 +1,30 @@
|
|
|
1
1
|
import { RelationExpression } from '../RelationExpression.js';
|
|
2
2
|
import { isString } from '../../utils/objectUtils.js';
|
|
3
3
|
|
|
4
|
-
const NO_RELATE = 'noRelate';
|
|
5
|
-
const NO_UNRELATE = 'noUnrelate';
|
|
6
|
-
const NO_INSERT = 'noInsert';
|
|
7
|
-
const NO_UPDATE = 'noUpdate';
|
|
8
|
-
const NO_DELETE = 'noDelete';
|
|
9
|
-
|
|
10
|
-
const UPDATE = 'update';
|
|
11
|
-
const RELATE = 'relate';
|
|
12
|
-
const UNRELATE = 'unrelate';
|
|
13
|
-
const INSERT_MISSING = 'insertMissing';
|
|
14
|
-
const FETCH_STRATEGY = 'fetchStrategy';
|
|
15
|
-
const ALLOW_REFS = 'allowRefs';
|
|
16
|
-
const ALLOW_MARKERS = 'allowMarkers';
|
|
17
|
-
|
|
18
4
|
// The options that are matched against the relation path of each node, see
|
|
19
5
|
// `has()`.
|
|
20
6
|
const NODE_OPTION_NAMES = [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
7
|
+
'noRelate',
|
|
8
|
+
'noUnrelate',
|
|
9
|
+
'noInsert',
|
|
10
|
+
'noUpdate',
|
|
11
|
+
'noDelete',
|
|
12
|
+
'update',
|
|
13
|
+
'relate',
|
|
14
|
+
'unrelate',
|
|
15
|
+
'insertMissing',
|
|
30
16
|
];
|
|
31
17
|
|
|
32
|
-
const OPTION_NAMES = [...NODE_OPTION_NAMES,
|
|
18
|
+
const OPTION_NAMES = [...NODE_OPTION_NAMES, 'fetchStrategy', 'allowRefs', 'allowMarkers'];
|
|
33
19
|
|
|
34
20
|
// `insertGraph` never fetches, updates or removes existing rows, so only the
|
|
35
21
|
// options about inserting and relating apply to it, and the others are set:
|
|
36
|
-
const INSERT_OPTION_NAMES = [
|
|
22
|
+
const INSERT_OPTION_NAMES = ['relate', 'noRelate', 'noInsert', 'allowRefs'];
|
|
37
23
|
const INSERT_ONLY_OPTIONS = {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
24
|
+
noUpdate: true,
|
|
25
|
+
noDelete: true,
|
|
26
|
+
noUnrelate: true,
|
|
27
|
+
insertMissing: true,
|
|
42
28
|
};
|
|
43
29
|
|
|
44
30
|
export const FetchStrategy = {
|
|
@@ -59,14 +45,10 @@ export class GraphOptions {
|
|
|
59
45
|
}
|
|
60
46
|
|
|
61
47
|
isFetchStrategy(strategy) {
|
|
62
|
-
if (!
|
|
63
|
-
throw new Error(`unknown strategy "${strategy}"`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (!this.options[FETCH_STRATEGY]) {
|
|
48
|
+
if (!this.options.fetchStrategy) {
|
|
67
49
|
return strategy === FetchStrategy.OnlyNeeded;
|
|
68
50
|
} else {
|
|
69
|
-
return this.options
|
|
51
|
+
return this.options.fetchStrategy === strategy;
|
|
70
52
|
}
|
|
71
53
|
}
|
|
72
54
|
|
|
@@ -78,19 +60,19 @@ export class GraphOptions {
|
|
|
78
60
|
// or `update()`. With `noUpdate`, it's neither, and only the foreign keys of
|
|
79
61
|
// its relates and BelongsToOneRelations are patched.
|
|
80
62
|
shouldPatch(node) {
|
|
81
|
-
return !this.has(node,
|
|
63
|
+
return !this.has(node, 'noUpdate') && !this.has(node, 'update');
|
|
82
64
|
}
|
|
83
65
|
|
|
84
66
|
shouldUpdate(node) {
|
|
85
|
-
return !this.has(node,
|
|
67
|
+
return !this.has(node, 'noUpdate') && this.has(node, 'update');
|
|
86
68
|
}
|
|
87
69
|
|
|
88
70
|
allowRefs() {
|
|
89
|
-
return !!this.options
|
|
71
|
+
return !!this.options.allowRefs;
|
|
90
72
|
}
|
|
91
73
|
|
|
92
74
|
allowMarkers() {
|
|
93
|
-
return !!this.options
|
|
75
|
+
return !!this.options.allowMarkers;
|
|
94
76
|
}
|
|
95
77
|
|
|
96
78
|
// Whether the option applies to the relation path of `node`.
|
|
@@ -125,16 +107,12 @@ function checkForUnknownOptions(options, insertOnly) {
|
|
|
125
107
|
// Parses the string values of the node options into relation expressions,
|
|
126
108
|
// once per operation, and checks the types of the other values.
|
|
127
109
|
function parseRelationExpressions(options) {
|
|
128
|
-
|
|
110
|
+
const parsedOptions = { ...options };
|
|
129
111
|
|
|
130
112
|
for (const optionName of NODE_OPTION_NAMES) {
|
|
131
113
|
const option = options[optionName];
|
|
132
114
|
|
|
133
115
|
if (isString(option)) {
|
|
134
|
-
if (parsedOptions === options) {
|
|
135
|
-
parsedOptions = { ...options };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
116
|
parsedOptions[optionName] = parseRelationExpression(optionName, option);
|
|
139
117
|
} else if (!(option === undefined || typeof option === 'boolean' || Array.isArray(option))) {
|
|
140
118
|
throw new Error(
|
|
@@ -147,11 +125,8 @@ function parseRelationExpressions(options) {
|
|
|
147
125
|
}
|
|
148
126
|
|
|
149
127
|
function parseRelationExpression(optionName, expression) {
|
|
150
|
-
const createError = (message) =>
|
|
151
|
-
|
|
152
|
-
`invalid relation expression "${expression}" in ${optionName} option: ${message}`,
|
|
153
|
-
);
|
|
154
|
-
};
|
|
128
|
+
const createError = (message) =>
|
|
129
|
+
new Error(`invalid relation expression "${expression}" in ${optionName} option: ${message}`);
|
|
155
130
|
|
|
156
131
|
let parsedExpression;
|
|
157
132
|
|