Skip to content

Commit a53513a

Browse files
authored
Merge pull request #46 from paperhive/v2
add support for promises and arrays
2 parents ba1c18f + 28cf52f commit a53513a

7 files changed

Lines changed: 392 additions & 109 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
node_modules
33
npm-debug.log
44
coverage
5+
.nyc_output

.istanbul.yml

Lines changed: 0 additions & 2 deletions
This file was deleted.

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ script:
66
- npm run lint
77
- npm test
88
after_success:
9-
- npm run coveralls
9+
- npm run codecov

README.md

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# walter-whitelist [![Build Status](https://travis-ci.org/paperhive/walter-whitelist.svg?branch=master)](https://travis-ci.org/paperhive/walter-whitelist) [![Coverage Status](https://coveralls.io/repos/paperhive/walter-whitelist/badge.svg?branch=master&service=github)](https://coveralls.io/github/paperhive/walter-whitelist?branch=master)
1+
# walter-whitelist [![Build Status](https://travis-ci.org/paperhive/walter-whitelist.svg?branch=master)](https://travis-ci.org/paperhive/walter-whitelist) [![codecov](https://codecov.io/gh/paperhive/walter-whitelist/branch/master/graph/badge.svg)](https://codecov.io/gh/paperhive/walter-whitelist)
22

33
This lightweight module whitelists javascript objects recursively. This is particularly useful in the following situations:
44

@@ -14,42 +14,45 @@ Before storing user-supplied data in a database, you usually want to check if th
1414

1515
```javascript
1616
let allowed = {name: true, age: true};
17-
whitelist({name: 'Darth', age: 42}, allowed); // returns {name: 'Darth', age: 42}
18-
whitelist({id: 23}, allowed); // throws WhitelistError (field 'id' is not allowed)
19-
whitelist({name: 'Darth'}, allowed); // returns {name: 'Darth', age: undefined}
17+
whitelist({name: 'Darth', age: 42}, allowed); // resolves with {name: 'Darth', age: 42}
18+
whitelist({id: 23}, allowed); // rejects with WhitelistError (field 'id' is not allowed)
19+
whitelist({name: 'Darth'}, allowed); // resolves with {name: 'Darth', age: undefined}
2020
// omit keys with undefined values:
21-
whitelist({name: 'Darth'}, allowed, {omitUndefined: true}); // returns {name: 'Darth'}
21+
whitelist({name: 'Darth'}, allowed, {omitUndefined: true}); // resolves with {name: 'Darth'}
2222
```
2323

2424
You can also use a function to check fields:
2525
```javascript
2626
let allowed = {
2727
name: true,
28-
age: (v) => v < 50 ? v : undefined
28+
age: (age, options) => {
29+
if (age < 50) return age;
30+
throw WhitelistError('age must be less than 50', options.path);
31+
},
2932
};
30-
whitelist({name: 'Darth', age: 42}, allowed); // returns {name: 'Darth', age: 42}
31-
whitelist({name: 'Darth', age: 66}, allowed); // returns {name: 'Darth', age: undefined}
33+
whitelist({name: 'Darth', age: 42}, allowed); // resolves with {name: 'Darth', age: 42}
34+
whitelist({name: 'Darth', age: 66}, allowed); // rejects with WhitelistError ('age must be less than 50')
3235
```
3336

3437
Nested objects work, too:
3538
```javascript
3639
allowed = {name: true, lightsaber: {color: true}};
37-
whitelist({name: 'Darth', lightsaber: {color: 'red'}}, allowed); // returns {name: 'Darth', lightsaber: {color: 'red'}}
38-
whitelist({name: 'Darth'}, allowed); // returns {name: 'Darth', lightsaber: {color: undefined}}
40+
whitelist({name: 'Darth', lightsaber: {color: 'red'}}, allowed); // resolves with {name: 'Darth', lightsaber: {color: 'red'}}
41+
whitelist({name: 'Darth'}, allowed); // resolves with {name: 'Darth', lightsaber: {color: undefined}}
3942
// omit keys with undefined values:
40-
whitelist({name: 'Darth'}, allowed, {omitUndefined: true}); // returns {name: 'Darth', lightsaber: {}}
43+
whitelist({name: 'Darth'}, allowed, {omitUndefined: true}); // resolves with {name: 'Darth', lightsaber: {}}
4144
```
4245

