123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- 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;
|