breadth-filter
Advanced tools
Comparing version 1.2.0 to 2.0.0
37
index.js
const entries = require('object.entries') | ||
function targetFor (source, destructive) { | ||
function defaultOnArray () { return [] } | ||
function defaultOnObject () { return {} } | ||
function targetFor (source, key, fieldPath, isNew, { | ||
onArray = defaultOnArray, | ||
onObject = defaultOnObject | ||
} = {}) { | ||
if (Array.isArray(source)) { | ||
return destructive ? source : [] | ||
} else if (typeof source === 'object') { | ||
return destructive ? source : {} | ||
return onArray(source, key, fieldPath, isNew) | ||
} else if (source !== null && typeof source === 'object') { | ||
return onObject(source, key, fieldPath, isNew) | ||
} | ||
} | ||
module.exports = function breadthFilter (root, fn, destructive) { | ||
const target = targetFor(root, destructive) | ||
module.exports = function breadthFilter (root, opts = {}) { | ||
const { onValue } = opts | ||
const target = targetFor(root, null, [], true, opts) | ||
if (!target) return root | ||
const queue = [[ root, target, [] ]] | ||
const seen = new Set() | ||
const seen = new Set([ root ]) | ||
let item | ||
@@ -23,13 +30,13 @@ | ||
const fieldPath = path.concat(key) | ||
const newTarget = targetFor(value, destructive) | ||
const isNew = !seen.has(value) | ||
if (isNew) seen.add(value) | ||
const newTarget = targetFor(value, key, fieldPath, isNew, opts) | ||
if (newTarget) { | ||
if (!seen.has(value)) { | ||
seen.add(value) | ||
} else { | ||
continue | ||
target[key] = newTarget | ||
if (isNew) { | ||
queue.push([ value, target[key], fieldPath ]) | ||
} | ||
target[key] = newTarget | ||
queue.push([ value, target[key], fieldPath ]) | ||
} else { | ||
target[key] = fn(value, key, fieldPath) | ||
target[key] = onValue(value, key, fieldPath) | ||
} | ||
@@ -36,0 +43,0 @@ } |
{ | ||
"name": "breadth-filter", | ||
"version": "1.2.0", | ||
"version": "2.0.0", | ||
"description": "Breadth-first deep object filter", | ||
@@ -5,0 +5,0 @@ "author": "Stephen Belanger <admin@stephenbelanger.com> (https://github.com/qard)", |
# breadth-filter | ||
Apply a deep object filter via breadth traversal. | ||
[![Greenkeeper badge](https://badges.greenkeeper.io/Qard/breadth-filter.svg)](https://greenkeeper.io/) | ||
Apply a deep object filter via breadth traversal. It allows replacing values, including retargeting objects and arrays and even detecting circular references to allow the user to decide how to handle the circle. Note that the filter will _not_ descend _into_ circular objects but will only _reach_ them. | ||
## Install | ||
@@ -16,3 +18,3 @@ | ||
const filtered = breadthFilter({ | ||
const data = { | ||
user: { | ||
@@ -22,3 +24,5 @@ name: 'someone', | ||
} | ||
}, (value, key, path) => { | ||
} | ||
function onValue (value, key, path) { | ||
if (key === 'password') { | ||
@@ -29,8 +33,71 @@ console.log('redacted field at', path.join('.')) | ||
return value | ||
} | ||
// build a new filtered object... | ||
const filtered = breadthFilter(data, { | ||
onValue | ||
}) | ||
// or, mutate in-place and break cycles... | ||
breadthFilter(data, { | ||
onValue, | ||
onObject (value, key, path, isNew) { | ||
return isNew ? value : '[Circular]' | ||
}, | ||
onArray (value, key, path, isNew) { | ||
return isNew ? value : '[Circular]' | ||
} | ||
}) | ||
``` | ||
## Options | ||
### onValue(value, key, path) | ||
This function handles primitive value types such as numbers or strings. It can be used to filter things like sensitive data such as passwords or credit card numbers. | ||
Arguments: | ||
* `value` - any | ||
The encountered value to filter. | ||
* `key` - string | number | ||
The property key of the encountered value, at the current depth level. Will be `null` when encountering the root object. | ||
* `path` - array<string> | ||
Any array of all property keys leading from the root to this encountered value. | ||
### onObject(value, key, path, isNew) | ||
This handles encountered objects. It can be used to filter entire objects out of the result, by returning `undefined`, it can enable in-place mutation by return the value directly, and the `isNew` property can be used to identify and break out of circular references, replacing them with something else such as `'[Circular]'`. | ||
Arguments: | ||
* `value` - any | ||
The encountered value to filter. | ||
* `key` - string | number | ||
The property key of the encountered value, at the current depth level. Will be `null` when encountering the root object. | ||
* `path` - array<string> | ||
Any array of all property keys leading from the root to this encountered value. | ||
* `isNew` - boolean | ||
Indicates if this is the first time encountering this value, a false value indicates a circular reference. | ||
### onArray(value, key, path, isNew) | ||
This handles encountered arrays. It can be used to filter entire arrays out of the result, by returning `undefined`, it can enable in-place mutation by return the value directly, and the `isNew` property can be used to identify and break out of circular references, replacing them with something else such as `'[Circular]'`. | ||
Arguments: | ||
* `value` - any | ||
The encountered value to filter. | ||
* `key` - string | number | ||
The property key of the encountered value, at the current depth level. Will be `null` when encountering the root object. | ||
* `path` - array<string> | ||
Any array of all property keys leading from the root to this encountered value. | ||
* `isNew` - boolean | ||
Indicates if this is the first time encountering this value, a false value indicates a circular reference. | ||
--- | ||
### Copyright (c) 2018 Stephen Belanger | ||
### Copyright (c) 2019 Stephen Belanger | ||
#### Licensed under MIT License | ||
@@ -37,0 +104,0 @@ |
90
test.js
@@ -9,2 +9,6 @@ const tap = require('tap') | ||
function identity (input) { | ||
return input | ||
} | ||
function whenKeyMatches (expected, fn) { | ||
@@ -22,2 +26,10 @@ return (value, key) => { | ||
// Mutation handlers | ||
function onObject (source) { | ||
return source | ||
} | ||
function onArray (source) { | ||
return source | ||
} | ||
tap.test('basic value mapping', (t) => { | ||
@@ -33,3 +45,5 @@ const input = { | ||
const output = breadthFilter(input, reverse) | ||
const output = breadthFilter(input, { | ||
onValue: reverse | ||
}) | ||
@@ -62,3 +76,5 @@ const expected = { | ||
const output = breadthFilter(input, whenKeyMatches('bar', reverse)) | ||
const output = breadthFilter(input, { | ||
onValue: whenKeyMatches('bar', reverse) | ||
}) | ||
@@ -91,3 +107,5 @@ const expected = { | ||
const output = breadthFilter(input, whenPathMatches('foo.bar.baz', reverse)) | ||
const output = breadthFilter(input, { | ||
onValue: whenPathMatches('foo.bar.baz', reverse) | ||
}) | ||
@@ -115,3 +133,5 @@ const expected = { | ||
const output = breadthFilter(input, whenPathMatches('foo.0.bar', reverse)) | ||
const output = breadthFilter(input, { | ||
onValue: whenPathMatches('foo.0.bar', reverse) | ||
}) | ||
@@ -139,3 +159,5 @@ const expected = { | ||
breadthFilter(input, reverse) | ||
breadthFilter(input, { | ||
onValue: reverse | ||
}) | ||
@@ -161,7 +183,11 @@ const expected = { | ||
}, | ||
bux: 'bax' | ||
bux: ['bax'] | ||
} | ||
} | ||
breadthFilter(input, reverse, true) | ||
breadthFilter(input, { | ||
onValue: reverse, | ||
onObject, | ||
onArray | ||
}) | ||
@@ -173,3 +199,3 @@ const expected = { | ||
}, | ||
bux: 'xab' | ||
bux: ['xab'] | ||
} | ||
@@ -193,5 +219,12 @@ } | ||
// Form a circular reference | ||
input.foo.input = input | ||
input.foo.foo = input.foo | ||
input.foo.bar.root = input | ||
breadthFilter(input, reverse, true) | ||
const output = breadthFilter(input, { | ||
onValue: reverse, | ||
onArray, | ||
onObject(source, key, path, isNew) { | ||
return isNew ? {} : '[Circular]' | ||
} | ||
}) | ||
@@ -201,13 +234,42 @@ const expected = { | ||
bar: { | ||
baz: 'zub' | ||
baz: 'zub', | ||
root: '[Circular]' | ||
}, | ||
bux: 'xab' | ||
bux: 'xab', | ||
foo: '[Circular]' | ||
} | ||
} | ||
// The expectation also needs a circular reference | ||
expected.foo.input = expected | ||
t.deepEqual(output, expected, 'matches expected output') | ||
t.end() | ||
}) | ||
tap.test('supports null and undefined values', (t) => { | ||
const input = { | ||
foo: { | ||
bar: { | ||
baz: null | ||
}, | ||
bux: undefined | ||
} | ||
} | ||
breadthFilter(input, { | ||
onValue: identity, | ||
onObject, | ||
onArray | ||
}) | ||
const expected = { | ||
foo: { | ||
bar: { | ||
baz: null | ||
}, | ||
bux: undefined | ||
} | ||
} | ||
t.deepEqual(input, expected, 'matches expected output') | ||
t.end() | ||
}) |
10410
5
263
107