Skip to content

Commit

Permalink
Merge branch 'master' into perf/id-and-in
Browse files Browse the repository at this point in the history
  • Loading branch information
DaddyWarbucks committed Jun 6, 2024
2 parents b7a01b6 + 7e9f939 commit 226b0bf
Showing 1 changed file with 333 additions and 0 deletions.
333 changes: 333 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
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 Model = this.applyScope(params);

if (params.$returning === false) {
params = {
...params,
query: {
...params.query,
$select: [this.id]
}
};
} else if (params.query && params.query.$select) {
params = {
...params,
query: {
...params.query,
$select: [...params.query.$select, this.id]
}
};
}

return this._getOrFind(id, params).then(results => {
const items = Array.isArray(results) ? results : [results];
const idList = items.map(item => item[this.id]);

const seqOptions = Object.assign(
{ raw: this.raw },
params.sequelize,
{ where: { [this.id]: { [this.Op.in]: idList } } }
);

return Model.destroy(seqOptions)
.then(() => {
if (params.$returning === false) {
return Promise.resolve([]);
} else {
return results;
}
});
}).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
});

0 comments on commit 226b0bf

Please sign in to comment.