Commit c57598f2 authored by Frauke's avatar Frauke

Initial commit

parents
---
extends: eslint-config-semistandard
\ No newline at end of file
This diff is collapsed.
The MIT License (MIT)
Copyright (c) 2015 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.
# Feathers Commons
[![Greenkeeper badge](https://badges.greenkeeper.io/feathersjs/commons.svg)](https://greenkeeper.io/)
[![Build Status](https://travis-ci.org/feathersjs/commons.png?branch=master)](https://travis-ci.org/feathersjs/commons)
[![Test Coverage](https://codeclimate.com/github/feathersjs/commons/badges/coverage.svg)](https://codeclimate.com/github/feathersjs/commons/coverage)
[![Dependency Status](https://img.shields.io/david/feathersjs/commons.svg?style=flat-square)](https://david-dm.org/feathersjs/commons)
[![Download Status](https://img.shields.io/npm/dm/@feathersjs/commons.svg?style=flat-square)](https://www.npmjs.com/package/@feathersjs/commons)
> Shared Feathers utility functions
## About
This is a repository for utility functionality that is shared between different Feathers plugin and used by the main repository.
## Authors
[Feathers contributors](https://github.com/feathersjs/commons/graphs/contributors)
## License
Copyright (c) 2017 Feathers contributors
Licensed under the [MIT license](LICENSE).
const paramCounts = {
find: 1,
get: 2,
create: 2,
update: 3,
patch: 3,
remove: 2
};
function isObjectOrArray (value) {
return typeof value === 'object' && value !== null;
}
exports.validateArguments = function validateArguments (method, args) {
// Check if the last argument is a callback which are no longer supported
if (typeof args[args.length - 1] === 'function') {
throw new Error('Callbacks are no longer supported. Use Promises or async/await instead.');
}
const methodParamCount = paramCounts[method];
// Check the number of arguments and throw an error if too many are provided
if (methodParamCount && args.length > methodParamCount) {
throw new Error(`Too many arguments for '${method}' method`);
}
// `params` is always the last argument
const params = args[methodParamCount - 1];
// Check if `params` is an object (can be undefined though)
if (params !== undefined && !isObjectOrArray(params)) {
throw new Error(`Params for '${method}' method must be an object`);
}
// Validate other arguments for each method
switch (method) {
case 'create':
if (!isObjectOrArray(args[0])) {
throw new Error(`A data object must be provided to the 'create' method`);
}
break;
case 'get':
case 'remove':
case 'update':
case 'patch':
if (args[0] === undefined) {
throw new Error(`An id must be provided to the '${method}' method`);
}
if ((method === 'update' || method === 'patch') && !isObjectOrArray(args[1])) {
throw new Error(`A data object must be provided to the '${method}' method`);
}
}
return true;
};
const utils = require('./utils');
const hooks = require('./hooks');
const args = require('./arguments');
const filterQuery = require('./filter-query');
module.exports = Object.assign({}, utils, args, { hooks, filterQuery });
const { _ } = require('./utils');
// Officially supported query parameters ($populate is kind of special)
const PROPERTIES = ['$sort', '$limit', '$skip', '$select', '$populate'];
function parse (number) {
if (typeof number !== 'undefined') {
return Math.abs(parseInt(number, 10));
}
}
// Returns the pagination limit and will take into account the
// default and max pagination settings
function getLimit (limit, paginate) {
if (paginate && paginate.default) {
const lower = typeof limit === 'number' ? limit : paginate.default;
const upper = typeof paginate.max === 'number' ? paginate.max : Number.MAX_VALUE;
return Math.min(lower, upper);
}
return limit;
}
// Makes sure that $sort order is always converted to an actual number
function convertSort (sort) {
if (typeof sort !== 'object' || Array.isArray(sort)) {
return sort;
}
const result = {};
Object.keys(sort).forEach(key => {
result[key] = typeof sort[key] === 'object'
? sort[key] : parseInt(sort[key], 10);
});
return result;
}
// Converts Feathers special query parameters and pagination settings
// and returns them separately a `filters` and the rest of the query
// as `query`
module.exports = function (query, paginate) {
let filters = {
$sort: convertSort(query.$sort),
$limit: getLimit(parse(query.$limit), paginate),
$skip: parse(query.$skip),
$select: query.$select,
$populate: query.$populate
};
return { filters, query: _.omit(query, ...PROPERTIES) };
};
const { each, pick } = require('./utils')._;
function convertGetOrRemove (args) {
const [ id, params = {} ] = args;
return { id, params };
}
function convertUpdateOrPatch (args) {
const [ id, data, params = {} ] = args;
return { id, data, params };
}
// To skip further hooks
const SKIP = exports.SKIP = typeof Symbol !== 'undefined' ? Symbol('__feathersSkipHooks') : '__feathersSkipHooks';
// Converters from service method arguments to hook object properties
exports.converters = {
find (args) {
const [ params = {} ] = args;
return { params };
},
create (args) {
const [ data, params = {} ] = args;
return { data, params };
},
get: convertGetOrRemove,
remove: convertGetOrRemove,
update: convertUpdateOrPatch,
patch: convertUpdateOrPatch
};
// Create a hook object for a method with arguments `args`
// `data` is additional data that will be added
exports.createHookObject = function createHookObject (method, args, data = {}) {
const hook = exports.converters[method](args);
Object.defineProperty(hook, 'toJSON', {
value () {
return pick(this, 'type', 'method', 'path',
'params', 'id', 'data', 'result', 'error');
}
});
return Object.assign(hook, data, {
method,
// A dynamic getter that returns the path of the service
get path () {
const { app, service } = data;
if (!service || !app || !app.services) {
return null;
}
return Object.keys(app.services)
.find(path => app.services[path] === service);
}
});
};
// Fallback used by `makeArguments` which usually won't be used
exports.defaultMakeArguments = function defaultMakeArguments (hook) {
const result = [];
if (typeof hook.id !== 'undefined') {
result.push(hook.id);
}
if (hook.data) {
result.push(hook.data);
}
result.push(hook.params || {});
return result;
};
// Turns a hook object back into a list of arguments
// to call a service method with
exports.makeArguments = function makeArguments (hook) {
switch (hook.method) {
case 'find':
return [ hook.params ];
case 'get':
case 'remove':
return [ hook.id, hook.params ];
case 'update':
case 'patch':
return [ hook.id, hook.data, hook.params ];
case 'create':
return [ hook.data, hook.params ];
}
return exports.defaultMakeArguments(hook);
};
// Converts different hook registration formats into the
// same internal format
exports.convertHookData = function convertHookData (obj) {
var hook = {};
if (Array.isArray(obj)) {
hook = { all: obj };
} else if (typeof obj !== 'object') {
hook = { all: [ obj ] };
} else {
each(obj, function (value, key) {
hook[key] = !Array.isArray(value) ? [ value ] : value;
});
}
return hook;
};
// Duck-checks a given object to be a hook object
// A valid hook object has `type` and `method`
exports.isHookObject = function isHookObject (hookObject) {
return typeof hookObject === 'object' &&
typeof hookObject.method === 'string' &&
typeof hookObject.type === 'string';
};
// Returns all service and application hooks combined
// for a given method and type `appLast` sets if the hooks
// from `app` should be added last (or first by default)
exports.getHooks = function getHooks (app, service, type, method, appLast = false) {
const appHooks = app.__hooks[type][method] || [];
const serviceHooks = service.__hooks[type][method] || [];
if (appLast) {
// Run hooks in the order of service -> app -> finally
return serviceHooks.concat(appHooks);
}
return appHooks.concat(serviceHooks);
};
exports.processHooks = function processHooks (hooks, initialHookObject) {
let hookObject = initialHookObject;
let updateCurrentHook = current => {
// Either use the returned hook object or the current
// hook object from the chain if the hook returned undefined
if (current) {
if (current === SKIP) {
return SKIP;
}
if (!exports.isHookObject(current)) {
throw new Error(`${hookObject.type} hook for '${hookObject.method}' method returned invalid hook object`);
}
hookObject = current;
}
return hookObject;
};
// First step of the hook chain with the initial hook object
let promise = Promise.resolve(hookObject);
// Go through all hooks and chain them into our promise
hooks.forEach(fn => {
const hook = fn.bind(this);
if (hook.length === 2) { // function(hook, next)
promise = promise.then(hookObject => hookObject === SKIP ? SKIP : new Promise((resolve, reject) => {
hook(hookObject, (error, result) =>
error ? reject(error) : resolve(result)
);
}));
} else { // function(hook)
promise = promise.then(hookObject => hookObject === SKIP ? SKIP : hook(hookObject));
}
// Use the returned hook object or the old one
promise = promise.then(updateCurrentHook);
});
return promise
.then(() => hookObject)
.catch(error => {
// Add the hook information to any errors
error.hook = hookObject;
throw error;
});
};
// Add `.hooks` functionality to an object
exports.enableHooks = function enableHooks (obj, methods, types) {
if (typeof obj.hooks === 'function') {
return obj;
}
let __hooks = {};
types.forEach(type => {
// Initialize properties where hook functions are stored
__hooks[type] = {};
});
// Add non-enumerable `__hooks` property to the object
Object.defineProperty(obj, '__hooks', {
value: __hooks
});
return Object.assign(obj, {
hooks (allHooks) {
each(allHooks, (obj, type) => {
if (!this.__hooks[type]) {
throw new Error(`'${type}' is not a valid hook type`);
}
const hooks = exports.convertHookData(obj);
each(hooks, (value, method) => {
if (method !== 'all' && methods.indexOf(method) === -1) {
throw new Error(`'${method}' is not a valid hook method`);
}
});
methods.forEach(method => {
const myHooks = this.__hooks[type][method] ||
(this.__hooks[type][method] = []);
if (hooks.all) {
myHooks.push.apply(myHooks, hooks.all);
}
if (hooks[method]) {
myHooks.push.apply(myHooks, hooks[method]);
}
});
});
return this;
}
});
};
const assert = require('assert');
module.exports = function (app, name) {
const getService = () => (name && typeof app.service === 'function')
? app.service(name) : app;
describe('Service base tests', () => {
it('.find', () => {
return getService().find().then(todos =>
assert.deepEqual(todos, [{
text: 'some todo',
complete: false,
id: 0
}])
);
});
it('.get and params passing', () => {
const query = {
some: 'thing',
other: ['one', 'two'],
nested: {a: {b: 'object'}}
};
return getService().get(0, { query })
.then(todo => assert.deepEqual(todo, {
id: 0,
text: 'some todo',
complete: false,
query: query
}));
});
it('.create and created event', done => {
getService().once('created', function (data) {
assert.equal(data.text, 'created todo');
assert.ok(data.complete);
done();
});
getService().create({text: 'created todo', complete: true});
});
it('.update and updated event', done => {
getService().once('updated', data => {
assert.equal(data.text, 'updated todo');
assert.ok(data.complete);
done();
});
getService().create({text: 'todo to update', complete: false})
.then(todo => getService().update(todo.id, {
text: 'updated todo',
complete: true
}));
});
it('.patch and patched event', done => {
getService().once('patched', data => {
assert.equal(data.text, 'todo to patch');
assert.ok(data.complete);
done();
});
getService().create({text: 'todo to patch', complete: false})
.then(todo => getService().patch(todo.id, {complete: true}));
});
it('.remove and removed event', done => {
getService().once('removed', data => {
assert.equal(data.text, 'todo to remove');
assert.equal(data.complete, false);
done();
});
getService().create({text: 'todo to remove', complete: false})
.then(todo => getService().remove(todo.id)).catch(done);
});
it('.get with error', () => {
let query = {error: true};
return getService().get(0, {query}).catch(error =>
assert.ok(error && error.message)
);
});
});
};
const assert = require('assert');
const findAllData = [{
id: 0,
description: 'You have to do something'
}, {
id: 1,
description: 'You have to do laundry'
}];
exports.Service = {
events: [ 'log' ],
find () {
return Promise.resolve(findAllData);
},
get (name, params) {
if (params.query.error) {
return Promise.reject(new Error(`Something for ${name} went wrong`));
}
if (params.query.runtimeError) {
thingThatDoesNotExist(); // eslint-disable-line
}
return Promise.resolve({
id: name,
description: `You have to do ${name}!`
});
},
create (data) {
const result = Object.assign({}, data, {
id: 42,
status: 'created'
});
if (Array.isArray(data)) {
result.many = true;
}
return Promise.resolve(result);
},
update (id, data) {
const result = Object.assign({}, data, {
id, status: 'updated'
});
if (id === null) {
result.many = true;
}
return Promise.resolve(result);
},
patch (id, data) {
const result = Object.assign({}, data, {
id, status: 'patched'
});
if (id === null) {
result.many = true;
}
return Promise.resolve(result);
},
remove (id) {
return Promise.resolve({ id });
}
};
exports.verify = {
find (data) {
assert.deepEqual(findAllData, data, 'Data as expected');
},
get (id, data) {
assert.equal(data.id, id, 'Got id in data');
assert.equal(data.description, `You have to do ${id}!`, 'Got description');
},
create (original, current) {
var expected = Object.assign({}, original, {
id: 42,
status: 'created'
});
assert.deepEqual(expected, current, 'Data ran through .create as expected');
},
update (id, original, current) {
var expected = Object.assign({}, original, {
id: id,
status: 'updated'
});
assert.deepEqual(expected, current, 'Data ran through .update as expected');
},
patch (id, original, current) {
var expected = Object.assign({}, original, {
id: id,
status: 'patched'
});
assert.deepEqual(expected, current, 'Data ran through .patch as expected');
},
remove (id, data) {
assert.deepEqual({ id }, data, '.remove called');
}
};
// Removes all leading and trailing slashes from a path
exports.stripSlashes = function stripSlashes (name) {
return name.replace(/^(\/*)|(\/*)$/g, '');
};
// A set of lodash-y utility functions that use ES6
const _ = exports._ = {
each (obj, callback) {
if (obj && typeof obj.forEach === 'function') {
obj.forEach(callback);
} else if (_.isObject(obj)) {
Object.keys(obj).forEach(key => callback(obj[key], key));
}
},
some (value, callback) {
return Object.keys(value)
.map(key => [ value[key], key ])
.some(([val, key]) => callback(val, key));
},
every (value, callback) {
return Object.keys(value)
.map(key => [ value[key], key ])
.every(([val, key]) => callback(val, key));
},
keys (obj) {
return Object.keys(obj);
},
values (obj) {
return _.keys(obj).map(key => obj[key]);
},
isMatch (obj, item) {
return _.keys(item).every(key => obj[key] === item[key]);
},
isEmpty (obj) {
return _.keys(obj).length === 0;
},
isObject (item) {
return (typeof item === 'object' && !Array.isArray(item) && item !== null);