Layer Patch Javascript Utility
For more information about why Layer-Patch, read the Layer-Patch Format spec.
The goal of this utility is to take as input
- Layer Patch Operations Arrays
- An object to modify
Installation
NPM
The recommended approach for installation is npm:
npm install layer-patch
Your initialization code will then look like:
var LayerPatchParser = require("layer-patch");
var parser = new LayerPatchParser({});
Github
You can directly download the file layer-patch.js and load that from a script tag. If you load it this way, your initialization code will look like:
var parser = new LayerPatchParser({});
You can download and build the repo itself via:
> git clone git@github.com:layerhq/node-layer-patch.git
> cd node-layer-patch
> npm install
> npm test
Basic Example
var parser = new layer.js.LayerPatchParser({});
var testObj = {
"a": "Hello",
"b": "There"
};
parser.parse({
object: testObj,
operations: [
{"operation": "set", "property": "a", "value": "Goodbye"},
{"operation": "set", "property": "c", "value": 5}
]
});
The above example transforms testObj
to
{
"a": "Goodbye",
"b": "There",
"c": 5
}
Layer Websocket Example
This example shows using this library for receiving operations from the Layer Platform's Websocket API.
It depends upon the getObjectCallback and changeCallbacks documented below.
var objectCache = {};
var EventManager = new EventManager();
var parser = new LayerParser({
getObjectCallback: function(id) {
return objectCache[id]
},
createObjectCallback: function(id, value) {
objectCache[id] = value;
return value;
},
changeCallbacks: {
Message: {
all: function(object, newValue, oldValue, paths) {
console.log(object.id + " has changed " + paths.join(", ") + " from " + newValue + " to " + oldValue);
EventManager.trigger("patch", "Message", object, newValue, oldValue, paths);
}
},
Conversation: {
all: function(object, newValue, oldValue, paths) {
console.log(object.id + " has changed " + paths.join(", ") + " from " + newValue + " to " + oldValue);
EventManager.trigger("patch", "Conversation", object, newValue, oldValue, paths);
}
}
}
});
socket.addEventListener("message", function(evt) {
var msg = JSON.parse(evt.data);
try {
switch(msg.type + "." + msg.operation) {
case "change.create":
EventManager.trigger("create", msg.object.type, msg.data);
objectCache[msg.object.id] = msg.data;
break;
case "change.delete":
EventManager.trigger("delete", msg.object.type, objectCache[msg.object.id]);
delete objectCache[msg.object.id];
break;
case "change.patch":
var objectToChange = objectCache[msg.object.id];
if (objectToChange) {
parser.parse({
object: objectToChange,
type: msg.object.type,
operations: msg.data
});
}
break;
}
} catch(e) {
console.error("layer-patch Error: " + e);
}
});
Library Properties
The parser takes a number of optional parameters when initializing it. Many of these depend upon
the type
parameter.
Every call to the parse
method has an input of type
:
parser.parse({
object: testObj,
type: "Person",
operations: ops
});
This type
value is used as an index into many of the configuration properties shown below.
Note that subproperty names are NOT supported in any of these configurations. For example, if you have a property called "metadata" you can use any of these configuration parameters to affect "metadata", but if an operation were to set "metadata.age", configurations on "metadata" would continue to apply, but you can not add configurations for the "age" subproperty.
getObjectCallback
The getObjectCallback allows the parser to handle operations such as
[{"operation": "set", "property": "friend", "id": "fred"}]
As the operation is setting by id rather than by value, the parser needs a way to lookup the object identified by "fred". The parser will use the getObjectCallback
method provided to find the object specified by "fred" and use that as the value.
var objectCache = {
"fred": {
"firstName": "fred",
"lastName": "flinstone",
"status": "stoneAged"
}
};
var getObjectCallback = function(id) {
return objectCache[id];
}
var parser = new layer.js.LayerPatchParser({
getObjectCallback: getObjectCallback
});
var testObj = {
"a": "Hello",
"b": "There",
"friend": null
};
parser.parse({
object: testObj,
operations: [{"operation": "set", "property": "friend", "id": "fred"}]
});
The above operation will result in a final state for testObj:
{
"a": "Hello",
"b": "There",
"friend": {
"firstName": "fred",
"lastName": "flinstone",
"status": "stoneAged"
}
}
doesObjectMatchIdCallback
When adding or removing objects to a set, a way of comparing objects is needed. While adding/removing objects is only allowed by passing in an id rather than object, we need a way to compare that id to the objects in the set. The doesObjectMatchIdCallback method will be called on each object in the set and returns true if its a match. If its a match, an add
operation will determine that the object is already present and does not need adding; a remove
operation will remove the matching entry.
Note that if using the Layer Platform Websocket, this method is not required; sets managed by Layer
do not contain objects.
function doesObjectMatchIdCallback(id, obj) {
return obj.id == id;
}
createObjectCallback
The createObjectCallback allows the parser to handle operations such as
[{"operation": "set", "property": "friend", "id": "fred", "value": {"id": "fred", "last_name": "Flinstone"}}]
As the operation is setting by id rather than by value, the parser needs a way to lookup the object identified by "fred". The parser will use the getObjectCallback
method provided to find the object specified by "fred" and use that as the value. But what happens if "fred" is not found? Either one must do an asynchronous lookup to get the value... or have the value
provided as is done in the above structure. The createObjectCallback
allows you to take that value, create and return an instance or object, and to register the object for future calls to getObjectCallback
.
var objectCache = {
"wilma": {
"firstName": "wilma",
"lastName": "flinstone",
"status": "stoneAged"
}
};
var getObjectCallback = function(id) {
return objectCache[id];
}
var createObjectCallback = function(id, obj) {
objectCache[id] = new Person(obj);
return objectCache[id];
}
var parser = new layer.js.LayerPatchParser({
getObjectCallback: getObjectCallback,
createObjectCallback: createObjectCallback
});
var testObj = {
"a": "Hello",
"b": "There",
"friend": null
};
parser.parse({
object: testObj,
operations: [{"operation": "set", "property": "friend", "id": "fred", "value": {id: "fred", last_name: "Flinstone", status: "stoneAged"}}]
});
The above operation will result in a final state for testObj:
{
"a": "Hello",
"b": "There",
"friend": {
"firstName": "fred",
"lastName": "flinstone",
"status": "stoneAged"
}
}
And a final state for objectCache
:
var objectCache = {
"wilma": {
"firstName": "wilma",
"lastName": "flinstone",
"status": "stoneAged"
},
"fred": {
"firstName": "fred",
"lastName": "flinstone",
"status": "stoneAged"
}
};
camelCase
If true, camelCase says take any uncamel cased property names in the
layer-patch operations array, and assume that the local copy uses the camelCased equivalent.
var parser = new layer.js.LayerPatchParser({
camelCase: true
});
var testObj = {
"isAFriend": true,
"myEnemy": "fred"
};
parser.parse({
object: testObj,
operations: [
{"operation": "set", "property": "is_a_friend", "value": false},
{"operation": "set", "property": "my_enemy", "value": "wilma"}
]
});
The above operation will result in a final state for testObj:
{
"isAFriend": false,
"myEnemy": "wilma"
}
propertyNameMap
The Property Name Map: Allows us to map a property name received from a
remote client/server to our local object models which may have different property names.
This is similar to the camelCase
property but provides fine grained control.
The map is organized by object type.
var propertyNameMap = {
"Person": {
"age": "year_count"
},
"Dog": {
"breed": "dog_type"
}
};
var parser = new layer.js.LayerPatchParser({
"propertyNameMap": propertyNameMap
});
var testObj = {
"year_count": 50,
"name": "fred"
};
parser.parse({
"object": testObj,
"type": "Person",
"operations": [
{"operation": "set", "property": "age", "value": 51}
]
});
The above operation will result in a final state for testObj:
{
"year_count": 51,
"name": "fred"
}
changeCallbacks
The Change Event Handler allows side effects and events to be fired based on a change
executed by the parser. The changeCallback parameter should be broken down by object type,
and each object type can either contain an "all" function or individual functions for each property
name.
var changeCallbacks = {
Person: {
year_count: function(object, oldValue, newValue, paths) {
alert("Metadata has changed; The following paths were changed: " + paths.join(", "));
},
profession: function(object, oldValue, newValue, paths) {
alert("The person is now a " + newValue);
}
},
Dog: {
all: function(object, oldValue, newValue, paths) {
alert("The dog's " + paths.join(", ") + " properties have changed to " + newValue);
}
}
}
var parser = new layer.js.LayerPatchParser({
changeCallbacks: changeCallbacks
});
var testPerson = {
"year_count": 50,
"name": "fred",
"metadata": {
"nickname": "Freaky Fred",
"last_nickname": "Friendly Fred"
}
};
var testDog = {
"breed": "poodle",
"attitude": "hostile",
"preferred_food": "zombie"
};
parser.parse({
object: testPerson,
type: "Person",
operations: [
{"operation": "set", "property": "year_count", "value": 51},
{"operation": "set", "property": "metadata.nickname", value: "Freaky Frodo"},
{"operation": "set", "property": "metadata.last_nickname", value: "Freaky Fred"}
]
});
parser.parse({
object: testDog,
type: "Dog",
operations: [
{"operation": "set", "property": "preferred_food", "value": "Frankenstein"}
]
});
The two parse calls above will result in the following events:
- year_count callback called with (testPerson, 50, 51, ["year_count"])
- metadata callback called with (testPerson, {nickname: "Freaky Fred", last_nickname: "Friendly Fred"}, {nickname: "Freaky Frodo", last_nickname: "Freaky Fred"}, ["metadata.nickname", "metadata.last_nickname"])
- all callback called with (testDog, "zombie", "Frankenstein", ["preferred_food"])
abortCallback
The Abort Event Handler allows an operation to be rejected before its performed. The abortCallback parameter should be broken down by object type,
and each object type can either contain an "all" function or individual functions for each property
name.
Each function should return true or a truthy value to abort the change; a falsy value will allow the
change to procede.
var abortCallbacks = {
Person: {
year_count: function(property, operation, value) {
if (operation == "set" && value < 0) return true;
}
},
Dog: {
all: function(property, operation, value) {
if (operation == "set" && property.match(/_at$/)) {
var d = new Date(value);
if (isNaN(d.getTime())) return true;
}
}
}
};
var parser = new layer.js.LayerPatchParser({
abortCallbacks: abortCallbacks
});
var testPerson = {
year_count: 50,
name: "fred"
};
var testDog = {
breed: "poodle",
attitude: "hostile",
preferred_food: "zombie",
ate_zombie_at: "10/10/2010"
};
parser.parse({
object: testPerson,
type: "Person",
operations: [
{"operation": "set", "property": "year_count", "value": -51},
{"operation": "set", "property": "year_count", "value": 52},
]
});
parser.parse({
object: testDog,
type: "Dog",
operations: [
{"operation": "set", "property": "ate_zombie_at", "value": "101010"},
{"operation": "set", "property": "preferred_food", "value": "Bad Dates"}
]
});
The two parse calls above will result in the following objects:
var testPerson = {
year_count: 52,
name: "fred"
};
var testDog = {
breed: "poodle",
attitude: "hostile",
preferred_food: "Bad Dates",
ate_zombie_at: "10/10/2010"
};
returnIds
When setting values by ID, proper behavior when the object associated
with that ID is not well defined by the Layer Patch specification.
The default behavior is to set the property to null if the ID is not
found. Setting the returnIds property to true will set the property
to the string ID if the object is not found.
Testing
To run unit tests use the following command:
npm test
Contributing
Layer Patch Javascript Utility is an Open Source project maintained by Layer, inc. Feedback and contributions are always welcome and the maintainers try to process patches as quickly as possible. Feel free to open up a Pull Request or Issue on Github.
Contact
Layer Web SDK was developed in San Francisco by the Layer team. If you have any technical questions or concerns about this project feel free to reach out to engineers responsible for the development: