GraphInserter.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. const Promise = require('bluebird');
  2. const DependencyGraph = require('./DependencyGraph');
  3. const TableInsertion = require('./TableInsertion');
  4. const { uniqBy } = require('../../utils/objectUtils');
  5. class GraphInserter {
  6. constructor(args) {
  7. this.allowedRelations = args.allowedRelations || null;
  8. this.queryContext = args.queryContext;
  9. this.modelClass = args.modelClass;
  10. this.models = args.models;
  11. this.knex = args.knex;
  12. this.opt = args.opt;
  13. this.graph = this._buildDependencyGraph();
  14. }
  15. execute(inserter) {
  16. return Promise.try(() => this._executeNormalBatches(inserter))
  17. .then(() => this._executeJoinRowBatch(inserter))
  18. .then(() => this._finalize());
  19. }
  20. _buildDependencyGraph() {
  21. const graph = new DependencyGraph(this.opt, this.allowedRelations);
  22. graph.build(this.modelClass, this.models);
  23. return graph;
  24. }
  25. _executeNormalBatches(inserter) {
  26. return this._executeNextBatch(inserter);
  27. }
  28. _executeNextBatch(inserter) {
  29. const batch = this._nextBatch();
  30. if (!batch) {
  31. // No more normal batches to execute.
  32. return null;
  33. }
  34. // Since we are not performing the inserts using relation.insert()
  35. // we need to explicitly run the beforeInsert hooks here.
  36. return this._beforeInsertBatch(batch, 'executeBeforeInsert')
  37. .then(() => {
  38. // Insert the batch one table at a time.
  39. return Promise.map(
  40. Object.keys(batch),
  41. tableName => {
  42. const tableInsertion = batch[tableName];
  43. // We need to omit the uid properties so that they don't get inserted
  44. // into the database.
  45. this._omitUids(tableInsertion);
  46. return inserter(tableInsertion).then(() => {
  47. // Resolve dependencies to the inserted objects.
  48. return this._resolveDepsForInsertion(tableInsertion);
  49. });
  50. },
  51. { concurrency: this.modelClass.concurrency }
  52. );
  53. })
  54. .then(() => {
  55. return this._executeNextBatch(inserter);
  56. });
  57. }
  58. _nextBatch() {
  59. const batch = this._createBatch();
  60. if (batch) {
  61. // Mark the batch as inserted now even though the its is not yet inserted.
  62. // It is the very next thing we are going to do. We need to do this here
  63. // because the uid gets removed before insert.
  64. this._markBatchInserted(batch);
  65. }
  66. return batch;
  67. }
  68. _createBatch() {
  69. const batch = Object.create(null);
  70. const nodes = this.graph.nodes;
  71. let empty = true;
  72. for (let n = 0, ln = nodes.length; n < ln; ++n) {
  73. const node = nodes[n];
  74. if (!node.handled && !node.hasUnresolvedDependencies) {
  75. let tableInsertion = batch[node.modelClass.getTableName()];
  76. if (!tableInsertion) {
  77. tableInsertion = new TableInsertion(node.modelClass, false);
  78. batch[node.modelClass.getTableName()] = tableInsertion;
  79. }
  80. tableInsertion.items.push({
  81. model: node.model,
  82. relation: node.relation,
  83. node
  84. });
  85. empty = false;
  86. }
  87. }
  88. if (empty) {
  89. return null;
  90. } else {
  91. return batch;
  92. }
  93. }
  94. _beforeInsertBatch(batch, executorMethod) {
  95. const tableNames = Object.keys(batch);
  96. const modelsByRelation = new Map();
  97. for (let t = 0, lt = tableNames.length; t < lt; ++t) {
  98. const tableName = tableNames[t];
  99. const tableInsertion = batch[tableName];
  100. for (let i = 0, li = tableInsertion.items.length; i < li; ++i) {
  101. const item = tableInsertion.items[i];
  102. const model = item.model;
  103. const relation = item.relation;
  104. if (relation) {
  105. let relModels = modelsByRelation.get(relation);
  106. if (relModels === undefined) {
  107. relModels = [];
  108. modelsByRelation.set(relation, relModels);
  109. }
  110. relModels.push(model);
  111. }
  112. }
  113. }
  114. return Promise.map(
  115. Array.from(modelsByRelation.keys()),
  116. relation => {
  117. const models = modelsByRelation.get(relation);
  118. return relation[executorMethod](models, this.queryContext, null);
  119. },
  120. { concurrency: this.modelClass.concurrency }
  121. );
  122. }
  123. _markBatchInserted(batch) {
  124. const tableNames = Object.keys(batch);
  125. for (let t = 0, lt = tableNames.length; t < lt; ++t) {
  126. const tableInsertion = batch[tableNames[t]];
  127. const modelClass = tableInsertion.modelClass;
  128. const items = tableInsertion.items;
  129. for (let i = 0, li = items.length; i < li; ++i) {
  130. items[i].node.markAsInserted();
  131. }
  132. }
  133. }
  134. _executeJoinRowBatch(inserter) {
  135. const batch = this._createJoinRowBatch();
  136. // Since we are not performing the inserts using relation.insert()
  137. // we need to explicitly run the beforeInsert hooks here.
  138. return this._beforeInsertBatch(batch, 'executeJoinTableBeforeInsert').then(() => {
  139. // Insert the batch one table at a time.
  140. return Promise.map(Object.keys(batch), tableName => inserter(batch[tableName]), {
  141. concurrency: this.modelClass.concurrency
  142. });
  143. });
  144. }
  145. _createJoinRowBatch() {
  146. const batch = Object.create(null);
  147. for (let n = 0, ln = this.graph.nodes.length; n < ln; ++n) {
  148. const node = this.graph.nodes[n];
  149. for (let m = 0, lm = node.manyToManyConnections.length; m < lm; ++m) {
  150. const conn = node.manyToManyConnections[m];
  151. let tableInsertion = batch[conn.relation.joinTable];
  152. const ownerProp = conn.relation.ownerProp.getProps(node.model);
  153. const modelClass = conn.relation.getJoinModelClass(this.knex);
  154. let joinModel = conn.relation.createJoinModels(ownerProp, [conn.node.model])[0];
  155. if (conn.refNode) {
  156. // Also take extra properties from the referring model, it there was one.
  157. for (let k = 0, lk = conn.relation.joinTableExtras.length; k < lk; ++k) {
  158. const extra = conn.relation.joinTableExtras[k];
  159. if (conn.refNode.model[extra.aliasProp] !== undefined) {
  160. joinModel[extra.joinTableProp] = conn.refNode.model[extra.aliasProp];
  161. }
  162. }
  163. }
  164. joinModel = modelClass.fromJson(joinModel);
  165. if (!tableInsertion) {
  166. tableInsertion = new TableInsertion(modelClass, true);
  167. batch[modelClass.getTableName()] = tableInsertion;
  168. }
  169. tableInsertion.items.push({
  170. model: joinModel,
  171. relation: conn.relation,
  172. node: conn.node
  173. });
  174. }
  175. }
  176. return this._removeJoinRowDuplicatesFromBatch(batch);
  177. }
  178. _removeJoinRowDuplicatesFromBatch(batch) {
  179. const tableNames = Object.keys(batch);
  180. for (let t = 0, lt = tableNames.length; t < lt; ++t) {
  181. const tableName = tableNames[t];
  182. const tableInsertion = batch[tableName];
  183. if (tableInsertion.items.length) {
  184. const items = tableInsertion.items;
  185. const keySet = new Set();
  186. const keys = [];
  187. for (let i = 0, li = items.length; i < li; ++i) {
  188. const item = items[i];
  189. const model = item.model;
  190. const modelKeys = Object.keys(model);
  191. for (let k = 0, lk = modelKeys.length; k < lk; ++k) {
  192. const key = modelKeys[k];
  193. if (!keySet.has(key)) {
  194. keySet.add(modelKeys[k]);
  195. keys.push(key);
  196. }
  197. }
  198. }
  199. tableInsertion.items = uniqBy(items, item => item.model.$propKey(keys));
  200. }
  201. }
  202. return batch;
  203. }
  204. _omitUids(tableInsertion) {
  205. const modelClass = tableInsertion.modelClass;
  206. const uidProp = modelClass.uidProp;
  207. for (let i = 0, li = tableInsertion.items.length; i < li; ++i) {
  208. const item = tableInsertion.items[i];
  209. const model = item.model;
  210. modelClass.omitImpl(model, uidProp);
  211. }
  212. }
  213. _resolveDepsForInsertion(tableInsertion) {
  214. for (let i = 0, li = tableInsertion.items.length; i < li; ++i) {
  215. const node = tableInsertion.items[i].node;
  216. const model = tableInsertion.items[i].model;
  217. for (let d = 0, ld = node.isNeededBy.length; d < ld; ++d) {
  218. node.isNeededBy[d].resolve(model);
  219. }
  220. }
  221. }
  222. _finalize() {
  223. for (let n = 0, ln = this.graph.nodes.length; n < ln; ++n) {
  224. const refNode = this.graph.nodes[n];
  225. const modelClass = refNode.modelClass;
  226. const ref = refNode.model[modelClass.uidRefProp];
  227. if (ref) {
  228. // Copy all the properties to the reference nodes.
  229. const actualNode = this.graph.nodesById.get(ref);
  230. const relations = actualNode.modelClass.getRelations();
  231. const keys = Object.keys(actualNode.model);
  232. for (let i = 0, l = keys.length; i < l; ++i) {
  233. const key = keys[i];
  234. const value = actualNode.model[key];
  235. if (!relations[key] && typeof value !== 'function') {
  236. refNode.model[key] = value;
  237. }
  238. }
  239. modelClass.omitImpl(refNode.model, modelClass.uidProp);
  240. modelClass.omitImpl(refNode.model, modelClass.uidRefProp);
  241. modelClass.omitImpl(refNode.model, modelClass.dbRefProp);
  242. } else if (refNode.model[modelClass.uidProp]) {
  243. // Make sure the model no longer has an uid.
  244. modelClass.omitImpl(refNode.model, modelClass.uidProp);
  245. modelClass.omitImpl(refNode.model, modelClass.dbRefProp);
  246. }
  247. }
  248. return Promise.resolve(this.models);
  249. }
  250. }
  251. module.exports = GraphInserter;