Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

notation

onury233.7kMIT2.0.0

Utility for modifying / processing the contents of Javascript objects or arrays via object notation strings or globs.

object, array, notation, dot-notation, property, bracket, glob, pointer, enumerable, template, placeholder, data, value, parse, build

readme

Notation.js

build-status coverage-status npm release dependencies vulnerabilities license maintained documentation

© 2020, Onur Yıldırım (@onury). MIT License.

Utility for modifying / processing the contents of JavaScript objects and arrays, via object or bracket notation strings or globs. (Node and Browser)

Notation.create({ x: 1 }).set('some.prop', true).filter(['*.prop']).value // { some: { prop: true } }

Note that this library should be used to manipulate data objects with enumerable properties. It will NOT deal with preserving the prototype-chain of the given object or objects with circular references.

Table of Contents

Usage

Install via NPM:

npm i notation

In Node/CommonJS environments:

const { Notation } = require('notation');

With transpilers (TypeScript, Babel):

import { Notation } from 'notation';

In (Modern) Browsers:

<script src="js/notation.min.js"></script>
<script>
    const { Notation } = notation;
</script>

Notation

Notation is a class for modifying or inspecting the contents (property keys and values) of a data object or array.

When reading or inspecting an enumerable property value such as obj.very.deep.prop; with pure JS, you would have to do several checks:

if (obj 
        && obj.hasOwnProperty('very') 
        && obj.very.hasOwnProperty('deep')  
        && obj.very.deep.hasOwnProperty('prop')
    ) {
    return obj.very.deep.prop === undefined ? defaultValue : obj.very.deep.prop;
}

With Notation, you could do this:

const notate = Notation.create;
return notate(obj).get('very.deep.prop', defaultValue);

You can also inspect & get the value:

console.log(notate(obj).inspectGet('very.deep.prop'));
// {
//     notation: 'very.deep.prop',
//     has: true,
//     value: 'some value',
//     type: 'string',
//     level: 3,
//     lastNote: 'prop'
// }

To modify or build a data object:

const notate = Notation.create;
const obj = { car: { brand: "Dodge", model: "Charger" }, dog: { breed: "Akita" } };
notate(obj)                          // initialize. equivalent to `new Notation(obj)`
    .set('car.color', 'red')         // { car: { brand: "Dodge", model: "Charger", color: "red" }, dog: { breed: "Akita" } }
    .remove('car.model')             // { car: { brand: "Dodge", color: "red" }, dog: { breed: "Akita" } }
    .filter(['*', '!car'])           // { dog: { breed: "Akita" } } // equivalent to .filter(['dog'])
    .flatten()                       // { "dog.breed": "Akita" }
    .expand()                        // { dog: { breed: "Akita" } }
    .merge({ 'dog.color': 'white' }) // { dog: { breed: "Akita", color: "white" } }
    .copyFrom(other, 'boat.name')    // { dog: { breed: "Akita", color: "white" }, boat: { name: "Mojo" } }
    .rename('boat.name', 'dog.name') // { dog: { breed: "Akita", color: "white", name: "Mojo" } }
    .value;                          // result object ^

See API Reference for more...

Glob Notation

With a glob-notation, you can use wildcard stars * and bang ! prefix. A wildcard star will include all the properties at that level and a bang prefix negates that notation for exclusion.

  • Only Notation#filter() method accepts glob notations. Regular notations (without any wildcard * or ! prefix) should be used with all other members of the Notation class.
  • For raw Glob operations, you can use the Notation.Glob class.

Normalizing a glob notation list

Removes duplicates, redundant items and logically sorts the array:

const { Notation } = require('notation');

const globs = ['*', '!id', 'name', 'car.model', '!car.*', 'id', 'name', 'age'];
console.log(Notation.Glob.normalize(globs));
// ——» ['*', '!car.*', '!id', 'car.model']

In the normalized result ['*', '!car.*', '!id', 'car.model']:

  • id is removed and !id (negated version) is kept. (In normalization, negated always wins over the positive, if both are same).
  • Duplicate glob, name is removed. The remaining name is also removed bec. * renders it redundant; which covers all possible notations.
  • (In non-restrictive mode) car.model is kept (although * matches it) bec. it's explicitly defined while we have a negated glob that also matches it: !car.*.
console.log(Notation.Glob.normalize(globs, { restrictive: true }));
// ——» ['*', '!car.*', '!id']
  • In restrictive mode, negated removes every match.

