objection 3.9.0 → 4.0.0-rc.1
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/dbErrors/LICENSE +21 -0
- package/lib/dbErrors/errorCodes/mssql.js +19 -0
- package/lib/dbErrors/errorCodes/mysql.js +805 -0
- package/lib/dbErrors/errorCodes/postgres.js +241 -0
- package/lib/dbErrors/errors/CheckViolationError.js +10 -0
- package/lib/dbErrors/errors/ConstraintViolationError.js +3 -0
- package/lib/dbErrors/errors/DBError.js +9 -0
- package/lib/dbErrors/errors/DataError.js +3 -0
- package/lib/dbErrors/errors/ForeignKeyViolationError.js +11 -0
- package/lib/dbErrors/errors/NotNullViolationError.js +12 -0
- package/lib/dbErrors/errors/UniqueViolationError.js +12 -0
- package/lib/dbErrors/index.js +59 -0
- package/lib/dbErrors/parsers/mssql/DBError/CheckViolationError/parser.js +28 -0
- package/lib/dbErrors/parsers/mssql/DBError/DataError/parser.js +25 -0
- package/lib/dbErrors/parsers/mssql/DBError/ForeignKeyViolationError/parser.js +39 -0
- package/lib/dbErrors/parsers/mssql/DBError/NotNullViolationError/parser.js +30 -0
- package/lib/dbErrors/parsers/mssql/DBError/UniqueViolationError/parser.js +53 -0
- package/lib/dbErrors/parsers/mssql/DBError/parser.js +51 -0
- package/lib/dbErrors/parsers/mssql/DBError/util.js +16 -0
- package/lib/dbErrors/parsers/mysql/DBError/ConstraintViolationError/ForeignKeyViolationError/parser.js +43 -0
- package/lib/dbErrors/parsers/mysql/DBError/ConstraintViolationError/NotNullViolationError/parser.js +29 -0
- package/lib/dbErrors/parsers/mysql/DBError/ConstraintViolationError/UniqueViolationError/parser.js +27 -0
- package/lib/dbErrors/parsers/mysql/DBError/ConstraintViolationError/parser.js +23 -0
- package/lib/dbErrors/parsers/mysql/DBError/DataError/parser.js +24 -0
- package/lib/dbErrors/parsers/mysql/DBError/parser.js +28 -0
- package/lib/dbErrors/parsers/postgres/DBError/ConstraintViolationError/CheckViolationError/parser.js +18 -0
- package/lib/dbErrors/parsers/postgres/DBError/ConstraintViolationError/ForeignKeyViolationError/parser.js +18 -0
- package/lib/dbErrors/parsers/postgres/DBError/ConstraintViolationError/NotNullViolationError/parser.js +18 -0
- package/lib/dbErrors/parsers/postgres/DBError/ConstraintViolationError/UniqueViolationError/parser.js +40 -0
- package/lib/dbErrors/parsers/postgres/DBError/ConstraintViolationError/parser.js +25 -0
- package/lib/dbErrors/parsers/postgres/DBError/DataError/parser.js +16 -0
- package/lib/dbErrors/parsers/postgres/DBError/parser.js +27 -0
- package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/CheckViolationError/parser.js +20 -0
- package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/ForeignKeyViolationError/parser.js +20 -0
- package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/NotNullViolationError/parser.js +27 -0
- package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/UniqueViolationError/parser.js +28 -0
- package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/parser.js +24 -0
- package/lib/dbErrors/parsers/sqlite/DBError/parser.js +23 -0
- package/lib/dbErrors/utils/sqlState.js +3 -0
- package/lib/initialize.js +1 -7
- package/lib/model/AjvValidator.js +51 -117
- package/lib/model/Model.js +84 -59
- package/lib/model/ModifierNotFoundError.js +1 -7
- package/lib/model/NotFoundError.js +1 -7
- package/lib/model/RelationDoesNotExistError.js +1 -7
- package/lib/model/ValidationError.js +3 -10
- package/lib/model/Validator.js +1 -7
- package/lib/model/getModel.js +7 -6
- package/lib/model/inheritModel.js +1 -7
- package/lib/model/modelBindKnex.js +4 -10
- package/lib/model/modelClone.js +5 -11
- package/lib/model/modelColPropMap.js +3 -10
- package/lib/model/modelId.js +3 -10
- package/lib/model/modelJsonAttributes.js +35 -16
- package/lib/model/modelParseRelations.js +2 -8
- package/lib/model/modelQueryProps.js +43 -27
- package/lib/model/modelSet.js +89 -41
- package/lib/model/modelTableMetadata.js +4 -11
- package/lib/model/modelToJson.js +8 -23
- package/lib/model/modelUtils.js +14 -13
- package/lib/model/modelValidate.js +3 -9
- package/lib/model/modelValues.js +5 -14
- package/lib/model/modelVisitor.js +1 -7
- package/lib/objection.js +33 -71
- package/lib/queryBuilder/FunctionBuilder.js +4 -11
- package/lib/queryBuilder/InternalOptions.js +4 -9
- package/lib/queryBuilder/JoinBuilder.js +5 -8
- package/lib/queryBuilder/QueryBuilder.js +126 -95
- package/lib/queryBuilder/QueryBuilderBase.js +41 -123
- package/lib/queryBuilder/QueryBuilderContext.js +2 -8
- package/lib/queryBuilder/QueryBuilderContextBase.js +2 -8
- package/lib/queryBuilder/QueryBuilderOperationSupport.js +7 -21
- package/lib/queryBuilder/QueryBuilderUserContext.js +1 -7
- package/lib/queryBuilder/RawBuilder.js +5 -13
- package/lib/queryBuilder/ReferenceBuilder.js +20 -48
- package/lib/queryBuilder/RelationExpression.js +18 -16
- package/lib/queryBuilder/StaticHookArguments.js +74 -16
- package/lib/queryBuilder/ValueBuilder.js +11 -13
- package/lib/queryBuilder/getJoinBuilder.js +10 -0
- package/lib/queryBuilder/graph/GraphLoader.js +304 -0
- package/lib/queryBuilder/graph/GraphNode.js +235 -0
- package/lib/queryBuilder/graph/GraphOptions.js +55 -248
- package/lib/queryBuilder/graph/GraphPlan.js +395 -0
- package/lib/queryBuilder/graph/GraphUpsert.js +466 -410
- package/lib/queryBuilder/graph/GraphWriter.js +1263 -0
- package/lib/queryBuilder/join/JoinResultColumn.js +1 -7
- package/lib/queryBuilder/join/JoinResultParser.js +6 -11
- package/lib/queryBuilder/join/RelationJoiner.js +66 -84
- package/lib/queryBuilder/join/TableNode.js +2 -8
- package/lib/queryBuilder/join/TableTree.js +3 -9
- package/lib/queryBuilder/join/utils.js +5 -15
- package/lib/queryBuilder/jsonDialects/JsonDialect.js +51 -0
- package/lib/queryBuilder/jsonDialects/MySqlJsonDialect.js +81 -0
- package/lib/queryBuilder/jsonDialects/PostgresJsonDialect.js +45 -0
- package/lib/queryBuilder/jsonDialects/SqliteJsonDialect.js +67 -0
- package/lib/queryBuilder/jsonDialects/index.js +44 -0
- package/lib/queryBuilder/operations/DelegateOperation.js +2 -8
- package/lib/queryBuilder/operations/DeleteOperation.js +13 -35
- package/lib/queryBuilder/operations/FindByIdOperation.js +7 -12
- package/lib/queryBuilder/operations/FindByIdsOperation.js +4 -8
- package/lib/queryBuilder/operations/FindOperation.js +14 -34
- package/lib/queryBuilder/operations/FirstOperation.js +6 -10
- package/lib/queryBuilder/operations/FromOperation.js +3 -9
- package/lib/queryBuilder/operations/InsertAndFetchOperation.js +13 -12
- package/lib/queryBuilder/operations/InsertGraphAndFetchOperation.js +4 -10
- package/lib/queryBuilder/operations/InsertGraphOperation.js +7 -18
- package/lib/queryBuilder/operations/InsertOperation.js +118 -43
- package/lib/queryBuilder/operations/InstanceDeleteOperation.js +5 -12
- package/lib/queryBuilder/operations/InstanceFindOperation.js +4 -11
- package/lib/queryBuilder/operations/InstanceInsertOperation.js +2 -8
- package/lib/queryBuilder/operations/InstanceUpdateOperation.js +7 -14
- package/lib/queryBuilder/operations/JoinRelatedOperation.js +5 -11
- package/lib/queryBuilder/operations/KnexOperation.js +2 -8
- package/lib/queryBuilder/operations/LimitPerParentOperation.js +7 -69
- package/lib/queryBuilder/operations/MergeOperation.js +5 -11
- package/lib/queryBuilder/operations/NoneOperation.js +2 -8
- package/lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js +20 -49
- package/lib/queryBuilder/operations/OnBuildKnexOperation.js +2 -8
- package/lib/queryBuilder/operations/OnBuildOperation.js +2 -8
- package/lib/queryBuilder/operations/OnErrorOperation.js +2 -8
- package/lib/queryBuilder/operations/QueryBuilderOperation.js +1 -7
- package/lib/queryBuilder/operations/RangeOperation.js +11 -26
- package/lib/queryBuilder/operations/RelateOperation.js +2 -8
- package/lib/queryBuilder/operations/ReturningOperation.js +3 -9
- package/lib/queryBuilder/operations/RunAfterOperation.js +2 -12
- package/lib/queryBuilder/operations/RunBeforeOperation.js +2 -8
- package/lib/queryBuilder/operations/UnrelateOperation.js +2 -8
- package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +14 -17
- package/lib/queryBuilder/operations/UpdateOperation.js +39 -125
- package/lib/queryBuilder/operations/UpsertGraphAndFetchOperation.js +8 -11
- package/lib/queryBuilder/operations/UpsertGraphOperation.js +6 -12
- package/lib/queryBuilder/operations/WhereCompositeOperation.js +3 -9
- package/lib/queryBuilder/operations/WhereGroupOperation.js +2 -8
- package/lib/queryBuilder/operations/eager/EagerOperation.js +3 -9
- package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +72 -10
- package/lib/queryBuilder/operations/eager/NaiveEagerOperation.js +2 -8
- package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +23 -44
- package/lib/queryBuilder/operations/fieldExpressions.js +122 -0
- package/lib/queryBuilder/operations/jsonApi/WhereJsonHasOperation.js +20 -0
- package/lib/queryBuilder/operations/jsonApi/WhereJsonOperation.js +44 -0
- package/lib/queryBuilder/operations/jsonApi/WhereJsonTypeOperation.js +14 -0
- package/lib/queryBuilder/operations/select/SelectOperation.js +11 -21
- package/lib/queryBuilder/operations/select/Selection.js +19 -17
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +4 -10
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeOperation.js +4 -10
- package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +4 -10
- package/lib/queryBuilder/operations/windowOrder.js +56 -0
- package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +3 -7
- package/lib/queryBuilder/parsers/relationExpressionParser.js +42 -43
- package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +39 -36
- package/lib/queryBuilder/transformations/CompositeQueryTransformation.js +2 -8
- package/lib/queryBuilder/transformations/QueryTransformation.js +1 -7
- package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +3 -9
- package/lib/queryBuilder/transformations/index.js +3 -11
- package/lib/queryBuilder/whereGroups.js +9 -24
- package/lib/queryBuilder/withoutOrder.js +4 -11
- package/lib/relations/Relation.js +47 -28
- package/lib/relations/RelationDeleteOperation.js +4 -14
- package/lib/relations/RelationFindOperation.js +119 -90
- package/lib/relations/RelationInsertOperation.js +2 -8
- package/lib/relations/RelationOwner.js +5 -11
- package/lib/relations/RelationProperty.js +4 -10
- package/lib/relations/RelationUpdateOperation.js +4 -14
- package/lib/relations/RelationWriteResultOperation.js +15 -0
- package/lib/relations/belongsToMany/BelongsToManyDeleteOperation.js +23 -0
- package/lib/relations/belongsToMany/BelongsToManyFindOperation.js +35 -0
- package/lib/relations/belongsToMany/BelongsToManyInsertOperation.js +28 -0
- package/lib/relations/belongsToMany/BelongsToManyRelateOperation.js +16 -0
- package/lib/relations/belongsToMany/BelongsToManyRelation.js +276 -0
- package/lib/relations/belongsToMany/BelongsToManyUnrelateOperation.js +11 -0
- package/lib/relations/belongsToOne/BelongsToOneDeleteOperation.js +2 -8
- package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +6 -11
- package/lib/relations/belongsToOne/BelongsToOneRelateOperation.js +3 -9
- package/lib/relations/belongsToOne/BelongsToOneRelation.js +8 -19
- package/lib/relations/belongsToOne/BelongsToOneUnrelateOperation.js +53 -27
- package/lib/relations/hasMany/HasManyInsertOperation.js +8 -9
- package/lib/relations/hasMany/HasManyRelateOperation.js +5 -11
- package/lib/relations/hasMany/HasManyRelation.js +9 -11
- package/lib/relations/hasMany/HasManyUnrelateOperation.js +3 -9
- package/lib/relations/hasOne/HasOneRelation.js +2 -8
- package/lib/relations/hasOneThrough/HasOneThroughRelation.js +2 -8
- package/lib/relations/manyToMany/ManyToManyModifyMixin.js +10 -15
- package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +2 -8
- package/lib/relations/manyToMany/ManyToManyRelation.js +82 -50
- package/lib/relations/manyToMany/ManyToManySqliteModifyMixin.js +2 -8
- package/lib/relations/manyToMany/delete/ManyToManyDeleteOperation.js +5 -9
- package/lib/relations/manyToMany/delete/ManyToManyDeleteOperationBase.js +2 -8
- package/lib/relations/manyToMany/delete/ManyToManyDeleteSqliteOperation.js +3 -9
- package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +20 -59
- package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +6 -11
- package/lib/relations/manyToMany/relate/ManyToManyRelateOperation.js +4 -10
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +3 -9
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperation.js +5 -9
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +3 -9
- package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateSqliteOperation.js +3 -9
- package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +3 -9
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperation.js +5 -9
- package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +79 -64
- package/lib/relations/manyToMany/update/ManyToManyUpdateSqliteOperation.js +3 -9
- package/lib/transaction.js +3 -9
- package/lib/utils/assert.js +3 -11
- package/lib/utils/buildUtils.js +2 -8
- package/lib/utils/classUtils.js +1 -7
- package/lib/utils/createModifier.js +2 -8
- package/lib/utils/identifierMapping.js +23 -27
- package/lib/utils/internalPropUtils.js +1 -7
- package/lib/utils/knexUtils.js +84 -40
- package/lib/utils/mixin.js +3 -10
- package/lib/utils/normalizeIds.js +2 -8
- package/lib/utils/objectUtils.js +41 -60
- package/lib/utils/parseFieldExpression.js +29 -14
- package/lib/utils/promiseUtils/after.js +2 -8
- package/lib/utils/promiseUtils/afterReturn.js +2 -8
- package/lib/utils/promiseUtils/index.js +7 -16
- package/lib/utils/promiseUtils/isPromise.js +2 -8
- package/lib/utils/promiseUtils/map.js +25 -14
- package/lib/utils/promiseUtils/mapAfterAllReturn.js +2 -8
- package/lib/utils/promiseUtils/try.js +2 -8
- package/lib/utils/resolveModel.js +10 -17
- package/lib/utils/tmpColumnUtils.js +2 -9
- package/lib/utils/warnings.js +2 -14
- package/package.json +25 -17
- package/typings/objection/index.d.ts +3217 -2179
- package/lib/.eslintrc.json +0 -8
- package/lib/model/graph/ModelGraph.js +0 -80
- package/lib/model/graph/ModelGraphBuilder.js +0 -241
- package/lib/model/graph/ModelGraphEdge.js +0 -52
- package/lib/model/graph/ModelGraphNode.js +0 -253
- package/lib/queryBuilder/graph/GraphAction.js +0 -69
- package/lib/queryBuilder/graph/GraphData.js +0 -53
- package/lib/queryBuilder/graph/GraphFetcher.js +0 -199
- package/lib/queryBuilder/graph/GraphNodeDbExistence.js +0 -184
- package/lib/queryBuilder/graph/GraphOperation.js +0 -27
- package/lib/queryBuilder/graph/delete/GraphDelete.js +0 -73
- package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +0 -88
- package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +0 -57
- package/lib/queryBuilder/graph/insert/GraphInsert.js +0 -257
- package/lib/queryBuilder/graph/insert/GraphInsertAction.js +0 -238
- package/lib/queryBuilder/graph/insert/JoinRowGraphInsertAction.js +0 -92
- package/lib/queryBuilder/graph/patch/GraphPatch.js +0 -20
- package/lib/queryBuilder/graph/patch/GraphPatchAction.js +0 -277
- package/lib/queryBuilder/graph/recursiveUpsert/GraphRecursiveUpsert.js +0 -31
- package/lib/queryBuilder/graph/recursiveUpsert/GraphRecursiveUpsertAction.js +0 -126
- package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +0 -30
- package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +0 -37
- package/lib/queryBuilder/operations/jsonApi/WhereJsonPostgresOperation.js +0 -62
- package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +0 -192
- package/lib/utils/wrapDbError.js +0 -50
|
@@ -0,0 +1,1263 @@
|
|
|
1
|
+
import { getModel } from '../../model/getModel.js';
|
|
2
|
+
import { restoreOmitFromDatabaseJson } from '../../model/modelUtils.js';
|
|
3
|
+
import { UpdateOperation } from '../operations/UpdateOperation.js';
|
|
4
|
+
import { getMaxRows, hasBatchInsert } from '../../utils/knexUtils.js';
|
|
5
|
+
import { isInternalProp } from '../../utils/internalPropUtils.js';
|
|
6
|
+
import {
|
|
7
|
+
chunk,
|
|
8
|
+
difference,
|
|
9
|
+
get,
|
|
10
|
+
groupBy,
|
|
11
|
+
isObject,
|
|
12
|
+
jsonEquals,
|
|
13
|
+
nonStrictEquals,
|
|
14
|
+
set,
|
|
15
|
+
} from '../../utils/objectUtils.js';
|
|
16
|
+
import { map as promiseMap } from '../../utils/promiseUtils/index.js';
|
|
17
|
+
|
|
18
|
+
const MAX_INSERT_BATCH_SIZE = 100;
|
|
19
|
+
const MAX_CONCURRENCY = 100;
|
|
20
|
+
|
|
21
|
+
// Write a planned graph in steps, in an order that keeps keys and constraints
|
|
22
|
+
// intact (#214):
|
|
23
|
+
// 1. Move the rows that the graph relates to existing owners of HasMany and
|
|
24
|
+
// HasOne relations, as they may still sit below rows that the next step
|
|
25
|
+
// deletes. Rows that wait for inserts are related in step 3 or 4.
|
|
26
|
+
// 2. Remove rows that reference their owners, before the inserts that may need
|
|
27
|
+
// their unique keys.
|
|
28
|
+
// 3. Insert the new rows in the order of their keys and references, then the
|
|
29
|
+
// keys deferred to break cycles and the join rows. Rows that move out of
|
|
30
|
+
// the slots of HasOneRelations that new rows go into are related in the
|
|
31
|
+
// same order, as the foreign key may be unique.
|
|
32
|
+
// 4. Patch the changed rows, the other relates, and the keys of owners whose
|
|
33
|
+
// BelongsToOne rows were inserted, related or removed.
|
|
34
|
+
// 5. Remove the BelongsToOne rows that the owners no longer reference.
|
|
35
|
+
// Each step writes a row that goes into the slot of a HasOneRelation after the
|
|
36
|
+
// row that moves out of it, unless that closes a cycle.
|
|
37
|
+
// `insertGraph()` only inserts.
|
|
38
|
+
export class GraphWriter {
|
|
39
|
+
constructor(plan) {
|
|
40
|
+
this.plan = plan;
|
|
41
|
+
// The nodes that each node waits for, through its keys, its references and
|
|
42
|
+
// the rows that move out of the slots that it goes into:
|
|
43
|
+
this.dependencies = createDependencies(plan);
|
|
44
|
+
// Dependencies that aren't waited for, to break cycles. Deferred keys are
|
|
45
|
+
// written after the inserts:
|
|
46
|
+
this.deferredDependencies = new Set();
|
|
47
|
+
// Relates whose rows are moved to their new owners before the removals:
|
|
48
|
+
this.movedNodes = new Set(
|
|
49
|
+
plan.graphOptions.isInsertOnly()
|
|
50
|
+
? []
|
|
51
|
+
: plan.activeNodes.filter((node) => this._isMovedFirst(node)),
|
|
52
|
+
);
|
|
53
|
+
// Relates that move the rows out of the slots of HasOneRelations that new
|
|
54
|
+
// rows go into, and that aren't moved first:
|
|
55
|
+
this.slotMovedNodes = this._getSlotMovedNodes();
|
|
56
|
+
this.steps = this._planSteps();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async write(builder) {
|
|
60
|
+
// Fill in the keys that existing models leave out, before new children read them:
|
|
61
|
+
for (const node of this.plan.activeNodes) {
|
|
62
|
+
copyKeysFromExisting(node, this.plan.actions);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const step of this.steps) {
|
|
66
|
+
await this._writeStep(builder, step);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// `#ref` models return the props of the models they refer to:
|
|
70
|
+
for (const node of this.plan.activeNodes) {
|
|
71
|
+
resolveReference(node);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_planSteps() {
|
|
76
|
+
const { activeNodes, activeCurrentNodes, actions, graphOptions } = this.plan;
|
|
77
|
+
const insertSteps = [
|
|
78
|
+
...this._planLevels(activeNodes, { resolvesReferences: true }).map((level) => ({
|
|
79
|
+
kind: 'insert',
|
|
80
|
+
...level,
|
|
81
|
+
})),
|
|
82
|
+
{
|
|
83
|
+
kind: 'deferredKeys',
|
|
84
|
+
dependencies: [...this.deferredDependencies].filter((dependency) => dependency.relation),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
kind: 'joinRows',
|
|
88
|
+
nodes: activeNodes.filter(
|
|
89
|
+
(node) =>
|
|
90
|
+
node.relation?.keySide === 'join' &&
|
|
91
|
+
!node.current &&
|
|
92
|
+
['insert', 'relate'].includes(actions.get(node)) &&
|
|
93
|
+
// Models that `noInsert` skips have no key for join rows (#369):
|
|
94
|
+
actions.get(node.parent) !== 'ignore',
|
|
95
|
+
),
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
if (graphOptions.isInsertOnly()) {
|
|
100
|
+
return insertSteps;
|
|
101
|
+
} else {
|
|
102
|
+
const removedNodes = activeCurrentNodes.filter((node) => actions.get(node) !== 'keep');
|
|
103
|
+
const isReferencedByOwner = (currentNode) => currentNode.relation.keySide === 'owner';
|
|
104
|
+
|
|
105
|
+
return [
|
|
106
|
+
...this._planLevels([...this.movedNodes]).map(({ nodes }) => ({ kind: 'move', nodes })),
|
|
107
|
+
{
|
|
108
|
+
kind: 'remove',
|
|
109
|
+
nodes: removedNodes.filter((node) => !isReferencedByOwner(node)),
|
|
110
|
+
areReferenced: true,
|
|
111
|
+
},
|
|
112
|
+
...insertSteps,
|
|
113
|
+
...this._planLevels(
|
|
114
|
+
activeNodes.filter((node) => {
|
|
115
|
+
const action = actions.get(node);
|
|
116
|
+
return (
|
|
117
|
+
(action === 'patch' ||
|
|
118
|
+
(action === 'relate' &&
|
|
119
|
+
!this.movedNodes.has(node) &&
|
|
120
|
+
!this.slotMovedNodes.has(node))) &&
|
|
121
|
+
// The row of a `#ref` model is written once, by its model:
|
|
122
|
+
!this._isWrittenByModel(node)
|
|
123
|
+
);
|
|
124
|
+
}),
|
|
125
|
+
).map(({ nodes }) => ({ kind: 'patch', nodes })),
|
|
126
|
+
{
|
|
127
|
+
kind: 'remove',
|
|
128
|
+
// Owners unrelate their BelongsToOne rows by patching their keys:
|
|
129
|
+
nodes: removedNodes.filter(
|
|
130
|
+
(node) => isReferencedByOwner(node) && actions.get(node) === 'delete',
|
|
131
|
+
),
|
|
132
|
+
areReferenced: false,
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_writeStep(builder, step) {
|
|
139
|
+
switch (step.kind) {
|
|
140
|
+
case 'move':
|
|
141
|
+
return this._move(builder, step.nodes);
|
|
142
|
+
case 'remove':
|
|
143
|
+
return this._remove(builder, step.nodes, step.areReferenced);
|
|
144
|
+
case 'insert':
|
|
145
|
+
return this._insert(builder, step.nodes, step.references);
|
|
146
|
+
case 'deferredKeys':
|
|
147
|
+
return this._writeDeferredKeys(builder, step.dependencies);
|
|
148
|
+
case 'joinRows':
|
|
149
|
+
return this._insertJoinRows(builder, step.nodes);
|
|
150
|
+
case 'patch':
|
|
151
|
+
return this._patch(builder, step.nodes);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Group the nodes into levels that can be written once the levels before
|
|
156
|
+
// them are: Nodes wait for the nodes whose keys they store, the nodes that
|
|
157
|
+
// they reference, and the rows that move out of the slots they go into. For
|
|
158
|
+
// the inserts, all nodes take part, so that the keys of the nodes that aren't
|
|
159
|
+
// inserted are resolved in order too, and `resolvesReferences` says so.
|
|
160
|
+
_planLevels(nodes, { resolvesReferences = false } = {}) {
|
|
161
|
+
const nodeSet = new Set(nodes);
|
|
162
|
+
const handledNodes = new Set();
|
|
163
|
+
const levels = [];
|
|
164
|
+
|
|
165
|
+
// The keys of models with rows are known, also of related models that were
|
|
166
|
+
// fetched, except for the props that the graph fills in:
|
|
167
|
+
const hasRow = (node) => !!(node.referencedNode || node).existing;
|
|
168
|
+
// Nodes outside this pass were written by an earlier one, or are never
|
|
169
|
+
// written, see `checkReferences()`:
|
|
170
|
+
const getPendingDependencies = (node) =>
|
|
171
|
+
(this.dependencies.get(node) || []).filter(
|
|
172
|
+
(dependency) =>
|
|
173
|
+
nodeSet.has(dependency.node) &&
|
|
174
|
+
!this.deferredDependencies.has(dependency) &&
|
|
175
|
+
!handledNodes.has(dependency.node) &&
|
|
176
|
+
(dependency.isSlotMove || !hasRow(dependency.node) || this._readsFilledProps(dependency)),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Return true if `node` waits for `blockingNode`, through other nodes too.
|
|
180
|
+
const isBlockedBy = (node, blockingNode, visitedNodes = new Set()) => {
|
|
181
|
+
if (node === blockingNode) {
|
|
182
|
+
return true;
|
|
183
|
+
} else if (visitedNodes.has(node)) {
|
|
184
|
+
return false;
|
|
185
|
+
} else {
|
|
186
|
+
visitedNodes.add(node);
|
|
187
|
+
return getPendingDependencies(node).some((dependency) =>
|
|
188
|
+
isBlockedBy(dependency.node, blockingNode, visitedNodes),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Break a cycle by deferring one BelongsToOne key that points to a `#ref`
|
|
194
|
+
// model: Its owner is inserted without the key, which is patched in once
|
|
195
|
+
// the referenced model is inserted. Without `#ref`, the graph is a tree,
|
|
196
|
+
// and only waits for the rows that move out of slots can close a cycle,
|
|
197
|
+
// like rows that swap slots. One of these isn't waited for then.
|
|
198
|
+
const breakCycle = () => {
|
|
199
|
+
for (const canDefer of [
|
|
200
|
+
(dependency, node) => this._canDefer(dependency, node),
|
|
201
|
+
(dependency) => dependency.isSlotMove,
|
|
202
|
+
]) {
|
|
203
|
+
for (const node of nodes) {
|
|
204
|
+
if (!handledNodes.has(node)) {
|
|
205
|
+
for (const dependency of getPendingDependencies(node)) {
|
|
206
|
+
if (canDefer(dependency, node) && isBlockedBy(dependency.node, node)) {
|
|
207
|
+
this.deferredDependencies.add(dependency);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return false;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const isReady = (node) => !handledNodes.has(node) && getPendingDependencies(node).length === 0;
|
|
219
|
+
const isReadyReference = (node) => resolvesReferences && node.isReference && isReady(node);
|
|
220
|
+
|
|
221
|
+
while (handledNodes.size < nodes.length) {
|
|
222
|
+
const levelNodes = nodes.filter(isReady);
|
|
223
|
+
|
|
224
|
+
if (levelNodes.length > 0) {
|
|
225
|
+
const level = { nodes: levelNodes, references: [] };
|
|
226
|
+
levels.push(level);
|
|
227
|
+
|
|
228
|
+
for (const node of levelNodes) {
|
|
229
|
+
handledNodes.add(node);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// `#ref` models aren't inserted. They're resolved right after the
|
|
233
|
+
// inserts of the level that completes their dependencies, so that the
|
|
234
|
+
// nodes that wait for them can be inserted in the next level (#311).
|
|
235
|
+
// The other steps write them in levels of their own.
|
|
236
|
+
for (
|
|
237
|
+
let references = nodes.filter(isReadyReference);
|
|
238
|
+
references.length > 0;
|
|
239
|
+
references = nodes.filter(isReadyReference)
|
|
240
|
+
) {
|
|
241
|
+
for (const node of references) {
|
|
242
|
+
handledNodes.add(node);
|
|
243
|
+
level.references.push(node);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} else if (!breakCycle()) {
|
|
247
|
+
throw new Error('the object graph contains cyclic references');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return levels;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_canDefer(dependency, node) {
|
|
255
|
+
const { actions } = this.plan;
|
|
256
|
+
const { relation } = dependency;
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
relation &&
|
|
260
|
+
hasOwnerKey(relation) &&
|
|
261
|
+
dependency.node.isReference &&
|
|
262
|
+
// The owner is patched by its id, which models without one don't have (#2305).
|
|
263
|
+
node.modelClass.getIdColumnArray().length > 0 &&
|
|
264
|
+
// Both ends need to be inserted for the key to be patched in later.
|
|
265
|
+
actions.get(node) === 'insert' &&
|
|
266
|
+
actions.get(dependency.node.referencedNode) === 'insert' &&
|
|
267
|
+
// A `#ref{}` that reads the key might be resolved before it's patched in.
|
|
268
|
+
!node.propertyReferencingNodes.some((referencingNode) =>
|
|
269
|
+
referencingNode.propertyReferences.some(
|
|
270
|
+
(reference) =>
|
|
271
|
+
reference.referencedNode === node &&
|
|
272
|
+
relation.ownerProp.props.includes(reference.refPath[0]),
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Return true if the HasMany or HasOne relate of `node` moves its row to an
|
|
279
|
+
// existing owner before the removals, see the steps above. `#ref` models may
|
|
280
|
+
// get their ids from the inserts, rows that wait for the inserts are written
|
|
281
|
+
// once they're known, and a relate into the slot of a HasOne row waits for
|
|
282
|
+
// the removal of that row, or its move, as its key may be unique.
|
|
283
|
+
_isMovedFirst(node, visitedNodes = new Set()) {
|
|
284
|
+
const { actions } = this.plan;
|
|
285
|
+
visitedNodes.add(node);
|
|
286
|
+
|
|
287
|
+
if (
|
|
288
|
+
actions.get(node) === 'relate' &&
|
|
289
|
+
!node.isReference &&
|
|
290
|
+
node.relation.keySide === 'related' &&
|
|
291
|
+
node.parent.existing &&
|
|
292
|
+
!this._waitsForInserts(node)
|
|
293
|
+
) {
|
|
294
|
+
const slotNode = getSlotNode(node);
|
|
295
|
+
const moverNode = getSlotMover(this.plan, node);
|
|
296
|
+
return (
|
|
297
|
+
!(slotNode && actions.get(slotNode) !== 'keep') &&
|
|
298
|
+
// Swapped rows are related together:
|
|
299
|
+
(!moverNode || visitedNodes.has(moverNode) || this._isMovedFirst(moverNode, visitedNodes))
|
|
300
|
+
);
|
|
301
|
+
} else {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Find the relates that move the rows out of the slots of HasOneRelations
|
|
307
|
+
// that new rows go into, and that aren't moved first. They're related in the
|
|
308
|
+
// inserts, along with the relates that move the rows out of their own slots,
|
|
309
|
+
// as the foreign key may be unique.
|
|
310
|
+
_getSlotMovedNodes() {
|
|
311
|
+
const nodes = new Set();
|
|
312
|
+
const addMoverNodes = (node) => {
|
|
313
|
+
for (const { node: moverNode, isSlotMove } of this.dependencies.get(node) || []) {
|
|
314
|
+
if (isSlotMove && !this.movedNodes.has(moverNode) && !nodes.has(moverNode)) {
|
|
315
|
+
nodes.add(moverNode);
|
|
316
|
+
addMoverNodes(moverNode);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
for (const node of this.plan.activeNodes) {
|
|
322
|
+
if (this.plan.actions.get(node) === 'insert') {
|
|
323
|
+
addMoverNodes(node);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return nodes;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Return the props of `node` that the graph fills in from other models,
|
|
331
|
+
// through its relations and its references.
|
|
332
|
+
_getFilledProps(node) {
|
|
333
|
+
return (this.dependencies.get(node) || []).flatMap((dependency) =>
|
|
334
|
+
dependency.reference
|
|
335
|
+
? [dependency.reference.path[0]]
|
|
336
|
+
: dependency.relation
|
|
337
|
+
? getKeyProps(dependency, node)
|
|
338
|
+
: [],
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Return true if `dependency` reads props that the graph fills into the
|
|
343
|
+
// model it depends on: the keys of a relation, or the value of a `#ref{}`.
|
|
344
|
+
// The other props of existing models are known right away, but these only
|
|
345
|
+
// once they're filled in (#356). A `#ref` model is the model it refers to,
|
|
346
|
+
// so its relations wait for the props filled into that model.
|
|
347
|
+
_readsFilledProps(dependency) {
|
|
348
|
+
const { node, relation, reference } = dependency;
|
|
349
|
+
const filledProps = this._getFilledProps(node.referencedNode || node);
|
|
350
|
+
const readProps = relation
|
|
351
|
+
? getKeyProps(dependency, node)
|
|
352
|
+
: reference
|
|
353
|
+
? [reference.refPath[0]]
|
|
354
|
+
: [];
|
|
355
|
+
return readProps.some((prop) => filledProps.includes(prop));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Return true if `node` waits for the keys or references of inserted models,
|
|
359
|
+
// also through the models that it references (#356), and through the models
|
|
360
|
+
// whose keys it reads once the graph fills them in.
|
|
361
|
+
_waitsForInserts(node, visitedNodes = new Set()) {
|
|
362
|
+
const { actions } = this.plan;
|
|
363
|
+
visitedNodes.add(node);
|
|
364
|
+
return (this.dependencies.get(node) || []).some((dependency) => {
|
|
365
|
+
const { node: dependencyNode, relation } = dependency;
|
|
366
|
+
const referencedNode = dependencyNode.referencedNode || dependencyNode;
|
|
367
|
+
return (
|
|
368
|
+
actions.get(referencedNode) === 'insert' ||
|
|
369
|
+
((!relation || this._readsFilledProps(dependency)) &&
|
|
370
|
+
!visitedNodes.has(referencedNode) &&
|
|
371
|
+
this._waitsForInserts(referencedNode, visitedNodes))
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Return true if the model of the `#ref` model `node` writes what `node`
|
|
377
|
+
// would write to the row: The model is inserted, or it's written and its path
|
|
378
|
+
// allows any write that the path of `node` allows.
|
|
379
|
+
_isCoveredByModel(node) {
|
|
380
|
+
const { actions, graphOptions } = this.plan;
|
|
381
|
+
const { referencedNode } = node;
|
|
382
|
+
const action = actions.get(referencedNode);
|
|
383
|
+
const allowsWrite = (node) => graphOptions.shouldPatch(node) || graphOptions.shouldUpdate(node);
|
|
384
|
+
return (
|
|
385
|
+
action === 'insert' ||
|
|
386
|
+
(['relate', 'patch'].includes(action) && (allowsWrite(referencedNode) || !allowsWrite(node)))
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Return true if `node` is a `#ref` model whose model writes all that it would
|
|
391
|
+
// write, as its place keeps nothing of its own: It's a BelongsToOne model, or
|
|
392
|
+
// it's placed where its model is. Many-to-many models write their join rows.
|
|
393
|
+
_isWrittenByModel(node) {
|
|
394
|
+
const { referencedNode, relation, parent } = node;
|
|
395
|
+
return (
|
|
396
|
+
!!referencedNode &&
|
|
397
|
+
(relation.keySide === 'owner' ||
|
|
398
|
+
(relation.keySide === 'related' &&
|
|
399
|
+
referencedNode.relation === relation &&
|
|
400
|
+
referencedNode.parent === parent)) &&
|
|
401
|
+
this._isCoveredByModel(node)
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
_move(builder, nodes) {
|
|
406
|
+
for (const node of nodes) {
|
|
407
|
+
this._resolveFilledDependencies(node);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return this._patch(builder, nodes);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Copy the keys and `#ref{}` values that `node` waits for into its model,
|
|
414
|
+
// after those that the graph fills into the models that it reads them from,
|
|
415
|
+
// e.g. the new key of an owner whose BelongsToOne row is replaced. Rows that
|
|
416
|
+
// move first only wait for rows that exist, see `_waitsForInserts()`.
|
|
417
|
+
_resolveFilledDependencies(node, visitedNodes = new Set()) {
|
|
418
|
+
visitedNodes.add(node);
|
|
419
|
+
|
|
420
|
+
for (const dependency of this.dependencies.get(node) || []) {
|
|
421
|
+
const referencedNode = dependency.node.referencedNode || dependency.node;
|
|
422
|
+
|
|
423
|
+
if (!visitedNodes.has(referencedNode) && this._readsFilledProps(dependency)) {
|
|
424
|
+
this._resolveFilledDependencies(referencedNode, visitedNodes);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this._resolveDependencies(node);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
_remove(builder, nodes, areReferenced) {
|
|
432
|
+
const { actions } = this.plan;
|
|
433
|
+
const queries = [];
|
|
434
|
+
|
|
435
|
+
for (const [relation, relationNodes] of groupBy(nodes, (node) => node.relation)) {
|
|
436
|
+
// Each row binds its id, and the owner keys of its owner at most.
|
|
437
|
+
const maxRows = getMaxRows(
|
|
438
|
+
builder.unsafeKnex(),
|
|
439
|
+
relation.relatedModelClass.getIdColumnArray().length + relation.ownerProp.size,
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
for (const [action, actionNodes] of groupBy(relationNodes, (node) => actions.get(node))) {
|
|
443
|
+
// One query would match the ids with all owners, so join rows are
|
|
444
|
+
// removed per owner, and so are unrelated rows, which may have moved
|
|
445
|
+
// from one owner to another before (#214).
|
|
446
|
+
const groups =
|
|
447
|
+
relation.keySide === 'join' || action === 'unrelate'
|
|
448
|
+
? [...groupBy(actionNodes, (node) => node.parent).values()]
|
|
449
|
+
: [actionNodes];
|
|
450
|
+
|
|
451
|
+
for (const groupNodes of groups.flatMap((nodes) => chunk(nodes, maxRows))) {
|
|
452
|
+
// Rows that reference their owners are found through the relation.
|
|
453
|
+
// BelongsToOne rows aren't referenced anymore once their owners are
|
|
454
|
+
// patched.
|
|
455
|
+
const owners = [...new Set(groupNodes.map((node) => node.parent.obj))];
|
|
456
|
+
const query = (
|
|
457
|
+
areReferenced
|
|
458
|
+
? relation.ownerModelClass.relatedQuery(relation.name).for(owners)
|
|
459
|
+
: relation.relatedModelClass.query()
|
|
460
|
+
)
|
|
461
|
+
.childQueryOf(builder)
|
|
462
|
+
.findByIds(groupNodes.map((node) => node.obj.$id()));
|
|
463
|
+
|
|
464
|
+
queries.push(action === 'unrelate' ? query.unrelate() : query.delete());
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return promiseMap(queries, (query) => query.execute(), {
|
|
470
|
+
concurrency: getConcurrency(builder, nodes),
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async _insert(builder, nodes, references) {
|
|
475
|
+
const batchSize = getBatchSize(builder);
|
|
476
|
+
const batches = [...groupBy(nodes, (node) => node.modelClass).values()].flatMap((nodes) =>
|
|
477
|
+
chunk(nodes, batchSize),
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
await promiseMap(batches, (batch) => this._insertBatch(builder, batch), {
|
|
481
|
+
concurrency: getConcurrency(builder, nodes),
|
|
482
|
+
});
|
|
483
|
+
await this._patch(
|
|
484
|
+
builder,
|
|
485
|
+
nodes.filter((node) => this.slotMovedNodes.has(node)),
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
for (const node of references) {
|
|
489
|
+
this._resolveDependencies(node);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Copy the keys and `#ref{}` values that `node` waits for into its model.
|
|
494
|
+
_resolveDependencies(node) {
|
|
495
|
+
for (const dependency of this.dependencies.get(node) || []) {
|
|
496
|
+
if (!this.deferredDependencies.has(dependency)) {
|
|
497
|
+
if (dependency.relation) {
|
|
498
|
+
resolveKeys(dependency.related);
|
|
499
|
+
} else if (dependency.reference) {
|
|
500
|
+
resolvePropertyReference(dependency.dependent, dependency.reference);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async _insertBatch(builder, nodes) {
|
|
507
|
+
const { actions } = this.plan;
|
|
508
|
+
|
|
509
|
+
for (const node of nodes) {
|
|
510
|
+
this._resolveDependencies(node);
|
|
511
|
+
|
|
512
|
+
// Join table extras are written to the join rows.
|
|
513
|
+
if (node.relation?.keySide === 'join') {
|
|
514
|
+
node.relation.omitExtraProps([node.obj]);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (actions.get(node) === 'insert') {
|
|
518
|
+
this._setKeyArrays(node);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const insertNodes = nodes.filter((node) => actions.get(node) === 'insert');
|
|
523
|
+
|
|
524
|
+
if (insertNodes.length > 0) {
|
|
525
|
+
const [{ modelClass }] = insertNodes;
|
|
526
|
+
|
|
527
|
+
for (const node of insertNodes) {
|
|
528
|
+
node.obj.$validate(null, { dataPath: node.dataPathKey });
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
await Promise.all(
|
|
532
|
+
insertNodes.map(
|
|
533
|
+
(node) =>
|
|
534
|
+
node.parent && node.relation.beforeInsert(node.obj, builder.context(), node.parent.obj),
|
|
535
|
+
),
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
await modelClass
|
|
539
|
+
.query()
|
|
540
|
+
.insert(insertNodes.map((node) => node.obj))
|
|
541
|
+
.childQueryOf(builder)
|
|
542
|
+
.copyFrom(builder, isReturningAll)
|
|
543
|
+
.execute();
|
|
544
|
+
|
|
545
|
+
await fetchGeneratedKeys(builder, modelClass, insertNodes);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
_writeDeferredKeys(builder, dependencies) {
|
|
550
|
+
const owners = groupBy(dependencies, (dependency) => dependency.dependent);
|
|
551
|
+
|
|
552
|
+
return promiseMap(
|
|
553
|
+
[...owners],
|
|
554
|
+
([owner, dependencies]) => {
|
|
555
|
+
const { modelClass } = owner;
|
|
556
|
+
const idProp = modelClass.getIdRelationProperty();
|
|
557
|
+
const patch = {};
|
|
558
|
+
|
|
559
|
+
for (const { relation, related } of dependencies) {
|
|
560
|
+
resolveKeys(related);
|
|
561
|
+
|
|
562
|
+
relation.ownerProp.forEach((i) => {
|
|
563
|
+
relation.ownerProp.patch(patch, i, relation.ownerProp.getProp(owner.obj, i));
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return modelClass
|
|
568
|
+
.query()
|
|
569
|
+
.childQueryOf(builder)
|
|
570
|
+
.patch(patch)
|
|
571
|
+
.whereInComposite(idProp.refs(builder), [idProp.getProps(owner.obj)]);
|
|
572
|
+
},
|
|
573
|
+
{ concurrency: getConcurrency(builder, [...owners.keys()]) },
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async _insertJoinRows(builder, nodes) {
|
|
578
|
+
const knex = builder.unsafeKnex();
|
|
579
|
+
const inserts = [];
|
|
580
|
+
|
|
581
|
+
for (const [joinModelClass, joinNodes] of groupBy(nodes, (node) =>
|
|
582
|
+
node.relation.getJoinModelClass(knex),
|
|
583
|
+
)) {
|
|
584
|
+
// Each pair of models has one join row, which gets the extras of all
|
|
585
|
+
// places of the pair:
|
|
586
|
+
const joinRows = new Map();
|
|
587
|
+
|
|
588
|
+
for (const node of joinNodes) {
|
|
589
|
+
const { relation, parent } = node;
|
|
590
|
+
resolveReference(node);
|
|
591
|
+
|
|
592
|
+
const key = `${relation.name}:${relation.ownerProp.propKey(parent.obj)}:${relation.relatedProp.propKey(node.obj)}`;
|
|
593
|
+
const row = relation.createJoinModel(relation.ownerProp.getProps(parent.obj), node.obj);
|
|
594
|
+
joinRows.set(key, { relation, row: { ...row, ...joinRows.get(key)?.row } });
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const objs = await Promise.all(
|
|
598
|
+
[...joinRows.values()].map(async ({ relation, row }) => {
|
|
599
|
+
const obj = joinModelClass.fromJson(row);
|
|
600
|
+
await relation.joinTableBeforeInsert(obj, builder.context());
|
|
601
|
+
return obj;
|
|
602
|
+
}),
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
// Join rows that have their ids can be inserted in batches everywhere.
|
|
606
|
+
const batchSize = objs.every((obj) => obj.$hasId())
|
|
607
|
+
? MAX_INSERT_BATCH_SIZE
|
|
608
|
+
: getBatchSize(builder);
|
|
609
|
+
|
|
610
|
+
for (const batch of chunk(objs, batchSize)) {
|
|
611
|
+
inserts.push(() => joinModelClass.query().childQueryOf(builder).insert(batch));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return promiseMap(inserts, (insert) => insert(), {
|
|
616
|
+
concurrency: getConcurrency(builder, nodes),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async _patch(builder, nodes) {
|
|
621
|
+
// The patches leave out the props that don't change, but the models that
|
|
622
|
+
// the graph returns get them back once the step is written (#371):
|
|
623
|
+
const omittedProps = new Map(nodes.map((node) => [node, node.obj.$omitFromDatabaseJson()]));
|
|
624
|
+
|
|
625
|
+
try {
|
|
626
|
+
const patches = nodes.map((node) => this._preparePatch(builder, node)).filter(Boolean);
|
|
627
|
+
const cycles = this._findReferenceCycles(patches.map(({ node }) => node));
|
|
628
|
+
// Patches whose rows reference each other lock them in opposite order
|
|
629
|
+
// when they run at the same time, which deadlocks on MySQL. We write the
|
|
630
|
+
// patches of each cycle one after the other (#385):
|
|
631
|
+
const groups = groupBy(patches, ({ node }) => cycles.get(node) || node);
|
|
632
|
+
await promiseMap(
|
|
633
|
+
[...groups.values()],
|
|
634
|
+
async (patches) => {
|
|
635
|
+
for (const patch of patches) {
|
|
636
|
+
await this._writePatch(builder, patch);
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
{ concurrency: getConcurrency(builder, nodes) },
|
|
640
|
+
);
|
|
641
|
+
} finally {
|
|
642
|
+
for (const [node, props] of omittedProps) {
|
|
643
|
+
restoreOmitFromDatabaseJson(node.obj, props);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Map the nodes whose rows reference each other through the keys and values
|
|
649
|
+
// that they write, also through the rows of other nodes, to the first node of
|
|
650
|
+
// their cycle: the strongly connected components of these references, found
|
|
651
|
+
// with Tarjan's algorithm.
|
|
652
|
+
_findReferenceCycles(nodes) {
|
|
653
|
+
const getRow = (node) => node.referencedNode || node;
|
|
654
|
+
const nodesByRow = groupBy(nodes, getRow);
|
|
655
|
+
const getReferencedNodes = (node) =>
|
|
656
|
+
(this.dependencies.get(node) || [])
|
|
657
|
+
.flatMap((dependency) => nodesByRow.get(getRow(dependency.node)) || [])
|
|
658
|
+
.filter((referencedNode) => referencedNode !== node);
|
|
659
|
+
|
|
660
|
+
const indices = new Map();
|
|
661
|
+
const lowLinks = new Map();
|
|
662
|
+
const stack = [];
|
|
663
|
+
const stackedNodes = new Set();
|
|
664
|
+
const cycles = new Map();
|
|
665
|
+
|
|
666
|
+
const visit = (node) => {
|
|
667
|
+
indices.set(node, indices.size);
|
|
668
|
+
lowLinks.set(node, indices.get(node));
|
|
669
|
+
stack.push(node);
|
|
670
|
+
stackedNodes.add(node);
|
|
671
|
+
|
|
672
|
+
for (const referencedNode of getReferencedNodes(node)) {
|
|
673
|
+
if (!indices.has(referencedNode)) {
|
|
674
|
+
visit(referencedNode);
|
|
675
|
+
lowLinks.set(node, Math.min(lowLinks.get(node), lowLinks.get(referencedNode)));
|
|
676
|
+
} else if (stackedNodes.has(referencedNode)) {
|
|
677
|
+
lowLinks.set(node, Math.min(lowLinks.get(node), indices.get(referencedNode)));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// `node` is the first node of a component, which holds the nodes above
|
|
682
|
+
// it on the stack:
|
|
683
|
+
if (lowLinks.get(node) === indices.get(node)) {
|
|
684
|
+
const componentNodes = stack.splice(stack.lastIndexOf(node));
|
|
685
|
+
|
|
686
|
+
for (const componentNode of componentNodes) {
|
|
687
|
+
stackedNodes.delete(componentNode);
|
|
688
|
+
|
|
689
|
+
if (componentNodes.length > 1) {
|
|
690
|
+
cycles.set(componentNode, node);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
for (const node of nodes) {
|
|
697
|
+
if (!indices.has(node)) {
|
|
698
|
+
visit(node);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return cycles;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Find out what to write for `node`, validate it, and return the patch, or
|
|
706
|
+
// nothing if there is nothing to write.
|
|
707
|
+
_preparePatch(builder, node) {
|
|
708
|
+
const { referencedNode } = node;
|
|
709
|
+
|
|
710
|
+
// The props that the graph fills into the model of a `#ref` model are only
|
|
711
|
+
// final now:
|
|
712
|
+
if (referencedNode) {
|
|
713
|
+
resolveReference(node, this._getFilledProps(referencedNode));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const { graphOptions } = this.plan;
|
|
717
|
+
const shouldPatch = graphOptions.shouldPatch(node);
|
|
718
|
+
const shouldUpdate = graphOptions.shouldUpdate(node);
|
|
719
|
+
// Inserts, relates and removals of BelongsToOne rows change the keys of
|
|
720
|
+
// their owners, and relates into other relations the keys of the related
|
|
721
|
+
// rows, which are written even with `noUpdate` (#53):
|
|
722
|
+
const keyProps = [
|
|
723
|
+
...this._setKeysOfRelatedRows(node),
|
|
724
|
+
...this._clearKeysOfRemovedRows(node),
|
|
725
|
+
...this._setKeyArrays(node),
|
|
726
|
+
...(this.plan.actions.get(node) === 'relate' && node.relation.keySide === 'related'
|
|
727
|
+
? node.relation.relatedProp.props
|
|
728
|
+
: []),
|
|
729
|
+
];
|
|
730
|
+
// Changing the id never makes sense with `upsertGraph()`. A many-to-many
|
|
731
|
+
// `#ref` model whose model writes its row only writes its join row:
|
|
732
|
+
const idProps = node.modelClass.getIdPropertyArray();
|
|
733
|
+
const isJoinRowOnly =
|
|
734
|
+
!!referencedNode && node.relation.keySide === 'join' && this._isCoveredByModel(node);
|
|
735
|
+
const isWritable = (prop) =>
|
|
736
|
+
!idProps.includes(prop) &&
|
|
737
|
+
(!isJoinRowOnly || node.relation.joinTableExtras.some((extra) => extra.aliasProp === prop));
|
|
738
|
+
|
|
739
|
+
const getPropsToUpdate = (changedProps) =>
|
|
740
|
+
(shouldPatch || shouldUpdate ? changedProps : keyProps).filter(isWritable);
|
|
741
|
+
|
|
742
|
+
// Leave out the props that don't change, and return true if any do.
|
|
743
|
+
const handleUpdate = ({ changedProps, unchangedProps } = this._findChanges(node)) => {
|
|
744
|
+
const propsToUpdate = getPropsToUpdate(changedProps);
|
|
745
|
+
|
|
746
|
+
if (propsToUpdate.length > 0) {
|
|
747
|
+
node.obj.$omitFromDatabaseJson(
|
|
748
|
+
difference([...changedProps, ...unchangedProps], propsToUpdate),
|
|
749
|
+
);
|
|
750
|
+
return true;
|
|
751
|
+
} else {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
// Without a `beforeUpdate` or `$beforeUpdate` hook, we know right away
|
|
757
|
+
// what to update. Otherwise, the hook may still change the model, and we
|
|
758
|
+
// check again after it runs, but only if the model has props to update, as
|
|
759
|
+
// relates are no updates (#2233, #2605).
|
|
760
|
+
const changes = this._findChanges(node);
|
|
761
|
+
const propsToUpdate = getPropsToUpdate(changes.changedProps);
|
|
762
|
+
const hasBeforeUpdate = hasHook(node.obj, 'beforeUpdate');
|
|
763
|
+
|
|
764
|
+
if (propsToUpdate.length === 0) {
|
|
765
|
+
return null;
|
|
766
|
+
} else if (!hasBeforeUpdate) {
|
|
767
|
+
handleUpdate(changes);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
node.obj.$validate(null, {
|
|
771
|
+
dataPath: node.dataPathKey,
|
|
772
|
+
patch: shouldPatch || !shouldUpdate,
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// Rows that their owners relate by their keys can't be found through the
|
|
776
|
+
// relation until the owners are patched, so they're patched directly.
|
|
777
|
+
const { relation } = node;
|
|
778
|
+
const isPatchedThroughRelation =
|
|
779
|
+
!!node.parent &&
|
|
780
|
+
(relation.keySide === 'join' ||
|
|
781
|
+
(relation.keySide === 'owner' && this.plan.actions.get(node) !== 'relate'));
|
|
782
|
+
|
|
783
|
+
return {
|
|
784
|
+
node,
|
|
785
|
+
shouldPatch,
|
|
786
|
+
hasBeforeUpdate,
|
|
787
|
+
handleUpdate,
|
|
788
|
+
propsToUpdate,
|
|
789
|
+
isPatchedThroughRelation,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
_writePatch(
|
|
794
|
+
builder,
|
|
795
|
+
{ node, shouldPatch, hasBeforeUpdate, handleUpdate, propsToUpdate, isPatchedThroughRelation },
|
|
796
|
+
) {
|
|
797
|
+
const query = (
|
|
798
|
+
isPatchedThroughRelation
|
|
799
|
+
? // Find the row through the keys of the owner's current row, as the
|
|
800
|
+
// graph may leave them out of the owner:
|
|
801
|
+
(node.parent.existing || node.parent).obj
|
|
802
|
+
.$relatedQuery(node.relation.name)
|
|
803
|
+
.findById(node.obj.$id())
|
|
804
|
+
: (node.current?.obj || node.obj).$query()
|
|
805
|
+
)
|
|
806
|
+
.childQueryOf(builder, { fork: true, isInternalQuery: true })
|
|
807
|
+
// The current graph was fetched through the relations, so the patched
|
|
808
|
+
// models are known to be related to their parents.
|
|
809
|
+
.internalOptions({
|
|
810
|
+
skipRelationFilter: true,
|
|
811
|
+
// Join rows are patched with the extras among the props to update. The
|
|
812
|
+
// extras of new join rows are written with them, see `_findChanges()`:
|
|
813
|
+
joinTableExtras: node.relation?.keySide === 'join' && !node.current ? [] : propsToUpdate,
|
|
814
|
+
})
|
|
815
|
+
.copyFrom(builder, isReturningAll);
|
|
816
|
+
|
|
817
|
+
if (hasBeforeUpdate) {
|
|
818
|
+
query.internalContext().runBefore.push((result, query) => {
|
|
819
|
+
if (!handleUpdate()) {
|
|
820
|
+
query.internalOptions().returnImmediatelyValue = null;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return result;
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return (shouldPatch ? query.patch(node.obj) : query.update(node.obj))
|
|
828
|
+
.runAfter(async (result, query) => {
|
|
829
|
+
if (isObject(result) && result.$isObjectionModel) {
|
|
830
|
+
// Handle `returning('*')`.
|
|
831
|
+
node.obj.$set(result);
|
|
832
|
+
} else if (
|
|
833
|
+
(result === 0 || result === undefined) &&
|
|
834
|
+
// Not if a hook resolved the query, e.g. with `cancelQuery()`, or
|
|
835
|
+
// there was no column to write:
|
|
836
|
+
!query.findOperation(UpdateOperation).isResolvedWithoutWrite(query)
|
|
837
|
+
) {
|
|
838
|
+
// Rows patched through relations are gone from there if not found.
|
|
839
|
+
if (isPatchedThroughRelation) {
|
|
840
|
+
throw createPatchNotFoundError(builder, node);
|
|
841
|
+
} else {
|
|
842
|
+
await throwForMissingRow(builder, node);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return result;
|
|
847
|
+
})
|
|
848
|
+
.execute();
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Change the arrays of keys that `node` holds for the relations that the graph
|
|
852
|
+
// gives it: Remove the keys of the current related rows that it deletes or
|
|
853
|
+
// unrelates, and add the keys of the related models that it writes. Other
|
|
854
|
+
// keys stay, e.g. of rows that the relation's filter leaves out. Relations
|
|
855
|
+
// that the graph leaves out keep their arrays (#1202). Return the props that
|
|
856
|
+
// change.
|
|
857
|
+
_setKeyArrays(node) {
|
|
858
|
+
const { actions, graphOptions } = this.plan;
|
|
859
|
+
const { existing } = node;
|
|
860
|
+
const props = [];
|
|
861
|
+
|
|
862
|
+
for (const name of node.modelClass.getRelationNames()) {
|
|
863
|
+
const relation = node.obj[name] !== undefined && node.modelClass.getRelation(name);
|
|
864
|
+
|
|
865
|
+
if (relation && hasOwnerKeyArray(relation)) {
|
|
866
|
+
const { ownerProp, relatedProp } = relation;
|
|
867
|
+
const getKey = (related) => relatedProp.getProp(related.obj, 0);
|
|
868
|
+
const keys = relation.getKeys(existing ? existing.obj : node.obj);
|
|
869
|
+
const removed = (existing?.activeChildren || []).filter(
|
|
870
|
+
(child) =>
|
|
871
|
+
child.relation.name === name && ['delete', 'unrelate'].includes(actions.get(child)),
|
|
872
|
+
);
|
|
873
|
+
const added = node.activeChildren.filter(
|
|
874
|
+
(child) =>
|
|
875
|
+
child.relation.name === name &&
|
|
876
|
+
!child.toBeUnrelatedOrDeleted &&
|
|
877
|
+
actions.get(child) !== 'ignore',
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
for (const related of added) {
|
|
881
|
+
resolveReference(related);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const newKeys = relation.changeKeys(keys, {
|
|
885
|
+
add: added.map(getKey),
|
|
886
|
+
remove: removed.map(getKey),
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
if (!existing || graphOptions.shouldUpdate(node)) {
|
|
890
|
+
// Inserts and updates write the whole row:
|
|
891
|
+
ownerProp.setProp(node.obj, 0, newKeys);
|
|
892
|
+
props.push(ownerProp.props[0]);
|
|
893
|
+
} else if (!jsonEquals(keys, newKeys)) {
|
|
894
|
+
const patch = relation.createKeysPatch(existing.obj, newKeys);
|
|
895
|
+
Object.assign(node.obj, patch);
|
|
896
|
+
props.push(...Object.keys(patch));
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return props;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Point the keys of `node` to its BelongsToOne rows that are inserted or
|
|
905
|
+
// related, and return the props that change.
|
|
906
|
+
_setKeysOfRelatedRows(node) {
|
|
907
|
+
const { actions } = this.plan;
|
|
908
|
+
const props = [];
|
|
909
|
+
|
|
910
|
+
for (const child of node.activeChildren) {
|
|
911
|
+
const { relation } = child;
|
|
912
|
+
|
|
913
|
+
if (
|
|
914
|
+
hasOwnerKey(relation) &&
|
|
915
|
+
relation.relatedProp.hasProps(child.obj) &&
|
|
916
|
+
// Ignored models don't replace the current one, and models marked with
|
|
917
|
+
// `#unrelate` or `#delete` are handled below.
|
|
918
|
+
actions.get(child) !== 'ignore'
|
|
919
|
+
) {
|
|
920
|
+
relation.ownerProp.forEach((i) => {
|
|
921
|
+
const currentValue = node.existing && relation.ownerProp.getProp(node.existing.obj, i);
|
|
922
|
+
const value = relation.relatedProp.getProp(child.obj, i);
|
|
923
|
+
|
|
924
|
+
if (currentValue != value) {
|
|
925
|
+
relation.ownerProp.setProp(node.obj, i, value);
|
|
926
|
+
props.push(relation.ownerProp.props[i]);
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return props;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Clear the keys of `node` that point to BelongsToOne rows that are removed,
|
|
936
|
+
// and return the props that change.
|
|
937
|
+
_clearKeysOfRemovedRows(node) {
|
|
938
|
+
const { actions } = this.plan;
|
|
939
|
+
const props = [];
|
|
940
|
+
|
|
941
|
+
for (const currentChild of node.existing?.activeChildren || []) {
|
|
942
|
+
const { relation } = currentChild;
|
|
943
|
+
|
|
944
|
+
if (
|
|
945
|
+
hasOwnerKey(relation) &&
|
|
946
|
+
(node.obj[relation.name] === null || currentChild.node?.toBeUnrelatedOrDeleted) &&
|
|
947
|
+
actions.get(currentChild) !== 'keep'
|
|
948
|
+
) {
|
|
949
|
+
relation.ownerProp.forEach((i) => {
|
|
950
|
+
if (relation.ownerProp.getProp(node.existing.obj, i) != null) {
|
|
951
|
+
relation.ownerProp.setProp(node.obj, i, null);
|
|
952
|
+
props.push(relation.ownerProp.props[i]);
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
return props;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
_findChanges(node) {
|
|
962
|
+
const { obj, relation } = node;
|
|
963
|
+
const currentObj = node.current?.obj || {};
|
|
964
|
+
const relationNames = node.modelClass.getRelationNames();
|
|
965
|
+
// The extras of a new join row are written with it:
|
|
966
|
+
const extraProps =
|
|
967
|
+
relation?.keySide === 'join' && !node.current
|
|
968
|
+
? relation.joinTableExtras.map((extra) => extra.aliasProp)
|
|
969
|
+
: [];
|
|
970
|
+
const changedProps = [];
|
|
971
|
+
const unchangedProps = [];
|
|
972
|
+
|
|
973
|
+
for (const prop of Object.keys(obj)) {
|
|
974
|
+
if (!isInternalProp(prop) && !relationNames.includes(prop)) {
|
|
975
|
+
const value = obj[prop];
|
|
976
|
+
const currentValue = currentObj[prop];
|
|
977
|
+
|
|
978
|
+
// We can't know if a prop that the current model doesn't have changes,
|
|
979
|
+
// so we assume it does (#732).
|
|
980
|
+
if (
|
|
981
|
+
!extraProps.includes(prop) &&
|
|
982
|
+
(currentValue === undefined || !nonStrictEquals(currentValue, value))
|
|
983
|
+
) {
|
|
984
|
+
changedProps.push(prop);
|
|
985
|
+
} else {
|
|
986
|
+
unchangedProps.push(prop);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Query properties may change the values too.
|
|
992
|
+
if (obj.$$queryProps) {
|
|
993
|
+
changedProps.push(...Object.keys(obj.$$queryProps));
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
return { changedProps, unchangedProps };
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Collect what each node waits for: The node on the other side of its relation
|
|
1001
|
+
// if the node stores its keys, and the nodes it references with `#ref` and
|
|
1002
|
+
// `#ref{}`.
|
|
1003
|
+
function createDependencies(plan) {
|
|
1004
|
+
const { actions } = plan;
|
|
1005
|
+
const dependencies = new Map();
|
|
1006
|
+
|
|
1007
|
+
const addDependency = (dependent, dependency) => {
|
|
1008
|
+
const nodeDependencies = dependencies.get(dependent);
|
|
1009
|
+
const entry = { dependent, ...dependency };
|
|
1010
|
+
|
|
1011
|
+
if (nodeDependencies) {
|
|
1012
|
+
nodeDependencies.push(entry);
|
|
1013
|
+
} else {
|
|
1014
|
+
dependencies.set(dependent, [entry]);
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
for (const node of plan.activeNodes) {
|
|
1019
|
+
const { parent, relation } = node;
|
|
1020
|
+
|
|
1021
|
+
// Models marked with `#unrelate` or `#delete` are never inserted or related,
|
|
1022
|
+
// so their keys aren't copied. Ignored models don't replace the current one,
|
|
1023
|
+
// so their keys aren't copied to their owners either.
|
|
1024
|
+
if (parent && !node.toBeUnrelatedOrDeleted) {
|
|
1025
|
+
if (relation.keySide === 'related') {
|
|
1026
|
+
addDependency(node, { node: parent, relation, related: node });
|
|
1027
|
+
} else if (relation.keySide === 'owner' && actions.get(node) !== 'ignore') {
|
|
1028
|
+
addDependency(parent, { node, relation, related: node });
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
for (const node of plan.activeNodes) {
|
|
1034
|
+
if (node.referencedNode) {
|
|
1035
|
+
addDependency(node, { node: node.referencedNode });
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
for (const reference of node.propertyReferences) {
|
|
1039
|
+
addDependency(node, { node: reference.referencedNode, reference });
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// A missing row that the graph inserts elsewhere is related once it is:
|
|
1043
|
+
const insertingNode =
|
|
1044
|
+
node.isMissing && actions.get(node) === 'relate' && plan.findInsertingNode(node);
|
|
1045
|
+
|
|
1046
|
+
if (insertingNode) {
|
|
1047
|
+
addDependency(node, { node: insertingNode });
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// A row that goes into the slot of a HasOneRelation waits for the relate
|
|
1051
|
+
// that moves the current row out of it, as the foreign key may be unique.
|
|
1052
|
+
const moverNode = getSlotMover(plan, node);
|
|
1053
|
+
|
|
1054
|
+
if (moverNode) {
|
|
1055
|
+
addDependency(node, { node: moverNode, isSlotMove: true });
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return dependencies;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Find the relate that moves the current row in the slot of the HasOneRelation
|
|
1063
|
+
// that `node` is inserted or related into to another owner.
|
|
1064
|
+
function getSlotMover(plan, node) {
|
|
1065
|
+
const { actions } = plan;
|
|
1066
|
+
const slotNode = ['insert', 'relate'].includes(actions.get(node)) && getSlotNode(node);
|
|
1067
|
+
const keptNode = slotNode && plan.keptNodeForRow(slotNode);
|
|
1068
|
+
return keptNode && actions.get(keptNode) === 'relate' ? keptNode : null;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Fetch the keys that the relations of the inserted nodes need, but that the
|
|
1072
|
+
// database generated and the insert didn't return, like a uuid that isn't the
|
|
1073
|
+
// id (Vincit#853).
|
|
1074
|
+
async function fetchGeneratedKeys(builder, modelClass, nodes) {
|
|
1075
|
+
const keyProps = (node) => [
|
|
1076
|
+
...(node.relation && node.relation.keySide !== 'related' ? [node.relation.relatedProp] : []),
|
|
1077
|
+
...node.activeChildren
|
|
1078
|
+
.filter((child) => child.relation.keySide !== 'owner')
|
|
1079
|
+
.map((child) => child.relation.ownerProp),
|
|
1080
|
+
];
|
|
1081
|
+
const missingNodes = nodes.filter((node) =>
|
|
1082
|
+
keyProps(node).some((prop) => prop.getProps(node.obj).some((value) => value === undefined)),
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
if (missingNodes.length > 0 && missingNodes.every((node) => node.obj.$hasId())) {
|
|
1086
|
+
// SQL Server returns columns selected twice as arrays, so the ids are
|
|
1087
|
+
// part of the set:
|
|
1088
|
+
const columns = new Set([
|
|
1089
|
+
...modelClass.getIdColumnArray(),
|
|
1090
|
+
...missingNodes.flatMap((node) => keyProps(node)).flatMap((prop) => prop.cols),
|
|
1091
|
+
]);
|
|
1092
|
+
const rows = await modelClass
|
|
1093
|
+
.query()
|
|
1094
|
+
.childQueryOf(builder, { fork: true, isInternalQuery: true })
|
|
1095
|
+
.findByIds(missingNodes.map((node) => node.obj.$id()))
|
|
1096
|
+
.select([...columns]);
|
|
1097
|
+
const rowsByKey = new Map(rows.map((row) => [row.$idKey(), row]));
|
|
1098
|
+
|
|
1099
|
+
for (const node of missingNodes) {
|
|
1100
|
+
const row = rowsByKey.get(node.obj.$idKey());
|
|
1101
|
+
|
|
1102
|
+
for (const prop of keyProps(node)) {
|
|
1103
|
+
prop.forEach((i) => {
|
|
1104
|
+
if (prop.getProp(node.obj, i) === undefined) {
|
|
1105
|
+
prop.setProp(node.obj, i, prop.getProp(row, i));
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Copy the keys of the relation of `node` to the side that stores them,
|
|
1114
|
+
// whichever node was inserted first (#311).
|
|
1115
|
+
function resolveKeys(node) {
|
|
1116
|
+
const { relation, parent } = node;
|
|
1117
|
+
|
|
1118
|
+
// The arrays of keys of owners are written by `_setKeyArrays()`.
|
|
1119
|
+
if (relation.keySide === 'related' || hasOwnerKey(relation)) {
|
|
1120
|
+
const { ownerProp, relatedProp } = relation;
|
|
1121
|
+
const [source, sourceProp, target, targetProp] =
|
|
1122
|
+
relation.keySide === 'owner'
|
|
1123
|
+
? [node, relatedProp, parent, ownerProp]
|
|
1124
|
+
: [parent, ownerProp, node, relatedProp];
|
|
1125
|
+
|
|
1126
|
+
resolveReference(source);
|
|
1127
|
+
resolveReference(target);
|
|
1128
|
+
|
|
1129
|
+
targetProp.forEach((i) => {
|
|
1130
|
+
targetProp.setProp(target.obj, i, sourceProp.getProp(source.obj, i));
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Give a `#ref` model the props of the model it refers to that it doesn't have,
|
|
1136
|
+
// and the `filledProps` again, as they may have changed since. Its own keys
|
|
1137
|
+
// stay its own, and the join table extras of the place of the model belong to
|
|
1138
|
+
// the join row there.
|
|
1139
|
+
function resolveReference(node, filledProps = []) {
|
|
1140
|
+
const { referencedNode, relation } = node;
|
|
1141
|
+
|
|
1142
|
+
if (referencedNode) {
|
|
1143
|
+
const ownKeys = relation?.keySide === 'related' ? relation.relatedProp.props : [];
|
|
1144
|
+
const modelExtras =
|
|
1145
|
+
referencedNode.relation?.keySide === 'join'
|
|
1146
|
+
? referencedNode.relation.joinTableExtras.map((extra) => extra.aliasProp)
|
|
1147
|
+
: [];
|
|
1148
|
+
|
|
1149
|
+
for (const prop of difference(Object.keys(referencedNode.obj), modelExtras)) {
|
|
1150
|
+
if (
|
|
1151
|
+
!Object.hasOwn(node.obj, prop) ||
|
|
1152
|
+
(filledProps.includes(prop) && !ownKeys.includes(prop))
|
|
1153
|
+
) {
|
|
1154
|
+
node.obj[prop] = referencedNode.obj[prop];
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Replace a `#ref{}` in a prop of `node` with the value it refers to. A whole
|
|
1161
|
+
// value is replaced even if the relation set the prop already (Vincit#1378).
|
|
1162
|
+
function resolvePropertyReference(node, { path, match, refPath, isWholeValue, referencedNode }) {
|
|
1163
|
+
const value = get(referencedNode.obj, refPath);
|
|
1164
|
+
set(node.obj, path, isWholeValue ? value : get(node.obj, path).replace(match, value));
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Fill in the keys of `node` that the graph leaves out from its row: the keys
|
|
1168
|
+
// that its related models read from wherever the row was found, and the key
|
|
1169
|
+
// of its parent relation only from its row in the same place. The keys that it
|
|
1170
|
+
// holds for the BelongsToOne rows that the graph writes come from the graph.
|
|
1171
|
+
function copyKeysFromExisting(node, actions) {
|
|
1172
|
+
const { existing, current } = node;
|
|
1173
|
+
|
|
1174
|
+
if (existing) {
|
|
1175
|
+
const copies = [
|
|
1176
|
+
...(node.parent && current ? [[node.relation.relatedProp, current]] : []),
|
|
1177
|
+
...node.activeChildren
|
|
1178
|
+
.filter(
|
|
1179
|
+
(child) =>
|
|
1180
|
+
!hasOwnerKeyArray(child.relation) &&
|
|
1181
|
+
(child.relation.keySide !== 'owner' || actions.get(child) === 'ignore'),
|
|
1182
|
+
)
|
|
1183
|
+
.map((child) => [child.relation.ownerProp, existing]),
|
|
1184
|
+
];
|
|
1185
|
+
|
|
1186
|
+
for (const [prop, source] of copies) {
|
|
1187
|
+
prop.forEach((i) => {
|
|
1188
|
+
if (prop.getProp(node.obj, i) === undefined) {
|
|
1189
|
+
prop.setProp(node.obj, i, prop.getProp(source.obj, i));
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function hasHook(obj, name) {
|
|
1197
|
+
const Model = getModel();
|
|
1198
|
+
return obj.constructor[name] !== Model[name] || obj[`$${name}`] !== Model.prototype[`$${name}`];
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Throw if the patched row of `node` doesn't exist anymore, e.g. deleted
|
|
1202
|
+
// through `ON DELETE CASCADE` by an earlier write of the graph. A row that still
|
|
1203
|
+
// exists counts as patched, even if a trigger skipped it, or MySQL didn't count
|
|
1204
|
+
// it as changed.
|
|
1205
|
+
async function throwForMissingRow(builder, node) {
|
|
1206
|
+
const { modelClass } = node;
|
|
1207
|
+
const row = await modelClass
|
|
1208
|
+
.query()
|
|
1209
|
+
.childQueryOf(builder, { fork: true, isInternalQuery: true })
|
|
1210
|
+
.findById(node.obj.$id())
|
|
1211
|
+
.select(modelClass.getIdColumnArray());
|
|
1212
|
+
|
|
1213
|
+
if (!row) {
|
|
1214
|
+
throw createPatchNotFoundError(builder, node);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function createPatchNotFoundError(builder, node) {
|
|
1219
|
+
return node.modelClass.createNotFoundError(builder.context(), {
|
|
1220
|
+
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`,
|
|
1221
|
+
dataPath: node.dataPath,
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// Find the current row in the slot of the HasOneRelation that `node` relates
|
|
1226
|
+
// or is inserted into, or null if it's in another relation or an empty slot.
|
|
1227
|
+
function getSlotNode(node) {
|
|
1228
|
+
const { relation } = node;
|
|
1229
|
+
return relation?.keySide === 'related' && relation.isOneToOne()
|
|
1230
|
+
? (node.parent.existing?.activeChildren.find(
|
|
1231
|
+
(child) => child.relation.name === relation.name,
|
|
1232
|
+
) ?? null)
|
|
1233
|
+
: null;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// Return the key props of `node` on its side of the relation of `dependency`.
|
|
1237
|
+
function getKeyProps({ relation, related }, node) {
|
|
1238
|
+
return (related === node ? relation.relatedProp : relation.ownerProp).props;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function hasOwnerKey(relation) {
|
|
1242
|
+
return relation.keySide === 'owner' && relation.isOneToOne();
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function hasOwnerKeyArray(relation) {
|
|
1246
|
+
return relation.keySide === 'owner' && !relation.isOneToOne();
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function getBatchSize(builder) {
|
|
1250
|
+
return hasBatchInsert(builder.unsafeKnex()) ? MAX_INSERT_BATCH_SIZE : 1;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function getConcurrency(builder, nodes) {
|
|
1254
|
+
const knex = builder.unsafeKnex();
|
|
1255
|
+
return nodes.reduce(
|
|
1256
|
+
(concurrency, node) => Math.min(concurrency, node.modelClass.getConcurrency(knex)),
|
|
1257
|
+
MAX_CONCURRENCY,
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function isReturningAll(operation) {
|
|
1262
|
+
return operation.name === 'returning' && operation.args.includes('*');
|
|
1263
|
+
}
|