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);
};
_.extend(
AV.Op.prototype,
/** @lends AV.Op.prototype */ {
_initialize: function() {},
}
);
_.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(opName, decoder) {
AV.Op._opDecoderMap[opName] = decoder;
},
/**
* Converts a json object into an instance of a subclass of AV.Op.
* @private
*/
_decode: function(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(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 AV.
* @return {Object}
*/
toJSON: function() {
return AV._encode(this.value());
},
_mergeWithPrevious: function(previous) {
return this;
},
_estimate: function(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() {
return { __op: 'Delete' };
},
_mergeWithPrevious: function(previous) {
return this;
},
_estimate: function(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(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 AV.
* @return {Object}
*/
toJSON: function() {
return { __op: 'Increment', amount: this._amount };
},
_mergeWithPrevious: function(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 new Error('Op is invalid after previous op.');
}
},
_estimate: function(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
* BitAnd is an atomic operation where the given value will be bit and to the
* value than is stored in this field.
*/
AV.Op.BitAnd = AV.Op._extend(
/** @lends AV.Op.BitAnd.prototype */ {
_initialize(value) {
this._value = value;
},
value() {
return this._value;
},
/**
* Returns a JSON version of the operation suitable for sending to AV.
* @return {Object}
*/
toJSON() {
return { __op: 'BitAnd', value: this.value() };
},
_mergeWithPrevious(previous) {
if (!previous) {
return this;
} else if (previous instanceof AV.Op.Unset) {
return new AV.Op.Set(0);
} else if (previous instanceof AV.Op.Set) {
return new AV.Op.Set(previous.value() & this.value());
} else {
throw new Error('Op is invalid after previous op.');
}
},
_estimate(oldValue) {
return oldValue & this.value();
},
}
);
AV.Op._registerDecoder('BitAnd', function(json) {
return new AV.Op.BitAnd(json.value);
});
/**
* @private
* @class
* BitOr is an atomic operation where the given value will be bit and to the
* value than is stored in this field.
*/
AV.Op.BitOr = AV.Op._extend(
/** @lends AV.Op.BitOr.prototype */ {
_initialize(value) {
this._value = value;
},
value() {
return this._value;
},
/**
* Returns a JSON version of the operation suitable for sending to AV.
* @return {Object}
*/
toJSON() {
return { __op: 'BitOr', value: this.value() };
},
_mergeWithPrevious(previous) {
if (!previous) {
return this;
} else if (previous instanceof AV.Op.Unset) {
return new AV.Op.Set(this.value());
} else if (previous instanceof AV.Op.Set) {
return new AV.Op.Set(previous.value() | this.value());
} else {
throw new Error('Op is invalid after previous op.');
}
},
_estimate(oldValue) {
return oldValue | this.value();
},
}
);
AV.Op._registerDecoder('BitOr', function(json) {
return new AV.Op.BitOr(json.value);
});
/**
* @private
* @class
* BitXor is an atomic operation where the given value will be bit and to the
* value than is stored in this field.
*/
AV.Op.BitXor = AV.Op._extend(
/** @lends AV.Op.BitXor.prototype */ {
_initialize(value) {
this._value = value;
},
value() {
return this._value;
},
/**
* Returns a JSON version of the operation suitable for sending to AV.
* @return {Object}
*/
toJSON() {
return { __op: 'BitXor', value: this.value() };
},
_mergeWithPrevious(previous) {
if (!previous) {
return this;
} else if (previous instanceof AV.Op.Unset) {
return new AV.Op.Set(this.value());
} else if (previous instanceof AV.Op.Set) {
return new AV.Op.Set(previous.value() ^ this.value());
} else {
throw new Error('Op is invalid after previous op.');
}
},
_estimate(oldValue) {
return oldValue ^ this.value();
},
}
);
AV.Op._registerDecoder('BitXor', function(json) {
return new AV.Op.BitXor(json.value);
});
/**
* @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(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 AV.
* @return {Object}
*/
toJSON: function() {
return { __op: 'Add', objects: AV._encode(this.objects()) };
},
_mergeWithPrevious: function(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 new Error('Op is invalid after previous op.');
}
},
_estimate: function(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(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(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 AV.
* @return {Object}
*/
toJSON: function() {
return { __op: 'AddUnique', objects: AV._encode(this.objects()) };
},
_mergeWithPrevious: function(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 new Error('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);
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(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(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 AV.
* @return {Object}
*/
toJSON: function() {
return { __op: 'Remove', objects: AV._encode(this.objects()) };
},
_mergeWithPrevious: function(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 new Error('Op is invalid after previous op.');
}
},
_estimate: function(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(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(adds, removes) {
this._targetClassName = null;
var self = this;
var pointerToId = function(object) {
if (object instanceof AV.Object) {
if (!object.id) {
throw new Error(
"You can't add an unsaved AV.Object to a relation."
);
}
if (!self._targetClassName) {
self._targetClassName = object.className;
}
if (self._targetClassName !== object.className) {
throw new Error(
'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() {
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() {
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() {
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 AV.Op.Unset) {
throw new Error("You can't modify a relation after deleting it.");
} else if (previous instanceof AV.Op.Relation) {
if (
previous._targetClassName &&
previous._targetClassName !== this._targetClassName
) {
throw new Error(
'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 new Error('Op is invalid after previous op.');
}
},
_estimate: function(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 new Error(
'Related object must be a ' +
oldValue.targetClassName +
', but a ' +
this._targetClassName +
' was passed in.'
);
}
} else {
oldValue.targetClassName = this._targetClassName;
}
}
return oldValue;
} else {
throw new Error('Op is invalid after previous op.');
}
},
}
);
AV.Op._registerDecoder('AddRelation', function(json) {
return new AV.Op.Relation(AV._decode(json.objects), []);
});
AV.Op._registerDecoder('RemoveRelation', function(json) {
return new AV.Op.Relation([], AV._decode(json.objects));
});
};