Note: Notation#filter() and Notation.Glob.union() methods automtically pre-normalize the given glob list(s).

Union of two glob notation lists

Unites two glob arrays optimistically and sorts the result array logically:

const globsA = ['*', '!car.model', 'car.brand', '!*.age'];
const globsB = ['car.model', 'user.age', 'user.name'];
const union = Notation.Glob.union(globsA, globsB); 
console.log(union);
// ——» ['*', '!*.age', 'user.age']

In the united result ['*', '!*.age', 'user.age']:

  • (negated) !car.model of globsA is removed because globsB has the exact positive version of it. (In union, positive wins over the negated, if both are same.)
  • But then, car.model is redundant and removed bec. we have * wildcard, which covers all possible non-negated notations.
  • Same applies to other redundant globs except user.age bec. we have a !*.age in globsA, which matches user.age. So both are kept in the final array.

Filtering Data with Glob patterns

When filtering a data object with a globs array; properties that are explicitly defined with globs or implied with wildcards, will be included. Any matching negated-pattern will be excluded. The resulting object is created from scratch without mutating the original.

const data = {
    car: {
        brand: 'Ford',
        model: 'Mustang',
        age: 52
    },
    user: {
        name: 'John',
        age: 40
    }
};
const globs = ['*', '!*.age', 'user.age'];
const filtered = Notation.create(data).filter(globs).value;
console.log(filtered);
// ——»
// {
//     car: {
//         brand: 'Ford',
//         model: 'Mustang'
//     },
//     user: {
//         name: 'John',
//         age: 40
//     }
// }

In non-restrictive mode; even though we have the !*.age negated glob; user.age is still included in the result because it's explicitly defined.

But you can also do restrictive filtering. Let's take the same example:

const globs = ['*', '!*.age', 'user.age'];
const filtered = Notation.create(data).filter(globs, { restrictive: true }).value;
console.log(filtered);
// ——»
// {
//     car: {
//         brand: 'Ford',
//         model: 'Mustang'
//     },
//     user: {
//         name: 'John'
//     }
// }

Note that in restrictive mode, user.age is removed this time; due to !*.age pattern.

Object and Bracket Notation Syntax

Each note (level) of a notation is validated against EcmaScript variable syntax, array index notation and object bracket notation.

Property Keys

  • x[y], x.1, x.y-z, x.@ are incorrect and will never match.
  • x["y"], x['1'], x["y-z"], x['@'] are correct object bracket notations.

Array Indexes

  • [0].x indicates x property of the first item of the root array.
  • x[1] indicates second item of x property of the root object.

Wildcards

  • * is valid wildcard for glob notation. Indicates all properties of an object.
  • [*] is valid wildcard for glob notation. Indicates all items of an array.
  • x[*] is valid wildcard for glob notation. Indicates all items of x property which should be an array.
  • x['*'] just indicates a property/key (star), not a wildcard. Valid regular notation.
  • x.* is valid wildcard for glob notation.
  • x, x.* and x.*.* (and so on) are all equivalent globs. All normalize to x.
  • Negated versions are NOT equivalent.
    • !x indicates removal of x.
    • !x.* only indicates removal of all first-level properties of x but not itself (empty object).
    • !x.*.* only indicates removal of all second-level properties of x; but not itself and its first-level properties (x.*).
    • Same rule applies for bracket notation or mixed notations.
      • [0] = [0][*] but ![0]![0][*]
      • x = x[*] but !x!x[*]
      • [*] = [*].* but ![*]![*].*

Example

Below, we filter to;

  • keep all properties of the source object,
  • remove the second item of colors property (which is an array),
  • and empty my-colors property (which is an object).
    const source = {
      name: 'Jack',
      colors: ['blue', 'green', 'red'],
      'my-colors': { '1': 'yellow' }     // non-standard name "my-colors"
    };
    const globs = ['*', '!colors[1]', '!["my-colors"].*'];
    console.log(Notation.create(source).filter(globs).value);
    // —» 
    // {
    //     name: 'Jack',
    //     colors: ['blue', 'red'],
    //     'my-colors': {}
    // }
    In the example above, colors item at index 1 is emptied.

Globs and Data Integrity

Glob List Integrity

In a glob list, you cannot have both object and array notations for root level. The root level implies the source type which is either an object or array; never both.

For example, ['[*]', '!x.y'] will throw because when you filter a source array with this glob list; !x.y will never match since the root x indicates an object property (e.g. source.x).

