leancloud-storage
Version:
LeanCloud JavaScript SDK.
533 lines (473 loc) • 15.9 kB
JavaScript
"use strict";
var _ = require('underscore');
module.exports = function (AV) {
/**
* @private
* @class
* A AV.Op is an atomic operation that can be applied to a field in a
* AV.Object. For example, calling <code>object.set("foo", "bar")</code>
* is an example of a AV.Op.Set. Calling <code>object.unset("foo")</code>
* is a AV.Op.Unset. These operations are stored in a AV.Object and
* sent to the server as part of <code>object.save()</code> operations.
* Instances of AV.Op should be immutable.
*
* You should not create subclasses of AV.Op or instantiate AV.Op
* directly.
*/
AV.Op = function () {
this._initialize.apply(this, arguments);
};
AV.Op.prototype = {
_initialize: function _initialize() {}
};
_.extend(AV.Op, {
/**
* To create a new Op, call AV.Op._extend();
* @private
*/
_extend: AV._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 AV.Op.
* @private
*/
_registerDecoder: function _registerDecoder(opName, decoder) {
AV.Op._opDecoderMap[opName] = decoder;
},
/**
* Converts a json object into an instance of a subclass of AV.Op.
* @private
*/
_decode: function _decode(json) {
var decoder = AV.Op._opDecoderMap[json.__op];
if (decoder) {
return decoder(json);
} else {
return undefined;
}
}
});
/*
* Add a handler for Batch ops.
*/
AV.Op._registerDecoder("Batch", function (json) {
var op = null;
AV._arrayEach(json.ops, function (nextOp) {
nextOp = AV.Op._decode(nextOp);
op = nextOp._mergeWithPrevious(op);
});
return op;
});
/**
* @private
* @class
* A Set operation indicates that either the field was changed using
* AV.Object.set, or it is a mutable container that was detected as being
* changed.
*/
AV.Op.Set = AV.Op._extend( /** @lends AV.Op.Set.prototype */{
_initialize: function _initialize(value) {
this._value = value;
},
/**
* Returns the new value of this field after the set.
*/
value: function value() {
return this._value;
},
/**
* Returns a JSON version of the operation suitable for sending to AV.
* @return {Object}
*/
toJSON: function toJSON() {
return AV._encode(this.value());
},
_mergeWithPrevious: function _mergeWithPrevious(previous) {
return this;
},
_estimate: function _estimate(oldValue) {
return this.value();
}
});
/**
* A sentinel value that is returned by AV.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.
*/
AV.Op._UNSET = {};
/**
* @private
* @class
* An Unset operation indicates that this field has been deleted from the
* object.
*/
AV.Op.Unset = AV.Op._extend( /** @lends AV.Op.Unset.prototype */{
/**
* Returns a JSON version of the operation suitable for sending to AV.
* @return {Object}
*/
toJSON: function toJSON() {
return { __op: "Delete" };
},
_mergeWithPrevious: function _mergeWithPrevious(previous) {
return this;
},
_estimate: function _estimate(oldValue) {
return AV.Op._UNSET;
}
});
AV.Op._registerDecoder("Delete", function (json) {
return new AV.Op.Unset();
});
/**
* @private
* @class
* An Increment is an atomic operation where the numeric value for the field
* will be increased by a given amount.
*/
AV.Op.Increment = AV.Op._extend(
/** @lends AV.Op.Increment.prototype */{
_initialize: function _initialize(amount) {
this._amount = amount;
},
/**
* Returns the amount to increment by.
* @return {Number} the amount to increment by.
*/
amount: function amount() {
return this._amount;
},
/**
* Returns a JSON version of the operation suitable for sending to AV.
* @return {Object}
*/
toJSON: function toJSON() {
return { __op: "Increment", amount: this._amount };
},
_mergeWithPrevious: function _mergeWithPrevious(previous) {
if (!previous) {
return this;
} else if (previous instanceof AV.Op.Unset) {
return new AV.Op.Set(this.amount());
} else if (previous instanceof AV.Op.Set) {
return new AV.Op.Set(previous.value() + this.amount());
} else if (previous instanceof AV.Op.Increment) {
return new AV.Op.Increment(this.amount() + previous.amount());
} else {
throw "Op is invalid after previous op.";
}
},
_estimate: function _estimate(oldValue) {
if (!oldValue) {
return this.amount();
}
return oldValue + this.amount();
}
});
AV.Op._registerDecoder("Increment", function (json) {
return new AV.Op.Increment(json.amount);
});
/**
* @private
* @class
* Add is an atomic operation where the given objects will be appended to the
* array that is stored in this field.
*/
AV.Op.Add = AV.Op._extend( /** @lends AV.Op.Add.prototype */{
_initialize: function _initialize(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 objects() {
return this._objects;
},
/**
* Returns a JSON version of the operation suitable for sending to AV.
* @return {Object}
*/
toJSON: function toJSON() {
return { __op: "Add", objects: AV._encode(this.objects()) };
},
_mergeWithPrevious: function _mergeWithPrevious(previous) {
if (!previous) {
return this;
} else if (previous instanceof AV.Op.Unset) {
return new AV.Op.Set(this.objects());
} else if (previous instanceof AV.Op.Set) {
return new AV.Op.Set(this._estimate(previous.value()));
} else if (previous instanceof AV.Op.Add) {
return new AV.Op.Add(previous.objects().concat(this.objects()));
} else {
throw "Op is invalid after previous op.";
}
},
_estimate: function _estimate(oldValue) {
if (!oldValue) {
return _.clone(this.objects());
} else {
return oldValue.concat(this.objects());
}
}
});
AV.Op._registerDecoder("Add", function (json) {
return new AV.Op.Add(AV._decode(undefined, json.objects));
});
/**
* @private
* @class
* 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.
*/
AV.Op.AddUnique = AV.Op._extend(
/** @lends AV.Op.AddUnique.prototype */{
_initialize: function _initialize(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 objects() {
return this._objects;
},
/**
* Returns a JSON version of the operation suitable for sending to AV.
* @return {Object}
*/
toJSON: function toJSON() {
return { __op: "AddUnique", objects: AV._encode(this.objects()) };
},
_mergeWithPrevious: function _mergeWithPrevious(previous) {
if (!previous) {
return this;
} else if (previous instanceof AV.Op.Unset) {
return new AV.Op.Set(this.objects());
} else if (previous instanceof AV.Op.Set) {
return new AV.Op.Set(this._estimate(previous.value()));
} else if (previous instanceof AV.Op.AddUnique) {
return new AV.Op.AddUnique(this._estimate(previous.objects()));
} else {
throw "Op is invalid after previous op.";
}
},
_estimate: function _estimate(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);
AV._arrayEach(this.objects(), function (obj) {
if (obj instanceof AV.Object && obj.id) {
var matchingObj = _.find(newValue, function (anObj) {
return anObj instanceof AV.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;
}
}
});
AV.Op._registerDecoder("AddUnique", function (json) {
return new AV.Op.AddUnique(AV._decode(undefined, json.objects));
});
/**
* @private
* @class
* Remove is an atomic operation where the given objects will be removed from
* the array that is stored in this field.
*/
AV.Op.Remove = AV.Op._extend( /** @lends AV.Op.Remove.prototype */{
_initialize: function _initialize(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 objects() {
return this._objects;
},
/**
* Returns a JSON version of the operation suitable for sending to AV.
* @return {Object}
*/
toJSON: function toJSON() {
return { __op: "Remove", objects: AV._encode(this.objects()) };
},
_mergeWithPrevious: function _mergeWithPrevious(previous) {
if (!previous) {
return this;
} else if (previous instanceof AV.Op.Unset) {
return previous;
} else if (previous instanceof AV.Op.Set) {
return new AV.Op.Set(this._estimate(previous.value()));
} else if (previous instanceof AV.Op.Remove) {
return new AV.Op.Remove(_.union(previous.objects(), this.objects()));
} else {
throw "Op is invalid after previous op.";
}
},
_estimate: function _estimate(oldValue) {
if (!oldValue) {
return [];
} else {
var newValue = _.difference(oldValue, this.objects());
// If there are saved AV Objects being removed, also remove them.
AV._arrayEach(this.objects(), function (obj) {
if (obj instanceof AV.Object && obj.id) {
newValue = _.reject(newValue, function (other) {
return other instanceof AV.Object && other.id === obj.id;
});
}
});
return newValue;
}
}
});
AV.Op._registerDecoder("Remove", function (json) {
return new AV.Op.Remove(AV._decode(undefined, json.objects));
});
/**
* @private
* @class
* A Relation operation indicates that the field is an instance of
* AV.Relation, and objects are being added to, or removed from, that
* relation.
*/
AV.Op.Relation = AV.Op._extend(
/** @lends AV.Op.Relation.prototype */{
_initialize: function _initialize(adds, removes) {
this._targetClassName = null;
var self = this;
var pointerToId = function pointerToId(object) {
if (object instanceof AV.Object) {
if (!object.id) {
throw "You can't add an unsaved AV.Object to a relation.";
}
if (!self._targetClassName) {
self._targetClassName = object.className;
}
if (self._targetClassName !== object.className) {
throw "Tried to create a AV.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 AV.Object that are being added to the
* relation.
* @return {Array}
*/
added: function added() {
var self = this;
return _.map(this.relationsToAdd, function (objectId) {
var object = AV.Object._create(self._targetClassName);
object.id = objectId;
return object;
});
},
/**
* Returns an array of unfetched AV.Object that are being removed from
* the relation.
* @return {Array}
*/
removed: function removed() {
var self = this;
return _.map(this.relationsToRemove, function (objectId) {
var object = AV.Object._create(self._targetClassName);
object.id = objectId;
return object;
});
},
/**
* Returns a JSON version of the operation suitable for sending to AV.
* @return {Object}
*/
toJSON: function toJSON() {
var adds = null;
var removes = null;
var self = this;
var idToPointer = function idToPointer(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 _mergeWithPrevious(previous) {
if (!previous) {
return this;
} else if (previous instanceof AV.Op.Unset) {
throw "You can't modify a relation after deleting it.";
} else if (previous instanceof AV.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 AV.Op.Relation(newAdd, newRemove);
newRelation._targetClassName = this._targetClassName;
return newRelation;
} else {
throw "Op is invalid after previous op.";
}
},
_estimate: function _estimate(oldValue, object, key) {
if (!oldValue) {
var relation = new AV.Relation(object, key);
relation.targetClassName = this._targetClassName;
} else if (oldValue instanceof AV.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.";
}
}
});
AV.Op._registerDecoder("AddRelation", function (json) {
return new AV.Op.Relation(AV._decode(undefined, json.objects), []);
});
AV.Op._registerDecoder("RemoveRelation", function (json) {
return new AV.Op.Relation([], AV._decode(undefined, json.objects));
});
};