Skip to content

Commit

Permalink
Merge pull request #75 from feathersjs/filters
Browse files Browse the repository at this point in the history
Created support for event filter and permission hooks. Reorganized service hooks because of this.
  • Loading branch information
eddyystop authored Jan 5, 2017
2 parents 006c96c + 5b022f5 commit dfee696
Show file tree
Hide file tree
Showing 68 changed files with 8,506 additions and 9 deletions.
13 changes: 4 additions & 9 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,13 @@ languages:
JavaScript: true
engines:
duplication:
enabled: true
enabled: false
# need this: Config Error: Unable to run the duplication engine without any languages enabled.
config:
languages:
- javascript
exclude_paths:
- "test/"
exclude_fingerprints:
- 5fe47a6f2c4df971a1b6f3958c9b3cd4
- e6fb8d617273153252180f7a10a24ac6
- 1a0b806990ced9ec7ba1e55749b18197
- 0323a4bda01997a1319c35acadcd8c4b
- 0b9a2fee69d7da77202b1cc0c801a1f4
ratings:
paths:
- "**.js"
exclude_paths:
- "lib/"
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@
},
"homepage": "https://github.com/feathers/feathers-hooks-common#readme",
"dependencies": {
"ajv": "4.10.0",
"debug": "^2.2.0",
"feathers-errors": "^2.4.0",
"feathers-socket-commons": "2.3.1",
"traverse": "0.6.6"
},
"devDependencies": {
Expand Down
65 changes: 65 additions & 0 deletions src/common/alter-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@

const traverser = require('traverse');
import { getByDot, setByDot } from '../hooks/utils';

// transformer(item /* modified */, fieldName, value)
export const _transformItems = (items /* modified */, fieldNames, transformer) => {
(Array.isArray(items) ? items : [items]).forEach(item => {
fieldNames.forEach(fieldName => {
transformer(item, fieldName, getByDot(item, fieldName));
});
});
};

export const _remove = (items /* modified */, fieldNames) => {
_transformItems(items, fieldNames, (item, fieldName, value) => {
if (value !== undefined) {
setByDot(item, fieldName, undefined, true);
}
});
};

export const _pluck = (items, fieldNames) => {
if (!Array.isArray(items)) {
return _pluckItem(items, fieldNames);
}

const pluckedItems = [];

(Array.isArray(items) ? items : [items]).forEach(item => {
pluckedItems.push(_pluckItem(item, fieldNames));
});

return pluckedItems;
};

function _pluckItem (item, fieldNames) {
const plucked = {};

fieldNames.forEach(fieldName => {
const value = getByDot(item, fieldName);
if (value !== undefined) { // prevent setByDot creating nested empty objects
setByDot(plucked, fieldName, value);
}
});

return plucked;
}

export const _traverse = (items, converter) => {
(Array.isArray(items) ? items : [items]).forEach(item => {
traverser(item).forEach(converter); // replacement is in place
});
};

export const _setFields = (items /* modified */, fieldValue, fieldNames, defaultFieldName) => {
const value = typeof fieldValue === 'function' ? fieldValue() : fieldValue;

if (!fieldNames.length) fieldNames = [defaultFieldName];

(Array.isArray(items) ? items : [items]).forEach(item => {
fieldNames.forEach(fieldName => {
setByDot(item, fieldName, value);
});
});
};
217 changes: 217 additions & 0 deletions src/common/conditionals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@

const errors = require('feathers-errors').errors;

// processFuncArray must handle case of null param.
module.exports = function Conditionals (processFuncArray) {
if (!(this instanceof Conditionals)) {
return new Conditionals(processFuncArray);
}

// fnArgs is [hook] for service & permission hooks, [data, connection, hook] for event filters
const iffElse = (predicate, trueFuncs, falseFuncs) => function (...fnArgs) {
if (typeof trueFuncs === 'function') { trueFuncs = [trueFuncs]; }
if (typeof falseFuncs === 'function') { falseFuncs = [falseFuncs]; }

// Babel 6.17.0 did not transpile something in the old version similar to this
// const runProcessFuncArray = funcs => processFuncArray.call(this, fnArgs, funcs);
var that = this;
var runProcessFuncArray = function (funcs) {
return processFuncArray.call(that, fnArgs, funcs);
};

const check = typeof predicate === 'function' ? predicate(...fnArgs) : !!predicate;

if (!check) {
return runProcessFuncArray(falseFuncs);
}

if (typeof check.then !== 'function') {
return runProcessFuncArray(trueFuncs);
}

return check.then(check1 => runProcessFuncArray(check1 ? trueFuncs : falseFuncs));
};

const iff = (predicate, ...rest) => {
const trueHooks = [].concat(rest);

const iffWithoutElse = function (hook) {
return iffElse(predicate, trueHooks, null).call(this, hook);
};
iffWithoutElse.else = (...falseHooks) => iffElse(predicate, trueHooks, falseHooks);

return iffWithoutElse;
};

const unless = (unlessFcn, ...rest) => {
if (typeof unlessFcn === 'function') {
return iff(isNot(unlessFcn), ...rest);
}

return iff(!unlessFcn, ...rest);
};

const some = (...rest) => function (...fnArgs) {
const promises = rest.map(fn => fn.apply(this, fnArgs));

return Promise.all(promises).then(results => {
return Promise.resolve(results.some(result => !!result));
});
};

const every = (...rest) => function (...fnArgs) {
const promises = rest.map(fn => fn.apply(this, fnArgs));

return Promise.all(promises).then(results => {
return Promise.resolve(results.every(result => !!result));
});
};

const isNot = (predicate) => {
if (typeof predicate !== 'function') {
throw new errors.MethodNotAllowed('Expected function as param. (isNot)');
}

return hook => {
const result = predicate(hook); // Should we pass a clone? (safety vs performance)

if (!result || typeof result.then !== 'function') {
return !result;
}

return result.then(result1 => !result1);
};
};

return {
iffElse,
iff,
when: iff,
unless,
some,
every,
isNot
};
};

/**
* Hook to conditionally execute one or another set of hooks.
*
* @param {Function|Promise|boolean} ifFcn - Predicate function(hook).
* @param {Array.function|Function} trueHooks - Hook functions to execute when ifFcn is truthy.
* @param {Array.function|Function} falseHooks - Hook functions to execute when ifFcn is falsey.
* @returns {Object} resulting hook
*
* The predicate is called with hook as a param.
* const isServer = hook => !hook.params.provider;
* iff(isServer, hook.remove( ... ));
* You can use a high order predicate to access other values.
* const isProvider = provider => hook => hook.params.provider === provider;
* iff(isProvider('socketio'), hook.remove( ... ));
*
* The hook functions may be sync, return a Promise, or use a callback.
* feathers-hooks will catch any errors from the predicate or hook Promises.
*
* Examples
* iffElse(isServer, [hookA, hookB], hookC)
*
* iffElse(isServer,
* [ hookA, iffElse(hook => hook.method === 'create', hook1, [hook2, hook3]), hookB ],
* iffElse(isProvider('rest'), [hook4, hook5], hook6])
* )
*/

/**
* Hook to conditionally execute one or another set of hooks using function chaining.
*
* @param {Function|Promise|boolean} ifFcn - Predicate function(hook).
* @param {Array.function} rest - Hook functions to execute when ifFcn is truthy.
* @returns {Function} iffWithoutElse
*
* Examples:
* iff(isServer, hookA, hookB)
* .else(hookC)
*
* iff(isServer,
* hookA,
* iff(isProvider('rest'), hook1, hook2, hook3)
* .else(hook4, hook5),
* hookB
* )
* .else(
* iff(hook => hook.method === 'create', hook6, hook7)
* )
*/

/**
* Alias for iff
*/

/**
* Hook that executes a set of hooks and returns true if at least one of
* the hooks returns a truthy value and false if none of them do.
*
* @param {Array.function} rest - Hook functions to execute.
* @returns {Boolean}
*
* Example 1
* service.before({
* create: hooks.iff(hooks.some(hook1, hook2, ...), hookA, hookB, ...)
* });
*
* Example 2 - called within a custom hook function
* function (hook) {
* ...
* hooks.some(hook1, hook2, ...).call(this, currentHook)
* .then(bool => { ... });
* }
*/

/**
* Hook that executes a set of hooks and returns true if all of
* the hooks returns a truthy value and false if one of them does not.
*
* @param {Array.function} rest - Hook functions to execute.
* @returns {Boolean}
*
* Example 1
* service.before({
* create: hooks.iff(hooks.every(hook1, hook2, ...), hookA, hookB, ...)
* });
*
* Example 2 - called within a custom hook function
* function (hook) {
* ...
* hooks.every(hook1, hook2, ...).call(this, currentHook)
* .then(bool => { ... })
* }
*/

/**
* Hook to conditionally execute one or another set of hooks using function chaining.
* if the predicate hook function returns a falsey value.
* Equivalent to iff(isNot(isProvider), hook1, hook2, hook3).
*
* @param {Function|Promise|boolean} unlessFcn - Predicate function(hook).
* @param {Array.function} rest - Hook functions to execute when unlessFcn is falsey.
* @returns {Function} iffWithoutElse
*
* Examples:
* unless(isServer, hookA, hookB)
*
* unless(isServer,
* hookA,
* unless(isProvider('rest'), hook1, hook2, hook3),
* hookB
* )
*/

/**
* Negate a predicate.
*
* @param {Function} predicate - returns a boolean or a promise resolving to a boolean.
* @returns {boolean} the not of the predicate result.
*
* const hooks, { iff, isNot, isProvider } from 'feathers-hooks-common';
* iff(isNot(isProvider('rest')), hooks.remove( ... ));
*/
20 changes: 20 additions & 0 deletions src/filters/alter-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

import { _remove, _pluck, _traverse, _setFields } from '../common/alter-data';

export const remove = (...fields) => data => {
_remove(data, fields);
return data;
};

export const pluck = (...fields) => data => _pluck(data, fields);

export const traverse = (converter, getObj) => (data, connection, hook) => {
const items = typeof getObj === 'function' ? getObj(data, connection, hook) : getObj || data;

_traverse(items, converter);
};

export const setFilteredAt = (...fieldNames) => data => {
_setFields(data, () => new Date(), fieldNames, 'filteredAt');
return data;
};
53 changes: 53 additions & 0 deletions src/filters/conditionals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@

import { promisify } from 'feathers-socket-commons/lib/utils';
import Conditionals from '../common/conditionals';
import makeDebug from 'debug';

const debug = makeDebug('filters/conditionals');
const ev = 'conditionals'; // todo work needed here before merge with feathers-sockets-common

// https://github.com/feathersjs/feathers-socket-commons/blob/master/src/events.js
// lines 44-69
const dispatchFilters = (promisify, promise, eventFilters, service, data, connection, hook) => {
if (eventFilters.length) {
eventFilters.forEach(filterFn => {
if (filterFn.length === 4) { // function(data, connection, hook, callback)
promise = promise.then(data => {
if (data) {
return promisify(filterFn, service, data, connection, hook);
}

return data;
});
} else { // function(data, connection, hook)
promise = promise.then(data => {
if (data) {
return filterFn.call(service, data, connection, hook);
}

return data;
});
}
});
}

promise.catch(e => debug(`Error in filter chain for '${ev}' event`, e));

return promise;
};

const combine = (...eventFilters) => function ([data, connection, hook]) {
let promise = Promise.resolve(data);

return dispatchFilters(promisify, promise, eventFilters, this, data, connection, hook);
};

const conditionals = new Conditionals(
function (filterFnArgs, eventFilters) {
return eventFilters ? combine(...eventFilters).call(this, filterFnArgs) : filterFnArgs[0];
});

export default Object.assign(
{ combine },
conditionals,
);
5 changes: 5 additions & 0 deletions src/filters/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

module.exports = Object.assign({},
require('./conditionals'),
require('./alter-data'),
);
Loading

0 comments on commit dfee696

Please sign in to comment.