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);- 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.
npm install sp2yarn add sp2import{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.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.

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).TBD
TBD
constoperation={$set: {"foo.bar[0].baz": 123},/*^^^^ UpdateOperator ^^^^^^^^^^^^^^^^ DocumentPath ^^^ UpdateOperandValue ^^^^^^^^^^^^^^^^^^^^^^^^^ UpdateOperand ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UpdateOperation};*/operationvalue is UpdateOperation.$setis UpdateOperator.{ 'foo.bar[0].baz': 123 }is UpdateOperand. In the example, it's SetOperand.foo.bar[0].bazis DocumentPath.123is UpdateOperandValue.
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 object following the given operation(s).
update(obj: Object,
...operations: <SetOperand|UpdateOperation>[]): ObjectObject 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 copiedUpdateOperation, 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, ...)Update the property of the obj located at the given documentPath following the given operation.
updateProp(obj: Object,docPath: DocumentPath,uOp: SetOperand|UpdateOperation): ObjectObject containing an object to be updated. Note that obj is unchanged after the call.
UpdateOperation or SetOperand. See update() API docs.
import{updateProp}from"sp2";constobj={foo: {bar: 1},baz: {biz: 2}};constnewObj=updateProp(obj,"baz",{$inc: {biz: 1}});assert(newObj.baz.biz===3);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): UpdateOperationDocumentPath of the new target object (target itself is not given).
UpdateOperation or SetOperand. operation to be modified. Note that operation itself is unchanged. New operation object is returned.
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);Assign new values and create a new instance of the original class ( = Restoration).
updateAndRestore<T: Restorable>(obj: T,uOp: SetOperand|UpdateOperation|<SetOperand|UpdateOperation>[]
): Tobj must be 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.
A Restorable instance.
UpdateOperation or SetOperand. See update() API docs.
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);The same arguments as updateProp() but it also restores the original object.
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,"");[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},});Convert SetOperand to normalized operation.
import{normalizeOperation}from'sp2'constop={'baz.biz': 3})assert.deepEqual(normalizeOperation(op),{$set: {'baz.biz': 3}})Almost the same as MongoDB's Update Operators.
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);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});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);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);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);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}]);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"]);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"]);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"}],});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");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"},});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".
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.
- Implement static method
toJSON()to classes.
classFoo{statictoJSON(){return"";}}- Use
updateOperationToJSON()function fromoad-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.
Apache License 2.0
