UNPKG

@feathersjs/mongodb

Version:

Feathers MongoDB service adapter

432 lines 15.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MongoDbAdapter = void 0; const mongodb_1 = require("mongodb"); const errors_1 = require("@feathersjs/errors"); const commons_1 = require("@feathersjs/commons"); const adapter_commons_1 = require("@feathersjs/adapter-commons"); const error_handler_1 = require("./error-handler"); // Create the service. class MongoDbAdapter extends adapter_commons_1.AdapterBase { constructor(options) { if (!options) { throw new Error('MongoDB options have to be provided'); } super({ id: '_id', ...options }); } getObjectId(id) { if (this.options.disableObjectify) { return id; } if (this.id === '_id' && mongodb_1.ObjectId.isValid(id)) { id = new mongodb_1.ObjectId(id.toString()); } return id; } filterQuery(id, 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); if (id !== null) { query.$and = (query.$and || []).concat({ [this.id]: this.getObjectId(id) }); } if (query[this.id]) { query[this.id] = this.getObjectId(query[this.id]); } return { filters: { $select, $sort, $limit, $skip }, query }; } getModel(params = {}) { const { Model } = this.getOptions(params); return Promise.resolve(Model); } async findRaw(params) { const { filters, query } = this.filterQuery(null, params); const model = await this.getModel(params); const q = model.find(query, params.mongodb); if (filters.$sort !== undefined) { q.sort(filters.$sort); } if (filters.$select !== undefined) { q.project(this.getProjection(filters.$select)); } if (filters.$skip !== undefined) { q.skip(filters.$skip); } if (filters.$limit !== undefined) { q.limit(filters.$limit); } return q; } /* TODO: Remove $out and $merge stages, else it returns an empty cursor. I think its safe to assume this is primarily for querying. */ async aggregateRaw(params) { const model = await this.getModel(params); const pipeline = params.pipeline || []; const index = pipeline.findIndex((stage) => stage.$feathers); const before = index >= 0 ? pipeline.slice(0, index) : []; const feathersPipeline = this.makeFeathersPipeline(params); const after = index >= 0 ? pipeline.slice(index + 1) : pipeline; return model.aggregate([...before, ...feathersPipeline, ...after], params.mongodb); } makeFeathersPipeline(params) { const { filters, query } = this.filterQuery(null, params); const pipeline = [{ $match: query }]; if (filters.$sort !== undefined) { pipeline.push({ $sort: filters.$sort }); } if (filters.$skip !== undefined) { pipeline.push({ $skip: filters.$skip }); } if (filters.$limit !== undefined) { pipeline.push({ $limit: filters.$limit }); } if (filters.$select !== undefined) { pipeline.push({ $project: this.getProjection(filters.$select) }); } return pipeline; } getProjection(select) { if (!select) { return undefined; } if (Array.isArray(select)) { if (!select.includes(this.id)) { select = [this.id, ...select]; } return select.reduce((value, name) => ({ ...value, [name]: 1 }), {}); } if (!select[this.id]) { return { ...select, [this.id]: 1 }; } return select; } normalizeId(id, data) { if (this.id === '_id') { // Default Mongo IDs cannot be updated. The Mongo library handles // this automatically. return commons_1._.omit(data, this.id); } else if (id !== null) { // If not using the default Mongo _id field set the ID to its // previous value. This prevents orphaned documents. return { ...data, [this.id]: id }; } return data; } async countDocuments(params) { const { useEstimatedDocumentCount } = this.getOptions(params); const { query } = this.filterQuery(null, params); if (params.pipeline) { const aggregateParams = { ...params, paginate: false, pipeline: [...params.pipeline, { $count: 'total' }], query: { ...params.query, $select: [this.id], $sort: undefined, $skip: undefined, $limit: undefined } }; const [result] = await this.aggregateRaw(aggregateParams).then((result) => result.toArray()); if (!result) { return 0; } return result.total; } const model = await this.getModel(params); if (useEstimatedDocumentCount && typeof model.estimatedDocumentCount === 'function') { return model.estimatedDocumentCount(); } return model.countDocuments(query, params.mongodb); } async _get(id, params = {}) { const { query, filters: { $select } } = this.filterQuery(id, params); if (params.pipeline) { const aggregateParams = { ...params, query: { ...params.query, $limit: 1, $and: (params.query.$and || []).concat({ [this.id]: this.getObjectId(id) }) } }; return this.aggregateRaw(aggregateParams) .then((result) => result.toArray()) .then(([result]) => { if (!result) { throw new errors_1.NotFound(`No record found for id '${id}'`); } return result; }) .catch(error_handler_1.errorHandler); } const findOptions = { projection: this.getProjection($select), ...params.mongodb }; return this.getModel(params) .then((model) => model.findOne(query, findOptions)) .then((result) => { if (!result) { throw new errors_1.NotFound(`No record found for id '${id}'`); } return result; }) .catch(error_handler_1.errorHandler); } async _find(params = {}) { const { paginate } = this.getOptions(params); const { filters } = this.filterQuery(null, params); const paginationDisabled = params.paginate === false || !paginate || !paginate.default; const getData = () => { const result = params.pipeline ? this.aggregateRaw(params) : this.findRaw(params); return result.then((result) => result.toArray()); }; if (paginationDisabled) { if (filters.$limit === 0) { return []; } const data = await getData(); return data; } if (filters.$limit === 0) { return { total: await this.countDocuments(params), data: [], limit: filters.$limit, skip: filters.$skip || 0 }; } const [data, total] = await Promise.all([getData(), this.countDocuments(params)]); return { total, data: data, limit: filters.$limit, skip: filters.$skip || 0 }; } async _create(data, params = {}) { var _a, _b; if (Array.isArray(data) && !this.allowsMulti('create', params)) { throw new errors_1.MethodNotAllowed('Can not create multiple entries'); } const model = await this.getModel(params); const setId = (item) => { const entry = Object.assign({}, item); if (this.id !== '_id' && typeof entry[this.id] === 'undefined') { return { [this.id]: new mongodb_1.ObjectId().toHexString(), ...entry }; } return entry; }; if (Array.isArray(data)) { const created = await model.insertMany(data.map(setId), params.mongodb).catch(error_handler_1.errorHandler); return this._find({ ...params, paginate: false, query: { _id: { $in: Object.values(created.insertedIds) }, $select: (_a = params.query) === null || _a === void 0 ? void 0 : _a.$select } }); } const created = await model.insertOne(setId(data), params.mongodb).catch(error_handler_1.errorHandler); const result = await this._find({ ...params, paginate: false, query: { _id: created.insertedId, $select: (_b = params.query) === null || _b === void 0 ? void 0 : _b.$select, $limit: 1 } }); return result[0]; } async _patch(id, _data, params = {}) { if (id === null && !this.allowsMulti('patch', params)) { throw new errors_1.MethodNotAllowed('Can not patch multiple entries'); } const data = this.normalizeId(id, _data); const model = await this.getModel(params); const { query, filters: { $sort, $select } } = this.filterQuery(id, params); const replacement = Object.keys(data).reduce((current, key) => { const value = data[key]; if (key.charAt(0) !== '$') { current.$set[key] = value; } else if (key === '$set') { current.$set = { ...current.$set, ...value }; } else { current[key] = value; } return current; }, { $set: {} }); if (id === null) { const findParams = { ...params, paginate: false, query: { ...params.query, $select: [this.id] } }; return this._find(findParams) .then(async (result) => { const idList = result.map((item) => item[this.id]); await model.updateMany({ [this.id]: { $in: idList } }, replacement, params.mongodb); return this._find({ ...params, paginate: false, query: { [this.id]: { $in: idList }, $sort, $select } }); }) .catch(error_handler_1.errorHandler); } if (params.pipeline) { const getParams = { ...params, query: { ...params.query, $select: [this.id] } }; return this._get(id, getParams) .then(async () => { await model.updateOne({ [this.id]: id }, replacement, params.mongodb); return this._get(id, { ...params, query: { $select } }); }) .catch(error_handler_1.errorHandler); } const updateOptions = { projection: this.getProjection($select), ...params.mongodb, returnDocument: 'after' }; return model .findOneAndUpdate(query, replacement, updateOptions) .then((result) => { if (!result) { throw new errors_1.NotFound(`No record found for id '${id}'`); } return result; }) .catch(error_handler_1.errorHandler); } 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 { query, filters: { $select } } = this.filterQuery(id, params); const model = await this.getModel(params); const replacement = this.normalizeId(id, data); if (params.pipeline) { const getParams = { ...params, query: { ...params.query, $select: [this.id] } }; return this._get(id, getParams) .then(async () => { await model.replaceOne({ [this.id]: id }, replacement, params.mongodb); return this._get(id, { ...params, query: { $select } }); }) .catch(error_handler_1.errorHandler); } const replaceOptions = { projection: this.getProjection($select), ...params.mongodb, returnDocument: 'after' }; return model .findOneAndReplace(query, replacement, replaceOptions) .then((result) => { if (!result) { throw new errors_1.NotFound(`No record found for id '${id}'`); } return result; }) .catch(error_handler_1.errorHandler); } 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 = await this.getModel(params); const { query } = this.filterQuery(id, params); const findParams = { ...params, paginate: false }; if (id === null) { return this._find(findParams) .then(async (result) => { const idList = result.map((item) => item[this.id]); await model.deleteMany({ [this.id]: { $in: idList } }, params.mongodb); return result; }) .catch(error_handler_1.errorHandler); } if (params.pipeline) { return this._get(id, params) .then(async (result) => { await model.deleteOne({ [this.id]: id }, params.mongodb); return result; }) .catch(error_handler_1.errorHandler); } const deleteOptions = { ...params.mongodb, projection: this.getProjection((_a = params.query) === null || _a === void 0 ? void 0 : _a.$select) }; return model .findOneAndDelete(query, deleteOptions) .then((result) => { if (!result) { throw new errors_1.NotFound(`No record found for id '${id}'`); } return result; }) .catch(error_handler_1.errorHandler); } } exports.MongoDbAdapter = MongoDbAdapter; //# sourceMappingURL=adapter.js.map