objection 4.0.0-rc.1 → 4.0.0-rc.2

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