Skip to content

🤠 Functional utilities using object property paths with wildcards and regexps 🌵

License

Notifications You must be signed in to change notification settings

ehmicky/wild-wild-utils

Repository files navigation

wild-wild-utils logo

Node Browsers TypeScript Codecov Minified size Mastodon Medium

🤠 Functional utilities using object property paths with wildcards and regexps. 🌵

Available functional methods include:

Unlike similar libraries, object properties can be get/set using dot-delimited paths, wildcards, regexps, slices and unions. It is built on top of wild-wild-path.

Hire me

Please reach out if you're looking for a Node.js API or CLI engineer (11 years of experience). Most recently I have been Netlify Build's and Netlify Plugins' technical lead for 2.5 years. I am available for full-time remote positions.

Install

npm install wild-wild-utils

This package works in both Node.js >=18.18.0 and browsers.

This is an ES module. It must be loaded using an import or import() statement, not require(). If TypeScript is used, it must be configured to output ES modules, not CommonJS.

API

Methods

map(target, query, mapFunction, options?)

target: Target
query: Query
mapFunction: (value) => value
options: Options?
Return value: Target

Use a mapFunction() to modify any property matching the query.

const target = { user: { firstName: 'Alice', lastName: 'Smith' } }
map(target, 'user.*', (userProp) => userProp.toLowerCase())
// { user: { firstName: 'alice', lastName: 'smith' } }

merge(target, query, value, options?)

target: Target
query: Query
value: any
options: Options?
Return value: Target

Deeply merge an object value with each object property matching the query.

If one of these properties is not an object, it is overridden instead.

Any object in value can change the merge mode using a _merge property with value "deep" (default), "shallow", "set" or "delete".

Arrays can be merged using objects in value where the keys are the array indices. Items can be updated, merged, added, inserted, appended, prepended or deleted.

const target = {
  userOne: { names: ['Alice', 'Smith'], settings: { deleted: true } },
  userTwo: { names: ['John', 'Doe'], settings: { deleted: false } },
}

merge(target, '*', { age: 72, settings: { admin: true } })
// {
//   userOne: {
//     names: ['Alice', 'Smith'],
//     settings: { deleted: true, admin: true },
//     age: 72,
//   },
//   userTwo: {
//     names: ['John', 'Doe'],
//     settings: { deleted: false, admin: true },
//     age: 72,
//   },
// }

merge(target, '*', { age: 72, settings: { admin: true }, _merge: 'shallow' })
// {
//   userOne: {
//     names: [ 'Alice', 'Smith' ],
//     settings: { admin: true },
//     age: 72,
//   },
//   userTwo: {
//     names: [ 'John', 'Doe' ],
//     settings: { admin: true },
//     age: 72,
//   },
// }

merge(target, '*', { names: { 1: 'Red' } })
// {
//   userOne: {
//     names: ['Alice', 'Red'],
//     settings: { deleted: true },
//     age: 72,
//   },
//   userTwo: {
//     names: ['John', 'Red'],
//     settings: { deleted: false },
//     age: 72,
//   },
// }

push(target, query, values, options?)

target: Target
query: Query
values: any[]
options: Options?
Return value: Target

Concatenate an array of values with each array property matching the query.

If one of these properties is not an array, it is overridden instead.

const target = {
  userOne: { firstName: 'Alice', colors: ['red'] },
  userTwo: { firstName: 'John', colors: ['blue'] },
}
push(target, '*.colors', ['yellow', 'silver'])
// {
//   userOne: { firstName: 'Alice', colors: ['red', 'yellow', 'silver'] },
//   userTwo: { firstName: 'John', colors: ['blue', 'yellow', 'silver'] },
// }

unshift(target, query, values, options?)

target: Target
query: Query
values: any[]
options: Options?
Return value: Target

Like push() but concatenates at the beginning of each property instead of at the end.

const target = {
  userOne: { firstName: 'Alice', colors: ['red'] },
  userTwo: { firstName: 'John', colors: ['blue'] },
}
unshift(target, '*.colors', ['yellow', 'silver'])
// {
//   userOne: { firstName: 'Alice', colors: ['yellow', 'silver', 'red'] },
//   userTwo: { firstName: 'John', colors: ['yellow', 'silver', 'blue'] },
// }

