obj-walker
Version:
Walk or map over objects in a depth-first preorder or postorder manner.
852 lines (715 loc) • 16.8 kB
Markdown
# obj-walker
Walk objects like this guy.
<img src="./chuck-norris.jpg" alt="Walker, Texas Ranger" width="200"/>
Map over an object in a preorder or postoder depth-first manner.
Also, provides functions for serializing and deserializng
self-referential objects using JSON pointer.
This library is designed to work well with functions that traverse
an object in the same way `JSON.stringify` and `JSON.parse` do. Namely,
preorder and postorder. To mimic that behavior entirely set the `jsonCompat`
option to `true`.
Custom traversal functions are supported for some functions. This allows you
to walk tree-like structures, such as a JSON schema, in a more efficient and
logical way. Prefer `walkEach` in these scenarios.
`map`, `walkEach`, `walkEachAsync`, `mapLeaves`, `compact`, and `truncate` support
the option `modifyInPlace` for in-place modification. Otherwise, the object is deep cloned.
```typescript
export interface MutationOption {
/** Set to true to modify the object instead of returning a new object. */
modifyInPlace?: boolean
}
```
## walker
```typescript
walker(obj: object, walkFn: WalkFn, options: Options = {}) => void
```
Generic walking fn that traverses an object in preorder (default) or postorder,
calling `walkFn` for each node. Can be used directly, but probably shouldn't.
```typescript
export interface Node {
key: string | undefined
val: any
parents: any[]
path: string[]
isLeaf: boolean
isRoot: boolean
}
export interface Options {
postOrder?: boolean
jsonCompat?: boolean
traverse?(x: any): any
}
```
```typescript
import { walker } from 'obj-walker'
const obj = {
a: {
b: 23,
c: 24,
},
d: {
e: 'Bob',
f: [10, 20, 30],
},
}
const nodes: Node[] = []
const walkFn = (node: Node) => {
nodes.push(node)
}
walker(obj, walkFn, options)
nodes
```
Returns an array of nodes. Note this is how `walk` works, so prefer
that fn.
```typescript
[
{
key: undefined,
parents: [],
val: { a: { b: 23, c: 24 }, d: { e: 'Bob', f: [10, 20, 30] } },
path: [],
isRoot: true,
isLeaf: false,
},
{
key: 'a',
val: { b: 23, c: 24 },
parents: [{ a: { b: 23, c: 24 }, d: { e: 'Bob', f: [10, 20, 30] } }],
path: ['a'],
isLeaf: false,
isRoot: false,
},
{
key: 'b',
val: 23,
parents: [
{ b: 23, c: 24 },
{ a: { b: 23, c: 24 }, d: { e: 'Bob', f: [10, 20, 30] } },
],
path: ['a', 'b'],
isLeaf: true,
isRoot: false,
},
...
]
```
## walk
```typescript
walk(obj: object, options: WalkOptions = {}) => Node[]
```
Walk an object. Returns an array of all nodes in the object in either
preorder or postorder.
```typescript
export interface WalkOptions extends Options {
leavesOnly?: boolean
}
```
```typescript
import { walk } from 'obj-walker'
const obj = {
a: {
b: 23,
c: 24,
},
d: {
e: 'Bob',
f: [10, 20, 30],
},
}
walk(obj).map((x) => x.path)
```
Produces:
```json
[
[],
["a"],
["a", "b"],
["a", "c"],
["d"],
["d", "e"],
["d", "f"],
["d", "f", "0"],
["d", "f", "1"],
["d", "f", "2"]
]
```
## walkEach
```typescript
walkEach(obj: object, walkFn: WalkFn, options: options?: WalkOptions & MutationOption) => object
```
```typescript
export type WalkFn = (node: Node) => void
```
Walk over an object calling `walkFn` for each node. The original
object is deep-cloned by default making it possible to simply mutate each
node as needed in order to transform the object. The cloned object
is returned if `options.modifyInPlace` is not set to true.
Below I want to walk a MongoDB JSON schema and set `additionalProperties` to `true`
wherever it exists. I traverse this tree using a custom `traverse` fn.
The original object is not modified.
```typescript
import { walkEach } from 'obj-walker'
const obj = {
bsonType: 'object',
additionalProperties: false,
required: ['name'],
properties: {
_id: {
bsonType: 'objectId',
},
name: { bsonType: 'string' },
addresses: {
bsonType: 'array',
items: {
bsonType: 'object',
additionalProperties: false,
properties: {
address: {
bsonType: 'object',
additionalProperties: false,
properties: {
zip: { bsonType: 'string' },
country: { bsonType: 'string' },
},
},
},
},
},
},
}
const traverse = (x: any) => x.properties || (x.items && { items: x.items })
const walkFn = ({ val }: Node) => {
if ('additionalProperties' in val) {
val.additionalProperties = true
}
}
walkEach(obj, walkFn, { traverse })
```
Produces:
```typescript
{
bsonType: 'object',
additionalProperties: true,
required: ['name'],
properties: {
_id: { bsonType: 'objectId' },
name: { bsonType: 'string' },
addresses: {
bsonType: 'array',
items: {
bsonType: 'object',
additionalProperties: true,
properties: {
address: {
bsonType: 'object',
additionalProperties: true,
properties: {
zip: { bsonType: 'string' },
country: { bsonType: 'string' },
},
},
},
},
},
},
}
```
## walkEachAsync
Like `walkEach` but awaits the promise returned by `walkFn` before proceeding to
the next node.
## map
Map over an object modifying values with a fn depth-first in a
preorder or postorder manner. The output of the mapper fn
will be traversed if possible when traversing preorder.
By default, nodes will be excluded by returning `undefined`.
Undefined array values will not be excluded. To customize
pass a fn for `options.shouldSkip`.
```typescript
map(obj: object, mapper: Mapper, options?: MapOptions & MutationOption) => object
```
```typescript
export type Mapper = (node: Node) => any
export type MapOptions = Omit<Options, 'traverse'> & {
shouldSkip?(val: any, node: Node): boolean
}
```
```typescript
import { map } from 'obj-walker'
const obj = {
a: {
b: 23,
c: 24,
},
d: {
e: 'Bob',
f: [10, null, 30, [31, undefined, 32], 40],
},
g: [25, '', { h: [null, 26, 27] }],
i: 'Frank',
}
map(obj, ({ val }) => (Array.isArray(val) ? _.compact(val) : val))
```
Produces:
```typescript
{
a: { b: 23, c: 24 },
d: { e: 'Bob', f: [10, 30, [31, 32], 40] },
g: [25, { h: [26, 27] }],
i: 'Frank',
}
```
Postorder
```typescript
const obj = {
bob: {
scores: ['87', 'x97', 95, false],
},
joe: {
scores: [92, 92.5, '73.2', ''],
},
frank: {
scores: ['abc', ''],
},
}
const result = map(
obj,
({ val, isLeaf }) => {
if (isLeaf) {
return parseFloat(val)
}
return Array.isArray(val) ? _.compact(val) : val
},
{ postOrder: true }
)
```
Produces:
```typescript
{
bob: { scores: [87, 95] },
joe: { scores: [92, 92.5, 73.2] },
frank: { scores: [] },
}
```
Custom `shouldSkip` fn
```typescript
const obj = {
bob: {
scores: ['87', 'x97', 95, false],
},
joe: {
scores: [92, 92.5, '73.2', ''],
},
frank: {
scores: ['abc', ''],
},
}
const shouldSkip = (val: any, node: Node) =>
_.isEmpty(val) && !parentIsArray(node)
const result = map(
obj,
({ val, isLeaf }) => {
if (isLeaf) {
return parseFloat(val)
}
return Array.isArray(val) ? _.compact(val) : val
},
{ postOrder: true, shouldSkip }
)
```
Produces:
```typescript
{
bob: { scores: [87, 95] },
joe: { scores: [92, 92.5, 73.2] },
}
```
## mapLeaves
```typescript
mapLeaves(obj: object, mapper: Mapper, options?: MapOptions) => object
```
Map over the leaves of an object with a fn. By default, nodes will be excluded
by returning `undefined`. Undefined array values will not be excluded. To customize
pass a fn for `options.shouldSkip`.
```typescript
import { mapLeaves } from 'obj-walker'
const obj = {
a: {
b: 23,
c: 24,
},
d: {
e: 100,
f: [10, 20, 30],
},
}
mapLeaves(obj, ({ val }) => val + 1)
```
Produces:
```typescript
{
a: { b: 24, c: 25 },
d: { e: 101, f: [11, 21, 31] },
}
```
## findNode
```typescript
findNode(obj: object, findFn: FindFn, options?: Options) => Node | undefined
```
Search for a node and short-circuit the tree traversal if it's found.
```typescript
import { findNode } from 'obj-walker'
const obj = {
name: 'Joe',
address: {
city: 'New York',
state: 'NY',
zipCode: '10001',
},
likes: ['Stock Market', 'Running'],
}
findNode(obj, (node) => {
return _.isEqual(node.path, ['address', 'zipCode'])
})
```
Produces:
```typescript
{
key: 'zipCode',
val: '10001',
parents: [
{ city: 'New York', state: 'NY', zipCode: '10001' },
{
name: 'Joe',
address: { city: 'New York', state: 'NY', zipCode: '10001' },
likes: ['Stock Market', 'Running'],
},
],
path: ['address', 'zipCode'],
isLeaf: true,
isRoot: false,
}
```
## flatten
```typescript
flatten(obj: object, options?: WalkOptions & FlattenOptions) => object
```
```typescript
interface FlattenOptions {
/** Defaults to '.' */
separator?: string
/** Flatten objects and not arrays */
objectsOnly?: boolean
}
```
Flatten an object's keys. Optionally pass `separator` to determine
what character to join keys with. Defaults to '.'. If an array is
passed, an object of path to values is returned unless the `objectsOnly`
option is set.
```typescript
import { flatten } from 'obj-walker'
const obj = {
a: {
b: 23,
c: 24,
},
d: {
e: 100,
f: [10, 20, 30],
},
}
flatten(obj)
```
Produces:
```typescript
{
'a.b': 23,
'a.c': 24,
'd.e': 100,
'd.f.0': 10,
'd.f.1': 20,
'd.f.2': 30,
}
```
## unflatten
```typescript
unflatten(obj: object, options?: UnflattenOptions) => object
```
```typescript
interface UnflattenOptions {
/** Defaults to '.' */
separator?: string | RegExp
}
```
Unflatten an object previously flattened. Optionally pass `separator`
to determine what character or RegExp to split keys with.
Defaults to '.'.
```typescript
import { unflatten } from 'obj-walker'
const obj = {
'a.b': 23,
'a.c': 24,
'd.e': 100,
'd.f.0': 10,
'd.f.1': 20,
'd.f.2.g': 30,
'd.f.2.h.i': 40,
}
unflatten(obj)
```
Produces:
```typescript
{
a: {
b: 23,
c: 24,
},
d: {
e: 100,
f: [10, 20, { g: 30, h: { i: 40 } }],
},
}
```
## compact
```typescript
compact(obj: object, options: CompactOptions & MutationOption) => object
```
```typescript
interface CompactOptions {
removeUndefined?: boolean
removeNull?: boolean
removeEmptyString?: boolean
removeFalse?: boolean
removeNaN?: boolean
removeEmptyObject?: boolean
removeEmptyArray?: boolean
compactArrays?: boolean
removeFn?: (val: any, node: Node) => boolean
}
```
Compact an object, removing fields recursively according to the supplied options.
All option flags are `false` by default. If `compactArrays` is set to `true`, arrays
will be compacted based on the enabled remove option flags.
```typescript
const obj = {
a: {
b: [null, null, 21, '', { b1: null }, { b2: 26 }],
},
c: [],
d: [42, null],
e: {
f: {
g: '',
h: undefined,
i: 'null',
},
},
}
const result = compact(obj, {
removeUndefined: true,
removeEmptyString: true,
removeNull: true,
compactArrays: true,
removeEmptyArray: true,
removeEmptyObject: true,
removeFn: (val: any) => val === 'null',
})
```
Produces:
```typescript
{
a: { b: [21, { b2: 26 }] },
d: [42],
}
```
## truncate
```typescript
truncate(obj: object, options: TruncateOptions & MutationOption) => object
```
```typescript
export interface TruncateOptions {
/** Max allowed depth of objects/arrays. Default to Infinity */
maxDepth?: number
/** What to replace an object/array at the maximum depth with. Defaults to '[Truncated]' */
replacementAtMaxDepth?: any
/** Max allowed length of a string. Defaults to Infinity */
maxStringLength?: number
/** What to replace the last characters of the truncated string with. Defaults to '...' */
replacementAtMaxStringLength?: string
/** Max allowed length of an array. Defaults to Infinity */
maxArrayLength?: number
/** Transform instances of Error into plain objects so that truncation can be performed. Defautls to false */
transformErrors?: boolean
}
```
Truncate allows you to limit the depth of nested objects/arrays,
the length of strings, and the length of arrays. Instances of Error
can be converted to plain objects so that the enabled truncation options
also apply to the error fields. All truncation methods are opt-in.
Note: For the best performance you should consider setting `modifyInPlace`
to `true`.
```typescript
const obj = {
a: {
b: 'Frank',
c: {
d: 'Joe',
},
e: null,
},
f: 42,
}
truncate(obj, { depth: 2 })
```
Produces:
```typescript
{
a: {
b: 'Frank',
c: '[Truncated]',
e: null,
},
f: 42,
}
```
## size
```typescript
size(val: any) => number
```
Estimate the size in bytes.
```typescript
const obj = {
a: {
b: 'hello',
},
c: Symbol('hello'),
d: {
e: [true, false],
},
f: [42, 10n],
}
size(obj)
// 44
```
## exclude
```typescript
exclude(obj: object, paths: string[], options?: MutationOption) => object
```
Exclude paths from an object. Supports star patterns where `*` matches
any single field name. For example, `documents.*.fileName` will match
`documents.0.fileName`, `documents.1.fileName`, etc.
Also supports prefix matching: excluding `documents` will exclude
`documents.fileName`, `documents.0.fileName`, etc.
```typescript
import { exclude } from 'obj-walker'
const obj = {
name: 'John',
age: 30,
documents: [
{ fileName: 'doc1.pdf', size: 1024 },
{ fileName: 'doc2.pdf', size: 2048 },
],
address: { street: '123 Main St', city: 'New York' },
}
exclude(obj, ['documents.*.fileName', 'age'])
```
Produces:
```typescript
{
name: 'John',
documents: [{ size: 1024 }, { size: 2048 }],
address: { street: '123 Main St', city: 'New York' },
}
```
## Helper fns
These helper fns are exported for your convenience.
```typescript
export const isObjectOrArray = _.overSome([_.isPlainObject, _.isArray])
export const defShouldSkip = (val: any, node: Node) =>
val === undefined && !parentIsArray(node)
export const parentIsArray = (node: Node) => {
const parent = node.parents[0]
return Array.isArray(parent)
}
export const defTraverse = (x: any) => isObjectOrArray(x) && !_.isEmpty(x) && x
```
## addRefs
```typescript
addRefs(obj: object, options?: RefOptions): object
```
Replace duplicate objects refs with pointers to the first
object seen.
```typescript
import { addRefs } from 'obj-walker'
const apiOutput = {
1: 'foo',
2: 'bar',
3: 'baz',
}
const detailsOutput = {
1: 'bla',
2: 'bla',
3: 'bla',
}
const obj = {
api: {
input: [1, 2, 3],
output: apiOutput,
},
details: {
input: apiOutput,
output: detailsOutput,
},
writeToDB: {
input: detailsOutput,
},
}
addRefs(obj)
```
Produces:
```typescript
{
api: {
input: [1, 2, 3],
output: { '1': 'foo', '2': 'bar', '3': 'baz' },
},
details: {
input: { $ref: '#/api/output' },
output: { '1': 'bla', '2': 'bla', '3': 'bla' },
},
writeToDB: { input: { $ref: '#/details/output' } },
}
```
## deref
```typescript
deref(obj: object, options?: RefOptions): object
```
Rehydrate objects by replacing refs with actual objects.
```typescript
import { deref } from 'obj-walker'
const obj = {
api: {
input: [1, 2, 3],
output: { '1': 'foo', '2': 'bar', '3': 'baz' },
},
details: {
input: { $ref: '#/api/output' },
output: { '1': 'bla', '2': 'bla', '3': 'bla' },
},
writeToDB: { input: { $ref: '#/details/output' } },
}
deref(obj)
```
Produces:
```typescript
{
api: {
input: [1, 2, 3],
output: { '1': 'foo', '2': 'bar', '3': 'baz' },
},
details: {
input: { '1': 'foo', '2': 'bar', '3': 'baz' },
output: { '1': 'bla', '2': 'bla', '3': 'bla' },
},
writeToDB: { input: { '1': 'bla', '2': 'bla', '3': 'bla' } },
}
```