'use strict'; const _ = require('lodash/fp'); const helpers = require('./helpers'); const transactionCtx = require('../transaction-context'); const createQueryBuilder = (uid, db, initialState = {}) => { const meta = db.metadata.get(uid); const { tableName } = meta; const state = _.defaults( { type: 'select', select: [], count: null, max: null, first: false, data: null, where: [], joins: [], populate: null, limit: null, offset: null, transaction: null, forUpdate: false, onConflict: null, merge: null, ignore: false, orderBy: [], groupBy: [], increments: [], decrements: [], aliasCounter: 0, }, initialState ); const getAlias = () => { const alias = `t${state.aliasCounter}`; state.aliasCounter += 1; return alias; }; return { alias: getAlias(), getAlias, state, clone() { return createQueryBuilder(uid, db, state); }, select(args) { state.type = 'select'; state.select = _.uniq(_.castArray(args)); return this; }, addSelect(args) { state.select = _.uniq([...state.select, ..._.castArray(args)]); return this; }, insert(data) { state.type = 'insert'; state.data = data; return this; }, onConflict(args) { state.onConflict = args; return this; }, merge(args) { state.merge = args; return this; }, ignore() { state.ignore = true; return this; }, delete() { state.type = 'delete'; return this; }, ref(name) { return db.connection.ref(helpers.toColumnName(meta, name)); }, update(data) { state.type = 'update'; state.data = data; return this; }, increment(column, amount = 1) { state.type = 'update'; state.increments.push({ column, amount }); return this; }, decrement(column, amount = 1) { state.type = 'update'; state.decrements.push({ column, amount }); return this; }, count(count = 'id') { state.type = 'count'; state.count = count; return this; }, max(column) { state.type = 'max'; state.max = column; return this; }, where(where = {}) { if (!_.isPlainObject(where)) { throw new Error('Where must be an object'); } state.where.push(where); return this; }, limit(limit) { state.limit = limit; return this; }, offset(offset) { state.offset = offset; return this; }, orderBy(orderBy) { state.orderBy = orderBy; return this; }, groupBy(groupBy) { state.groupBy = groupBy; return this; }, populate(populate) { state.populate = populate; return this; }, search(query) { state.search = query; return this; }, transacting(transaction) { state.transaction = transaction; return this; }, forUpdate() { state.forUpdate = true; return this; }, init(params = {}) { const { _q, filters, where, select, limit, offset, orderBy, groupBy, populate } = params; if (!_.isNil(where)) { this.where(where); } if (!_.isNil(_q)) { this.search(_q); } if (!_.isNil(select)) { this.select(select); } else { this.select('*'); } if (!_.isNil(limit)) { this.limit(limit); } if (!_.isNil(offset)) { this.offset(offset); } if (!_.isNil(orderBy)) { this.orderBy(orderBy); } if (!_.isNil(groupBy)) { this.groupBy(groupBy); } if (!_.isNil(populate)) { this.populate(populate); } if (!_.isNil(filters)) { this.filters(filters); } return this; }, filters(filters) { state.filters = filters; }, first() { state.first = true; return this; }, join(join) { if (!join.targetField) { state.joins.push(join); return this; } const model = db.metadata.get(uid); const attribute = model.attributes[join.targetField]; helpers.createJoin( { db, qb: this }, { alias: this.alias, refAlias: join.alias, attributeName: join.targetField, attribute, } ); return this; }, mustUseAlias() { return ['select', 'count'].includes(state.type); }, aliasColumn(key, alias) { if (typeof key !== 'string') { return key; } if (key.indexOf('.') >= 0) { return key; } if (!_.isNil(alias)) { return `${alias}.${key}`; } return this.mustUseAlias() ? `${this.alias}.${key}` : key; }, raw(...args) { return db.connection.raw(...args); }, shouldUseSubQuery() { return ['delete', 'update'].includes(state.type) && state.joins.length > 0; }, runSubQuery() { this.select('id'); const subQB = this.getKnexQuery(); const nestedSubQuery = db.getConnection().select('id').from(subQB.as('subQuery')); return db.getConnection(tableName)[state.type]().whereIn('id', nestedSubQuery); }, processState() { state.orderBy = helpers.processOrderBy(state.orderBy, { qb: this, uid, db }); if (!_.isNil(state.filters)) { if (_.isFunction(state.filters)) { const filters = state.filters({ qb: this, uid, meta, db }); if (!_.isNil(filters)) { state.where.push(filters); } } else { state.where.push(state.filters); } } state.where = helpers.processWhere(state.where, { qb: this, uid, db }); state.populate = helpers.processPopulate(state.populate, { qb: this, uid, db }); state.data = helpers.toRow(meta, state.data); this.processSelect(); }, shouldUseDistinct() { return state.joins.length > 0 && _.isEmpty(state.groupBy); }, processSelect() { state.select = state.select.map((field) => helpers.toColumnName(meta, field)); if (this.shouldUseDistinct()) { const joinsOrderByColumns = state.joins.flatMap((join) => { return _.keys(join.orderBy).map((key) => this.aliasColumn(key, join.alias)); }); const orderByColumns = state.orderBy.map(({ column }) => column); state.select = _.uniq([...joinsOrderByColumns, ...orderByColumns, ...state.select]); } }, getKnexQuery() { if (!state.type) { this.select('*'); } const aliasedTableName = this.mustUseAlias() ? `${tableName} as ${this.alias}` : tableName; const qb = db.getConnection(aliasedTableName); if (this.shouldUseSubQuery()) { return this.runSubQuery(); } this.processState(); switch (state.type) { case 'select': { qb.select(state.select.map((column) => this.aliasColumn(column))); if (this.shouldUseDistinct()) { qb.distinct(); } break; } case 'count': { const dbColumnName = this.aliasColumn(helpers.toColumnName(meta, state.count)); if (this.shouldUseDistinct()) { qb.countDistinct({ count: dbColumnName }); } else { qb.count({ count: dbColumnName }); } break; } case 'max': { const dbColumnName = this.aliasColumn(helpers.toColumnName(meta, state.max)); qb.max({ max: dbColumnName }); break; } case 'insert': { qb.insert(state.data); if (db.dialect.useReturning() && _.has('id', meta.attributes)) { qb.returning('id'); } break; } case 'update': { if (state.data) { qb.update(state.data); } break; } case 'delete': { qb.delete(); break; } case 'truncate': { db.truncate(); break; } default: { throw new Error('Unknown query type'); } } if (state.transaction) { qb.transacting(state.transaction); } if (state.forUpdate) { qb.forUpdate(); } if (!_.isEmpty(state.increments)) { state.increments.forEach((incr) => qb.increment(incr.column, incr.amount)); } if (!_.isEmpty(state.decrements)) { state.decrements.forEach((decr) => qb.decrement(decr.column, decr.amount)); } if (state.onConflict) { if (state.merge) { qb.onConflict(state.onConflict).merge(state.merge); } else if (state.ignore) { qb.onConflict(state.onConflict).ignore(); } } if (state.limit) { qb.limit(state.limit); } if (state.offset) { qb.offset(state.offset); } if (state.orderBy.length > 0) { qb.orderBy(state.orderBy); } if (state.first) { qb.first(); } if (state.groupBy.length > 0) { qb.groupBy(state.groupBy); } // if there are joins and it is a delete or update use a sub query if (state.where) { helpers.applyWhere(qb, state.where); } // if there are joins and it is a delete or update use a sub query if (state.search) { qb.where((subQb) => { helpers.applySearch(subQb, state.search, { qb: this, db, uid }); }); } if (state.joins.length > 0) { helpers.applyJoins(qb, state.joins); } return qb; }, async execute({ mapResults = true } = {}) { try { const qb = this.getKnexQuery(); if (transactionCtx.get()) { qb.transacting(transactionCtx.get()); } const rows = await qb; if (state.populate && !_.isNil(rows)) { await helpers.applyPopulate(_.castArray(rows), state.populate, { qb: this, uid, db, }); } let results = rows; if (mapResults && state.type === 'select') { results = helpers.fromRow(meta, rows); } return results; } catch (error) { db.dialect.transformErrors(error); } }, }; }; module.exports = createQueryBuilder;