find(target, query, testFunction, options?)

target: Target
query: Query
testFunction: (value) => boolean
options: Options?
Return value: any

Return the first property that matches the query and that returns true with the testFunction().

const target = {
  userOne: { firstName: 'Alice', colors: ['red'] },
  userTwo: { firstName: 'John', colors: ['blue'] },
}
find(target, '*.firstName', (firstName) => firstName !== 'John') // 'Alice'

pick(target, query, options?)

target: Target
query: Query
options: Options?
Return value: Target

Keep only the properties matching the query.

const target = {
  userOne: { firstName: 'Alice', lastName: 'Smith', age: 72, admin: true },
  userTwo: { firstName: 'John', lastName: 'Doe', age: 72, admin: true },
}
pick(target, '*./Name/')
// {
//   userOne: { firstName: 'Alice', lastName: 'Smith' },
//   userTwo: { firstName: 'John', lastName: 'Doe' },
// }

include(target, query, testFunction, options?)

target: Target
query: Query
testFunction: (value) => boolean
options: Options?
Return value: Target

Keep only the properties that match the query and that return true with the testFunction().

const target = {
  userOne: { firstName: 'Alice', lastName: 'Smith', age: 72, admin: true },
  userTwo: { firstName: 'John', lastName: 'Doe', age: 72, admin: true },
}
include(target, '**', (value) => typeof value === 'string')
// {
//   userOne: { firstName: 'Alice', lastName: 'Smith' },
//   userTwo: { firstName: 'John', lastName: 'Doe' },
// }

exclude(target, query, testFunction, options?)

target: Target
query: Query
testFunction: (value) => boolean
options: Options?
Return value: Target

Remove any property that matches the query and that returns true with the testFunction().

const target = {
  userOne: { firstName: 'Alice', lastName: 'Smith', age: 72, admin: true },
  userTwo: { firstName: 'John', lastName: 'Doe', age: 72, admin: true },
}
exclude(target, '**', (value) => typeof value === 'string')
// {
//   userOne: { age: 72, admin: true },
//   userTwo: { age: 72, admin: true },
// }

flatten(target, options?)

target: Target
options: Options?
Return value: Target

Flatten deep properties to shallow properties with dot-delimited paths.

const target = { user: { firstName: 'Bob', colors: ['red', 'blue'] } }
flatten(target)
// { 'user.firstName': 'Bob', 'user.colors.0': 'red', 'user.colors.1': 'blue' }

Target

The target value must be an object or an array.

Query

The query format is documented here. Both query strings and arrays can be used.

Options

Options are optional plain objects. They are almost the same as in wild-wild-path.

mutate

Methods: map(), merge(), push(), unshift(), exclude()
Type: boolean
Default: false

By default, the target is deeply cloned.
When true, it is directly mutated instead, which is faster but has side effects.

const target = { colors: ['red'] }
console.log(push(target, 'colors', ['blue']))
// { colors: ['red', 'blue'] }
console.log(target)
// { colors: ['red'] }
console.log(push(target, 'colors', ['blue'], { mutate: true }))
// { colors: ['red', 'blue'] }
console.log(target)
// { colors: ['red', 'blue'] }

entries

Methods: map(), find(), include(), exclude()
Type: boolean
Default: false

By default, properties' values are:

  • Passed as argument to callbacks like mapFunction() and testFunction()
  • Returned by find()

When true, objects with the following shape are used instead:

  • value any: property's value
  • path Path: property's full path
  • missing boolean: whether the property is missing from the target
const target = { job: '', firstName: 'Alice', lastName: 'Smith' }
find(target, '*', (value) => value !== '') // 'Alice'
find(
  target,
  '*',
  (entry) => entry.value !== '' && entry.path[0] !== 'firstName',
  { entries: true },
)
// { value: 'Smith', path: ['lastName'], missing: false },

missing

Methods: map(), merge(), push(), unshift()
Type: boolean
Default: false with map(), true with merge|push|unshift()

When false, properties not defined in the target are ignored.

const target = {}

push(target, 'colors', ['red']) // { colors: ['red'] }
push(target, 'colors', ['red'], { missing: false }) // {}

