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.
@@ -1,57 +1,57 @@
1
- import { asArray, chunk, groupBy, uniqBy } from '../../utils/objectUtils.js';
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
- for (const [isMovedRow, groupNodes] of groupBy(nodes, (node) => areRelated && isMoved(node))) {
29
- for (const nodesOfClass of groupBy(groupNodes, (node) => node.modelClass).values()) {
30
- const models = await fetchModels(builder, plan.graphOptions, nodesOfClass, {
31
- withRelations: !isMovedRow,
32
- });
33
- const modelsByKey = new Map(models.map((model) => [model.$idKey(), model]));
34
-
35
- for (const node of nodesOfClass) {
36
- const model = modelsByKey.get(node.obj.$idKey());
37
-
38
- if (model && isMovedRow && hasRelations(node.obj)) {
39
- movedNodes.push(node);
40
- } else if (model) {
41
- const currentNodes = GraphNode.buildGraph(node.modelClass, model, node.relationPath);
42
- const [currentNode] = currentNodes;
43
- plan.currentNodes.push(...currentNodes);
44
-
45
- if (areRelated) {
46
- node.row = currentNode;
47
- currentNode.node = node;
48
- pairChildren(node, currentNode);
49
- } else {
50
- pair(node, currentNode);
51
- }
52
- } else if (areRelated) {
53
- node.isMissing = true;
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; passNodes.has(parent); parent = parent.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) => !passNodes.has(node.parent) && node.hasId),
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
- hasRelations(node.obj) ||
96
- (plan.graphOptions.has(node, 'insertMissing') && !rowKeys.has(node.rowKey));
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, plan.graphOptions, nodesOfClass, {
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
- return movedNodes;
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 key = child.obj.$idKey();
236
+ const currentChildren = getChildrenByKey(child.relation.name).get(child.obj.$idKey());
140
237
 
141
- for (const currentChild of childrenByRelation.get(child.relation.name) || []) {
142
- if (currentChild.obj.$idKey() === key) {
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
- function isMoved(node) {
151
- return node.relation.keySide === 'related';
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
- function hasRelations(obj) {
155
- return obj.constructor.getRelationNames().some((name) => obj[name] !== undefined);
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 uniqueNodes = uniqBy(nodes, (node) => node.obj.$idKey());
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(uniqueNodes, maxIds)) {
167
- models.push(...(await fetchModelsOfNodes(builder, graphOptions, chunkNodes, options)));
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
- function fetchModelsOfNodes(
174
- builder,
175
- graphOptions,
176
- nodes,
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 nodesByRelationPath) {
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. Relations with nulls or empty arrays have no nodes, but
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
- const key = relationPath.join('.');
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.fullIdColumn());
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
- const values = [...selections.values()];
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` 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() {
@@ -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
- 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