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
|
@@ -6,6 +6,9 @@ export class InternalOptions {
|
|
|
6
6
|
this.skipRelationFilter = false;
|
|
7
7
|
// The join table extras that related updates write, all of them if null:
|
|
8
8
|
this.joinTableExtras = null;
|
|
9
|
+
// The field expressions that `upsertGraph()` writes keys in json columns of
|
|
10
|
+
// updated rows with:
|
|
11
|
+
this.fieldExpressionKeys = [];
|
|
9
12
|
this.debug = false;
|
|
10
13
|
this.schema = undefined;
|
|
11
14
|
}
|
|
@@ -18,6 +21,7 @@ export class InternalOptions {
|
|
|
18
21
|
copy.isInternalQuery = this.isInternalQuery;
|
|
19
22
|
copy.skipRelationFilter = this.skipRelationFilter;
|
|
20
23
|
copy.joinTableExtras = this.joinTableExtras;
|
|
24
|
+
copy.fieldExpressionKeys = this.fieldExpressionKeys;
|
|
21
25
|
copy.debug = this.debug;
|
|
22
26
|
copy.schema = this.schema;
|
|
23
27
|
|
|
@@ -1,60 +1,57 @@
|
|
|
1
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
node.isMissing = true;
|
|
57
|
-
}
|
|
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);
|
|
58
55
|
}
|
|
59
56
|
}
|
|
60
57
|
}
|
|
@@ -71,13 +68,14 @@ export async function loadCurrentGraph(builder, plan) {
|
|
|
71
68
|
!fetchedNodes.has(node) &&
|
|
72
69
|
!node.current &&
|
|
73
70
|
node.obj.$hasId() &&
|
|
71
|
+
!hasIdReference(node) &&
|
|
74
72
|
plan.actionFor(node) === 'relate' &&
|
|
75
73
|
isFetched(node, rowKeys),
|
|
76
74
|
);
|
|
77
75
|
const nodeSet = new Set(nodes);
|
|
78
76
|
|
|
79
77
|
return nodes.filter((node) => {
|
|
80
|
-
for (let parent = node.parent;
|
|
78
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
81
79
|
if (nodeSet.has(parent) || !plan.writesBranchOf(parent)) {
|
|
82
80
|
return false;
|
|
83
81
|
}
|
|
@@ -87,21 +85,17 @@ export async function loadCurrentGraph(builder, plan) {
|
|
|
87
85
|
});
|
|
88
86
|
};
|
|
89
87
|
|
|
90
|
-
// The roots
|
|
91
|
-
// earlier pass left to this one:
|
|
88
|
+
// The roots are at their places:
|
|
92
89
|
await fetch(
|
|
93
|
-
plan.nodes.filter(
|
|
94
|
-
(node) =>
|
|
95
|
-
!passNodes.has(node.parent) && node.hasId && !(plan.laterNodes.has(node) && !node.current),
|
|
96
|
-
),
|
|
90
|
+
plan.nodes.filter((node) => !node.parent && node.hasId && !hasIdReference(node)),
|
|
97
91
|
{ areRelated: false },
|
|
98
92
|
);
|
|
99
93
|
|
|
100
94
|
// The rows of the related models that the graph gives relations, or inserts
|
|
101
95
|
// with `insertMissing` if they don't exist:
|
|
102
96
|
const hasRelatedRow = (node, rowKeys) =>
|
|
103
|
-
|
|
104
|
-
(
|
|
97
|
+
node.relations.length > 0 ||
|
|
98
|
+
(graphOptions.has(node, 'insertMissing') && !rowKeys.has(node.rowKey));
|
|
105
99
|
|
|
106
100
|
for (
|
|
107
101
|
let nodes = findRelatedNodes(hasRelatedRow);
|
|
@@ -117,17 +111,100 @@ export async function loadCurrentGraph(builder, plan) {
|
|
|
117
111
|
const nodes = findRelatedNodes((node, rowKeys) => !node.isReference && !rowKeys.has(node.rowKey));
|
|
118
112
|
|
|
119
113
|
for (const nodesOfClass of groupBy(nodes, (node) => node.modelClass).values()) {
|
|
120
|
-
const models = await fetchModels(builder,
|
|
121
|
-
onlyIds: true,
|
|
122
|
-
});
|
|
114
|
+
const models = await fetchModels(builder, graphOptions, nodesOfClass, { onlyIds: true });
|
|
123
115
|
const idKeys = new Set(models.map((model) => model.$idKey()));
|
|
124
116
|
|
|
125
117
|
for (const node of nodesOfClass) {
|
|
126
118
|
node.isMissing = !idKeys.has(node.obj.$idKey());
|
|
127
119
|
}
|
|
128
120
|
}
|
|
121
|
+
}
|
|
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
|
+
}
|
|
129
145
|
|
|
130
|
-
return
|
|
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
|
+
}
|
|
131
208
|
}
|
|
132
209
|
|
|
133
210
|
function pair(node, currentNode) {
|
|
@@ -141,74 +218,77 @@ function pair(node, currentNode) {
|
|
|
141
218
|
// them pair with the same model.
|
|
142
219
|
function pairChildren(node, currentNode) {
|
|
143
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
|
+
};
|
|
144
233
|
|
|
145
234
|
for (const child of node.children) {
|
|
146
235
|
if (child.obj.$hasId()) {
|
|
147
|
-
const
|
|
236
|
+
const currentChildren = getChildrenByKey(child.relation.name).get(child.obj.$idKey());
|
|
148
237
|
|
|
149
|
-
for (const currentChild of
|
|
150
|
-
|
|
151
|
-
pair(child, currentChild);
|
|
152
|
-
}
|
|
238
|
+
for (const currentChild of currentChildren || []) {
|
|
239
|
+
pair(child, currentChild);
|
|
153
240
|
}
|
|
154
241
|
}
|
|
155
242
|
}
|
|
156
243
|
}
|
|
157
244
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return obj.constructor.getRelationNames().some((name) => obj[name] !== undefined);
|
|
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]));
|
|
164
250
|
}
|
|
165
251
|
|
|
166
|
-
// Fetch the rows of `nodes`,
|
|
167
|
-
// their ids,
|
|
168
|
-
async function fetchModels(builder, graphOptions, nodes,
|
|
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 } = {}) {
|
|
169
255
|
const [{ modelClass }] = nodes;
|
|
170
256
|
const nodesByRow = [...groupBy(nodes, (node) => node.obj.$idKey()).values()];
|
|
171
257
|
const maxIds = getMaxRows(builder.unsafeKnex(), modelClass.getIdColumnArray().length);
|
|
258
|
+
const expression = RelationExpression.create('[]');
|
|
172
259
|
const models = [];
|
|
173
260
|
|
|
174
|
-
// Fetch each row once, with the relations of all its places:
|
|
175
261
|
for (const chunkNodes of chunk(nodesByRow, maxIds)) {
|
|
176
|
-
models.push(
|
|
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
|
+
);
|
|
177
272
|
}
|
|
178
273
|
|
|
179
274
|
return models;
|
|
180
275
|
}
|
|
181
276
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
{ withRelations = false, onlyIds = false },
|
|
187
|
-
) {
|
|
188
|
-
const [[{ modelClass }]] = nodesByRow;
|
|
189
|
-
const ids = nodesByRow.map(([node]) => node.obj.$id());
|
|
190
|
-
const nodes = nodesByRow.flat();
|
|
191
|
-
const expression = RelationExpression.fromModelGraph(
|
|
192
|
-
withRelations ? nodes.map((node) => node.obj) : [],
|
|
193
|
-
);
|
|
194
|
-
|
|
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) {
|
|
195
281
|
return modelClass
|
|
196
282
|
.query()
|
|
197
283
|
.childQueryOf(builder, { fork: true, isInternalQuery: true })
|
|
198
284
|
.modify((fetchBuilder) => {
|
|
199
|
-
// Lock the fetched rows like the upsert query asks for.
|
|
200
285
|
for (const method of ['forUpdate', 'forShare', 'forNoKeyUpdate', 'forKeyShare']) {
|
|
201
286
|
if (builder.has(method)) {
|
|
202
287
|
fetchBuilder[method]();
|
|
203
288
|
}
|
|
204
289
|
}
|
|
205
|
-
})
|
|
206
|
-
.modify((fetchBuilder) => {
|
|
207
|
-
const nodesByRelationPath = withRelations
|
|
208
|
-
? getNodesByRelationPath(nodes, expression)
|
|
209
|
-
: new Map([['', nodes]]);
|
|
210
290
|
|
|
211
|
-
for (const [relationPath, pathNodes] of
|
|
291
|
+
for (const [relationPath, pathNodes] of getNodesByRelationPath(nodes, expression)) {
|
|
212
292
|
const select = onlyIds
|
|
213
293
|
? (builder) => builder.select(builder.fullIdColumn())
|
|
214
294
|
: createSelect(pathNodes, graphOptions, !relationPath);
|
|
@@ -220,52 +300,39 @@ function fetchModelsOfNodes(
|
|
|
220
300
|
}
|
|
221
301
|
}
|
|
222
302
|
})
|
|
223
|
-
.findByIds(ids)
|
|
224
303
|
.withGraphFetched(expression)
|
|
225
304
|
.internalOptions({ keepImplicitJoinProps: true });
|
|
226
305
|
}
|
|
227
306
|
|
|
228
307
|
// Group the fetched nodes and their descendants by their relation paths below
|
|
229
|
-
// the fetched nodes
|
|
230
|
-
// 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.
|
|
231
310
|
function getNodesByRelationPath(nodes, expression) {
|
|
232
|
-
const nodesByRelationPath = new Map();
|
|
233
|
-
|
|
234
|
-
for (const node of nodes) {
|
|
235
|
-
const depth = node.relationPath.length;
|
|
236
|
-
|
|
237
|
-
for (const descendant of [node, ...node.descendants]) {
|
|
238
|
-
const relationPath = descendant.relationPath.slice(depth).join('.');
|
|
239
|
-
const pathNodes = nodesByRelationPath.get(relationPath);
|
|
240
|
-
|
|
241
|
-
if (pathNodes) {
|
|
242
|
-
pathNodes.push(descendant);
|
|
243
|
-
} else {
|
|
244
|
-
nodesByRelationPath.set(relationPath, [descendant]);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
311
|
+
const nodesByRelationPath = new Map([['', []]]);
|
|
249
312
|
const addPaths = (expressionNode, path) => {
|
|
250
313
|
for (const name of expressionNode.$childNames) {
|
|
251
314
|
const relationPath = [...path, name];
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (!nodesByRelationPath.has(key)) {
|
|
255
|
-
nodesByRelationPath.set(key, []);
|
|
256
|
-
}
|
|
257
|
-
|
|
315
|
+
nodesByRelationPath.set(relationPath.join('.'), []);
|
|
258
316
|
addPaths(expressionNode[name], relationPath);
|
|
259
317
|
}
|
|
260
318
|
};
|
|
261
319
|
|
|
262
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
|
+
|
|
263
330
|
return nodesByRelationPath;
|
|
264
331
|
}
|
|
265
332
|
|
|
266
333
|
function createSelect(nodes, graphOptions, isFetchRoot) {
|
|
267
334
|
if (graphOptions.isFetchStrategy(FetchStrategy.OnlyIdentifiers)) {
|
|
268
|
-
return (builder) => builder.select(builder
|
|
335
|
+
return (builder) => builder.select(getKeySelections(builder, nodes));
|
|
269
336
|
} else if (graphOptions.isFetchStrategy(FetchStrategy.OnlyNeeded)) {
|
|
270
337
|
return (builder) => builder.select(getInputColumnSelections(builder, nodes, isFetchRoot));
|
|
271
338
|
} else {
|
|
@@ -273,6 +340,22 @@ function createSelect(nodes, graphOptions, isFetchRoot) {
|
|
|
273
340
|
}
|
|
274
341
|
}
|
|
275
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
|
+
|
|
276
359
|
// Select only the columns that the graph gives, and always the ids. Related
|
|
277
360
|
// models are fetched without the join tables of their relations, so their join
|
|
278
361
|
// table extras are left out at the root of the fetch.
|
|
@@ -302,13 +385,5 @@ function getInputColumnSelections(builder, nodes, isFetchRoot) {
|
|
|
302
385
|
}
|
|
303
386
|
}
|
|
304
387
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
for (const idColumn of asArray(builder.fullIdColumn())) {
|
|
308
|
-
if (!values.includes(idColumn)) {
|
|
309
|
-
values.push(idColumn);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return values;
|
|
388
|
+
return [...new Set([...selections.values(), ...getKeySelections(builder, nodes)])];
|
|
314
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() {
|
|
@@ -183,8 +191,15 @@ export class GraphNode {
|
|
|
183
191
|
|
|
184
192
|
for (const node of nodes) {
|
|
185
193
|
if (node.isReference) {
|
|
194
|
+
// A `#ref` places its model in a relation, which a root doesn't have:
|
|
195
|
+
if (!node.parent) {
|
|
196
|
+
throw node.modelClass.createValidationError({
|
|
197
|
+
type: ValidationErrorType.InvalidGraph,
|
|
198
|
+
message: `the reference at ${formatPath(node.dataPathKey)} has no relation to place its model in`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
186
202
|
node.referencedNode = getNode(node.reference);
|
|
187
|
-
node.referencedNode.referencingNodes.push(node);
|
|
188
203
|
}
|
|
189
204
|
|
|
190
205
|
forEachPropertyReference(node.obj, ({ path, match, uid, refPath, isWholeValue }) => {
|
|
@@ -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
|
|