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.

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']:

console.log(Notation.Glob.normalize(globs, { restrictive: true }));
// ——» ['*', '!car.*', '!id']

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']:

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

Array Indexes

Wildcards

Example

Below, we filter to;

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.