Notation.js
© 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)
Notationvalue // { 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
- Notation
- Glob Notation
- Filtering Data with Glob patterns
- Object and Bracket Notation Syntax
- Globs and Data Integrity
- Source Object Mutation
- API Reference
Usage
Install via NPM:
npm i notation
In Node/CommonJS environments:
const Notation = ;
With transpilers (TypeScript, Babel):
;
In (Modern) Browsers:
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 && objvery && objverydeep return objverydeepprop === undefined ? defaultValue : objverydeepprop;
With Notation
, you could do this:
const notate = Notationcreate;return ;
You can also inspect & get the value:
console;// {// notation: 'very.deep.prop',// has: true,// value: 'some value',// type: 'string',// level: 3,// lastNote: 'prop'// }
To modify or build a data object:
const notate = Notationcreate;const obj = car: brand: "Dodge" model: "Charger" dog: breed: "Akita" ; // initialize. equivalent to `new Notation(obj)` // { car: { brand: "Dodge", model: "Charger", color: "red" }, dog: { breed: "Akita" } } // { car: { brand: "Dodge", color: "red" }, dog: { breed: "Akita" } } // { dog: { breed: "Akita" } } // equivalent to .filter(['dog']) // { "dog.breed": "Akita" } // { dog: { breed: "Akita" } } // { dog: { breed: "Akita", color: "white" } } // { dog: { breed: "Akita", color: "white" }, boat: { name: "Mojo" } } // { 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 theNotation
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 = ; const globs = '*' '!id' 'name' 'car.model' '!car.*' 'id' 'name' 'age';console;// ——» ['*', '!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 remainingname
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;// ——» ['*', '!car.*', '!id']
- In restrictive mode, negated removes every match.
Note:
Notation#filter()
andNotation.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 = NotationGlob; console;// ——» ['*', '!*.age', 'user.age']
In the united result ['*', '!*.age', 'user.age']
:
- (negated)
!car.model
ofglobsA
is removed becauseglobsB
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
inglobsA
, which matchesuser.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 = Notationvalue;console;// ——»// {// 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 = Notationvalue;console;// ——»// {// 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
indicatesx
property of the first item of the root array.x[1]
indicates second item ofx
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 ofx
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.*
andx.*.*
(and so on) are all equivalent globs. All normalize tox
.- Negated versions are NOT equivalent.
!x
indicates removal ofx
.!x.*
only indicates removal of all first-level properties ofx
but not itself (empty object).!x.*.*
only indicates removal of all second-level properties ofx
; 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;// —» // {// 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;// ——» { 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 numberconst globs = '*' '!x.*';console;// ——» 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 = Notationcreate; const mutated = value;console; // ——» true const cloned = value;console; // ——» falseconsole; // ——» 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 asString
,Number
,Boolean
,Symbol
,null
andundefined
) and built-in types (such asDate
andRegExp
).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.