Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add query modifier function support. #409

Merged
merged 4 commits into from
Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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