feathers-sequelize
Version:
A service adapter for Sequelize an SQL ORM
347 lines • 14.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SequelizeAdapter = void 0;
const errors_1 = require("@feathersjs/errors");
const commons_1 = require("@feathersjs/commons");
const adapter_commons_1 = require("@feathersjs/adapter-commons");
const utils_1 = require("./utils");
const sequelize_1 = require("sequelize");
const defaultOpMap = () => {
return {
$eq: sequelize_1.Op.eq,
$ne: sequelize_1.Op.ne,
$gte: sequelize_1.Op.gte,
$gt: sequelize_1.Op.gt,
$lte: sequelize_1.Op.lte,
$lt: sequelize_1.Op.lt,
$in: sequelize_1.Op.in,
$nin: sequelize_1.Op.notIn,
$like: sequelize_1.Op.like,
$notLike: sequelize_1.Op.notLike,
$iLike: sequelize_1.Op.iLike,
$notILike: sequelize_1.Op.notILike,
$or: sequelize_1.Op.or,
$and: sequelize_1.Op.and
};
};
const defaultFilters = () => {
return {
$returning: (value) => {
if (value === true || value === false || value === undefined) {
return value;
}
throw new errors_1.BadRequest('Invalid $returning filter value');
},
$and: true
};
};
class SequelizeAdapter extends adapter_commons_1.AdapterBase {
constructor(options) {
if (!options.Model) {
throw new Error('You must provide a Sequelize Model');
}
if (options.operators && !Array.isArray(options.operators)) {
throw new Error('The \'operators\' option must be an array. For migration from feathers.js v4 see: https://github.com/feathersjs-ecosystem/feathers-sequelize/tree/dove#migrate-to-feathers-v5-dove');
}
const operatorMap = Object.assign(defaultOpMap(), options.operatorMap);
const operators = Object.keys(operatorMap);
if (options.operators) {
options.operators.forEach(op => {
if (!operators.includes(op)) {
operators.push(op);
}
});
}
const { primaryKeyAttributes } = options.Model;
const id = typeof primaryKeyAttributes === 'object' && primaryKeyAttributes[0] !== undefined
? primaryKeyAttributes[0]
: 'id';
const filters = Object.assign(defaultFilters(), options.filters);
super(Object.assign({ id }, options, { operatorMap, filters, operators }));
}
get raw() {
return this.options.raw !== false;
}
get Op() {
// @ts-ignore
return this.options.Model.sequelize.Sequelize.Op;
}
get Model() {
if (!this.options.Model) {
throw new Error('The Model getter was called with no Model provided in options!');
}
return this.options.Model;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getModel(_params) {
if (!this.options.Model) {
throw new Error('getModel was called without a Model present in the constructor options and without overriding getModel! Perhaps you intended to override getModel in a child class?');
}
return this.options.Model;
}
/**
* @deprecated use 'service.ModelWithScope' instead. 'applyScope' will be removed in a future release.
*/
applyScope(params) {
var _a;
const Model = this.getModel(params);
if ((_a = params === null || params === void 0 ? void 0 : params.sequelize) === null || _a === void 0 ? void 0 : _a.scope) {
return Model.scope(params.sequelize.scope);
}
return Model;
}
ModelWithScope(params) {
return this.applyScope(params);
}
convertOperators(q) {
if (Array.isArray(q)) {
return q.map(subQuery => this.convertOperators(subQuery));
}
if (!(0, utils_1.isPlainObject)(q)) {
return q;
}
const { operatorMap } = this.options;
const converted = Object.keys(q).reduce((result, prop) => {
const value = q[prop];
const key = (operatorMap[prop] ? operatorMap[prop] : prop);
result[key] = this.convertOperators(value);
return result;
}, {});
Object.getOwnPropertySymbols(q).forEach(symbol => {
converted[symbol] = q[symbol];
});
return converted;
}
filterQuery(params) {
const options = this.getOptions(params);
const { filters, query: _query } = (0, adapter_commons_1.filterQuery)(params.query || {}, options);
const query = this.convertOperators({
..._query,
...commons_1._.omit(filters, '$select', '$skip', '$limit', '$sort')
});
return {
filters,
query,
paginate: options.paginate
};
}
paramsToAdapter(id, _params) {
const params = _params || {};
if (id) {
const { query: where } = this.filterQuery(params);
const { and } = this.Op;
// Attach 'where' constraints, if any were used.
const q = Object.assign({
raw: this.raw,
where: Object.assign(where, {
[and]: where[and] ? [...where[and], { [this.id]: id }] : { [this.id]: id }
})
}, params.sequelize);
return q;
}
else {
const { filters, query: where } = this.filterQuery(params);
const order = (0, utils_1.getOrder)(filters.$sort);
const q = Object.assign({
where,
order,
limit: filters.$limit,
offset: filters.$skip,
raw: this.raw,
distinct: true
}, params.sequelize);
if (filters.$select) {
// Add the id to the select if it is not already there
if (!filters.$select.includes(this.id)) {
filters.$select.push(this.id);
}
q.attributes = filters.$select.map((select) => `${select}`);
}
// Until Sequelize fix all the findAndCount issues, a few 'hacks' are needed to get the total count correct
// Adding an empty include changes the way the count is done
// See: https://github.com/sequelize/sequelize/blob/7e441a6a5ca44749acd3567b59b1d6ceb06ae64b/lib/model.js#L1780-L1782
q.include = q.include || [];
return q;
}
}
async _getOrFind(id, _params) {
const params = _params || {};
if (id === null) {
return await this._find({
...params,
paginate: false
});
}
return await this._get(id, params);
}
async _find(params = {}) {
const { paginate } = this.filterQuery(params);
const Model = this.ModelWithScope(params);
const q = this.paramsToAdapter(null, params);
try {
if (paginate && paginate.default) {
const result = await Model.findAndCountAll(q);
return {
total: result.count,
limit: q.limit,
skip: q.offset || 0,
data: result.rows
};
}
return await Model.findAll(q);
}
catch (err) {
return (0, utils_1.errorHandler)(err);
}
}
async _get(id, params = {}) {
const Model = this.ModelWithScope(params);
const q = this.paramsToAdapter(id, params);
// findById calls findAll under the hood. We use findAll so that
// eager loading can be used without a separate code path.
try {
const result = await Model.findAll(q);
if (result.length === 0) {
throw new errors_1.NotFound(`No record found for id '${id}'`);
}
const item = result[0];
return (0, adapter_commons_1.select)(params, this.id)(item);
}
catch (err) {
return (0, utils_1.errorHandler)(err);
}
}
async _create(data, params = {}) {
if (Array.isArray(data) && !this.allowsMulti('create', params)) {
throw new errors_1.MethodNotAllowed('Can not create multiple entries');
}
const options = Object.assign({ raw: this.raw }, params.sequelize);
// Model.create's `raw` option is different from other methods.
// In order to use `raw` consistently to serialize the result,
// we need to shadow the Model.create use of raw, which we provide
// access to by specifying `ignoreSetters`.
const ignoreSetters = !!options.ignoreSetters;
const createOptions = Object.assign({
returning: true
}, options, { raw: ignoreSetters });
const isArray = Array.isArray(data);
const Model = this.ModelWithScope(params);
try {
const result = isArray
? await Model.bulkCreate(data, createOptions)
: await Model.create(data, createOptions);
const sel = (0, adapter_commons_1.select)(params, this.id);
if (options.raw === false) {
return result;
}
if (isArray) {
return result.map(item => sel(item.toJSON()));
}
return sel(result.toJSON());
}
catch (err) {
return (0, utils_1.errorHandler)(err);
}
}
async _patch(id, data, params = {}) {
var _a, _b;
if (id === null && !this.allowsMulti('patch', params)) {
throw new errors_1.MethodNotAllowed('Can not patch multiple entries');
}
const Model = this.ModelWithScope(params);
// Get a list of ids that match the id/query. Overwrite the
// $select because only the id is needed for this idList
const itemOrItems = await this._getOrFind(id, {
...params,
query: {
...params === null || params === void 0 ? void 0 : params.query,
$select: [this.id]
}
});
const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
const ids = items.map(item => item[this.id]);
try {
const seqOptions = Object.assign({ raw: this.raw }, params.sequelize, { where: { [this.id]: { [this.Op.in]: ids } } });
await Model.update(commons_1._.omit(data, this.id), seqOptions);
if (params.$returning === false) {
return Promise.resolve([]);
}
// Create a new query that re-queries all ids that
// were originally changed
const findParams = {
...params,
query: {
[this.id]: { $in: ids },
...(((_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 result = await this._getOrFind(id, findParams);
return (0, adapter_commons_1.select)(params, this.id)(result);
}
catch (err) {
return (0, utils_1.errorHandler)(err);
}
}
async _update(id, data, params = {}) {
var _a;
const query = Object.assign({}, this.filterQuery(params).query);
// Force the {raw: false} option as the instance is needed to properly update
const seqOptions = Object.assign({}, params.sequelize, { raw: false });
const instance = await this._get(id, { sequelize: seqOptions, query });
const itemToUpdate = Object.keys(instance.toJSON()).reduce((result, key) => {
// @ts-ignore
result[key] = key in data ? data[key] : null;
return result;
}, {});
try {
await instance.update(itemToUpdate, seqOptions);
const item = await this._get(id, {
sequelize: Object.assign({}, seqOptions, {
raw: typeof ((_a = params === null || params === void 0 ? void 0 : params.sequelize) === null || _a === void 0 ? void 0 : _a.raw) === 'boolean'
? params.sequelize.raw
: this.raw
})
});
return (0, adapter_commons_1.select)(params, this.id)(item);
}
catch (err) {
return (0, utils_1.errorHandler)(err);
}
}
async _remove(id, params = {}) {
var _a;
if (id === null && !this.allowsMulti('remove', params)) {
throw new errors_1.MethodNotAllowed('Can not remove multiple entries');
}
const Model = this.ModelWithScope(params);
const findParams = { ...params };
if (params.$returning === false) {
findParams.query = {
...findParams.query,
$select: [this.id]
};
}
else if ((_a = params.query) === null || _a === void 0 ? void 0 : _a.$select) {
findParams.query = {
...findParams.query,
$select: [...params.query.$select, this.id]
};
}
const itemOrItems = await this._getOrFind(id, findParams);
const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
const ids = items.map(item => item[this.id]);
const seqOptions = Object.assign({ raw: this.raw }, params.sequelize, { where: { [this.id]: { [this.Op.in]: ids } } });
try {
await Model.destroy(seqOptions);
if (params.$returning === false) {
return [];
}
return (0, adapter_commons_1.select)(params, this.id)(itemOrItems);
}
catch (err) {
return (0, utils_1.errorHandler)(err);
}
}
}
exports.SequelizeAdapter = SequelizeAdapter;
//# sourceMappingURL=adapter.js.map