@feathersjs/knex
Version:
Feathers SQL service adapter using KnexJS
280 lines • 11.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.KnexAdapter = void 0;
const commons_1 = require("@feathersjs/commons");
const adapter_commons_1 = require("@feathersjs/adapter-commons");
const errors_1 = require("@feathersjs/errors");
const error_handler_1 = require("./error-handler");
const METHODS = {
$ne: 'whereNot',
$in: 'whereIn',
$nin: 'whereNotIn',
$or: 'orWhere',
$and: 'andWhere'
};
const OPERATORS = {
$lt: '<',
$lte: '<=',
$gt: '>',
$gte: '>=',
$like: 'like',
$notlike: 'not like',
$ilike: 'ilike'
};
const RETURNING_CLIENTS = ['postgresql', 'pg', 'oracledb', 'mssql', 'sqlite3'];
class KnexAdapter extends adapter_commons_1.AdapterBase {
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.');
}
super({
id: 'id',
...options,
filters: {
...options.filters,
$and: (value) => value
},
operators: [
...(options.operators || []),
...Object.keys(options.extendedOperators || {}),
'$like',
'$notlike',
'$ilike'
]
});
}
get fullName() {
const { name, schema } = this.getOptions({});
return schema ? `${schema}.${name}` : name;
}
get Model() {
return this.getModel();
}
getModel(params = {}) {
const { Model } = this.getOptions(params);
return Model;
}
db(params) {
const { Model, name, schema, tableOptions } = this.getOptions(params);
if (params && params.transaction && params.transaction.trx) {
const { trx } = params.transaction;
// debug('ran %s with transaction %s', fullName, id)
return schema ? trx.withSchema(schema).table(name) : trx(name);
}
return schema ? Model.withSchema(schema).table(name) : Model(name, tableOptions);
}
knexify(knexQuery, query = {}, parentKey) {
const knexify = this.knexify.bind(this);
const { extendedOperators = {} } = this.getOptions({});
const operatorsMap = {
...OPERATORS,
...extendedOperators
};
return Object.keys(query || {}).reduce((currentQuery, key) => {
const value = query[key];
if (commons_1._.isObject(value) && !(value instanceof Date)) {
return knexify(currentQuery, value, key);
}
const column = parentKey || key;
const method = METHODS[key];
if (method) {
if (key === '$or' || key === '$and') {
// This will create a nested query
currentQuery.where(function () {
for (const condition of value) {
this[method](function () {
knexify(this, condition);
});
}
});
return currentQuery;
}
return currentQuery[method](column, value);
}
const operator = operatorsMap[key] || '=';
return operator === '='
? currentQuery.where(column, value)
: currentQuery.where(column, operator, value);
}, knexQuery);
}
createQuery(params = {}) {
const { name, id } = this.getOptions(params);
const { filters, query } = this.filterQuery(params);
const builder = this.db(params);
// $select uses a specific find syntax, so it has to come first.
if (filters.$select) {
const select = filters.$select.map((column) => (column.includes('.') ? column : `${name}.${column}`));
// always select the id field, but make sure we only select it once
builder.select(...new Set([...select, `${name}.${id}`]));
}
else {
builder.select(`${name}.*`);
}
// build up the knex query out of the query params, include $and and $or filters
this.knexify(builder, {
...query,
...commons_1._.pick(filters, '$and', '$or')
});
// Handle $sort
if (filters.$sort) {
return Object.keys(filters.$sort).reduce((currentQuery, key) => currentQuery.orderBy(key, filters.$sort[key] === 1 ? 'asc' : 'desc'), builder);
}
return builder;
}
filterQuery(params) {
const options = this.getOptions(params);
const { $select, $sort, $limit: _limit, $skip = 0, ...query } = (params.query || {});
const $limit = (0, adapter_commons_1.getLimit)(_limit, options.paginate);
return {
paginate: options.paginate,
filters: { $select, $sort, $limit, $skip },
query
};
}
async _find(params = {}) {
const { filters, paginate } = this.filterQuery(params);
const { name, id } = this.getOptions(params);
const builder = params.knex ? params.knex.clone() : this.createQuery(params);
const countBuilder = builder.clone().clearSelect().clearOrder().count(`${name}.${id} as total`);
// Handle $limit
if (filters.$limit) {
builder.limit(filters.$limit);
}
// Handle $skip
if (filters.$skip) {
builder.offset(filters.$skip);
}
// provide default sorting if its not set
if (!filters.$sort && builder.client.driverName === 'mssql') {
builder.orderBy(`${name}.${id}`, 'asc');
}
const data = filters.$limit === 0 ? [] : await builder.catch(error_handler_1.errorHandler);
if (paginate && paginate.default) {
const total = await countBuilder.then((count) => parseInt(count[0] ? count[0].total : 0));
return {
total,
limit: filters.$limit,
skip: filters.$skip || 0,
data
};
}
return data;
}
async _findOrGet(id, params) {
if (id !== null) {
const { name, id: idField } = this.getOptions(params);
const builder = params.knex ? params.knex.clone() : this.createQuery(params);
const idQuery = builder.andWhere(`${name}.${idField}`, '=', id).catch(error_handler_1.errorHandler);
return idQuery;
}
return this._find({
...params,
paginate: false
});
}
async _get(id, params = {}) {
const data = await this._findOrGet(id, params);
if (data.length !== 1) {
throw new errors_1.NotFound(`No record found for id '${id}'`);
}
return data[0];
}
async _create(_data, params = {}) {
const data = _data;
if (Array.isArray(data)) {
return Promise.all(data.map((current) => this._create(current, params)));
}
const { client } = this.db(params);
const returning = RETURNING_CLIENTS.includes(client.driverName) ? [this.id] : [];
const rows = await this.db(params)
.insert(data, returning, { includeTriggerModifications: true })
.catch(error_handler_1.errorHandler);
const id = data[this.id] || rows[0][this.id] || rows[0];
if (!id) {
return rows;
}
return this._get(id, {
...params,
query: commons_1._.pick((params === null || params === void 0 ? void 0 : params.query) || {}, '$select')
});
}
async _patch(id, raw, params = {}) {
var _a, _b;
if (id === null && !this.allowsMulti('patch', params)) {
throw new errors_1.MethodNotAllowed('Can not patch multiple entries');
}
const { name, id: idField } = this.getOptions(params);
const data = commons_1._.omit(raw, this.id);
const results = await this._findOrGet(id, {
...params,
query: {
...params === null || params === void 0 ? void 0 : params.query,
$select: [`${name}.${idField}`]
}
});
const idList = results.map((current) => current[idField]);
const updateParams = {
...params,
query: {
[`${name}.${idField}`]: { $in: idList },
...(((_a = params === null || params === void 0 ? void 0 : params.query) === null || _a === void 0 ? void 0 : _a.$select) ? { $select: (_b = params === null || params === void 0 ? void 0 : params.query) === null || _b === void 0 ? void 0 : _b.$select } : {})
}
};
const builder = this.createQuery(updateParams);
await builder.update(data, [], { includeTriggerModifications: true });
const items = await this._findOrGet(null, updateParams);
if (id !== null) {
if (items.length === 1) {
return items[0];
}
else {
throw new errors_1.NotFound(`No record found for id '${id}'`);
}
}
return items;
}
async _update(id, _data, params = {}) {
if (id === null || Array.isArray(_data)) {
throw new errors_1.BadRequest("You can not replace multiple instances. Did you mean 'patch'?");
}
const data = commons_1._.omit(_data, this.id);
const oldData = await this._get(id, params);
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;
}, {});
await this.db(params)
.update(newObject, '*', { includeTriggerModifications: true })
.where(this.id, id)
.catch(error_handler_1.errorHandler);
return this._get(id, params);
}
async _remove(id, params = {}) {
if (id === null && !this.allowsMulti('remove', params)) {
throw new errors_1.MethodNotAllowed('Can not remove multiple entries');
}
const items = await this._findOrGet(id, params);
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);
await q.delete([], { includeTriggerModifications: true }).catch(error_handler_1.errorHandler);
if (id !== null) {
if (items.length === 1) {
return items[0];
}
throw new errors_1.NotFound(`No record found for id '${id}'`);
}
return items;
}
}
exports.KnexAdapter = KnexAdapter;
//# sourceMappingURL=adapter.js.map