power-assign

0.2.10 • Public • Published

power-assign CircleCI

Immutable updater of POJO using MongoDB's operator, easier access to nested values.

import { assign } from 'power-assign'
 
const obj = {
  foo: 1,
  bar: { baz: 'abc' }
}
 
const newObj = assign(obj, {
  foo: 123,
  'bar.baz': 'xyz'
 })
 
assert(newObj !== obj) // obj is unchanged.
assert(newObj.foo === 123) // assigned
assert(newObj.bar.baz === 'xyz') // assigned nested value

Installation

npm install power-assign

Using types with flow

For Flow annotations, just use /jsnext entrypoint.

import { assign } from 'power-assign/jsnext'

All the interfaces are defined in the depending module mongolike-operations.

Concept

OAD: Operations As Data

Operations As Data(OAD) is the concept of handling large JSON data that all the data operations (update/find) should be written as JSON format (≒ plain object). This power-assign handles UpdateOperation as data.

The previous example is also written as below.

const newObj = assign(obj, { $set: { foo: 123, 'bar.baz': 'xyz' } })
// this is UpdateOperation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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

Here is another example using some more operators.

const obj = {
  count: 11,
  arr: ['value1'],
  bar: { baz: 'abc' },
}
const newObj = assign(obj, {
  $inc: { count: 1 }, // $inc: operator to increment number
  $push: { arr: 'value2' }, // $push: operator to push elements to array
  $unset: { 'bar.baz': '' }, // $unset: operator to unset a value
})
 
assert(newObj.count === 12)
assert(newObj.bar.hasOwnProperty('baz') === false)
assert.deepEqual(obj.arr, ['value1', 'value2'])

Phenyl Family

power-assign is one of Phenyl Family. Phenyl is a JavaScript Server/Client framework for State Synchronization over Environment(SSoE). UpdateOperation is the key to synchronize large JSON over environment. Thus power-assign plays one of the essential roles in Phenyl.

API Documentation

Definitions

For all flow type definitions, see mongolike-operations/update-operation.js.flow.

const operation = {
  $set: { 'foo.bar[0].baz': 123 }
}
  • operation value is UpdateOperation.
  • $set is UpdateOperatorName.
  • { 'foo.bar[0].baz': 123 } is UpdateOperator. In the example, it's SetOperator as the UpdateOperatorName is $set.
  • foo.bar[0].baz is DocumentPath.
  • 123 is Operand.

DocumentPath

The same definition as Amazon DynamoDB's DocumentPath.

DocumentPath expresses nested value location.

{ 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).

power-assign incidentally supports Dot Notation in that an array is also an object. Still we recommend DocumentPath because Phenyl family prefers the expression.

assign()

Assign new values to object following the given operation(s).

assign(
  obj: Object,
  ...uOps: Array<SetOperator | UpdateOperation>
): Object

Parameters

obj

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

import { assign } from 'power-assign'
const obj = { foo: { bar: 1 }, baz: { biz: 2 } }
const newObj = assign(obj, { 'baz.biz': 3 })
assert(obj.foo === newObj.foo) // unchanged values are shallowly copied
assert(obj.baz !== newObj.baz) // changed values are not copied

uOps (variable arguments)

UpdateOperation, SetOperator. SetOperator is just a key-value pairs object.

All the operations are fulfilled in order.

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

assignToProp()

Assign new values to the property of the obj located at the given documentPath following the given operation.

assignToProp(
  obj: Object,
  docPath: DocumentPath,
  uOp: SetOperator | UpdateOperation
): Object

Parameters

obj

Object containing an object to be assigned new values. Note that obj is unchanged after the call.

uOp

UpdateOperation or SetOperator. See assign() API docs.

Example

import { assignToProp } from 'power-assign'
const obj = { foo: { bar: 1 }, baz: { biz: 2 } }
const newObj = assignToProp(obj, 'baz', { $inc: { biz: 1 } })
 
assert(newObj.baz.biz === 3)

retargetToProp()

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 retargetToProp() if only child-object-handling layer offers the child object's UpdateOperation.

retargetToProp(
  docPath: DocumentPath,
  operation: SetOperator | UpdateOperation
): UpdateOperation

Parameters

docPath

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

operation

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

Example

import { assignToProp } from 'power-assign'
const parent = { child: { foo: { bar: 123 } } }
const childOp = { $mul: { 'foo.bar': 2 } }
const parentOp = retargetToProp(parent, 'child', childOp)
 
const newParent = assign(parent, parentOp)
assert.deepEqual(parentOp, { $mul: { 'child.foo.bar': 2 } })
assert(newParent.child.foo.bar === 246)

assignWithRestoration()

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

assignWithRestoration<T: Restorable>(
  obj: T,
  uOp: SetOperator | UpdateOperation | Array<SetOperator | UpdateOperation>
): T

obj must be Restorable.

What is "Restorable"?

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

const jsonStr = JSON.stringify(instance)
const plain = JSON.parse(jsonStr)
const newInstance = new TheClass(plain)
 
assert.deepEqual(newInstance, instance)

Roughly, Restorable object is an instance which can re-created by passing its JSON object to the class constructor.

See is-restorable module for more detail.

Parameters

obj

A Restorable instance.

uOp

UpdateOperation or SetOperator. See assign() API docs.

Example

class Name {
  constructor(params) {
    this.first = params.first
    this.last = params.last
  }
}
 
class Person {
  constructor(params) {
    this.name = new Name(params.name)
    this.age = params.age
  }
}
const person = new Person({
  name: { first: 'Shin', last: 'Suzuki' },
  age: 21,
})
 
const personWithRealAge = assignWithRestoration(person, { $inc: { age: 10 } })
assert(personWithRealAge instanceof Person)
assert(personWithRealAge.name instanceof Name)
assert(personWithRealAge.age === 31)

