objection 4.0.0-rc.2 → 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.
@@ -4,17 +4,18 @@ import { valuesKey } from '../../model/modelValues.js';
4
4
  import { UpdateOperation } from '../operations/UpdateOperation.js';
5
5
  import { getMaxRows, hasBatchInsert } from '../../utils/knexUtils.js';
6
6
  import { isInternalProp } from '../../utils/internalPropUtils.js';
7
- import { chunk, difference, groupBy, isObject, nonStrictEquals } from '../../utils/objectUtils.js';
8
- import { map as promiseMap } from '../../utils/promiseUtils/index.js';
9
7
  import {
10
- clearKeysOfRemovedRows,
11
- fetchGeneratedKeys,
12
- getChangedKeysOfRelatedRows,
13
- resolveDependencies,
14
- resolveFilledDependencies,
15
- resolveReference,
16
- setKeyArrays,
17
- } from './graphKeys.js';
8
+ chunk,
9
+ difference,
10
+ get,
11
+ groupBy,
12
+ isObject,
13
+ jsonEquals,
14
+ nonStrictEquals,
15
+ set,
16
+ } from '../../utils/objectUtils.js';
17
+ import { map as promiseMap } from '../../utils/promiseUtils/index.js';
18
+ import { hasOwnerKey, isOtherKey } from './graphKeys.js';
18
19
 
19
20
  const MAX_INSERT_BATCH_SIZE = 100;
20
21
  const MAX_CONCURRENCY = 100;
