Skip to content

Repository files navigation

sp2 - Immutable updater of objects using JSON operation

Github WorkflowCoverage Statusnpm versiontypesnpm downloadslicense

sp2 is an immutable updater of objects using JSON operation. Suitable for flux state transition like Redux.

// JSON operationconstoperation={$set: {"foo.bar": "baz"}};

The description of operation is almost the same format as MongoDB's Update Operators.

Then, update Object by the operation without mutation.

constobj={foo: {bar: "xx"}};constnewObj=update(obj,operation);

Features

  • Pure objects. No extension like new ImmutableMap().
  • Easy to immutably update nested values in objects. No { ...obj }.
  • Strong Type Support with TypeScript. Nested paths can be typed.
  • Portable. Operations can be queued, stored, and sent to other environments.
  • Mergable. Multiple operations can be merged into one operation.
  • Similar format to MongoDB's Update Operators. It's worth remembering.
  • Various operators: $push, $inc, $unset, etc...
  • Class instance support: it can restore instance when it's restorable.

The name "sp2" is derived from the name of orbital of chemical bond used in phenyl group. This was once a core function of Phenyl framework and extracted from the library. Another meaning of "sp2" is State-operating Procedures with Portability. Portability means that procedures are expressed by JSON data. This makes procedures portable and applicable over different environments.


Installation

npm install sp2
yarn add sp2

Example

import{update}from"sp2";constperson={name: {first: "Smith",last: "Doe"},age: 32,};constoperation={$set: {"name.first": "John"}};constupdatedPerson=update(person,operation);updatedPerson.name.first;// Johnassert(updatedPerson!==person);// obj is unchanged.assert(updatedPerson.age===32);// unchanged.assert(updatedPerson.name.first==="John");// updated.assert(updatedPerson.name.last==="Doe");// unchanged.

More powerful types with TypeScript

With TypeScript, sp2 can infer the types of nested properties and values. To do so, prepare a type and operation-creating functions using $bind<T>() like the following example.

import{$bind,update}from"sp2";// target object typetypePerson={name: {first: string;last: string};age: number;};const{ $set, $path }=$bind<Person>();// Inject the type and generate operation-creating functions.constoperation=$set($path("name","first"),"John");

sp2 provides property names of the target object type during writing codes. demo01

Then, just put the operation to update() function.

constoperation=$set($path("name","first"),"John");constupdatedPerson=update(person,operation);assert(operation={$set: {"name.first": "John"}});assert(updatedPerson!==person);// obj is unchanged.assert(updatedPerson.age===32);// unchanged.assert(updatedPerson.name.first==="John");// updated.assert(updatedPerson.name.last==="Doe");// unchanged.`sp2`can infer thereturnvalueof`update()`(`Person`typehere).

demo02

Redux Reducer

TBD

Integration with MongoDB using Phenyl

TBD

API Documentation

Definitions

