@warang580/datamix v6.2.0
Datamix
Manipulate data of different types with the same consistent API to fetch or update important data. (ie. objects and arrays are both key-value pairs)
Inspired by Clojure sequences.
Clojure defines many algorithms in terms of sequences (seqs). A seq is a logical list, and unlike most languages where the list is represented by a concrete ... structure, Clojure uses the ISeq interface to allow many data structures to provide access to their elements as sequences. (source)
Getting Started
Installation
Import
// ES6
import mix from "@warang580/datamix";
// NodeJS
const mix = require("@warang580/datamix");
You can also import specific functions with object notation, like :
import { get, set } from "@warang580/datamix";
API
This section contains all functions with a quick description and usage example. You can click on the function signature to check associated tests as more examples.
Reminder: all functions work on arrays AND objects.
get(data, path, notFoundValue = undefined)
Get a value in a deep data tree of values.
let userId = get(response, 'data.user.id'); // => <someId> or `undefined`
// OR (array notation)
let userName = get(users, [userId, 'name'], "unknown"); // => <name> or "unknown"
set(data, path, newValue)
Set a value (without side-effects) in a deep data tree of values.
let user = {
firstname: "John",
lastname: "Doe",
auth: {
login: "jdoe@email.com",
connections: 12,
}
};
user = set(user, 'age', 50);
user = set(user, 'auth.connections', c => c + 1);
user /* => {
firstname: "John",
lastname: "Doe",
age: 50,
auth: {
login: "jdoe@email.com",
connections: 13,
}
} */
defaultsTo(data, defaultValue = [])
Coerce a nil value (undefined
or null
) into another. Used to ensure that a value is always an array or object depending on needs.
let config = defaultsTo(getConfig(/* ... */), {})
only(data, paths, withMissing = true)
Get a subset of data using paths. Paths can be arrays 'path', ... or objects like {newpath: oldpath, ...}
.
only({x:1, y:2}, ['x']) // => {x: 1}
only({a:0}, {foo: 'a'}) // => {foo: 0}
only({}, ['a']) // => {a: undefined}
only({}, ['a'], false) // => {}
only(
{a: {x: 1, y: 2}, b: {z: 3}},
{'foo.a': 'a.x', 'foo.b': 'b.z'}
) // => {foo: {a: 1, b: 3}}
keys(data)
Get all data keys, like Object.keys().
keys({a: 1, b, 2, c: {x: 3, y: 4}}) // ['a', 'b', 'c']
values(data)
Get all data values, like Object.values().
values({a: 1, b, 2, c: {x: 3, y: 4}}) // => [1, 2, {x: 3, y: 4}]
size(data)
Get data size, like Array.length.
size({a: 1, b, 2, c: {x: 3, y: 4}}) // => 3
size(undefined) // => undefined
getFirst(data, paths, defaultValue = undefined)
Get the first non-nil value using a list of possible paths in a value.
let user = {
work_phone: "0456",
home_phone: "0123",
};
let number = getFirst(user, ['mobile_phone', 'home_phone', 'work_phone'], "?"); // => "0123"
getAll(data, wildcardPath, withPaths = false)
Aggregate all values that match a specific path with wildcards.
let users = [{
name: "Jane",
contacts: [{email: "paul@mail.com"}],
}, {
name: "Fred",
contacts: [{email: "john@mail.com"}, {email: "judy@mail.com"}],
}];
// Only get values
getAll(users, "*.contacts.*.email") // => ["paul@mail.com", "john@mail.com", "judy@mail.com"]
// Get paths and values (can be useful to "set" or "setWith" later)
getAll({list: [
{a: [1, 2]}, {z: [3]}, {a: [4, 5]},
]}, 'list.*.a.*', true) /* => {
'list.0.a.0': 1,
'list.0.a.1': 2,
'list.2.a.0': 4,
'list.2.a.1': 5,
} */
setAll(data, wildcardPath, newValue)
Set all values that matches path with a new value.
setAll(game, "players.*.isDead", false)
setAll(game, "players.*.score", s => (s || 0) + 1)
setWith(data, pathValuePairs)
Set all values in data using path-value pairs.
setWith({a: 1, b: 2, c: [3, 4]}, {'a': -1, 'c.0': 0}) // => {a: -1, b: 2, c: [0, 4]}
setWith({a: 1, b: 2, c: [3, 4]}, [['a', -1], ['c.0', 0]]) // => {a: -1, b: 2, c: [0, 4]}
paths(data, traverseArrays = false)
Get an array of all available paths in data.
let data = {a: 1, b: {x: 2, y: [3, 4]}, c: ['foo', 'bar']};
paths(data) // => ['a', 'b.x', 'b.y', 'c'],
paths(data, true) // => ['a', 'b.x', 'b.y.0', 'b.y.1', 'c.0', 'c.1'],
let list = [1, {a: 1, b: [3, 4]}, [5, 6]];
paths(list) // => ['0', '1.a', '1.b', '2']
paths(list, true) // => ['0', '1.a', '1.b.0', '1.b.1', '2.0', '2.1']
entries(data, deep = false, traverseArrays = false)
Get an array of all path, value in data, like Object.entries()
.
let data = {a: 1, b: {x: 2, y: [3, 4]}, c: ['foo', 'bar']};
entries(data) // => [['a', 1], ['b', {x: 2, y: [3,4]}], ['c', ['foo', 'bar']]]
entries(data, true) // => [['a', 1], ['b.x', 2], ['b.y', [3, 4]], ['c', ['foo', 'bar']]]
entries(data, true, true) // => [['a', 1], ['b.x', 2], ['b.y.0', 3], ['b.y.1', 4], ['c.0', 'foo'], ['c.1', 'bar']]
plain(data, traverseArrays = false)
Get an object of all {path: value} in data.
let data = {a: 1, b: {x: 2, y: [3, 4]}, c: ['foo', 'bar']};
paths(data) // => {'a': 1, 'b.x': 2, 'b.y': [3, 4], 'c': ['foo', 'bar']}
paths(data, true) // => {'a': 1, 'b.x': 2, 'b.y.0': 3, 'b.y.1': 4, 'c.0': 'foo', 'c.1': 'bar'}
isIterable(data)
Returns if data can be iterated upon using functions of this library.
isIterable([/* ... */]) // => true
isIterable({/* ... */}) // => true
isIterable(undefined) // => false
isIterable(null) // => false
isIterable("hello") // => false
isIterable(42) // => false
map(data, (v, k, data) => {...})
Update value on key-value pairs (reminder: all functions work on objects too).
let names = map(users, user => get(user, 'name', 'unknown'));
filter(data, (v, k, data) => {...})
Recreate a new data based on key-value filter.
let admins = filter(users, user => get(user, 'is_admin', false));
reduce(data, (acc, v, k, data) => {...})
Reduce data based on key-value pairs.
let shoppingList = {
"egg": {count: 6, unitPrice: 1},
"chocolate": {count: 2, unitPrice: 10},
};
let total = reduce(
shoppingList,
(total, ingredient) => total + get(ingredient, 'count', 0) * get(ingredient, 'price', 0),
0
); // => 26
each(data, (v, k, data) => {...})
Iterate on key-value pairs to do side-effects.
each(dictionary, word => console.log("word", word));
eachSync(data, async (v, k, data) => {...})
Iterate on key-value pairs to do asynchronous side-effects, but synchronously and in order (avoids boilerplate).
await eachSync(users, saveUserPromise);
// All promises are done here (in order)
eachAsync(data, async (v, k, data) => {...})
Like eachSync
but we don't ensure order, similar as Promise.all()
.
await eachAsync(users, saveUserPromise);
// All promises are done here (in parallel)
groupBy(list, path)
Returns an object of {value: entry, ...} pairs based on path. Entries contain only data that's not shared with other entries.
groupBy([
{name: "John", admin: false},
{name: "Jane", admin: true},
{name: "Paul", admin: false},
{name: "Fred", admin: false}
], '*.admin') /* => {
false: [
{name: "John", admin: false},
{name: "Jane", admin: true},
{name: "Paul", admin: false},
{name: "Fred", admin: false}
],
true: [
{name: "Jane", admin: true}
],
} */
match(data, predicates)
Returns true if data matches predicates.
match({a: {x: 1, y: 2}, b: 3}, {
'a.x': v => v < 2,
'b': 3,
}) // => true
let users = [{
name: "Jane",
contacts: [{email: "paul@mail.com"}],
}, {
name: "Fred",
contacts: [{email: "john@mail.com"}, {email: "judy@mail.com"}],
}];
// "Does someone known John ?"
match(users, {
"*.contacts.*.email": emails => emails.indexOf('john@mail.com') !== -1,
}) // => true
copy(data)
Returns a copy of data to ensure that we don't change data by side-effects.
let numbers = [1, 2, 3, 4];
let previous = copy(numbers);
// Editing number with side-effects
numbers.push(5);
// Previous hasn't changed
previous // => [1, 2, 3, 4]
tap(data)
Returns data after applying side-effect to allow chaining
// Logging without intermediate values
return tap(value, v => console.log("returned", v));
// Adding a user in a single line
return tap(users, users => users.push(user));
parseJson(raw, defaultValue = {})
Parse json without failing with invalid raw json.
parseJson('{"foo":"bar"}') // => {foo: "bar"}
parseJson('{invalid json}') // => {}
parseJson('{invalid json}', undefined) // => undefined
deferData(fn, ...args)
Transform any function of this library into a function that just takes data as input. Useful for map/filter/reduce/etc.
import { deferData:_, get } from '@warang580/datamix';
let names = map(users, _(get, 'name', 'unknown'));
// is equivalent to
let names = map(users, user => get(user, 'name', 'unknown'));