@@ -94,7 +95,9 @@ export class GraphWriter {
94
95
  // Rows that reference their owners are found through the relation.
95
96
  // BelongsToOne rows aren't referenced anymore once their owners are
96
97
  // patched.
97
- const owners = [...new Set(groupNodes.map((node) => node.parent.obj))];
98
+ const owners = [...new Set(groupNodes.map((node) => node.parent))].map((parent) =>
99
+ getRelationOwner(this.schedule.keys, parent.node, parent.obj, relation),
100
+ );
98
101
  const query = (
99
102
  areReferenced
100
103
  ? relation.ownerModelClass.relatedQuery(relation.name).for(owners)
@@ -371,9 +374,12 @@ export class GraphWriter {
371
374
  ) {
372
375
  const query = (
373
376
  isPatchedThroughRelation
374
- ? // Find the row through the keys of the owner's current row, as the
375
- // graph may leave them out of the owner:
376
- (node.parent.existing || node.parent).obj
377
+ ? getRelationOwner(
378
+ this.schedule.keys,
379
+ node.parent,
380
+ (node.parent.existing || node.parent).obj,
381
+ node.relation,
382
+ )
377
383
  .$relatedQuery(node.relation.name)
378
384
  .findById(node.obj.$id())
379
385
  : (node.current?.obj || node.obj).$query()
@@ -484,6 +490,26 @@ async function throwForMissingRow(builder, node) {
484
490
  }
485
491
  }
486
492
 
493
+ // Return the owner to find the rows of `relation` through: the current row
494
+ // `obj` of the owner, as the graph may leave its keys out, with the final keys
495
+ // of `node` if they change, as the relation was read at them.
496
+ function getRelationOwner(keys, node, obj, relation) {
497
+ const { ownerProp } = relation;
498
+ const values = keys.getReadProps(node, ownerProp);
499
+
500
+ if (isOtherKey(values, ownerProp.getProps(obj))) {
501
+ const owner = obj.$clone({ shallow: true });
502
+ values.forEach((value, i) => {
503
+ if (value !== undefined) {
504
+ ownerProp.setProp(owner, i, value);
505
+ }
506
+ });
507
+ return owner;
508
+ } else {
509
+ return obj;
510
+ }
511
+ }
512
+
487
513
  function createPatchNotFoundError(builder, node) {
488
514
  return node.modelClass.createNotFoundError(builder.context(), {
489
515
  message: `model (id=${node.obj.$id()}) was not found when updating it. An earlier write of the graph or a concurrent query may have deleted it`,
@@ -506,3 +532,220 @@ function getConcurrency(builder, nodes) {
506
532
  function isReturningAll(operation) {
507
533
  return operation.name === 'returning' && operation.args.includes('*');
508
534
  }
535
+
536
+ // Copy the keys and `#ref{}` values that `node` waits for into its model.
537
+ function resolveDependencies(schedule, node) {
538
+ for (const dependency of schedule.dependencies.get(node) || []) {
539
+ if (!schedule.deferredDependencies.has(dependency)) {
540
+ if (dependency.relation) {
541
+ // Hooks see the props of the models that `#ref` models refer to:
542
+ resolveReference(dependency.related);
543
+ resolveReference(dependency.dependent);
544
+ } else if (dependency.reference) {
545
+ resolvePropertyReference(dependency.dependent, dependency.reference);
546
+ }
547
+ }
548
+ }
549
+
550
+ schedule.keys.resolvedNodes.add(node);
551
+ schedule.keys.fill(node);
552
+ }
553
+
554
+ // Copy the keys and `#ref{}` values that `node` waits for into its model,
555
+ // after those that the graph fills into the models that it reads them from,
556
+ // e.g. the new key of an owner whose BelongsToOne row is replaced. Rows that
557
+ // move first only wait for rows that exist, see `GraphSchedule#_waitsForInserts()`.
558
+ function resolveFilledDependencies(schedule, node, visitedNodes = new Set()) {
559
+ visitedNodes.add(node);
560
+
561
+ for (const dependency of schedule.dependencies.get(node) || []) {
562
+ const referencedNode = dependency.node.referencedNode || dependency.node;
563
+
564
+ if (
565
+ !visitedNodes.has(referencedNode) &&
566
+ (dependency.isKeyWait || schedule.readsFilledProps(dependency))
567
+ ) {
568
+ resolveFilledDependencies(schedule, referencedNode, visitedNodes);
569
+ }
570
+ }
571
+
572
+ resolveDependencies(schedule, node);
573
+ }
574
+
575
+ // Change the arrays of keys that `node` holds for the relations that the graph
576
+ // gives it: Remove the keys of the current related rows that it deletes or
577
+ // unrelates, and add the keys of the related models that it writes. Other
578
+ // keys stay, e.g. of rows that the relation's filter leaves out. Relations
579
+ // that the graph leaves out keep their arrays (#1202). Return the props that
580
+ // change.
581
+ function setKeyArrays(plan, node) {
582
+ const { actions, graphOptions } = plan;
583
+ const { existing } = node;
584
+ const props = [];
585
+
586
+ for (const relation of node.relations) {
587
+ const { name } = relation;
588
+
589
+ if (hasOwnerKeyArray(relation)) {
590
+ const { ownerProp, relatedProp } = relation;
591
+ const getKey = (related) => relatedProp.getProp(related.obj, 0);
592
+ const keys = relation.getKeys(existing ? existing.obj : node.obj);
593
+ const removed = (existing?.activeChildren || []).filter(
594
+ (child) =>
595
+ child.relation.name === name && ['delete', 'unrelate'].includes(actions.get(child)),
596
+ );
597
+ const added = node.activeChildren.filter(
598
+ (child) =>
599
+ child.relation.name === name &&
600
+ !child.toBeUnrelatedOrDeleted &&
601
+ actions.get(child) !== 'ignore',
602
+ );
603
+
604
+ for (const related of added) {
605
+ resolveReference(related);
606
+ }
607
+
608
+ const newKeys = relation.changeKeys(keys, {
609
+ add: added.map(getKey),
610
+ remove: removed.map(getKey),
611
+ });
612
+
613
+ if (!existing || graphOptions.shouldUpdate(node)) {
614
+ // Inserts and updates write the whole row:
615
+ ownerProp.setProp(node.obj, 0, newKeys);
616
+ props.push(ownerProp.props[0]);
617
+ } else if (!jsonEquals(keys, newKeys)) {
618
+ const patch = relation.createKeysPatch(existing.obj, newKeys);
619
+ Object.assign(node.obj, patch);
620
+ props.push(...Object.keys(patch));
621
+ }
622
+ }
623
+ }
624
+
625
+ return props;
626
+ }
627
+
628
+ // Return the keys of `node` that point to other BelongsToOne rows than before,
629
+ // which the graph inserts or relates, and which `GraphKeys` filled in for their
630
+ // places.
631
+ function getChangedKeysOfRelatedRows(plan, keys, node) {
632
+ const { actions } = plan;
633
+ const props = [];
634
+
635
+ for (const child of node.activeChildren) {
636
+ const { relation } = child;
637
+
638
+ // Ignored models don't replace the current one, and models marked with
639
+ // `#unrelate` or `#delete` are handled by `clearKeysOfRemovedRows()`:
640
+ if (hasOwnerKey(relation) && actions.get(child) !== 'ignore') {
641
+ relation.ownerProp.forEach((i) => {
642
+ const currentValue = node.existing && relation.ownerProp.getProp(node.existing.obj, i);
643
+ const value = relation.ownerProp.getProp(node.obj, i);
644
+
645
+ if (
646
+ value !== undefined &&
647
+ currentValue != value &&
648
+ keys.isSet(node, relation.ownerProp, i)
649
+ ) {
650
+ props.push(relation.ownerProp.props[i]);
651
+ }
652
+ });
653
+ }
654
+ }
655
+
656
+ return props;
657
+ }
658
+
659
+ // Clear the keys of `node` that point to BelongsToOne rows that are removed,
660
+ // and return the props that change.
661
+ function clearKeysOfRemovedRows(plan, node) {
662
+ const props = [];
663
+
664
+ for (const relation of plan.getRemovedOwnerRelations(node)) {
665
+ relation.ownerProp.forEach((i) => {
666
+ if (relation.ownerProp.getProp(node.existing.obj, i) != null) {
667
+ relation.ownerProp.setProp(node.obj, i, null);
668
+ props.push(relation.ownerProp.props[i]);
669
+ }
670
+ });
671
+ }
672
+
673
+ return props;
674
+ }
675
+
676
+ // Give a `#ref` model the props of the model it refers to that it doesn't have,
677
+ // and the `filledProps` again, as they may have changed since. Its own keys
678
+ // stay its own, and the join table extras of the place of the model belong to
679
+ // the join row there.
680
+ function resolveReference(node, filledProps = []) {
681
+ const { referencedNode, relation } = node;
682
+
683
+ if (referencedNode) {
684
+ const ownKeys = relation?.keySide === 'related' ? relation.relatedProp.props : [];
685
+ const modelExtras =
686
+ referencedNode.relation?.keySide === 'join'
687
+ ? referencedNode.relation.joinTableExtras.map((extra) => extra.aliasProp)
688
+ : [];
689
+
690
+ for (const prop of difference(Object.keys(referencedNode.obj), modelExtras)) {
691
+ if (
692
+ !Object.hasOwn(node.obj, prop) ||
693
+ (filledProps.includes(prop) && !ownKeys.includes(prop))
694
+ ) {
695
+ node.obj[prop] = referencedNode.obj[prop];
696
+ }
697
+ }
698
+ }
699
+ }
700
+
701
+ // Replace a `#ref{}` in a prop of `node` with the value it refers to:
702
+ function resolvePropertyReference(node, { path, match, refPath, isWholeValue, referencedNode }) {
703
+ const value = get(referencedNode.obj, refPath);
704
+ set(node.obj, path, isWholeValue ? value : get(node.obj, path).replace(match, value));
705
+ }
706
+
707
+ // Fetch the keys that the relations of the inserted nodes need, but that the
708
+ // database generated and the insert didn't return, like a uuid that isn't the
709
+ // id (Vincit#853).
710
+ async function fetchGeneratedKeys(builder, modelClass, nodes) {
711
+ const keyProps = (node) => [
712
+ ...(node.relation && node.relation.keySide !== 'related' ? [node.relation.relatedProp] : []),
713
+ ...node.activeChildren
714
+ .filter((child) => child.relation.keySide !== 'owner')
715
+ .map((child) => child.relation.ownerProp),
716
+ ];
717
+ const missingNodes = nodes.filter((node) =>
718
+ keyProps(node).some((prop) => prop.getProps(node.obj).some((value) => value === undefined)),
719
+ );
720
+
721
+ if (missingNodes.length > 0 && missingNodes.every((node) => node.obj.$hasId())) {
722
+ // SQL Server returns columns selected twice as arrays, so the ids are
723
+ // part of the set:
724
+ const columns = new Set([
725
+ ...modelClass.getIdColumnArray(),
726
+ ...missingNodes.flatMap((node) => keyProps(node)).flatMap((prop) => prop.cols),
727
+ ]);
728
+ const rows = await modelClass
729
+ .query()
730
+ .childQueryOf(builder, { fork: true, isInternalQuery: true })
731
+ .findByIds(missingNodes.map((node) => node.obj.$id()))
732
+ .select([...columns]);
733
+ const rowsByKey = new Map(rows.map((row) => [row.$idKey(), row]));
734
+
735
+ for (const node of missingNodes) {
736
+ const row = rowsByKey.get(node.obj.$idKey());
737
+
738
+ for (const prop of keyProps(node)) {
739
+ prop.forEach((i) => {
740
+ if (prop.getProp(node.obj, i) === undefined) {
741
+ prop.setProp(node.obj, i, prop.getProp(row, i));
742
+ }
743
+ });
744
+ }
745
+ }
746
+ }
747
+ }
748
+
749
+ function hasOwnerKeyArray(relation) {
750
+ return relation.keySide === 'owner' && !relation.isOneToOne();
751
+ }