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
|
@@ -2,19 +2,21 @@ import { getModel } from '../../model/getModel.js';
|
|
|
2
2
|
import { restoreOmitFromDatabaseJson } from '../../model/modelUtils.js';
|
|
3
3
|
import { valuesKey } from '../../model/modelValues.js';
|
|
4
4
|
import { UpdateOperation } from '../operations/UpdateOperation.js';
|
|
5
|
+
import { removeFieldExpressions } from '../operations/fieldExpressions.js';
|
|
5
6
|
import { getMaxRows, hasBatchInsert } from '../../utils/knexUtils.js';
|
|
6
7
|
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
8
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
chunk,
|
|
10
|
+
difference,
|
|
11
|
+
get,
|
|
12
|
+
groupBy,
|
|
13
|
+
isObject,
|
|
14
|
+
jsonEquals,
|
|
15
|
+
nonStrictEquals,
|
|
16
|
+
set,
|
|
17
|
+
} from '../../utils/objectUtils.js';
|
|
18
|
+
import { map as promiseMap } from '../../utils/promiseUtils/index.js';
|
|
19
|
+
import { hasOwnerKey, isOtherKey } from './graphKeys.js';
|
|
18
20
|
|
|
19
21
|
const MAX_INSERT_BATCH_SIZE = 100;
|
|
20
22
|
const MAX_CONCURRENCY = 100;
|
|
@@ -94,7 +96,9 @@ export class GraphWriter {
|
|
|
94
96
|
// Rows that reference their owners are found through the relation.
|
|
95
97
|
// BelongsToOne rows aren't referenced anymore once their owners are
|
|
96
98
|
// patched.
|
|
97
|
-
const owners = [...new Set(groupNodes.map((node) => node.parent
|
|
99
|
+
const owners = [...new Set(groupNodes.map((node) => node.parent))].map((parent) =>
|
|
100
|
+
getRelationOwner(this.schedule.keys, parent.node, parent.obj, relation),
|
|
101
|
+
);
|
|
98
102
|
const query = (
|
|
99
103
|
areReferenced
|
|
100
104
|
? relation.ownerModelClass.relatedQuery(relation.name).for(owners)
|
|
@@ -122,14 +126,15 @@ export class GraphWriter {
|
|
|
122
126
|
await promiseMap(batches, (batch) => this._insertBatch(builder, batch), {
|
|
123
127
|
concurrency: getConcurrency(builder, nodes),
|
|
124
128
|
});
|
|
125
|
-
await this._patch(
|
|
126
|
-
builder,
|
|
127
|
-
nodes.filter((node) => this.schedule.slotMovedNodes.has(node)),
|
|
128
|
-
);
|
|
129
129
|
|
|
130
130
|
for (const node of references) {
|
|
131
131
|
resolveDependencies(this.schedule, node);
|
|
132
132
|
}
|
|
133
|
+
|
|
134
|
+
await this._patch(
|
|
135
|
+
builder,
|
|
136
|
+
[...nodes, ...references].filter((node) => this.schedule.earlyNodes.has(node)),
|
|
137
|
+
);
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
async _insertBatch(builder, nodes) {
|
|
@@ -272,6 +277,8 @@ export class GraphWriter {
|
|
|
272
277
|
} finally {
|
|
273
278
|
for (const [node, props] of omittedProps) {
|
|
274
279
|
restoreOmitFromDatabaseJson(node.obj, props);
|
|
280
|
+
// The keys of `setKeyPaths()` stay where a hook resolves the patch:
|
|
281
|
+
removeFieldExpressions(node.modelClass, node.obj);
|
|
275
282
|
}
|
|
276
283
|
}
|
|
277
284
|
}
|
|
@@ -281,6 +288,12 @@ export class GraphWriter {
|
|
|
281
288
|
_preparePatch(builder, node) {
|
|
282
289
|
const { referencedNode } = node;
|
|
283
290
|
|
|
291
|
+
// A row that the graph relates by its key alone isn't looked up, its key is
|
|
292
|
+
// only written to its owner:
|
|
293
|
+
if (this.plan.isRelatedByKey(node)) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
284
297
|
// The props that the graph fills into the model of a `#ref` model are only
|
|
285
298
|
// final now:
|
|
286
299
|
if (referencedNode) {
|
|
@@ -301,6 +314,8 @@ export class GraphWriter {
|
|
|
301
314
|
? node.relation.relatedProp.props
|
|
302
315
|
: []),
|
|
303
316
|
];
|
|
317
|
+
// The keys in json paths that the write sets or clears:
|
|
318
|
+
let pathKeys = new Map();
|
|
304
319
|
// Changing the id never makes sense with `upsertGraph()`. A many-to-many
|
|
305
320
|
// `#ref` model whose model writes its row only writes its join row:
|
|
306
321
|
const idProps = node.modelClass.getIdPropertyArray();
|
|
@@ -308,11 +323,15 @@ export class GraphWriter {
|
|
|
308
323
|
!!referencedNode && node.relation.keySide === 'join' && this.plan.isCoveredByModel(node);
|
|
309
324
|
const isWritable = (prop) =>
|
|
310
325
|
!idProps.includes(prop) &&
|
|
326
|
+
!pathKeys.has(prop) &&
|
|
311
327
|
!this.schedule.keys.isOnlyRead(node, prop) &&
|
|
312
328
|
(!isJoinRowOnly || node.relation.joinTableExtras.some((extra) => extra.aliasProp === prop));
|
|
313
329
|
|
|
314
330
|
const getPropsToUpdate = (changedProps) =>
|
|
315
|
-
(shouldPatch || shouldUpdate
|
|
331
|
+
(shouldPatch || shouldUpdate
|
|
332
|
+
? changedProps
|
|
333
|
+
: keyProps.flatMap((prop) => pathKeys.get(prop) || prop)
|
|
334
|
+
).filter(isWritable);
|
|
316
335
|
|
|
317
336
|
// Leave out the props that don't change, and return true if any do.
|
|
318
337
|
const handleUpdate = ({ changedProps, unchangedProps } = this._findChanges(node)) => {
|
|
@@ -332,21 +351,43 @@ export class GraphWriter {
|
|
|
332
351
|
// what to update. Otherwise, the hook may still change the model, and we
|
|
333
352
|
// check again after it runs, but only if the model has props to update, as
|
|
334
353
|
// relates are no updates (#2233, #2605).
|
|
335
|
-
const changes = this._findChanges(node);
|
|
336
|
-
const propsToUpdate = getPropsToUpdate(changes.changedProps);
|
|
337
354
|
const hasBeforeUpdate = hasHook(node.obj, 'beforeUpdate');
|
|
338
355
|
|
|
339
|
-
|
|
356
|
+
let changes = this._findChanges(node);
|
|
357
|
+
|
|
358
|
+
if (getPropsToUpdate(changes.changedProps).length === 0) {
|
|
340
359
|
return null;
|
|
341
|
-
} else if (!hasBeforeUpdate) {
|
|
342
|
-
handleUpdate(changes);
|
|
343
360
|
}
|
|
344
361
|
|
|
345
362
|
node.obj.$validate(null, {
|
|
346
363
|
dataPath: node.dataPathKey,
|
|
347
|
-
patch:
|
|
364
|
+
patch: !shouldUpdate,
|
|
348
365
|
});
|
|
349
366
|
|
|
367
|
+
// Write the keys in json columns as path updates where their column holds
|
|
368
|
+
// nothing else, which keeps its other values (#415). After the validation,
|
|
369
|
+
// as `update()` validates them as properties of their own:
|
|
370
|
+
pathKeys = setKeyPaths(node, [
|
|
371
|
+
...this.schedule.keys.getSetCells(node),
|
|
372
|
+
...this.plan
|
|
373
|
+
.getRemovedOwnerRelations(node)
|
|
374
|
+
.flatMap(({ ownerProp }) =>
|
|
375
|
+
ownerProp.props.map((_, index) => ({ prop: ownerProp, index })),
|
|
376
|
+
),
|
|
377
|
+
]);
|
|
378
|
+
|
|
379
|
+
if (pathKeys.size > 0) {
|
|
380
|
+
changes = this._findChanges(node);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const propsToUpdate = getPropsToUpdate(changes.changedProps);
|
|
384
|
+
|
|
385
|
+
if (propsToUpdate.length === 0) {
|
|
386
|
+
return null;
|
|
387
|
+
} else if (!hasBeforeUpdate) {
|
|
388
|
+
handleUpdate(changes);
|
|
389
|
+
}
|
|
390
|
+
|
|
350
391
|
// Rows that their owners relate by their keys can't be found through the
|
|
351
392
|
// relation until the owners are patched, so they're patched directly.
|
|
352
393
|
const { relation } = node;
|
|
@@ -357,23 +398,35 @@ export class GraphWriter {
|
|
|
357
398
|
|
|
358
399
|
return {
|
|
359
400
|
node,
|
|
360
|
-
|
|
401
|
+
shouldUpdate,
|
|
361
402
|
hasBeforeUpdate,
|
|
362
403
|
handleUpdate,
|
|
363
404
|
propsToUpdate,
|
|
405
|
+
fieldExpressionKeys: [...pathKeys.values()].flat(),
|
|
364
406
|
isPatchedThroughRelation,
|
|
365
407
|
};
|
|
366
408
|
}
|
|
367
409
|
|
|
368
410
|
_writePatch(
|
|
369
411
|
builder,
|
|
370
|
-
{
|
|
412
|
+
{
|
|
413
|
+
node,
|
|
414
|
+
shouldUpdate,
|
|
415
|
+
hasBeforeUpdate,
|
|
416
|
+
handleUpdate,
|
|
417
|
+
propsToUpdate,
|
|
418
|
+
fieldExpressionKeys,
|
|
419
|
+
isPatchedThroughRelation,
|
|
420
|
+
},
|
|
371
421
|
) {
|
|
372
422
|
const query = (
|
|
373
423
|
isPatchedThroughRelation
|
|
374
|
-
?
|
|
375
|
-
|
|
376
|
-
|
|
424
|
+
? getRelationOwner(
|
|
425
|
+
this.schedule.keys,
|
|
426
|
+
node.parent,
|
|
427
|
+
(node.parent.existing || node.parent).obj,
|
|
428
|
+
node.relation,
|
|
429
|
+
)
|
|
377
430
|
.$relatedQuery(node.relation.name)
|
|
378
431
|
.findById(node.obj.$id())
|
|
379
432
|
: (node.current?.obj || node.obj).$query()
|
|
@@ -386,6 +439,8 @@ export class GraphWriter {
|
|
|
386
439
|
// Join rows are patched with the extras among the props to update. The
|
|
387
440
|
// extras of new join rows are written with them, see `_findChanges()`:
|
|
388
441
|
joinTableExtras: node.relation?.keySide === 'join' && !node.current ? [] : propsToUpdate,
|
|
442
|
+
// The keys in json columns, also in updates:
|
|
443
|
+
fieldExpressionKeys,
|
|
389
444
|
})
|
|
390
445
|
.copyFrom(builder, isReturningAll);
|
|
391
446
|
|
|
@@ -399,7 +454,8 @@ export class GraphWriter {
|
|
|
399
454
|
});
|
|
400
455
|
}
|
|
401
456
|
|
|
402
|
-
|
|
457
|
+
// Writes of only the keys under `noUpdate` are patches too:
|
|
458
|
+
return (shouldUpdate ? query.update(node.obj) : query.patch(node.obj))
|
|
403
459
|
.runAfter(async (result, query) => {
|
|
404
460
|
if (isObject(result) && result.$isObjectionModel) {
|
|
405
461
|
// Handle `returning('*')`.
|
|
@@ -484,6 +540,26 @@ async function throwForMissingRow(builder, node) {
|
|
|
484
540
|
}
|
|
485
541
|
}
|
|
486
542
|
|
|
543
|
+
// Return the owner to find the rows of `relation` through: the current row
|
|
544
|
+
// `obj` of the owner, as the graph may leave its keys out, with the final keys
|
|
545
|
+
// of `node` if they change, as the relation was read at them.
|
|
546
|
+
function getRelationOwner(keys, node, obj, relation) {
|
|
547
|
+
const { ownerProp } = relation;
|
|
548
|
+
const values = keys.getReadProps(node, ownerProp);
|
|
549
|
+
|
|
550
|
+
if (isOtherKey(values, ownerProp.getProps(obj))) {
|
|
551
|
+
const owner = obj.$clone({ shallow: true });
|
|
552
|
+
values.forEach((value, i) => {
|
|
553
|
+
if (value !== undefined) {
|
|
554
|
+
ownerProp.setProp(owner, i, value);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
return owner;
|
|
558
|
+
} else {
|
|
559
|
+
return obj;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
487
563
|
function createPatchNotFoundError(builder, node) {
|
|
488
564
|
return node.modelClass.createNotFoundError(builder.context(), {
|
|
489
565
|
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 +582,254 @@ function getConcurrency(builder, nodes) {
|
|
|
506
582
|
function isReturningAll(operation) {
|
|
507
583
|
return operation.name === 'returning' && operation.args.includes('*');
|
|
508
584
|
}
|
|
585
|
+
|
|
586
|
+
// Copy the keys and `#ref{}` values that `node` waits for into its model.
|
|
587
|
+
function resolveDependencies(schedule, node) {
|
|
588
|
+
for (const dependency of schedule.dependencies.get(node) || []) {
|
|
589
|
+
if (!schedule.deferredDependencies.has(dependency)) {
|
|
590
|
+
if (dependency.relation) {
|
|
591
|
+
// Hooks see the props of the models that `#ref` models refer to:
|
|
592
|
+
resolveReference(dependency.related);
|
|
593
|
+
resolveReference(dependency.dependent);
|
|
594
|
+
} else if (dependency.reference) {
|
|
595
|
+
resolvePropertyReference(dependency.dependent, dependency.reference);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
schedule.keys.resolvedNodes.add(node);
|
|
601
|
+
schedule.keys.fill(node);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Copy the keys and `#ref{}` values that `node` waits for into its model,
|
|
605
|
+
// after those that the graph fills into the models that it reads them from,
|
|
606
|
+
// e.g. the new key of an owner whose BelongsToOne row is replaced. Rows that
|
|
607
|
+
// move first only wait for rows that exist, see `GraphSchedule#_waitsForWrites()`.
|
|
608
|
+
function resolveFilledDependencies(schedule, node, visitedNodes = new Set()) {
|
|
609
|
+
visitedNodes.add(node);
|
|
610
|
+
|
|
611
|
+
for (const dependency of schedule.dependencies.get(node) || []) {
|
|
612
|
+
const referencedNode = dependency.node.referencedNode || dependency.node;
|
|
613
|
+
|
|
614
|
+
if (
|
|
615
|
+
!visitedNodes.has(referencedNode) &&
|
|
616
|
+
(dependency.isKeyWait || schedule.readsFilledProps(dependency))
|
|
617
|
+
) {
|
|
618
|
+
resolveFilledDependencies(schedule, referencedNode, visitedNodes);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
resolveDependencies(schedule, node);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Change the arrays of keys that `node` holds for the relations that the graph
|
|
626
|
+
// gives it: Remove the keys of the current related rows that it deletes or
|
|
627
|
+
// unrelates, and add the keys of the related models that it writes. Other
|
|
628
|
+
// keys stay, e.g. of rows that the relation's filter leaves out. Relations
|
|
629
|
+
// that the graph leaves out keep their arrays (#1202). Return the props that
|
|
630
|
+
// change.
|
|
631
|
+
function setKeyArrays(plan, node) {
|
|
632
|
+
const { actions, graphOptions } = plan;
|
|
633
|
+
const { existing } = node;
|
|
634
|
+
const props = [];
|
|
635
|
+
|
|
636
|
+
for (const relation of node.relations) {
|
|
637
|
+
const { name } = relation;
|
|
638
|
+
|
|
639
|
+
if (hasOwnerKeyArray(relation)) {
|
|
640
|
+
const { ownerProp, relatedProp } = relation;
|
|
641
|
+
const getKey = (related) => relatedProp.getProp(related.obj, 0);
|
|
642
|
+
const keys = relation.getKeys(existing ? existing.obj : node.obj);
|
|
643
|
+
const removed = (existing?.activeChildren || []).filter(
|
|
644
|
+
(child) =>
|
|
645
|
+
child.relation.name === name && ['delete', 'unrelate'].includes(actions.get(child)),
|
|
646
|
+
);
|
|
647
|
+
const added = node.activeChildren.filter(
|
|
648
|
+
(child) =>
|
|
649
|
+
child.relation.name === name &&
|
|
650
|
+
!child.toBeUnrelatedOrDeleted &&
|
|
651
|
+
actions.get(child) !== 'ignore',
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
for (const related of added) {
|
|
655
|
+
resolveReference(related);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const newKeys = relation.changeKeys(keys, {
|
|
659
|
+
add: added.map(getKey),
|
|
660
|
+
remove: removed.map(getKey),
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
if (!existing || graphOptions.shouldUpdate(node)) {
|
|
664
|
+
// Inserts and updates write the whole row:
|
|
665
|
+
ownerProp.setProp(node.obj, 0, newKeys);
|
|
666
|
+
props.push(ownerProp.props[0]);
|
|
667
|
+
} else if (!jsonEquals(keys, newKeys)) {
|
|
668
|
+
const patch = relation.createKeysPatch(existing.obj, newKeys);
|
|
669
|
+
Object.assign(node.obj, patch);
|
|
670
|
+
props.push(...Object.keys(patch));
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return props;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Return the keys of `node` that point to other BelongsToOne rows than before,
|
|
679
|
+
// which the graph inserts or relates, and which `GraphKeys` filled in for their
|
|
680
|
+
// places.
|
|
681
|
+
function getChangedKeysOfRelatedRows(plan, keys, node) {
|
|
682
|
+
const { actions } = plan;
|
|
683
|
+
const props = [];
|
|
684
|
+
|
|
685
|
+
for (const child of node.activeChildren) {
|
|
686
|
+
const { relation } = child;
|
|
687
|
+
|
|
688
|
+
// Ignored models don't replace the current one, and models marked with
|
|
689
|
+
// `#unrelate` or `#delete` are handled by `clearKeysOfRemovedRows()`:
|
|
690
|
+
if (hasOwnerKey(relation) && actions.get(child) !== 'ignore') {
|
|
691
|
+
relation.ownerProp.forEach((i) => {
|
|
692
|
+
const currentValue = node.existing && relation.ownerProp.getProp(node.existing.obj, i);
|
|
693
|
+
const value = relation.ownerProp.getProp(node.obj, i);
|
|
694
|
+
|
|
695
|
+
if (
|
|
696
|
+
value !== undefined &&
|
|
697
|
+
currentValue != value &&
|
|
698
|
+
keys.isSet(node, relation.ownerProp, i)
|
|
699
|
+
) {
|
|
700
|
+
props.push(relation.ownerProp.props[i]);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return props;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Clear the keys of `node` that point to BelongsToOne rows that are removed,
|
|
710
|
+
// and return the props that change.
|
|
711
|
+
function clearKeysOfRemovedRows(plan, node) {
|
|
712
|
+
const props = [];
|
|
713
|
+
|
|
714
|
+
for (const relation of plan.getRemovedOwnerRelations(node)) {
|
|
715
|
+
relation.ownerProp.forEach((i) => {
|
|
716
|
+
if (relation.ownerProp.getProp(node.existing.obj, i) != null) {
|
|
717
|
+
relation.ownerProp.setProp(node.obj, i, null);
|
|
718
|
+
props.push(relation.ownerProp.props[i]);
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return props;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Give a `#ref` model the props of the model it refers to that it doesn't have,
|
|
727
|
+
// and the `filledProps` again, as they may have changed since. Its own keys
|
|
728
|
+
// stay its own, and the join table extras of the place of the model belong to
|
|
729
|
+
// the join row there.
|
|
730
|
+
function resolveReference(node, filledProps = []) {
|
|
731
|
+
const { referencedNode, relation } = node;
|
|
732
|
+
|
|
733
|
+
if (referencedNode) {
|
|
734
|
+
const ownKeys = relation?.keySide === 'related' ? relation.relatedProp.props : [];
|
|
735
|
+
const modelExtras =
|
|
736
|
+
referencedNode.relation?.keySide === 'join'
|
|
737
|
+
? referencedNode.relation.joinTableExtras.map((extra) => extra.aliasProp)
|
|
738
|
+
: [];
|
|
739
|
+
|
|
740
|
+
for (const prop of difference(Object.keys(referencedNode.obj), modelExtras)) {
|
|
741
|
+
if (
|
|
742
|
+
!Object.hasOwn(node.obj, prop) ||
|
|
743
|
+
(filledProps.includes(prop) && !ownKeys.includes(prop))
|
|
744
|
+
) {
|
|
745
|
+
node.obj[prop] = referencedNode.obj[prop];
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Replace a `#ref{}` in a prop of `node` with the value it refers to:
|
|
752
|
+
function resolvePropertyReference(node, { path, match, refPath, isWholeValue, referencedNode }) {
|
|
753
|
+
const value = get(referencedNode.obj, refPath);
|
|
754
|
+
set(node.obj, path, isWholeValue ? value : get(node.obj, path).replace(match, value));
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Fetch the keys that the relations of the inserted nodes need, but that the
|
|
758
|
+
// database generated and the insert didn't return, like a uuid that isn't the
|
|
759
|
+
// id (Vincit#853).
|
|
760
|
+
async function fetchGeneratedKeys(builder, modelClass, nodes) {
|
|
761
|
+
const keyProps = (node) => [
|
|
762
|
+
...(node.relation && node.relation.keySide !== 'related' ? [node.relation.relatedProp] : []),
|
|
763
|
+
...node.activeChildren
|
|
764
|
+
.filter((child) => child.relation.keySide !== 'owner')
|
|
765
|
+
.map((child) => child.relation.ownerProp),
|
|
766
|
+
];
|
|
767
|
+
const missingNodes = nodes.filter((node) =>
|
|
768
|
+
keyProps(node).some((prop) => prop.getProps(node.obj).some((value) => value === undefined)),
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
if (missingNodes.length > 0 && missingNodes.every((node) => node.obj.$hasId())) {
|
|
772
|
+
// SQL Server returns columns selected twice as arrays, so the ids are
|
|
773
|
+
// part of the set:
|
|
774
|
+
const columns = new Set([
|
|
775
|
+
...modelClass.getIdColumnArray(),
|
|
776
|
+
...missingNodes.flatMap((node) => keyProps(node)).flatMap((prop) => prop.cols),
|
|
777
|
+
]);
|
|
778
|
+
const rows = await modelClass
|
|
779
|
+
.query()
|
|
780
|
+
.childQueryOf(builder, { fork: true, isInternalQuery: true })
|
|
781
|
+
.findByIds(missingNodes.map((node) => node.obj.$id()))
|
|
782
|
+
.select([...columns]);
|
|
783
|
+
const rowsByKey = new Map(rows.map((row) => [row.$idKey(), row]));
|
|
784
|
+
|
|
785
|
+
for (const node of missingNodes) {
|
|
786
|
+
const row = rowsByKey.get(node.obj.$idKey());
|
|
787
|
+
|
|
788
|
+
for (const prop of keyProps(node)) {
|
|
789
|
+
prop.forEach((i) => {
|
|
790
|
+
if (prop.getProp(node.obj, i) === undefined) {
|
|
791
|
+
prop.setProp(node.obj, i, prop.getProp(row, i));
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Add the changed keys of `cells` at the top level of json columns to `node` as
|
|
800
|
+
// field expressions where their column holds nothing but these keys, and return
|
|
801
|
+
// them by column. PostgreSQL and MySQL can't write nested paths whose parents
|
|
802
|
+
// are missing, so their columns are written whole.
|
|
803
|
+
function setKeyPaths(node, cells) {
|
|
804
|
+
const keysByColumn = new Map();
|
|
805
|
+
const pathCells = cells.filter(({ prop, index }) => prop.isJsonKey(index));
|
|
806
|
+
|
|
807
|
+
for (const [column, columnCells] of groupBy(pathCells, ({ prop, index }) => prop.props[index])) {
|
|
808
|
+
const keys = {};
|
|
809
|
+
|
|
810
|
+
for (const { prop, index } of columnCells) {
|
|
811
|
+
prop.setProp(keys, index, prop.getProp(node.obj, index));
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (jsonEquals(node.obj[column], keys[column])) {
|
|
815
|
+
const patch = {};
|
|
816
|
+
|
|
817
|
+
for (const { prop, index } of columnCells) {
|
|
818
|
+
const value = prop.getProp(node.obj, index);
|
|
819
|
+
|
|
820
|
+
if (!node.current || !nonStrictEquals(prop.getProp(node.current.obj, index), value)) {
|
|
821
|
+
prop.patch(patch, index, value);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
Object.assign(node.obj, patch);
|
|
826
|
+
keysByColumn.set(column, Object.keys(patch));
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return keysByColumn;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function hasOwnerKeyArray(relation) {
|
|
834
|
+
return relation.keySide === 'owner' && !relation.isOneToOne();
|
|
835
|
+
}
|