constoperation={$set: {"foo.bar[0].baz": 123},/*^^^^ UpdateOperator ^^^^^^^^^^^^^^^^ DocumentPath ^^^ UpdateOperandValue ^^^^^^^^^^^^^^^^^^^^^^^^^ UpdateOperand ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UpdateOperation};*/
  • operation value is UpdateOperation.
  • $set is UpdateOperator.
  • { 'foo.bar[0].baz': 123 } is UpdateOperand. In the example, it's SetOperand.
  • foo.bar[0].baz is DocumentPath.
  • 123 is UpdateOperandValue.

DocumentPath

DocumentPath expresses location to nested value. It's the same format as Amazon DynamoDB's DocumentPath and similar to MongoDB.

{
foo: {
arr: [{bar: "baz"}];}}

The string 'baz' is expressed as 'foo.arr[0].bar' in DocumentPath format.

This DocumentPath is slightly different from Dot Notation in MongoDB which expresses 'baz' as 'foo.arr.0.bar' (array index expression is different).

update(obj, ...operations)

Update object following the given operation(s).

update(obj: Object,
...operations: <SetOperand|UpdateOperation>[]): Object

Parameters

obj

Object to be copied and updated new values. Note that obj is unchanged after the call. Unchanged values are shallowly copied to the returned object.

import{update}from"sp2";constobj={foo: {bar: 1},baz: {biz: 2}};constnewObj=update(obj,{"baz.biz": 3});assert(obj.foo===newObj.foo);// unchanged values are shallowly copiedassert(obj.baz!==newObj.baz);// changed values are not copied

operations (variable arguments)

UpdateOperation, SetOperand. SetOperand is just a key-value pair object.

All the operations are fulfilled in order.

update(obj,operation1)// operations are applied in order: 1, 2, 3...update(obj,operation1,operation1,operation2,operation3, ...)

updateProp(obj, docPath, ...operations)

Update the property of the obj located at the given documentPath following the given operation.

updateProp(obj: Object,docPath: DocumentPath,uOp: SetOperand|UpdateOperation): Object

Parameters

obj

Object containing an object to be updated. Note that obj is unchanged after the call.

operations

UpdateOperation or SetOperand. See update() API docs.

Example

import{updateProp}from"sp2";constobj={foo: {bar: 1},baz: {biz: 2}};constnewObj=updateProp(obj,"baz",{$inc: {biz: 1}});assert(newObj.baz.biz===3);

retarget(docPath, operation)

Retarget the given UpdateOperation to the given docPath.

It helps realize loose coupling between parent object and child Object. Even if a parent-object-handling layer doesn't know its child object's shape, the layer can create an UpdateOperation to modify its child object using retarget() if only child-object-handling layer offers the child object's UpdateOperation.

retarget(docPath: DocumentPath,operation: SetOperand|UpdateOperation): UpdateOperation

Parameters

docPath

DocumentPath of the new target object (target itself is not given).

operation

UpdateOperation or SetOperand. operation to be modified. Note that operation itself is unchanged. New operation object is returned.

Example

import{retarget,update}from"sp2";constparent={child: {foo: {bar: 123}}};constchildOp={$mul: {"foo.bar": 2}};constparentOp=retarget(parent,"child",childOp);constnewParent=update(parent,parentOp);assert.deepEqual(parentOp,{$mul: {"child.foo.bar": 2}});assert(newParent.child.foo.bar===246);

updateAndRestore()

Assign new values and create a new instance of the original class ( = Restoration).

updateAndRestore<T: Restorable>(obj: T,uOp: SetOperand|UpdateOperation|<SetOperand|UpdateOperation>[]
): T

obj must be Restorable.

What is "Restorable"?

Restorable is a characteristic of JavaScript class instances which meets the following requirement.

constjsonStr=JSON.stringify(instance);constplain=JSON.parse(jsonStr);constnewInstance=newTheClass(plain);assert.deepEqual(newInstance,instance);

Roughly, Restorable object is an instance which can re-created by passing its JSON object to the class constructor. Restorablility can be guaranteed by @Restorable decorator (TypeScript's grammer, see the following example).

import{Restorable}from"sp2";
@Restorable<Person>()classPerson{name: Name;constructor(params: {name: {first: string;last: string}}){this.name=newName(params.name);}}
@Restorable<Name>()className{first: string;last: string;constructor(params: {first: string;last: string}){this.first=params.first;this.last=params.last;}}

It may look awkward, but it actually checks if the passed class is restorable.

Parameters

obj

A Restorable instance.

uOp

UpdateOperation or SetOperand. See update() API docs.

Example

className{constructor(params){this.first=params.first;this.last=params.last;}}classPerson{constructor(params){this.name=newName(params.name);this.age=params.age;}}constperson=newPerson({name: {first: "Shin",last: "Suzuki"},age: 21,});constpersonWithRealAge=updateAndRestore(person,{$inc: {age: 10}});assert(personWithRealAgeinstanceofPerson);assert(personWithRealAge.nameinstanceofName);assert(personWithRealAge.age===31);

updatePropAndRestore()

The same arguments as updateProp() but it also restores the original object.

toJSON()

Convert UpdateOperation to Restorable JSON format.

import{toJSON}from"sp2";className{constructor(params){this.first=params.first;this.last=params.last;}}constperson={name: newName({first: "Shin",last: "Suzuki"})};constop={$restore: {name: Name},$set: {"name.first": "Shun"}};assert.deepEqual(JSON.parse(JSON.stringify(op)).$restore.name,{});// classes re converted to {} over serializationassert.deepEqual(JSON.parse(JSON.stringify(toJSON(op))).$restore.name,"");

mergeOperations()

[Experimental] Merge UpdateOperations into one UpdateOperation.

import{mergeOperations}from"sp2";constmerged=mergeOperations({$set: {foo: 123}},{$inc: {count: 1}});assert.deepEqual(merged,{$set: {foo: 123},$inc: {count: 1},});

normalizeOperation()

Convert SetOperand to normalized operation.

import{normalizeOperation}from'sp2'constop={'baz.biz': 3})assert.deepEqual(normalizeOperation(op),{$set: {'baz.biz': 3}})

Update Operators

Almost the same as MongoDB's Update Operators.

$inc

An operator to increment number values.

constvalue=update({a: 10,b: 100},{$inc: {a: 2,b: -3}});assert(value.a===12);assert(value.b===97);

$set

An operator to set values.

constvalue=update({a: "foo",b: 100},{$set: {a: "bar",b: 101}});assert(value.a==="bar");assert(value.b===101);

$set operator can be omitted when the whole operation is$set.

constvalue=update({a: "foo",b: 100},{a: "bar",b: 101});

$min

An operator to compare the existing value with the given operand and set smaller one.

constvalue=update({a: 10,b: 100},{$min: {a: 8,b: 101}});assert(value.a===8);assert(value.b===100);

$max

An operator to compare the existing value with the given operand and set greater one.

constvalue=update({a: 10,b: 100},{$max: {a: 8,b: 101}});assert(value.a===10);assert(value.b===101);

$mul

An operator to multiply number values.

constvalue=update({a: 10,b: 100},{$mul: {a: 2,b: 0}});assert(value.a===20);assert(value.b===0);

$addToSet

An operator to add element(s) to array values when the same value(s) doesn't exist.

constvalue=update({arr: [{a: 1},{a: 88}]},{$addToSet: {arr: {a: 3}}});assert.deepEqual(value.arr,[{a: 1},{a: 88},{a: 3}]);

$each modifier can be available like MongoDB.

constvalue=update({arr: [{a: 1},{a: 88}]},{$addToSet: {arr: {$each: [{a: 1},{a: 3},{a: 5}]}}});assert.deepEqual(value.arr,[{a: 1},{a: 88},{a: 3},{a: 5}]);

$pop

An operator to pop/shift an element from array values.

Pop:

constobj={categories: ["fashion","news","cooking-recipes"]};constnewObj=update(obj,{$pop: {categories: 1}});assert.deepEqual(newObj.categories,["fashion","news"]);

Shift:

constobj={categories: ["fashion","news","cooking-recipes"]};constnewObj=update(obj,{$pop: {categories: -1}});assert.deepEqual(newObj.categories,["news","cooking-recipes"]);

$pull

An operator to remove elements in array matching the given condition. For all condition definitions, see mongolike-operations/find-operation.js.flow.

They are almost compatible with MongoDB's Query Operators.

typePullOperator={[field: DocumentPath]: QueryCondition|EqCondition};// type QueryCondition => See the link above.typeEqCondition=Object|Array<Basic>|string|number|boolean;
constobj={categories: ["fashion","news","cooking-recipes"]};constnewObj=update(obj,{$pull: {categories: {$regex: /fash/}}});assert.deepEqual(newObj.categories,["news","cooking-recipes"]);

$push

An operator to add/sort/slice/splice element(s) to array values.

Add a value:

constobj={users: [{id: "user1"},{id: "user2"},{id: "user3"}]};constnewObj=update(obj,{$push: {users: {id: "user4"}}});assert.deepEqual(newObj,{users: [{id: "user1"},{id: "user2"},{id: "user3"},{id: "user4"}],});

Add values:

constobj={users: [{id: "user1"},{id: "user2"},{id: "user3"}]};constnewObj=update(obj,{$push: {users: {$each: [{id: "user4"},{id: "user5"},{id: "user6"}]},},});assert.deepEqual(newObj,{users: [{id: "user1"},{id: "user2"},{id: "user3"},{id: "user4"},{id: "user5"},{id: "user6"},],});

Add values to the specific position:

constobj={users: [{id: "user1"},{id: "user2"},{id: "user3"}]};constnewObj=update(obj,{$push: {users: {$each: [{id: "user4"},{id: "user5"},{id: "user6"}],$position: 1,},},});assert.deepEqual(newObj,{users: [{id: "user1"},{id: "user4"},{id: "user5"},{id: "user6"},{id: "user2"},{id: "user3"},],});

Sort values:

constobj={users: [{id: "user2",age: 31},{id: "user4",age: 35},{id: "user6",age: 24},],};constnewObj=update(obj,{$push: {users: {$each: [{id: "user1",age: 36},{id: "user3",age: 31},{id: "user5",age: 37},],$sort: {age: -1,id: 1},},},});assert.deepEqual(newObj,{users: [{id: "user5",age: 37},{id: "user1",age: 36},{id: "user4",age: 35},{id: "user2",age: 31},{id: "user3",age: 31},{id: "user6",age: 24},],});

Slice values:

constobj={users: [{id: "user2"},{id: "user4"},{id: "user6"}]};constnewObj=update(obj,{$push: {users: {$each: [{id: "user1"},{id: "user3"},{id: "user5"}],$slice: 3,$sort: {id: -1},},},});assert.deepEqual(newObj,{users: [{id: "user6"},{id: "user5"},{id: "user4"}],});

Slice with negative number:

constobj={users: [{id: "user2"},{id: "user4"},{id: "user6"}]};constnewObj=update(obj,{$push: {users: {$each: [{id: "user1"},{id: "user3"},{id: "user5"}],$slice: -4,},},});assert.deepEqual(newObj,{users: [{id: "user6"},{id: "user1"},{id: "user3"},{id: "user5"}],});

$bit

An operator to execute bitwise operations.

constobj={flags: parseInt("1010",10)};constnewObj=update(obj,{$bit: {flags: {and: parseInt("0101",10)}}});assert(newObj.flags.toString(2)==="1100000");

$unset

An operator to remove values.

constobj={categories: ["fashion","news","cooking-recipes"],name: {first: "Shin",last: "Suzuki"},};constnewObj=update(obj,{$unset: {"categories[1]": "","name.last": ""},});assert.deepEqual(newObj,{categories: ["fashion",null,"cooking-recipes"],name: {first: "Shin"},});

$rename

An operator to rename field names.

constobj={ttle: "October",names: [{first: "Shin",lsat: "Suzuki"}],};constnewObj=update(obj,{$rename: {ttle: "title","names[0].lsat": "last","names[0].nonExistingField": "abc",// no effect with non-existing field},});assert.deepEqual(newObj,{title: "October",names: [{first: "Shin",last: "Suzuki"}],});

Note that this operator is a bit different from MongoDB's $rename operator.

The operands are not 'Dot Notation' but field names. The following sample object in MongoDB

{$rename: {"name.first": "name.fname"}}

will be the following object in sp2.

{$rename: {"name.first": "fname"}}

See that the value doesn't contain "name".

$restore

An operator to construct instance of the given path. New operator, Not defined at MongoDB.

typeRestoreOperator={[field: DocumentPath]: ""|Class<Restorable>,};

Example:

constuser=newUser({id: "user1",name: {first: "Shin",last: "Suzuki"},age: {value: 31},});constnewUser=update(user,{$inc: {"age.value": 1},$set: {id: "user001","name.first": "Shinji",name2: {first: "Shinzo",last: "Sasaki"},},$restore: {name: "",name2: Name,age: Age},});constexpectedNewUser={id: "user001",name: newName({first: "Shinji",last: "Suzuki"}),name2: newName({first: "Shinzo",last: "Sasaki"}),age: newAge({value: 32}),};assert(newUser.nameinstanceofName);assert(newUser.name2instanceofName);assert(newUser.ageinstanceofAge);assert.deepEqual(newUser,expectedNewUser);

Once RestoreOperator is JSON.stringify-ed, the fields with Class<Restorable> will be removed. To avoid this, you can choose two alternatives.

  1. Implement static method toJSON() to classes.
classFoo{statictoJSON(){return"";}}
  1. Use updateOperationToJSON() function from oad-utils
import{updateOperationToJSON}from"oad-utils";constoperation={$restore: {foo: Foo}};JSON.stringify(updateOperationToJSON(operation));// {"$restore":{"foo":""}}

oad-utils is also one of Phenyl family offering OAD-related utility functions.

LICENSE

Apache License 2.0

About

Immutable updater of objects using JSON operation.

Topics

Resources

Stars

11 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages