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.
Files changed (250) 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 +314 -0
  81. package/lib/queryBuilder/graph/GraphNode.js +241 -0
  82. package/lib/queryBuilder/graph/GraphOptions.js +55 -248
  83. package/lib/queryBuilder/graph/GraphPlan.js +431 -0
  84. package/lib/queryBuilder/graph/GraphSchedule.js +513 -0
  85. package/lib/queryBuilder/graph/GraphUpsert.js +519 -412
  86. package/lib/queryBuilder/graph/GraphWriter.js +508 -0
  87. package/lib/queryBuilder/graph/graphKeys.js +929 -0
  88. package/lib/queryBuilder/join/JoinResultColumn.js +1 -7
  89. package/lib/queryBuilder/join/JoinResultParser.js +6 -11
  90. package/lib/queryBuilder/join/RelationJoiner.js +66 -84
  91. package/lib/queryBuilder/join/TableNode.js +2 -8
  92. package/lib/queryBuilder/join/TableTree.js +3 -9
  93. package/lib/queryBuilder/join/utils.js +5 -15
  94. package/lib/queryBuilder/jsonDialects/JsonDialect.js +51 -0
  95. package/lib/queryBuilder/jsonDialects/MySqlJsonDialect.js +81 -0
  96. package/lib/queryBuilder/jsonDialects/PostgresJsonDialect.js +45 -0
  97. package/lib/queryBuilder/jsonDialects/SqliteJsonDialect.js +67 -0
  98. package/lib/queryBuilder/jsonDialects/index.js +44 -0
  99. package/lib/queryBuilder/operations/DelegateOperation.js +2 -8
  100. package/lib/queryBuilder/operations/DeleteOperation.js +10 -39
  101. package/lib/queryBuilder/operations/FindByIdOperation.js +7 -12
  102. package/lib/queryBuilder/operations/FindByIdsOperation.js +4 -8
  103. package/lib/queryBuilder/operations/FindOperation.js +14 -34
  104. package/lib/queryBuilder/operations/FirstOperation.js +6 -10
  105. package/lib/queryBuilder/operations/FromOperation.js +3 -9
  106. package/lib/queryBuilder/operations/InsertAndFetchOperation.js +13 -12
  107. package/lib/queryBuilder/operations/InsertGraphAndFetchOperation.js +4 -10
  108. package/lib/queryBuilder/operations/InsertGraphOperation.js +7 -18
  109. package/lib/queryBuilder/operations/InsertOperation.js +118 -45
  110. package/lib/queryBuilder/operations/InstanceDeleteOperation.js +5 -12
  111. package/lib/queryBuilder/operations/InstanceFindOperation.js +4 -11
  112. package/lib/queryBuilder/operations/InstanceInsertOperation.js +2 -8
  113. package/lib/queryBuilder/operations/InstanceUpdateOperation.js +7 -14
  114. package/lib/queryBuilder/operations/JoinRelatedOperation.js +5 -11
  115. package/lib/queryBuilder/operations/KnexOperation.js +2 -8
  116. package/lib/queryBuilder/operations/LimitPerParentOperation.js +7 -69
  117. package/lib/queryBuilder/operations/MergeOperation.js +5 -11
  118. package/lib/queryBuilder/operations/NoneOperation.js +2 -8
  119. package/lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js +20 -49
  120. package/lib/queryBuilder/operations/OnBuildKnexOperation.js +2 -8
  121. package/lib/queryBuilder/operations/OnBuildOperation.js +2 -8
  122. package/lib/queryBuilder/operations/OnErrorOperation.js +2 -8
  123. package/lib/queryBuilder/operations/QueryBuilderOperation.js +1 -7
  124. package/lib/queryBuilder/operations/RangeOperation.js +11 -26
  125. package/lib/queryBuilder/operations/RelateOperation.js +2 -8
  126. package/lib/queryBuilder/operations/ReturningOperation.js +3 -9
  127. package/lib/queryBuilder/operations/RunAfterOperation.js +2 -12
  128. package/lib/queryBuilder/operations/RunBeforeOperation.js +2 -8
  129. package/lib/queryBuilder/operations/UnrelateOperation.js +2 -8
  130. package/lib/queryBuilder/operations/UpdateAndFetchOperation.js +14 -17
  131. package/lib/queryBuilder/operations/UpdateOperation.js +32 -132
  132. package/lib/queryBuilder/operations/UpsertGraphAndFetchOperation.js +8 -11
  133. package/lib/queryBuilder/operations/UpsertGraphOperation.js +6 -12
  134. package/lib/queryBuilder/operations/WhereCompositeOperation.js +3 -9
  135. package/lib/queryBuilder/operations/WhereGroupOperation.js +2 -8
  136. package/lib/queryBuilder/operations/eager/EagerOperation.js +3 -9
  137. package/lib/queryBuilder/operations/eager/JoinEagerOperation.js +72 -10
  138. package/lib/queryBuilder/operations/eager/NaiveEagerOperation.js +2 -8
  139. package/lib/queryBuilder/operations/eager/WhereInEagerOperation.js +23 -44
  140. package/lib/queryBuilder/operations/fieldExpressions.js +122 -0
  141. package/lib/queryBuilder/operations/jsonApi/WhereJsonHasOperation.js +20 -0
  142. package/lib/queryBuilder/operations/jsonApi/WhereJsonOperation.js +44 -0
  143. package/lib/queryBuilder/operations/jsonApi/WhereJsonTypeOperation.js +14 -0
  144. package/lib/queryBuilder/operations/select/SelectOperation.js +11 -21
  145. package/lib/queryBuilder/operations/select/Selection.js +19 -17
  146. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeMsSqlOperation.js +4 -10
  147. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeOperation.js +4 -10
  148. package/lib/queryBuilder/operations/whereInComposite/WhereInCompositeSqliteOperation.js +4 -10
  149. package/lib/queryBuilder/operations/windowOrder.js +56 -0
  150. package/lib/queryBuilder/parsers/jsonFieldExpressionParser.js +3 -7
  151. package/lib/queryBuilder/parsers/relationExpressionParser.js +42 -43
  152. package/lib/queryBuilder/parsers/relationExpressionParser.pegjs +39 -36
  153. package/lib/queryBuilder/transformations/CompositeQueryTransformation.js +2 -8
  154. package/lib/queryBuilder/transformations/QueryTransformation.js +1 -7
  155. package/lib/queryBuilder/transformations/WrapMysqlModifySubqueryTransformation.js +3 -9
  156. package/lib/queryBuilder/transformations/index.js +3 -11
  157. package/lib/queryBuilder/whereGroups.js +9 -24
  158. package/lib/queryBuilder/withoutOrder.js +4 -11
  159. package/lib/relations/Relation.js +47 -28
  160. package/lib/relations/RelationDeleteOperation.js +4 -14
  161. package/lib/relations/RelationFindOperation.js +119 -90
  162. package/lib/relations/RelationInsertOperation.js +2 -8
  163. package/lib/relations/RelationOwner.js +5 -11
  164. package/lib/relations/RelationProperty.js +4 -10
  165. package/lib/relations/RelationUpdateOperation.js +4 -14
  166. package/lib/relations/RelationWriteResultOperation.js +15 -0
  167. package/lib/relations/belongsToMany/BelongsToManyDeleteOperation.js +23 -0
  168. package/lib/relations/belongsToMany/BelongsToManyFindOperation.js +35 -0
  169. package/lib/relations/belongsToMany/BelongsToManyInsertOperation.js +28 -0
  170. package/lib/relations/belongsToMany/BelongsToManyRelateOperation.js +16 -0
  171. package/lib/relations/belongsToMany/BelongsToManyRelation.js +276 -0
  172. package/lib/relations/belongsToMany/BelongsToManyUnrelateOperation.js +11 -0
  173. package/lib/relations/belongsToOne/BelongsToOneDeleteOperation.js +2 -8
  174. package/lib/relations/belongsToOne/BelongsToOneInsertOperation.js +6 -11
  175. package/lib/relations/belongsToOne/BelongsToOneRelateOperation.js +3 -9
  176. package/lib/relations/belongsToOne/BelongsToOneRelation.js +8 -19
  177. package/lib/relations/belongsToOne/BelongsToOneUnrelateOperation.js +53 -27
  178. package/lib/relations/hasMany/HasManyInsertOperation.js +5 -10
  179. package/lib/relations/hasMany/HasManyRelateOperation.js +5 -11
  180. package/lib/relations/hasMany/HasManyRelation.js +9 -11
  181. package/lib/relations/hasMany/HasManyUnrelateOperation.js +3 -9
  182. package/lib/relations/hasOne/HasOneRelation.js +2 -8
  183. package/lib/relations/hasOneThrough/HasOneThroughRelation.js +2 -8
  184. package/lib/relations/manyToMany/ManyToManyModifyMixin.js +10 -15
  185. package/lib/relations/manyToMany/ManyToManyMySqlModifyMixin.js +2 -8
  186. package/lib/relations/manyToMany/ManyToManyRelation.js +82 -50
  187. package/lib/relations/manyToMany/ManyToManySqliteModifyMixin.js +2 -8
  188. package/lib/relations/manyToMany/delete/ManyToManyDeleteOperation.js +5 -9
  189. package/lib/relations/manyToMany/delete/ManyToManyDeleteOperationBase.js +2 -8
  190. package/lib/relations/manyToMany/delete/ManyToManyDeleteSqliteOperation.js +3 -9
  191. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +20 -59
  192. package/lib/relations/manyToMany/insert/ManyToManyInsertOperation.js +6 -11
  193. package/lib/relations/manyToMany/relate/ManyToManyRelateOperation.js +4 -10
  194. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateMySqlOperation.js +3 -9
  195. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperation.js +5 -9
  196. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateOperationBase.js +3 -9
  197. package/lib/relations/manyToMany/unrelate/ManyToManyUnrelateSqliteOperation.js +3 -9
  198. package/lib/relations/manyToMany/update/ManyToManyUpdateMySqlOperation.js +3 -9
  199. package/lib/relations/manyToMany/update/ManyToManyUpdateOperation.js +5 -9
  200. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +64 -72
  201. package/lib/relations/manyToMany/update/ManyToManyUpdateSqliteOperation.js +3 -9
  202. package/lib/transaction.js +3 -9
  203. package/lib/utils/assert.js +3 -11
  204. package/lib/utils/buildUtils.js +2 -8
  205. package/lib/utils/classUtils.js +1 -7
  206. package/lib/utils/createModifier.js +2 -8
  207. package/lib/utils/identifierMapping.js +23 -27
  208. package/lib/utils/internalPropUtils.js +1 -7
  209. package/lib/utils/knexUtils.js +84 -40
  210. package/lib/utils/mixin.js +3 -10
  211. package/lib/utils/normalizeIds.js +2 -8
  212. package/lib/utils/objectUtils.js +41 -60
  213. package/lib/utils/parseFieldExpression.js +16 -16
  214. package/lib/utils/promiseUtils/after.js +2 -8
  215. package/lib/utils/promiseUtils/afterReturn.js +2 -8
  216. package/lib/utils/promiseUtils/index.js +7 -16
  217. package/lib/utils/promiseUtils/isPromise.js +2 -8
  218. package/lib/utils/promiseUtils/map.js +2 -8
  219. package/lib/utils/promiseUtils/mapAfterAllReturn.js +2 -8
  220. package/lib/utils/promiseUtils/try.js +2 -8
  221. package/lib/utils/resolveModel.js +10 -17
  222. package/lib/utils/tmpColumnUtils.js +2 -9
  223. package/lib/utils/warnings.js +2 -14
  224. package/package.json +25 -17
  225. package/typings/objection/index.d.ts +3217 -2179
  226. package/lib/.eslintrc.json +0 -8
  227. package/lib/model/graph/ModelGraph.js +0 -80
  228. package/lib/model/graph/ModelGraphBuilder.js +0 -241
  229. package/lib/model/graph/ModelGraphEdge.js +0 -52
  230. package/lib/model/graph/ModelGraphNode.js +0 -238
  231. package/lib/queryBuilder/graph/GraphAction.js +0 -69
  232. package/lib/queryBuilder/graph/GraphData.js +0 -53
  233. package/lib/queryBuilder/graph/GraphFetcher.js +0 -197
  234. package/lib/queryBuilder/graph/GraphNodeDbExistence.js +0 -184
  235. package/lib/queryBuilder/graph/GraphOperation.js +0 -27
  236. package/lib/queryBuilder/graph/delete/GraphDelete.js +0 -73
  237. package/lib/queryBuilder/graph/delete/GraphDeleteAction.js +0 -88
  238. package/lib/queryBuilder/graph/insert/DeferredRelationGraphInsertAction.js +0 -58
  239. package/lib/queryBuilder/graph/insert/GraphInsert.js +0 -296
  240. package/lib/queryBuilder/graph/insert/GraphInsertAction.js +0 -233
  241. package/lib/queryBuilder/graph/insert/JoinRowGraphInsertAction.js +0 -92
  242. package/lib/queryBuilder/graph/patch/GraphPatch.js +0 -20
  243. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +0 -289
  244. package/lib/queryBuilder/graph/recursiveUpsert/GraphRecursiveUpsert.js +0 -31
  245. package/lib/queryBuilder/graph/recursiveUpsert/GraphRecursiveUpsertAction.js +0 -126
  246. package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +0 -30
  247. package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +0 -37
  248. package/lib/queryBuilder/operations/jsonApi/WhereJsonPostgresOperation.js +0 -62
  249. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +0 -192
  250. package/lib/utils/wrapDbError.js +0 -64