4346
## Pick allowed fields
4447
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`.
4548

4649
```javascript
4750
let allowed = {name: true, age: true};
48-
whitelist({id: 23, name: 'Darth', age: 42}, allowed, {omitDisallowed: true}); // returns {name: 'Darth', age: 42}
49-
whitelist({id: 23, name: 'Darth'}, allowed, {omitDisallowed: true}); // returns {name: 'Darth', age: undefined}
51+
whitelist({id: 23, name: 'Darth', age: 42}, allowed, {omitDisallowed: true}); // resolves with {name: 'Darth', age: 42}
52+
whitelist({id: 23, name: 'Darth'}, allowed, {omitDisallowed: true}); // resolves with {name: 'Darth', age: undefined}
5053
// omitDisallowed can be combined with omitUndefined:
5154
whitelist({id: 23, name: 'Darth'}, allowed,
52-
{omitDisallowed: true, omitUndefined: true}); // returns {name: 'Darth'}
55+
{omitDisallowed: true, omitUndefined: true}); // resolves with {name: 'Darth'}
5356
```
5457

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

6568
## `whitelist(src, allowed, options)`
66-
* `src`: source object
67-
* `allowed`: an object that specifies which fields are allowed. The values can be
68-
* a boolean: if the value is `true`, the field is allowed and *copied* to the result object
69-
* an object: whitelist is called recursively (for nested objects)
70-
* a function `fn(value, path)`: the result of the function is placed in the result object
69+
* `src`: source object, array or primitive
70+
* `allowed`: the checks on `src` are performed according to this value. The following values are accepted:
71+
* an object `{key: value, ...}`:
72+
* expects `src` to be an object.
73+
* iterates over keys and uses the value for whitelisting the corresponding key/value pair in `src`
74+
* `value` can be any value that is accepted as the `allowed` parameter
75+
* an array with one element `[value]`:
76+
* expects `src` to be an array
77+
* iterates over elements of array `src` and whitelists according to `value`
78+
* `value` can be any value that is accepted as the `allowed parameter`
79+
* a function `fn(src, options)`:
80+
* should return the whitelisted `src` (directly or via a promise)
81+
* 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`)
82+
* a boolean: if the value is `true`, `src` is allowed and returned as the result
7183
* `options`: an object with the following optional keys:
7284
* `omitUndefined`: if set to `true`, it omits fields in the result whose values are undefined
73-
* `omitDisallowed`: if set to `true`, it omits fields from src that are not present in `allowed`
85+
* `omitDisallowed`: if set to `true`, it omits fields from src that are not present in `allowed`.
86+
* `data`: custom data that is recursively passed to any function in the `allowed` parameter.
7487

