objection 3.9.1 → 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.
Files changed (248) hide show
  1. package/lib/dbErrors/LICENSE +21 -0
  2. package/lib/dbErrors/errorCodes/mssql.js +19 -0
  3. package/lib/dbErrors/errorCodes/mysql.js +805 -0
  4. package/lib/dbErrors/errorCodes/postgres.js +241 -0
  5. package/lib/dbErrors/errors/CheckViolationError.js +10 -0
  6. package/lib/dbErrors/errors/ConstraintViolationError.js +3 -0
  7. package/lib/dbErrors/errors/DBError.js +9 -0
  8. package/lib/dbErrors/errors/DataError.js +3 -0
  9. package/lib/dbErrors/errors/ForeignKeyViolationError.js +11 -0
  10. package/lib/dbErrors/errors/NotNullViolationError.js +12 -0
  11. package/lib/dbErrors/errors/UniqueViolationError.js +12 -0
  12. package/lib/dbErrors/index.js +59 -0
  13. package/lib/dbErrors/parsers/mssql/DBError/CheckViolationError/parser.js +28 -0
  14. package/lib/dbErrors/parsers/mssql/DBError/DataError/parser.js +25 -0
  15. package/lib/dbErrors/parsers/mssql/DBError/ForeignKeyViolationError/parser.js +39 -0
  16. package/lib/dbErrors/parsers/mssql/DBError/NotNullViolationError/parser.js +30 -0
  17. package/lib/dbErrors/parsers/mssql/DBError/UniqueViolationError/parser.js +53 -0
  18. package/lib/dbErrors/parsers/mssql/DBError/parser.js +51 -0
  19. package/lib/dbErrors/parsers/mssql/DBError/util.js +16 -0
  20. package/lib/dbErrors/parsers/mysql/DBError/ConstraintViolationError/ForeignKeyViolationError/parser.js +43 -0
  21. package/lib/dbErrors/parsers/mysql/DBError/ConstraintViolationError/NotNullViolationError/parser.js +29 -0
  22. package/lib/dbErrors/parsers/mysql/DBError/ConstraintViolationError/UniqueViolationError/parser.js +27 -0
  23. package/lib/dbErrors/parsers/mysql/DBError/ConstraintViolationError/parser.js +23 -0
  24. package/lib/dbErrors/parsers/mysql/DBError/DataError/parser.js +24 -0
  25. package/lib/dbErrors/parsers/mysql/DBError/parser.js +28 -0
  26. package/lib/dbErrors/parsers/postgres/DBError/ConstraintViolationError/CheckViolationError/parser.js +18 -0
  27. package/lib/dbErrors/parsers/postgres/DBError/ConstraintViolationError/ForeignKeyViolationError/parser.js +18 -0
  28. package/lib/dbErrors/parsers/postgres/DBError/ConstraintViolationError/NotNullViolationError/parser.js +18 -0
  29. package/lib/dbErrors/parsers/postgres/DBError/ConstraintViolationError/UniqueViolationError/parser.js +40 -0
  30. package/lib/dbErrors/parsers/postgres/DBError/ConstraintViolationError/parser.js +25 -0
  31. package/lib/dbErrors/parsers/postgres/DBError/DataError/parser.js +16 -0
  32. package/lib/dbErrors/parsers/postgres/DBError/parser.js +27 -0
  33. package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/CheckViolationError/parser.js +20 -0
  34. package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/ForeignKeyViolationError/parser.js +20 -0
  35. package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/NotNullViolationError/parser.js +27 -0
  36. package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/UniqueViolationError/parser.js +28 -0
  37. package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/parser.js +24 -0
  38. package/lib/dbErrors/parsers/sqlite/DBError/parser.js +23 -0
  39. package/lib/dbErrors/utils/sqlState.js +3 -0
  40. package/lib/initialize.js +1 -7
  41. package/lib/model/AjvValidator.js +51 -117
  42. package/lib/model/Model.js +84 -59
  43. package/lib/model/ModifierNotFoundError.js +1 -7
  44. package/lib/model/NotFoundError.js +1 -7
  45. package/lib/model/RelationDoesNotExistError.js +1 -7
  46. package/lib/model/ValidationError.js +3 -10
  47. package/lib/model/Validator.js +1 -7
  48. package/lib/model/getModel.js +7 -6
  49. package/lib/model/inheritModel.js +1 -7
  50. package/lib/model/modelBindKnex.js +4 -10
  51. package/lib/model/modelClone.js +5 -11
  52. package/lib/model/modelColPropMap.js +3 -10
  53. package/lib/model/modelId.js +3 -10
  54. package/lib/model/modelJsonAttributes.js +35 -16
  55. package/lib/model/modelParseRelations.js +2 -8
  56. package/lib/model/modelQueryProps.js +11 -23
  57. package/lib/model/modelSet.js +89 -41
  58. package/lib/model/modelTableMetadata.js +4 -11
  59. package/lib/model/modelToJson.js +6 -13
  60. package/lib/model/modelUtils.js +14 -13
  61. package/lib/model/modelValidate.js +3 -9
  62. package/lib/model/modelValues.js +5 -14
  63. package/lib/model/modelVisitor.js +1 -7
  64. package/lib/objection.js +33 -71
  65. package/lib/queryBuilder/FunctionBuilder.js +4 -11
  66. package/lib/queryBuilder/InternalOptions.js +4 -9
  67. package/lib/queryBuilder/JoinBuilder.js +5 -8
  68. package/lib/queryBuilder/QueryBuilder.js +126 -95
  69. package/lib/queryBuilder/QueryBuilderBase.js +41 -123
  70. package/lib/queryBuilder/QueryBuilderContext.js +2 -8
  71. package/lib/queryBuilder/QueryBuilderContextBase.js +2 -8
  72. package/lib/queryBuilder/QueryBuilderOperationSupport.js +7 -21
  73. package/lib/queryBuilder/QueryBuilderUserContext.js +1 -7
  74. package/lib/queryBuilder/RawBuilder.js +5 -13
  75. package/lib/queryBuilder/ReferenceBuilder.js +20 -48
  76. package/lib/queryBuilder/RelationExpression.js +18 -16
  77. package/lib/queryBuilder/StaticHookArguments.js +74 -16
  78. package/lib/queryBuilder/ValueBuilder.js +11 -13
  79. package/lib/queryBuilder/getJoinBuilder.js +10 -0
  80. package/lib/queryBuilder/graph/GraphLoader.js +304 -0
  81. package/lib/queryBuilder/graph/GraphNode.js +235 -0
  82. package/lib/queryBuilder/graph/GraphOptions.js +55 -248
  83. package/lib/queryBuilder/graph/GraphPlan.js +395 -0
  84. package/lib/queryBuilder/graph/GraphUpsert.js +466 -414
  85. package/lib/queryBuilder/graph/GraphWriter.js +1263 -0
  86. package/lib/queryBuilder/join/JoinResultColumn.js +1 -7
  87. package/lib/queryBuilder/join/JoinResultParser.js +6 -11
  88. package/lib/queryBuilder/join/RelationJoiner.js +66 -84
  89. package/lib/queryBuilder/join/TableNode.js +2 -8
  90. package/lib/queryBuilder/join/TableTree.js +3 -9
  91. package/lib/queryBuilder/join/utils.js +5 -15
  92. package/lib/queryBuilder/jsonDialects/JsonDialect.js +51 -0
  93. package/lib/queryBuilder/jsonDialects/MySqlJsonDialect.js +81 -0
  94. package/lib/queryBuilder/jsonDialects/PostgresJsonDialect.js +45 -0
  95. package/lib/queryBuilder/jsonDialects/SqliteJsonDialect.js +67 -0
  96. package/lib/queryBuilder/jsonDialects/index.js +44 -0
  97. package/lib/queryBuilder/operations/DelegateOperation.js +2 -8
  98. package/lib/queryBuilder/operations/DeleteOperation.js +10 -39
  99. package/lib/queryBuilder/operations/FindByIdOperation.js +7 -12
  100. package/lib/queryBuilder/operations/FindByIdsOperation.js +4 -8
  101. package/lib/queryBuilder/operations/FindOperation.js +14 -34
  102. package/lib/queryBuilder/operations/FirstOperation.js +6 -10
  103. package/lib/queryBuilder/operations/FromOperation.js +3 -9
  104. package/lib/queryBuilder/operations/InsertAndFetchOperation.js +13 -12
  105. package/lib/queryBuilder/operations/InsertGraphAndFetchOperation.js +4 -10
  106. package/lib/queryBuilder/operations/InsertGraphOperation.js +7 -18
  107. package/lib/queryBuilder/operations/InsertOperation.js +118 -45
  108. package/lib/queryBuilder/operations/InstanceDeleteOperation.js +5 -12
  109. package/lib/queryBuilder/operations/InstanceFindOperation.js +4 -11
  110. package/lib/queryBuilder/operations/InstanceInsertOperation.js +2 -8
  111. package/lib/queryBuilder/operations/InstanceUpdateOperation.js +7 -14
  112. package/lib/queryBuilder/operations/JoinRelatedOperation.js +5 -11
  113. package/lib/queryBuilder/operations/KnexOperation.js +2 -8
  114. package/lib/queryBuilder/operations/LimitPerParentOperation.js +7 -69
  115. package/lib/queryBuilder/operations/MergeOperation.js +5 -11
  116. package/lib/queryBuilder/operations/NoneOperation.js +2 -8
  117. package/lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js +20 -49
  118. package/lib/queryBuilder/operations/OnBuildKnexOperation.js +2 -8
  119. package/lib/queryBuilder/operations/OnBuildOperation.js +2 -8
  120. package/lib/queryBuilder/operations/OnErrorOperation.js +2 -8
  121. package/lib/queryBuilder/operations/QueryBuilderOperation.js +1 -7
  122. package/lib/queryBuilder/operations/RangeOperation.js +11 -26
  123. package/lib/queryBuilder/operations/RelateOperation.js +2 -8
  124. package/lib/queryBuilder/operations/ReturningOperation.js +3 -9
  125. package/lib/queryBuilder/operations/RunAfterOperation.js +2 -12
  126. package/lib/queryBuilder/operations/RunBeforeOperation.js +2 -8
  127. package/lib/queryBuilder/operations/UnrelateOperation.js +2 -8
  128. package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +14 -17
  129. package/lib/queryBuilder/operations/UpdateOperation.js +32 -132
  130. package/lib/queryBuilder/operations/UpsertGraphAndFetchOperation.js +8 -11
  131. package/lib/queryBuilder/operations/UpsertGraphOperation.js +6 -12
  132. package/lib/queryBuilder/operations/WhereCompositeOperation.js +3 -9
  133. package/lib/queryBuilder/operations/WhereGroupOperation.js +2 -8
  134. package/lib/queryBuilder/operations/eager/EagerOperation.js +3 -9
  135. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +72 -10
  136. package/lib/queryBuilder/operations/eager/NaiveEagerOperation.js +2 -8
  137. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +23 -44
  138. package/lib/queryBuilder/operations/fieldExpressions.js +122 -0
  139. package/lib/queryBuilder/operations/jsonApi/WhereJsonHasOperation.js +20 -0
  140. package/lib/queryBuilder/operations/jsonApi/WhereJsonOperation.js +44 -0
  141. package/lib/queryBuilder/operations/jsonApi/WhereJsonTypeOperation.js +14 -0
  142. package/lib/queryBuilder/operations/select/SelectOperation.js +11 -21
  143. package/lib/queryBuilder/operations/select/Selection.js +19 -17
  144. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +4 -10
  145. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeOperation.js +4 -10
  146. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +4 -10
  147. package/lib/queryBuilder/operations/windowOrder.js +56 -0
  148. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +3 -7
  149. package/lib/queryBuilder/parsers/relationExpressionParser.js +42 -43
  150. package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +39 -36
  151. package/lib/queryBuilder/transformations/CompositeQueryTransformation.js +2 -8
  152. package/lib/queryBuilder/transformations/QueryTransformation.js +1 -7
  153. package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +3 -9
  154. package/lib/queryBuilder/transformations/index.js +3 -11
  155. package/lib/queryBuilder/whereGroups.js +9 -24
  156. package/lib/queryBuilder/withoutOrder.js +4 -11
  157. package/lib/relations/Relation.js +47 -28
  158. package/lib/relations/RelationDeleteOperation.js +4 -14
  159. package/lib/relations/RelationFindOperation.js +119 -90
  160. package/lib/relations/RelationInsertOperation.js +2 -8
  161. package/lib/relations/RelationOwner.js +5 -11
  162. package/lib/relations/RelationProperty.js +4 -10
  163. package/lib/relations/RelationUpdateOperation.js +4 -14
  164. package/lib/relations/RelationWriteResultOperation.js +15 -0
  165. package/lib/relations/belongsToMany/BelongsToManyDeleteOperation.js +23 -0
  166. package/lib/relations/belongsToMany/BelongsToManyFindOperation.js +35 -0
  167. package/lib/relations/belongsToMany/BelongsToManyInsertOperation.js +28 -0
  168. package/lib/relations/belongsToMany/BelongsToManyRelateOperation.js +16 -0
  169. package/lib/relations/belongsToMany/BelongsToManyRelation.js +276 -0
  170. package/lib/relations/belongsToMany/BelongsToManyUnrelateOperation.js +11 -0
  171. package/lib/relations/belongsToOne/BelongsToOneDeleteOperation.js +2 -8
  172. package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +6 -11
  173. package/lib/relations/belongsToOne/BelongsToOneRelateOperation.js +3 -9
  174. package/lib/relations/belongsToOne/BelongsToOneRelation.js +8 -19
  175. package/lib/relations/belongsToOne/BelongsToOneUnrelateOperation.js +53 -27
  176. package/lib/relations/hasMany/HasManyInsertOperation.js +5 -10
  177. package/lib/relations/hasMany/HasManyRelateOperation.js +5 -11
  178. package/lib/relations/hasMany/HasManyRelation.js +9 -11
  179. package/lib/relations/hasMany/HasManyUnrelateOperation.js +3 -9
  180. package/lib/relations/hasOne/HasOneRelation.js +2 -8
  181. package/lib/relations/hasOneThrough/HasOneThroughRelation.js +2 -8
  182. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +10 -15
  183. package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +2 -8
  184. package/lib/relations/manyToMany/ManyToManyRelation.js +82 -50
  185. package/lib/relations/manyToMany/ManyToManySqliteModifyMixin.js +2 -8
  186. package/lib/relations/manyToMany/delete/ManyToManyDeleteOperation.js +5 -9
  187. package/lib/relations/manyToMany/delete/ManyToManyDeleteOperationBase.js +2 -8
  188. package/lib/relations/manyToMany/delete/ManyToManyDeleteSqliteOperation.js +3 -9
  189. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +20 -59
  190. package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +6 -11
  191. package/lib/relations/manyToMany/relate/ManyToManyRelateOperation.js +4 -10
  192. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +3 -9
  193. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperation.js +5 -9
  194. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +3 -9
  195. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateSqliteOperation.js +3 -9
  196. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +3 -9
  197. package/lib/relations/manyToMany/update/ManyToManyUpdateOperation.js +5 -9
  198. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +64 -72
  199. package/lib/relations/manyToMany/update/ManyToManyUpdateSqliteOperation.js +3 -9
  200. package/lib/transaction.js +3 -9
  201. package/lib/utils/assert.js +3 -11
  202. package/lib/utils/buildUtils.js +2 -8
  203. package/lib/utils/classUtils.js +1 -7
  204. package/lib/utils/createModifier.js +2 -8
  205. package/lib/utils/identifierMapping.js +23 -27
  206. package/lib/utils/internalPropUtils.js +1 -7
  207. package/lib/utils/knexUtils.js +84 -40
  208. package/lib/utils/mixin.js +3 -10
  209. package/lib/utils/normalizeIds.js +2 -8
  210. package/lib/utils/objectUtils.js +41 -60
  211. package/lib/utils/parseFieldExpression.js +16 -16
  212. package/lib/utils/promiseUtils/after.js +2 -8
  213. package/lib/utils/promiseUtils/afterReturn.js +2 -8
  214. package/lib/utils/promiseUtils/index.js +7 -16
  215. package/lib/utils/promiseUtils/isPromise.js +2 -8
  216. package/lib/utils/promiseUtils/map.js +2 -8
  217. package/lib/utils/promiseUtils/mapAfterAllReturn.js +2 -8
  218. package/lib/utils/promiseUtils/try.js +2 -8
  219. package/lib/utils/resolveModel.js +10 -17
  220. package/lib/utils/tmpColumnUtils.js +2 -9
  221. package/lib/utils/warnings.js +2 -14
  222. package/package.json +25 -17
  223. package/typings/objection/index.d.ts +3217 -2179
  224. package/lib/.eslintrc.json +0 -8
  225. package/lib/model/graph/ModelGraph.js +0 -80
  226. package/lib/model/graph/ModelGraphBuilder.js +0 -241
  227. package/lib/model/graph/ModelGraphEdge.js +0 -52
  228. package/lib/model/graph/ModelGraphNode.js +0 -238
  229. package/lib/queryBuilder/graph/GraphAction.js +0 -69
  230. package/lib/queryBuilder/graph/GraphData.js +0 -53
  231. package/lib/queryBuilder/graph/GraphFetcher.js +0 -197
  232. package/lib/queryBuilder/graph/GraphNodeDbExistence.js +0 -184
  233. package/lib/queryBuilder/graph/GraphOperation.js +0 -27
  234. package/lib/queryBuilder/graph/delete/GraphDelete.js +0 -73
  235. package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +0 -88
  236. package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +0 -58
  237. package/lib/queryBuilder/graph/insert/GraphInsert.js +0 -296
  238. package/lib/queryBuilder/graph/insert/GraphInsertAction.js +0 -233
  239. package/lib/queryBuilder/graph/insert/JoinRowGraphInsertAction.js +0 -92
  240. package/lib/queryBuilder/graph/patch/GraphPatch.js +0 -20
  241. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +0 -289
  242. package/lib/queryBuilder/graph/recursiveUpsert/GraphRecursiveUpsert.js +0 -31
  243. package/lib/queryBuilder/graph/recursiveUpsert/GraphRecursiveUpsertAction.js +0 -126
  244. package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +0 -30
  245. package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +0 -37
  246. package/lib/queryBuilder/operations/jsonApi/WhereJsonPostgresOperation.js +0 -62
  247. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +0 -192
  248. package/lib/utils/wrapDbError.js +0 -64
