UNPKG

objection

Version:
663 lines (524 loc) 16 kB
import _ from 'lodash'; import Promise from 'bluebird'; import EagerOperation from './EagerOperation'; import ValidationError from '../../model/ValidationError'; const columnInfo = Object.create(null); const idLengthLimit = 63; const relationRecursionLimit = 64; export default class JoinEagerOperation extends EagerOperation { constructor(name, opt) { super(name, opt); this.allRelations = null; this.rootModelClass = null; this.pathInfo = Object.create(null); this.encodings = Object.create(null); this.decodings = Object.create(null); this.encIdx = 0; this.opt = _.defaults(opt, { minimize: false, separator: ':', aliases: {} }); } clone() { const copy = super.clone(); copy.allRelations = this.allRelations; copy.allModelClasses = this.allModelClasses; copy.rootModelClass = this.rootModelClass; copy.pathInfo = this.pathInfo; copy.encodings = this.encodings; copy.decodings = this.decodings; copy.encIdx = this.encIdx; return this; } call(builder, args) { const ret = super.call(builder, args); const ModelClass = builder.modelClass(); if (ret) { this.rootModelClass = ModelClass; this.allModelClasses = findAllModels(this.expression, ModelClass); this.allRelations = findAllRelations(this.expression, ModelClass); } return ret; } onBeforeInternal(builder) { return fetchColumnInfo(builder, this.allModelClasses); } onBeforeBuild(builder) { const builderClone = builder.clone(); builder.table(`${this.rootModelClass.tableName} as ${this.rootModelClass.tableName}`); builder.findOptions({callAfterGetDeeply: true}); this.build({ expr: this.expression, builder: builder, modelClass: builder.modelClass(), parentInfo: null, relation: null, path: '', selectFilter: (col) => { return builderClone.hasSelection(col); } }); } onRawResult(builder, rows) { if (_.isEmpty(rows)) { return rows; } const keyInfoByPath = this.createKeyInfo(rows); const pathInfo = _.values(this.pathInfo); const tree = Object.create(null); const stack = Object.create(null); for (let i = 0, lr = rows.length; i < lr; ++i) { const row = rows[i]; let curBranch = tree; for (let j = 0, lp = pathInfo.length; j < lp; ++j) { const pInfo = pathInfo[j]; const id = pInfo.idGetter(row); if (!id) { continue; } if (pInfo.relation) { const parentModel = stack[pInfo.encParentPath]; curBranch = pInfo.getBranch(parentModel); if (!curBranch) { curBranch = pInfo.createBranch(parentModel); } } let model = pInfo.getModelFromBranch(curBranch, id); if (!model) { model = createModel(row, pInfo, keyInfoByPath); pInfo.setModelToBranch(curBranch, id, model); } stack[pInfo.encPath] = model; } } return this.finalize(pathInfo[0], _.values(tree)); } createKeyInfo(rows) { const keys = Object.keys(rows[0]); const keyInfo = []; for (let i = 0, l = keys.length; i < l; ++i) { const key = keys[i]; const sepIdx = key.lastIndexOf(this.sep); if (sepIdx === -1) { const pInfo = this.pathInfo['']; const col = key; if (!pInfo.omitProps[col]) { keyInfo.push({ pInfo: pInfo, key: key, col: col }); } } else { const encPath = key.substr(0, sepIdx); const path = this.decode(encPath); const col = key.substr(sepIdx + 1); const pInfo = this.pathInfo[path]; if (!pInfo.omitProps[col]) { keyInfo.push({ pInfo: pInfo, key: key, col: col }); } } } return _.groupBy(keyInfo, kInfo => kInfo.pInfo.encPath); } finalize(pInfo, models) { const relNames = Object.keys(pInfo.children); if (Array.isArray(models)) { for (let m = 0, lm = models.length; m < lm; ++m) { this.finalizeOne(pInfo, relNames, models[m]); } } else if (models) { this.finalizeOne(pInfo, relNames, models); } return models; } finalizeOne(pInfo, relNames, model) { for (let r = 0, lr = relNames.length; r < lr; ++r) { const relName = relNames[r]; const branch = model[relName]; const childPathInfo = pInfo.children[relName]; const finalized = childPathInfo.finalizeBranch(branch, model); this.finalize(childPathInfo, finalized); } } build({expr, builder, selectFilter, modelClass, relation, path, parentInfo}) { const info = this.createPathInfo({ modelClass, path, relation, parentInfo }); this.pathInfo[path] = info; this.buildSelects({ builder, selectFilter, modelClass, relation, info }); forEachExpr(expr, modelClass, (childExpr, relation) => { const nextPath = this.joinPath(path, relation.name); const encNextPath = this.encode(nextPath); const encJoinTablePath = relation.joinTable ? this.encode(joinTableForPath(nextPath)) : null; const filterQuery = createFilterQuery({ builder, relation, expr: childExpr }); const relatedJoinSelectQuery = createRelatedJoinFromQuery({ filterQuery, relation, allRelations: this.allRelations }); relation.join(builder, { joinOperation: 'leftJoin', ownerTable: info.encPath, relatedTableAlias: encNextPath, joinTableAlias: encJoinTablePath, relatedJoinSelectQuery: relatedJoinSelectQuery }); // Apply relation.modify since it may also contains selections. Don't move this // to the createFilterQuery function because relatedJoinSelectQuery is cloned // From the return value of that function and we don't want relation.modify // to be called twice for it. filterQuery.modify(relation.modify); this.build({ expr: childExpr, builder: builder, modelClass: relation.relatedModelClass, relation: relation, parentInfo: info, path: nextPath, selectFilter: (col) => { return filterQuery.hasSelection(col); } }); }); } createPathInfo({modelClass, path, relation, parentInfo}) { const encPath = this.encode(path); let info; if (relation && relation.isOneToOne()) { info = new OneToOnePathInfo(); } else { info = new PathInfo(); } info.path = path; info.encPath = encPath; info.parentPath = parentInfo && parentInfo.path; info.encParentPath = parentInfo && parentInfo.encPath; info.modelClass = modelClass; info.relation = relation; info.idGetter = this.createIdGetter(modelClass, encPath); if (parentInfo) { parentInfo.children[relation.name] = info; } return info; } buildSelects({builder, selectFilter, modelClass, relation, info}) { const selects = []; const idCols = modelClass.getIdColumnArray(); const rootTable = this.rootModelClass.tableName; columnInfo[modelClass.tableName].columns.forEach(col => { const filterPassed = selectFilter(col); const isIdColumn = idCols.indexOf(col) !== -1; if (filterPassed || isIdColumn) { selects.push({ col: `${info.encPath || rootTable}.${col}`, alias: this.joinPath(info.encPath, col) }); if (!filterPassed) { info.omitProps[col] = true; } } }); if (relation && relation.joinTableExtras) { const joinTable = this.encode(joinTableForPath(info.path)); relation.joinTableExtras.forEach(extra => { if (selectFilter(extra.joinTableCol)) { selects.push({ col: `${joinTable}.${extra.joinTableCol}`, alias: this.joinPath(info.encPath, extra.aliasCol) }); } }); } const tooLongAliases = selects.filter(select => select.alias.length > idLengthLimit); if (tooLongAliases.length) { throw new ValidationError({ eager: `identifier ${tooLongAliases[0].alias} is over ${idLengthLimit} characters long ` + `and would be truncated by the database engine.` }); } builder.select(selects.map(select => `${select.col} as ${select.alias}`)); } encode(path) { if (!this.opt.minimize) { let encPath = this.encodings[path]; if (!encPath) { const parts = path.split(this.sep); // Don't encode the root. if (!path) { encPath = path; } else { encPath = parts.map(part => this.opt.aliases[part] || part).join(this.sep); } this.encodings[path] = encPath; this.decodings[encPath] = path; } return encPath; } else { let encPath = this.encodings[path]; if (!encPath) { // Don't encode the root. if (!path) { encPath = path; } else { encPath = this.nextEncodedPath(); } this.encodings[path] = encPath; this.decodings[encPath] = path; } return encPath; } } decode(path) { return this.decodings[path]; } nextEncodedPath() { return `_t${++this.encIdx}`; } createIdGetter(modelClass, path) { const idCols = modelClass.getIdColumnArray().map(col => this.joinPath(path, col)); if (idCols.length === 1) { return createSingleIdGetter(idCols); } else if (idCols.length === 2) { return createTwoIdGetter(idCols); } else if (idCols.length === 3) { return createThreeIdGetter(idCols); } else { return createNIdGetter(idCols); } } get sep() { return this.opt.separator; } joinPath(path, nextPart) { if (path) { return `${path}${this.sep}${nextPart}`; } else { return nextPart; } } } function findAllModels(expr, modelClass) { const models = []; findAllModelsImpl(expr, modelClass, models); return _.uniqBy(models, 'tableName'); } function findAllModelsImpl(expr, modelClass, models) { models.push(modelClass); forEachExpr(expr, modelClass, (childExpr, relation) => { findAllModelsImpl(childExpr, relation.relatedModelClass, models); }); } function findAllRelations(expr, modelClass) { const relations = []; findAllRelationsImpl(expr, modelClass, relations); return _.uniqWith(relations, (lhs, rhs) => lhs === rhs); } function findAllRelationsImpl(expr, modelClass, relations) { forEachExpr(expr, modelClass, (childExpr, relation) => { relations.push(relation); findAllRelationsImpl(childExpr, relation.relatedModelClass, relations); }); } function fetchColumnInfo(builder, models) { const knex = builder.knex(); return Promise.all(models.map(ModelClass => { const table = ModelClass.tableName; if (columnInfo[table]) { return columnInfo[table]; } else { columnInfo[table] = knex(table).columnInfo().then(info => { const result = { columns: Object.keys(info) }; columnInfo[table] = result; return result; }); return columnInfo[table]; } })); } function forEachExpr(expr, modelClass, callback) { const relations = modelClass.getRelationArray(); if (expr.isAllRecursive() || expr.maxRecursionDepth() > relationRecursionLimit) { throw new ValidationError({ eager: `recursion depth of eager expression ${expr.toString()} too big for JoinEagerAlgorithm` }); } for (let i = 0, l = relations.length; i < l; ++i) { const relation = relations[i]; const childExpr = expr.childExpression(relation.name); if (childExpr) { callback(childExpr, relation, relation.name); } } } function createSingleIdGetter(idCols) { const idCol = idCols[0]; return (row) => { const val = row[idCol]; if (!val) { return null; } else { return val; } }; } function createTwoIdGetter(idCols) { const idCol1 = idCols[0]; const idCol2 = idCols[1]; return (row) => { const val1 = row[idCol1]; const val2 = row[idCol2]; if (!val1 || !val2) { return null; } else { return `${val1},${val2}`; } }; } function createThreeIdGetter(idCols) { const idCol1 = idCols[0]; const idCol2 = idCols[1]; const idCol3 = idCols[2]; return (row) => { const val1 = row[idCol1]; const val2 = row[idCol2]; const val3 = row[idCol3]; if (!val1 || !val2 || !val3) { return null; } else { return `${val1},${val2},${val3}`; } }; } function createNIdGetter(idCols) { return (row) => { let id = ''; for (let i = 0, l = idCols.length; i < l; ++i) { const val = row[idCols[i]]; if (!val) { return null; } id += (i > 0 ? ',' : '') + val; } return id; }; } function createFilterQuery({builder, expr, relation}) { const filterQuery = relation.relatedModelClass .query() .childQueryOf(builder); for (let i = 0, l = expr.args.length; i < l; ++i) { const filterName = expr.args[i]; const filter = expr.filters[filterName]; if (typeof filter !== 'function') { throw new ValidationError({eager: `could not find filter "${filterName}" for relation "${relation.name}"`}); } filter(filterQuery); } return filterQuery; } function createRelatedJoinFromQuery({filterQuery, relation, allRelations}) { const relatedJoinFromQuery = filterQuery.clone(); const allForeignKeys = findAllForeignKeysForModel({ modelClass: relation.relatedModelClass, allRelations }); return relatedJoinFromQuery.select(allForeignKeys.filter(col => { return !relatedJoinFromQuery.hasSelection(col); })); } function findAllForeignKeysForModel({modelClass, allRelations}) { let foreignKeys = modelClass.getIdColumnArray().slice(); allRelations.forEach(rel => { if (rel.relatedModelClass.tableName === modelClass.tableName) { rel.relatedCol.forEach(col => foreignKeys.push(col)); } if (rel.ownerModelClass.tableName === modelClass.tableName) { rel.ownerCol.forEach(col => foreignKeys.push(col)); } }); return _.uniq(foreignKeys); } function createModel(row, pInfo, keyInfoByPath) { const keyInfo = keyInfoByPath[pInfo.encPath]; const json = {}; for (let k = 0, lk = keyInfo.length; k < lk; ++k) { const kInfo = keyInfo[k]; json[kInfo.col] = row[kInfo.key]; } return pInfo.modelClass.fromDatabaseJson(json); } function joinTableForPath(path) { return path + '_join'; } class PathInfo { constructor() { this.path = null; this.encPath = null; this.encParentPath = null; this.modelClass = null; this.relation = null; this.omitProps = Object.create(null); this.children = Object.create(null); this.idGetter = null; } createBranch(parentModel) { const branch = Object.create(null); parentModel[this.relation.name] = branch; return branch; } getBranch(parentModel) { return parentModel[this.relation.name]; } getModelFromBranch(branch, id) { return branch[id]; } setModelToBranch(branch, id, model) { branch[id] = model; } finalizeBranch(branch, parentModel) { const relModels = _.values(branch); parentModel[this.relation.name] = relModels; return relModels; } } class OneToOnePathInfo extends PathInfo { createBranch(parentModel) { return parentModel; } getBranch(parentModel) { return parentModel; } getModelFromBranch(branch, id) { return branch[this.relation.name]; } setModelToBranch(branch, id, model) { branch[this.relation.name] = model; } finalizeBranch(branch, parentModel) { parentModel[this.relation.name] = branch || null; return branch || null; } }