diff --git a/README.md b/README.md index d9cfacb..07d6ce7 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ __Options:__ - `overwrite` (*optional*, default: `true`) - Overwrite the document when update, making mongoose detect is new document and trigger default value for unspecified properties in mongoose schema. - `discriminators` (*optional*) - A list of mongoose models that inherit from `Model`. - `useEstimatedDocumentCount` (*optional*, default: `false`) - Use `estimatedDocumentCount` instead (usually not necessary) +- `queryModifier` (*optional*) - A function that takes in the raw mongoose Query object and params, which modifies all find and get requests unless overridden. (see Query Modifiers below) +- `queryModifierKey` (*optional*, default: `'queryModifier'`) - The key to use to get the override query modifier function from the params. (see Query Modifiers below) > **Important:** To avoid odd error handling behaviour, always set `mongoose.Promise = global.Promise`. If not available already, Feathers comes with a polyfill for native Promises. @@ -346,6 +348,48 @@ let moduleExports = { module.exports = moduleExports; ``` +## Query Modifiers + +Sometimes it's important to use an unusual Mongoose Query method, like [specifying whether to read from a primary or secondary node,](https://mongoosejs.com/docs/api.html#query_Query-read) but maybe only for certain requests. + +You can access the internal Mongoose Query object used for a find/get request by specifying the queryModifier function. It is also possible to override that global function by specifying the function in a requests params. + +```js +// Specify a global query modifier when creating the service +app.use('/messages', service({ + Model, + queryModifier: (query, params) => { + query.read('secondaryPreferred'); + } +})); + +app.service('messages').find({ + query: { ... }, +}).then((result) => { + console.log('Result from secondary:', result) +}); + +// Override the modifier on a per-request basis +app.service('messages').find({ + query: { ... }, + queryModifier: (query, params) => { + query.read('primaryPreferred'); + } +}).then((result) => { + console.log('Result from primary:', result) +}); + +// Disable the global modifier on a per-request basis +app.service('messages').find({ + query: { ... }, + queryModifier: false +}).then((result) => { + console.log('Result from default option:', result) +}); +``` + +> **Note:** Due to replication lag, a secondary node can have "stale" data. You should ensure that this "staleness" will not be an issue for your application before reading from the secondary set. + ## Contributing This module is community maintained and open for pull requests. Features and bug fixes should contain diff --git a/lib/service.js b/lib/service.js index fb4980f..7a9de8a 100755 --- a/lib/service.js +++ b/lib/service.js @@ -19,7 +19,8 @@ class Service extends AdapterService { $populate (value) { return value; } - }, options.filters) + }, options.filters), + queryModifierKey: 'queryModifier' }, options, { whitelist: whitelist.concat('$and') })); @@ -40,6 +41,18 @@ class Service extends AdapterService { return this.options.Model; } + _getQueryModifier (params) { + if (typeof params[this.options.queryModifierKey] === 'function') { + return params[this.options.queryModifierKey]; + } + if (params[this.options.queryModifierKey] !== false) { + if (typeof this.options.queryModifier === 'function') { + return this.options.queryModifier; + } + } + return () => {}; + } + _getOrFind (id, params = {}) { if (id === null) { return this._find(params); @@ -88,6 +101,8 @@ class Service extends AdapterService { q.populate(filters.$populate); } + this._getQueryModifier(params)(q, params); + let executeQuery = total => q.session(params.mongoose && params.mongoose.session).exec().then(data => { return { total, @@ -141,6 +156,8 @@ class Service extends AdapterService { modelQuery.select(filters.$select); } + this._getQueryModifier(params)(modelQuery, params); + return modelQuery.session(params.mongoose && params.mongoose.session) .lean(this.lean).exec().then(data => { if (!data) { diff --git a/test/index.test.js b/test/index.test.js index 2a99c3c..cb4ef20 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -115,6 +115,13 @@ const app = feathers() multi: true, whitelist: ['$populate'] })) + .use('/pets3', adapter({ + Model: Pet, + multi: true, + queryModifier: (query) => { + query.where('type', 'cat'); + } + })) .use('/posts', adapter({ Model: Post, discriminators: [TextPost], @@ -125,6 +132,7 @@ const people = app.service('people'); const pets = app.service('pets'); const leanPeople = app.service('people2'); const leanPets = app.service('pets2'); +const QMPets = app.service('pets3'); const posts = app.service('posts'); describe('Feathers Mongoose Service', () => { @@ -631,6 +639,92 @@ describe('Feathers Mongoose Service', () => { }); }); + describe('Query Modifiers', () => { + beforeEach(async () => { + const rufus = await QMPets.create({ type: 'dog', name: 'Rufus' }); + const margeaux = await QMPets.create({ type: 'cat', name: 'Margeaux' }); + const bob = await QMPets.create({ type: 'cat', name: 'Bob' }); + + _petIds.Rufus = rufus._id; + _petIds.Margeaux = margeaux._id; + _petIds.Bob = bob._id; + }); + + afterEach(async () => { + await QMPets.remove(null, { query: {} }); + }); + + it('can apply a global query modifier with find', async () => { + const params = { + query: {} + }; + + const data = await QMPets.find(params); + + expect(data.length).to.equal(2); + expect(data[0].type).to.equal('cat'); + expect(data[1].type).to.equal('cat'); + }); + + it('can apply a global query modifier with get', async () => { + const margeaux = await QMPets.get(_petIds.Margeaux); + + expect(margeaux.name).to.equal('Margeaux'); + + try { + await QMPets.get(_petIds.Rufus); + throw new Error('Should never get here'); + } catch (error) { + expect(error.name).to.equal('NotFound'); + } + }); + + it('can apply a local query modifier with find', async () => { + const params = { + query: {}, + queryModifier: (query) => { + query.where('type', 'dog'); + } + }; + + const data = await QMPets.find(params); + + expect(data.length).to.equal(1); + expect(data[0].type).to.equal('dog'); + }); + + it('can apply a local query modifier with get', async () => { + const params = { + query: {}, + queryModifier: (query) => { + query.where('type', 'dog'); + } + }; + + const result = await QMPets.get(_petIds.Rufus, params); + + expect(result.name).to.equal('Rufus'); + + try { + await QMPets.get(_petIds.Margeaux, params); + throw new Error('Should never get here'); + } catch (error) { + expect(error.name).to.equal('NotFound'); + } + }); + + it('can disable the global query modifier', async () => { + const params = { + query: {}, + queryModifier: false + }; + + const data = await QMPets.find(params); + + expect(data.length).to.equal(3); + }); + }); + testSuite(app, errors, 'peeps', '_id'); testSuite(app, errors, 'peeps-customid', 'customid'); }); diff --git a/types/index.d.ts b/types/index.d.ts index 8e02c61..07ef6ac 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,7 +1,7 @@ // TypeScript Version: 4.0 import { Params, Paginated, Id, NullableId, Hook } from '@feathersjs/feathers'; import { AdapterService, ServiceOptions, InternalServiceMethods } from '@feathersjs/adapter-commons'; -import { Model, Document } from 'mongoose'; +import { Model, Document, Query } from 'mongoose'; export namespace hooks { function toObject(options?: any, dataField?: string): Hook; @@ -18,6 +18,8 @@ export interface MongooseServiceOptions extends Servic lean: boolean; overwrite: boolean; useEstimatedDocumentCount: boolean; + queryModifier?: (query: Query, params: Params) => void; + queryModifierKey?: string; } export class Service extends AdapterService implements InternalServiceMethods {