const RelationExpression = require('../RelationExpression'); const UpsertNode = require('./UpsertNode'); const { isSqlite } = require('../../utils/knexUtils'); const { asArray } = require('../../utils/objectUtils'); const { keyByProps } = require('../../model/modelUtils'); const { appendDataPath } = require('../../utils/dataPath'); const { Type: ValidationErrorType } = require('../../model/ValidationError'); // Given an upsert model graph, creates a set of nodes that describe what to do // to each individual model in the graph. node.types returns the needed actions // (any of insert, relate, update, delete and unrelate). This class determines // the needed actions by fetching the current state of the graph from the // database. Only ids and foreign keys needed by the relations are fetched. class UpsertGraph { constructor(upsert, isArray, opt) { this.upsert = upsert; this.isArray = isArray; this.rootModelClass = this.upsert[0].constructor; this.relExpr = RelationExpression.fromModelGraph(upsert); this.nodes = []; // Keys are upsert models and values are corresponding nodes. this.nodesByUpsert = new Map(); this.opt = opt || {}; } build(builder) { return this.fetchCurrentState(builder).then(currentState => this.buildGraph(currentState)); } // Fetches the current state of the graph from the database. This method // only fetches ids and all foreign keys needed by the relations. fetchCurrentState(builder) { const rootIds = getRootIds(this.upsert); const rootIdCols = builder.fullIdColumnFor(this.rootModelClass); const allowedExpr = builder.allowedUpsertExpression(); const oldContext = builder.context(); if (allowedExpr && !allowedExpr.isSubExpression(this.relExpr)) { const modelClass = builder.modelClass(); throw modelClass.createValidationError({ type: ValidationErrorType.UnallowedRelation, message: 'trying to upsert an unallowed relation' }); } if (rootIds.length === 0) { return Promise.resolve([]); } return builder .modelClass() .query() .childQueryOf(builder, true) .whereInComposite(rootIdCols, rootIds) .eager(this.relExpr) .internalOptions({ keepImplicitJoinProps: true }) .mergeContext({ onBuild(builder) { // There may be an onBuild hook in the old context. if (oldContext.onBuild) { oldContext.onBuild(builder); } const modelClass = builder.modelClass(); const idColumn = builder.fullIdColumnFor(modelClass); builder.select(idColumn); } }); } buildGraph(current) { this.doBuildGraph({ modelClass: this.rootModelClass, upsert: this.upsert, current: current, isArray: this.isArray, parentNode: null, relExpr: this.relExpr, dataPath: null }); } doBuildGraph({ modelClass, upsert, current, isArray, parentNode, relExpr, dataPath }) { this.buildGraphArray({ modelClass, upsert: ensureArray(upsert), current: ensureArray(current), isArray, parentNode, relExpr, dataPath }); } buildGraphArray({ modelClass, upsert, current, isArray, parentNode, relExpr, dataPath }) { const idProp = modelClass.getIdPropertyArray(); const currentById = keyByProps(current, idProp); const upsertById = keyByProps(upsert, idProp); upsert.forEach((upsert, index) => { const key = upsert.$propKey(idProp); const current = currentById.get(key); const nextDataPath = isArray ? appendDataPath(dataPath, index) : dataPath; this.buildGraphSingle({ modelClass, upsert, current, parentNode, relExpr, dataPath: nextDataPath }); }); current.forEach(current => { const key = current.$propKey(idProp); const upsert = upsertById.get(key); if (!upsert) { // These nodes result in delete and unrelate operations and nothing gets validated. // Use an index of -1 for dataPath here, as it should never actually get used. const nextDataPath = isArray ? appendDataPath(dataPath, -1) : dataPath; this.buildGraphSingle({ modelClass, upsert, current, parentNode, relExpr, dataPath: nextDataPath }); } }); } buildGraphSingle({ modelClass, upsert, current, parentNode, relExpr, dataPath }) { if (!upsert && !current) { return; } const node = new UpsertNode({ parentNode, relExpr, upsertModel: upsert, currentModel: current, dataPath, opt: this.opt }); this.nodes.push(node); if (upsert) { this.nodesByUpsert.set(upsert, node); } if (parentNode) { const relations = parentNode.relations; const relation = (relations[relExpr.$relation] = relations[relExpr.$relation] || []); relation.push(node); } // No need to build the graph down from a deleted node. if (node.upsertModel === null) { return; } // No need to build the graph down from a node which will be recursively upserted if (node.hasType(UpsertNode.Type.UpsertRecursively)) { return; } relExpr.forEachChildExpression(modelClass.getRelations(), (expr, relation) => { const relUpsert = upsert && upsert[relation.name]; const relCurrent = current && current[relation.name]; const nextDataPath = appendDataPath(dataPath, relation); this.doBuildGraph({ modelClass: relation.relatedModelClass, upsert: relUpsert, current: relCurrent, isArray: Array.isArray(relUpsert), parentNode: node, relExpr: expr, dataPath: nextDataPath }); }); } } function getRootIds(graph) { return asArray(graph) .filter(it => it.$hasId()) .map(root => root.$id()); } function ensureArray(item) { if (item && !Array.isArray(item)) { return [item]; } else if (!item) { return []; } else { return item; } } module.exports = UpsertGraph;