Skip to content

Commit

Permalink
Add query modifier support. (#409)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrfrase3 authored Nov 16, 2021
1 parent 685c331 commit 8aa157f
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 2 deletions.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion lib/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class Service extends AdapterService {
$populate (value) {
return value;
}
}, options.filters)
}, options.filters),
queryModifierKey: 'queryModifier'
}, options, {
whitelist: whitelist.concat('$and')
}));
Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
94 changes: 94 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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');
});
4 changes: 3 additions & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,6 +18,8 @@ export interface MongooseServiceOptions<T extends Document = any> extends Servic
lean: boolean;
overwrite: boolean;
useEstimatedDocumentCount: boolean;
queryModifier?: (query: Query, params: Params) => void;
queryModifierKey?: string;
}

export class Service<T = any> extends AdapterService<T> implements InternalServiceMethods<T> {
Expand Down

0 comments on commit 8aa157f

Please sign in to comment.