@@ -1,30 +1,25 @@
1
- 'use strict';
2
-
3
- const { ModelGraph } = require('../../model/graph/ModelGraph');
4
- const { ModelGraphEdge } = require('../../model/graph/ModelGraphEdge');
5
- const { createNotModelError } = require('../../model/graph/ModelGraphBuilder');
6
- const { GraphFetcher } = require('../graph/GraphFetcher');
7
- const { GraphInsert } = require('./insert/GraphInsert');
8
- const { GraphPatch } = require('./patch/GraphPatch');
9
- const { GraphDelete, GraphDereferencedBelongsToOneDelete } = require('./delete/GraphDelete');
10
- const { GraphRecursiveUpsert } = require('./recursiveUpsert/GraphRecursiveUpsert');
11
- const { GraphOptions, OPTION_NAMES } = require('./GraphOptions');
12
- const { ValidationErrorType } = require('../../model/ValidationError');
13
- const { RelationExpression } = require('../RelationExpression');
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 } from './GraphNode.js';
2
+ import { GraphOptions } from './GraphOptions.js';
3
+ import { GraphPlan } from './GraphPlan.js';
4
+ import { GraphWriter } from './GraphWriter.js';
5
+ import { loadCurrentGraph } from './GraphLoader.js';
6
+ import { ValidationErrorType } from '../../model/ValidationError.js';
7
+ import { RelationExpression } from '../RelationExpression.js';
8
+ import { asArray, isObject, nonStrictEquals } from '../../utils/objectUtils.js';
9
+ import { isInternalProp } from '../../utils/internalPropUtils.js';
10
+
11
+ export class GraphUpsert {
20
12
  constructor({ rootModelClass, objects, upsertOptions, insertOnly = false }) {
21
- checkCanBeConvertedToModels(rootModelClass, objects);
22
- warnOnUnknownOptions(upsertOptions);
13
+ // Wrap `undefined` too, so that it fails the check:
14
+ for (const obj of Array.isArray(objects) ? objects : [objects]) {
15
+ if (!isObject(obj)) {
16
+ throw createNotModelError(rootModelClass, obj);
17
+ }
18
+ }
23
19
 
24
20
  this.objects = rootModelClass.ensureModelArray(objects, GraphUpsert.modelOptions);
25
21
  this.isArray = Array.isArray(objects);
26
- this.upsertOpt = upsertOptions;
27
- this.insertOnly = insertOnly;
22
+ this.graphOptions = new GraphOptions(upsertOptions, insertOnly);
28
23
  }
29
24
 
30
25
  static get modelOptions() {
@@ -36,482 +31,539 @@ class GraphUpsert {
36
31
  // to. Even applying it to the root table only would be unsound: rows skipped
37
32
  // by `ignore()` aren't returned, which leaves the root models without ids,
38
33
  // so that related rows would be inserted with missing or wrong foreign keys.
39
- // The clause is ignored, and a warning is logged once per method.
40
- // TODO: Throw an error instead in objection 4.0 (#53).
41
- static warnOnConflict(builder, method) {
34
+ static checkForOnConflict(builder, method) {
42
35
  if (builder.has(isOnConflictOperation)) {
43
- warnOnce(
36
+ throw new Error(
44
37
  `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.',
38
+ 'Insert the conflicting rows with a separate insert() query instead.',
47
39
  );
48
40
  }
49
41
  }
50
42
 
51
- run(builder) {
52
- const modelClass = builder.modelClass();
53
- const graphOptions = new GraphOptions(this.upsertOpt, [], this.insertOnly);
43
+ async run(builder) {
44
+ const { graphOptions } = this;
45
+ const nodes = GraphNode.buildGraph(builder.modelClass(), this.roots);
46
+ GraphNode.linkReferences(nodes);
47
+ const restoreMarkers = GraphNode.removeMarkers(nodes);
48
+
49
+ // A `#ref` to a model with an id is that model, also when its relations
50
+ // are loaded and upserted. Models inserted in a pass have their ids in the
51
+ // next ones:
52
+ const identifyReferences = (nodes) => {
53
+ for (const { obj, referencedNode } of nodes) {
54
+ if (referencedNode?.obj.$hasId() && !obj.$hasId()) {
55
+ obj.$id(referencedNode.obj.$id());
56
+ }
57
+ }
58
+ };
54
59
 
55
- const graph = ModelGraph.create(modelClass, this.objects);
56
- assignDbRefsAsRelateProps(graph);
60
+ for (const node of nodes) {
61
+ if (node.parent && node.isDbReference) {
62
+ node.relation.setRelateProp(node.obj, asArray(node.dbReference));
63
+ }
64
+ }
57
65
 
58
- return createGraphData(builder, graphOptions, graph)
59
- .then(checkForErrors(builder))
60
- .then(pruneGraphs())
61
- .then(checkForJoinTableRelatedTableErrors(builder))
62
- .then(executeOperations(builder))
63
- .then(removeReferenceProps(graph.nodes))
64
- .then(returnResult(this.objects, this.isArray));
65
- }
66
- }
66
+ identifyReferences(nodes);
67
+
68
+ try {
69
+ checkGraph(builder, new GraphPlan({ nodes, graphOptions }));
70
+ checkPlacements(builder, nodes);
71
+ const writtenNodes = new Set();
72
+ let keptNodes = new Map();
73
+
74
+ // Rows that the graph relates into a HasManyRelation or HasOneRelation
75
+ // and gives relations are moved first, and their relations are upserted
76
+ // in another pass, against their rows as they are after the move (#214).
77
+ for (let passNodes = nodes; passNodes.length > 0;) {
78
+ const plan = new GraphPlan({
79
+ nodes: passNodes,
80
+ graphOptions,
81
+ keptNodes,
82
+ });
83
+ const movedNodes = graphOptions.isInsertOnly() ? [] : await loadCurrentGraph(builder, plan);
84
+
85
+ plan.defer(movedNodes.flatMap((node) => node.descendants));
86
+ plan.cacheDecisions();
87
+ checkMissingModels(builder, plan);
88
+ plan.prune().plan();
89
+ checkChangedIds(builder, plan);
90
+ // The rows that this pass keeps stay kept in the next ones, as decided
91
+ // before its writes:
92
+ keptNodes = plan.keptNodes;
93
+
94
+ // Models that `noInsert` or `noRelate` ignore aren't written, but the
95
+ // ones marked with `#delete` or `#unrelate` keep their values:
96
+ for (const node of plan.activeNodes) {
97
+ if (plan.actions.get(node) !== 'ignore' || node.toBeUnrelatedOrDeleted) {
98
+ writtenNodes.add(node);
99
+ }
100
+ }
67
101
 
68
- // Misspelled options are silently ignored, so the operation runs with the
69
- // default behaviour instead (#84). A warning is logged once per option.
70
- // TODO: Throw an error instead in objection 4.0 (#53).
71
- function warnOnUnknownOptions(upsertOptions) {
72
- // Recursive upserts pass on the already checked options of their parent.
73
- if (upsertOptions instanceof GraphOptions) {
74
- return;
75
- }
102
+ checkReferences(builder, plan, writtenNodes);
103
+ checkJoinTables(builder, plan);
104
+ await new GraphWriter(plan).write(builder);
76
105
 
77
- for (const name of Object.keys(upsertOptions)) {
78
- if (!OPTION_NAMES.includes(name)) {
79
- warnOnce(
80
- `Unknown graph option "${name}" is ignored. ` +
81
- `Valid options are: ${OPTION_NAMES.join(', ')}. This will throw in objection 4.0.`,
82
- );
106
+ passNodes = movedNodes.flatMap((node) => [node, ...node.descendants]);
107
+ identifyReferences(passNodes);
108
+ }
109
+ } catch (err) {
110
+ // The input can be retried as it was:
111
+ restoreMarkers();
112
+ throw err;
83
113
  }
114
+
115
+ return this.roots;
84
116
  }
85
- }
86
117
 
87
- function checkCanBeConvertedToModels(modelClass, objects) {
88
- asArray(objects).forEach((obj) => {
89
- if (!isObject(obj)) {
90
- throw createNotModelError(modelClass, obj);
91
- }
92
- });
118
+ get roots() {
119
+ return this.isArray ? this.objects : this.objects[0];
120
+ }
93
121
  }
94
122
 
95
- function assignDbRefsAsRelateProps(graph) {
96
- for (const node of graph.nodes) {
97
- if (!node.parentEdge || !node.parentEdge.relation || !node.isDbReference) {
98
- continue;
123
+ // Check the models with ids that are neither at their place nor related, once
124
+ // the current graph of a pass is loaded.
125
+ function checkMissingModels(builder, plan) {
126
+ for (const node of plan.nodes) {
127
+ if (plan.intentFor(node) === 'missing') {
128
+ const id = node.obj.$id();
129
+
130
+ throw node.modelClass.createNotFoundError(builder.context(), {
131
+ message: node.isMissing
132
+ ? `model (id=${id}) does not exist. If you want to insert it with an id, use the insertMissing option`
133
+ : node.isRoot
134
+ ? `root model (id=${id}) does not exist. If you want to insert it with an id, use the insertMissing option`
135
+ : `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`,
136
+ dataPath: node.dataPath,
137
+ });
99
138
  }
100
-
101
- node.parentEdge.relation.setRelateProp(node.obj, asArray(node.dbReference));
102
139
  }
103
140
  }
104
141
 
105
- async function createGraphData(builder, graphOptions, graph) {
106
- const currentGraph = await fetchCurrentGraph(builder, graphOptions, graph);
107
-
108
- const nodeDbExistence = await GraphNodeDbExistence.create({
109
- builder,
110
- graph,
111
- graphOptions,
112
- currentGraph,
113
- });
142
+ // The graph writes keys of rows that can't change if they're part of the ids
143
+ // of the rows: the foreign key of a row that a HasMany or HasOne relate moves
144
+ // to another owner, and the foreign key of an existing row that its own
145
+ // BelongsToOne relate or insert points to another row, or that `null`,
146
+ // `#unrelate` or `#delete` clears. Keys that aren't known before the writes
147
+ // are compared only where they surely change, as new models get new keys.
148
+ function checkChangedIds(builder, plan) {
149
+ const { actions } = plan;
150
+ const newKey = {};
151
+ const getKey = (node, prop, i) =>
152
+ actions.get(node) === 'insert' && !node.obj.$hasId()
153
+ ? newKey
154
+ : (prop.getProp(node.obj, i) ?? (node.existing && prop.getProp(node.existing.obj, i)));
155
+
156
+ const checkKey = (row, relation, rowProp, getNewKey) => {
157
+ const idProps = row.modelClass.getIdPropertyArray();
158
+ const isChanged =
159
+ actions.get(row) !== 'insert' &&
160
+ rowProp.props.some((prop, i) => {
161
+ const key = row.obj[prop];
162
+ const changedKey = getNewKey(i);
163
+ return (
164
+ idProps.includes(prop) &&
165
+ key !== undefined &&
166
+ changedKey !== undefined &&
167
+ (changedKey === newKey || !nonStrictEquals(key, changedKey))
168
+ );
169
+ });
114
170
 
115
- return new GraphData({ graph, currentGraph, graphOptions, nodeDbExistence });
116
- }
171
+ if (isChanged) {
172
+ throw builder.modelClass().createValidationError({
173
+ type: ValidationErrorType.InvalidGraph,
174
+ message: `the graph changes the key of the ${row.modelClass.name} at ${formatPath(row.dataPathKey)} through ${relation.name}, which is part of its id`,
175
+ });
176
+ }
177
+ };
117
178
 
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 });
123
- }
124
- }
179
+ for (const node of plan.activeNodes) {
180
+ const { relation, parent } = node;
181
+ const action = actions.get(node);
125
182
 
126
- // Remove branches from the graph that require no operations. For example
127
- // we never want to do anything for descendant nodes of a node that is
128
- // deleted or unrelated. We never delete recursively.
129
- function pruneGraphs() {
130
- return (graphData) => {
131
- pruneRelatedBranches(graphData);
132
- pruneSkippedBranches(graphData);
133
-
134
- if (!graphData.graphOptions.isInsertOnly()) {
135
- pruneDeletedBranches(graphData);
136
- pruneUnrelatedOrDeletedBranches(graphData);
183
+ if (relation?.keySide === 'related' && action === 'relate') {
184
+ checkKey(node.referencedNode || node, relation, relation.relatedProp, (i) =>
185
+ getKey(parent, relation.ownerProp, i),
186
+ );
187
+ } else if (relation?.keySide === 'owner' && relation.isOneToOne()) {
188
+ if (node.toBeUnrelatedOrDeleted) {
189
+ if (node.current) {
190
+ checkKey(parent, relation, relation.ownerProp, () => null);
191
+ }
192
+ } else if (action === 'relate' || action === 'insert') {
193
+ checkKey(parent, relation, relation.ownerProp, (i) =>
194
+ getKey(node.referencedNode || node, relation.relatedProp, i),
195
+ );
196
+ }
137
197
  }
138
198
 
139
- return graphData;
140
- };
141
- }
199
+ // `null` clears the key:
200
+ const nullNames = node.modelClass.getRelationNames().filter((name) => node.obj[name] === null);
142
201
 
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
- });
202
+ for (const name of nullNames) {
203
+ const nullRelation = node.modelClass.getRelation(name);
150
204
 
151
- removeBranchesFromGraph(findRoots(relateNodes), graphData.graph);
205
+ if (nullRelation.keySide === 'owner' && nullRelation.isOneToOne()) {
206
+ checkKey(node, nullRelation, nullRelation.ownerProp, () => null);
207
+ }
208
+ }
209
+ }
152
210
  }
153
211
 
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
- });
167
-
168
- removeBranchesFromGraph(findRoots(skippedNodes), graph);
169
- }
212
+ // Check the whole graph before anything is written.
213
+ function checkGraph(builder, plan) {
214
+ const { nodes, graphOptions } = plan;
215
+ const modelClass = builder.modelClass();
216
+ const createInvalidGraphError = (message) =>
217
+ modelClass.createValidationError({ type: ValidationErrorType.InvalidGraph, message });
170
218
 
171
- function isReferencedFromOutside(nodes) {
172
- const nodeSet = new Set(nodes);
219
+ const allowedExpression = builder.allowedGraphExpression();
220
+ const rootObjs = nodes.filter((node) => node.isRoot).map((node) => node.obj);
221
+
222
+ if (
223
+ allowedExpression &&
224
+ !allowedExpression.isSubExpression(RelationExpression.fromModelGraph(rootObjs))
225
+ ) {
226
+ throw modelClass.createValidationError({
227
+ type: ValidationErrorType.UnallowedRelation,
228
+ message: 'trying to upsert an unallowed relation',
229
+ });
230
+ }
173
231
 
174
- return nodes.some((node) =>
175
- node.refEdges.some((edge) => edge.isRelatedNode(node) && !nodeSet.has(edge.ownerNode)),
176
- );
177
- }
232
+ if (
233
+ !graphOptions.allowRefs() &&
234
+ nodes.some((node) => node.isReference || node.propertyReferences.length > 0)
235
+ ) {
236
+ throw createInvalidGraphError(
237
+ '#ref references are not allowed in a graph by default. see the allowRefs insert/upsert graph option',
238
+ );
239
+ }
178
240
 
179
- function pruneDeletedBranches(graphData) {
180
- const { graph, currentGraph } = graphData;
241
+ // `#unrelate` and `#delete` markers come from the graph data, which may be
242
+ // untrusted input. Like `#ref`, they have to be allowed explicitly (#2410).
243
+ // Root models can't be marked even with `allowMarkers`, so report that first.
244
+ // An insert has nothing to unrelate or delete, so `insertGraph` rejects them.
245
+ const markedNode = nodes.find((node) => node.toBeUnrelatedOrDeleted);
181
246
 
182
- const deleteNodes = currentGraph.nodes.filter((currentNode) => !graph.nodeForNode(currentNode));
183
- const roots = findRoots(deleteNodes);
247
+ if (markedNode) {
248
+ const { graphUnrelateProp, graphDeleteProp } = markedNode.modelClass;
249
+ const markers = `${graphUnrelateProp} and ${graphDeleteProp}`;
184
250
 
185
- // Don't delete relations the current graph doesn't even mention.
186
- // So if the parent node doesn't even have the relation, it's not
187
- // supposed to be deleted.
188
- const rootsNotInRelation = roots.filter((deleteRoot) => {
189
- if (!deleteRoot.parentNode) {
190
- return false;
251
+ if (nodes.some((node) => node.isRoot && node.toBeUnrelatedOrDeleted)) {
252
+ throw createInvalidGraphError(
253
+ `${markers} can only be used for related models, not for root models`,
254
+ );
255
+ } else if (!graphOptions.allowMarkers()) {
256
+ throw createInvalidGraphError(
257
+ graphOptions.isInsertOnly()
258
+ ? `${markers} can only be used in upsertGraph, not in insertGraph`
259
+ : `${markers} markers are not allowed in a graph by default. see the allowMarkers upsertGraph option`,
260
+ );
191
261
  }
262
+ }
192
263
 
193
- const { relation } = deleteRoot.parentEdge;
194
- const parentNode = graph.nodeForNode(deleteRoot.parentNode);
264
+ if (graphOptions.isInsertOnly()) {
265
+ const relatedNodes = nodes.filter((node) => plan.actionFor(node) === 'relate');
195
266
 
196
- if (!parentNode) {
197
- return false;
267
+ if (relatedNodes.some((node) => node.relation.keySide === 'related')) {
268
+ throw new Error(
269
+ 'You cannot relate HasManyRelation or HasOneRelation using insertGraph, because those require update operations. Consider using upsertGraph instead.',
270
+ );
198
271
  }
199
272
 
200
- return parentNode.obj[relation.name] === undefined;
201
- });
202
-
203
- removeBranchesFromGraph(roots, currentGraph);
204
- removeNodesFromGraph(new Set(rootsNotInRelation), currentGraph);
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);
273
+ // `upsertGraph()` upserts the branches below related models, but
274
+ // `insertGraph()` leaves them out, which would drop the new models there
275
+ // (#217). Models with ids below related models are taken as fetched models
276
+ // with loaded relations, and are ignored.
277
+ for (const node of relatedNodes) {
278
+ const newNode = node.descendants.find(
279
+ (descendant) => !descendant.hasId && plan.actionFor(descendant) === 'insert',
280
+ );
215
281
 
216
- removeBranchesFromGraph(roots, graph);
217
- removeBranchesFromGraph(currentRoots, currentGraph);
282
+ if (newNode) {
283
+ throw createInvalidGraphError(
284
+ `insertGraph cannot insert the new model at ${formatPath(newNode.dataPathKey)} below a related model. Insert it with a separate query or use upsertGraph instead`,
285
+ );
286
+ }
287
+ }
288
+ }
218
289
  }
219
290
 
220
- function findRoots(nodes) {
221
- const nodeSet = new Set(nodes);
291
+ // One row can't get two values for one column, and the slot of a
292
+ // HasOneRelation can't get two rows. A `#ref` or `#dbRef` model is a place of
293
+ // the row that it refers to. The keys that relations give a row are the values
294
+ // of the key props across them, or stand for these where the graph doesn't
295
+ // give them, e.g. for new models.
296
+ function checkPlacements(builder, nodes) {
297
+ const newRowIds = new Map();
298
+ const getRowId = (node) => {
299
+ const target = node.referencedNode || node;
300
+
301
+ if (target.obj.$hasId()) {
302
+ return target.rowKey;
303
+ } else {
304
+ if (!newRowIds.has(target)) {
305
+ newRowIds.set(target, `new:${newRowIds.size}`);
306
+ }
222
307
 
223
- return uniqBy(
224
- nodes.filter((node) => {
225
- let parentNode = node.parentNode;
308
+ return newRowIds.get(target);
309
+ }
310
+ };
226
311
 
227
- while (parentNode) {
228
- if (nodeSet.has(parentNode)) {
229
- return false;
312
+ // The values that the graph gives each row, by prop, and the model classes
313
+ // and relations through which the graph reaches the rows:
314
+ const rows = new Map();
315
+ const rowTables = new Map();
316
+ const rowRelations = new Map();
317
+ const setValue = (rowId, name, prop, value, path) => {
318
+ const values = rows.get(rowId) || new Map();
319
+ const other = values.get(prop);
320
+ rows.set(rowId, values);
321
+
322
+ if (!other) {
323
+ values.set(prop, { value, path });
324
+ } else if (!nonStrictEquals(other.value, value)) {
325
+ throw builder.modelClass().createValidationError({
326
+ type: ValidationErrorType.InvalidGraph,
327
+ message: `the graph gives ${prop} of one ${name} two values, at ${formatPath(other.path)} and at ${formatPath(path)}`,
328
+ });
329
+ }
330
+ };
331
+ const setNodeValue = (node, prop, value, path) =>
332
+ setValue(getRowId(node), node.modelClass.name, prop, value, path);
333
+
334
+ // The join row of the pair of models that `node` and its parent are, the
335
+ // same through both relations of the join table:
336
+ const getJoinRowId = (node) =>
337
+ [
338
+ node.relation.joinTable,
339
+ ...[
340
+ [node.relation.joinTableOwnerProp.cols, getRowId(node.parent)],
341
+ [node.relation.joinTableRelatedProp.cols, getRowId(node)],
342
+ ]
343
+ .sort(([cols1], [cols2]) => cols1.join().localeCompare(cols2.join()))
344
+ .map(([cols, rowId]) => `${cols}=${rowId}`),
345
+ ].join(':');
346
+
347
+ const getPath = (node, ...keys) => [node.dataPathKey, ...keys].filter(Boolean).join('.');
348
+ const getRelations = (node) =>
349
+ node.modelClass
350
+ .getRelationNames()
351
+ .filter((name) => node.obj[name] !== undefined)
352
+ .map((name) => node.modelClass.getRelation(name));
353
+ const isOwnerKey = (relation) => relation.keySide === 'owner' && relation.isOneToOne();
354
+ const isSlot = (relation) => relation.keySide === 'related' && relation.isOneToOne();
355
+
356
+ // The props that the models give, except for the ids and the keys that the
357
+ // relations of the same model set, which follow these relations:
358
+ for (const node of nodes) {
359
+ const { modelClass, obj, relation } = node;
360
+ const skippedProps = [
361
+ ...modelClass.getRelationNames(),
362
+ ...modelClass.getIdPropertyArray(),
363
+ ...(relation?.keySide === 'related' ? relation.relatedProp.props : []),
364
+ // The extras of join rows belong to the join rows:
365
+ ...(relation?.keySide === 'join' ? relation.joinTableExtras.map((e) => e.aliasProp) : []),
366
+ ...getRelations(node)
367
+ .filter(isOwnerKey)
368
+ .flatMap((relation) => relation.ownerProp.props),
369
+ ];
370
+
371
+ const rowId = getRowId(node);
372
+ rowTables.set(rowId, modelClass.getTableName());
373
+ rowRelations.set(rowId, (rowRelations.get(rowId) || new Set()).add(relation));
374
+
375
+ if (!node.toBeUnrelatedOrDeleted) {
376
+ for (const prop of Object.keys(obj)) {
377
+ if (obj[prop] !== undefined && !isInternalProp(prop) && !skippedProps.includes(prop)) {
378
+ setNodeValue(node, prop, obj[prop], getPath(node, prop));
230
379
  }
231
-
232
- parentNode = parentNode.parentNode;
233
380
  }
381
+ }
234
382
 
235
- return true;
236
- }),
237
- );
238
- }
239
-
240
- function removeBranchesFromGraph(branchRoots, graph) {
241
- const nodesToRemove = new Set(
242
- branchRoots.reduce(
243
- (nodesToRemove, node) => [...nodesToRemove, ...node.descendantRelationNodes],
244
- [],
245
- ),
246
- );
247
-
248
- removeNodesFromGraph(nodesToRemove, graph);
249
- }
250
-
251
- function removeNodesFromGraph(nodesToRemove, graph) {
252
- const edgesToRemove = new Set();
253
-
254
- for (const node of nodesToRemove) {
255
- for (const edge of node.edges) {
256
- const otherNode = edge.getOtherNode(node);
257
-
258
- if (!nodesToRemove.has(otherNode)) {
259
- otherNode.removeEdge(edge);
260
- edgesToRemove.add(edge);
383
+ // A pair of models has one join row, which gets the extras of all places
384
+ // of the pair:
385
+ if (relation?.keySide === 'join' && !node.toBeUnrelatedOrDeleted) {
386
+ for (const { aliasProp } of relation.joinTableExtras) {
387
+ if (obj[aliasProp] !== undefined) {
388
+ const path = getPath(node, aliasProp);
389
+ setValue(
390
+ getJoinRowId(node),
391
+ `join row of ${relation.name}`,
392
+ aliasProp,
393
+ obj[aliasProp],
394
+ path,
395
+ );
396
+ }
261
397
  }
262
398
  }
263
399
  }
264
400
 
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
- }
401
+ // The value of the key prop `i` of `prop` that `node` gives across a
402
+ // relation, from wherever the graph gives the row, or one that stands for it:
403
+ const getKey = (node, prop, i) =>
404
+ prop.getProp(node.obj, i) ??
405
+ rows.get(getRowId(node))?.get(prop.props[i])?.value ?? {
406
+ row: getRowId(node),
407
+ prop: prop.props[i],
408
+ };
409
+
410
+ // The keys that the relations give the rows, the rows that the graph puts
411
+ // in the slots of HasOneRelations, and whether the rows are deleted:
412
+ const slots = [];
413
+ for (const node of nodes) {
414
+ const { relation, parent } = node;
415
+ const path = getPath(node);
416
+ const isUnrelated = node.toBeUnrelated;
417
+ const isDeleted = node.toBeDeleted && !isUnrelated;
418
+
419
+ if (relation?.keySide === 'related') {
420
+ if (!isDeleted) {
421
+ relation.relatedProp.forEach((i) => {
422
+ const key = isUnrelated ? null : getKey(parent, relation.ownerProp, i);
423
+ setNodeValue(node, relation.relatedProp.props[i], key, path);
424
+ });
425
+ }
270
426
 
271
- function checkForErrors(builder) {
272
- return (graphData) => {
273
- checkForNotFoundErrors(graphData, builder);
274
- checkForUnallowedRelationErrors(graphData, builder);
275
- checkForUnallowedReferenceErrors(graphData, builder);
276
- // Root models can't be marked even with `allowMarkers`, so report that
277
- // first instead of pointing to the option.
278
- checkForRootUnrelateOrDeleteErrors(graphData, builder);
279
- checkForUnallowedMarkerErrors(graphData, builder);
280
-
281
- if (graphData.graphOptions.isInsertOnly()) {
282
- checkForHasManyRelateErrors(graphData);
283
- warnOnInsertBelowRelate(graphData);
427
+ if (isSlot(relation)) {
428
+ const row = node.toBeUnrelatedOrDeleted ? null : getRowId(node);
429
+ slots.push({ owner: parent, relation, row, path });
430
+ }
431
+ } else if (relation && isOwnerKey(relation)) {
432
+ relation.ownerProp.forEach((i) => {
433
+ const key = node.toBeUnrelatedOrDeleted ? null : getKey(node, relation.relatedProp, i);
434
+ setNodeValue(parent, relation.ownerProp.props[i], key, path);
435
+ });
284
436
  }
285
437
 
286
- return graphData;
287
- };
288
- }
438
+ setNodeValue(node, '#delete', isDeleted, path);
289
439
 
290
- // Inserting or relating a node of a many-to-many relation inserts a join row,
291
- // which would be a new related row if the join table is also the related
292
- // table (#133). Check this before this graph is written. It runs after
293
- // pruning, as the branches below related nodes are upserted and checked by
294
- // nested `upsertGraph` calls against their own current graphs.
295
- function checkForJoinTableRelatedTableErrors(builder) {
296
- return (graphData) => {
297
- const { graphOptions, currentGraph, graph } = graphData;
298
-
299
- for (const node of graph.nodes) {
300
- const relation = node.parentEdge && node.parentEdge.relation;
301
-
302
- if (
303
- relation &&
304
- relation.isObjectionManyToManyRelation &&
305
- !currentGraph.nodeForNode(node) &&
306
- graphOptions.shouldInsertOrRelate(node, graphData)
307
- ) {
308
- relation.assertCanModifyJoinTable(
309
- builder,
310
- graphOptions.shouldRelate(node, graphData) ? 'relate' : 'insert',
311
- );
312
- }
440
+ // All places of a pair of models keep or remove its join row:
441
+ if (relation?.keySide === 'join') {
442
+ const name = `join row of ${relation.name}`;
443
+ setValue(getJoinRowId(node), name, '#unrelate', node.toBeUnrelatedOrDeleted, path);
313
444
  }
314
445
 
315
- return graphData;
316
- };
317
- }
318
-
319
- function checkForNotFoundErrors(graphData, builder) {
320
- const { graphOptions, currentGraph, graph } = graphData;
321
- // The branches below related nodes are upserted and checked by nested
322
- // `upsertGraph` calls, against the current graphs of the related nodes:
323
- const nestedNodes = new Set(
324
- graph.nodes
325
- .filter((node) => graphOptions.shouldRelate(node, graphData))
326
- .flatMap((node) => node.descendantRelationNodes),
327
- );
328
-
329
- for (const node of graph.nodes) {
330
- if (
331
- node.obj.$hasId() &&
332
- !nestedNodes.has(node) &&
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,
446
+ // `null` clears the key or the slot:
447
+ for (const nullRelation of getRelations(node).filter(({ name }) => node.obj[name] === null)) {
448
+ if (isOwnerKey(nullRelation)) {
449
+ nullRelation.ownerProp.forEach((i) => {
450
+ setNodeValue(
451
+ node,
452
+ nullRelation.ownerProp.props[i],
453
+ null,
454
+ getPath(node, nullRelation.name),
455
+ );
347
456
  });
457
+ } else if (isSlot(nullRelation)) {
458
+ const path = getPath(node, nullRelation.name);
459
+ slots.push({ owner: node, relation: nullRelation, row: null, path });
348
460
  }
349
461
  }
350
462
  }
351
- }
352
463
 
353
- function checkForUnallowedRelationErrors(graphData, builder) {
354
- const { graph } = graphData;
355
- const allowedExpression = builder.allowedGraphExpression();
464
+ // The rows of a table by the values that the graph gives the props of a key,
465
+ // with the values that stand for keys as they are and the others as strings,
466
+ // so that e.g. `1` and `'1'` are the same key:
467
+ const toIndexKey = (values) =>
468
+ JSON.stringify(values.map((value) => (isObject(value) ? value : String(value))));
469
+ const indexes = new Map();
470
+ const getRowsByKey = (table, props) => {
471
+ const indexName = `${table}:${props.join()}`;
472
+ let index = indexes.get(indexName);
473
+
474
+ if (!index) {
475
+ index = new Map();
476
+ indexes.set(indexName, index);
477
+
478
+ for (const [rowId, values] of rows) {
479
+ const keyValues = props.map((prop) => values.get(prop)?.value);
480
+
481
+ if (rowTables.get(rowId) === table && keyValues.every((value) => value != null)) {
482
+ const indexKey = toIndexKey(keyValues);
483
+ const rowIds = index.get(indexKey);
484
+
485
+ if (rowIds) {
486
+ rowIds.push(rowId);
487
+ } else {
488
+ index.set(indexKey, [rowId]);
489
+ }
490
+ }
491
+ }
492
+ }
356
493
 
357
- if (allowedExpression) {
358
- const rootsObjs = graph.nodes.filter((node) => !node.parentEdge).map((node) => node.obj);
359
- const expression = RelationExpression.fromModelGraph(rootsObjs);
494
+ return index;
495
+ };
360
496
 
361
- if (!allowedExpression.isSubExpression(expression)) {
497
+ // Any other row that the graph gives the key of the owner of a slot is a
498
+ // second row in it, unless the relation filters its rows and the graph
499
+ // doesn't reach the row through it:
500
+ for (const { owner, relation, row, path } of slots) {
501
+ const { relatedModelClass, relatedProp, ownerProp } = relation;
502
+ const ownerKey = ownerProp.props.map((_, i) => getKey(owner, ownerProp, i));
503
+ const rowsByKey = getRowsByKey(relatedModelClass.getTableName(), relatedProp.props);
504
+ const otherRowId = (rowsByKey.get(toIndexKey(ownerKey)) || []).find(
505
+ (rowId) => rowId !== row && (!relation.modify || rowRelations.get(rowId).has(relation)),
506
+ );
507
+
508
+ if (otherRowId) {
509
+ const other = rows.get(otherRowId).get(relatedProp.props[0]);
362
510
  throw builder.modelClass().createValidationError({
363
- type: ValidationErrorType.UnallowedRelation,
364
- message: 'trying to upsert an unallowed relation',
511
+ type: ValidationErrorType.InvalidGraph,
512
+ message: `the graph puts two ${relatedModelClass.name} rows in ${relation.name} of one ${owner.modelClass.name}, at ${formatPath(path)} and at ${formatPath(other.path)}`,
365
513
  });
366
514
  }
367
515
  }
368
516
  }
369
517
 
370
- function checkForUnallowedReferenceErrors(graphData, builder) {
371
- const { graph, graphOptions } = graphData;
372
-
373
- if (graphOptions.allowRefs()) {
374
- return;
375
- }
376
-
377
- if (graph.edges.some((edge) => edge.type === ModelGraphEdge.Type.Reference)) {
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();
392
-
393
- if (graphOptions.allowMarkers() && !isInsertOnly) {
394
- return;
395
- }
518
+ // References need the ids of the models they point to, which the database
519
+ // only gives the models that the graph writes in the same pass or an earlier
520
+ // one. The ids of new models that the graph doesn't write, e.g. as `noInsert`
521
+ // skips them, are never known. Everything else reads the models as the graph
522
+ // gives them. The references of models that aren't written are never resolved.
523
+ function checkReferences(builder, plan, writtenNodes) {
524
+ const isReadable = (node, refPath) =>
525
+ writtenNodes.has(node) ||
526
+ node.obj.$hasId() ||
527
+ (!!refPath && !node.modelClass.getIdPropertyArray().includes(refPath[0]));
396
528
 
397
- const node = graph.nodes.find((node) => node.toBeUnrelatedOrDeleted);
398
-
399
- if (node) {
400
- const { graphUnrelateProp, graphDeleteProp } = node.modelClass;
401
-
402
- throw builder.modelClass().createValidationError({
403
- type: ValidationErrorType.InvalidGraph,
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
- }
410
-
411
- function checkForRootUnrelateOrDeleteErrors(graphData, builder) {
412
- for (const node of graphData.graph.nodes) {
413
- if (!node.parentEdge && node.toBeUnrelatedOrDeleted) {
414
- const { graphUnrelateProp, graphDeleteProp } = node.modelClass;
529
+ for (const node of plan.activeNodes.filter((node) => writtenNodes.has(node))) {
530
+ const { referencedNode, propertyReferences } = node;
415
531
 
532
+ if (
533
+ (referencedNode && !isReadable(referencedNode)) ||
534
+ propertyReferences.some(({ referencedNode, refPath }) => !isReadable(referencedNode, refPath))
535
+ ) {
416
536
  throw builder.modelClass().createValidationError({
417
537
  type: ValidationErrorType.InvalidGraph,
418
- message: `${graphUnrelateProp} and ${graphDeleteProp} can only be used for related models, not for root models`,
538
+ message: `the reference at ${formatPath(node.dataPathKey)} points to a model that the graph doesn't write before it`,
419
539
  });
420
540
  }
421
541
  }
422
542
  }
423
543
 
424
- function checkForHasManyRelateErrors(graphData) {
425
- const { graph, graphOptions } = graphData;
544
+ // Inserting or relating a model of a many-to-many relation inserts a join row,
545
+ // which would be a new related row if the join table is also the related table
546
+ // (#133).
547
+ function checkJoinTables(builder, plan) {
548
+ for (const node of plan.activeNodes) {
549
+ const action = plan.actions.get(node);
426
550
 
427
- for (const node of graph.nodes) {
428
551
  if (
429
- graphOptions.shouldRelate(node, graphData) &&
430
- node.parentEdge.relation.isObjectionHasManyRelation
552
+ node.relation?.keySide === 'join' &&
553
+ !node.current &&
554
+ (action === 'insert' || action === 'relate')
431
555
  ) {
432
- throw new Error(
433
- 'You cannot relate HasManyRelation or HasOneRelation using insertGraph, because those require update operations. Consider using upsertGraph instead.',
434
- );
556
+ node.relation.assertCanModifyJoinTable(builder, action);
435
557
  }
436
558
  }
437
559
  }
438
560
 
439
- // `upsertGraph` upserts the branches below related models recursively, but
440
- // `insertGraph` prunes them, which drops the models that would be inserted
441
- // there (#217). A warning is logged once.
442
- // TODO: Throw an error instead in objection 4.0 (#53).
443
- function warnOnInsertBelowRelate(graphData) {
444
- const { graph, graphOptions } = graphData;
445
-
446
- const hasInsertBelowRelate = graph.nodes.some(
447
- (node) =>
448
- graphOptions.shouldRelate(node, graphData) &&
449
- node.descendantRelationNodes.some((descendantNode) =>
450
- graphOptions.shouldInsert(descendantNode, graphData),
451
- ),
452
- );
453
-
454
- if (hasInsertBelowRelate) {
455
- warnOnce(
456
- 'insertGraph() ignores new models below related models. ' +
457
- 'Insert them with a separate query or use upsertGraph() instead. ' +
458
- 'This will throw in objection 4.0.',
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
- );
485
- };
486
- }
487
-
488
- function executeActions(builder, actions) {
489
- return actions.reduce(
490
- (promise, action) => promise.then(() => action.run(builder)),
491
- Promise.resolve(),
492
- );
561
+ // Name the place of a model in error messages, by its data path.
562
+ function formatPath(dataPathKey) {
563
+ const path = dataPathKey.replace(/^\./, '');
564
+ return path ? `'${path}'` : 'the root';
493
565
  }
494
566
 
495
- // The reference markers only describe the input graph, don't return them.
496
- // `nodes` are the nodes before pruning, since the results of pruned branches
497
- // contain markers too.
498
- function removeReferenceProps(nodes) {
499
- return () => {
500
- for (const node of nodes) {
501
- delete node.obj[node.modelClass.uidRefProp];
502
- delete node.obj[node.modelClass.dbRefProp];
503
- }
504
- };
567
+ function isOnConflictOperation(operation) {
568
+ return ['onConflict', 'ignore', 'merge'].includes(operation.name);
505
569
  }
506
-
507
- function isOnConflictOperation(op) {
508
- return op.name === 'onConflict' || op.name === 'ignore' || op.name === 'merge';
509
- }
510
-
511
- function returnResult(objects, isArray) {
512
- return () => (isArray ? objects : objects[0]);
513
- }
514
-
515
- module.exports = {
516
- GraphUpsert,
517
- };