sinks
A set of simple utilities for defining, validating, updating, diffing, and ultimately syncing (get it?!) state stored as simple objects and arrays.
Small enough for use in client-side code < 2kb. In fact, it's very handy if you're looking for a way to enforce more structure in redux reducers.
Enables validation (optional), deep immutable setting of values, and efficient diffing.
What does it do?
It was designed to be part of a system where data stored as simple objects needs to be synchronized between servers and clients yet allow for disconnected changes and caching state locally then synchronizing it again when reconnected.
Basic use cases
-
You have a large object and a modified version of that large object, you want them to be the same, but you don't want to send the entire new object.
-
Defining/validating/updating objects stored in redux reducers. Updates are always immutable!
-
You want to define, at a high-level, a simple object shape where all values are optional but any values have to match defined shape and types.
-
You want to try merging two object not knowing if they have conflicting changes or not. Giving you the information about what properties are in conflict.
Main exports
getChanges(originalObject, finalState, {includeDeletions: true, ignoredKeys: []})
This will return an object containing changes that can be applied to another object using updateObject()
. If there are no changes, returns null
.
You it takes an optional options object where you can opt out of including deletions and you can specify a list of top-level object keys to ignore.
updateObject(currentObject, changes)
This returns an updated object with changes applied. These changes need to be structured as an object of paths to update.
Example of updating a nested item:
const obj1 = {
name: 'Henrik',
}
const updatedObject = updateObject(obj1, {
'favoriteColors.foo.name': 'yellow',
})
console.log(updatedObject)
Example of deleting a value:
const obj1 = {
name: 'Henrik',
something: {
foo: 'cool',
},
}
const updatedObject = updateObject(obj1, {
'something.foo': null,
})
console.log(updatedObject)
Example of updating item in an array
const obj1 = {
name: 'Henrik',
myStuff: [{ id: 'thing', description: 'pizza' }],
}
const updated = updateObject(obj1, {
'myStuff.0.description': 'skis',
})
console.log(updated)
Updating by array index doesn't work if the array doesn't exist in the object. But we can explicitly specify that an item in an array is an index by putting square brackets around it:
const obj1 = {
name: 'Henrik',
}
const updated = updateObject(obj1, {
'myStuff.[0].description': 'skis',
})
console.log(updated)
console.log(
updateObject(obj1, {
'myStuff.0.description': 'skis',
})
)
You don't have to supply a whole path if you want to set an object:
const obj1 = {
name: 'Henrik',
}
console.log(
updateObject(obj1, {
other: {
nested: 'thing',
},
})
)
Empty objects and arrays and null
values are automatically removed.
const obj1 = {
name: 'Henrik',
}
console.log(
updateObject(obj1, {
other: {
nested: {
hi: [],
},
},
})
)
setValue(obj1, keyPath, updatedValue)
This is the single-key update version of updateObject
in fact, this what updateObject
calls for each key you provide in the update object.
mergeObjects(obj1, obj2)
This will get additive changes (not deletions) from each object compared to the other, and try to build a shared object of merged changes.
It returns an object with two properties:
updated
: the new merged objectconflicts
: this property only exists if there are conflicts. These conflicts are an object keyed by conflicting key name and containing an array of original and new values for that key.
const obj1 = {
name: 'bob',
favoriteColor: 'blue',
}
const obj2 = {
name: 'sue',
age: 28,
}
const { updated, conflicts } = mergeObjects(obj1, obj2)
console.log(updated)
console.log(conflicts)
buildDefinition(definitionObject, fnsObject[optional])
Using validation and object definitions
You can optionally choose to create a definition that describes valid shape of the object. Doing this can give you some comfort at runtime that you're not getting unexpected values.
buildDefinition
returns an object with the following methods:
definition.validate(object)
takes object to validate,definition.setValue(startingObject, keyPath, newValue, shouldValidate [defaults to true])
: ,definition.update(startingObject, updatesObject, shouldValidate [defaults to true])
definition.merge(startingObject, otherObject, shouldValidate [defaults to true])
You define an object as follows. Please note that the "types" are just strings. These get mapped to functions you can supply as a second argument to buildDefinition
.
If you don't supply one, we have a simple default set of very basic type checks out of the box. Please see src/basic-types.js
. These are also exported as import { basicTypes } from 'sinks'
so they can easily be extended.
import { buildDefinition } from 'sinks'
const bareDefinition = {
lastChanged: 'timestamp',
lastSaved: 'timestamp',
created: 'timestamp',
sedationProviderName: 'str',
surgeonName: 'str',
recorderName: 'str',
office: 'str',
'trackedVitalTypes.{}.id': 'str',
'trackedVitalTypes.{}.selected': 'bool',
'trackedVitalTypes.{}.hasReceivedAutoValue': 'bool',
medications: 'arr',
'medications.[].id': 'str',
'medications.[].name': 'str',
}
const definition = buildDefinition(bareDefinition)
const startingObject = {}
try {
const newObject = definition.setValue(
startingObject,
'somethingSilly',
'blah'
)
} catch (e) {
}
const finalObject = definition.setValue(
startingObject,
'somethingSilly',
'blah',
false
)
console.log(startingObject === finalObject)
Running tests
npm test
install
npm install sinks
Change log
3.1.4
: Fixed build errors caused by recent dependency changes3.1.3
: Some more, minor performance improvements for validate() functions by doing a bit more pre-sorting.3.1.2
: Bugfixes and 10x performance improvement of validate() function.3.1.1
: Now exporting simpleObjectDeepEqual
as part of main export.3.1.0
: Added simpleObjectDeepEqual
utility for lightweight object comparisons of simple objects.3.0.4
: Added other test for getChanges
to ensure it handle nested objects with integer keys correctly.3.0.3
: Fixed bug where getChanges
not handle objects with integer keys correctly.3.0.2
: Fix bad prepublish script.3.0.1
: Removing unnecessary conditional check.3.0.0
: getChanges
now returns individual key path updates, this is important for simultaneous changes of nested objects. Technically, this should not be a breaking change unless you manually modify or somehow deal with the changes object. But since it changes the shape of something returned by public API I decided to bump the major version.2.0.0
: Now recursively removes all keys with values {}
, []
, or null
at the end of all set/update operations.1.0.0
: getChanges
now takes an options object instead of just a boolean and that option option now can take a ignoredKeys: []
option to ignore changes to specified top-level keys.0.0.1
: First public release.
credits
If you like this follow @HenrikJoreteg on twitter.
Props to Jason Miller for dlv (a dependency) and Frank Wilkerson for clean-set which I modified and included here (along with his MIT license).
license
MIT