@@ -1,30 +1,27 @@
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, 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
- checkCanBeConvertedToModels(rootModelClass, objects);
22
- warnOnUnknownOptions(upsertOptions);
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.upsertOpt = upsertOptions;
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
- // 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) {
36
+ static checkForOnConflict(builder, method) {
42
37
  if (builder.has(isOnConflictOperation)) {
43
- warnOnce(
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 modelClass = builder.modelClass();
53
- const graphOptions = new GraphOptions(this.upsertOpt, [], this.insertOnly);
54
-
55
- const graph = ModelGraph.create(modelClass, this.objects);
56
- assignDbRefsAsRelateProps(graph);
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
- 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
- }
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
- // 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
- }
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
- 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
- );
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
- function checkCanBeConvertedToModels(modelClass, objects) {
88
- asArray(objects).forEach((obj) => {
89
- if (!isObject(obj)) {
90
- throw createNotModelError(modelClass, obj);
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
- function assignDbRefsAsRelateProps(graph) {
96
- for (const node of graph.nodes) {
97
- if (!node.parentEdge || !node.parentEdge.relation || !node.isDbReference) {
98
- continue;
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
- node.parentEdge.relation.setRelateProp(node.obj, asArray(node.dbReference));
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
- const nodeDbExistence = await GraphNodeDbExistence.create({
109
- builder,
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
- // 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);
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
- removeBranchesFromGraph(findRoots(skippedNodes), graph);
151
+ return [laterNodes, keys];
169
152
  }
170
153
 
171
- function isReferencedFromOutside(nodes) {
172
- const nodeSet = new Set(nodes);
173
-
174
- return nodes.some((node) =>
175
- node.refEdges.some((edge) => edge.isRelatedNode(node) && !nodeSet.has(edge.ownerNode)),
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
- function pruneDeletedBranches(graphData) {
180
- const { graph, currentGraph } = graphData;
181
-
182
- const deleteNodes = currentGraph.nodes.filter((currentNode) => !graph.nodeForNode(currentNode));
183
- const roots = findRoots(deleteNodes);
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
- // 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;
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
- const { relation } = deleteRoot.parentEdge;
194
- const parentNode = graph.nodeForNode(deleteRoot.parentNode);
210
+ for (const node of plan.activeNodes) {
211
+ const { relation, parent } = node;
212
+ const action = actions.get(node);
195
213
 
196
- if (!parentNode) {
197
- return false;
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
- return parentNode.obj[relation.name] === undefined;
201
- });
230
+ // `null` clears the key:
231
+ const nullNames = node.modelClass.getRelationNames().filter((name) => node.obj[name] === null);
202
232
 
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);
233
+ for (const name of nullNames) {
234
+ const nullRelation = node.modelClass.getRelation(name);
215
235
 
216
- removeBranchesFromGraph(roots, graph);
217
- removeBranchesFromGraph(currentRoots, currentGraph);
236
+ if (nullRelation.keySide === 'owner' && nullRelation.isOneToOne()) {
237
+ checkKey(node, nullRelation, nullRelation.ownerProp, () => null);
238
+ }
239
+ }
240
+ }
218
241
  }
219
242
 
220
- function findRoots(nodes) {
221
- const nodeSet = new Set(nodes);
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
- return uniqBy(
224
- nodes.filter((node) => {
225
- let parentNode = node.parentNode;
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
- while (parentNode) {
228
- if (nodeSet.has(parentNode)) {
229
- return false;
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
- parentNode = parentNode.parentNode;
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
- return true;
236
- }),
237
- );
238
- }
278
+ if (markedNode) {
279
+ const { graphUnrelateProp, graphDeleteProp } = markedNode.modelClass;
280
+ const markers = `${graphUnrelateProp} and ${graphDeleteProp}`;
239
281
 
240
- function removeBranchesFromGraph(branchRoots, graph) {
241
- const nodesToRemove = new Set(
242
- branchRoots.reduce(
243
- (nodesToRemove, node) => [...nodesToRemove, ...node.descendantRelationNodes],
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
- removeNodesFromGraph(nodesToRemove, graph);
249
- }
295
+ if (graphOptions.isInsertOnly()) {
296
+ const relatedNodes = nodes.filter((node) => plan.actionFor(node) === 'relate');
250
297
 
251
- function removeNodesFromGraph(nodesToRemove, graph) {
252
- const edgesToRemove = new Set();
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
- for (const node of nodesToRemove) {
255
- for (const edge of node.edges) {
256
- const otherNode = edge.getOtherNode(node);
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 (!nodesToRemove.has(otherNode)) {
259
- otherNode.removeEdge(edge);
260
- edgesToRemove.add(edge);
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
- 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);
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
- return graphData;
339
+ return newRowIds.get(target);
340
+ }
287
341
  };
288
- }
289
342
 
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
- }
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
- 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,
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
- if (allowedExpression) {
358
- const rootsObjs = graph.nodes.filter((node) => !node.parentEdge).map((node) => node.obj);
359
- const expression = RelationExpression.fromModelGraph(rootsObjs);
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
- if (!allowedExpression.isSubExpression(expression)) {
362
- throw builder.modelClass().createValidationError({
363
- type: ValidationErrorType.UnallowedRelation,
364
- message: 'trying to upsert an unallowed relation',
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
- function checkForUnallowedReferenceErrors(graphData, builder) {
371
- const { graph, graphOptions } = graphData;
474
+ setNodeValue(node, '#delete', isDeleted, path);
372
475
 
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();
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
- if (graphOptions.allowMarkers() && !isInsertOnly) {
394
- return;
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
- const node = graph.nodes.find((node) => node.toBeUnrelatedOrDeleted);
398
-
399
- if (node) {
400
- const { graphUnrelateProp, graphDeleteProp } = node.modelClass;
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
- 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
- }
530
+ return index;
531
+ };
410
532
 
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;
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: `${graphUnrelateProp} and ${graphDeleteProp} can only be used for related models, not for root models`,
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
- function checkForHasManyRelateErrors(graphData) {
425
- const { graph, graphOptions } = graphData;
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
- graphOptions.shouldRelate(node, graphData) &&
430
- node.parentEdge.relation.isObjectionHasManyRelation
569
+ (referencedNode && !isReadable(referencedNode)) ||
570
+ propertyReferences.some(({ referencedNode, refPath }) => !isReadable(referencedNode, refPath))
431
571
  ) {
432
- throw new Error(
433
- 'You cannot relate HasManyRelation or HasOneRelation using insertGraph, because those require update operations. Consider using upsertGraph instead.',
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
- // `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
- );
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
- function executeActions(builder, actions) {
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
- // 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
- };
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
- function isOnConflictOperation(op) {
508
- return op.name === 'onConflict' || op.name === 'ignore' || op.name === 'merge';
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 returnResult(objects, isArray) {
512
- return () => (isArray ? objects : objects[0]);
622
+ function isOnConflictOperation(operation) {
623
+ return ['onConflict', 'ignore', 'merge'].includes(operation.name);
513
624
  }
514
-
515
- module.exports = {
516
- GraphUpsert,
517
- };