feathers-sequelize
Version:
A service adapter for Sequelize an SQL ORM
316 lines (259 loc) • 9.42 kB
JavaScript
const errors = require('@feathersjs/errors');
const { _ } = require('@feathersjs/commons');
const { select, AdapterService } = require('@feathersjs/adapter-commons');
const hooks = require('./hooks');
const utils = require('./utils');
const defaultOperators = Op => {
return {
$eq: Op.eq,
$ne: Op.ne,
$gte: Op.gte,
$gt: Op.gt,
$lte: Op.lte,
$lt: Op.lt,
$in: Op.in,
$nin: Op.notIn,
$like: Op.like,
$notLike: Op.notLike,
$iLike: Op.iLike,
$notILike: Op.notILike,
$or: Op.or,
$and: Op.and
};
};
class Service extends AdapterService {
constructor (options) {
let Sequelize;
if (options.Model) {
Sequelize = options.Model.sequelize.Sequelize;
} else if (options.Sequelize) {
Sequelize = options.Sequelize;
} else {
throw new Error('You must provide a Sequelize Model or the Sequelize class');
}
const defaultOps = defaultOperators(Sequelize.Op);
const operators = Object.assign(defaultOps, options.operators);
const whitelist = Object.keys(operators).concat(options.whitelist || []);
const { primaryKeyAttributes } = options.Model;
const id = typeof primaryKeyAttributes === 'object' && primaryKeyAttributes[0] !== undefined
? primaryKeyAttributes[0]
: 'id';
super(Object.assign({ id }, options, { operators, whitelist }));
this.raw = options.raw !== false;
}
get Op () {
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;
}
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;
}
applyScope (params = {}) {
if ((params.sequelize || {}).scope) {
return this.getModel(params).scope(params.sequelize.scope);
}
return this.getModel(params);
}
filterQuery (params = {}) {
const filtered = super.filterQuery(params);
const operators = this.options.operators;
const convertOperators = query => {
if (Array.isArray(query)) {
return query.map(convertOperators);
}
if (!utils.isPlainObject(query)) {
return query;
}
const converted = Object.keys(query).reduce((result, prop) => {
const value = query[prop];
const key = operators[prop] ? operators[prop] : prop;
result[key] = convertOperators(value);
return result;
}, {});
Object.getOwnPropertySymbols(query).forEach(symbol => {
converted[symbol] = query[symbol];
});
return converted;
};
filtered.query = Object.assign({}, convertOperators(filtered.query));
return filtered;
}
// returns either the model intance for an id or all unpaginated
// items for `params` if id is null
_getOrFind (id, params = {}) {
if (id === null) {
return this._find(Object.assign(params, {
paginate: false
}));
}
return this._get(id, params);
}
_find (params = {}) {
const { filters, query: where, paginate } = this.filterQuery(params);
const order = utils.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) {
q.attributes = filters.$select.map(select => `${select}`);
}
const Model = this.applyScope(params);
// 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 || [];
if (paginate && paginate.default) {
return Model.findAndCountAll(q).then(result => {
return {
total: result.count,
limit: filters.$limit,
skip: filters.$skip || 0,
data: result.rows
};
}).catch(utils.errorHandler);
}
return Model.findAll(q).catch(utils.errorHandler);
}
_get (id, params = {}) {
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);
const Model = this.applyScope(params);
// findById calls findAll under the hood. We use findAll so that
// eager loading can be used without a separate code path.
return Model.findAll(q).then(result => {
if (result.length === 0) {
throw new errors.NotFound(`No record found for id '${id}'`);
}
return result[0];
}).then(select(params, this.id)).catch(utils.errorHandler);
}
_create (data, params = {}) {
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 = Boolean(options.ignoreSetters);
const createOptions = Object.assign({
returning: true
}, options, { raw: ignoreSetters });
const isArray = Array.isArray(data);
const Model = this.applyScope(params);
const promise = isArray
? Model.bulkCreate(data, createOptions)
: Model.create(data, createOptions);
return promise.then(result => {
const sel = select(params, this.id);
if (options.raw === false) {
return result;
}
if (isArray) {
return result.map(item => sel(item.toJSON()));
}
return sel(result.toJSON());
}).catch(utils.errorHandler);
}
_patch (id, data, params = {}) {
const Model = this.applyScope(params);
// Get a list of ids that match the id/query. Overwrite the
// $select because only the id is needed for this idList
const idQuery = Object.assign({}, params.query, { $select: [this.id] });
const idParams = Object.assign({}, params, { query: idQuery });
const ids = this._getOrFind(id, idParams).then(result => {
const items = Array.isArray(result) ? result : [result];
return items.map(item => item[this.id]);
});
return ids.then(idList => {
// Create a new query that re-queries all ids that
// were originally changed
const findQuery = Object.assign(
{ [this.id]: { $in: idList } },
this.filterQuery(params).filters
);
const findParams = Object.assign({}, params, { query: findQuery });
const seqOptions = Object.assign(
{ raw: this.raw },
params.sequelize,
{ where: { [this.id]: { [this.Op.in]: idList } } }
);
return Model.update(_.omit(data, this.id), seqOptions)
.then(() => {
if (params.$returning !== false) {
return this._getOrFind(id, findParams);
} else {
return Promise.resolve([]);
}
});
}).then(select(params, this.id)).catch(utils.errorHandler);
}
_update (id, data, params = {}) {
const where = 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 });
return this._get(id, { sequelize: seqOptions, query: where }).then(instance => {
const copy = Object.keys(instance.toJSON()).reduce((result, key) => {
result[key] = typeof data[key] === 'undefined' ? null : data[key];
return result;
}, {});
return instance.update(copy, seqOptions)
.then(() => this._get(id, {
sequelize: Object.assign({}, seqOptions, {
raw: typeof (params.sequelize || {}).raw !== 'undefined'
? params.sequelize.raw
: this.raw
})
}));
})
.then(select(params, this.id))
.catch(utils.errorHandler);
}
_remove (id, params = {}) {
const opts = Object.assign({ raw: this.raw }, params);
const where = Object.assign({}, this.filterQuery(params).query);
if (id !== null) {
where[this.Op.and] = { [this.id]: id };
}
const options = Object.assign({}, { where }, params.sequelize);
const Model = this.applyScope(params);
if (params.$returning !== false) {
return this._getOrFind(id, opts).then(data => {
return Model.destroy(options).then(() => data);
})
.then(select(params, this.id))
.catch(utils.errorHandler);
} else {
return Model.destroy(options).then(() => Promise.resolve([]))
.then(select(params, this.id))
.catch(utils.errorHandler);
}
}
}
const init = options => new Service(options);
// Exposed Modules
module.exports = Object.assign(init, {
default: init,
ERROR: utils.ERROR,
hooks,
Service
});