Skip to content

Commit

Permalink
Merge 53038dd into ba1c18f
Browse files Browse the repository at this point in the history
  • Loading branch information
andrenarchy committed Sep 19, 2016
2 parents ba1c18f + 53038dd commit fc53ba8
Show file tree
Hide file tree
Showing 4 changed files with 387 additions and 104 deletions.
51 changes: 32 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,42 +14,45 @@ Before storing user-supplied data in a database, you usually want to check if th

```javascript
let allowed = {name: true, age: true};
whitelist({name: 'Darth', age: 42}, allowed); // returns {name: 'Darth', age: 42}
whitelist({id: 23}, allowed); // throws WhitelistError (field 'id' is not allowed)
whitelist({name: 'Darth'}, allowed); // returns {name: 'Darth', age: undefined}
whitelist({name: 'Darth', age: 42}, allowed); // resolves with {name: 'Darth', age: 42}
whitelist({id: 23}, allowed); // rejects with WhitelistError (field 'id' is not allowed)
whitelist({name: 'Darth'}, allowed); // resolves with {name: 'Darth', age: undefined}
// omit keys with undefined values:
whitelist({name: 'Darth'}, allowed, {omitUndefined: true}); // returns {name: 'Darth'}
whitelist({name: 'Darth'}, allowed, {omitUndefined: true}); // resolves with {name: 'Darth'}
```

You can also use a function to check fields:
```javascript
let allowed = {
name: true,
age: (v) => v < 50 ? v : undefined
age: (age, options) => {
if (age < 50) return age;
throw WhitelistError('age must be less than 50', options.path);
},
};
whitelist({name: 'Darth', age: 42}, allowed); // returns {name: 'Darth', age: 42}
whitelist({name: 'Darth', age: 66}, allowed); // returns {name: 'Darth', age: undefined}
whitelist({name: 'Darth', age: 42}, allowed); // resolves with {name: 'Darth', age: 42}
whitelist({name: 'Darth', age: 66}, allowed); // rejects with WhitelistError ('age must be less than 50')
```

Nested objects work, too:
```javascript
allowed = {name: true, lightsaber: {color: true}};
whitelist({name: 'Darth', lightsaber: {color: 'red'}}, allowed); // returns {name: 'Darth', lightsaber: {color: 'red'}}
whitelist({name: 'Darth'}, allowed); // returns {name: 'Darth', lightsaber: {color: undefined}}
whitelist({name: 'Darth', lightsaber: {color: 'red'}}, allowed); // resolves with {name: 'Darth', lightsaber: {color: 'red'}}
whitelist({name: 'Darth'}, allowed); // resolves with {name: 'Darth', lightsaber: {color: undefined}}
// omit keys with undefined values:
whitelist({name: 'Darth'}, allowed, {omitUndefined: true}); // returns {name: 'Darth', lightsaber: {}}
whitelist({name: 'Darth'}, allowed, {omitUndefined: true}); // resolves with {name: 'Darth', lightsaber: {}}
```

## Pick allowed fields
Before sending data from a database to a client, you want to pick only fields that the client is allowed to see. This can be achieved by using the option `omitDisallowed: true`.

```javascript
let allowed = {name: true, age: true};
whitelist({id: 23, name: 'Darth', age: 42}, allowed, {omitDisallowed: true}); // returns {name: 'Darth', age: 42}
whitelist({id: 23, name: 'Darth'}, allowed, {omitDisallowed: true}); // returns {name: 'Darth', age: undefined}
whitelist({id: 23, name: 'Darth', age: 42}, allowed, {omitDisallowed: true}); // resolves with {name: 'Darth', age: 42}
whitelist({id: 23, name: 'Darth'}, allowed, {omitDisallowed: true}); // resolves with {name: 'Darth', age: undefined}
// omitDisallowed can be combined with omitUndefined:
whitelist({id: 23, name: 'Darth'}, allowed,
{omitDisallowed: true, omitUndefined: true}); // returns {name: 'Darth'}
{omitDisallowed: true, omitUndefined: true}); // resolves with {name: 'Darth'}
```

