@feathersjs/mongodb
Version:
Feathers MongoDB service adapter
432 lines • 15.8 kB
JavaScript
"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