objection 3.9.1 → 4.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +11 -23
- package/lib/model/modelSet.js +89 -41
- package/lib/model/modelTableMetadata.js +4 -11
- package/lib/model/modelToJson.js +6 -13
- 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 +314 -0
- package/lib/queryBuilder/graph/GraphNode.js +241 -0
- package/lib/queryBuilder/graph/GraphOptions.js +55 -248
- package/lib/queryBuilder/graph/GraphPlan.js +431 -0
- package/lib/queryBuilder/graph/GraphSchedule.js +513 -0
- package/lib/queryBuilder/graph/GraphUpsert.js +519 -412
- package/lib/queryBuilder/graph/GraphWriter.js +508 -0
- package/lib/queryBuilder/graph/graphKeys.js +929 -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 +10 -39
- 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 -45
- 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 +32 -132
- 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 +5 -10
- 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 +64 -72
- 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 +16 -16
- 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 +2 -8
- 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 -238
- package/lib/queryBuilder/graph/GraphAction.js +0 -69
- package/lib/queryBuilder/graph/GraphData.js +0 -53
- package/lib/queryBuilder/graph/GraphFetcher.js +0 -197
- 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 -58
- package/lib/queryBuilder/graph/insert/GraphInsert.js +0 -296
- package/lib/queryBuilder/graph/insert/GraphInsertAction.js +0 -233
- 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 -289
- 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 -64
|
@@ -1,30 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const { GraphNodeDbExistence } = require('./GraphNodeDbExistence');
|
|
15
|
-
const { GraphData } = require('./GraphData');
|
|
16
|
-
const { uniqBy, asArray, isObject } = require('../../utils/objectUtils');
|
|
17
|
-
const { warnOnce } = require('../../utils/warnings');
|
|
18
|
-
|
|
19
|
-
class GraphUpsert {
|
|
1
|
+
import { GraphNode, createNotModelError, formatPath } from './GraphNode.js';
|
|
2
|
+
import { GraphOptions } from './GraphOptions.js';
|
|
3
|
+
import { GraphPlan } from './GraphPlan.js';
|
|
4
|
+
import { GraphKeys, hasSameColumns, readsWrittenValue } from './graphKeys.js';
|
|
5
|
+
import { GraphSchedule } from './GraphSchedule.js';
|
|
6
|
+
import { GraphWriter } from './GraphWriter.js';
|
|
7
|
+
import { loadCurrentGraph } from './GraphLoader.js';
|
|
8
|
+
import { ValidationErrorType } from '../../model/ValidationError.js';
|
|
9
|
+
import { RelationExpression } from '../RelationExpression.js';
|
|
10
|
+
import { asArray, isObject, nonStrictEquals } from '../../utils/objectUtils.js';
|
|
11
|
+
import { isInternalProp } from '../../utils/internalPropUtils.js';
|
|
12
|
+
|
|
13
|
+
export class GraphUpsert {
|
|
20
14
|
constructor({ rootModelClass, objects, upsertOptions, insertOnly = false }) {
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
// Wrap `undefined` too, so that it fails the check:
|
|
16
|
+
for (const obj of Array.isArray(objects) ? objects : [objects]) {
|
|
17
|
+
if (!isObject(obj)) {
|
|
18
|
+
throw createNotModelError(rootModelClass, obj);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
23
21
|
|
|
24
22
|
this.objects = rootModelClass.ensureModelArray(objects, GraphUpsert.modelOptions);
|
|
25
23
|
this.isArray = Array.isArray(objects);
|
|
26
|
-
this.
|
|
27
|
-
this.insertOnly = insertOnly;
|
|
24
|
+
this.graphOptions = new GraphOptions(upsertOptions, insertOnly);
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
static get modelOptions() {
|
|
@@ -36,482 +33,592 @@ class GraphUpsert {
|
|
|
36
33
|
// to. Even applying it to the root table only would be unsound: rows skipped
|
|
37
34
|
// by `ignore()` aren't returned, which leaves the root models without ids,
|
|
38
35
|
// so that related rows would be inserted with missing or wrong foreign keys.
|
|
39
|
-
|
|
40
|
-
// TODO: Throw an error instead in objection 4.0 (#53).
|
|
41
|
-
static warnOnConflict(builder, method) {
|
|
36
|
+
static checkForOnConflict(builder, method) {
|
|
42
37
|
if (builder.has(isOnConflictOperation)) {
|
|
43
|
-
|
|
38
|
+
throw new Error(
|
|
44
39
|
`onConflict(), ignore() and merge() are not supported by ${method}(). ` +
|
|
45
|
-
'Insert the conflicting rows with a separate insert() query instead.
|
|
46
|
-
'This will throw in objection 4.0.',
|
|
40
|
+
'Insert the conflicting rows with a separate insert() query instead.',
|
|
47
41
|
);
|
|
48
42
|
}
|
|
49
43
|
}
|
|
50
44
|
|
|
51
|
-
run(builder) {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
45
|
+
async run(builder) {
|
|
46
|
+
const { graphOptions } = this;
|
|
47
|
+
const nodes = GraphNode.buildGraph(builder.modelClass(), this.roots);
|
|
48
|
+
GraphNode.linkReferences(nodes);
|
|
49
|
+
const restoreMarkers = GraphNode.removeMarkers(nodes);
|
|
50
|
+
|
|
51
|
+
// A `#ref` to a model with an id is that model, also when its relations
|
|
52
|
+
// are loaded and upserted. Models inserted in a pass have their ids in the
|
|
53
|
+
// next ones:
|
|
54
|
+
const identifyReferences = (nodes) => {
|
|
55
|
+
for (const { obj, referencedNode } of nodes) {
|
|
56
|
+
if (referencedNode?.obj.$hasId() && !obj.$hasId()) {
|
|
57
|
+
obj.$id(referencedNode.obj.$id());
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.then(removeReferenceProps(graph.nodes))
|
|
64
|
-
.then(returnResult(this.objects, this.isArray));
|
|
65
|
-
}
|
|
66
|
-
}
|
|
62
|
+
for (const node of nodes) {
|
|
63
|
+
if (node.parent && node.isDbReference) {
|
|
64
|
+
node.relation.setRelateProp(node.obj, asArray(node.dbReference));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
identifyReferences(nodes);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
checkGraph(builder, new GraphPlan({ nodes, graphOptions }));
|
|
72
|
+
checkPlacements(builder, nodes);
|
|
73
|
+
checkReferenceCycles(nodes);
|
|
74
|
+
const writtenNodes = new Set();
|
|
75
|
+
let keptNodes = new Map();
|
|
76
|
+
let laterNodes = new Set();
|
|
77
|
+
|
|
78
|
+
// Rows that the graph relates into a HasManyRelation or HasOneRelation
|
|
79
|
+
// and gives relations are moved first, and their relations are upserted
|
|
80
|
+
// in another pass, against their rows as they are after the move (#214).
|
|
81
|
+
for (let passNodes = nodes; passNodes.length > 0;) {
|
|
82
|
+
const plan = new GraphPlan({
|
|
83
|
+
nodes: passNodes,
|
|
84
|
+
graphOptions,
|
|
85
|
+
keptNodes,
|
|
86
|
+
laterNodes,
|
|
87
|
+
});
|
|
88
|
+
const movedNodes = graphOptions.isInsertOnly() ? [] : await loadCurrentGraph(builder, plan);
|
|
89
|
+
|
|
90
|
+
plan.defer(movedNodes.flatMap((node) => node.descendants));
|
|
91
|
+
plan.cacheDecisions();
|
|
92
|
+
checkMissingModels(builder, plan);
|
|
93
|
+
plan.prune().plan();
|
|
94
|
+
checkChangedIds(builder, plan);
|
|
95
|
+
const [nextLaterNodes, keys] = deferLaterNodes(plan);
|
|
96
|
+
|
|
97
|
+
// A pass that only leaves its writes to the next one waits for itself:
|
|
98
|
+
if (plan.activeNodes.length === 0) {
|
|
99
|
+
throw new Error('the object graph contains cyclic references');
|
|
100
|
+
}
|
|
76
101
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
}
|
|
102
|
+
// Models that `noInsert` or `noRelate` ignore aren't written, but the
|
|
103
|
+
// ones marked with `#delete` or `#unrelate` keep their values:
|
|
104
|
+
for (const node of plan.activeNodes) {
|
|
105
|
+
if (plan.actions.get(node) !== 'ignore' || node.toBeUnrelatedOrDeleted) {
|
|
106
|
+
writtenNodes.add(node);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
86
109
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
}
|
|
110
|
+
checkReferences(builder, plan, writtenNodes);
|
|
111
|
+
checkJoinTables(builder, plan);
|
|
112
|
+
await new GraphWriter(new GraphSchedule(plan, keys)).write(builder);
|
|
113
|
+
keptNodes = plan.getNextKeptNodes();
|
|
94
114
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
// Moved rows may also wait for a later pass:
|
|
116
|
+
passNodes = [
|
|
117
|
+
...new Set(
|
|
118
|
+
[...movedNodes, ...nextLaterNodes].flatMap((node) => [node, ...node.descendants]),
|
|
119
|
+
),
|
|
120
|
+
];
|
|
121
|
+
laterNodes = new Set(nextLaterNodes);
|
|
122
|
+
identifyReferences(passNodes);
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
// The input can be retried as it was:
|
|
126
|
+
restoreMarkers();
|
|
127
|
+
throw err;
|
|
99
128
|
}
|
|
100
129
|
|
|
101
|
-
|
|
130
|
+
return this.roots;
|
|
102
131
|
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function createGraphData(builder, graphOptions, graph) {
|
|
106
|
-
const currentGraph = await fetchCurrentGraph(builder, graphOptions, graph);
|
|
107
132
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
graph,
|
|
111
|
-
graphOptions,
|
|
112
|
-
currentGraph,
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
return new GraphData({ graph, currentGraph, graphOptions, nodeDbExistence });
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function fetchCurrentGraph(builder, graphOptions, graph) {
|
|
119
|
-
if (graphOptions.isInsertOnly()) {
|
|
120
|
-
return Promise.resolve(ModelGraph.createEmpty());
|
|
121
|
-
} else {
|
|
122
|
-
return GraphFetcher.fetchCurrentGraph({ builder, graph, graphOptions });
|
|
133
|
+
get roots() {
|
|
134
|
+
return this.isArray ? this.objects : this.objects[0];
|
|
123
135
|
}
|
|
124
136
|
}
|
|
125
137
|
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return graphData;
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function pruneRelatedBranches(graphData) {
|
|
144
|
-
const relateNodes = graphData.graph.nodes.filter((node) => {
|
|
145
|
-
return (
|
|
146
|
-
!graphData.currentGraph.nodeForNode(node) &&
|
|
147
|
-
!graphData.graphOptions.shouldInsertIgnoreDisable(node, graphData)
|
|
148
|
-
);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
removeBranchesFromGraph(findRoots(relateNodes), graphData.graph);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Nothing is written below nodes that `noInsert` skips, as their descendants
|
|
155
|
-
// would be orphans (#215). Branches that references from outside point into
|
|
156
|
-
// are kept, since the references need their models.
|
|
157
|
-
function pruneSkippedBranches(graphData) {
|
|
158
|
-
const { graph, graphOptions } = graphData;
|
|
159
|
-
|
|
160
|
-
const skippedNodes = graph.nodes.filter((node) => {
|
|
161
|
-
return (
|
|
162
|
-
graphOptions.shouldInsertIgnoreDisable(node, graphData) &&
|
|
163
|
-
!graphOptions.shouldInsert(node, graphData) &&
|
|
164
|
-
!isReferencedFromOutside(node.descendantRelationNodes)
|
|
165
|
-
);
|
|
166
|
-
});
|
|
138
|
+
// Leave the writes that need keys or `#ref{}` values that only a later pass
|
|
139
|
+
// gives to that pass, with their branches, and return them with the keys of
|
|
140
|
+
// the rest.
|
|
141
|
+
function deferLaterNodes(plan) {
|
|
142
|
+
const laterNodes = [];
|
|
143
|
+
let keys = new GraphKeys(plan);
|
|
144
|
+
|
|
145
|
+
for (let nodes = keys.getLaterNodes(); nodes.length > 0; nodes = keys.getLaterNodes()) {
|
|
146
|
+
laterNodes.push(...nodes);
|
|
147
|
+
plan.defer([...plan.deferredNodes, ...nodes.flatMap((node) => [node, ...node.descendants])]);
|
|
148
|
+
keys = new GraphKeys(plan);
|
|
149
|
+
}
|
|
167
150
|
|
|
168
|
-
|
|
151
|
+
return [laterNodes, keys];
|
|
169
152
|
}
|
|
170
153
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
154
|
+
// Check the models with ids that are neither at their place nor related, once
|
|
155
|
+
// the current graph of a pass is loaded.
|
|
156
|
+
function checkMissingModels(builder, plan) {
|
|
157
|
+
for (const node of plan.nodes) {
|
|
158
|
+
if (plan.intentFor(node) === 'missing') {
|
|
159
|
+
const id = node.obj.$id();
|
|
160
|
+
|
|
161
|
+
throw node.modelClass.createNotFoundError(builder.context(), {
|
|
162
|
+
message: node.isMissing
|
|
163
|
+
? `model (id=${id}) does not exist. If you want to insert it with an id, use the insertMissing option`
|
|
164
|
+
: node.isRoot
|
|
165
|
+
? `root model (id=${id}) does not exist. If you want to insert it with an id, use the insertMissing option`
|
|
166
|
+
: `model (id=${id}) is not a child of model (id=${node.parent.obj.$id()}). If you want to relate it, use the relate option. If you want to insert it with an id, use the insertMissing option`,
|
|
167
|
+
dataPath: node.dataPath,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
177
171
|
}
|
|
178
172
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
173
|
+
// The graph writes keys of rows that can't change if they're part of the ids
|
|
174
|
+
// of the rows: the foreign key of a row that a HasMany or HasOne relate moves
|
|
175
|
+
// to another owner, and the foreign key of an existing row that its own
|
|
176
|
+
// BelongsToOne relate or insert points to another row, or that `null`,
|
|
177
|
+
// `#unrelate` or `#delete` clears. Keys that aren't known before the writes
|
|
178
|
+
// are compared only where they surely change, as new models get new keys.
|
|
179
|
+
function checkChangedIds(builder, plan) {
|
|
180
|
+
const { actions } = plan;
|
|
181
|
+
const newKey = {};
|
|
182
|
+
const getKey = (node, prop, i) =>
|
|
183
|
+
actions.get(node) === 'insert' && !node.obj.$hasId()
|
|
184
|
+
? newKey
|
|
185
|
+
: (prop.getProp(node.obj, i) ?? (node.existing && prop.getProp(node.existing.obj, i)));
|
|
186
|
+
|
|
187
|
+
const checkKey = (row, relation, rowProp, getNewKey) => {
|
|
188
|
+
const idProps = row.modelClass.getIdPropertyArray();
|
|
189
|
+
const isChanged =
|
|
190
|
+
actions.get(row) !== 'insert' &&
|
|
191
|
+
rowProp.props.some((prop, i) => {
|
|
192
|
+
const key = row.obj[prop];
|
|
193
|
+
const changedKey = getNewKey(i);
|
|
194
|
+
return (
|
|
195
|
+
idProps.includes(prop) &&
|
|
196
|
+
key !== undefined &&
|
|
197
|
+
changedKey !== undefined &&
|
|
198
|
+
(changedKey === newKey || !nonStrictEquals(key, changedKey))
|
|
199
|
+
);
|
|
200
|
+
});
|
|
184
201
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return false;
|
|
202
|
+
if (isChanged) {
|
|
203
|
+
throw builder.modelClass().createValidationError({
|
|
204
|
+
type: ValidationErrorType.InvalidGraph,
|
|
205
|
+
message: `the graph changes the key of the ${row.modelClass.name} at ${formatPath(row.dataPathKey)} through ${relation.name}, which is part of its id`,
|
|
206
|
+
});
|
|
191
207
|
}
|
|
208
|
+
};
|
|
192
209
|
|
|
193
|
-
|
|
194
|
-
const
|
|
210
|
+
for (const node of plan.activeNodes) {
|
|
211
|
+
const { relation, parent } = node;
|
|
212
|
+
const action = actions.get(node);
|
|
195
213
|
|
|
196
|
-
if (
|
|
197
|
-
|
|
214
|
+
if (relation?.keySide === 'related' && action === 'relate') {
|
|
215
|
+
checkKey(node.referencedNode || node, relation, relation.relatedProp, (i) =>
|
|
216
|
+
getKey(parent, relation.ownerProp, i),
|
|
217
|
+
);
|
|
218
|
+
} else if (relation?.keySide === 'owner' && relation.isOneToOne()) {
|
|
219
|
+
if (node.toBeUnrelatedOrDeleted) {
|
|
220
|
+
if (node.current) {
|
|
221
|
+
checkKey(parent, relation, relation.ownerProp, () => null);
|
|
222
|
+
}
|
|
223
|
+
} else if (action === 'relate' || action === 'insert') {
|
|
224
|
+
checkKey(parent, relation, relation.ownerProp, (i) =>
|
|
225
|
+
getKey(node.referencedNode || node, relation.relatedProp, i),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
198
228
|
}
|
|
199
229
|
|
|
200
|
-
|
|
201
|
-
|
|
230
|
+
// `null` clears the key:
|
|
231
|
+
const nullNames = node.modelClass.getRelationNames().filter((name) => node.obj[name] === null);
|
|
202
232
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Models marked with `#unrelate` or `#delete` are unrelated or deleted
|
|
208
|
-
// as a whole. Their relations are ignored, both in the upserted graph
|
|
209
|
-
// and in the current graph.
|
|
210
|
-
function pruneUnrelatedOrDeletedBranches(graphData) {
|
|
211
|
-
const { graph, currentGraph } = graphData;
|
|
212
|
-
|
|
213
|
-
const roots = findRoots(graph.nodes.filter((node) => node.toBeUnrelatedOrDeleted));
|
|
214
|
-
const currentRoots = roots.map((node) => currentGraph.nodeForNode(node)).filter(Boolean);
|
|
233
|
+
for (const name of nullNames) {
|
|
234
|
+
const nullRelation = node.modelClass.getRelation(name);
|
|
215
235
|
|
|
216
|
-
|
|
217
|
-
|
|
236
|
+
if (nullRelation.keySide === 'owner' && nullRelation.isOneToOne()) {
|
|
237
|
+
checkKey(node, nullRelation, nullRelation.ownerProp, () => null);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
218
241
|
}
|
|
219
242
|
|
|
220
|
-
|
|
221
|
-
|
|
243
|
+
// Check the whole graph before anything is written.
|
|
244
|
+
function checkGraph(builder, plan) {
|
|
245
|
+
const { nodes, graphOptions } = plan;
|
|
246
|
+
const modelClass = builder.modelClass();
|
|
247
|
+
const createInvalidGraphError = (message) =>
|
|
248
|
+
modelClass.createValidationError({ type: ValidationErrorType.InvalidGraph, message });
|
|
222
249
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
250
|
+
const allowedExpression = builder.allowedGraphExpression();
|
|
251
|
+
const rootObjs = nodes.filter((node) => node.isRoot).map((node) => node.obj);
|
|
252
|
+
|
|
253
|
+
if (
|
|
254
|
+
allowedExpression &&
|
|
255
|
+
!allowedExpression.isSubExpression(RelationExpression.fromModelGraph(rootObjs))
|
|
256
|
+
) {
|
|
257
|
+
throw modelClass.createValidationError({
|
|
258
|
+
type: ValidationErrorType.UnallowedRelation,
|
|
259
|
+
message: 'trying to upsert an unallowed relation',
|
|
260
|
+
});
|
|
261
|
+
}
|
|
226
262
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
263
|
+
if (
|
|
264
|
+
!graphOptions.allowRefs() &&
|
|
265
|
+
nodes.some((node) => node.isReference || node.propertyReferences.length > 0)
|
|
266
|
+
) {
|
|
267
|
+
throw createInvalidGraphError(
|
|
268
|
+
'#ref references are not allowed in a graph by default. see the allowRefs insert/upsert graph option',
|
|
269
|
+
);
|
|
270
|
+
}
|
|
231
271
|
|
|
232
|
-
|
|
233
|
-
|
|
272
|
+
// `#unrelate` and `#delete` markers come from the graph data, which may be
|
|
273
|
+
// untrusted input. Like `#ref`, they have to be allowed explicitly (#2410).
|
|
274
|
+
// Root models can't be marked even with `allowMarkers`, so report that first.
|
|
275
|
+
// An insert has nothing to unrelate or delete, so `insertGraph` rejects them.
|
|
276
|
+
const markedNode = nodes.find((node) => node.toBeUnrelatedOrDeleted);
|
|
234
277
|
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
}
|
|
278
|
+
if (markedNode) {
|
|
279
|
+
const { graphUnrelateProp, graphDeleteProp } = markedNode.modelClass;
|
|
280
|
+
const markers = `${graphUnrelateProp} and ${graphDeleteProp}`;
|
|
239
281
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
282
|
+
if (nodes.some((node) => node.isRoot && node.toBeUnrelatedOrDeleted)) {
|
|
283
|
+
throw createInvalidGraphError(
|
|
284
|
+
`${markers} can only be used for related models, not for root models`,
|
|
285
|
+
);
|
|
286
|
+
} else if (!graphOptions.allowMarkers()) {
|
|
287
|
+
throw createInvalidGraphError(
|
|
288
|
+
graphOptions.isInsertOnly()
|
|
289
|
+
? `${markers} can only be used in upsertGraph, not in insertGraph`
|
|
290
|
+
: `${markers} markers are not allowed in a graph by default. see the allowMarkers upsertGraph option`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
247
294
|
|
|
248
|
-
|
|
249
|
-
|
|
295
|
+
if (graphOptions.isInsertOnly()) {
|
|
296
|
+
const relatedNodes = nodes.filter((node) => plan.actionFor(node) === 'relate');
|
|
250
297
|
|
|
251
|
-
|
|
252
|
-
|
|
298
|
+
if (relatedNodes.some((node) => node.relation.keySide === 'related')) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
'You cannot relate HasManyRelation or HasOneRelation using insertGraph, because those require update operations. Consider using upsertGraph instead.',
|
|
301
|
+
);
|
|
302
|
+
}
|
|
253
303
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
304
|
+
// `upsertGraph()` upserts the branches below related models, but
|
|
305
|
+
// `insertGraph()` leaves them out, which would drop the new models there
|
|
306
|
+
// (#217). Models with ids below related models are taken as fetched models
|
|
307
|
+
// with loaded relations, and are ignored.
|
|
308
|
+
for (const node of relatedNodes) {
|
|
309
|
+
const newNode = node.descendants.find(
|
|
310
|
+
(descendant) => !descendant.hasId && plan.actionFor(descendant) === 'insert',
|
|
311
|
+
);
|
|
257
312
|
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
313
|
+
if (newNode) {
|
|
314
|
+
throw createInvalidGraphError(
|
|
315
|
+
`insertGraph cannot insert the new model at ${formatPath(newNode.dataPathKey)} below a related model. Insert it with a separate query or use upsertGraph instead`,
|
|
316
|
+
);
|
|
261
317
|
}
|
|
262
318
|
}
|
|
263
319
|
}
|
|
264
|
-
|
|
265
|
-
graph.nodes = graph.nodes.filter((node) => !nodesToRemove.has(node));
|
|
266
|
-
graph.edges = graph.edges.filter((edge) => !edgesToRemove.has(edge));
|
|
267
|
-
|
|
268
|
-
return graph;
|
|
269
320
|
}
|
|
270
321
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
322
|
+
// One row can't get two values for one column, and the slot of a
|
|
323
|
+
// HasOneRelation can't get two rows. A `#ref` or `#dbRef` model is a place of
|
|
324
|
+
// the row that it refers to. The keys that relations give a row are the values
|
|
325
|
+
// of the key props across them, or stand for these where the graph doesn't
|
|
326
|
+
// give them, e.g. for new models.
|
|
327
|
+
function checkPlacements(builder, nodes) {
|
|
328
|
+
const newRowIds = new Map();
|
|
329
|
+
const getRowId = (node) => {
|
|
330
|
+
const target = node.referencedNode || node;
|
|
331
|
+
|
|
332
|
+
if (target.obj.$hasId()) {
|
|
333
|
+
return target.rowKey;
|
|
334
|
+
} else {
|
|
335
|
+
if (!newRowIds.has(target)) {
|
|
336
|
+
newRowIds.set(target, `new:${newRowIds.size}`);
|
|
337
|
+
}
|
|
285
338
|
|
|
286
|
-
|
|
339
|
+
return newRowIds.get(target);
|
|
340
|
+
}
|
|
287
341
|
};
|
|
288
|
-
}
|
|
289
342
|
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
) {
|
|
308
|
-
relation.assertCanModifyJoinTable(
|
|
309
|
-
builder,
|
|
310
|
-
graphOptions.shouldRelate(node, graphData) ? 'relate' : 'insert',
|
|
311
|
-
);
|
|
312
|
-
}
|
|
343
|
+
// The values that the graph gives each row, by prop, and the model classes
|
|
344
|
+
// and relations through which the graph reaches the rows:
|
|
345
|
+
const rows = new Map();
|
|
346
|
+
const rowTables = new Map();
|
|
347
|
+
const rowRelations = new Map();
|
|
348
|
+
const setValue = (rowId, name, prop, value, path) => {
|
|
349
|
+
const values = rows.get(rowId) || new Map();
|
|
350
|
+
const other = values.get(prop);
|
|
351
|
+
rows.set(rowId, values);
|
|
352
|
+
|
|
353
|
+
if (!other) {
|
|
354
|
+
values.set(prop, { value, path });
|
|
355
|
+
} else if (!nonStrictEquals(other.value, value)) {
|
|
356
|
+
throw builder.modelClass().createValidationError({
|
|
357
|
+
type: ValidationErrorType.InvalidGraph,
|
|
358
|
+
message: `the graph gives ${prop} of one ${name} two values, at ${formatPath(other.path)} and at ${formatPath(path)}`,
|
|
359
|
+
});
|
|
313
360
|
}
|
|
314
|
-
|
|
315
|
-
return graphData;
|
|
316
361
|
};
|
|
317
|
-
|
|
362
|
+
const setNodeValue = (node, prop, value, path) =>
|
|
363
|
+
setValue(getRowId(node), node.modelClass.name, prop, value, path);
|
|
364
|
+
|
|
365
|
+
// The join row of the pair of models that `node` and its parent are, the
|
|
366
|
+
// same through both relations of the join table:
|
|
367
|
+
const getJoinRowId = (node) =>
|
|
368
|
+
[
|
|
369
|
+
node.relation.joinTable,
|
|
370
|
+
...[
|
|
371
|
+
[node.relation.joinTableOwnerProp.cols, getRowId(node.parent)],
|
|
372
|
+
[node.relation.joinTableRelatedProp.cols, getRowId(node)],
|
|
373
|
+
]
|
|
374
|
+
.sort(([cols1], [cols2]) => cols1.join().localeCompare(cols2.join()))
|
|
375
|
+
.map(([cols, rowId]) => `${cols}=${rowId}`),
|
|
376
|
+
].join(':');
|
|
377
|
+
|
|
378
|
+
const getPath = (node, ...keys) => [node.dataPathKey, ...keys].filter(Boolean).join('.');
|
|
379
|
+
const getRelations = (node) =>
|
|
380
|
+
node.modelClass
|
|
381
|
+
.getRelationNames()
|
|
382
|
+
.filter((name) => node.obj[name] !== undefined)
|
|
383
|
+
.map((name) => node.modelClass.getRelation(name));
|
|
384
|
+
const isOwnerKey = (relation) => relation.keySide === 'owner' && relation.isOneToOne();
|
|
385
|
+
const isSlot = (relation) => relation.keySide === 'related' && relation.isOneToOne();
|
|
386
|
+
|
|
387
|
+
// The props that the models give, except for the ids and the keys that the
|
|
388
|
+
// relations of the same model set, which follow these relations:
|
|
389
|
+
for (const node of nodes) {
|
|
390
|
+
const { modelClass, obj, relation } = node;
|
|
391
|
+
const skippedProps = [
|
|
392
|
+
...modelClass.getRelationNames(),
|
|
393
|
+
...modelClass.getIdPropertyArray(),
|
|
394
|
+
...(relation?.keySide === 'related' ? relation.relatedProp.props : []),
|
|
395
|
+
// The extras of join rows belong to the join rows:
|
|
396
|
+
...(relation?.keySide === 'join' ? relation.joinTableExtras.map((e) => e.aliasProp) : []),
|
|
397
|
+
...getRelations(node)
|
|
398
|
+
.filter(isOwnerKey)
|
|
399
|
+
.flatMap((relation) => relation.ownerProp.props),
|
|
400
|
+
];
|
|
401
|
+
|
|
402
|
+
const rowId = getRowId(node);
|
|
403
|
+
rowTables.set(rowId, modelClass.getTableName());
|
|
404
|
+
rowRelations.set(rowId, (rowRelations.get(rowId) || new Set()).add(relation));
|
|
405
|
+
|
|
406
|
+
if (!node.toBeUnrelatedOrDeleted) {
|
|
407
|
+
for (const prop of Object.keys(obj)) {
|
|
408
|
+
if (obj[prop] !== undefined && !isInternalProp(prop) && !skippedProps.includes(prop)) {
|
|
409
|
+
setNodeValue(node, prop, obj[prop], getPath(node, prop));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
318
413
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
!node.toBeUnrelatedOrDeleted &&
|
|
334
|
-
!graphOptions.shouldInsertIgnoreDisable(node, graphData) &&
|
|
335
|
-
!graphOptions.shouldRelateIgnoreDisable(node, graphData) &&
|
|
336
|
-
!currentGraph.nodeForNode(node)
|
|
337
|
-
) {
|
|
338
|
-
if (!node.parentNode) {
|
|
339
|
-
throw node.modelClass.createNotFoundError(builder.context(), {
|
|
340
|
-
message: `root model (id=${node.obj.$id()}) does not exist. If you want to insert it with an id, use the insertMissing option`,
|
|
341
|
-
dataPath: node.dataPath,
|
|
342
|
-
});
|
|
343
|
-
} else {
|
|
344
|
-
throw node.modelClass.createNotFoundError(builder.context(), {
|
|
345
|
-
message: `model (id=${node.obj.$id()}) is not a child of model (id=${node.parentNode.obj.$id()}). If you want to relate it, use the relate option. If you want to insert it with an id, use the insertMissing option`,
|
|
346
|
-
dataPath: node.dataPath,
|
|
347
|
-
});
|
|
414
|
+
// A pair of models has one join row, which gets the extras of all places
|
|
415
|
+
// of the pair:
|
|
416
|
+
if (relation?.keySide === 'join' && !node.toBeUnrelatedOrDeleted) {
|
|
417
|
+
for (const { aliasProp } of relation.joinTableExtras) {
|
|
418
|
+
if (obj[aliasProp] !== undefined) {
|
|
419
|
+
const path = getPath(node, aliasProp);
|
|
420
|
+
setValue(
|
|
421
|
+
getJoinRowId(node),
|
|
422
|
+
`join row of ${relation.name}`,
|
|
423
|
+
aliasProp,
|
|
424
|
+
obj[aliasProp],
|
|
425
|
+
path,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
348
428
|
}
|
|
349
429
|
}
|
|
350
430
|
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function checkForUnallowedRelationErrors(graphData, builder) {
|
|
354
|
-
const { graph } = graphData;
|
|
355
|
-
const allowedExpression = builder.allowedGraphExpression();
|
|
356
431
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
432
|
+
// The value of the key prop `i` of `prop` that `node` gives across a
|
|
433
|
+
// relation, from wherever the graph gives the row, or one that stands for it:
|
|
434
|
+
const getKey = (node, prop, i) =>
|
|
435
|
+
prop.getProp(node.obj, i) ??
|
|
436
|
+
rows.get(getRowId(node))?.get(prop.props[i])?.value ?? {
|
|
437
|
+
row: getRowId(node),
|
|
438
|
+
prop: prop.props[i],
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// The keys that the relations give the rows, the rows that the graph puts
|
|
442
|
+
// in the slots of HasOneRelations, and whether the rows are deleted:
|
|
443
|
+
const slots = [];
|
|
444
|
+
for (const node of nodes) {
|
|
445
|
+
const { relation, parent } = node;
|
|
446
|
+
const path = getPath(node);
|
|
447
|
+
const isUnrelated = node.toBeUnrelated;
|
|
448
|
+
const isDeleted = node.toBeDeleted && !isUnrelated;
|
|
449
|
+
|
|
450
|
+
if (relation?.keySide === 'related') {
|
|
451
|
+
// A row that lists itself on the same column restates its own key:
|
|
452
|
+
const isSelfLink =
|
|
453
|
+
getRowId(node) === getRowId(parent) &&
|
|
454
|
+
hasSameColumns(relation.relatedProp, relation.ownerProp);
|
|
455
|
+
|
|
456
|
+
if (!isDeleted && !isSelfLink) {
|
|
457
|
+
relation.relatedProp.forEach((i) => {
|
|
458
|
+
const key = isUnrelated ? null : getKey(parent, relation.ownerProp, i);
|
|
459
|
+
setNodeValue(node, relation.relatedProp.props[i], key, path);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
360
462
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
463
|
+
if (isSlot(relation)) {
|
|
464
|
+
const row = node.toBeUnrelatedOrDeleted ? null : getRowId(node);
|
|
465
|
+
slots.push({ owner: parent, relation, row, path });
|
|
466
|
+
}
|
|
467
|
+
} else if (relation && isOwnerKey(relation)) {
|
|
468
|
+
relation.ownerProp.forEach((i) => {
|
|
469
|
+
const key = node.toBeUnrelatedOrDeleted ? null : getKey(node, relation.relatedProp, i);
|
|
470
|
+
setNodeValue(parent, relation.ownerProp.props[i], key, path);
|
|
365
471
|
});
|
|
366
472
|
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
473
|
|
|
370
|
-
|
|
371
|
-
const { graph, graphOptions } = graphData;
|
|
474
|
+
setNodeValue(node, '#delete', isDeleted, path);
|
|
372
475
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
throw builder.modelClass().createValidationError({
|
|
379
|
-
type: ValidationErrorType.InvalidGraph,
|
|
380
|
-
message:
|
|
381
|
-
'#ref references are not allowed in a graph by default. see the allowRefs insert/upsert graph option',
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// `#unrelate` and `#delete` markers come from the graph data, which may be
|
|
387
|
-
// untrusted input. Like `#ref`, they have to be allowed explicitly (#2410).
|
|
388
|
-
// An insert has nothing to unrelate or delete, so `insertGraph` rejects them.
|
|
389
|
-
function checkForUnallowedMarkerErrors(graphData, builder) {
|
|
390
|
-
const { graph, graphOptions } = graphData;
|
|
391
|
-
const isInsertOnly = graphOptions.isInsertOnly();
|
|
476
|
+
// All places of a pair of models keep or remove its join row:
|
|
477
|
+
if (relation?.keySide === 'join') {
|
|
478
|
+
const name = `join row of ${relation.name}`;
|
|
479
|
+
setValue(getJoinRowId(node), name, '#unrelate', node.toBeUnrelatedOrDeleted, path);
|
|
480
|
+
}
|
|
392
481
|
|
|
393
|
-
|
|
394
|
-
|
|
482
|
+
// `null` clears the key or the slot:
|
|
483
|
+
for (const nullRelation of getRelations(node).filter(({ name }) => node.obj[name] === null)) {
|
|
484
|
+
if (isOwnerKey(nullRelation)) {
|
|
485
|
+
nullRelation.ownerProp.forEach((i) => {
|
|
486
|
+
setNodeValue(
|
|
487
|
+
node,
|
|
488
|
+
nullRelation.ownerProp.props[i],
|
|
489
|
+
null,
|
|
490
|
+
getPath(node, nullRelation.name),
|
|
491
|
+
);
|
|
492
|
+
});
|
|
493
|
+
} else if (isSlot(nullRelation)) {
|
|
494
|
+
const path = getPath(node, nullRelation.name);
|
|
495
|
+
slots.push({ owner: node, relation: nullRelation, row: null, path });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
395
498
|
}
|
|
396
499
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
500
|
+
// The rows of a table by the values that the graph gives the props of a key,
|
|
501
|
+
// with the values that stand for keys as they are and the others as strings,
|
|
502
|
+
// so that e.g. `1` and `'1'` are the same key:
|
|
503
|
+
const toIndexKey = (values) =>
|
|
504
|
+
JSON.stringify(values.map((value) => (isObject(value) ? value : String(value))));
|
|
505
|
+
const indexes = new Map();
|
|
506
|
+
const getRowsByKey = (table, props) => {
|
|
507
|
+
const indexName = `${table}:${props.join()}`;
|
|
508
|
+
let index = indexes.get(indexName);
|
|
509
|
+
|
|
510
|
+
if (!index) {
|
|
511
|
+
index = new Map();
|
|
512
|
+
indexes.set(indexName, index);
|
|
513
|
+
|
|
514
|
+
for (const [rowId, values] of rows) {
|
|
515
|
+
const keyValues = props.map((prop) => values.get(prop)?.value);
|
|
516
|
+
|
|
517
|
+
if (rowTables.get(rowId) === table && keyValues.every((value) => value != null)) {
|
|
518
|
+
const indexKey = toIndexKey(keyValues);
|
|
519
|
+
const rowIds = index.get(indexKey);
|
|
520
|
+
|
|
521
|
+
if (rowIds) {
|
|
522
|
+
rowIds.push(rowId);
|
|
523
|
+
} else {
|
|
524
|
+
index.set(indexKey, [rowId]);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
401
529
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
message: isInsertOnly
|
|
405
|
-
? `${graphUnrelateProp} and ${graphDeleteProp} can only be used in upsertGraph, not in insertGraph`
|
|
406
|
-
: `${graphUnrelateProp} and ${graphDeleteProp} markers are not allowed in a graph by default. see the allowMarkers upsertGraph option`,
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
}
|
|
530
|
+
return index;
|
|
531
|
+
};
|
|
410
532
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
533
|
+
// Any other row that the graph gives the key of the owner of a slot is a
|
|
534
|
+
// second row in it, unless the relation filters its rows and the graph
|
|
535
|
+
// doesn't reach the row through it:
|
|
536
|
+
for (const { owner, relation, row, path } of slots) {
|
|
537
|
+
const { relatedModelClass, relatedProp, ownerProp } = relation;
|
|
538
|
+
const ownerKey = ownerProp.props.map((_, i) => getKey(owner, ownerProp, i));
|
|
539
|
+
const rowsByKey = getRowsByKey(relatedModelClass.getTableName(), relatedProp.props);
|
|
540
|
+
const otherRowId = (rowsByKey.get(toIndexKey(ownerKey)) || []).find(
|
|
541
|
+
(rowId) => rowId !== row && (!relation.modify || rowRelations.get(rowId).has(relation)),
|
|
542
|
+
);
|
|
415
543
|
|
|
544
|
+
if (otherRowId) {
|
|
545
|
+
const other = rows.get(otherRowId).get(relatedProp.props[0]);
|
|
416
546
|
throw builder.modelClass().createValidationError({
|
|
417
547
|
type: ValidationErrorType.InvalidGraph,
|
|
418
|
-
message:
|
|
548
|
+
message: `the graph puts two ${relatedModelClass.name} rows in ${relation.name} of one ${owner.modelClass.name}, at ${formatPath(path)} and at ${formatPath(other.path)}`,
|
|
419
549
|
});
|
|
420
550
|
}
|
|
421
551
|
}
|
|
422
552
|
}
|
|
423
553
|
|
|
424
|
-
|
|
425
|
-
|
|
554
|
+
// References need the ids of the models they point to, which the database
|
|
555
|
+
// only gives the models that the graph writes in the same pass or an earlier
|
|
556
|
+
// one. The ids of new models that the graph doesn't write, e.g. as `noInsert`
|
|
557
|
+
// skips them, are never known. Everything else reads the models as the graph
|
|
558
|
+
// gives them. The references of models that aren't written are never resolved.
|
|
559
|
+
function checkReferences(builder, plan, writtenNodes) {
|
|
560
|
+
const isReadable = (node, refPath) =>
|
|
561
|
+
writtenNodes.has(node) ||
|
|
562
|
+
node.obj.$hasId() ||
|
|
563
|
+
(!!refPath && !node.modelClass.getIdPropertyArray().includes(refPath[0]));
|
|
564
|
+
|
|
565
|
+
for (const node of plan.activeNodes.filter((node) => writtenNodes.has(node))) {
|
|
566
|
+
const { referencedNode, propertyReferences } = node;
|
|
426
567
|
|
|
427
|
-
for (const node of graph.nodes) {
|
|
428
568
|
if (
|
|
429
|
-
|
|
430
|
-
|
|
569
|
+
(referencedNode && !isReadable(referencedNode)) ||
|
|
570
|
+
propertyReferences.some(({ referencedNode, refPath }) => !isReadable(referencedNode, refPath))
|
|
431
571
|
) {
|
|
432
|
-
throw
|
|
433
|
-
|
|
434
|
-
|
|
572
|
+
throw builder.modelClass().createValidationError({
|
|
573
|
+
type: ValidationErrorType.InvalidGraph,
|
|
574
|
+
message: `the reference at ${formatPath(node.dataPathKey)} points to a model that the graph doesn't write before it`,
|
|
575
|
+
});
|
|
435
576
|
}
|
|
436
577
|
}
|
|
437
578
|
}
|
|
438
579
|
|
|
439
|
-
// `
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function executeOperations(builder) {
|
|
464
|
-
return (graphData) => {
|
|
465
|
-
const operations = graphData.graphOptions.isInsertOnly()
|
|
466
|
-
? [GraphInsert]
|
|
467
|
-
: [
|
|
468
|
-
GraphDelete,
|
|
469
|
-
GraphInsert,
|
|
470
|
-
GraphPatch,
|
|
471
|
-
GraphDereferencedBelongsToOneDelete,
|
|
472
|
-
GraphRecursiveUpsert,
|
|
473
|
-
];
|
|
474
|
-
|
|
475
|
-
// Create all actions before chaining the first ones, which would run even
|
|
476
|
-
// if creating the actions of a later operation throws, e.g. for a cycle.
|
|
477
|
-
const operationActions = operations.map((Operation) =>
|
|
478
|
-
new Operation(graphData).createActions(),
|
|
479
|
-
);
|
|
480
|
-
|
|
481
|
-
return operationActions.reduce(
|
|
482
|
-
(promise, actions) => promise.then(() => executeActions(builder, actions)),
|
|
483
|
-
Promise.resolve(),
|
|
484
|
-
);
|
|
580
|
+
// A `#ref{}` waits for the model it reads if the value only exists once that
|
|
581
|
+
// model is written. These waits follow from the graph alone, so their cycles
|
|
582
|
+
// are found across all passes, before the first write.
|
|
583
|
+
function checkReferenceCycles(nodes) {
|
|
584
|
+
const getWaitedNodes = (node) =>
|
|
585
|
+
node.propertyReferences
|
|
586
|
+
.filter((reference) => reference.referencedNode !== node && readsWrittenValue(reference))
|
|
587
|
+
.map(({ referencedNode }) => referencedNode);
|
|
588
|
+
const visitingNodes = new Set();
|
|
589
|
+
const visitedNodes = new Set();
|
|
590
|
+
|
|
591
|
+
const visit = (node) => {
|
|
592
|
+
if (visitingNodes.has(node)) {
|
|
593
|
+
throw new Error('the object graph contains cyclic references');
|
|
594
|
+
} else if (!visitedNodes.has(node)) {
|
|
595
|
+
visitingNodes.add(node);
|
|
596
|
+
getWaitedNodes(node).forEach(visit);
|
|
597
|
+
visitingNodes.delete(node);
|
|
598
|
+
visitedNodes.add(node);
|
|
599
|
+
}
|
|
485
600
|
};
|
|
486
|
-
}
|
|
487
601
|
|
|
488
|
-
|
|
489
|
-
return actions.reduce(
|
|
490
|
-
(promise, action) => promise.then(() => action.run(builder)),
|
|
491
|
-
Promise.resolve(),
|
|
492
|
-
);
|
|
602
|
+
nodes.forEach(visit);
|
|
493
603
|
}
|
|
494
604
|
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
function
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
delete node.obj[node.modelClass.uidRefProp];
|
|
502
|
-
delete node.obj[node.modelClass.dbRefProp];
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
}
|
|
605
|
+
// Inserting or relating a model of a many-to-many relation inserts a join row,
|
|
606
|
+
// which would be a new related row if the join table is also the related table
|
|
607
|
+
// (#133).
|
|
608
|
+
function checkJoinTables(builder, plan) {
|
|
609
|
+
for (const node of plan.activeNodes) {
|
|
610
|
+
const action = plan.actions.get(node);
|
|
506
611
|
|
|
507
|
-
|
|
508
|
-
|
|
612
|
+
if (
|
|
613
|
+
node.relation?.keySide === 'join' &&
|
|
614
|
+
!node.current &&
|
|
615
|
+
(action === 'insert' || action === 'relate')
|
|
616
|
+
) {
|
|
617
|
+
node.relation.assertCanModifyJoinTable(builder, action);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
509
620
|
}
|
|
510
621
|
|
|
511
|
-
function
|
|
512
|
-
return
|
|
622
|
+
function isOnConflictOperation(operation) {
|
|
623
|
+
return ['onConflict', 'ignore', 'merge'].includes(operation.name);
|
|
513
624
|
}
|
|
514
|
-
|
|
515
|
-
module.exports = {
|
|
516
|
-
GraphUpsert,
|
|
517
|
-
};
|