map(target, 'name', (value = 'defaultName') => value) // {}
map(target, 'name', ({ value = 'defaultName' }) => value, {
  missing: true,
  entries: true,
}) // { name: 'defaultName' }

sort

Methods: find(), pick(), include(),
flatten()
Type: boolean
Default: false

When returning sibling object properties, sort them by the lexigographic order of their names (not values).

const target = { user: { lastName: 'Doe', firstName: 'John', age: 72 } }
flatten(target)
// { 'user.lastName': 'Doe', 'user.firstName': 'John', 'user.age': 72 }
flatten(target, { sort: true })
// { 'user.age': 72, 'user.firstName': 'John', 'user.lastName': 'Doe' }

childFirst

Methods: find()
Type: boolean
Default: false

When using unions or deep wildcards, a query might match both a property and some of its children.

This option decides whether the returned properties should be sorted from children to parents, or the reverse.

const target = { user: { firstName: 'Alice', lastName: '' } }
const isDefined = (value) => value !== ''
find(target, 'user.**', isDefined) // { firstName: 'Alice', lastName: '' }
find(target, 'user.**', isDefined, { childFirst: true }) // 'Alice'

leaves

Methods: map(), merge(), push(), unshift(), find()
Type: boolean
Default: false

When using unions or deep wildcards, a query might match both a property and some of its children.

When true, only leaves are matched. In other words, a matching property is ignored if one of its children also matches.

const target = { user: { settings: { firstName: 'Alice', lastName: 'Smith' } } }
merge(target, 'user user.settings', { age: 72 })
// {
//   user: {
//     settings: { firstName: 'Alice', lastName: 'Smith', age: 72 },
//     age: 72,
//   }
// }
merge(target, 'user user.settings', { age: 72 }, { leaves: true })
// {
//   user: {
//     settings: { firstName: 'Alice', lastName: 'Smith', age: 72 },
//   }
// }

roots

Methods: map(), merge(), push(), unshift(), find()
Type: boolean
Default: false

When using unions or deep wildcards, a query might match both a property and some of its children.

When true, only roots are matched. In other words, a matching property is ignored if one of its parents also matches.

const target = { user: { settings: { firstName: 'Alice', lastName: 'Smith' } } }
merge(target, 'user user.settings', { age: 72 })
// {
//   user: {
//     settings: { firstName: 'Alice', lastName: 'Smith', age: 72 },
//     age: 72,
//   }
// }
merge(target, 'user user.settings', { age: 72 }, { roots: true })
// {
//   user: {
//     settings: { firstName: 'Alice', lastName: 'Smith' },
//     age: 72,
//   }
// }

shallowArrays

Methods: all
Type: boolean
Default: false

If true, wildcards do not recurse on arrays. Array items can still be matched by using indices or slices.

const target = { user: { firstName: 'Bob', colors: ['red', 'blue'] } }
flatten(target)
// { 'user.firstName': 'Bob', 'user.colors.0': 'red', 'user.colors.1': 'blue' }
flatten(target, { shallowArrays: true })
// { 'user.firstName': 'Bob', 'user.colors': ['red', 'blue'] }

classes

Methods: all
Type: boolean
Default: false

Unless true, wildcards and regexps ignore properties of objects that are not plain objects (like class instances, errors or functions). Those can still be matched by using their property name.

const target = { user: new User({ name: 'Alice' }) }
const isDefined = (value) => value !== ''
find(target, 'user.*', isDefined) // undefined
find(target, 'user.*', isDefined, { classes: true }) // 'Alice'

inherited

Methods: all
Type: boolean
Default: false

By default, wildcards and regexps ignore properties that are either inherited or not enumerable. Those can still be matched by using their property name.

When true, inherited properties are not ignored, but not enumerable ones still are.

Related projects

Support

For any question, don't hesitate to submit an issue on GitHub.

Everyone is welcome regardless of personal background. We enforce a Code of conduct in order to promote a positive and inclusive environment.

Contributing

This project was made with ❤️. The simplest way to give back is by starring and sharing it online.

If the documentation is unclear or has a typo, please click on the page's Edit button (pencil icon) and suggest a correction.

If you would like to help us fix a bug or add a new feature, please check our guidelines. Pull requests are welcome!