From 17b3dc826fe162a07c4378838f37bc91620ab3f9 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Sun, 16 Dec 2018 13:41:16 -0800 Subject: [PATCH] feat: Common database adapter utilities and test suite (#1130) BREAKING CHANGE: Move database adapter utilities from @feathersjs/commons into its own module --- .codeclimate.yml | 1 + packages/adapter-commons/LICENSE | 22 + packages/adapter-commons/README.md | 22 + .../lib/filter-query.js | 19 +- packages/adapter-commons/lib/index.js | 38 ++ packages/adapter-commons/lib/service.js | 87 ++++ packages/adapter-commons/lib/sort.js | 92 ++++ packages/adapter-commons/lib/tests/basic.js | 53 +++ packages/adapter-commons/lib/tests/index.js | 46 ++ packages/adapter-commons/lib/tests/methods.js | 446 ++++++++++++++++++ packages/adapter-commons/lib/tests/syntax.js | 339 +++++++++++++ packages/adapter-commons/package-lock.json | 93 ++++ packages/adapter-commons/package.json | 44 ++ packages/adapter-commons/test/commons.test.js | 72 +++ .../adapter-commons/test/filter-query.test.js | 222 +++++++++ packages/adapter-commons/test/service.test.js | 115 +++++ packages/adapter-commons/test/sort.test.js | 180 +++++++ packages/adapter-commons/tests.js | 1 + packages/commons/lib/commons.js | 5 - packages/commons/lib/index.js | 4 + packages/commons/lib/utils.js | 120 ----- packages/commons/package.json | 2 +- packages/commons/test/filter-query.test.js | 180 ------- packages/commons/test/hooks.test.js | 76 +-- packages/commons/test/module.test.js | 6 +- packages/commons/test/utils.test.js | 251 +--------- 26 files changed, 1933 insertions(+), 603 deletions(-) create mode 100644 packages/adapter-commons/LICENSE create mode 100644 packages/adapter-commons/README.md rename packages/{commons => adapter-commons}/lib/filter-query.js (84%) create mode 100644 packages/adapter-commons/lib/index.js create mode 100644 packages/adapter-commons/lib/service.js create mode 100644 packages/adapter-commons/lib/sort.js create mode 100644 packages/adapter-commons/lib/tests/basic.js create mode 100644 packages/adapter-commons/lib/tests/index.js create mode 100644 packages/adapter-commons/lib/tests/methods.js create mode 100644 packages/adapter-commons/lib/tests/syntax.js create mode 100644 packages/adapter-commons/package-lock.json create mode 100644 packages/adapter-commons/package.json create mode 100644 packages/adapter-commons/test/commons.test.js create mode 100644 packages/adapter-commons/test/filter-query.test.js create mode 100644 packages/adapter-commons/test/service.test.js create mode 100644 packages/adapter-commons/test/sort.test.js create mode 100644 packages/adapter-commons/tests.js delete mode 100644 packages/commons/lib/commons.js create mode 100644 packages/commons/lib/index.js delete mode 100644 packages/commons/test/filter-query.test.js diff --git a/.codeclimate.yml b/.codeclimate.yml index 7db34f33a3..3bc1b9848a 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -37,6 +37,7 @@ plugins: count_threshold: 3 exclude_patterns: - "**/test/*" + - "**/tests/*" - "**/dist/*" - "**/*.dist.js" - "**/templates/*" \ No newline at end of file diff --git a/packages/adapter-commons/LICENSE b/packages/adapter-commons/LICENSE new file mode 100644 index 0000000000..6bfc0adefc --- /dev/null +++ b/packages/adapter-commons/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2018 Feathers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/packages/adapter-commons/README.md b/packages/adapter-commons/README.md new file mode 100644 index 0000000000..e68f3dcc0c --- /dev/null +++ b/packages/adapter-commons/README.md @@ -0,0 +1,22 @@ +# Feathers Adapter Commons + +[![Build Status](https://travis-ci.org/feathersjs/feathers.png?branch=master)](https://travis-ci.org/feathersjs/feathers) +[![Dependency Status](https://img.shields.io/david/feathersjs/feathers.svg?style=flat-square&path=packages/commons)](https://david-dm.org/feathersjs/feathers?path=packages/commons) +[![Download Status](https://img.shields.io/npm/dm/@feathersjs/adapter-commons.svg?style=flat-square)](https://www.npmjs.com/package/@feathersjs/adapter-commons) + +> Shared utility functions for Feathers adatabase adapters + +## About + +This is a repository for handling Feathers common database syntax. + + +## Authors + +[Feathers contributors](https://github.com/feathersjs/commons/graphs/contributors) + +## License + +Copyright (c) 2018 Feathers contributors + +Licensed under the [MIT license](LICENSE). diff --git a/packages/commons/lib/filter-query.js b/packages/adapter-commons/lib/filter-query.js similarity index 84% rename from packages/commons/lib/filter-query.js rename to packages/adapter-commons/lib/filter-query.js index a82bb5c0e6..42d79b069b 100644 --- a/packages/commons/lib/filter-query.js +++ b/packages/adapter-commons/lib/filter-query.js @@ -1,4 +1,4 @@ -const { _ } = require('./utils'); +const { _ } = require('@feathersjs/commons'); function parse (number) { if (typeof number !== 'undefined') { @@ -34,10 +34,13 @@ function convertSort (sort) { } function cleanQuery (query, operators) { - if (_.isObject(query)) { + if (_.isObject(query) && query.constructor === {}.constructor) { const result = {}; _.each(query, (query, key) => { - if (key[0] === '$' && operators.indexOf(key) === -1) return; + if (key[0] === '$' && operators.indexOf(key) === -1) { + return; + } + result[key] = cleanQuery(query, operators); }); return result; @@ -49,11 +52,17 @@ function cleanQuery (query, operators) { function assignFilters (object, query, filters, options) { if (Array.isArray(filters)) { _.each(filters, (key) => { - object[key] = query[key]; + if (query[key] !== undefined) { + object[key] = query[key]; + } }); } else { _.each(filters, (converter, key) => { - object[key] = converter(query[key], options); + const converted = converter(query[key], options); + + if (converted !== undefined) { + object[key] = converted; + } }); } diff --git a/packages/adapter-commons/lib/index.js b/packages/adapter-commons/lib/index.js new file mode 100644 index 0000000000..ee2ab501e1 --- /dev/null +++ b/packages/adapter-commons/lib/index.js @@ -0,0 +1,38 @@ +const { _ } = require('@feathersjs/commons'); + +const AdapterService = require('./service'); +const filterQuery = require('./filter-query'); +const sort = require('./sort'); + +// Return a function that filters a result object or array +// and picks only the fields passed as `params.query.$select` +// and additional `otherFields` +const select = function select (params, ...otherFields) { + const fields = params && params.query && params.query.$select; + + if (Array.isArray(fields) && otherFields.length) { + fields.push(...otherFields); + } + + const convert = result => { + if (!Array.isArray(fields)) { + return result; + } + + return _.pick(result, ...fields); + }; + + return result => { + if (Array.isArray(result)) { + return result.map(convert); + } + + return convert(result); + }; +}; + +module.exports = Object.assign({ + select, + filterQuery, + AdapterService +}, sort); diff --git a/packages/adapter-commons/lib/service.js b/packages/adapter-commons/lib/service.js new file mode 100644 index 0000000000..c2dddcd03b --- /dev/null +++ b/packages/adapter-commons/lib/service.js @@ -0,0 +1,87 @@ +const { NotImplemented, BadRequest, MethodNotAllowed } = require('@feathersjs/errors'); +const filterQuery = require('./filter-query'); + +const callMethod = (self, name, ...args) => { + if (typeof self[name] !== 'function') { + return Promise.reject(new NotImplemented(`Method ${name} not available`)); + } + + return self[name](...args); +}; + +const checkMulti = (method, option) => { + if (option === true) { + return; + } + + return Array.isArray(option) ? option.includes(method) : false; +}; + +module.exports = class AdapterService { + constructor (options) { + this.options = Object.assign({ + events: [], + paginate: {}, + multi: false + }, options); + } + + get id () { + return this.options.id; + } + + get events () { + return this.options.events; + } + + filterQuery (params = {}, options = {}) { + const paginate = typeof params.paginate !== 'undefined' + ? params.paginate : this.options.paginate; + const { query = {} } = params; + const result = filterQuery(query, Object.assign({ paginate }, options)); + + return Object.assign(result, { paginate }); + } + + find (params) { + return callMethod(this, '_find', params); + } + + get (id, params) { + return callMethod(this, '_get', id, params); + } + + create (data, params) { + if (Array.isArray(data) && !checkMulti('create', this.options.multi)) { + return Promise.reject(new MethodNotAllowed(`Can not create multiple entries`)); + } + + return callMethod(this, '_create', data, params); + } + + update (id, data, params) { + if (id === null || Array.isArray(data)) { + return Promise.reject(new BadRequest( + `You can not replace multiple instances. Did you mean 'patch'?` + )); + } + + return callMethod(this, '_update', id, data, params); + } + + patch (id, data, params) { + if (id === null && !checkMulti('patch', this.options.multi)) { + return Promise.reject(new MethodNotAllowed(`Can not patch multiple entries`)); + } + + return callMethod(this, '_patch', id, data, params); + } + + remove (id, data, params) { + if (id === null && !checkMulti('remove', this.options.multi)) { + return Promise.reject(new MethodNotAllowed(`Can not remove multiple entries`)); + } + + return callMethod(this, '_remove', id, data, params); + } +}; diff --git a/packages/adapter-commons/lib/sort.js b/packages/adapter-commons/lib/sort.js new file mode 100644 index 0000000000..5c75429592 --- /dev/null +++ b/packages/adapter-commons/lib/sort.js @@ -0,0 +1,92 @@ +// Sorting algorithm taken from NeDB (https://github.com/louischatriot/nedb) +// See https://github.com/louischatriot/nedb/blob/e3f0078499aa1005a59d0c2372e425ab789145c1/lib/model.js#L189 + +exports.compareNSB = function (a, b) { + if (a < b) { return -1; } + if (a > b) { return 1; } + return 0; +}; + +exports.compareArrays = function (a, b) { + var i, comp; + + for (i = 0; i < Math.min(a.length, b.length); i += 1) { + comp = exports.compare(a[i], b[i]); + + if (comp !== 0) { return comp; } + } + + // Common section was identical, longest one wins + return exports.compareNSB(a.length, b.length); +}; + +exports.compare = function (a, b, compareStrings = exports.compareNSB) { + const { compareNSB, compare, compareArrays } = exports; + + // undefined + if (a === undefined) { return b === undefined ? 0 : -1; } + if (b === undefined) { return a === undefined ? 0 : 1; } + + // null + if (a === null) { return b === null ? 0 : -1; } + if (b === null) { return a === null ? 0 : 1; } + + // Numbers + if (typeof a === 'number') { return typeof b === 'number' ? compareNSB(a, b) : -1; } + if (typeof b === 'number') { return typeof a === 'number' ? compareNSB(a, b) : 1; } + + // Strings + if (typeof a === 'string') { return typeof b === 'string' ? compareStrings(a, b) : -1; } + if (typeof b === 'string') { return typeof a === 'string' ? compareStrings(a, b) : 1; } + + // Booleans + if (typeof a === 'boolean') { return typeof b === 'boolean' ? compareNSB(a, b) : -1; } + if (typeof b === 'boolean') { return typeof a === 'boolean' ? compareNSB(a, b) : 1; } + + // Dates + if (a instanceof Date) { return b instanceof Date ? compareNSB(a.getTime(), b.getTime()) : -1; } + if (b instanceof Date) { return a instanceof Date ? compareNSB(a.getTime(), b.getTime()) : 1; } + + // Arrays (first element is most significant and so on) + if (Array.isArray(a)) { return Array.isArray(b) ? compareArrays(a, b) : -1; } + if (Array.isArray(b)) { return Array.isArray(a) ? compareArrays(a, b) : 1; } + + // Objects + const aKeys = Object.keys(a).sort(); + const bKeys = Object.keys(b).sort(); + let comp = 0; + + for (let i = 0; i < Math.min(aKeys.length, bKeys.length); i += 1) { + comp = compare(a[aKeys[i]], b[bKeys[i]]); + + if (comp !== 0) { return comp; } + } + + return compareNSB(aKeys.length, bKeys.length); +}; + +// An in-memory sorting function according to the +// $sort special query parameter +exports.sorter = function ($sort) { + const criteria = Object.keys($sort).map(key => { + const direction = $sort[key]; + + return { key, direction }; + }); + + return function (a, b) { + let compare; + + for (let i = 0; i < criteria.length; i++) { + const criterion = criteria[i]; + + compare = criterion.direction * exports.compare(a[criterion.key], b[criterion.key]); + + if (compare !== 0) { + return compare; + } + } + + return 0; + }; +}; diff --git a/packages/adapter-commons/lib/tests/basic.js b/packages/adapter-commons/lib/tests/basic.js new file mode 100644 index 0000000000..e5e8bc03da --- /dev/null +++ b/packages/adapter-commons/lib/tests/basic.js @@ -0,0 +1,53 @@ +const assert = require('assert'); + +module.exports = (test, app, errors, serviceName, idProp) => { + describe('Basic Functionality', () => { + let service; + + beforeEach(() => { + service = app.service(serviceName); + }); + + it('.id', () => { + assert.strictEqual(service.id, idProp, + 'id property is set to expected name' + ); + }); + + test('.options', () => { + assert.ok(service.options, 'Options are available in service.options'); + }); + + test('.events', () => { + assert.ok(service.events.includes('testing'), + 'service.events is set and includes "testing"' + ); + }); + + describe('Raw Methods', () => { + test('._get', () => { + assert.strictEqual(typeof service._get, 'function'); + }); + + test('._find', () => { + assert.strictEqual(typeof service._find, 'function'); + }); + + test('._create', () => { + assert.strictEqual(typeof service._create, 'function'); + }); + + test('._update', () => { + assert.strictEqual(typeof service._update, 'function'); + }); + + test('._patch', () => { + assert.strictEqual(typeof service._patch, 'function'); + }); + + test('._remove', () => { + assert.strictEqual(typeof service._remove, 'function'); + }); + }); + }); +}; diff --git a/packages/adapter-commons/lib/tests/index.js b/packages/adapter-commons/lib/tests/index.js new file mode 100644 index 0000000000..b578ae6102 --- /dev/null +++ b/packages/adapter-commons/lib/tests/index.js @@ -0,0 +1,46 @@ +const basicTests = require('./basic'); +const methodTests = require('./methods'); +const syntaxTests = require('./syntax'); + +module.exports = testNames => { + return (app, errors, serviceName, idProp = 'id') => { + if (!serviceName) { + throw new Error('You must pass a service name'); + } + + const skippedTests = []; + const allTests = []; + + const test = (name, runner) => { + const skip = !testNames.includes(name); + const its = skip ? it.skip : it; + + if (skip) { + skippedTests.push(name); + } + + allTests.push(name); + + its(name, runner); + }; + + describe(`Adapter tests for '${serviceName}' service with '${idProp}' id property`, () => { + after(() => { + console.log('\n'); + testNames.forEach(name => { + if (!allTests.includes(name)) { + console.error(`WARNING: '${name}' test is not part of the test suite`); + } + }); + if (skippedTests.length) { + console.log(`\nSkipped the following ${skippedTests.length} Feathers adapter test(s) out of ${allTests.length} total:`); + console.log(JSON.stringify(skippedTests, null, ' ')); + } + }); + + basicTests(test, app, errors, serviceName, idProp); + methodTests(test, app, errors, serviceName, idProp); + syntaxTests(test, app, errors, serviceName, idProp); + }); + }; +}; diff --git a/packages/adapter-commons/lib/tests/methods.js b/packages/adapter-commons/lib/tests/methods.js new file mode 100644 index 0000000000..33641e4e8d --- /dev/null +++ b/packages/adapter-commons/lib/tests/methods.js @@ -0,0 +1,446 @@ +const assert = require('assert'); + +module.exports = (test, app, errors, serviceName, idProp) => { + describe(' Methods', () => { + let doug, service; + + beforeEach(async () => { + service = app.service(serviceName); + doug = await app.service(serviceName).create({ + name: 'Doug', + age: 32 + }); + }); + + afterEach(async () => { + try { + await app.service(serviceName).remove(doug[idProp]); + } catch (error) {} + }); + + describe('get', () => { + test('.get', async () => { + const data = await service.get(doug[idProp]); + + assert.strictEqual(data[idProp].toString(), doug[idProp].toString(), + `${idProp} id matches` + ); + assert.strictEqual(data.name, 'Doug', 'data.name matches'); + assert.strictEqual(data.age, 32, 'data.age matches'); + }); + + test('.get + $select', async () => { + const data = await service.get(doug[idProp], { + query: { $select: [ 'name' ] } + }); + + assert.strictEqual(data[idProp].toString(), doug[idProp].toString(), + `${idProp} id property matches` + ); + assert.strictEqual(data.name, 'Doug', 'data.name matches'); + assert.strictEqual(data.age, undefined, 'data.age is undefined'); + }); + + test('.get + id + query', async () => { + try { + await service.get(doug[idProp], { + query: { name: 'Tester' } + }); + throw new Error('Should never get here'); + } catch (error) { + assert.ok(error instanceof errors.NotFound, + 'Got a NotFound Feathers error' + ); + } + }); + + test('.get + NotFound', async () => { + try { + await service.get('568225fbfe21222432e836ff'); + throw new Error('Should never get here'); + } catch (error) { + assert.ok(error instanceof errors.NotFound, + 'Error is a NotFound Feathers error' + ); + } + }); + }); + + describe('find', () => { + test('.find', async () => { + const data = await service.find(); + + assert.ok(Array.isArray(data), 'Data is an array'); + assert.strictEqual(data.length, 1, 'Got one entry'); + }); + }); + + describe('remove', () => { + test('.remove', async () => { + const data = await service.remove(doug[idProp]); + + assert.strictEqual(data.name, 'Doug', 'data.name matches'); + }); + + test('.remove + $select', async () => { + const data = await service.remove(doug[idProp], { + query: { $select: [ 'name' ] } + }); + + assert.strictEqual(data.name, 'Doug', 'data.name matches'); + assert.strictEqual(data.age, undefined, 'data.age is undefined'); + }); + + test('.remove + id + query', async () => { + try { + await service.remove(doug[idProp], { + query: { name: 'Tester' } + }); + throw new Error('Should never get here'); + } catch (error) { + assert.ok(error instanceof errors.NotFound, + 'Got a NotFound Feathers error' + ); + } + }); + + test('.remove + multi', async () => { + try { + await service.remove(null); + throw new Error('Should never get here'); + } catch (error) { + assert.strictEqual(error.name, 'MethodNotAllowed', + 'Removing multiple without option set throws MethodNotAllowed' + ); + } + + service.options.multi = [ 'remove' ]; + + await service.create({ name: 'Dave', age: 29, created: true }); + await service.create({ + name: 'David', + age: 3, + created: true + }); + + const data = await service.remove(null, { + query: { created: true } + }); + + assert.strictEqual(data.length, 2); + + let names = data.map(person => person.name); + + assert.ok(names.includes('Dave'), 'Dave removed'); + assert.ok(names.includes('David'), 'David removed'); + }); + }); + + describe('update', () => { + test('.update', async () => { + const originalData = { [idProp]: doug[idProp], name: 'Dougler' }; + const originalCopy = Object.assign({}, originalData); + + const data = await service.update(doug[idProp], originalData); + + assert.deepStrictEqual(originalData, originalCopy, + 'data was not modified' + ); + assert.strictEqual(data[idProp].toString(), doug[idProp].toString(), + `${idProp} id matches` + ); + assert.strictEqual(data.name, 'Dougler', 'data.name matches'); + assert.strictEqual(data.age, undefined, 'data.age is undefined'); + }); + + test('.update + $select', async () => { + const originalData = { + [idProp]: doug[idProp], + name: 'Dougler', + age: 10 + }; + + const data = await service.update(doug[idProp], originalData, { + query: { $select: [ 'name' ] } + }); + + assert.strictEqual(data.name, 'Dougler', 'data.name matches'); + assert.strictEqual(data.age, undefined, 'data.age is undefined'); + }); + + test('.update + id + query', async () => { + try { + await service.update(doug[idProp], {}, { + query: { name: 'Tester' } + }); + throw new Error('Should never get here'); + } catch (error) { + assert.ok(error instanceof errors.NotFound, + 'Got a NotFound Feathers error' + ); + } + }); + + test('.update + NotFound', async () => { + try { + await service.update('568225fbfe21222432e836ff', { name: 'NotFound' }); + throw new Error('Should never get here'); + } catch (error) { + assert.ok(error instanceof errors.NotFound, 'Error is a NotFound Feathers error'); + } + }); + }); + + describe('patch', () => { + test('.patch', async () => { + const originalData = { [idProp]: doug[idProp], name: 'PatchDoug' }; + const originalCopy = Object.assign({}, originalData); + + const data = await service.patch(doug[idProp], originalData); + + assert.deepStrictEqual(originalData, originalCopy, + 'original data was not modified' + ); + assert.strictEqual(data[idProp].toString(), doug[idProp].toString(), + `${idProp} id matches` + ); + assert.strictEqual(data.name, 'PatchDoug', 'data.name matches'); + assert.strictEqual(data.age, 32, 'data.age matches'); + }); + + test('.patch + $select', async () => { + const originalData = { [idProp]: doug[idProp], name: 'PatchDoug' }; + + const data = await service.patch(doug[idProp], originalData, { + query: { $select: [ 'name' ] } + }); + + assert.strictEqual(data.name, 'PatchDoug', 'data.name matches'); + assert.strictEqual(data.age, undefined, 'data.age is undefined'); + }); + + test('.patch + id + query', async () => { + try { + await service.patch(doug[idProp], {}, { + query: { name: 'Tester' } + }); + throw new Error('Should never get here'); + } catch (error) { + assert.ok(error instanceof errors.NotFound, + 'Got a NotFound Feathers error' + ); + } + }); + + test('.patch multiple', async () => { + try { + await service.patch(null, {}); + throw new Error('Should never get here'); + } catch (error) { + assert.strictEqual(error.name, 'MethodNotAllowed', + 'Removing multiple without option set throws MethodNotAllowed' + ); + } + + const params = { + query: { created: true } + }; + const dave = await service.create({ + name: 'Dave', + age: 29, + created: true + }); + const david = await service.create({ + name: 'David', + age: 3, + created: true + }); + + service.options.multi = [ 'patch' ]; + + const data = await service.patch(null, { + age: 2 + }, params); + + assert.strictEqual(data.length, 2, 'returned two entries'); + assert.strictEqual(data[0].age, 2, 'First entry age was updated'); + assert.strictEqual(data[1].age, 2, 'Sceond entry age was updated'); + + await service.remove(dave[idProp], params); + await service.remove(david[idProp], params); + }); + + test('.patch multi query', async () => { + const service = app.service(serviceName); + const params = { + query: { age: { $lt: 10 } } + }; + const dave = await service.create({ + name: 'Dave', + age: 8, + created: true + }); + const david = await service.create({ + name: 'David', + age: 4, + created: true + }); + + const data = await service.patch(null, { + age: 2 + }, params); + + assert.strictEqual(data.length, 2, 'returned two entries'); + assert.strictEqual(data[0].age, 2, 'First entry age was updated'); + assert.strictEqual(data[1].age, 2, 'Sceond entry age was updated'); + + await service.remove(dave[idProp], params); + await service.remove(david[idProp], params); + }); + + test('.patch + NotFound', async () => { + try { + await service.patch('568225fbfe21222432e836ff', { name: 'PatchDoug' }); + throw new Error('Should never get here'); + } catch (error) { + assert.ok(error instanceof errors.NotFound, + 'Error is a NotFound Feathers error' + ); + } + }); + }); + + describe('create', () => { + test('.create', async () => { + const originalData = { + name: 'Bill', + age: 40 + }; + const originalCopy = Object.assign({}, originalData); + + const data = await service.create(originalData); + + assert.deepStrictEqual(originalData, originalCopy, + 'original data was not modified' + ); + assert.ok(data instanceof Object, 'data is an object'); + assert.strictEqual(data.name, 'Bill', 'data.name matches'); + + await service.remove(data[idProp]); + }); + + test('.create + $select', async () => { + const originalData = { + name: 'William', + age: 23 + }; + + const data = await service.create(originalData, { + query: { $select: [ 'name' ] } + }); + + assert.strictEqual(data.name, 'William', 'data.name matches'); + assert.strictEqual(data.age, undefined, 'data.age is undefined'); + + await service.remove(data[idProp]); + }); + + test('.create multi', async () => { + try { + await service.create([], {}); + throw new Error('Should never get here'); + } catch (error) { + assert.strictEqual(error.name, 'MethodNotAllowed', + 'Removing multiple without option set throws MethodNotAllowed' + ); + } + + const items = [ + { + name: 'Gerald', + age: 18 + }, + { + name: 'Herald', + age: 18 + } + ]; + + service.options.multi = [ 'create', 'patch' ]; + + const data = await service.create(items); + + assert.ok(Array.isArray(data), 'data is an array'); + assert.ok(typeof data[0][idProp] !== 'undefined', 'id is set'); + assert.strictEqual(data[0].name, 'Gerald', 'first name matches'); + assert.ok(typeof data[1][idProp] !== 'undefined', 'id is set'); + assert.strictEqual(data[1].name, 'Herald', 'second name macthes'); + + await service.remove(data[0][idProp]); + await service.remove(data[1][idProp]); + }); + }); + + describe('doesn\'t call public methods internally', () => { + let throwing; + + before(() => { + throwing = app.service(serviceName).extend({ + get store () { + return app.service(serviceName).store; + }, + + find () { + throw new Error('find method called'); + }, + get () { + throw new Error('get method called'); + }, + create () { + throw new Error('create method called'); + }, + update () { + throw new Error('update method called'); + }, + patch () { + throw new Error('patch method called'); + }, + remove () { + throw new Error('remove method called'); + } + }); + }); + + test('internal .find', () => app.service(serviceName).find.call(throwing)); + + test('internal .get', () => + service.get.call(throwing, doug[idProp]) + ); + + test('internal .create', async () => { + const bob = await service.create.call(throwing, { + name: 'Bob', + age: 25 + }); + + await service.remove(bob[idProp]); + }); + + test('internal .update', () => + service.update.call(throwing, doug[idProp], { + name: 'Dougler' + }) + ); + + test('internal .patch', () => + service.patch.call(throwing, doug[idProp], { + name: 'PatchDoug' + }) + ); + + test('internal .remove', () => + service.remove.call(throwing, doug[idProp]) + ); + }); + }); +}; diff --git a/packages/adapter-commons/lib/tests/syntax.js b/packages/adapter-commons/lib/tests/syntax.js new file mode 100644 index 0000000000..bd3b1f8284 --- /dev/null +++ b/packages/adapter-commons/lib/tests/syntax.js @@ -0,0 +1,339 @@ +const assert = require('assert'); + +module.exports = (test, app, errors, serviceName, idProp) => { + describe('Query Syntax', () => { + let bob, alice, doug, service; + + beforeEach(async () => { + service = app.service(serviceName); + bob = await app.service(serviceName).create({ + name: 'Bob', + age: 25 + }); + doug = await app.service(serviceName).create({ + name: 'Doug', + age: 32 + }); + alice = await app.service(serviceName).create({ + name: 'Alice', + age: 19 + }); + }); + + afterEach(async () => { + await service.remove(bob[idProp]); + await service.remove(alice[idProp]); + await service.remove(doug[idProp]); + }); + + test('.find + equal', async () => { + const params = { query: { name: 'Alice' } }; + const data = await service.find(params); + + assert.ok(Array.isArray(data)); + assert.strictEqual(data.length, 1); + assert.strictEqual(data[0].name, 'Alice'); + }); + + test('.find + equal multiple', async () => { + const data = await service.find({ + query: { name: 'Alice', age: 20 } + }); + + assert.strictEqual(data.length, 0); + }); + + describe('special filters', () => { + test('.find + $sort', async () => { + let data = await service.find({ + query: { + $sort: { name: 1 } + } + }); + + assert.strictEqual(data.length, 3); + assert.strictEqual(data[0].name, 'Alice'); + assert.strictEqual(data[1].name, 'Bob'); + assert.strictEqual(data[2].name, 'Doug'); + + data = await service.find({ + query: { + $sort: { name: -1 } + } + }); + + assert.strictEqual(data.length, 3); + assert.strictEqual(data[0].name, 'Doug'); + assert.strictEqual(data[1].name, 'Bob'); + assert.strictEqual(data[2].name, 'Alice'); + }); + + test('.find + $sort + string', async () => { + const data = await service.find({ + query: { + $sort: { name: '1' } + } + }); + + assert.strictEqual(data.length, 3); + assert.strictEqual(data[0].name, 'Alice'); + assert.strictEqual(data[1].name, 'Bob'); + assert.strictEqual(data[2].name, 'Doug'); + }); + + test('.find + $limit', async () => { + const data = await service.find({ + query: { + $limit: 2 + } + }); + + assert.strictEqual(data.length, 2); + }); + + test('.find + $limit 0', async () => { + const data = await service.find({ + query: { + $limit: 0 + } + }); + + assert.strictEqual(data.length, 0); + }); + + test('.find + $skip', async () => { + const data = await service.find({ + query: { + $sort: { name: 1 }, + $skip: 1 + } + }); + + assert.strictEqual(data.length, 2); + assert.strictEqual(data[0].name, 'Bob'); + assert.strictEqual(data[1].name, 'Doug'); + }); + + test('.find + $select', async () => { + const data = await service.find({ + query: { + name: 'Alice', + $select: ['name'] + } + }); + + assert.strictEqual(data.length, 1); + assert.strictEqual(data[0].name, 'Alice'); + assert.strictEqual(data[0].age, undefined); + }); + + test('.find + $or', async () => { + const data = await service.find({ + query: { + $or: [ + { name: 'Alice' }, + { name: 'Bob' } + ], + $sort: { name: 1 } + } + }); + + assert.strictEqual(data.length, 2); + assert.strictEqual(data[0].name, 'Alice'); + assert.strictEqual(data[1].name, 'Bob'); + }); + + test('.find + $in', async () => { + const data = await service.find({ + query: { + name: { + $in: ['Alice', 'Bob'] + }, + $sort: { name: 1 } + } + }); + + assert.strictEqual(data.length, 2); + assert.strictEqual(data[0].name, 'Alice'); + assert.strictEqual(data[1].name, 'Bob'); + }); + + test('.find + $nin', async () => { + const data = await service.find({ + query: { + name: { + $nin: [ 'Alice', 'Bob' ] + } + } + }); + + assert.strictEqual(data.length, 1); + assert.strictEqual(data[0].name, 'Doug'); + }); + + test('.find + $lt', async () => { + const data = await service.find({ + query: { + age: { + $lt: 30 + } + } + }); + + assert.strictEqual(data.length, 2); + }); + + test('.find + $lte', async () => { + const data = await service.find({ + query: { + age: { + $lte: 25 + } + } + }); + + assert.strictEqual(data.length, 2); + }); + + test('.find + $gt', async () => { + const data = await service.find({ + query: { + age: { + $gt: 30 + } + } + }); + + assert.strictEqual(data.length, 1); + }); + + test('.find + $gte', async () => { + const data = await service.find({ + query: { + age: { + $gte: 25 + } + } + }); + + assert.strictEqual(data.length, 2); + }); + + test('.find + $ne', async () => { + const data = await service.find({ + query: { + age: { + $ne: 25 + } + } + }); + + assert.strictEqual(data.length, 2); + }); + }); + + test('.find + $gt + $lt + $sort', async () => { + const params = { + query: { + age: { + $gt: 18, + $lt: 30 + }, + $sort: { name: 1 } + } + }; + + const data = await service.find(params); + + assert.strictEqual(data.length, 2); + assert.strictEqual(data[0].name, 'Alice'); + assert.strictEqual(data[1].name, 'Bob'); + }); + + test('.find + $or nested + $sort', async () => { + const params = { + query: { + $or: [ + { name: 'Doug' }, + { + age: { + $gte: 18, + $lt: 25 + } + } + ], + $sort: { name: 1 } + } + }; + + const data = await service.find(params); + + assert.strictEqual(data.length, 2); + assert.strictEqual(data[0].name, 'Alice'); + assert.strictEqual(data[1].name, 'Doug'); + }); + + describe('paginate', function () { + beforeEach(() => { + service.options.paginate = { + default: 1, + max: 2 + }; + }); + + afterEach(() => { + service.options.paginate = {}; + }); + + test('.find + paginate', async () => { + const page = await service.find({ + query: { $sort: { name: -1 } } + }); + + assert.strictEqual(page.total, 3); + assert.strictEqual(page.limit, 1); + assert.strictEqual(page.skip, 0); + assert.strictEqual(page.data[0].name, 'Doug'); + }); + + test('.find + paginate + $limit + $skip', async () => { + const params = { + query: { + $skip: 1, + $limit: 4, + $sort: { name: -1 } + } + }; + + const page = await service.find(params); + + assert.strictEqual(page.total, 3); + assert.strictEqual(page.limit, 2); + assert.strictEqual(page.skip, 1); + assert.strictEqual(page.data[0].name, 'Bob'); + assert.strictEqual(page.data[1].name, 'Alice'); + }); + + test('.find + paginate + $limit 0', async () => { + const page = await service.find({ + query: { $limit: 0 } + }); + + assert.strictEqual(page.total, 3); + assert.strictEqual(page.data.length, 0); + }); + + test('.find + paginate + params', async () => { + const page = await service.find({ paginate: { default: 3 } }); + + assert.strictEqual(page.limit, 3); + assert.strictEqual(page.skip, 0); + + const results = await service.find({ paginate: false }); + + assert.ok(Array.isArray(results)); + assert.strictEqual(results.length, 3); + }); + }); + }); +}; diff --git a/packages/adapter-commons/package-lock.json b/packages/adapter-commons/package-lock.json new file mode 100644 index 0000000000..f86cfbe9b7 --- /dev/null +++ b/packages/adapter-commons/package-lock.json @@ -0,0 +1,93 @@ +{ + "name": "@feathersjs/adapter-commons", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "mongodb": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.10.tgz", + "integrity": "sha512-Uml42GeFxhTGQVml1XQ4cD0o/rp7J2ROy0fdYUcVitoE7vFqEhKH4TYVqRDpQr/bXtCJVxJdNQC1ntRxNREkPQ==", + "dev": true, + "requires": { + "mongodb-core": "3.1.9", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bson": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.0.tgz", + "integrity": "sha512-9Aeai9TacfNtWXOYarkFJRW2CWo+dRon+fuLZYJmvLV3+MiUp0bEI6IAZfXEIg7/Pl/7IWlLaDnhzTsD81etQA==", + "dev": true + }, + "memory-pager": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.3.1.tgz", + "integrity": "sha512-pUf/sGkym2WqFZYTVmdASnSbNfpGc9rwxEHOePx4lT/fD+NHGL1U16Uy4o6PMiVcDv4mp6MI/vaF0c/Kd1QEUQ==", + "dev": true, + "optional": true + }, + "mongodb-core": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.9.tgz", + "integrity": "sha512-MJpciDABXMchrZphh3vMcqu8hkNf/Mi+Gk6btOimVg1XMxLXh87j6FAvRm+KmwD1A9fpu3qRQYcbQe4egj23og==", + "dev": true, + "requires": { + "bson": "^1.1.0", + "require_optional": "^1.0.1", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + } + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "dev": true, + "requires": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + } + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "saslprep": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.2.tgz", + "integrity": "sha512-4cDsYuAjXssUSjxHKRe4DTZC0agDwsCqcMqtJAQPzC74nJ7LfAJflAtC1Zed5hMzEQKj82d3tuzqdGNRsLJ4Gw==", + "dev": true, + "optional": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "dev": true, + "optional": true, + "requires": { + "memory-pager": "^1.0.2" + } + } + } + } + } +} diff --git a/packages/adapter-commons/package.json b/packages/adapter-commons/package.json new file mode 100644 index 0000000000..3fda7eb43b --- /dev/null +++ b/packages/adapter-commons/package.json @@ -0,0 +1,44 @@ +{ + "name": "@feathersjs/adapter-commons", + "version": "0.1.0", + "description": "Shared database adapter utility functions", + "homepage": "https://feathersjs.com", + "keywords": [ + "feathers" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git://github.com/feathersjs/feathers.git" + }, + "author": { + "name": "Feathers contributor", + "email": "hello@feathersjs.com", + "url": "https://feathersjs.com" + }, + "contributors": [], + "bugs": { + "url": "https://github.com/feathersjs/feathers/issues" + }, + "engines": { + "node": ">= 6" + }, + "main": "lib/index.js", + "scripts": { + "test": "mocha --opts ../../mocha.opts" + }, + "directories": { + "lib": "lib" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "mocha": "^5.2.0", + "mongodb": "^3.1.10" + }, + "dependencies": { + "@feathersjs/commons": "^3.0.1", + "@feathersjs/errors": "^3.3.4" + } +} diff --git a/packages/adapter-commons/test/commons.test.js b/packages/adapter-commons/test/commons.test.js new file mode 100644 index 0000000000..fb818af934 --- /dev/null +++ b/packages/adapter-commons/test/commons.test.js @@ -0,0 +1,72 @@ +const assert = require('assert'); +const { select } = require('../lib'); + +describe('@feathersjs/adapter-commons', () => { + describe('select', () => { + it('select', () => { + const selector = select({ + query: { $select: ['name', 'age'] } + }); + + return Promise.resolve({ + name: 'David', + age: 3, + test: 'me' + }).then(selector).then(result => assert.deepStrictEqual(result, { + name: 'David', + age: 3 + })); + }); + + it('select with arrays', () => { + const selector = select({ + query: { $select: ['name', 'age'] } + }); + + return Promise.resolve([{ + name: 'David', + age: 3, + test: 'me' + }, { + name: 'D', + age: 4, + test: 'you' + }]).then(selector).then(result => assert.deepStrictEqual(result, [{ + name: 'David', + age: 3 + }, { + name: 'D', + age: 4 + }])); + }); + + it('select with no query', () => { + const selector = select({}); + const data = { + name: 'David' + }; + + return Promise.resolve(data).then(selector).then(result => + assert.deepStrictEqual(result, data) + ); + }); + + it('select with other fields', () => { + const selector = select({ + query: { $select: [ 'name' ] } + }, 'id'); + const data = { + id: 'me', + name: 'David', + age: 10 + }; + + return Promise.resolve(data) + .then(selector) + .then(result => assert.deepStrictEqual(result, { + id: 'me', + name: 'David' + })); + }); + }); +}); diff --git a/packages/adapter-commons/test/filter-query.test.js b/packages/adapter-commons/test/filter-query.test.js new file mode 100644 index 0000000000..69c852a1e6 --- /dev/null +++ b/packages/adapter-commons/test/filter-query.test.js @@ -0,0 +1,222 @@ +const assert = require('assert'); +const { ObjectId } = require('mongodb'); +const { filterQuery } = require('../lib'); + +describe('@feathersjs/adapter-commons/filterQuery', () => { + describe('$sort', () => { + it('returns $sort when present in query', () => { + const originalQuery = { $sort: { name: 1 } }; + const { filters, query } = filterQuery(originalQuery); + + assert.strictEqual(filters.$sort.name, 1); + assert.deepStrictEqual(query, {}); + assert.deepStrictEqual(originalQuery, { + $sort: { name: 1 } + }, 'does not modify original query'); + }); + + it('returns $sort when present in query as an object', () => { + const { filters, query } = filterQuery({ $sort: { name: { something: 10 } } }); + + assert.strictEqual(filters.$sort.name.something, 10); + assert.deepStrictEqual(query, {}); + }); + + it('converts strings in $sort', () => { + const { filters, query } = filterQuery({ $sort: { test: '-1' } }); + + assert.strictEqual(filters.$sort.test, -1); + assert.deepStrictEqual(query, {}); + }); + + it('does not convert $sort arrays', () => { + const $sort = [ [ 'test', '-1' ], [ 'a', '1' ] ]; + const { filters, query } = filterQuery({ $sort }); + + assert.strictEqual(filters.$sort, $sort); + assert.deepStrictEqual(query, {}); + }); + + it('returns undefined when not present in query', () => { + const query = { $foo: 1 }; + const { filters } = filterQuery(query); + + assert.strictEqual(filters.$sort, undefined); + }); + }); + + describe('$limit', () => { + beforeEach(() => { + this.query = { $limit: 1 }; + }); + + it('returns $limit when present in query', () => { + const { filters, query } = filterQuery(this.query); + + assert.strictEqual(filters.$limit, 1); + assert.deepStrictEqual(query, {}); + }); + + it('returns undefined when not present in query', () => { + const query = { $foo: 1 }; + const { filters } = filterQuery(query); + + assert.strictEqual(filters.$limit, undefined); + }); + + it('removes $limit from query when present', () => { + assert.deepStrictEqual(filterQuery(this.query).query, {}); + }); + + it('parses $limit strings into integers (#4)', () => { + const { filters } = filterQuery({ $limit: '2' }); + + assert.strictEqual(filters.$limit, 2); + }); + + it('allows $limit 0', () => { + const { filters } = filterQuery({ $limit: 0 }, { default: 10 }); + + assert.strictEqual(filters.$limit, 0); + }); + + describe('pagination', () => { + it('limits with default pagination', () => { + const { filters } = filterQuery({}, { paginate: { default: 10 } }); + + assert.strictEqual(filters.$limit, 10); + }); + + it('limits with max pagination', () => { + const { filters } = filterQuery({ $limit: 20 }, { paginate: { default: 5, max: 10 } }); + const { filters: filtersNeg } = filterQuery({ $limit: -20 }, { paginate: { default: 5, max: 10 } }); + + assert.strictEqual(filters.$limit, 10); + assert.strictEqual(filtersNeg.$limit, 10); + }); + }); + }); + + describe('$skip', () => { + beforeEach(() => { + this.query = { $skip: 1 }; + }); + + it('returns $skip when present in query', () => { + const { filters } = filterQuery(this.query); + + assert.strictEqual(filters.$skip, 1); + }); + + it('removes $skip from query when present', () => { + assert.deepStrictEqual(filterQuery(this.query).query, {}); + }); + + it('returns undefined when not present in query', () => { + const query = { $foo: 1 }; + const { filters } = filterQuery(query); + + assert.strictEqual(filters.$skip, undefined); + }); + + it('parses $skip strings into integers (#4)', () => { + const { filters } = filterQuery({ $skip: '33' }); + + assert.strictEqual(filters.$skip, 33); + }); + }); + + describe('$select', () => { + beforeEach(() => { + this.query = { $select: 1 }; + }); + + it('returns $select when present in query', () => { + const { filters } = filterQuery(this.query); + + assert.strictEqual(filters.$select, 1); + }); + + it('removes $select from query when present', () => { + assert.deepStrictEqual(filterQuery(this.query).query, {}); + }); + + it('returns undefined when not present in query', () => { + const query = { $foo: 1 }; + const { filters } = filterQuery(query); + + assert.strictEqual(filters.$select, undefined); + }); + + it('only converts plain objects', () => { + const userId = ObjectId(); + const original = { + userId + }; + + const { query } = filterQuery(original); + + assert.deepStrictEqual(query, original); + }); + }); + + describe('additional filters', () => { + beforeEach(() => { + this.query = { $select: 1, $known: 1, $unknown: 1 }; + }); + + it('returns only default filters when no additionals', () => { + const { filters } = filterQuery(this.query); + + assert.strictEqual(filters.$unknown, undefined); + assert.strictEqual(filters.$known, undefined); + assert.strictEqual(filters.$select, 1); + }); + + it('returns default and known additional filters (array)', () => { + const { filters } = filterQuery(this.query, { filters: [ '$known' ] }); + + assert.strictEqual(filters.$unknown, undefined); + assert.strictEqual(filters.$known, 1); + assert.strictEqual(filters.$select, 1); + }); + + it('returns default and known additional filters (object)', () => { + const { filters } = filterQuery(this.query, { filters: { $known: (value) => value.toString() } }); + + assert.strictEqual(filters.$unknown, undefined); + assert.strictEqual(filters.$known, '1'); + assert.strictEqual(filters.$select, 1); + }); + }); + + describe('additional operators', () => { + beforeEach(() => { + this.query = { $ne: 1, $known: 1, $unknown: 1 }; + }); + + it('returns query with only default operators when no additionals', () => { + const { query } = filterQuery(this.query); + + assert.strictEqual(query.$ne, 1); + assert.strictEqual(query.$known, undefined); + assert.strictEqual(query.$unknown, undefined); + }); + + it('returns query with default and known additional operators', () => { + const { query } = filterQuery(this.query, { operators: [ '$known' ] }); + + assert.strictEqual(query.$ne, 1); + assert.strictEqual(query.$known, 1); + assert.strictEqual(query.$unknown, undefined); + }); + + it('returns query with default and known additional operators (nested)', () => { + const { query } = filterQuery({ field: this.query }, { operators: [ '$known' ] }); + + assert.strictEqual(query.field.$ne, 1); + assert.strictEqual(query.field.$known, 1); + assert.strictEqual(query.field.$unknown, undefined); + }); + }); +}); diff --git a/packages/adapter-commons/test/service.test.js b/packages/adapter-commons/test/service.test.js new file mode 100644 index 0000000000..9fd37400db --- /dev/null +++ b/packages/adapter-commons/test/service.test.js @@ -0,0 +1,115 @@ +const assert = require('assert'); +const { NotImplemented } = require('@feathersjs/errors'); +const { AdapterService } = require('../lib'); +const METHODS = [ 'find', 'get', 'create', 'update', 'patch', 'remove' ]; + +describe('@feathersjs/adapter-commons/service', () => { + class CustomService extends AdapterService { + } + + describe('errors when method does not exit', () => { + METHODS.forEach(method => { + it(`${method}`, () => { + const service = new CustomService(); + + return service[method]().then(() => { + throw new Error('Should never get here'); + }).catch(error => { + assert.ok(error instanceof NotImplemented); + assert.strictEqual(error.message, `Method _${method} not available`); + }); + }); + }); + }); + + describe('works when methods exist', () => { + class MethodService extends AdapterService { + _find () { + return Promise.resolve([]); + } + + _get (id) { + return Promise.resolve({ id }); + } + + _create (data) { + return Promise.resolve(data); + } + + _update (id) { + return Promise.resolve({ id }); + } + + _patch (id) { + return Promise.resolve({ id }); + } + + _remove (id) { + return Promise.resolve({ id }); + } + } + + METHODS.forEach(method => { + it(`${method}`, () => { + const service = new MethodService(); + const args = []; + + if (method !== 'find') { + args.push('test'); + } + + if (method === 'update' || method === 'patch') { + args.push({}); + } + + return service[method](...args); + }); + }); + + it('does not allow multi patch', () => { + const service = new MethodService(); + + return service.patch(null, {}) + .then(() => assert.ok(false)) + .catch(error => { + assert.strictEqual(error.name, 'MethodNotAllowed'); + assert.strictEqual(error.message, 'Can not patch multiple entries'); + }); + }); + + it('does not allow multi remove', () => { + const service = new MethodService(); + + return service.remove(null, {}) + .then(() => assert.ok(false)) + .catch(error => { + assert.strictEqual(error.name, 'MethodNotAllowed'); + assert.strictEqual(error.message, 'Can not remove multiple entries'); + }); + }); + + it('does not allow multi create', () => { + const service = new MethodService(); + + return service.create([]) + .then(() => assert.ok(false)) + .catch(error => { + assert.strictEqual(error.name, 'MethodNotAllowed'); + assert.strictEqual(error.message, 'Can not create multiple entries'); + }); + }); + }); + + it('getFilters', () => { + const service = new CustomService(); + const filtered = service.filterQuery({ + query: { $limit: 10, test: 'me' } + }); + + assert.deepStrictEqual(filtered, { + paginate: {}, + filters: { $limit: 10 }, + query: { test: 'me' } + }); + }); +}); diff --git a/packages/adapter-commons/test/sort.test.js b/packages/adapter-commons/test/sort.test.js new file mode 100644 index 0000000000..8f8221dbee --- /dev/null +++ b/packages/adapter-commons/test/sort.test.js @@ -0,0 +1,180 @@ +/* eslint-disable no-unused-expressions */ +const assert = require('assert'); +const { sorter } = require('../lib'); + +describe('@feathersjs/adapter-commons', () => { + describe('sorter', () => { + it('simple sorter', () => { + const array = [{ + name: 'David' + }, { + name: 'Eric' + }]; + + const sort = sorter({ + name: -1 + }); + + assert.deepStrictEqual(array.sort(sort), [{ + name: 'Eric' + }, { + name: 'David' + }]); + }); + + it('simple sorter with arrays', () => { + const array = [{ + names: [ 'a', 'b' ] + }, { + names: [ 'c', 'd' ] + }]; + + const sort = sorter({ + names: -1 + }); + + assert.deepStrictEqual(array.sort(sort), [{ + names: [ 'c', 'd' ] + }, { + names: [ 'a', 'b' ] + }]); + }); + + it('simple sorter with objects', () => { + const array = [{ + names: { + first: 'Dave', + last: 'L' + } + }, { + names: { + first: 'A', + last: 'B' + } + }]; + + const sort = sorter({ + names: 1 + }); + + assert.deepStrictEqual(array.sort(sort), [{ + names: { + first: 'A', + last: 'B' + } + }, { + names: { + first: 'Dave', + last: 'L' + } + }]); + }); + + it('two property sorter', () => { + const array = [{ + name: 'David', + counter: 0 + }, { + name: 'Eric', + counter: 1 + }, { + name: 'David', + counter: 1 + }, { + name: 'Eric', + counter: 0 + }]; + + const sort = sorter({ + name: -1, + counter: 1 + }); + + assert.deepStrictEqual(array.sort(sort), [ + { name: 'Eric', counter: 0 }, + { name: 'Eric', counter: 1 }, + { name: 'David', counter: 0 }, + { name: 'David', counter: 1 } + ]); + }); + + it('two property sorter with names', () => { + const array = [{ + name: 'David', + counter: 0 + }, { + name: 'Eric', + counter: 1 + }, { + name: 'Andrew', + counter: 1 + }, { + name: 'David', + counter: 1 + }, { + name: 'Andrew', + counter: 0 + }, { + name: 'Eric', + counter: 0 + }]; + + const sort = sorter({ + name: -1, + counter: 1 + }); + + assert.deepStrictEqual(array.sort(sort), [ + { name: 'Eric', counter: 0 }, + { name: 'Eric', counter: 1 }, + { name: 'David', counter: 0 }, + { name: 'David', counter: 1 }, + { name: 'Andrew', counter: 0 }, + { name: 'Andrew', counter: 1 } + ]); + }); + + it('three property sorter with names', () => { + const array = [{ + name: 'David', + counter: 0, + age: 2 + }, { + name: 'Eric', + counter: 1, + age: 2 + }, { + name: 'David', + counter: 1, + age: 1 + }, { + name: 'Eric', + counter: 0, + age: 1 + }, { + name: 'Andrew', + counter: 0, + age: 2 + }, { + name: 'Andrew', + counter: 0, + age: 1 + }]; + + const sort = sorter({ + name: -1, + counter: 1, + age: -1 + }); + + assert.deepStrictEqual(array.sort(sort), [ + { name: 'Eric', counter: 0, age: 1 }, + { name: 'Eric', counter: 1, age: 2 }, + { name: 'David', counter: 0, age: 2 }, + { name: 'David', counter: 1, age: 1 }, + { name: 'Andrew', counter: 0, age: 2 }, + { name: 'Andrew', counter: 0, age: 1 } + ]); + }); + }); +}); diff --git a/packages/adapter-commons/tests.js b/packages/adapter-commons/tests.js new file mode 100644 index 0000000000..1a6002e056 --- /dev/null +++ b/packages/adapter-commons/tests.js @@ -0,0 +1 @@ +module.exports = require('./lib/tests'); diff --git a/packages/commons/lib/commons.js b/packages/commons/lib/commons.js deleted file mode 100644 index 604edf8633..0000000000 --- a/packages/commons/lib/commons.js +++ /dev/null @@ -1,5 +0,0 @@ -const utils = require('./utils'); -const hooks = require('./hooks'); -const filterQuery = require('./filter-query'); - -module.exports = Object.assign({}, utils, { hooks, filterQuery }); diff --git a/packages/commons/lib/index.js b/packages/commons/lib/index.js new file mode 100644 index 0000000000..b1e013571c --- /dev/null +++ b/packages/commons/lib/index.js @@ -0,0 +1,4 @@ +const utils = require('./utils'); +const hooks = require('./hooks'); + +module.exports = Object.assign({}, utils, { hooks }); diff --git a/packages/commons/lib/utils.js b/packages/commons/lib/utils.js index c5b7d623d5..d513198de0 100644 --- a/packages/commons/lib/utils.js +++ b/packages/commons/lib/utils.js @@ -88,33 +88,6 @@ const _ = exports._ = { } }; -// Return a function that filters a result object or array -// and picks only the fields passed as `params.query.$select` -// and additional `otherFields` -exports.select = function select (params, ...otherFields) { - const fields = params && params.query && params.query.$select; - - if (Array.isArray(fields) && otherFields.length) { - fields.push(...otherFields); - } - - const convert = result => { - if (!Array.isArray(fields)) { - return result; - } - - return _.pick(result, ...fields); - }; - - return result => { - if (Array.isArray(result)) { - return result.map(convert); - } - - return convert(result); - }; -}; - // Duck-checks if an object looks like a promise exports.isPromise = function isPromise (result) { return _.isObject(result) && @@ -137,96 +110,3 @@ exports.makeUrl = function makeUrl (path, app = {}) { exports.createSymbol = name => { return typeof Symbol !== 'undefined' ? Symbol(name) : name; }; - -// Sorting algorithm taken from NeDB (https://github.com/louischatriot/nedb) -// See https://github.com/louischatriot/nedb/blob/e3f0078499aa1005a59d0c2372e425ab789145c1/lib/model.js#L189 - -exports.compareNSB = function (a, b) { - if (a < b) { return -1; } - if (a > b) { return 1; } - return 0; -}; - -exports.compareArrays = function (a, b) { - var i, comp; - - for (i = 0; i < Math.min(a.length, b.length); i += 1) { - comp = exports.compare(a[i], b[i]); - - if (comp !== 0) { return comp; } - } - - // Common section was identical, longest one wins - return exports.compareNSB(a.length, b.length); -}; - -exports.compare = function (a, b, compareStrings = exports.compareNSB) { - const { compareNSB, compare, compareArrays } = exports; - - // undefined - if (a === undefined) { return b === undefined ? 0 : -1; } - if (b === undefined) { return a === undefined ? 0 : 1; } - - // null - if (a === null) { return b === null ? 0 : -1; } - if (b === null) { return a === null ? 0 : 1; } - - // Numbers - if (typeof a === 'number') { return typeof b === 'number' ? compareNSB(a, b) : -1; } - if (typeof b === 'number') { return typeof a === 'number' ? compareNSB(a, b) : 1; } - - // Strings - if (typeof a === 'string') { return typeof b === 'string' ? compareStrings(a, b) : -1; } - if (typeof b === 'string') { return typeof a === 'string' ? compareStrings(a, b) : 1; } - - // Booleans - if (typeof a === 'boolean') { return typeof b === 'boolean' ? compareNSB(a, b) : -1; } - if (typeof b === 'boolean') { return typeof a === 'boolean' ? compareNSB(a, b) : 1; } - - // Dates - if (a instanceof Date) { return b instanceof Date ? compareNSB(a.getTime(), b.getTime()) : -1; } - if (b instanceof Date) { return a instanceof Date ? compareNSB(a.getTime(), b.getTime()) : 1; } - - // Arrays (first element is most significant and so on) - if (Array.isArray(a)) { return Array.isArray(b) ? compareArrays(a, b) : -1; } - if (Array.isArray(b)) { return Array.isArray(a) ? compareArrays(a, b) : 1; } - - // Objects - const aKeys = Object.keys(a).sort(); - const bKeys = Object.keys(b).sort(); - let comp = 0; - - for (let i = 0; i < Math.min(aKeys.length, bKeys.length); i += 1) { - comp = compare(a[aKeys[i]], b[bKeys[i]]); - - if (comp !== 0) { return comp; } - } - - return compareNSB(aKeys.length, bKeys.length); -}; - -// An in-memory sorting function according to the -// $sort special query parameter -exports.sorter = function ($sort) { - const criteria = Object.keys($sort).map(key => { - const direction = $sort[key]; - - return { key, direction }; - }); - - return function (a, b) { - let compare; - - for (let i = 0; i < criteria.length; i++) { - const criterion = criteria[i]; - - compare = criterion.direction * exports.compare(a[criterion.key], b[criterion.key]); - - if (compare !== 0) { - return compare; - } - } - - return 0; - }; -}; diff --git a/packages/commons/package.json b/packages/commons/package.json index e23a4f9b21..fb8afff5f9 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -23,7 +23,7 @@ "engines": { "node": ">= 6" }, - "main": "lib/commons.js", + "main": "lib/index.js", "scripts": { "test": "mocha --opts ../../mocha.opts" }, diff --git a/packages/commons/test/filter-query.test.js b/packages/commons/test/filter-query.test.js deleted file mode 100644 index 1a15652e7e..0000000000 --- a/packages/commons/test/filter-query.test.js +++ /dev/null @@ -1,180 +0,0 @@ -const chai = require('chai'); -const filter = require('../lib/filter-query'); - -const expect = chai.expect; - -describe('.filterQuery', function () { - describe('$sort', function () { - it('returns $sort when present in query', function () { - const originalQuery = { $sort: { name: 1 } }; - const { filters, query } = filter(originalQuery); - - expect(filters.$sort.name).to.equal(1); - expect(query).to.deep.equal({}); - expect(originalQuery).to.deep.equal({ - $sort: { name: 1 } - }, 'does not modify original query'); - }); - - it('returns $sort when present in query as an object', function () { - const { filters, query } = filter({ $sort: { name: { something: 10 } } }); - expect(filters.$sort.name.something).to.equal(10); - expect(query).to.deep.equal({}); - }); - - it('converts strings in $sort', function () { - const { filters, query } = filter({ $sort: { test: '-1' } }); - expect(filters.$sort.test).to.equal(-1); - expect(query).to.deep.equal({}); - }); - - it('does not convert $sort arrays', function () { - const $sort = [ [ 'test', '-1' ], [ 'a', '1' ] ]; - const { filters, query } = filter({ $sort }); - - expect(filters.$sort).to.deep.equal($sort); - expect(query).to.deep.equal({}); - }); - - it('returns undefined when not present in query', function () { - const query = { $foo: 1 }; - const { filters } = filter(query); - expect(filters.$sort).to.equal(undefined); - }); - }); - - describe('$limit', function () { - beforeEach(function () { - this.query = { $limit: 1 }; - }); - - it('returns $limit when present in query', function () { - const { filters, query } = filter(this.query); - expect(filters.$limit).to.equal(1); - expect(query).to.deep.equal({}); - }); - - it('returns undefined when not present in query', function () { - const query = { $foo: 1 }; - const { filters } = filter(query); - expect(filters.$limit).to.equal(undefined); - }); - - it('removes $limit from query when present', function () { - expect(filter(this.query).query).to.deep.equal({}); - }); - - it('parses $limit strings into integers (#4)', function () { - const { filters } = filter({ $limit: '2' }); - expect(filters.$limit).to.equal(2); - }); - - it('allows $limit 0', function () { - const { filters } = filter({ $limit: 0 }, { default: 10 }); - expect(filters.$limit).to.equal(0); - }); - - describe('pagination', function () { - it('limits with default pagination', function () { - const { filters } = filter({}, { paginate: { default: 10 } }); - expect(filters.$limit).to.equal(10); - }); - - it('limits with max pagination', function () { - const { filters } = filter({ $limit: 20 }, { paginate: { default: 5, max: 10 } }); - const { filters: filtersNeg } = filter({ $limit: -20 }, { paginate: { default: 5, max: 10 } }); - expect(filters.$limit).to.equal(10); - expect(filtersNeg.$limit).to.equal(10); - }); - }); - }); - - describe('$skip', function () { - beforeEach(function () { - this.query = { $skip: 1 }; - }); - - it('returns $skip when present in query', function () { - const { filters } = filter(this.query); - expect(filters.$skip).to.equal(1); - }); - - it('removes $skip from query when present', function () { - expect(filter(this.query).query).to.deep.equal({}); - }); - - it('returns undefined when not present in query', function () { - const query = { $foo: 1 }; - const { filters } = filter(query); - expect(filters.$skip).to.equal(undefined); - }); - - it('parses $skip strings into integers (#4)', function () { - const { filters } = filter({ $skip: '33' }); - expect(filters.$skip).to.equal(33); - }); - }); - - describe('$select', function () { - beforeEach(function () { - this.query = { $select: 1 }; - }); - - it('returns $select when present in query', function () { - const { filters } = filter(this.query); - expect(filters.$select).to.equal(1); - }); - - it('removes $select from query when present', function () { - expect(filter(this.query).query).to.deep.equal({}); - }); - - it('returns undefined when not present in query', function () { - const query = { $foo: 1 }; - const { filters } = filter(query); - expect(filters.$select).to.equal(undefined); - }); - }); - - describe('additional filters', () => { - beforeEach(function () { - this.query = { $select: 1, $known: 1, $unknown: 1 }; - }); - - it('returns only default filters when no additionals', function () { - const { filters } = filter(this.query); - expect(filters).to.include({ $select: 1 }).and.to.not.have.any.keys('$known', '$unknown'); - }); - - it('returns default and known additional filters (array)', function () { - const { filters } = filter(this.query, { filters: [ '$known' ] }); - expect(filters).to.include({ $select: 1, $known: 1 }).and.to.not.have.key('$unknown'); - }); - - it('returns default and known additional filters (object)', function () { - const { filters } = filter(this.query, { filters: { $known: (value) => value.toString() } }); - expect(filters).to.include({ $select: 1, $known: '1' }).and.to.not.have.key('$unknown'); - }); - }); - - describe('additional operators', () => { - beforeEach(function () { - this.query = { $ne: 1, $known: 1, $unknown: 1 }; - }); - - it('returns query with only default operators when no additionals', function () { - const { query } = filter(this.query); - expect(query).to.include({ $ne: 1 }).and.to.not.have.any.keys('$known', '$unknown'); - }); - - it('returns query with default and known additional operators', function () { - const { query } = filter(this.query, { operators: [ '$known' ] }); - expect(query).to.eql({ $ne: 1, $known: 1 }).and.to.not.have.key('$unknown'); - }); - - it('returns query with default and known additional operators (nested)', function () { - const { query } = filter({ field: this.query }, { operators: [ '$known' ] }); - expect(query).to.deep.include({ field: { $ne: 1, $known: 1 } }).and.to.not.have.nested.property('field.$unknown'); - }); - }); -}); diff --git a/packages/commons/test/hooks.test.js b/packages/commons/test/hooks.test.js index ae8e1a2220..231db857b7 100644 --- a/packages/commons/test/hooks.test.js +++ b/packages/commons/test/hooks.test.js @@ -1,10 +1,10 @@ const { expect } = require('chai'); -const utils = require('../lib/hooks'); +const { hooks } = require('../lib'); describe('hook utilities', () => { describe('.makeArguments', () => { it('basic functionality', () => { - let args = utils.makeArguments({ + let args = hooks.makeArguments({ id: 2, data: { my: 'data' }, params: { some: 'thing' }, @@ -13,7 +13,7 @@ describe('hook utilities', () => { expect(args).to.deep.equal([2, { my: 'data' }, { some: 'thing' }]); - args = utils.makeArguments({ + args = hooks.makeArguments({ id: 0, data: { my: 'data' }, params: { some: 'thing' }, @@ -22,7 +22,7 @@ describe('hook utilities', () => { expect(args).to.deep.equal([0, { my: 'data' }, { some: 'thing' }]); - args = utils.makeArguments({ + args = hooks.makeArguments({ params: { some: 'thing' }, method: 'find' }); @@ -33,7 +33,7 @@ describe('hook utilities', () => { }); it('uses .defaultMakeArguments', () => { - let args = utils.makeArguments({ + let args = hooks.makeArguments({ params: { some: 'thing' }, method: 'something', data: { test: 'me' } @@ -44,7 +44,7 @@ describe('hook utilities', () => { { some: 'thing' } ]); - args = utils.makeArguments({ + args = hooks.makeArguments({ id: 'testing', method: 'something' }); @@ -55,7 +55,7 @@ describe('hook utilities', () => { }); it('.makeArguments makes correct argument list for known methods', () => { - let args = utils.makeArguments({ + let args = hooks.makeArguments({ data: { my: 'data' }, params: { some: 'thing' }, method: 'update' @@ -63,7 +63,7 @@ describe('hook utilities', () => { expect(args).to.deep.equal([undefined, { my: 'data' }, { some: 'thing' }]); - args = utils.makeArguments({ + args = hooks.makeArguments({ id: 2, data: { my: 'data' }, params: { some: 'thing' }, @@ -72,7 +72,7 @@ describe('hook utilities', () => { expect(args).to.deep.equal([2, { some: 'thing' }]); - args = utils.makeArguments({ + args = hooks.makeArguments({ id: 2, data: { my: 'data' }, params: { some: 'thing' }, @@ -85,19 +85,19 @@ describe('hook utilities', () => { describe('.convertHookData', () => { it('converts existing', () => { - expect(utils.convertHookData('test')).to.deep.equal({ + expect(hooks.convertHookData('test')).to.deep.equal({ all: [ 'test' ] }); }); it('converts to `all`', () => { - expect(utils.convertHookData([ 'test', 'me' ])).to.deep.equal({ + expect(hooks.convertHookData([ 'test', 'me' ])).to.deep.equal({ all: [ 'test', 'me' ] }); }); it('converts all properties into arrays', () => { - expect(utils.convertHookData({ + expect(hooks.convertHookData({ all: 'thing', other: 'value', hi: [ 'foo', 'bar' ] @@ -112,14 +112,14 @@ describe('hook utilities', () => { describe('.isHookObject', () => { it('with a valid hook object', () => { - expect(utils.isHookObject({ + expect(hooks.isHookObject({ type: 'before', method: 'here' })).to.equal(true); }); it('with an invalid hook object', () => { - expect(utils.isHookObject({ + expect(hooks.isHookObject({ type: 'before' })).to.equal(false); }); @@ -135,7 +135,7 @@ describe('hook utilities', () => { const hookData = { app, service }; it('.toJSON', () => { - let hookObject = utils.createHookObject('find', hookData); + let hookObject = hooks.createHookObject('find', hookData); expect(hookObject.toJSON()).to.deep.equal({ method: 'find', @@ -149,7 +149,7 @@ describe('hook utilities', () => { }); it('for find', () => { - let hookObject = utils.createHookObject('find', hookData); + let hookObject = hooks.createHookObject('find', hookData); expect(hookObject).to.deep.equal({ method: 'find', @@ -158,14 +158,14 @@ describe('hook utilities', () => { path: 'testing' }); - hookObject = utils.createHookObject('find'); + hookObject = hooks.createHookObject('find'); expect(hookObject).to.deep.equal({ method: 'find', path: null }); - hookObject = utils.createHookObject('find', hookData); + hookObject = hooks.createHookObject('find', hookData); expect(hookObject).to.deep.equal({ method: 'find', @@ -176,7 +176,7 @@ describe('hook utilities', () => { }); it('for get', () => { - let hookObject = utils.createHookObject('get', hookData); + let hookObject = hooks.createHookObject('get', hookData); expect(hookObject).to.deep.equal({ method: 'get', @@ -185,7 +185,7 @@ describe('hook utilities', () => { path: 'testing' }); - hookObject = utils.createHookObject('get', hookData); + hookObject = hooks.createHookObject('get', hookData); expect(hookObject).to.deep.equal({ method: 'get', @@ -196,7 +196,7 @@ describe('hook utilities', () => { }); it('for remove', () => { - let hookObject = utils.createHookObject('remove', hookData); + let hookObject = hooks.createHookObject('remove', hookData); expect(hookObject).to.deep.equal({ method: 'remove', @@ -205,7 +205,7 @@ describe('hook utilities', () => { path: 'testing' }); - hookObject = utils.createHookObject('remove', hookData); + hookObject = hooks.createHookObject('remove', hookData); expect(hookObject).to.deep.equal({ method: 'remove', @@ -216,7 +216,7 @@ describe('hook utilities', () => { }); it('for create', () => { - const hookObject = utils.createHookObject('create', hookData); + const hookObject = hooks.createHookObject('create', hookData); expect(hookObject).to.deep.equal({ method: 'create', @@ -227,7 +227,7 @@ describe('hook utilities', () => { }); it('for update', () => { - const hookObject = utils.createHookObject('update', hookData); + const hookObject = hooks.createHookObject('update', hookData); expect(hookObject).to.deep.equal({ method: 'update', @@ -238,7 +238,7 @@ describe('hook utilities', () => { }); it('for patch', () => { - const hookObject = utils.createHookObject('patch', hookData); + const hookObject = hooks.createHookObject('patch', hookData); expect(hookObject).to.deep.equal({ method: 'patch', @@ -249,7 +249,7 @@ describe('hook utilities', () => { }); it('for custom method', () => { - const hookObject = utils.createHookObject('custom', hookData); + const hookObject = hooks.createHookObject('custom', hookData); expect(hookObject).to.deep.equal({ method: 'custom', @@ -267,7 +267,7 @@ describe('hook utilities', () => { method: 'something' }; - const promise = utils.processHooks([ + const promise = hooks.processHooks([ function (hook) { hook.chain = [ 'first' ]; @@ -306,7 +306,7 @@ describe('hook utilities', () => { method: 'something' }; - const promise = utils.processHooks([ + const promise = hooks.processHooks([ function (hook) { hook.chain = [ 'first' ]; @@ -322,7 +322,7 @@ describe('hook utilities', () => { hook => { hook.chain.push('third'); - return utils.SKIP; + return hooks.SKIP; }, function (hook) { @@ -353,7 +353,7 @@ describe('hook utilities', () => { method: 'something' }; - const promise = utils.processHooks([ + const promise = hooks.processHooks([ function (hook, next) { hook.test = 'first ran'; @@ -377,7 +377,7 @@ describe('hook utilities', () => { method: 'something' }; - const promise = utils.processHooks([ + const promise = hooks.processHooks([ function () { return {}; } @@ -394,7 +394,7 @@ describe('hook utilities', () => { it('with custom types', () => { const base = {}; - utils.enableHooks(base, [], ['test']); + hooks.enableHooks(base, [], ['test']); expect(typeof base.__hooks).to.equal('object'); expect(typeof base.__hooks.test).to.equal('object'); @@ -406,7 +406,7 @@ describe('hook utilities', () => { hooks () {} }; - utils.enableHooks(base, [], ['test']); + hooks.enableHooks(base, [], ['test']); expect(typeof base.__hooks).to.equal('undefined'); }); @@ -414,7 +414,7 @@ describe('hook utilities', () => { let base = {}; beforeEach(() => { - base = utils.enableHooks({}, [ 'testMethod' ], [ 'dummy' ]); + base = hooks.enableHooks({}, [ 'testMethod' ], [ 'dummy' ]); }); it('registers hook with custom type and `all` method', () => { @@ -460,8 +460,8 @@ describe('hook utilities', () => { }); describe('.getHooks', () => { - const app = utils.enableHooks({}, [ 'testMethod' ], [ 'dummy' ]); - const service = utils.enableHooks({}, [ 'testMethod' ], [ 'dummy' ]); + const app = hooks.enableHooks({}, [ 'testMethod' ], [ 'dummy' ]); + const service = hooks.enableHooks({}, [ 'testMethod' ], [ 'dummy' ]); const appHook = function () {}; const serviceHook = function () {}; @@ -474,12 +474,12 @@ describe('hook utilities', () => { }); it('combines app and service hooks', () => { - expect(utils.getHooks(app, service, 'dummy', 'testMethod')) + expect(hooks.getHooks(app, service, 'dummy', 'testMethod')) .to.deep.equal([ appHook, serviceHook ]); }); it('combines app and service hooks with appLast', () => { - expect(utils.getHooks(app, service, 'dummy', 'testMethod', true)) + expect(hooks.getHooks(app, service, 'dummy', 'testMethod', true)) .to.deep.equal([ serviceHook, appHook ]); }); }); diff --git a/packages/commons/test/module.test.js b/packages/commons/test/module.test.js index 80f6d0266a..3c048f0c06 100644 --- a/packages/commons/test/module.test.js +++ b/packages/commons/test/module.test.js @@ -1,14 +1,12 @@ const { expect } = require('chai'); -const { _ } = require('../lib/commons'); +const { _ } = require('../lib'); describe('module', () => { it('is commonjs compatible', () => { - let commons = require('../lib/commons'); + let commons = require('../lib'); expect(typeof commons).to.equal('object'); expect(typeof commons.stripSlashes).to.equal('function'); - expect(typeof commons.sorter).to.equal('function'); - expect(typeof commons.select).to.equal('function'); expect(typeof commons.hooks).to.equal('object'); expect(typeof commons._).to.equal('object'); }); diff --git a/packages/commons/test/utils.test.js b/packages/commons/test/utils.test.js index 5d559b6978..6eaa80efec 100644 --- a/packages/commons/test/utils.test.js +++ b/packages/commons/test/utils.test.js @@ -4,13 +4,11 @@ const { expect } = require('chai'); const { _, - sorter, stripSlashes, - select, isPromise, makeUrl, createSymbol -} = require('../lib/utils'); +} = require('../lib'); describe('@feathersjs/commons utils', () => { it('stripSlashes', () => { @@ -173,253 +171,6 @@ describe('@feathersjs/commons utils', () => { }); }); - describe('select', () => { - it('select', () => { - const selector = select({ - query: { $select: ['name', 'age'] } - }); - - return Promise.resolve({ - name: 'David', - age: 3, - test: 'me' - }) - .then(selector) - .then(result => expect(result).to.deep.equal({ - name: 'David', - age: 3 - })); - }); - - it('select with arrays', () => { - const selector = select({ - query: { $select: ['name', 'age'] } - }); - - return Promise.resolve([{ - name: 'David', - age: 3, - test: 'me' - }, { - name: 'D', - age: 4, - test: 'you' - }]) - .then(selector) - .then(result => expect(result).to.deep.equal([{ - name: 'David', - age: 3 - }, { - name: 'D', - age: 4 - }])); - }); - - it('select with no query', () => { - const selector = select({}); - const data = { - name: 'David' - }; - - return Promise.resolve(data) - .then(selector) - .then(result => expect(result).to.deep.equal(data)); - }); - - it('select with other fields', () => { - const selector = select({ - query: { $select: [ 'name' ] } - }, 'id'); - const data = { - id: 'me', - name: 'David', - age: 10 - }; - - return Promise.resolve(data) - .then(selector) - .then(result => expect(result).to.deep.equal({ - id: 'me', - name: 'David' - })); - }); - }); - - describe('sorter', () => { - it('simple sorter', () => { - const array = [{ - name: 'David' - }, { - name: 'Eric' - }]; - - const sort = sorter({ - name: -1 - }); - - expect(array.sort(sort)).to.deep.equal([{ - name: 'Eric' - }, { - name: 'David' - }]); - }); - - it('simple sorter with arrays', () => { - const array = [{ - names: [ 'a', 'b' ] - }, { - names: [ 'c', 'd' ] - }]; - - const sort = sorter({ - names: -1 - }); - - expect(array.sort(sort)).to.deep.equal([{ - names: [ 'c', 'd' ] - }, { - names: [ 'a', 'b' ] - }]); - }); - - it('simple sorter with objects', () => { - const array = [{ - names: { - first: 'Dave', - last: 'L' - } - }, { - names: { - first: 'A', - last: 'B' - } - }]; - - const sort = sorter({ - names: 1 - }); - - expect(array.sort(sort)).to.deep.equal([{ - names: { - first: 'A', - last: 'B' - } - }, { - names: { - first: 'Dave', - last: 'L' - } - }]); - }); - - it('two property sorter', () => { - const array = [{ - name: 'David', - counter: 0 - }, { - name: 'Eric', - counter: 1 - }, { - name: 'David', - counter: 1 - }, { - name: 'Eric', - counter: 0 - }]; - - const sort = sorter({ - name: -1, - counter: 1 - }); - - expect(array.sort(sort)).to.deep.equal([ - { name: 'Eric', counter: 0 }, - { name: 'Eric', counter: 1 }, - { name: 'David', counter: 0 }, - { name: 'David', counter: 1 } - ]); - }); - - it('two property sorter with names', () => { - const array = [{ - name: 'David', - counter: 0 - }, { - name: 'Eric', - counter: 1 - }, { - name: 'Andrew', - counter: 1 - }, { - name: 'David', - counter: 1 - }, { - name: 'Andrew', - counter: 0 - }, { - name: 'Eric', - counter: 0 - }]; - - const sort = sorter({ - name: -1, - counter: 1 - }); - - expect(array.sort(sort)).to.deep.equal([ - { name: 'Eric', counter: 0 }, - { name: 'Eric', counter: 1 }, - { name: 'David', counter: 0 }, - { name: 'David', counter: 1 }, - { name: 'Andrew', counter: 0 }, - { name: 'Andrew', counter: 1 } - ]); - }); - - it('three property sorter with names', () => { - const array = [{ - name: 'David', - counter: 0, - age: 2 - }, { - name: 'Eric', - counter: 1, - age: 2 - }, { - name: 'David', - counter: 1, - age: 1 - }, { - name: 'Eric', - counter: 0, - age: 1 - }, { - name: 'Andrew', - counter: 0, - age: 2 - }, { - name: 'Andrew', - counter: 0, - age: 1 - }]; - - const sort = sorter({ - name: -1, - counter: 1, - age: -1 - }); - - expect(array.sort(sort)).to.deep.equal([ - { name: 'Eric', counter: 0, age: 1 }, - { name: 'Eric', counter: 1, age: 2 }, - { name: 'David', counter: 0, age: 2 }, - { name: 'David', counter: 1, age: 1 }, - { name: 'Andrew', counter: 0, age: 2 }, - { name: 'Andrew', counter: 0, age: 1 } - ]); - }); - }); - describe('makeUrl', function () { let mockApp;