Skip to content
master
Go to file
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
lib
 
 
src
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

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.

About

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

Resources

License

Packages

No packages published
You can’t perform that action at this time.