UpsertGraph.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. const RelationExpression = require('../RelationExpression');
  2. const UpsertNode = require('./UpsertNode');
  3. const { isSqlite } = require('../../utils/knexUtils');
  4. const { asArray } = require('../../utils/objectUtils');
  5. const { keyByProps } = require('../../model/modelUtils');
  6. const { appendDataPath } = require('../../utils/dataPath');
  7. const { Type: ValidationErrorType } = require('../../model/ValidationError');
  8. // Given an upsert model graph, creates a set of nodes that describe what to do
  9. // to each individual model in the graph. node.types returns the needed actions
  10. // (any of insert, relate, update, delete and unrelate). This class determines
  11. // the needed actions by fetching the current state of the graph from the
  12. // database. Only ids and foreign keys needed by the relations are fetched.
  13. class UpsertGraph {
  14. constructor(upsert, isArray, opt) {
  15. this.upsert = upsert;
  16. this.isArray = isArray;
  17. this.rootModelClass = this.upsert[0].constructor;
  18. this.relExpr = RelationExpression.fromModelGraph(upsert);
  19. this.nodes = [];
  20. // Keys are upsert models and values are corresponding nodes.
  21. this.nodesByUpsert = new Map();
  22. this.opt = opt || {};
  23. }
  24. build(builder) {
  25. return this.fetchCurrentState(builder).then(currentState => this.buildGraph(currentState));
  26. }
  27. // Fetches the current state of the graph from the database. This method
  28. // only fetches ids and all foreign keys needed by the relations.
  29. fetchCurrentState(builder) {
  30. const rootIds = getRootIds(this.upsert);
  31. const rootIdCols = builder.fullIdColumnFor(this.rootModelClass);
  32. const allowedExpr = builder.allowedUpsertExpression();
  33. const oldContext = builder.context();
  34. if (allowedExpr && !allowedExpr.isSubExpression(this.relExpr)) {
  35. const modelClass = builder.modelClass();
  36. throw modelClass.createValidationError({
  37. type: ValidationErrorType.UnallowedRelation,
  38. message: 'trying to upsert an unallowed relation'
  39. });
  40. }
  41. if (rootIds.length === 0) {
  42. return Promise.resolve([]);
  43. }
  44. return builder
  45. .modelClass()
  46. .query()
  47. .childQueryOf(builder, true)
  48. .whereInComposite(rootIdCols, rootIds)
  49. .eager(this.relExpr)
  50. .internalOptions({
  51. keepImplicitJoinProps: true
  52. })
  53. .mergeContext({
  54. onBuild(builder) {
  55. // There may be an onBuild hook in the old context.
  56. if (oldContext.onBuild) {
  57. oldContext.onBuild(builder);
  58. }
  59. const modelClass = builder.modelClass();
  60. const idColumn = builder.fullIdColumnFor(modelClass);
  61. builder.select(idColumn);
  62. }
  63. });
  64. }
  65. buildGraph(current) {
  66. this.doBuildGraph({
  67. modelClass: this.rootModelClass,
  68. upsert: this.upsert,
  69. current: current,
  70. isArray: this.isArray,
  71. parentNode: null,
  72. relExpr: this.relExpr,
  73. dataPath: null
  74. });
  75. }
  76. doBuildGraph({ modelClass, upsert, current, isArray, parentNode, relExpr, dataPath }) {
  77. this.buildGraphArray({
  78. modelClass,
  79. upsert: ensureArray(upsert),
  80. current: ensureArray(current),
  81. isArray,
  82. parentNode,
  83. relExpr,
  84. dataPath
  85. });
  86. }
  87. buildGraphArray({ modelClass, upsert, current, isArray, parentNode, relExpr, dataPath }) {
  88. const idProp = modelClass.getIdPropertyArray();
  89. const currentById = keyByProps(current, idProp);
  90. const upsertById = keyByProps(upsert, idProp);
  91. upsert.forEach((upsert, index) => {
  92. const key = upsert.$propKey(idProp);
  93. const current = currentById.get(key);
  94. const nextDataPath = isArray ? appendDataPath(dataPath, index) : dataPath;
  95. this.buildGraphSingle({
  96. modelClass,
  97. upsert,
  98. current,
  99. parentNode,
  100. relExpr,
  101. dataPath: nextDataPath
  102. });
  103. });
  104. current.forEach(current => {
  105. const key = current.$propKey(idProp);
  106. const upsert = upsertById.get(key);
  107. if (!upsert) {
  108. // These nodes result in delete and unrelate operations and nothing gets validated.
  109. // Use an index of -1 for dataPath here, as it should never actually get used.
  110. const nextDataPath = isArray ? appendDataPath(dataPath, -1) : dataPath;
  111. this.buildGraphSingle({
  112. modelClass,
  113. upsert,
  114. current,
  115. parentNode,
  116. relExpr,
  117. dataPath: nextDataPath
  118. });
  119. }
  120. });
  121. }
  122. buildGraphSingle({ modelClass, upsert, current, parentNode, relExpr, dataPath }) {
  123. if (!upsert && !current) {
  124. return;
  125. }
  126. const node = new UpsertNode({
  127. parentNode,
  128. relExpr,
  129. upsertModel: upsert,
  130. currentModel: current,
  131. dataPath,
  132. opt: this.opt
  133. });
  134. this.nodes.push(node);
  135. if (upsert) {
  136. this.nodesByUpsert.set(upsert, node);
  137. }
  138. if (parentNode) {
  139. const relations = parentNode.relations;
  140. const relation = (relations[relExpr.$relation] = relations[relExpr.$relation] || []);
  141. relation.push(node);
  142. }
  143. // No need to build the graph down from a deleted node.
  144. if (node.upsertModel === null) {
  145. return;
  146. }
  147. // No need to build the graph down from a node which will be recursively upserted
  148. if (node.hasType(UpsertNode.Type.UpsertRecursively)) {
  149. return;
  150. }
  151. relExpr.forEachChildExpression(modelClass.getRelations(), (expr, relation) => {
  152. const relUpsert = upsert && upsert[relation.name];
  153. const relCurrent = current && current[relation.name];
  154. const nextDataPath = appendDataPath(dataPath, relation);
  155. this.doBuildGraph({
  156. modelClass: relation.relatedModelClass,
  157. upsert: relUpsert,
  158. current: relCurrent,
  159. isArray: Array.isArray(relUpsert),
  160. parentNode: node,
  161. relExpr: expr,
  162. dataPath: nextDataPath
  163. });
  164. });
  165. }
  166. }
  167. function getRootIds(graph) {
  168. return asArray(graph)
  169. .filter(it => it.$hasId())
  170. .map(root => root.$id());
  171. }
  172. function ensureArray(item) {
  173. if (item && !Array.isArray(item)) {
  174. return [item];
  175. } else if (!item) {
  176. return [];
  177. } else {
  178. return item;
  179. }
  180. }
  181. module.exports = UpsertGraph;