diff --git a/README.md b/README.md index 06588a94..2f3c2bba 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,34 @@ Now in your query, you can specify a value for your discriminatorKey: and Feathers will automatically swap in the correct model and execute the query it instead of its parent model. +## Collation Support + +This adapter includes support for [collation and case insensitive indexes available in MongoDB v3.4](https://docs.mongodb.com/manual/release-notes/3.4/#collation-and-case-insensitive-indexes). Collation parameters may be passed using the special `collation` parameter to the `find()`, `remove()` and `patch()` methods. + +### Example: Patch records with case-insensitive alphabetical ordering + +The example below would patch all student records with grades of `'c'` or `'C'` and above (a natural language ordering). Without collations this would not be as simple, since the comparison `{ $gt: 'c' }` would not include uppercase grades of `'C'` because the code point of `'C'` is less than that of `'c'`. + +```js +const patch = { shouldStudyMore: true }; +const query = { grade: { $gte: 'c' } }; +const collation = { locale: 'en', strength: 1 }; +students.patch(null, patch, { query, collation }).then( ... ); +``` + +### Example: Find records with a case-insensitive search + +Similar to the above example, this would find students with a grade of `'c'` or greater, in a case-insensitive manner. + +```js +const query = { grade: { $gte: 'c' } }; +const collation = { locale: 'en', strength: 1 }; +students.find({ query, collation }).then( ... ); +``` + +For more information on MongoDB's collation feature, visit the [collation reference page](https://docs.mongodb.com/manual/reference/collation/). + + ## License [MIT](LICENSE) diff --git a/lib/service.js b/lib/service.js index cd519d04..cf6e93f1 100755 --- a/lib/service.js +++ b/lib/service.js @@ -61,6 +61,11 @@ class Service { q.sort(filters.$sort); } + // Handle collation + if (params.collation) { + q.collation(params.collation); + } + // Handle $limit if (typeof filters.$limit !== 'undefined') { q.limit(filters.$limit); @@ -232,6 +237,10 @@ class Service { const query = Object.assign({}, filterQuery(params.query || {}).query); const mapIds = page => page.data.map(current => current[this.id]); + if (params.collation) { + query.collation = params.collation; + } + // By default we will just query for the one id. For multi patch // we create a list of the ids of all items that will be changed // to re-query them after the update @@ -303,6 +312,10 @@ class Service { remove (id, params) { const query = Object.assign({}, filterQuery(params.query || {}).query); + if (params.collation) { + query.collation = params.collation; + } + if (id !== null) { query[this.id] = id; } diff --git a/test/error-handler.test.js b/test/error-handler.test.js index 7a273b86..45aac15b 100644 --- a/test/error-handler.test.js +++ b/test/error-handler.test.js @@ -82,7 +82,7 @@ describe('Feathers Mongoose Error Handler', () => { }); it('wraps a VersionError as a BadRequest', done => { - let e = new mongoose.Error.VersionError({ _id: 'testing' }); + let e = new mongoose.Error.VersionError({ _id: 'testing' }, null, []); errorHandler(e).catch(error => { expect(error).to.be.an.instanceof(errors.BadRequest); done(); diff --git a/test/index.test.js b/test/index.test.js index c34d819d..6ddaa8ec 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -99,6 +99,98 @@ describe('Feathers Mongoose Service', () => { }); }); + describe('Special collation param', () => { + function indexOfName (results, name) { + let index; + results.every(function (person, i) { + if (person.name === name) { + index = i; + return false; + } + return true; + }); + return index; + } + + beforeEach(() => { + return people.remove(null, {}).then(() => { + return people.create([ + { name: 'AAA' }, + { name: 'aaa' }, + { name: 'ccc' } + ]); + }); + }); + + it('sorts with default behavior without collation param', () => { + return people.find({ query: { $sort: { name: -1 } } }).then(r => { + expect(indexOfName(r, 'aaa')).to.be.below(indexOfName(r, 'AAA')); + }); + }); + + it('sorts using collation param if present', () => { + return people + .find({ + query: { $sort: { name: -1 } }, + collation: { locale: 'en', strength: 1 } + }) + .then(r => { + expect(indexOfName(r, 'AAA')).to.be.below(indexOfName(r, 'aaa')); + }); + }); + + it('removes with default behavior without collation param', () => { + return people + .remove(null, { query: { name: { $gt: 'AAA' } } }) + .then(() => { + return people.find().then(r => { + expect(r).to.have.lengthOf(1); + expect(r[0].name).to.equal('AAA'); + }); + }); + }); + + it('removes using collation param if present', () => { + return people + .remove(null, { + query: { name: { $gt: 'AAA' } }, + collation: { locale: 'en', strength: 1 } + }) + .then(() => { + return people.find().then(r => { + expect(r).to.have.lengthOf(3); + }); + }); + }); + + it('updates with default behavior without collation param', () => { + const query = { name: { $gt: 'AAA' } }; + + return people.patch(null, { age: 99 }, { query }).then(r => { + expect(r).to.have.lengthOf(2); + r.forEach(person => { + expect(person.age).to.equal(99); + }); + }); + }); + + it('updates using collation param if present', () => { + return people + .patch( + null, + { age: 110 }, + { + query: { name: { $gt: 'AAA' } }, + collation: { locale: 'en', strength: 1 } + } + ) + .then(r => { + expect(r).to.have.lengthOf(1); + expect(r[0].name).to.equal('ccc'); + }); + }); + }); + describe('Common functionality', () => { beforeEach(() => { // FIXME (EK): This is shit. We should be loading fixtures