UNPKG

feathers-knex

Version:

A service plugin for KnexJS a query builder for PostgreSQL, MySQL and SQLite3

335 lines (272 loc) 8.93 kB
const { _ } = require('@feathersjs/commons'); const { AdapterService } = require('@feathersjs/adapter-commons'); const { isPlainObject } = require('is-plain-object'); const errors = require('@feathersjs/errors'); const errorHandler = require('./error-handler'); const hooks = require('./hooks'); const debug = require('debug')('feathers-knex'); const METHODS = { $or: 'orWhere', $and: 'andWhere', $ne: 'whereNot', $in: 'whereIn', $nin: 'whereNotIn' }; const OPERATORS = { $lt: '<', $lte: '<=', $gt: '>', $gte: '>=', $like: 'like', $notlike: 'not like', $ilike: 'ilike' }; // Create the service. class Service extends AdapterService { constructor (options) { if (!options || !options.Model) { throw new Error('You must provide a Model (the initialized knex object)'); } if (typeof options.name !== 'string') { throw new Error('No table name specified.'); } const { whitelist = [] } = options; super(Object.assign({ id: 'id' }, options, { whitelist: whitelist.concat(['$like', '$notlike', '$ilike', '$and']) })); this.table = options.name; this.schema = options.schema; } get Model () { return this.options.Model; } get knex () { return this.Model; } get fullName () { return this.schema ? `${this.schema}.${this.table}` : this.table; } // NOTE (EK): We need this method so that we return a new query // instance each time, otherwise it will reuse the same query. db (params = {}) { const { knex, table, schema, fullName } = this; if (params.transaction) { const { trx, id } = params.transaction; debug('ran %s with transaction %s', fullName, id); return schema ? trx.withSchema(schema).table(table) : trx(table); } return schema ? knex.withSchema(schema).table(table) : knex(table); } knexify (query, params, parentKey) { Object.keys(params || {}).forEach(key => { const value = params[key]; if (isPlainObject(value)) { return this.knexify(query, value, key); } // const self = this; const column = parentKey || key; const method = METHODS[key]; const operator = OPERATORS[key] || '='; if (method) { if (key === '$or' || key === '$and') { const self = this; return query.where(function () { return value.forEach((condition) => { this[method](function () { self.knexify(this, condition); }); }); }); } // eslint-disable-next-line no-useless-call return query[method].call(query, column, value); } return operator === '=' ? query.where(column, value) : query.where(column, operator, value); }); return query; } createQuery (params = {}) { const { schema, table, id } = this; const { filters, query } = this.filterQuery(params); let q = this.db(params); if (schema) { q = q.withSchema(schema).from(`${table} as ${table}`); } // $select uses a specific find syntax, so it has to come first. q = filters.$select // always select the id field, but make sure we only select it once ? q.select([...new Set([...filters.$select, `${table}.${id}`])]) : q.select([`${table}.*`]); // build up the knex query out of the query params this.knexify(q, query); // Handle $sort if (filters.$sort) { Object.keys(filters.$sort).forEach(key => { q = q.orderBy(key, filters.$sort[key] === 1 ? 'asc' : 'desc'); }); } return q; } _find (params = {}) { const { filters, query, paginate } = this.filterQuery(params); const q = params.knex ? params.knex.clone() : this.createQuery(params); // Handle $limit if (filters.$limit) { q.limit(filters.$limit); } // Handle $skip if (filters.$skip) { q.offset(filters.$skip); // provide default sorting if its not set if (!filters.$sort) { q.orderBy(this.id, 'asc'); } } let executeQuery = total => q.then(data => { return { total: parseInt(total, 10), limit: filters.$limit, skip: filters.$skip || 0, data }; }); if (filters.$limit === 0) { executeQuery = total => Promise.resolve({ total: parseInt(total, 10), limit: filters.$limit, skip: filters.$skip || 0, data: [] }); } if (paginate && paginate.default) { const countQuery = (params.knex || this.db(params)) .clone().clearSelect().clearOrder() .count(`${this.table}.${this.id} as total`); if (!params.knex) { this.knexify(countQuery, query); } return countQuery.then(count => count[0] ? count[0].total : 0) .then(executeQuery) .catch(errorHandler); } return executeQuery().then(page => page.data).catch(errorHandler); } _findOrGet (id, params = {}) { const findParams = Object.assign({}, params, { paginate: false, query: Object.assign({}, params.query) }); if (id === null) { return this._find(findParams); } findParams.query.$and = [ ...(findParams.query.$and || []), { [`${this.table}.${this.id}`]: id } ]; return this._find(findParams); } _get (id, params = {}) { return this._findOrGet(id, params).then(data => { if (data.length !== 1) { throw new errors.NotFound(`No record found for id '${id}'`); } return data[0]; }).catch(errorHandler); } _create (data, params = {}) { if (Array.isArray(data)) { return Promise.all(data.map(current => this._create(current, params))); } const client = this.db(params).client.config.client; const returning = client === 'pg' || client === 'oracledb' || client === 'mssql' ? [this.id] : []; return this.db(params) .insert(data, returning) .then(rows => { let id; if (data[this.id] !== undefined) { id = data[this.id]; } else if (rows[0]) { id = rows[0][this.id] !== undefined ? rows[0][this.id] : rows[0]; } if (!id) return rows; return this._get(id, params); }) .catch(errorHandler); } _patch (id, raw, params = {}) { // Do not allow to patch the id const data = _.omit(raw, this.id); // By default we will just query for the one id. For multi patch // we create a list of the ids of all items that will be changed // to re-query them after the update return this._findOrGet(id, Object.assign({}, params, { query: _.extend({}, params.query, { $select: [`${this.table}.${this.id}`] }) })).then(results => { const idList = results.map(current => current[this.id]); const query = { [`${this.table}.${this.id}`]: { $in: idList } }; const q = this.knexify(this.db(params), query); const originalQuerySubset = params.query && params.query.$select ? { $select: params.query.$select } : {}; const findParams = Object.assign({}, params, { query: Object.assign(originalQuerySubset, query) }); return q.update(data).then((rows) => { return this._findOrGet(null, findParams).then(items => { if (id !== null) { if (items.length === 1) { return items[0]; } else { throw new errors.NotFound(`No record found for id '${id}'`); } } return items; }); }); }).catch(errorHandler); } _update (id, data, params = {}) { return this._get(id, params).then(oldData => { const newObject = Object.keys(oldData).reduce((result, key) => { if (key !== this.id) { // We don't want the id field to be changed result[key] = data[key] === undefined ? null : data[key]; } return result; }, {}); return this.db(params).update(newObject).where(this.id, id).then(() => this._get(id, params) ); }).catch(errorHandler); } _remove (id, params = {}) { return this._findOrGet(id, params).then(items => { const { query } = this.filterQuery(params); const q = this.db(params); const idList = items.map(current => current[this.id]); query[this.id] = { $in: idList }; // build up the knex query out of the query params this.knexify(q, query); return q.del().then((...args) => { if (id !== null) { if (items.length === 1) { return items[0]; } throw new errors.NotFound(`No record found for id '${id}'`); } return items; }); }).catch(errorHandler); } } module.exports = function init (options) { return new Service(options); }; Object.assign(module.exports, { hooks, Service, ERROR: errorHandler.ERROR });