# Installation
Expand All @@ -63,13 +66,23 @@ const whitelist = require('walter-whitelist');
```

## `whitelist(src, allowed, options)`
* `src`: source object
* `allowed`: an object that specifies which fields are allowed. The values can be
* a boolean: if the value is `true`, the field is allowed and *copied* to the result object
* an object: whitelist is called recursively (for nested objects)
* a function `fn(value, path)`: the result of the function is placed in the result object
* `src`: source object, array or primitive
* `allowed`: the checks on `src` are performed according to this value. The following values are accepted:
* an object `{key: value, ...}`:
* expects `src` to be an object.
* iterates over keys and uses the value for whitelisting the corresponding key/value pair in `src`
* `value` can be any value that is accepted as the `allowed` parameter
* an array with one element `[value]`:
* expects `src` to be an array
* iterates over elements of array `src` and whitelists according to `value`
* `value` can be any value that is accepted as the `allowed parameter`
* a function `fn(src, options)`:
* should return the whitelisted `src` (directly or via a promise)
* if `omitDisallowed` is `false` and `src` contains disallowed data, the function is responsible for throwing a `WhitelistError` (or rejecting the returned promise with a `WhitelistError`)
* a boolean: if the value is `true`, `src` is allowed and returned as the result
* `options`: an object with the following optional keys:
* `omitUndefined`: if set to `true`, it omits fields in the result whose values are undefined
* `omitDisallowed`: if set to `true`, it omits fields from src that are not present in `allowed`
* `omitDisallowed`: if set to `true`, it omits fields from src that are not present in `allowed`.
* `data`: custom data that is recursively passed to any function in the `allowed` parameter.

The function returns a new object with the whitelisted fields and throws a `whitelist.WhitelistError` if a field in `src` is not allowed (unless `omitDisallow` is `true`).
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "walter-whitelist",
"version": "1.0.14",
"version": "2.0.0-alpha.1",
"description": "Whitelist javascript objects",
"main": "src/index.js",
"scripts": {
"coveralls": "isparta cover ./node_modules/mocha/bin/_mocha test && cat coverage/lcov.info | coveralls",
"coveralls": "nyc --reporter=lcov npm test && cat coverage/lcov.info | coveralls",
"lint": "eslint src test",
"test": "mocha test"
},
Expand Down Expand Up @@ -38,11 +38,12 @@
"eslint": "^3.5.0",
"eslint-config-airbnb": "^11.0.0",
"eslint-plugin-import": "^1.15.0",
"isparta": "^4.0.0",
"mocha": "^3.0.2",
"nyc": "^8.3.0",
"should": "^11.1.0"
},
"dependencies": {
"co": "^4.6.0",
"lodash": "^4.0.0"
}
}
119 changes: 79 additions & 40 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const co = require('co');
const _ = require('lodash');
const util = require('util');

Expand All @@ -16,68 +17,106 @@ function setDifference(a, b) {

/* check if obj is valid and return object with value 'undefined' for
* missing keys in obj */
function whitelist(src, allowed, _options, _path) {
const whitelist = co.wrap(function* whitelist(src, allowed, _options) {
// init default options
const options = _.defaults(_options || {}, {
const options = _.defaults({}, _options, {
// ignore keys in `src` that are not whitelisted in the `allowed` obj
// (otherwise a WhitelistError is thrown)
omitDisallowed: false,
// remove keys with undefined values
// (values of keys which are in `allowed` obj but not in `src` are set to
// undefined by default)
omitUndefined: false,
// path in allowed parameter (for recursive calls)
path: '',
});

// init path
const path = _path || '';

// check input
if (!_.isObject(src) || !_.isObject(allowed)) {
throw new WhitelistError(
`expected an object${(path ? ` at path ${path}` : '')}`
);
if (_.isBoolean(allowed)) {
if (allowed) return src;
if (options.omitDisallowed) return undefined;
throw new WhitelistError('value not allowed', options.path);
}

// check for extra keys
if (!options.omitDisallowed) {
const srcKeys = new Set(Object.keys(src));
const allowedKeys = new Set(Object.keys(allowed));
const disallowedKeys = setDifference(srcKeys, allowedKeys);
if (disallowedKeys.size) {
throw new WhitelistError(
`The following fields are not allowed: ${
Array.from(disallowedKeys).map(key => path + key).join(', ')
}. Allowed fields: ${Array.from(allowedKeys).join(', ')}.`,
disallowedKeys
);
if (_.isFunction(allowed)) {
try {
return yield Promise.resolve(allowed(src, options));
} catch (error) {
if (options.omitDisallowed) return undefined;
throw error;
}
}

// construct new object
const res = _.mapValues(allowed, (val, key) => {
const currentPath = path + key;
if (_.isArray(allowed)) {
if (allowed.length !== 1) {
throw new WhitelistError('allowed array not of length 1', options.path);
}
if (!_.isArray(src)) {
throw new WhitelistError('src is not an array', options.path);
}

const result = yield src.map(co.wrap(function* whitelistArray(el, index) {
const arrayOptions = _.clone(options);
arrayOptions.path += `[${index}]`;
try {
return yield whitelist(el, allowed[0], arrayOptions);
} catch (error) {
if (error instanceof WhitelistError && options.omitDisallowed) return undefined;
throw error;
}
}));

// falsy: undefined
if (!val) return undefined;
// remove undefined (not using _.compact because it removes all falsy elements)
if (options.omitUndefined) {
const cleanResult = [];
result.forEach((el) => {
if (el !== undefined) cleanResult.push(el);
});
return cleanResult;
}

// true: use full object
if (val === true) return src[key];
return result;
}

// function: use result of function call
if (_.isFunction(val)) return val(src[key], currentPath);
if (_.isObject(allowed)) {
if (!_.isObject(src)) throw new WhitelistError('src is not an object');

// object: get whitelisted object recursively
if (_.isObject(val)) {
return whitelist(src[key] || {}, val, options, `${currentPath}.`);
// check for extra keys
const srcKeys = new Set(Object.keys(src));
const allowedKeys = new Set(Object.keys(allowed));
const disallowedKeys = Array.from(setDifference(srcKeys, allowedKeys));
if (!options.omitDisallowed) {
if (disallowedKeys.length > 0) {
const key = disallowedKeys[0];
const path = options.path ? `${options.path}.${key}` : key;
throw new WhitelistError(
`The following field is not allowed: ${path}. Allowed fields at ${options.path}: ${Array.from(allowedKeys).join(', ')}.`,
path
);
}
}

// unhandled value
throw new Error(`unknown value in allowed object for key ${key}: ${val}`);
});
const result = yield _.mapValues(allowed, co.wrap(function* whitelistObject(value, key) {
const objectOptions = _.clone(options);
if (!objectOptions.path) objectOptions.path = key;
else objectOptions.path += `.${key}`;
try {
return yield whitelist(src[key], value, objectOptions);
} catch (error) {
if (error instanceof WhitelistError && options.omitDisallowed) return undefined;
throw error;
}
}));

// filter undefined values (if required by options)
return options.omitUndefined ? _.pickBy(res, v => v !== undefined) : res;
}
// set disallowed fields to undefined if omitDisallowed is true
// (useful for updating database with user-supplied data)
if (options.omitDisallowed) disallowedKeys.forEach((k) => { result[k] = undefined; });

// filter undefined values (if required by options)
return options.omitUndefined ? _.pickBy(result, v => v !== undefined) : result;
}

throw new Error('allowed parameter type not recognized');
});

module.exports = whitelist.default = whitelist.whitelist = whitelist;
whitelist.WhitelistError = WhitelistError;

0 comments on commit fc53ba8

Please sign in to comment.