UNPKG

maxleap-react-native

Version:
523 lines (462 loc) 15.4 kB
'use strict'; var _ = require('underscore'); module.exports = function(ML) { /** * A ML.Op is an atomic operation that can be applied to a field in a * ML.Object. For example, calling <code>object.set("foo", "bar")</code> * is an example of a ML.Op.Set. Calling <code>object.unset("foo")</code> * is a ML.Op.Unset. These operations are stored in a ML.Object and * sent to the server as part of <code>object.save()</code> operations. * Instances of ML.Op should be immutable. * * You should not create subclasses of ML.Op or instantiate ML.Op * directly. */ ML.Op = function() { this._initialize.apply(this, arguments); }; ML.Op.prototype = { _initialize: function() {} }; _.extend(ML.Op, { /** * To create a new Op, call ML.Op._extend(); */ _extend: ML._extend, // A map of __op string to decoder function. _opDecoderMap: {}, /** * Registers a function to convert a json object with an __op field into an * instance of a subclass of ML.Op. */ _registerDecoder: function(opName, decoder) { ML.Op._opDecoderMap[opName] = decoder; }, /** * Converts a json object into an instance of a subclass of ML.Op. */ _decode: function(json) { var decoder = ML.Op._opDecoderMap[json.__op]; if (decoder) { return decoder(json); } else { return undefined; } } }); /* * Add a handler for Batch ops. */ ML.Op._registerDecoder("Batch", function(json) { var op = null; ML._arrayEach(json.ops, function(nextOp) { nextOp = ML.Op._decode(nextOp); op = nextOp._mergeWithPrevious(op); }); return op; }); /** * A Set operation indicates that either the field was changed using * ML.Object.set, or it is a mutable container that was detected as being * changed. */ ML.Op.Set = ML.Op._extend(/** @lends ML.Op.Set.prototype */ { _initialize: function(value) { this._value = value; }, /** * Returns the new value of this field after the set. */ value: function() { return this._value; }, /** * Returns a JSON version of the operation suitable for sending to ML. * @return {Object} */ toJSON: function() { return ML._encode(this.value()); }, _mergeWithPrevious: function(previous) { return this; }, _estimate: function(oldValue) { return this.value(); } }); /** * A sentinel value that is returned by ML.Op.Unset._estimate to * indicate the field should be deleted. Basically, if you find _UNSET as a * value in your object, you should remove that key. */ ML.Op._UNSET = {}; /** * An Unset operation indicates that this field has been deleted from the * object. */ ML.Op.Unset = ML.Op._extend(/** @lends ML.Op.Unset.prototype */ { /** * Returns a JSON version of the operation suitable for sending to ML. * @return {Object} */ toJSON: function() { return { __op: "Delete" }; }, _mergeWithPrevious: function(previous) { return this; }, _estimate: function(oldValue) { return ML.Op._UNSET; } }); ML.Op._registerDecoder("Delete", function(json) { return new ML.Op.Unset(); }); /** * An Increment is an atomic operation where the numeric value for the field * will be increased by a given amount. */ ML.Op.Increment = ML.Op._extend( /** @lends ML.Op.Increment.prototype */ { _initialize: function(amount) { this._amount = amount; }, /** * Returns the amount to increment by. * @return {Number} the amount to increment by. */ amount: function() { return this._amount; }, /** * Returns a JSON version of the operation suitable for sending to ML. * @return {Object} */ toJSON: function() { return { __op: "Increment", amount: this._amount }; }, _mergeWithPrevious: function(previous) { if (!previous) { return this; } else if (previous instanceof ML.Op.Unset) { return new ML.Op.Set(this.amount()); } else if (previous instanceof ML.Op.Set) { return new ML.Op.Set(previous.value() + this.amount()); } else if (previous instanceof ML.Op.Increment) { return new ML.Op.Increment(this.amount() + previous.amount()); } else { throw "Op is invalid after previous op."; } }, _estimate: function(oldValue) { if (!oldValue) { return this.amount(); } return oldValue + this.amount(); } }); ML.Op._registerDecoder("Increment", function(json) { return new ML.Op.Increment(json.amount); }); /** * Add is an atomic operation where the given objects will be appended to the * array that is stored in this field. */ ML.Op.Add = ML.Op._extend(/** @lends ML.Op.Add.prototype */ { _initialize: function(objects) { this._objects = objects; }, /** * Returns the objects to be added to the array. * @return {Array} The objects to be added to the array. */ objects: function() { return this._objects; }, /** * Returns a JSON version of the operation suitable for sending to ML. * @return {Object} */ toJSON: function() { return ML._encode(this.objects()); }, _mergeWithPrevious: function(previous) { if (!previous) { return this; } else if (previous instanceof ML.Op.Unset) { return new ML.Op.Set(this.objects()); } else if (previous instanceof ML.Op.Set) { return new ML.Op.Set(this._estimate(previous.value())); } else if (previous instanceof ML.Op.Add) { return new ML.Op.Add(previous.objects().concat(this.objects())); } else { throw "Op is invalid after previous op."; } }, _estimate: function(oldValue) { if (!oldValue) { return _.clone(this.objects()); } else { return oldValue.concat(this.objects()); } } }); ML.Op._registerDecoder("Add", function(json) { return new ML.Op.Add(ML._decode(undefined, json.objects)); }); /** * AddUnique is an atomic operation where the given items will be appended to * the array that is stored in this field only if they were not already * present in the array. */ ML.Op.AddUnique = ML.Op._extend( /** @lends ML.Op.AddUnique.prototype */ { _initialize: function(objects) { this._objects = _.uniq(objects); }, /** * Returns the objects to be added to the array. * @return {Array} The objects to be added to the array. */ objects: function() { return this._objects; }, /** * Returns a JSON version of the operation suitable for sending to ML. * @return {Object} */ toJSON: function() { return { __op: "AddUnique", objects: ML._encode(this.objects()) }; }, _mergeWithPrevious: function(previous) { if (!previous) { return this; } else if (previous instanceof ML.Op.Unset) { return new ML.Op.Set(this.objects()); } else if (previous instanceof ML.Op.Set) { return new ML.Op.Set(this._estimate(previous.value())); } else if (previous instanceof ML.Op.AddUnique) { return new ML.Op.AddUnique(this._estimate(previous.objects())); } else { throw "Op is invalid after previous op."; } }, _estimate: function(oldValue) { if (!oldValue) { return _.clone(this.objects()); } else { // We can't just take the _.uniq(_.union(...)) of oldValue and // this.objects, because the uniqueness may not apply to oldValue // (especially if the oldValue was set via .set()) var newValue = _.clone(oldValue); ML._arrayEach(this.objects(), function(obj) { if (obj instanceof ML.Object && obj.id) { var matchingObj = _.find(newValue, function(anObj) { return (anObj instanceof ML.Object) && (anObj.id === obj.id); }); if (!matchingObj) { newValue.push(obj); } else { var index = _.indexOf(newValue, matchingObj); newValue[index] = obj; } } else if (!_.contains(newValue, obj)) { newValue.push(obj); } }); return newValue; } } }); ML.Op._registerDecoder("AddUnique", function(json) { return new ML.Op.AddUnique(ML._decode(undefined, json.objects)); }); /** * Remove is an atomic operation where the given objects will be removed from * the array that is stored in this field. */ ML.Op.Remove = ML.Op._extend(/** @lends ML.Op.Remove.prototype */ { _initialize: function(objects) { this._objects = _.uniq(objects); }, /** * Returns the objects to be removed from the array. * @return {Array} The objects to be removed from the array. */ objects: function() { return this._objects; }, /** * Returns a JSON version of the operation suitable for sending to ML. * @return {Object} */ toJSON: function() { return { __op: "Remove", objects: ML._encode(this.objects()) }; }, _mergeWithPrevious: function(previous) { if (!previous) { return this; } else if (previous instanceof ML.Op.Unset) { return previous; } else if (previous instanceof ML.Op.Set) { return new ML.Op.Set(this._estimate(previous.value())); } else if (previous instanceof ML.Op.Remove) { return new ML.Op.Remove(_.union(previous.objects(), this.objects())); } else { throw "Op is invalid after previous op."; } }, _estimate: function(oldValue) { if (!oldValue) { return []; } else { var newValue = _.difference(oldValue, this.objects()); // If there are saved ML Objects being removed, also remove them. ML._arrayEach(this.objects(), function(obj) { if (obj instanceof ML.Object && obj.id) { newValue = _.reject(newValue, function(other) { return (other instanceof ML.Object) && (other.id === obj.id); }); } }); return newValue; } } }); ML.Op._registerDecoder("Remove", function(json) { return new ML.Op.Remove(ML._decode(undefined, json.objects)); }); /** * A Relation operation indicates that the field is an instance of * ML.Relation, and objects are being added to, or removed from, that * relation. */ ML.Op.Relation = ML.Op._extend( /** @lends ML.Op.Relation.prototype */ { _initialize: function(adds, removes) { this._targetClassName = null; var self = this; var pointerToId = function(object) { if (object instanceof ML.Object) { if (!object.id) { throw "You can't add an unsaved ML.Object to a relation."; } if (!self._targetClassName) { self._targetClassName = object.className; } if (self._targetClassName !== object.className) { throw "Tried to create a ML.Relation with 2 different types: " + self._targetClassName + " and " + object.className + "."; } return object.id; } return object; }; this.relationsToAdd = _.uniq(_.map(adds, pointerToId)); this.relationsToRemove = _.uniq(_.map(removes, pointerToId)); }, /** * Returns an array of unfetched ML.Object that are being added to the * relation. * @return {Array} */ added: function() { var self = this; return _.map(this.relationsToAdd, function(objectId) { var object = ML.Object._create(self._targetClassName); object.id = objectId; return object; }); }, /** * Returns an array of unfetched ML.Object that are being removed from * the relation. * @return {Array} */ removed: function() { var self = this; return _.map(this.relationsToRemove, function(objectId) { var object = ML.Object._create(self._targetClassName); object.id = objectId; return object; }); }, /** * Returns a JSON version of the operation suitable for sending to ML. * @return {Object} */ toJSON: function() { var adds = null; var removes = null; var self = this; var idToPointer = function(id) { return { __type: 'Pointer', className: self._targetClassName, objectId: id }; }; var pointers = null; if (this.relationsToAdd.length > 0) { pointers = _.map(this.relationsToAdd, idToPointer); adds = { "__op": "AddRelation", "objects": pointers }; } if (this.relationsToRemove.length > 0) { pointers = _.map(this.relationsToRemove, idToPointer); removes = { "__op": "RemoveRelation", "objects": pointers }; } if (adds && removes) { return { "__op": "Batch", "ops": [adds, removes]}; } return adds || removes || {}; }, _mergeWithPrevious: function(previous) { if (!previous) { return this; } else if (previous instanceof ML.Op.Unset) { throw "You can't modify a relation after deleting it."; } else if (previous instanceof ML.Op.Relation) { if (previous._targetClassName && previous._targetClassName !== this._targetClassName) { throw "Related object must be of class " + previous._targetClassName + ", but " + this._targetClassName + " was passed in."; } var newAdd = _.union(_.difference(previous.relationsToAdd, this.relationsToRemove), this.relationsToAdd); var newRemove = _.union(_.difference(previous.relationsToRemove, this.relationsToAdd), this.relationsToRemove); var newRelation = new ML.Op.Relation(newAdd, newRemove); newRelation._targetClassName = this._targetClassName; return newRelation; } else { throw "Op is invalid after previous op."; } }, _estimate: function(oldValue, object, key) { if (!oldValue) { var relation = new ML.Relation(object, key); relation.targetClassName = this._targetClassName; } else if (oldValue instanceof ML.Relation) { if (this._targetClassName) { if (oldValue.targetClassName) { if (oldValue.targetClassName !== this._targetClassName) { throw "Related object must be a " + oldValue.targetClassName + ", but a " + this._targetClassName + " was passed in."; } } else { oldValue.targetClassName = this._targetClassName; } } return oldValue; } else { throw "Op is invalid after previous op."; } } }); ML.Op._registerDecoder("AddRelation", function(json) { return new ML.Op.Relation(ML._decode(undefined, json.objects), []); }); ML.Op._registerDecoder("RemoveRelation", function(json) { return new ML.Op.Relation([], ML._decode(undefined, json.objects)); }); };