A powerful JSON path processor with no third-party dependencies.
Read, write, delete, compare, search and flatten JavaScript object
trees using a simple dot-delimited path format e.g. "user.address.city".
- Get, set and delete deeply nested values, including inside arrays
- Push, pull and splice array values at any path
- Immutable versions of every write operation with structural sharing, ideal for React, Redux and other state management systems
- Diff and compare objects, find the paths to matching data and flatten objects into paths
- Safe by default: protected against prototype pollution
- TypeScript types included, zero runtime dependencies
- Install
- Quick Start
- Paths
- Behaviour
- Security
- API Reference
- Reading Data: get(), getMany(), values(), furthest(), distill(), traverse(), query()
- Writing Data: set(), setImmutable(), unSet(), unSetImmutable(), update(), updateImmutable(), pushVal(), pushValImmutable(), pullVal(), pullValImmutable(), splicePath(), merge(), mergeImmutable(), decouple()
- Comparing and Searching: diff(), diffValues(), isEqual(), isNotEqual(), match(), findPath(), findOnePath(), hasMatchingPathsInObject(), countMatchingPathsInObject()
- Inspecting Structure: flatten(), flattenValues(), leafNodes(), countLeafNodes(), type()
- Working With Path Strings: join(), joinEscaped(), escape(), unEscape(), split(), clean(), chop(), up(), down(), pop(), shift(), push(), isCompositePath(), isNonCompositePath()
- Helpers: keyDedup(), returnWhatWasGiven(), wildcardToZero(), numberToWildcard()
- TypeScript
- Upgrading
- Contributing
- License
npm i @irrelon/pathThe package is published as CommonJS compiled to ES2015 and works in Node.js and in browsers via any bundler. TypeScript type definitions are included.
const {get, set, setImmutable, unSet, diff} = require("@irrelon/path");
const user = {
"name": "Jim",
"address": {
"city": "London"
},
"roles": ["admin", "editor"]
};
console.log(get(user, "address.city")); // Logs: London
console.log(get(user, "roles.1")); // Logs: editor
console.log(get(user, "address.postcode", "Unknown")); // Logs: Unknown
// Set a value, creating any missing objects on the way
set(user, "address.postcode", "N1 9GU");
console.log(user.address.postcode); // Logs: N1 9GU
// Remove a value
unSet(user, "address.postcode");
console.log("postcode" in user.address); // Logs: false
// Get a new object with the change, leaving the original untouched
const renamed = setImmutable(user, "name", "James");
console.log(user.name); // Logs: Jim
console.log(renamed.name); // Logs: James
console.log(renamed.address === user.address); // Logs: true
// Find what changed
console.log(diff(user, renamed)); // Logs: ["name"]With ES modules or TypeScript:
import {get, set} from "@irrelon/path";A path is a string of keys separated by periods. Array elements are addressed by their index like any other key.
const {get} = require("@irrelon/path");
const obj = {
"users": [{
"name": "Jim",
"pets": [{"name": "Rex"}]
}]
};
console.log(get(obj, "users.0.name")); // Logs: Jim
console.log(get(obj, "users.0.pets.0.name")); // Logs: Rex- A leading period is ignored, so
".users.0"is the same as"users.0"(see clean()). - An empty path
""refers to the object itself when reading. Write functions like set() do nothing when given an empty path, while pushVal(), pullVal() and splicePath() treat it as "the passed object is the array to operate on". $is used as an array wildcard by some options, see get()wildcardExpansion, furthest() and wildcardToZero().
Keys such as email addresses or domain names contain periods that would otherwise be read as path delimiters:
const {get, escape, joinEscaped} = require("@irrelon/path");
const obj = {
"users": {
"test@test.com": {
"name": "My Test User"
}
}
};
// Without escaping, this looks for "test@test" then "com"
console.log(get(obj, "users.test@test.com.name")); // Logs: undefined
// Escape the key to use it as a single path part
console.log(get(obj, `users.${escape("test@test.com")}.name`)); // Logs: My Test User
// Or escape and join several keys at once
console.log(get(obj, joinEscaped("users", "test@test.com", "name"))); // Logs: My Test UserThe escaping rules are:
\.is a literal period inside a key.\\is a literal backslash inside a key, so keys containing backslashes (e.g.C:\temp) can be escaped safely with escape().- A backslash followed by any other character is kept as it is, so
"C:\temp"still refers to a key namedC:\temp.
Every path returned by this library is escaped. Paths from flatten(), flattenValues(), leafNodes(), diff(), diffValues(), findPath(), findOnePath(), traverse() and query() can always be passed straight back to get(), set() and the other functions:
const {flatten, get, unEscape, split} = require("@irrelon/path");
const obj = {"domains": {"www.google.com": {"visits": 3}}};
const paths = flatten(obj);
console.log(paths); // Logs: ["domains", "domains.www\\.google\\.com", "domains.www\\.google\\.com.visits"]
console.log(get(obj, paths[2])); // Logs: 3
// Use split() and unEscape() to get the original keys back
console.log(split(paths[2]).map(unEscape)); // Logs: ["domains", "www.google.com", "visits"]If data or an object to traverse does not exist, read functions return
undefined (or your default value) and write functions either create the
missing path or do nothing. They do not throw:
const {get, unSet, pullVal, furthest} = require("@irrelon/path");
const obj = {
"foo": null
};
console.log(get(obj, "foo.bar.one")); // Logs: undefined
console.log(furthest(obj, "foo.bar.one")); // Logs: foo
console.log(unSet(obj, "foo.bar.one") === obj); // Logs: true
console.log(pullVal(obj, "missing.array", "value") === obj); // Logs: trueErrors are only thrown for programming mistakes such as passing a path that is not a string, or asking pushVal(), pullVal() or splicePath() to operate on an existing value that is not an array.
Traversal stops at values that are not objects or arrays, so a path never reads properties from inside strings, numbers or functions:
const {get} = require("@irrelon/path");
console.log(get({"name": "Jim"}, "name.length")); // Logs: undefinedget() and getMany() accept a default value that is
returned when the value at the path is undefined. A null value is a
real value, so the default is not used:
const {get} = require("@irrelon/path");
const obj = {
"foo": null
};
console.log(get(obj, "foo")); // Logs: null
console.log(get(obj, "foo.bar.one", "My Default Value")); // Logs: My Default Value
console.log(get(obj, "missing", 0)); // Logs: 0When set(), update() or pushVal() need to
create part of a path, they create an array if the next key is an
array index (0, 1, 2...) and an object otherwise. Keys such as
"-1", "01" or "1.5" are not array indexes, so they create objects.
const {set} = require("@irrelon/path");
console.log(set({}, "users.0.name", "Jim")); // Logs: {"users": [{"name": "Jim"}]}
console.log(set({}, "grid.0.1", "x")); // Logs: {"grid": [[null, "x"]]}
console.log(set({}, "scores.-1", 10)); // Logs: {"scores": {"-1": 10}}The second example logs
nullbecause JSON cannot show the empty slot at index0of the inner array.
If a value in the middle of the path exists but is not an object or
array (e.g. a string or null), set() replaces it with a new
object or array so the value can be set:
const {set} = require("@irrelon/path");
console.log(set({"profile": null}, "profile.age", 42)); // Logs: {"profile": {"age": 42}}By default write functions modify the object you pass in and return it. Every write function also has an immutable mode that leaves the original untouched and returns a new object instead:
| Mutable | Immutable |
|---|---|
| set() | setImmutable() |
| unSet() | unSetImmutable() |
| update() | updateImmutable() |
| pushVal() | pushValImmutable() |
| pullVal() | pullValImmutable() |
| splicePath() | splicePath(..., {immutable: true}) |
| merge() | mergeImmutable() |
The immutable versions are the same as passing {immutable: true} in the
options of the mutable version.
Immutable operations use structural sharing: only the objects and
arrays on the path that changed are copied, everything else keeps the
same reference. This means you can detect exactly what changed with a
cheap === check, which is what React, Redux and similar libraries rely
on:
const {setImmutable} = require("@irrelon/path");
const state = {
"user": {"name": "Jim", "settings": {"theme": "dark"}},
"todos": [{"title": "Write docs"}]
};
const newState = setImmutable(state, "user.name", "James");
console.log(newState === state); // Logs: false
console.log(newState.user === state.user); // Logs: false
console.log(newState.user.settings === state.user.settings); // Logs: true
console.log(newState.todos === state.todos); // Logs: true
console.log(state.user.name); // Logs: JimWhen an immutable operation has nothing to change (e.g. unsetting a key
that doesn't exist, or pulling a value that isn't in the array), the
original object is returned so === comparisons stay true.
When copying, the library keeps the type of each value:
- Plain objects and arrays are shallow copied.
- Class instances keep their prototype, so their methods still work.
Date,RegExp,MapandSetinstances are copied with their data.- Objects without a prototype (
Object.create(null)) stay without one. - Primitives and functions are used as they are.
See decouple() for the function that does this.
Many functions accept an optional options object. The library never
modifies an options object you pass in, so it is safe to reuse one
between calls.
| Option | Type | Used by | Description |
|---|---|---|---|
immutable |
boolean |
set(), unSet(), update(), pushVal(), pullVal(), splicePath(), merge(), decouple() | Leave the original untouched and return a new object. See Mutable and Immutable Operations. |
strict |
boolean |
pullVal() | Defaults to true. When false, the first array item matching the structure of the value is removed (see match()). |
transformKey |
function |
get(), set(), unSet(), furthest(), values(), flatten(), flattenValues(), traverse() | Called with each key in the path (after unescaping) and returns the key to actually use. |
transformRead |
function |
flatten(), flattenValues(), traverse() | Called with each object before it is read and returns the object to read from. |
transformWrite |
function |
flattenValues() | Called with each value before it is written to the result. |
leavesOnly |
boolean |
flattenValues() | Only include leaf values, not the objects and arrays containing them. |
ignore |
RegExp |
flatten() | Keys matching the expression (and everything inside them) are skipped. |
wildcardExpansion |
boolean |
get(), getMany() | Treat $ in a path as "every item in this array". |
arrayTraversal |
boolean |
get(), getMany() | Look inside arrays automatically without an index in the path. |
arrayExpansion |
boolean |
get(), getMany() | With arrayTraversal, return every matching value instead of the first. |
pathData |
object |
get(), getMany() | Pass {directPaths: []} to receive the concrete path of each value found by array expansion. |
Prototype pollution protection. Paths and keys are frequently built
from user input, such as the body of an HTTP request. This library makes
sure that data can never be written to Object.prototype (or any other
prototype) through it:
- Writes to a
__proto__key or through a__proto__path part are ignored by every write function: set(), unSet(), update(), pushVal(), pullVal(), splicePath(), merge() and their immutable versions. This includes keys produced by atransformKeyoption. - Write functions never traverse into inherited properties or functions,
so paths like
constructor.prototype.xcannot reach a prototype. Where needed, an own property is created instead. __proto__keys that exist in your data as own properties (which is whatJSON.parse()produces) are treated as normal data. Copies made by immutable operations and the objects returned by flattenValues(), diffValues(), distill(), values(), query() and countMatchingPathsInObject() keep them as own properties and never change their prototype.- Data containing a key named
hasOwnProperty, and objects created withObject.create(null), are handled safely.
const {merge, set} = require("@irrelon/path");
const payload = JSON.parse('{"__proto__": {"isAdmin": true}, "constructor": {"prototype": {"isAdmin": true}}}');
merge({}, payload);
set({}, "__proto__.isAdmin", true);
set({}, "constructor.prototype.isAdmin", true);
console.log({}.isAdmin); // Logs: undefinedRecommendations for untrusted input. The library is safe to use with
untrusted paths and data, but you should still validate them for your
application's own rules. For example, decide which paths a user is
allowed to change rather than accepting any path. Also be aware that a
path with a very large array index (e.g. "items.4000000000") creates a
sparse array with a huge length. This library copies such arrays
efficiently, but other code such as JSON.stringify() will try to
output every empty slot.
Reporting a vulnerability. Please report security issues privately through GitHub's private vulnerability reporting rather than opening a public issue.
Security history.
| Advisory | Affected | Fixed in | Description |
|---|---|---|---|
Prototype pollution in merge() |
6.2.2 to 7.0.2 | 7.0.3, 8.0.0 | merge() and mergeImmutable() did not ignore __proto__ keys or inherited properties in the merged data. Related hardening was applied to pushVal(), pullVal(), splicePath(), the immutable copy functions and functions that build result objects. Reported by Vishal Suwalka. |
| CVE-2020-7708 | Before 4.7.0 | 4.7.0 | Prototype pollution in set(), unSet(), pushVal() and pullVal(). |
All functions are named exports:
const {get, set} = require("@irrelon/path");Some functions accept extra trailing parameters such as objCache,
parentPath or tracking that are documented as internal. They are used
when the function calls itself and should not be passed.
get(obj, path, defaultVal?, options?)
Gets a value from obj at the given path. If the value is undefined,
returns defaultVal.
Changed in 8.0.0
- An array of paths now returns the values. It previously returned an array of
undefined.- Paths with a single key now apply
transformKeyand unescaping, e.g.get({"a.b": 1}, "a\\.b")now returns1instead ofundefined.- The options object is no longer modified. Reusing an options object between calls no longer mixes results from previous calls, and
pathData.directPathsis now correct for paths with several parts.- A path that is not a string throws
"Path argument must be a string"instead of aTypeError.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj | Object or Array |
Yes | The object to read from. If null or undefined, defaultVal is returned. |
|
| path | string or string[] |
Yes | The path to read. An empty path returns obj. An array of paths returns an array of values. |
|
| defaultVal | any |
No | undefined |
Returned when the value at the path is undefined. |
| options | Object |
No | {} |
Supports transformKey, wildcardExpansion, arrayTraversal, arrayExpansion and pathData. See Options. |
Returns the value at the path, or defaultVal.
Throws if path is not a string or an array of strings.
const {get} = require("@irrelon/path");
const obj = {
"myArr": [
"hello",
{"bar": "goodbye"}
],
"user": {"name": "Jim", "age": 22}
};
console.log(get(obj, "myArr.0")); // Logs: hello
console.log(get(obj, "myArr.1.bar")); // Logs: goodbye
console.log(get(obj, "user.missing", "default")); // Logs: default
console.log(get(obj, ["user.name", "user.age"])); // Logs: ["Jim", 22]With arrayTraversal, arrays in the middle of a path are searched
automatically and the first defined value is returned. Add
arrayExpansion to get every value instead:
const {get} = require("@irrelon/path");
const obj = {
"orders": [
{"items": [{"sku": "A1"}, {"sku": "B2"}]},
{"items": [{"sku": "C3"}]}
]
};
console.log(get(obj, "orders.items.sku", undefined, {arrayTraversal: true})); // Logs: A1
console.log(get(obj, "orders.items.sku", undefined, {arrayTraversal: true, arrayExpansion: true})); // Logs: ["A1", "B2", "C3"]To find out where each value came from, pass a pathData object. Its
directPaths array receives the concrete path of each value:
const {get} = require("@irrelon/path");
const obj = {
"orders": [
{"items": [{"sku": "A1"}, {"sku": "B2"}]},
{"items": [{"sku": "C3"}]}
]
};
const pathData = {"directPaths": []};
get(obj, "orders.items.sku", undefined, {arrayTraversal: true, arrayExpansion: true, pathData});
console.log(pathData.directPaths); // Logs: ["orders.0.items.0.sku", "orders.0.items.1.sku", "orders.1.items.0.sku"]With wildcardExpansion, a $ in the path means "every item in this
array". The result is an array of every value found:
const {get} = require("@irrelon/path");
const obj = {
"teams": [
{"members": [{"name": "Ann"}, {"name": "Bob"}]},
{"members": [{"name": "Cat"}]}
]
};
console.log(get(obj, "teams.$.members.$.name", undefined, {wildcardExpansion: true})); // Logs: ["Ann", "Bob", "Cat"]
console.log(get(obj, "teams.$.members", undefined, {wildcardExpansion: true})); // Logs: [[{"name": "Ann"}, {"name": "Bob"}], [{"name": "Cat"}]]getMany(data, path, defaultVal?, options?)
Like get() but always returns an array. If data is an array,
the path is read from every item in it. If the first key of the path
points to an array inside an item, every element of that array is
included too.
Changed in 8.0.0
nulland primitive items in an array are skipped instead of throwing aTypeError.- An escaped first key now finds arrays nested inside each item.
- The options object is no longer modified, and
pathData.directPathsis now correct for paths with several parts.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| data | Object or Array |
Yes | The object or array to read from. | |
| path | string |
Yes | The path to read from data or from each item. |
|
| defaultVal | any |
No | undefined |
Used in place of values that are undefined. |
| options | Object |
No | {} |
The same options as get(). |
Returns an array of values. If nothing is found, returns [], or
[defaultVal] when a default value is given.
const {getMany} = require("@irrelon/path");
console.log(getMany({"user": {"name": "Jim"}}, "user.name")); // Logs: ["Jim"]
console.log(getMany([{"name": "Jim"}, {"name": "Ann"}, {"age": 3}], "name")); // Logs: ["Jim", "Ann"]
console.log(getMany([{"tags": ["a", "b"]}, {"tags": ["c"]}], "tags")); // Logs: ["a", "b", "c"]
console.log(getMany([], "name", "Nobody")); // Logs: ["Nobody"]values(obj, path, options?)
Walks the path and returns the value at every step of it, keyed by the path to that step.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj | Object or Array |
Yes | The object to read from. | |
| path | string |
Yes | The path to walk. | |
| options | Object |
No | {} |
Supports transformKey. |
Returns an object where each key is a path and each value is the value at that path.
const {values} = require("@irrelon/path");
const obj = {"user": {"address": {"city": "London"}}};
console.log(values(obj, "user.address.city"));Outputs:
{
"user": {"address": {"city": "London"}},
"user.address": {"city": "London"},
"user.address.city": "London"
}furthest(obj, path, options?)
Finds how far along path the data exists and returns the path to the
deepest value that is not undefined.
Changed in 8.0.0
- No longer throws a
TypeErrorwhen the path reachesnull.- Paths with a single key now apply unescaping and
transformKey, e.g.furthest(["a"], "$")now returns"$"instead of"".- Stops at primitive values instead of reading their properties, e.g.
furthest({a: "str"}, "a.length")now returns"a"instead of"a.length".- A path that is not a string throws
"Path argument must be a string"instead of aTypeError.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj | Object or Array |
Yes | The object to read from. | |
| path | string |
Yes | The path to follow. | |
| options | Object |
No | {} |
Supports transformKey, which defaults to wildcardToZero() so that $ matches the first array item. |
Returns the longest part of path that exists, or "" if none of it
exists.
Throws if path is not a string.
const {furthest} = require("@irrelon/path");
const myData = {
"profile": {
"id": 1,
"name": "Ron Swanson"
},
"posts": [{"title": "Hello"}]
};
console.log(furthest(myData, "profile.id.bson")); // Logs: profile.id
console.log(furthest(myData, "posts.$.title.length")); // Logs: posts.$.title
console.log(furthest(myData, "missing.path")); // Logs:distill(obj, pathArr)
Gets the value of each path in pathArr and returns them as an object
keyed by path.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj | Object or Array |
Yes | The object to read from. | |
| pathArr | string[] |
Yes | The paths to read. |
Returns an object where each key is one of the paths and each value is
the value at that path (or undefined).
const {distill} = require("@irrelon/path");
const obj = {
"user": {
"firstName": "Jim",
"lastName": "Jones",
"age": 22
}
};
console.log(distill(obj, ["user.firstName", "user.lastName"])); // Logs: {"user.firstName": "Jim", "user.lastName": "Jones"}traverse(obj, path, operation, options?)
Calls operation for every step of path. When a step is an array, the
operation is called for each item in the array and the rest of the path
is followed inside every item.
Changed in 8.0.0
- Reported paths are escaped.
- Stops at missing or primitive values. The operation was previously called for the rest of the path with incorrect paths, e.g.
"b"instead of"a.b".- Single-key paths containing an escaped period now read the right key.
optionsare now applied when an array of paths is passed and inside array items.- A path that is not a string throws
"Path argument must be a string"instead of aTypeError.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj | Object or Array |
Yes | The object to walk. | |
| path | string or string[] |
Yes | The path to walk. An array of paths walks each of them in turn. | |
| operation | function |
Yes | Called with {purePath, flatPath, key, value} for each step. |
|
| options | Object |
No | {} |
Supports transformKey and transformRead. |
The operation function receives an object with:
| Property | Description |
|---|---|
purePath |
The escaped path to this value including array indexes, e.g. "teams.0.name". |
flatPath |
The escaped path without array indexes, e.g. "teams.name". |
key |
The key of this value in its parent. |
value |
The value at this step, or undefined if it doesn't exist. |
Walking stops at a value that is missing or not an object, because the rest of the path cannot exist inside it.
Throws if path is not a string or an array of strings.
const {traverse} = require("@irrelon/path");
const obj = {
"teams": [
{"name": "Red"},
{"name": "Blue"}
]
};
const visited = [];
traverse(obj, "teams.name", ({purePath, flatPath}) => {
visited.push(`${purePath} (${flatPath})`);
return true;
});
console.log(visited);Outputs:
[
"teams (teams)",
"teams.0 (teams)",
"teams.0.name (teams.name)",
"teams.1 (teams)",
"teams.1.name (teams.name)"
]query(obj, query)
Finds the paths to every value that matches a set of criteria. Each key
of query is a path (array indexes are not needed, arrays are searched
automatically) and each value describes the criteria for that path.
Changed in 8.0.0
- Returned paths are escaped, and query paths containing escaped periods now match.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj | Object |
Yes | The object to search. | |
| query | Object |
Yes | The query, see below. |
Supported operators:
| Operator | Value | Matches when |
|---|---|---|
$in |
An array of values and/or functions, or a single function | The value is strictly equal (===) to one of the values, or one of the functions returns true for it. |
Returns an object with the same keys as query, each containing an
array of the escaped paths to the matching values.
const {query} = require("@irrelon/path");
const obj = {
"users": [
{"name": "Jim", "role": "admin"},
{"name": "Ann", "role": "user"},
{"name": "", "role": "admin"}
]
};
const result = query(obj, {
"users.role": {"$in": ["admin"]},
"users.name": {"$in": [(name) => name.length === 0]}
});
console.log(result);Outputs:
{
"users.role": ["users.0.role", "users.2.role"],
"users.name": ["users.2.name"]
}set(obj, path, val, options?)
Sets val in obj at the given path, creating any missing objects or
arrays along the way (see
How set() Creates Missing Path Parts).
This modifies obj unless options.immutable is true. Use
setImmutable() for an immutable update.
Changed in 8.0.0
set({}, "arr.0.name", "x")now creates{arr: [{name: "x"}]}. It previously created an array inside the array and the value was lost.- Negative numbers such as
"-1"now create objects instead of arrays. Other keys that are not canonical array indexes, such as"01", already created objects.- An escaped key in the middle of a path no longer creates a stray key containing the backslash, and no longer replaces existing data inside the escaped key.
- A path that is not a string throws
"Path argument must be a string"instead of aTypeError.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj | Object or Array |
Yes | The object to set the value in. | |
| path | string |
Yes | The path to set. | |
| val | any |
Yes | The value to set. | |
| options | Object |
No | {} |
Supports immutable and transformKey. |
Returns obj (or the new object when immutable). Returns undefined
and does nothing if obj is not an object or path is empty.
Throws if path is not a string.
const {set, get} = require("@irrelon/path");
const obj = {
"foo": null
};
console.log(get(obj, "foo.bar")); // Logs: undefined
set(obj, "foo.bar", "hello");
console.log(get(obj, "foo.bar")); // Logs: hello
console.log(set(obj, "list.0", "first")); // Logs: {"foo": {"bar": "hello"}, "list": ["first"]}setImmutable(obj, path, val, options?)
The same as set() with options.immutable set to true. Returns
a new object with the change and does not modify obj. Objects that were
not on the changed path keep the same reference (see
Mutable and Immutable Operations).
Changed in 8.0.0
- Includes all the changes to set().
- Copies keep the prototype of class instances, the data of
Date,RegExp,MapandSetvalues and thenullprototype ofObject.create(null)objects. They previously became plain objects.- An own
__proto__key in the data (e.g. fromJSON.parse()) is kept as data. It previously became the prototype of the copy.
const {setImmutable, get} = require("@irrelon/path");
const obj = {
"foo": {
"bar": "goodbye",
"subBar": {
"somethingElse": true
}
},
"otherObj": {
"enabled": true
}
};
const newObj = setImmutable(obj, "foo.bar", "hello");
// The original object is unmodified
console.log(get(obj, "foo.bar")); // Logs: goodbye
// The new object has the new value
console.log(get(newObj, "foo.bar")); // Logs: hello
// Objects that were not modified still share a reference
console.log(obj.otherObj === newObj.otherObj); // Logs: true
console.log(obj.foo.subBar === newObj.foo.subBar); // Logs: true
// Objects on the modified path are new
console.log(obj.foo === newObj.foo); // Logs: falseunSet(obj, path, options?)
Deletes the key at the given path. Only own properties are deleted,
inherited properties are never touched.
Changed in 8.0.0
- A primitive in the middle of the path is no longer replaced with
undefined.unSet({a: "hello"}, "a.b")previously setatoundefined.- Paths through inherited properties such as
constructor.prototype.xare ignored. They previously setconstructortoundefined.transformKeyis used both to check for and to delete the key. The untransformed key was previously checked.- Data with a key named
hasOwnProperty, andObject.create(null)objects, no longer throw.- A path that is not a string throws
"Path argument must be a string"instead of aTypeError.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj | Object or Array |
Yes | The object to delete from. | |
| path | string |
Yes | The path of the key to delete. | |
| options | Object |
No | {} |
Supports immutable and transformKey. |
Returns obj (or the new object when immutable). If there is nothing
to delete, the original obj is returned even in immutable mode. Returns
undefined if obj is not an object or path is empty.
Throws if path is not a string.
Deleting an array index with unSet() leaves an empty slot, the same as the
deleteoperator. Use splicePath() or pullVal() to remove array items.
const {unSet} = require("@irrelon/path");
const obj = {
"foo": {
"bar": [{
"moo": true,
"baa": "ram you"
}]
}
};
unSet(obj, "foo.bar.0.baa");
console.log(obj.foo.bar[0]); // Logs: {"moo": true}
console.log(unSet(obj, "foo.missing.key") === obj); // Logs: trueunSetImmutable(obj, path, options?)
The same as unSet() with options.immutable set to true.
Changed in 8.0.0
- Includes all the changes to unSet() and the copy changes listed for setImmutable().
const {unSetImmutable} = require("@irrelon/path");
const obj = {"user": {"name": "Jim", "password": "secret"}, "other": {}};
const safe = unSetImmutable(obj, "user.password");
console.log(safe.user); // Logs: {"name": "Jim"}
console.log(obj.user.password); // Logs: secret
console.log(safe.other === obj.other); // Logs: trueupdate(obj, basePath, updateData, options?)
Applies several set() operations at once. Each key of
updateData is a path (relative to basePath) and each value is the
value to set at that path.
Changed in 8.0.0
- Includes all the changes to set().
- Update data containing a key named
hasOwnPropertyno longer throws.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj | Object or Array |
Yes | The object to update. | |
| basePath | string |
Yes | "" |
The path inside obj that the update paths are relative to. Use "" to update obj itself. |
| updateData | Object |
Yes | An object of paths to values. Only its own keys are used. | |
| options | Object |
No | {} |
Supports immutable and transformKey. |
Returns obj (or the new object when immutable).
const {update} = require("@irrelon/path");
const obj = {
"foo": {
"bar": [{
"moo": true,
"baa": "ram you"
}]
}
};
update(obj, "", {
"foo.bar.0.baa": "hello I've been updated",
"and.so": "have I!"
});
console.log(obj.foo.bar[0].baa); // Logs: hello I've been updated
console.log(obj.and.so); // Logs: have I!
update(obj, "foo.bar.0", {"moo": false});
console.log(obj.foo.bar[0].moo); // Logs: falseupdateImmutable(obj, basePath, updateData, options?)
The same as update() with options.immutable set to true.
Changed in 8.0.0
- Includes all the changes to update() and the copy changes listed for setImmutable().
const {updateImmutable} = require("@irrelon/path");
const obj = {"settings": {"theme": "dark", "fontSize": 12}};
const newObj = updateImmutable(obj, "settings", {"theme": "light", "fontSize": 14});
console.log(newObj.settings); // Logs: {"theme": "light", "fontSize": 14}
console.log(obj.settings); // Logs: {"theme": "dark", "fontSize": 12}pushVal(obj, path, val, options?)
Pushes val onto the end of the array at path. If the array (or any
part of the path to it) does not exist, it is created.
Changed in 8.0.0
- Paths containing escaped periods now push to the right key. They previously created a key containing the backslash.
- A primitive, function or inherited property in the middle of the path is replaced with an object. Truthy primitives previously threw a
TypeError, and inherited properties such asconstructorwere traversed.- Throws an
Errorinstead of a string when the value at the path is not an array.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj | Object or Array |
Yes | The object containing the array. | |
| path | string |
Yes | The path to the array. Use "" if obj is the array. |
|
| val | any |
Yes | The value to push. | |
| options | Object |
No | {} |
Supports immutable. |
Returns obj (or the new object when immutable). If obj is null,
undefined or a primitive, it is returned unchanged.
Throws an Error if the value at path exists but is not an array.
const {pushVal} = require("@irrelon/path");
const obj = {"todos": ["Write docs"]};
pushVal(obj, "todos", "Ship it");
pushVal(obj, "projects.irrelon.tags", "open-source");
console.log(obj); // Logs: {"todos": ["Write docs", "Ship it"], "projects": {"irrelon": {"tags": ["open-source"]}}}pushValImmutable(obj, path, val, options?)
The same as pushVal() with options.immutable set to true.
Changed in 8.0.0
- No longer modifies the original object. Objects along the path in the original were previously replaced with copies.
- Includes all the changes to pushVal() and the copy changes listed for setImmutable().
const {pushValImmutable} = require("@irrelon/path");
const obj = {"todos": ["Write docs"]};
const newObj = pushValImmutable(obj, "todos", "Ship it");
console.log(newObj.todos); // Logs: ["Write docs", "Ship it"]
console.log(obj.todos); // Logs: ["Write docs"]pullVal(obj, path, val, options?)
Removes the first item equal to val from the array at path. By
default items are compared with strict equality (===), so objects must
be the same reference. Pass {strict: false} to remove the first item
that matches the structure of val instead (see match()).
Changed in 8.0.0
- Does nothing when the path doesn't exist. It previously created keys containing
undefinedalong the path.- Uses strict equality unless
strictisfalse. Passing an options object withoutstrict, such as{immutable: true}, previously switched to structural matching.- In immutable mode, returns the original object when nothing was removed instead of a copy.
- Paths containing escaped periods now pull from the right key.
- Inherited properties such as
constructorare no longer traversed.- Pulling
nullwithstrict: falseno longer throws aTypeError.- Throws an
Errorinstead of a string when the value at the path is not an array.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj | Object or Array |
Yes | The object containing the array. | |
| path | string |
Yes | The path to the array. Use "" if obj is the array. |
|
| val | any |
Yes | The value to remove. | |
| options | Object |
No | {} |
Supports immutable and strict (defaults to true). |
Returns obj (or the new object when immutable). If the path does not
exist or the value is not found, nothing changes and the original obj
is returned.
Throws an Error if the value at path exists but is not an array.
const {pullVal} = require("@irrelon/path");
const obj = {
"tags": ["a", "b", "a"],
"users": [{"_id": "1", "name": "Jim"}, {"_id": "2", "name": "Ann"}]
};
pullVal(obj, "tags", "a");
console.log(obj.tags); // Logs: ["b", "a"]
// A different object with the same data is not strictly equal...
pullVal(obj, "users", {"_id": "2"});
console.log(obj.users.length); // Logs: 2
// ...but it matches when strict is false
pullVal(obj, "users", {"_id": "2"}, {strict: false});
console.log(obj.users); // Logs: [{"_id": "1", "name": "Jim"}]pullValImmutable(obj, path, val, options?)
The same as pullVal() with options.immutable set to true.
Like pullVal(), it uses strict equality unless
{strict: false} is passed.
Changed in 8.0.0
const {pullValImmutable} = require("@irrelon/path");
const obj = {"tags": ["a", "b"]};
const newObj = pullValImmutable(obj, "tags", "a");
console.log(newObj.tags); // Logs: ["b"]
console.log(obj.tags); // Logs: ["a", "b"]
console.log(pullValImmutable(obj, "tags", "z") === obj); // Logs: truesplicePath(obj, path, start, deleteCount, itemsToAdd?, options?)
Calls Array.prototype.splice() on the array at path, removing
deleteCount items from start and inserting itemsToAdd in their
place.
Changed in 8.0.0
- Does nothing when the path doesn't exist. It previously threw when the array was missing and created keys containing
undefinedwhen a parent was missing.- In immutable mode, returns the original object when the path doesn't exist.
- Paths containing escaped periods now splice the right key.
- Throws an
Errorinstead of a string when the value at the path is not an array.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj | Object or Array |
Yes | The object containing the array. | |
| path | string |
Yes | The path to the array. Use "" if obj is the array. |
|
| start | number |
Yes | The index to start at. | |
| deleteCount | number |
Yes | The number of items to remove. | |
| itemsToAdd | any[] |
No | [] |
Items to insert at start. |
| options | Object |
No | {} |
Supports immutable. |
Returns obj (or the new object when immutable). If the path does not
exist, nothing changes and the original obj is returned.
Throws an Error if the value at path exists but is not an array.
const {splicePath} = require("@irrelon/path");
const obj = {"letters": ["a", "b", "c", "d"]};
splicePath(obj, "letters", 1, 2, ["x"]);
console.log(obj.letters); // Logs: ["a", "x", "d"]
const newObj = splicePath(obj, "letters", 0, 0, ["start"], {immutable: true});
console.log(newObj.letters); // Logs: ["start", "a", "x", "d"]
console.log(obj.letters); // Logs: ["a", "x", "d"]merge(obj1, obj2, options?)
Deeply merges obj2 into obj1, like a "deep spread". This modifies
obj1 unless options.immutable is true.
Changed in 8.0.0
__proto__keys inobj2are ignored, and inherited properties ofobj1are never merged into. This fixes a prototype pollution vulnerability, see Security.- Values from
obj2that are not plain objects or arrays, such asDates and class instances, are now assigned as they are. They were previously merged into new plain objects, soDates became{}.- A value in
obj1of a different kind to the value inobj2is now replaced. Primitives previously threw aTypeErrorand objects were merged into arrays.- An
obj2that isnullorundefinedno longer throws.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj1 | Object or Array |
Yes | The object to merge into. | |
| obj2 | Object or Array |
Yes | The object to merge from. If it is not an object, nothing is merged. | |
| options | Object |
No | {} |
Supports immutable and ignoreUndefined. |
| Option | Description |
|---|---|
immutable |
Leave obj1 untouched and return a new object. |
ignoreUndefined |
Values in obj2 that are undefined do not overwrite values in obj1. |
The merge rules are:
- Plain objects in
obj2are merged key by key into the plain object at the same key inobj1, and arrays are merged index by index into the array at the same key. - If
obj1has no value at that key, or a value of a different kind (e.g. an array whereobj2has an object), the value fromobj2replaces it. - Every other value from
obj2(strings, numbers,null,Dates, class instances and so on) replaces the value inobj1as it is. __proto__keys inobj2are ignored, and inherited properties ofobj1(likeconstructor) are never merged into. See Security.
Returns obj1 (or the new object when immutable).
const {merge} = require("@irrelon/path");
const defaults = {
"theme": {"colour": "blue", "fontSize": 12},
"plugins": ["a", "b"],
"createdAt": null
};
const userSettings = {
"theme": {"fontSize": 16},
"plugins": ["c"],
"createdAt": new Date(0)
};
const result = merge(defaults, userSettings);
console.log(result.theme); // Logs: {"colour": "blue", "fontSize": 16}
console.log(result.plugins); // Logs: ["c", "b"]
console.log(result.createdAt === userSettings.createdAt); // Logs: true
console.log(result === defaults); // Logs: truemergeImmutable(obj1, obj2, options?)
The same as merge() with options.immutable set to true.
Changed in 8.0.0
- Includes all the changes to merge() and the copy changes listed for setImmutable().
const {mergeImmutable} = require("@irrelon/path");
const obj1 = {"a": {"b": 1}, "untouched": {}};
const result = mergeImmutable(obj1, {"a": {"c": 2}});
console.log(result.a); // Logs: {"b": 1, "c": 2}
console.log(obj1.a); // Logs: {"b": 1}
console.log(result.untouched === obj1.untouched); // Logs: truedecouple(obj, options?)
If options.immutable is true, returns a shallow copy of obj.
Otherwise returns obj itself. This is the function the immutable
operations use to copy each object on a changed path. See
Mutable and Immutable Operations
for how each type of value is copied.
Changed in 8.0.0
- Primitives and functions are returned as they are. They previously returned
undefinedin immutable mode.- Copies keep the prototype of class instances, the data of
Date,RegExp,MapandSetvalues and thenullprototype ofObject.create(null)objects. They previously became plain objects.- An own
__proto__key in the data is kept as data. It previously became the prototype of the copy.- Empty slots in sparse arrays stay empty instead of becoming
undefined, and very large sparse arrays no longer exhaust memory.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj | any |
Yes | The value to copy. | |
| options | Object |
No | {} |
|
| options.immutable | boolean |
No | false |
If true, a copy is returned. |
const {decouple} = require("@irrelon/path");
const obj = {"foo": {"bar": true}};
const copy = decouple(obj, {immutable: true});
console.log(copy); // Logs: {"foo": {"bar": true}}
console.log(copy === obj); // Logs: false
console.log(copy.foo === obj.foo); // Logs: true
console.log(decouple(obj) === obj); // Logs: truediff(obj1, obj2, options?)
Compares two objects or arrays and returns the paths to the values that are different. Use diffValues() to also get the values and how they differ.
Changed in 8.0.0
- Returned paths are escaped. Changes to keys containing periods were previously missed entirely.
- A field changing between an object or array and another type is reported in non-strict mode too.
diff({a: {}}, {a: null})previously returned[].- The same paths are reported whichever object is passed first.
diff({}, {a: {b: 1}})previously returned["a"]whilediff({a: {b: 1}}, {})returned["a.b"]. Both now return["a", "a.b"].- Objects referenced from more than one place are compared at every place. Only the first place was previously compared.
- Escaped keys count as a single level for
maxDepth.- The options object is no longer modified.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj1 | Object or Array |
Yes | The first object. | |
| obj2 | Object or Array |
Yes | The second object. | |
| options | Object |
No | {} |
|
| options.strict | boolean |
No | false |
Compare values with === instead of ==. |
| options.maxDepth | number |
No | Infinity |
The number of path levels to compare. Objects at the maximum depth are compared by reference. |
| options.basePath | string or string[] |
No | "" |
Only compare inside this path, or each of these paths. |
| options.exclusive | boolean |
No | false |
Only compare the keys present in obj2. Useful when obj2 is a partial update of obj1. |
Returns an array of escaped paths.
How differences are reported:
- Values are compared field by field. A field that is
undefinedin one object and missing from the other is not a difference. - When a field changes between an object or array and a different type
(e.g.
{}tonull), the field itself is reported, followed by any leaf paths inside it that changed. - When an array changes length, the array itself is reported as well as the items that differ.
- The same paths are reported whichever object is passed first (unless
exclusiveistrue).
const {diff} = require("@irrelon/path");
const obj1 = {
"user": {
"_id": 1,
"firstName": "Jimbo",
"lastName": "Jetson"
}
};
const obj2 = {
"user": {
"_id": "1", // Notice string instead of numerical _id
"firstName": "James", // We also changed the name from "Jimbo" to "James"
"lastName": "Jetson"
}
};
console.log(diff(obj1, obj2)); // Logs: ["user.firstName"]
console.log(diff(obj1, obj2, {strict: true})); // Logs: ["user._id", "user.firstName"]
console.log(diff(obj1, obj2, {basePath: "user.lastName"})); // Logs: []
console.log(diff({"a": {"b": 1}}, {"a": null})); // Logs: ["a", "a.b"]
console.log(diff({"tags": ["x"]}, {"tags": ["x", "y"]})); // Logs: ["tags", "tags.1"]
console.log(diff({"a": 1, "b": 2}, {"a": 3}, {exclusive: true})); // Logs: ["a"]diffValues(obj1, obj2, options?)
The same as diff() but returns an object where each key is a path to a difference and each value describes it:
Changed in 8.0.0
- Includes all the changes to diff().
- In strict mode, a field changing between an object and a value of another type now has the
difference"both"instead of"type"when the values also differ.
| Property | Description |
|---|---|
val1 |
The value in obj1. |
val2 |
The value in obj2. |
type1 |
The type of val1, see type(). |
type2 |
The type of val2, see type(). |
difference |
"value" if the values differ, "type" if only the types differ (e.g. 1 and "1" in strict mode), or "both". |
const {diffValues} = require("@irrelon/path");
const obj1 = {"user": {"_id": 1, "firstName": "Jimbo", "lastName": "Jetson"}};
const obj2 = {"user": {"_id": "1", "firstName": "James", "lastName": "Jetson"}};
console.log(diffValues(obj1, obj2, {strict: true}));Outputs:
{
"user._id": {
"val1": 1,
"val2": "1",
"type1": "number",
"type2": "string",
"difference": "type"
},
"user.firstName": {
"val1": "Jimbo",
"val2": "James",
"type1": "string",
"type2": "string",
"difference": "value"
}
}isEqual(obj1, obj2, path, deep?, strict?)
Checks if the values at path in both objects are equal.
Changed in 8.0.0
- Deep comparison now compares the keys of both objects. Keys that only existed in the second object were previously ignored, so
isEqual({a: {}}, {a: {b: 1}}, "a", true)wastrue.- Deep comparison now requires both values to be the same kind of object. An object was previously equal to a missing value if it had no keys, and arrays could equal plain objects.
Dates andRegExps are compared by value in deep comparison. Any two were previously equal.- Keys containing periods are compared correctly in deep comparison, and circular references no longer overflow the stack.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj1 | any |
Yes | The first object. | |
| obj2 | any |
Yes | The second object. | |
| path | string or string[] |
Yes | The path to compare. Use "" to compare the objects themselves. If an array is passed, every path must be equal. |
|
| deep | boolean |
No | false |
Compare objects and arrays by their contents instead of by reference. |
| strict | boolean |
No | false |
Compare values with === instead of ==. |
When deep is true:
- Every key of both objects is compared, so extra keys on either side make them unequal.
- Objects must be the same kind (e.g. an array is never equal to a plain object, and a class instance is only equal to an instance of the same class).
Dates are equal if they represent the same time, andRegExps are equal if they have the same pattern and flags.- Circular references are supported.
Returns true if the values are equal.
const {isEqual} = require("@irrelon/path");
const obj1 = {"user": {"id": 1, "tags": ["a"]}};
const obj2 = {"user": {"id": "1", "tags": ["a"]}};
console.log(isEqual(obj1, obj2, "user.id")); // Logs: true
console.log(isEqual(obj1, obj2, "user.id", false, true)); // Logs: false
console.log(isEqual(obj1, obj2, "user.tags")); // Logs: false
console.log(isEqual(obj1, obj2, "user.tags", true)); // Logs: true
console.log(isEqual(obj1, obj2, ["user.id", "user.tags"], true)); // Logs: trueisNotEqual(obj1, obj2, path, deep?, strict?)
The opposite of isEqual(), with the same parameters.
Changed in 8.0.0
- Includes all the changes to isEqual().
const {isNotEqual} = require("@irrelon/path");
console.log(isNotEqual({"a": 1}, {"a": 2}, "a")); // Logs: truematch(source, query)
Checks if source contains the structure and values of query. Every
key in query must exist in source with an equal (===) value, but
source may contain other keys too. Nested objects and arrays are
checked recursively, with arrays compared index by index.
Changed in 8.0.0
- Returns
falsewhenqueryisnullandsourceis an object. It previously threw aTypeError.
| Param | Type | Required | Description |
|---|---|---|---|
| source | any |
Yes | The data to check. |
| query | any |
Yes | The structure and values to look for. |
Returns true if source matches query.
const {match} = require("@irrelon/path");
const user = {"name": "Jim", "address": {"city": "London", "postcode": "N1"}};
console.log(match(user, {"address": {"city": "London"}})); // Logs: true
console.log(match(user, {"address": {"city": "Paris"}})); // Logs: false
console.log(match("Jim", "Jim")); // Logs: true
console.log(match([1, 2, 3], [1, 2])); // Logs: truefindPath(source, query, options?)
Finds every value in source that matches query (see
match()) and returns the paths to them.
Changed in 8.0.0
- Returned paths are escaped.
- Circular references no longer overflow the stack.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| source | any |
Yes | The data to search. | |
| query | any |
Yes | The structure and values to look for. | |
| options | Object |
No | {} |
|
| options.maxDepth | number |
No | Infinity |
The number of levels to search. |
| options.includeRoot | boolean |
No | true |
Whether source itself can match (with the path ""). |
Returns {match: boolean, path: string[]} where path contains the
escaped path to every match. Circular references are supported.
const {findPath} = require("@irrelon/path");
const myData = {
"profile": {
"id": 1,
"name": "Ron Swanson",
"data": {
"mobile": "+001293284732"
}
},
"backup": {
"data": {
"mobile": "+001293284732"
}
}
};
console.log(findPath(myData, {"data": {"mobile": "+001293284732"}})); // Logs: {"match": true, "path": ["profile", "backup"]}
console.log(findPath(myData, "+001293284732")); // Logs: {"match": true, "path": ["profile.data.mobile", "backup.data.mobile"]}
console.log(findPath(myData, "+001293284732", {maxDepth: 2})); // Logs: {"match": false, "path": []}findOnePath(source, query, options?)
The same as findPath() but stops at the first match.
Changed in 8.0.0
- Includes all the changes to findPath().
Returns {match: true, path: string} for the first match, or
{match: false}.
const {findOnePath} = require("@irrelon/path");
const myDataArray = [{
"profile": {
"id": 1,
"name": "Ron Swanson"
}
}, {
"profile": {
"id": 2,
"name": "April Ludgate"
}
}];
// Find the item that has a "profile" with an "id" of 1
console.log(findOnePath(myDataArray, {"profile": {"id": 1}})); // Logs: {"match": true, "path": "0"}
// Find the first object with an "id" of 2
console.log(findOnePath(myDataArray, {"id": 2})); // Logs: {"match": true, "path": "1.profile"}
console.log(findOnePath(myDataArray, {"id": 3})); // Logs: {"match": false}See the unit tests for findPath() and findOnePath() for many more examples of usage.
hasMatchingPathsInObject(testKeys, testObj)
Checks if every key path in testKeys exists in testObj with a value
that is not undefined. Only the structure of testKeys is used, not its
values. Only own properties are considered.
Changed in 8.0.0
- Only own properties of
testObjcount as existing. Inherited properties liketoStringpreviously matched.- Returns
falsewhen a parent intestObjisnullor missing. It previously threw aTypeError.
| Param | Type | Required | Description |
|---|---|---|---|
| testKeys | Object or Array |
Yes | An object whose key structure describes the paths to check. |
| testObj | Object or Array |
Yes | The object to check. |
Returns true if every path exists.
Not safe for circular structures in
testKeys.
const {hasMatchingPathsInObject} = require("@irrelon/path");
const testObj = {"user": {"name": "Jim", "age": null}};
console.log(hasMatchingPathsInObject({"user": {"name": true}}, testObj)); // Logs: true
console.log(hasMatchingPathsInObject({"user": {"age": true}}, testObj)); // Logs: true
console.log(hasMatchingPathsInObject({"user": {"email": true}}, testObj)); // Logs: falsecountMatchingPathsInObject(testKeys, testObj)
For every leaf key in testObj, checks if testKeys has the same key
with a truthy, non-object value. Only own properties are considered.
Changed in 8.0.0
- Only own properties of
testKeyscount as matches. Inherited properties liketoStringpreviously matched.- No longer throws when
testKeysis missing a nested object thattestObjhas.
| Param | Type | Required | Description |
|---|---|---|---|
| testKeys | Object or Array |
Yes | The keys to look for. |
| testObj | Object or Array |
Yes | The object whose leaf keys are counted. |
Returns an object with:
| Property | Description |
|---|---|
matchedKeys |
The same structure as testObj with each leaf replaced by true if it was matched or false if not. |
matchedKeyCount |
The number of matched leaves. |
totalKeyCount |
The total number of leaves in testObj. |
Not safe for circular structures in
testObj.
const {countMatchingPathsInObject} = require("@irrelon/path");
const result = countMatchingPathsInObject({
"moo": true
}, {
"foo": {
"bar": null
},
"moo": true
});
console.log(result);Outputs:
{
"matchedKeys": {
"foo": {
"bar": false
},
"moo": true
},
"matchedKeyCount": 1,
"totalKeyCount": 2
}flatten(obj, finalArr?, parentPath?, options?)
Returns the path to every value in obj, including the objects and
arrays along the way. Circular references are included once but not
followed.
Changed in 8.0.0
- Returned paths are escaped.
- Objects referenced from more than one place are included at every place. Only the first place was previously included.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj | Object or Array |
Yes | The object to scan. | |
| finalArr | string[] |
No | [] |
An array to add the paths to. Pass undefined to get a new array. |
| parentPath | string |
No | "" |
A path to prefix every path with. |
| options | Object |
No | {} |
Supports ignore, transformKey and transformRead. |
Returns an array of escaped paths, each parent before its children.
const {flatten, numberToWildcard} = require("@irrelon/path");
const myData = {
"profile": {
"id": 1,
"name": "Ron Swanson"
},
"tags": ["a", "b"]
};
console.log(flatten(myData)); // Logs: ["profile", "profile.id", "profile.name", "tags", "tags.0", "tags.1"]
console.log(flatten(myData, undefined, "", {ignore: /^profile$/})); // Logs: ["tags", "tags.0", "tags.1"]
console.log(flatten(myData, undefined, "", {transformKey: numberToWildcard})); // Logs: ["profile", "profile.id", "profile.name", "tags", "tags.$", "tags.$"]flattenValues(obj, finalObj?, parentPath?, options?)
Returns an object where each key is the path to a value in obj and each
value is the value at that path. Dates, RegExps and null are
treated as leaf values. Circular references are included once but not
followed.
Changed in 8.0.0
- Returned paths are escaped.
- Objects referenced from more than one place are included at every place.
nullvalues are included whenleavesOnlyistrue, andinfo.isFlatistruefor them.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj | Object or Array |
Yes | The object to scan. | |
| finalObj | Object |
No | {} |
An object to add the paths to. Pass undefined to get a new object. |
| parentPath | string |
No | "" |
A path to prefix every path with. |
| options | Object |
No | {} |
Supports leavesOnly, transformKey, transformRead and transformWrite. |
For this function transformKey is called with (key, info) where
info is {type, isArrayIndex, isFlat}: the typeof the value, whether
the key is an array index and whether the value is a leaf.
Returns an object of escaped paths to values. Each object's own children are listed before the object itself.
const {flattenValues} = require("@irrelon/path");
const myData = {
"profile": {
"id": 1,
"name": "Ron Swanson"
}
};
console.log(flattenValues(myData));Outputs:
{
"profile.id": 1,
"profile.name": "Ron Swanson",
"profile": {
"id": 1,
"name": "Ron Swanson"
}
}Only the leaf values, with array indexes replaced by a wildcard:
const {flattenValues} = require("@irrelon/path");
const myData = {
"users": [{"name": "Jim"}, {"name": "Ann"}],
"deletedAt": null
};
const result = flattenValues(myData, undefined, "", {
leavesOnly: true,
transformKey: (key, info) => info.isArrayIndex ? "$" : key
});
console.log(result); // Logs: {"users.$.name": "Ann", "deletedAt": null}Paths that are the same after transforming overwrite each other, which is why only
"Ann"remains above.
leafNodes(obj, parentPath?)
Returns the paths to every leaf value in obj. Unlike
flatten() it does not include the objects and arrays along
the way. Values that are undefined are skipped, null is a leaf, and
circular references are treated as leaves.
Changed in 8.0.0
- Returned paths are escaped.
- Objects referenced from more than one place are included at every place.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| obj | Object or Array |
Yes | The object to scan. | |
| parentPath | string |
No | "" |
A path to prefix every path with. |
Returns an array of escaped paths.
const {leafNodes} = require("@irrelon/path");
const structure = {
"rootArray": [{
"arr": [{"id": 1}, {"id": 2}],
"name": "An array",
"type": null
}]
};
console.log(leafNodes(structure)); // Logs: ["rootArray.0.arr.0.id", "rootArray.0.arr.1.id", "rootArray.0.name", "rootArray.0.type"]countLeafNodes(obj)
Counts the leaf values in obj, using the same rules as
leafNodes().
Changed in 8.0.0
- Objects referenced from more than one place are counted at every place.
| Param | Type | Required | Description |
|---|---|---|---|
| obj | Object or Array |
Yes | The object to count leaf values in. |
Returns the number of leaf values.
const {countLeafNodes} = require("@irrelon/path");
console.log(countLeafNodes({"foo": {"bar": null}, "moo": true})); // Logs: 2type(item)
Like typeof but distinguishes arrays and null from objects.
Returns one of "undefined", "object", "boolean", "number",
"string", "function", "symbol", "bigint", "null" or "array".
const {type} = require("@irrelon/path");
console.log(type([])); // Logs: array
console.log(type(null)); // Logs: null
console.log(type({})); // Logs: object
console.log(type("hello")); // Logs: stringjoin(...parts)
Joins path parts with periods, ignoring empty and undefined parts. The
parts are not escaped, use joinEscaped() for keys that
may contain periods.
const {join} = require("@irrelon/path");
console.log(join("users", "0", "name")); // Logs: users.0.name
console.log(join("", "users", undefined, "name")); // Logs: users.namejoinEscaped(...keys)
Escapes each key and joins them into a path.
Changed in 8.0.0
- Backslashes in keys are escaped, see escape().
const {joinEscaped} = require("@irrelon/path");
console.log(joinEscaped("domains", "www.google.com")); // Logs: domains.www\.google\.comescape(key)
Escapes the periods and backslashes in a key so it is treated as a single path part. See Keys Containing Periods or Backslashes.
Changed in 8.0.0
- Backslashes are now escaped as
\\, e.g.escape("C:\\temp")returns"C:\\\\temp". Keys ending in a backslash previously could not be used.
const {escape} = require("@irrelon/path");
console.log(escape("www.google.com")); // Logs: www\.google\.com
console.log(escape("C:\\temp")); // Logs: C:\\tempunEscape(key)
Reverses escape(), returning the original key.
Changed in 8.0.0
- Only
\.and\\are unescaped. Previously a backslash followed by any character became a period, e.g."C:\\temp"became"C:.emp".
const {unEscape} = require("@irrelon/path");
console.log(unEscape("www\\.google\\.com")); // Logs: www.google.comsplit(path)
Splits a path into its parts on every period that is not escaped. Each part is still escaped, use unEscape() to get the original key.
Changed in 8.0.0
const {split} = require("@irrelon/path");
console.log(split("users.test@test\\.com.name")); // Logs: ["users", "test@test\\.com", "name"]clean(path)
Removes a leading period from the path.
const {clean} = require("@irrelon/path");
console.log(clean(".foo.bar.one")); // Logs: foo.bar.onechop(path, level)
Shortens path to the given number of parts. A path shorter than
level is returned intact.
const {chop} = require("@irrelon/path");
console.log(chop("foo.bar.ram.you.too", 2)); // Logs: foo.bar
console.log(chop("foo", 2)); // Logs: fooup(path, levels = 1)
Removes levels parts from the end of the path.
const {up} = require("@irrelon/path");
console.log(up("user.friends.0.firstName")); // Logs: user.friends.0
console.log(up("user.friends.0.firstName", 2)); // Logs: user.friendsdown(path, levels = 1)
Removes levels parts from the start of the path.
const {down} = require("@irrelon/path");
console.log(down("user.friends.0.firstName")); // Logs: friends.0.firstName
console.log(down("user.friends.0.firstName", 2)); // Logs: 0.firstNamepop(path, levels = 1)
Returns the last part of the path, or the part levels from the end.
const {pop} = require("@irrelon/path");
console.log(pop("user.friends.0.firstName")); // Logs: firstName
console.log(pop("user.friends.0.firstName", 2)); // Logs: 0shift(path, levels = 1)
Returns the first part of the path, or the part levels from the start.
const {shift} = require("@irrelon/path");
console.log(shift("user.friends.0.firstName")); // Logs: user
console.log(shift("user.friends.0.firstName", 2)); // Logs: friendspush(path, key = "")
Adds a part to the end of the path. An empty path or key does not add a period.
Changed in 8.0.0
- An empty path or key no longer adds a period.
push("user")previously returned"user.".
const {push} = require("@irrelon/path");
console.log(push("user.friends", "0")); // Logs: user.friends.0
console.log(push("", "user")); // Logs: userisCompositePath(path)
Checks if the path has more than one part (i.e. contains a period that is not escaped).
Changed in 8.0.0
- A period after an escaped backslash (
\\.) now makes the path composite.
const {isCompositePath} = require("@irrelon/path");
console.log(isCompositePath("user.name")); // Logs: true
console.log(isCompositePath("user")); // Logs: false
console.log(isCompositePath("test@test\\.com")); // Logs: falseisNonCompositePath(path)
The opposite of isCompositePath().
Changed in 8.0.0
- Includes the change to isCompositePath().
const {isNonCompositePath} = require("@irrelon/path");
console.log(isNonCompositePath("user")); // Logs: truekeyDedup(keys)
Removes duplicate strings from an array, keeping the first occurrence of each.
const {keyDedup} = require("@irrelon/path");
console.log(keyDedup(["b", "a", "b", "c"])); // Logs: ["b", "a", "c"]returnWhatWasGiven(val)
Returns its first argument. This is the default transform function for
the options that accept one.
wildcardToZero(key)
Returns "0" if the key is the wildcard "$", otherwise returns the key.
Use it as a transformKey option to make $ mean "the first array
item".
const {get, wildcardToZero} = require("@irrelon/path");
const obj = {"users": [{"name": "Jim"}, {"name": "Ann"}]};
console.log(get(obj, "users.$.name", undefined, {transformKey: wildcardToZero})); // Logs: JimnumberToWildcard(key)
Returns "$" if the key is a number such as "12", otherwise returns
the key. Use it as a transformKey option to replace array indexes with a
wildcard, see flatten().
const {numberToWildcard} = require("@irrelon/path");
console.log(numberToWildcard("12")); // Logs: $
console.log(numberToWildcard("name")); // Logs: nameType definitions are included. The following types are exported for use in your own code:
| Type | Description |
|---|---|
ObjectType |
Any object with string keys. |
ValueType |
The values returned by type(). |
DifferenceType |
"value", "type" or "both", see diffValues(). |
DiffValue |
A single difference returned by diffValues(). |
DiffOptionsType |
The options for diff() and diffValues(). |
OptionsType |
The common transform* and leavesOnly options. |
GetOptionsType |
The options for get() and getMany(). |
SetOptionsType |
The options for the write functions. |
FindOptionsType |
The options for findPath() and findOnePath(). |
MergeOptionsType |
The options for merge(). |
PathData |
The pathData option of get(). |
FindPathReturn |
The result of findPath(). |
FindOnePathReturn |
The result of findOnePath(). |
OperationFunctionProps |
The argument passed to a traverse() operation. |
QueryMatchFunction |
A function used as $in criteria in query(). |
import {get, type DiffValue, diffValues} from "@irrelon/path";
const changes: Record<string, DiffValue> = diffValues({a: 1}, {a: 2});Version 8 fixes a prototype pollution vulnerability (see Security) along with a large number of bugs. Some of the bug fixes change results that your code may rely on, so they are released as a new major version. If you can't upgrade yet, version 7.0.3 contains the security fixes without these changes.
The most wide-reaching change is that paths returned by the library are now escaped, so keys containing periods or backslashes can be passed straight back to get() and set(). Keys without those characters are unaffected:
// 7.x
flatten({"a.b": 1}); // ["a.b"], which get() can't find
// 8.x
flatten({"a.b": 1}); // ["a\\.b"]The table below summarises what changed in each function. Each function's section in the API Reference has a Changed in 8.0.0 note with the full details and before / after behaviour.
| Function | What changed |
|---|---|
| get() | An array of paths returns the values instead of undefineds. Single-key paths are unescaped and use transformKey. The options object is not modified. |
| getMany() | Skips null items instead of throwing. The options object is not modified and pathData paths are correct. |
| furthest() | No longer throws at null. Stops at primitives. Single-key paths are unescaped and use transformKey. |
| traverse() | Paths are escaped. Stops at missing or primitive values. Options apply to arrays of paths. |
| query() | Paths are escaped and escaped query paths match. |
| set() | "arr.0.name" creates an object in the array. Negative numbers create objects. Escaped keys in a path no longer create stray keys or lose data. |
| setImmutable() | As set(), plus copies keep class instances, built-ins and own __proto__ data keys. |
| unSet() / unSetImmutable() | No longer replaces a primitive in the path with undefined. Uses the transformed key consistently. |
| update() / updateImmutable() | As set(). |
| pushVal() | Escaped keys work. Primitives in the path are replaced. Throws Error objects. |
| pushValImmutable() | No longer modifies the original object. |
| pullVal() | Missing paths do nothing. Always strict unless strict: false. Returns the original when nothing is removed. Throws Error objects. |
| pullValImmutable() | Strict equality by default. Pass {strict: false} for the old behaviour. |
| splicePath() | Missing paths do nothing instead of throwing. Throws Error objects. |
| merge() / mergeImmutable() | Ignores __proto__ (security fix). Keeps Dates and class instances. Replaces values of a different kind. |
| decouple() | Returns primitives as they are. Copies keep class instances, built-ins and sparse array holes. |
| diff() / diffValues() | Paths are escaped. Reports object / non-object changes in non-strict mode. Symmetric results. Compares shared objects everywhere. |
| isEqual() / isNotEqual() | Deep comparison checks both objects' keys, object kinds, Dates and RegExps, and supports circular references. |
| match() | Returns false instead of throwing for a null query. |
| findPath() / findOnePath() | Paths are escaped. Supports circular references. |
| hasMatchingPathsInObject() / countMatchingPathsInObject() | Only own properties match. No longer throw on missing or null parents. |
| flatten() / flattenValues() / leafNodes() / countLeafNodes() | Paths are escaped. Shared objects are included everywhere. leavesOnly includes null. |
| escape() / joinEscaped() | Backslashes are escaped. |
| unEscape() | Only \. and \\ are unescaped. |
| split() and the functions using it | Handles escaped backslashes. Keys containing [--] are no longer corrupted. |
| isCompositePath() / isNonCompositePath() | A period after an escaped backslash is a delimiter. |
| push() | No trailing period for an empty key. |
Unchanged: values(), distill(), join(), clean(), chop(), up(), down(), pop(), shift(), type(), keyDedup(), returnWhatWasGiven(), wildcardToZero() and numberToWildcard(), apart from the escaping changes inherited from split() and the security hardening described in Security.
Across every function, a path that is not a string now throws
"Path argument must be a string" as intended, instead of an unrelated
TypeError, and data containing a key named hasOwnProperty or created
with Object.create(null) no longer causes errors.
The update() and updateImmutable() functions have their signature changed to include a base path in the arguments. If migrating from a previous version you can simply add an empty string as the basePath argument to have the functions operate in the same way as before e.g.
update(obj, updateObj);
updateImmutable(obj, updateObj);update(obj, "", updateObj);
updateImmutable(obj, "", updateObj);The basePath argument was added so that you can target a path within
the passed obj to receive the update e.g.
const {update} = require("@irrelon/path");
const obj = {"subObj": {}};
update(obj, "subObj", {"foo": true});
console.log(obj.subObj.foo); // Logs: trueThe update above will modify obj.subObj.foo to equal true.
There was a bug in the get() function that would return an incorrect value when a non-object was passed to get data from and a path was passed e.g.
get("foo-im-not-an-object", "some.path.to.get.data.from"); // Version 2.x returned "foo-im-not-an-object"In version 3.x, this call will return undefined as expected.
Version 1.x exported a class that you could instantiate. Version 2.x exports an object with all available functions. You can require version 2.x either all at once (all functions) or you can destructure to require only the functions you need. This change is primarily to support tree shaking, as well as move to a more functional programming style, albeit not pure functional style :)
Version 2.x is a breaking change from version 1.x and you will need to migrate your code to work with the new version. Migration is fairly simple and instead of using an instance of the 1.x class, you simply require the parts of the library you need e.g.
// DON'T DO THIS !!!!!!!!!!!
const Path = require("irrelon-path");
const pathSolver = new Path();
const a = {hello: {foo: true}};
const b = pathSolver.get(a, "hello.foo"); // b === true// DO THIS :)
const {get} = require("@irrelon/path");
const a = {hello: {foo: true}};
const b = get(a, "hello.foo");
console.log(b); // Logs: trueSee contributing.md and the code of conduct. Run the tests with:
npm testThe test suite requires 100% code coverage.
MIT, see LICENSE.