npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@deboxsoft/notation

v2.0.0

Published

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

Downloads

4

Readme

Notation.js

build-status coverage-status npm release vulnerabilities license documentation

© 2021, 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.