assignToPropWithRestoration()

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

retargetToPropWithRestoration()

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

toJSON()

Convert UpdateOperation to Restorable JSON format.

import { toJSON } from 'power-assign'
class Name {
  constructor(params) {
    this.first = params.first
    this.last = params.last
  }
}
 
const person = { name: new Name({ first: 'Shin', last: 'Suzuki' }) }
const op = { $restore: { name: Name }, $set: { 'name.first': 'Shun' } }
assert.deepEqual(JSON.parse(JSON.stringify(op)).$restore.name, {}) // classes re converted to {} over serialization
assert.deepEqual(JSON.parse(JSON.stringify(toJSON(op))).$restore.name, '')

mergeOperations()

[Experimental] Merge UpdateOperations into one UpdateOperation.

import { mergeOperations } from 'power-assign'
const merged = mergeOperations(
  { $set: { foo: 123 } },
  { $inc: { count: 1 } }
)
assert.deepEqual(merged, {
  $set: { foo: 123 },
  $inc: { count: 1 }
})

normalizeOperation()

Convert SetOperator to normalized operation.

import { normalizeOperation } from 'power-assign'
const op = { '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.

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

$set

An operator to set values.

const value = assign({ 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.

const value = assign({ a: 'foo', b: 100 } , { a: 'bar', b: 101 })

$min

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

const value = assign({ 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.

const value = assign({ a: 10, b: 100 } , { $max: { a: 8, b: 101 } })
assert(value.a === 10)
assert(value.b === 101)

$mul

An operator to multiply number values.

const value = assign({ 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.

const value = assign({ 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.

const value = assign({ 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:

const obj = { categories: ['fashion', 'news', 'cooking-recipes'] }
const newObj = assign(obj, { $pop: { categories: 1 }})
assert.deepEqual(newObj.categories, ['fashion', 'news'])

Shift:

const obj = { categories: ['fashion', 'news', 'cooking-recipes'] }
const newObj = assign(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.

type PullOperator = { [field: DocumentPath]: QueryCondition | EqCondition }
// type QueryCondition => See the link above.
type EqCondition = Object | Array<Basic> | string | number | boolean
const obj = { categories: ['fashion', 'news', 'cooking-recipes'] }
const newObj = assign(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:

const obj = { users: [
  { id: 'user1' }, { id: 'user2' }, { id: 'user3' }
]}
 
const newObj = assign(obj, { $push: { users: { id: 'user4'} }})
 
assert.deepEqual(newObj, { users: [
  { id: 'user1' }, { id: 'user2' }, { id: 'user3'}, { id: 'user4' }
]})

Add values:

const obj = { users: [
  { id: 'user1' }, { id: 'user2' }, { id: 'user3' }
]}
const newObj = assign(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:

const obj = { users: [
  { id: 'user1' }, { id: 'user2' }, { id: 'user3' }
]}
const newObj = assign(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:

const obj = { users: [
  { id: 'user2', age: 31 },
  { id: 'user4', age: 35 },
  { id: 'user6', age: 24 }
]}
 
const newObj = assign(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:

const obj = { users: [
  { id: 'user2' }, { id: 'user4' }, { id: 'user6' }
]}
const newObj = assign(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:

const obj = { users: [
  { id: 'user2' }, { id: 'user4' }, { id: 'user6' }
]}
const newObj = assign(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.

const obj = { flags: parseInt('1010', 10) }
const newObj = assign(obj, { $bit: { flags: { and: parseInt('0101', 10) }}})
assert(newObj.flags.toString(2) === '1100000')

$unset

An operator to remove values.

const obj = {
  categories: ['fashion', 'news', 'cooking-recipes'],
  name: { first: 'Shin', last: 'Suzuki' }
}
const newObj = assign(obj, { $unset: { 'categories[1]': '', 'name.last': '' } })
assert.deepEqual(newObj, {
  categories: ['fashion', null, 'cooking-recipes'],
  name: { first: 'Shin' }
})

$rename

An operator to rename field names.

const obj = {
  ttle: 'October',
  names: [
    { first: 'Shin', lsat: 'Suzuki' }
  ],
}
const newObj = assign(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 power-assign.

{ $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.

type RestoreOperator = {
  [field: DocumentPath]: '' | Class<Restorable>
}

Example:

const user = new User({
  id: 'user1',
  name: { first: 'Shin', last: 'Suzuki' },
  age: { value: 31 },
})
const newUser = assign(user, {
  $inc: { 'age.value': 1 },
  $set: { id: 'user001', 'name.first': 'Shinji', name2: { first: 'Shinzo', last: 'Sasaki' } },
  $restore: { name: '', name2: Name, age: Age },
})
 
const expectedNewUser = {
  id: 'user001',
  name: new Name({ first: 'Shinji', last: 'Suzuki' }),
  name2: new Name({ first: 'Shinzo', last: 'Sasaki' }),
  age: new Age({ value: 32 }),
}
assert(newUser.name instanceof Name)
assert(newUser.name2 instanceof Name)
assert(newUser.age instanceof Age)
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.
class Foo {
  static toJSON() { return '' }
}
  1. Use updateOperationToJSON() function from oad-utils
 import { updateOperationToJSON } from 'oad-utils'
 const operation = { $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

Dependents (22)

Package Sidebar

Install

npm i power-assign

Weekly Downloads

1,758

Version

0.2.10

License

Apache License 2.0

Unpacked Size

68 kB

Total Files

13

Last publish

Collaborators

  • camcam-lemon
  • alternacrow
  • bowzstandard
  • shinout
  • naturalclar
  • sasurau4
  • wh1tecat