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.
@@ -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 of the pass with their relations,
11
- // then the rows of the models that the graph relates from elsewhere, with the
12
- // relations that the graph gives them, level by level, as the rows of related
13
- // models decide which models below them are related too. Models that
14
- // `insertMissing` inserts if they don't exist are looked up too. The rows that
15
- // a relate moves into a HasManyRelation or HasOneRelation are fetched without
16
- // their relations, which change with the move. Return the ones that the graph
17
- // gives relations, to be upserted in a pass of their own after the move.
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
- const movedNodes = [];
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
- // 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)) {
32
- for (const nodesOfClass of groupBy(groupNodes, (node) => node.modelClass).values()) {
33
- const models = await fetchModels(builder, plan.graphOptions, nodesOfClass, {
34
- withRelations: !isMovedRow,
35
- });
36
- const modelsByKey = new Map(models.map((model) => [model.$idKey(), model]));
37
-
38
- for (const node of nodesOfClass) {
39
- const model = modelsByKey.get(node.obj.$idKey());
40
-
41
- if (model && isMovedRow && hasRelations(node.obj)) {
42
- movedNodes.push(node);
43
- } else if (model) {
44
- const currentNodes = GraphNode.buildGraph(node.modelClass, model, node.relationPath);
45
- const [currentNode] = currentNodes;
46
- plan.currentNodes.push(...currentNodes);
47
-
48
- if (areRelated) {
49
- node.row = currentNode;
50
- currentNode.node = node;
51
- pairChildren(node, currentNode);
52
- } else {
53
- pair(node, currentNode);
54
- }
55
- } else if (areRelated) {
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; passNodes.has(parent); parent = parent.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 of the pass are at their places, except for the relates that an
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
- hasRelations(node.obj) ||
104
- (plan.graphOptions.has(node, 'insertMissing') && !rowKeys.has(node.rowKey));
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, plan.graphOptions, nodesOfClass, {
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 movedNodes;
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 key = child.obj.$idKey();
236
+ const currentChildren = getChildrenByKey(child.relation.name).get(child.obj.$idKey());
148
237
 
149
- for (const currentChild of childrenByRelation.get(child.relation.name) || []) {
150
- if (currentChild.obj.$idKey() === key) {
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
- function isMoved(node) {
159
- return node.relation.keySide === 'related';
160
- }
161
-
162
- function hasRelations(obj) {
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`, with their relations unless they're moved, or only
167
- // their ids, in as many queries as the number of their ids requires.
168
- 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 } = {}) {
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(...(await fetchModelsOfNodes(builder, graphOptions, chunkNodes, options)));
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
- function fetchModelsOfNodes(
183
- builder,
184
- graphOptions,
185
- nodesByRow,
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 nodesByRelationPath) {
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. Relations with nulls or empty arrays have no nodes, but
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
- const key = relationPath.join('.');
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.fullIdColumn());
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
- const values = [...selections.values()];
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` or `#ref{}` point to this node:
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
- NO_RELATE,
22
- NO_UNRELATE,
23
- NO_INSERT,
24
- NO_UPDATE,
25
- NO_DELETE,
26
- UPDATE,
27
- RELATE,
28
- UNRELATE,
29
- INSERT_MISSING,
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, FETCH_STRATEGY, ALLOW_REFS, ALLOW_MARKERS];
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 = [RELATE, NO_RELATE, NO_INSERT, ALLOW_REFS];
22
+ const INSERT_OPTION_NAMES = ['relate', 'noRelate', 'noInsert', 'allowRefs'];
37
23
  const INSERT_ONLY_OPTIONS = {
38
- [NO_UPDATE]: true,
39
- [NO_DELETE]: true,
40
- [NO_UNRELATE]: true,
41
- [INSERT_MISSING]: true,
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 (!FetchStrategy[strategy]) {
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[FETCH_STRATEGY] === strategy;
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, NO_UPDATE) && !this.has(node, UPDATE);
63
+ return !this.has(node, 'noUpdate') && !this.has(node, 'update');
82
64
  }
83
65
 
84
66
  shouldUpdate(node) {
85
- return !this.has(node, NO_UPDATE) && this.has(node, UPDATE);
67
+ return !this.has(node, 'noUpdate') && this.has(node, 'update');
86
68
  }
87
69
 
88
70
  allowRefs() {
89
- return !!this.options[ALLOW_REFS];
71
+ return !!this.options.allowRefs;
90
72
  }
91
73
 
92
74
  allowMarkers() {
93
- return !!this.options[ALLOW_MARKERS];
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
- let parsedOptions = options;
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
- return new Error(
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