feathers-ottoman
Version:
A Feathers service adapter for the Ottoman ODM
347 lines • 12.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Service = void 0;
const adapter_commons_1 = require("@feathersjs/adapter-commons");
const errors_1 = require("@feathersjs/errors");
const ottoman_1 = require("ottoman");
const defaultOptions = {
id: 'id',
ottoman: {
lean: true,
consistency: ottoman_1.SearchConsistency.NONE,
},
};
const SORT_ORDER = new Map([
[1, 'ASC'],
[-1, 'DESC'],
]);
const _select = (data, params, ...args) => {
if (!params.query?.$select)
return data;
const base = (0, adapter_commons_1.select)(params, ...args);
return base(JSON.parse(JSON.stringify(data)));
};
/**
* Allow `$ignoreCase` as additional filters option
* See {@link https://ottomanjs.com/classes/findoptions.html findOptions}
*/
const filterQueryOpts = {
filters: ['$ignoreCase'],
};
const _hasQueryDefined = (data) => Object.keys(data).length > 0;
// this.filterQuery returns { filters: {}, query: {}, paginate: {} }
// filters = $sort, $skip, $limit, $select
class OttomanService extends adapter_commons_1.AdapterService {
_options;
constructor(config) {
if (!config.Model)
throw new Error('Model must be provided');
super({ ...defaultOptions, ...config });
this._options = config;
}
get Model() {
return this._options.Model;
}
/**
* In Couchbase, an id can be number but internally, it is still stored as string
* Hence, the query by Id still needs to pass in as String rather than Number
*
* @param id Id
* @returns id in string
*/
_getId(id) {
if (typeof id === 'string')
return id;
return id.toString();
}
/**
* Maps $select to select
*
* Append `id` field to $select if not defined by caller
* Without doing so, the result to caller would be without `id` field
*
* @param filters filters
* @returns select filters
*/
_getSelectQuery(filters) {
const { $select } = filters;
if (!$select)
return {};
// must always concat with id
if (!$select.includes(this.id)) {
$select.push(this.id);
}
return { select: $select };
}
/**
* Maps $sort to sort
* - 1 to ASC
* - -1 to DESC
*
* @param filters filters
* @returns sort filters
*/
_getSortQuery(filters) {
const { $sort } = filters;
if (!$sort)
return {};
const sort = Object.entries($sort).reduce((acc, [k, v]) => ({
...acc,
[k]: SORT_ORDER.get(v),
}), {});
return { sort };
}
/**
* Maps $limit to limit
* - 1 to ASC
* - -1 to DESC
*
* @param filters filters
* @returns limit filters
*/
_getLimitQuery(filters) {
const { $limit } = filters;
// 0 is a valid value where should return empty data array
// See https://docs.feathersjs.com/api/databases/querying.html#limit
if (!$limit && $limit !== 0)
return {};
return {
// may need to abs($limit)
// see https://github.com/Automattic/mongoose/issues/3473
// but check with what's the behavior for common API
limit: $limit,
};
}
/**
* Maps $skip to skip
*
* @param filters filters
* @returns skip filters
*/
_getSkipQuery(filters) {
const { $skip } = filters;
if (!$skip)
return {};
return { skip: $skip };
}
/**
* Maps $ignoreCase to ignoreCase
*
* @param filters filters
* @returns ignoreCase filters
*/
_getIgnoreCaseQuery(filters) {
const { $ignoreCase } = filters;
if (!$ignoreCase)
return {};
return { ignoreCase: $ignoreCase };
}
/**
* We need to map some of the operator between Common API and Ottoman
*
* Maps (Common API : Ottoman):
* - $ne : $neq
* - $nin: $notIn
*
* After mapping, the correct query construct can then be passed into
* Ottoman API options so that it can process correctly
*
* Since `Ottoman.beta.3`, it simplify some of the operator query such as `$in, $notIn, etc`
* See {@link https://github.com/bwgjoseph/mongoose-vs-ottoman/issues/87 simplify operator usage}
*
* @param query query
* @returns Query
*/
_mapQueryOperator(query) {
const keys = new Map();
Object.entries(query)
.forEach(([k, v]) => {
if (v && typeof v === 'object' && v.$ne) {
keys.set(k, { $neq: v.$ne });
}
else if (v && typeof v === 'object' && v.$nin) {
keys.set(k, { $notIn: v.$nin });
}
else {
keys.set(k, v);
}
});
let operatorQuery = {};
keys.forEach((v, k) => {
// assign default query
const q = { [k]: v };
operatorQuery = {
...operatorQuery,
...q,
};
});
return operatorQuery;
}
/**
* Workaround to support '.get/remove/update/patch + id + query *' syntax
* as Ottoman does not allow to pass in additional query with `*ById` method
*
* @param id Id
* @param params Params
* @returns reconstructed query
*/
_getQuery(id, params) {
let { query } = this.filterQuery(params);
query = this._mapQueryOperator(query);
if (id) {
// Pass in both `id` and `query.id`
if (query[this.id]) {
const { [this.id]: tId, ...restQuery } = query;
return {
...restQuery,
$and: [
{
[this.id]: id,
},
{
[this.id]: query[this.id],
},
],
};
}
return {
...query,
[this.id]: id,
};
}
return query;
}
/**
* Construct the filters to pass into Ottoman API options
*
* @param filters filters
* @param method find | default
* @returns Ottoman options
*/
_getOptions(filters, method = 'default') {
if (method === 'default') {
return {
...this._options.ottoman,
...this._getSelectQuery(filters),
};
}
return {
...this._options.ottoman,
...this._getSelectQuery(filters),
...this._getSortQuery(filters),
...this._getLimitQuery(filters),
...this._getSkipQuery(filters),
...this._getIgnoreCaseQuery(filters),
};
}
async _find(params = {}) {
const { filters, paginate, query } = this.filterQuery(params, filterQueryOpts);
const cQuery = this._mapQueryOperator(query);
const cOptions = this._getOptions(filters, 'find');
const [result, total] = await Promise.all([
this.Model.find(cQuery, cOptions),
Object.keys(paginate).length > 0 ? this.Model.count(cQuery) : -1,
]);
if (total >= 0) {
return {
total,
limit: filters.$limit,
skip: filters.$skip || 0,
data: result.rows,
};
}
return (result.rows && result.rows.length > 0) ? result.rows : [];
}
async _get(id, params = {}) {
const { filters, query } = this.filterQuery(params);
const cOptions = this._getOptions(filters);
if (_hasQueryDefined(query)) {
const cQuery = this._getQuery(this._getId(id), params);
const { rows } = await this.Model.find(cQuery, cOptions);
if (rows && rows[0])
return rows[0];
throw new errors_1.NotFound(`No record found for id ${id}`);
}
return this.Model
.findById(this._getId(id), cOptions)
.catch(() => { throw new errors_1.NotFound(`No record found for id ${id}`); });
}
async _create(data, params = {}) {
if (Array.isArray(data)) {
return Promise.all(data.map((current) => this._create(current, params)));
}
return this.Model
.create(data)
.then((result) => _select(result, params, this.id));
}
async _update(id, data, params = {}) {
if (id === null) {
return Promise.reject(new errors_1.BadRequest('You can not replace multiple instances. Did you mean \'patch\'?'));
}
const { filters, query } = this.filterQuery(params);
if (_hasQueryDefined(query)) {
const cQuery = this._getQuery(this._getId(id), params);
const cOptions = this._getOptions(filters);
const { message } = await this.Model.updateMany(cQuery, data, cOptions);
if (message && message.success > 0)
return _select(message.data, params, this.id);
throw new errors_1.NotFound(`No record found for id ${id}`);
}
return this.Model
.replaceById(this._getId(id), data)
.then((result) => _select(result, params, this.id))
.catch(() => { throw new errors_1.NotFound(`No record found for id ${id}`); });
}
async _patch(id, data, params = {}) {
const { filters, query } = this.filterQuery(params);
const cOptions = this._getOptions(filters);
if (id === null) {
const cQuery = this._mapQueryOperator(query);
const entries = await this._find({ ...params, paginate: false });
const { message } = await this.Model.updateMany(cQuery, data, cOptions);
if (message && message.success > 0) {
return entries.map((e) => ({ ...e, ...data }));
}
throw new errors_1.NotFound(`No record found for query ${query}`);
}
if (_hasQueryDefined(query)) {
const cQuery = this._getQuery(this._getId(id), params);
const { message } = await this.Model.updateMany(cQuery, data, cOptions);
if (message && message.success > 0)
return _select(message.data, params, this.id);
throw new errors_1.NotFound(`No record found for id ${id}`);
}
return this.Model
.updateById(this._getId(id), data)
.then((result) => _select(result, params, this.id))
.catch(() => { throw new errors_1.NotFound(`No record found for id ${id}`); });
}
async _remove(id, params = {}) {
const { query } = this.filterQuery(params);
if (id === null) {
const cQuery = this._mapQueryOperator(query);
// get all current data before removing
const allData = await this._find({ ...params, paginate: false });
await this.Model.removeMany(cQuery, this._options.ottoman);
return allData;
}
// get current data before removing
const data = await this._get(this._getId(id));
if (_hasQueryDefined(query)) {
const { filters } = this.filterQuery(params);
const cQuery = this._getQuery(this._getId(id), params);
const cOptions = this._getOptions(filters);
const { message } = await this.Model.removeMany(cQuery, cOptions);
if (message && message.success > 0)
return _select(data, params, this.id);
throw new errors_1.NotFound(`No record found for id ${id}`);
}
return this.Model
.removeById(this._getId(id))
.then(() => _select(data, params, this.id));
}
}
exports.Service = OttomanService;
const InternalOttomanService = (options) => new OttomanService(options);
exports.default = InternalOttomanService;
//# sourceMappingURL=index.js.map