7588
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`).

package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
{
22
"name": "walter-whitelist",
3-
"version": "1.0.14",
3+
"version": "2.0.0-alpha.1",
44
"description": "Whitelist javascript objects",
55
"main": "src/index.js",
66
"scripts": {
7-
"coveralls": "isparta cover ./node_modules/mocha/bin/_mocha test && cat coverage/lcov.info | coveralls",
7+
"cover": "nyc --reporter=lcov npm test",
8+
"codecov": "npm run cover && codecov",
89
"lint": "eslint src test",
910
"test": "mocha test"
1011
},
@@ -34,15 +35,16 @@
3435
},
3536
"homepage": "https://github.com/paperhive/walter-whitelist#readme",
3637
"devDependencies": {
37-
"coveralls": "^2.11.13",
38+
"codecov": "^1.0.1",
3839
"eslint": "^3.5.0",
3940
"eslint-config-airbnb": "^11.0.0",
4041
"eslint-plugin-import": "^1.15.0",
41-
"isparta": "^4.0.0",
4242
"mocha": "^3.0.2",
43+
"nyc": "^8.3.0",
4344
"should": "^11.1.0"
4445
},
4546
"dependencies": {
47+
"co": "^4.6.0",
4648
"lodash": "^4.0.0"
4749
}
4850
}

src/index.js

Lines changed: 79 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const co = require('co');
12
const _ = require('lodash');
23
const util = require('util');
34

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

1718
/* check if obj is valid and return object with value 'undefined' for
1819
* missing keys in obj */
19-
function whitelist(src, allowed, _options, _path) {
20+
const whitelist = co.wrap(function* whitelist(src, allowed, _options) {
2021
// init default options
21-
const options = _.defaults(_options || {}, {
22+
const options = _.defaults({}, _options, {
2223
// ignore keys in `src` that are not whitelisted in the `allowed` obj
2324
// (otherwise a WhitelistError is thrown)
2425
omitDisallowed: false,
2526
// remove keys with undefined values
2627
// (values of keys which are in `allowed` obj but not in `src` are set to
2728
// undefined by default)
2829
omitUndefined: false,
30+
// path in allowed parameter (for recursive calls)
31+
path: '',
2932
});
3033

31-
// init path
32-
const path = _path || '';
33-
34-
// check input
35-
if (!_.isObject(src) || !_.isObject(allowed)) {
36-
throw new WhitelistError(
37-
`expected an object${(path ? ` at path ${path}` : '')}`
38-
);
34+
if (_.isBoolean(allowed)) {
35+
if (allowed) return src;
36+
if (options.omitDisallowed) return undefined;
37+
throw new WhitelistError('value not allowed', options.path);
3938
}
4039

41-
// check for extra keys
42-
if (!options.omitDisallowed) {
43-
const srcKeys = new Set(Object.keys(src));
44-
const allowedKeys = new Set(Object.keys(allowed));
45-
const disallowedKeys = setDifference(srcKeys, allowedKeys);
46-
if (disallowedKeys.size) {
47-
throw new WhitelistError(
48-
`The following fields are not allowed: ${
49-
Array.from(disallowedKeys).map(key => path + key).join(', ')
50-
}. Allowed fields: ${Array.from(allowedKeys).join(', ')}.`,
51-
disallowedKeys
52-
);
40+
if (_.isFunction(allowed)) {
41+
try {
42+
return yield Promise.resolve(allowed(src, options));
43+
} catch (error) {
44+
if (options.omitDisallowed) return undefined;
45+
throw error;
5346
}
5447
}
5548

56-
// construct new object
57-
const res = _.mapValues(allowed, (val, key) => {
58-
const currentPath = path + key;
49+
if (_.isArray(allowed)) {
50+
if (allowed.length !== 1) {
51+
throw new WhitelistError('allowed array not of length 1', options.path);
52+
}
53+
if (!_.isArray(src)) {
54+
throw new WhitelistError('src is not an array', options.path);
55+
}
56+
57+
const result = yield src.map(co.wrap(function* whitelistArray(el, index) {
58+
const arrayOptions = _.clone(options);
59+
arrayOptions.path += `[${index}]`;
60+
try {
61+
return yield whitelist(el, allowed[0], arrayOptions);
62+
} catch (error) {
63+
if (error instanceof WhitelistError && options.omitDisallowed) return undefined;
64+
throw error;
65+
}
66+
}));
5967

60-
// falsy: undefined
61-
if (!val) return undefined;
68+
// remove undefined (not using _.compact because it removes all falsy elements)
69+
if (options.omitUndefined) {
70+
const cleanResult = [];
71+
result.forEach((el) => {
72+
if (el !== undefined) cleanResult.push(el);
73+
});
74+
return cleanResult;
75+
}
6276

63-
// true: use full object
64-
if (val === true) return src[key];
77+
return result;
78+
}
6579

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

69-
// object: get whitelisted object recursively
70-
if (_.isObject(val)) {
71-
return whitelist(src[key] || {}, val, options, `${currentPath}.`);
83+
// check for extra keys
84+
const srcKeys = new Set(Object.keys(src));
85+
const allowedKeys = new Set(Object.keys(allowed));
86+
const disallowedKeys = Array.from(setDifference(srcKeys, allowedKeys));
87+
if (!options.omitDisallowed) {
88+
if (disallowedKeys.length > 0) {
89+
const key = disallowedKeys[0];
90+
const path = options.path ? `${options.path}.${key}` : key;
91+
throw new WhitelistError(
92+
`The following field is not allowed: ${path}. Allowed fields at ${options.path}: ${Array.from(allowedKeys).join(', ')}.`,
93+
path
94+
);
95+
}
7296
}
7397

74-
// unhandled value
75-
throw new Error(`unknown value in allowed object for key ${key}: ${val}`);
76-
});
98+
const result = yield _.mapValues(allowed, co.wrap(function* whitelistObject(value, key) {
99+
const objectOptions = _.clone(options);
100+
if (!objectOptions.path) objectOptions.path = key;
101+
else objectOptions.path += `.${key}`;
102+
try {
103+
return yield whitelist(src[key], value, objectOptions);
104+
} catch (error) {
105+
if (error instanceof WhitelistError && options.omitDisallowed) return undefined;
106+
throw error;
107+
}
108+
}));
77109

78-
// filter undefined values (if required by options)
79-
return options.omitUndefined ? _.pickBy(res, v => v !== undefined) : res;
80-
}
110+
// set disallowed fields to undefined if omitDisallowed is true
111+
// (useful for updating database with user-supplied data)
112+
if (options.omitDisallowed) disallowedKeys.forEach((k) => { result[k] = undefined; });
113+
114+
// filter undefined values (if required by options)
115+
return options.omitUndefined ? _.pickBy(result, v => v !== undefined) : result;
116+
}
117+
118+
throw new Error('allowed parameter type not recognized');
119+
});
81120

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

0 commit comments

Comments
 (0)