Glob vs Data (Value) Integrity

Each glob you use should conform with the given source object.

For example:

const obj = { x: { y: 1 } };
const globs = ['*', '!x.*'];
console.log(Notation.create(obj).filter(globs).value);
// ——» { x: {} }

Here, we used !x.* negated glob to remove all the properties of x but not itself. So the result object has an x property with an empty object as its value. All good.

But in the source object; if the actual value of x is not an object, using the same glob list would throw:

const obj = { x: 1 }; // x is number
const globs = ['*', '!x.*'];
console.log(Notation.create(obj).filter(globs).value);
// ——» ERROR

This kind of type mismatch is critical so it will throw. The value 1 is a Number not an object, so it cannot be emptied with !x.*. (But we could have removed it instead, with glob !x.)

Source Object Mutation

The source object or array will be mutated by default (except the #filter() method). To prevent mutation; you can call #clone() method before calling any method that modifies the object. The source object will be cloned deeply.

const notate = Notation.create;

const mutated = notate(source1).set('newProp', true).value;
console.log(source1.newProp); // ——» true

const cloned = notate(source2).clone().set('newProp', true).value;
console.log('newProp' in source2); // ——» false
console.log(cloned.newProp); // ——» true

Note that Notation expects a data object (or array) with enumerable properties. In addition to plain objects and arrays; supported cloneable property/value types are primitives (such as String, Number, Boolean, Symbol, null and undefined) and built-in types (such as Date and RegExp).

Enumerable properties with types other than these (such as methods, special objects, custom class instances, etc) will be copied by reference. Non-enumerable properties will not be cloned.

If you still need full clone support, you can use a library like lodash. e.g. `Notation.create(.cloneDeep(source))`_

Documentation

You can read the full API reference here.

Change-Log

Read the CHANGELOG especially if you're migrating from version 1.x.x to version 2.0.0 and above.

License

MIT.

changelog

Notation Changelog

All notable changes to this project will be documented in this file. The format is based on Keep a Changelog and this project adheres to Semantic Versioning.

2.0.0 [Unreleased]

This is a big major release with lots of improvements and some breaking changes. Please read the changes below and re-run your application tests after you upgrade to v2.

Added

  • Array index notation and object bracket notation support. This brings ability to manipulate arrays with indexes and objects with non-standard property names (keys). See documentation for more info.
  • strict: boolean class option that specifies whether to throw when a notation path does not exist on the source. (Note that #inspectGet() and #inspectRemove() methods are exceptions). It's recommended to set this to true and prevent silent failures if you're working with sensitive data. Regardless of strict option, it will always throw on invalid notation syntax or other crucial failures.
  • restrictive: boolean option for Notation#filter() instance method that specifies whether negated items strictly remove every match. Note that, regardless of this option; if any item has an exact negated version; non-negated is always removed. Default is false. This option is also available for .normalize() and .union() static methods of Notation.Glob class. For example; ['*', 'name', 'car.model', '!car.*'] will normalize to ['*', '!car.*', 'car.model']. In restrictive mode, it will normalize to ['*', '!car'].
  • Notation.join() static method for joining given notes into a notation string.
  • Notation.split() static method for splitting given regular notation into a notes array.
  • Notation.Glob.split() static method for splitting given glob notation into a notes array.
  • Notation.Glob#intersect() instance method for checking whether current glob has any intersection with the given glob/notation.
  • Notation.Glob#covers() instance method for checking whether given glob covers another specific glob.
  • Ability to "insert" array item at given index via Notation#set() method; instead of overwriting only.
  • 100% full test coverage.

Changed

  • Breaking » (For browser) dropped support for IE 10 and below.
  • Breaking » (For Node) dropped support for Node v8 and below (might still work though).
  • Breaking » Notation is a named export now. (Meaning require or import within curly brackets. e.g. const { Notation } = require('notation'))
  • Breaking » Now that bracket-notation support is added, there will be some changed behaviour. Notation that has an array value is now also notated with a bracket-index for each item, instead of only the key (array name) itself. e.g. prop1.prop2[3]
  • Breaking » Improved notation and glob validation. Now we strictly validate each note of a notation against EcmaScript variable syntax, array index notation and object bracket notation. For example:
    • x[y], x.1, x.y-z, x.@ are incorrect and will never match.
    • x["y"], x['1'], x["y-z"], x["@"] are correct.
    • x.* is valid (wildcard) for glob notation but invalid (object property) as regular notation.
    • x[*] is valid (wildcard) for glob notation but invalid (array index) as regular notation.
    • x['*'] just indicates a property/key (star), not a wildcard. Valid regular notation.
  • When an element is removed from an array; that item will be emptied and indices will be preserved (and will NOT shift). e.g.
      Notation.create([0, 1, 2]).remove('[1]').value
      // will return [0, (empty), 2]
    The empty item can be treated as undefined in the sparse array result above. Set preserveIndices option to false to change this behavior.
      const options = { preserveIndices: false };
      Notation.create([0, 1, 2], options).remove('[1]').value
      // will return [0, 2]
  • Breaking » Changed/improved normalization and union logic. Also now, introducing (linear) intersections within normalization and (cross) intersections within union. An intersection glob is only produced when needed. For example; previously, ['!*.y', 'x'] would normalize as is but this had side-effects when union'ed with another glob list. Now it normalizes to ['x', '!x.y']. Notice that in this example, intersection glob '!x.y' is added and '!*.y' is removed. See documentation for more.
  • Breaking » All levels of negated globs are now respected. For example; when an object is filtered with ['*', '!x.*'], the x property would be completely removed. Now, x will be emptied instead (i.e. {}) due to explicit trailing wildcard star. To completely remove the x property; filtering globs should be ['*', '!x'].
  • Passing undefined as the source object will now throw. This prevents accidental empty initialization. To initialize a Notation instance with a new empty object, just omit the argument or explicitly define it. e.g. new Notation() or new Notation({}).
  • Breaking » #inspectGet() method of Notation class is renamed to #inspectGet() for compatibility with Node.js version 10+. See DEP0079.
  • Improved collection deep cloning.
  • Updated globs comparison/sort logic.

Removed

  • Breaking » Instance method Notation#eachKey() (alias of #each()) is removed. Now that bracket (and array) notation support is added, this name is misleading. (Now, "each" indicates each key and/or index.)

Fixed

  • In some cases, when an empty string or invalid notation is passed, it would silently fail.
  • An issue with Notation.Glob.normalize() where it would throw RangeError: Maximum call stack size exceeded when a glob list had both object and array notations for root level.
  • An issue with Notatin.Glob.union() where less restrictive globA would be removed incorrectly when globB had multiple trailing wildcards and both globs were negated.
  • An issue with Notation.Glob.normalize() where some redundant non-negated globs were not removed.
  • When Notation throws an error, error instanceof Notation.Error would return false. Fixed.

Thanks to @marcinkumorek and @BenoitRanque for their help.

1.3.6 (2018-02-24)

Fixed

  • An issue with Notation.Glob.toRegExp() method that would cause some globs to be cleared out incorrectly when .normalize()d. e.g. "!password" would match "!password_reset" and get removed the later. Fixes #7.

1.3.5 (2017-10-04)

Changed

  • Redundant, negated globs are also removed when normalized. Fixes #5.
  • Minor revisions.

Fixed

  • Shifted index issue with Notation.Glob.normalize(array).
  • countNotes() method.

1.3.0 (2017-09-30)

Added

  • Notation.Glob.normalize(array) static method.
  • Notation.Glob.toRegExp(glob) static method.
  • Notation.countNotes(notation) convenience method.

Changed

  • Completely re-wrote Notation.Glob.union() static method. sort (boolean) argument is removed (the output is now always sorted.)
  • Union output is now properly normalized, duplicates and redundant globs are removed, etc...
  • Improved glob validation.

Fixed

  • Array mutation issue. Fixes #2.
  • An issue where a glob with wildcard is not properly union'ed. Fixes [#3(https://github.com/onury/notation/issues/3).
  • An issue where negated wildcards would be filtered incorrectly in some edge cases (e.g. !*.*.*).
  • Import typo that prevents Travis builds succeed.

Removed

  • (dev) Removed dev-dependencies (Grunt and plugins) in favor of NPM scripts. Updated other dev-dependencies. Added more, comprehensive tests.

1.1.0 (2016-09-27)

Added

  • Notation#expand() method (alias Notation#aggregate()).

Changed

  • Refactored Notation#getFlat() to Notation#flatten(). Returns instance (chainable) instead of source.
  • Notation#separate() returns instance (chainable) instead of source.
  • Minor revisions.

1.0.0 (2016-04-10)

Added

  • initial release.