123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- const Promise = require('bluebird');
- const DependencyGraph = require('./DependencyGraph');
- const TableInsertion = require('./TableInsertion');
- const { uniqBy } = require('../../utils/objectUtils');
- class GraphInserter {
- constructor(args) {
- this.allowedRelations = args.allowedRelations || null;
- this.queryContext = args.queryContext;
- this.modelClass = args.modelClass;
- this.models = args.models;
- this.knex = args.knex;
- this.opt = args.opt;
- this.graph = this._buildDependencyGraph();
- }
- execute(inserter) {
- return Promise.try(() => this._executeNormalBatches(inserter))
- .then(() => this._executeJoinRowBatch(inserter))
- .then(() => this._finalize());
- }
- _buildDependencyGraph() {
- const graph = new DependencyGraph(this.opt, this.allowedRelations);
- graph.build(this.modelClass, this.models);
- return graph;
- }
- _executeNormalBatches(inserter) {
- return this._executeNextBatch(inserter);
- }
- _executeNextBatch(inserter) {
- const batch = this._nextBatch();
- if (!batch) {
- // No more normal batches to execute.
- return null;
- }
- // Since we are not performing the inserts using relation.insert()
- // we need to explicitly run the beforeInsert hooks here.
- return this._beforeInsertBatch(batch, 'executeBeforeInsert')
- .then(() => {
- // Insert the batch one table at a time.
- return Promise.map(
- Object.keys(batch),
- tableName => {
- const tableInsertion = batch[tableName];
- // We need to omit the uid properties so that they don't get inserted
- // into the database.
- this._omitUids(tableInsertion);
- return inserter(tableInsertion).then(() => {
- // Resolve dependencies to the inserted objects.
- return this._resolveDepsForInsertion(tableInsertion);
- });
- },
- { concurrency: this.modelClass.concurrency }
- );
- })
- .then(() => {
- return this._executeNextBatch(inserter);
- });
- }
- _nextBatch() {
- const batch = this._createBatch();
- if (batch) {
- // Mark the batch as inserted now even though the its is not yet inserted.
- // It is the very next thing we are going to do. We need to do this here
- // because the uid gets removed before insert.
- this._markBatchInserted(batch);
- }
- return batch;
- }
- _createBatch() {
- const batch = Object.create(null);
- const nodes = this.graph.nodes;
- let empty = true;
- for (let n = 0, ln = nodes.length; n < ln; ++n) {
- const node = nodes[n];
- if (!node.handled && !node.hasUnresolvedDependencies) {
- let tableInsertion = batch[node.modelClass.getTableName()];
- if (!tableInsertion) {
- tableInsertion = new TableInsertion(node.modelClass, false);
- batch[node.modelClass.getTableName()] = tableInsertion;
- }
- tableInsertion.items.push({
- model: node.model,
- relation: node.relation,
- node
- });
- empty = false;
- }
- }
- if (empty) {
- return null;
- } else {
- return batch;
- }
- }
- _beforeInsertBatch(batch, executorMethod) {
- const tableNames = Object.keys(batch);
- const modelsByRelation = new Map();
- for (let t = 0, lt = tableNames.length; t < lt; ++t) {
- const tableName = tableNames[t];
- const tableInsertion = batch[tableName];
- for (let i = 0, li = tableInsertion.items.length; i < li; ++i) {
- const item = tableInsertion.items[i];
- const model = item.model;
- const relation = item.relation;
- if (relation) {
- let relModels = modelsByRelation.get(relation);
- if (relModels === undefined) {
- relModels = [];
- modelsByRelation.set(relation, relModels);
- }
- relModels.push(model);
- }
- }
- }
- return Promise.map(
- Array.from(modelsByRelation.keys()),
- relation => {
- const models = modelsByRelation.get(relation);
- return relation[executorMethod](models, this.queryContext, null);
- },
- { concurrency: this.modelClass.concurrency }
- );
- }
- _markBatchInserted(batch) {
- const tableNames = Object.keys(batch);
- for (let t = 0, lt = tableNames.length; t < lt; ++t) {
- const tableInsertion = batch[tableNames[t]];
- const modelClass = tableInsertion.modelClass;
- const items = tableInsertion.items;
- for (let i = 0, li = items.length; i < li; ++i) {
- items[i].node.markAsInserted();
- }
- }
- }
- _executeJoinRowBatch(inserter) {
- const batch = this._createJoinRowBatch();
- // Since we are not performing the inserts using relation.insert()
- // we need to explicitly run the beforeInsert hooks here.
- return this._beforeInsertBatch(batch, 'executeJoinTableBeforeInsert').then(() => {
- // Insert the batch one table at a time.
- return Promise.map(Object.keys(batch), tableName => inserter(batch[tableName]), {
- concurrency: this.modelClass.concurrency
- });
- });
- }
- _createJoinRowBatch() {
- const batch = Object.create(null);
- for (let n = 0, ln = this.graph.nodes.length; n < ln; ++n) {
- const node = this.graph.nodes[n];
- for (let m = 0, lm = node.manyToManyConnections.length; m < lm; ++m) {
- const conn = node.manyToManyConnections[m];
- let tableInsertion = batch[conn.relation.joinTable];
- const ownerProp = conn.relation.ownerProp.getProps(node.model);
- const modelClass = conn.relation.getJoinModelClass(this.knex);
- let joinModel = conn.relation.createJoinModels(ownerProp, [conn.node.model])[0];
- if (conn.refNode) {
- // Also take extra properties from the referring model, it there was one.
- for (let k = 0, lk = conn.relation.joinTableExtras.length; k < lk; ++k) {
- const extra = conn.relation.joinTableExtras[k];
- if (conn.refNode.model[extra.aliasProp] !== undefined) {
- joinModel[extra.joinTableProp] = conn.refNode.model[extra.aliasProp];
- }
- }
- }
- joinModel = modelClass.fromJson(joinModel);
- if (!tableInsertion) {
- tableInsertion = new TableInsertion(modelClass, true);
- batch[modelClass.getTableName()] = tableInsertion;
- }
- tableInsertion.items.push({
- model: joinModel,
- relation: conn.relation,
- node: conn.node
- });
- }
- }
- return this._removeJoinRowDuplicatesFromBatch(batch);
- }
- _removeJoinRowDuplicatesFromBatch(batch) {
- const tableNames = Object.keys(batch);
- for (let t = 0, lt = tableNames.length; t < lt; ++t) {
- const tableName = tableNames[t];
- const tableInsertion = batch[tableName];
- if (tableInsertion.items.length) {
- const items = tableInsertion.items;
- const keySet = new Set();
- const keys = [];
- for (let i = 0, li = items.length; i < li; ++i) {
- const item = items[i];
- const model = item.model;
- const modelKeys = Object.keys(model);
- for (let k = 0, lk = modelKeys.length; k < lk; ++k) {
- const key = modelKeys[k];
- if (!keySet.has(key)) {
- keySet.add(modelKeys[k]);
- keys.push(key);
- }
- }
- }
- tableInsertion.items = uniqBy(items, item => item.model.$propKey(keys));
- }
- }
- return batch;
- }
- _omitUids(tableInsertion) {
- const modelClass = tableInsertion.modelClass;
- const uidProp = modelClass.uidProp;
- for (let i = 0, li = tableInsertion.items.length; i < li; ++i) {
- const item = tableInsertion.items[i];
- const model = item.model;
- modelClass.omitImpl(model, uidProp);
- }
- }
- _resolveDepsForInsertion(tableInsertion) {
- for (let i = 0, li = tableInsertion.items.length; i < li; ++i) {
- const node = tableInsertion.items[i].node;
- const model = tableInsertion.items[i].model;
- for (let d = 0, ld = node.isNeededBy.length; d < ld; ++d) {
- node.isNeededBy[d].resolve(model);
- }
- }
- }
- _finalize() {
- for (let n = 0, ln = this.graph.nodes.length; n < ln; ++n) {
- const refNode = this.graph.nodes[n];
- const modelClass = refNode.modelClass;
- const ref = refNode.model[modelClass.uidRefProp];
- if (ref) {
- // Copy all the properties to the reference nodes.
- const actualNode = this.graph.nodesById.get(ref);
- const relations = actualNode.modelClass.getRelations();
- const keys = Object.keys(actualNode.model);
- for (let i = 0, l = keys.length; i < l; ++i) {
- const key = keys[i];
- const value = actualNode.model[key];
- if (!relations[key] && typeof value !== 'function') {
- refNode.model[key] = value;
- }
- }
- modelClass.omitImpl(refNode.model, modelClass.uidProp);
- modelClass.omitImpl(refNode.model, modelClass.uidRefProp);
- modelClass.omitImpl(refNode.model, modelClass.dbRefProp);
- } else if (refNode.model[modelClass.uidProp]) {
- // Make sure the model no longer has an uid.
- modelClass.omitImpl(refNode.model, modelClass.uidProp);
- modelClass.omitImpl(refNode.model, modelClass.dbRefProp);
- }
- }
- return Promise.resolve(this.models);
- }
- }
- module.exports = GraphInserter;
|