UNPKG

19.6 kBJavaScriptView Raw
1'use strict';
2
3var _ = require('underscore');
4
5module.exports = function (AV) {
6 /**
7 * @private
8 * @class
9 * A AV.Op is an atomic operation that can be applied to a field in a
10 * AV.Object. For example, calling <code>object.set("foo", "bar")</code>
11 * is an example of a AV.Op.Set. Calling <code>object.unset("foo")</code>
12 * is a AV.Op.Unset. These operations are stored in a AV.Object and
13 * sent to the server as part of <code>object.save()</code> operations.
14 * Instances of AV.Op should be immutable.
15 *
16 * You should not create subclasses of AV.Op or instantiate AV.Op
17 * directly.
18 */
19 AV.Op = function () {
20 this._initialize.apply(this, arguments);
21 };
22
23 _.extend(AV.Op.prototype,
24 /** @lends AV.Op.prototype */{
25 _initialize: function _initialize() {}
26 });
27
28 _.extend(AV.Op, {
29 /**
30 * To create a new Op, call AV.Op._extend();
31 * @private
32 */
33 _extend: AV._extend,
34
35 // A map of __op string to decoder function.
36 _opDecoderMap: {},
37
38 /**
39 * Registers a function to convert a json object with an __op field into an
40 * instance of a subclass of AV.Op.
41 * @private
42 */
43 _registerDecoder: function _registerDecoder(opName, decoder) {
44 AV.Op._opDecoderMap[opName] = decoder;
45 },
46
47 /**
48 * Converts a json object into an instance of a subclass of AV.Op.
49 * @private
50 */
51 _decode: function _decode(json) {
52 var decoder = AV.Op._opDecoderMap[json.__op];
53 if (decoder) {
54 return decoder(json);
55 } else {
56 return undefined;
57 }
58 }
59 });
60
61 /*
62 * Add a handler for Batch ops.
63 */
64 AV.Op._registerDecoder('Batch', function (json) {
65 var op = null;
66 AV._arrayEach(json.ops, function (nextOp) {
67 nextOp = AV.Op._decode(nextOp);
68 op = nextOp._mergeWithPrevious(op);
69 });
70 return op;
71 });
72
73 /**
74 * @private
75 * @class
76 * A Set operation indicates that either the field was changed using
77 * AV.Object.set, or it is a mutable container that was detected as being
78 * changed.
79 */
80 AV.Op.Set = AV.Op._extend(
81 /** @lends AV.Op.Set.prototype */{
82 _initialize: function _initialize(value) {
83 this._value = value;
84 },
85
86 /**
87 * Returns the new value of this field after the set.
88 */
89 value: function value() {
90 return this._value;
91 },
92
93 /**
94 * Returns a JSON version of the operation suitable for sending to AV.
95 * @return {Object}
96 */
97 toJSON: function toJSON() {
98 return AV._encode(this.value());
99 },
100
101 _mergeWithPrevious: function _mergeWithPrevious(previous) {
102 return this;
103 },
104
105 _estimate: function _estimate(oldValue) {
106 return this.value();
107 }
108 });
109
110 /**
111 * A sentinel value that is returned by AV.Op.Unset._estimate to
112 * indicate the field should be deleted. Basically, if you find _UNSET as a
113 * value in your object, you should remove that key.
114 */
115 AV.Op._UNSET = {};
116
117 /**
118 * @private
119 * @class
120 * An Unset operation indicates that this field has been deleted from the
121 * object.
122 */
123 AV.Op.Unset = AV.Op._extend(
124 /** @lends AV.Op.Unset.prototype */{
125 /**
126 * Returns a JSON version of the operation suitable for sending to AV.
127 * @return {Object}
128 */
129 toJSON: function toJSON() {
130 return { __op: 'Delete' };
131 },
132
133 _mergeWithPrevious: function _mergeWithPrevious(previous) {
134 return this;
135 },
136
137 _estimate: function _estimate(oldValue) {
138 return AV.Op._UNSET;
139 }
140 });
141
142 AV.Op._registerDecoder('Delete', function (json) {
143 return new AV.Op.Unset();
144 });
145
146 /**
147 * @private
148 * @class
149 * An Increment is an atomic operation where the numeric value for the field
150 * will be increased by a given amount.
151 */
152 AV.Op.Increment = AV.Op._extend(
153 /** @lends AV.Op.Increment.prototype */{
154 _initialize: function _initialize(amount) {
155 this._amount = amount;
156 },
157
158 /**
159 * Returns the amount to increment by.
160 * @return {Number} the amount to increment by.
161 */
162 amount: function amount() {
163 return this._amount;
164 },
165
166 /**
167 * Returns a JSON version of the operation suitable for sending to AV.
168 * @return {Object}
169 */
170 toJSON: function toJSON() {
171 return { __op: 'Increment', amount: this._amount };
172 },
173
174 _mergeWithPrevious: function _mergeWithPrevious(previous) {
175 if (!previous) {
176 return this;
177 } else if (previous instanceof AV.Op.Unset) {
178 return new AV.Op.Set(this.amount());
179 } else if (previous instanceof AV.Op.Set) {
180 return new AV.Op.Set(previous.value() + this.amount());
181 } else if (previous instanceof AV.Op.Increment) {
182 return new AV.Op.Increment(this.amount() + previous.amount());
183 } else {
184 throw new Error('Op is invalid after previous op.');
185 }
186 },
187
188 _estimate: function _estimate(oldValue) {
189 if (!oldValue) {
190 return this.amount();
191 }
192 return oldValue + this.amount();
193 }
194 });
195
196 AV.Op._registerDecoder('Increment', function (json) {
197 return new AV.Op.Increment(json.amount);
198 });
199
200 /**
201 * @private
202 * @class
203 * BitAnd is an atomic operation where the given value will be bit and to the
204 * value than is stored in this field.
205 */
206 AV.Op.BitAnd = AV.Op._extend(
207 /** @lends AV.Op.BitAnd.prototype */{
208 _initialize: function _initialize(value) {
209 this._value = value;
210 },
211 value: function value() {
212 return this._value;
213 },
214
215
216 /**
217 * Returns a JSON version of the operation suitable for sending to AV.
218 * @return {Object}
219 */
220 toJSON: function toJSON() {
221 return { __op: 'BitAnd', value: this.value() };
222 },
223 _mergeWithPrevious: function _mergeWithPrevious(previous) {
224 if (!previous) {
225 return this;
226 } else if (previous instanceof AV.Op.Unset) {
227 return new AV.Op.Set(0);
228 } else if (previous instanceof AV.Op.Set) {
229 return new AV.Op.Set(previous.value() & this.value());
230 } else {
231 throw new Error('Op is invalid after previous op.');
232 }
233 },
234 _estimate: function _estimate(oldValue) {
235 return oldValue & this.value();
236 }
237 });
238
239 AV.Op._registerDecoder('BitAnd', function (json) {
240 return new AV.Op.BitAnd(json.value);
241 });
242
243 /**
244 * @private
245 * @class
246 * BitOr is an atomic operation where the given value will be bit and to the
247 * value than is stored in this field.
248 */
249 AV.Op.BitOr = AV.Op._extend(
250 /** @lends AV.Op.BitOr.prototype */{
251 _initialize: function _initialize(value) {
252 this._value = value;
253 },
254 value: function value() {
255 return this._value;
256 },
257
258
259 /**
260 * Returns a JSON version of the operation suitable for sending to AV.
261 * @return {Object}
262 */
263 toJSON: function toJSON() {
264 return { __op: 'BitOr', value: this.value() };
265 },
266 _mergeWithPrevious: function _mergeWithPrevious(previous) {
267 if (!previous) {
268 return this;
269 } else if (previous instanceof AV.Op.Unset) {
270 return new AV.Op.Set(this.value());
271 } else if (previous instanceof AV.Op.Set) {
272 return new AV.Op.Set(previous.value() | this.value());
273 } else {
274 throw new Error('Op is invalid after previous op.');
275 }
276 },
277 _estimate: function _estimate(oldValue) {
278 return oldValue | this.value();
279 }
280 });
281
282 AV.Op._registerDecoder('BitOr', function (json) {
283 return new AV.Op.BitOr(json.value);
284 });
285
286 /**
287 * @private
288 * @class
289 * BitXor is an atomic operation where the given value will be bit and to the
290 * value than is stored in this field.
291 */
292 AV.Op.BitXor = AV.Op._extend(
293 /** @lends AV.Op.BitXor.prototype */{
294 _initialize: function _initialize(value) {
295 this._value = value;
296 },
297 value: function value() {
298 return this._value;
299 },
300
301
302 /**
303 * Returns a JSON version of the operation suitable for sending to AV.
304 * @return {Object}
305 */
306 toJSON: function toJSON() {
307 return { __op: 'BitXor', value: this.value() };
308 },
309 _mergeWithPrevious: function _mergeWithPrevious(previous) {
310 if (!previous) {
311 return this;
312 } else if (previous instanceof AV.Op.Unset) {
313 return new AV.Op.Set(this.value());
314 } else if (previous instanceof AV.Op.Set) {
315 return new AV.Op.Set(previous.value() ^ this.value());
316 } else {
317 throw new Error('Op is invalid after previous op.');
318 }
319 },
320 _estimate: function _estimate(oldValue) {
321 return oldValue ^ this.value();
322 }
323 });
324
325 AV.Op._registerDecoder('BitXor', function (json) {
326 return new AV.Op.BitXor(json.value);
327 });
328
329 /**
330 * @private
331 * @class
332 * Add is an atomic operation where the given objects will be appended to the
333 * array that is stored in this field.
334 */
335 AV.Op.Add = AV.Op._extend(
336 /** @lends AV.Op.Add.prototype */{
337 _initialize: function _initialize(objects) {
338 this._objects = objects;
339 },
340
341 /**
342 * Returns the objects to be added to the array.
343 * @return {Array} The objects to be added to the array.
344 */
345 objects: function objects() {
346 return this._objects;
347 },
348
349 /**
350 * Returns a JSON version of the operation suitable for sending to AV.
351 * @return {Object}
352 */
353 toJSON: function toJSON() {
354 return { __op: 'Add', objects: AV._encode(this.objects()) };
355 },
356
357 _mergeWithPrevious: function _mergeWithPrevious(previous) {
358 if (!previous) {
359 return this;
360 } else if (previous instanceof AV.Op.Unset) {
361 return new AV.Op.Set(this.objects());
362 } else if (previous instanceof AV.Op.Set) {
363 return new AV.Op.Set(this._estimate(previous.value()));
364 } else if (previous instanceof AV.Op.Add) {
365 return new AV.Op.Add(previous.objects().concat(this.objects()));
366 } else {
367 throw new Error('Op is invalid after previous op.');
368 }
369 },
370
371 _estimate: function _estimate(oldValue) {
372 if (!oldValue) {
373 return _.clone(this.objects());
374 } else {
375 return oldValue.concat(this.objects());
376 }
377 }
378 });
379
380 AV.Op._registerDecoder('Add', function (json) {
381 return new AV.Op.Add(AV._decode(json.objects));
382 });
383
384 /**
385 * @private
386 * @class
387 * AddUnique is an atomic operation where the given items will be appended to
388 * the array that is stored in this field only if they were not already
389 * present in the array.
390 */
391 AV.Op.AddUnique = AV.Op._extend(
392 /** @lends AV.Op.AddUnique.prototype */{
393 _initialize: function _initialize(objects) {
394 this._objects = _.uniq(objects);
395 },
396
397 /**
398 * Returns the objects to be added to the array.
399 * @return {Array} The objects to be added to the array.
400 */
401 objects: function objects() {
402 return this._objects;
403 },
404
405 /**
406 * Returns a JSON version of the operation suitable for sending to AV.
407 * @return {Object}
408 */
409 toJSON: function toJSON() {
410 return { __op: 'AddUnique', objects: AV._encode(this.objects()) };
411 },
412
413 _mergeWithPrevious: function _mergeWithPrevious(previous) {
414 if (!previous) {
415 return this;
416 } else if (previous instanceof AV.Op.Unset) {
417 return new AV.Op.Set(this.objects());
418 } else if (previous instanceof AV.Op.Set) {
419 return new AV.Op.Set(this._estimate(previous.value()));
420 } else if (previous instanceof AV.Op.AddUnique) {
421 return new AV.Op.AddUnique(this._estimate(previous.objects()));
422 } else {
423 throw new Error('Op is invalid after previous op.');
424 }
425 },
426
427 _estimate: function _estimate(oldValue) {
428 if (!oldValue) {
429 return _.clone(this.objects());
430 } else {
431 // We can't just take the _.uniq(_.union(...)) of oldValue and
432 // this.objects, because the uniqueness may not apply to oldValue
433 // (especially if the oldValue was set via .set())
434 var newValue = _.clone(oldValue);
435 AV._arrayEach(this.objects(), function (obj) {
436 if (obj instanceof AV.Object && obj.id) {
437 var matchingObj = _.find(newValue, function (anObj) {
438 return anObj instanceof AV.Object && anObj.id === obj.id;
439 });
440 if (!matchingObj) {
441 newValue.push(obj);
442 } else {
443 var index = _.indexOf(newValue, matchingObj);
444 newValue[index] = obj;
445 }
446 } else if (!_.contains(newValue, obj)) {
447 newValue.push(obj);
448 }
449 });
450 return newValue;
451 }
452 }
453 });
454
455 AV.Op._registerDecoder('AddUnique', function (json) {
456 return new AV.Op.AddUnique(AV._decode(json.objects));
457 });
458
459 /**
460 * @private
461 * @class
462 * Remove is an atomic operation where the given objects will be removed from
463 * the array that is stored in this field.
464 */
465 AV.Op.Remove = AV.Op._extend(
466 /** @lends AV.Op.Remove.prototype */{
467 _initialize: function _initialize(objects) {
468 this._objects = _.uniq(objects);
469 },
470
471 /**
472 * Returns the objects to be removed from the array.
473 * @return {Array} The objects to be removed from the array.
474 */
475 objects: function objects() {
476 return this._objects;
477 },
478
479 /**
480 * Returns a JSON version of the operation suitable for sending to AV.
481 * @return {Object}
482 */
483 toJSON: function toJSON() {
484 return { __op: 'Remove', objects: AV._encode(this.objects()) };
485 },
486
487 _mergeWithPrevious: function _mergeWithPrevious(previous) {
488 if (!previous) {
489 return this;
490 } else if (previous instanceof AV.Op.Unset) {
491 return previous;
492 } else if (previous instanceof AV.Op.Set) {
493 return new AV.Op.Set(this._estimate(previous.value()));
494 } else if (previous instanceof AV.Op.Remove) {
495 return new AV.Op.Remove(_.union(previous.objects(), this.objects()));
496 } else {
497 throw new Error('Op is invalid after previous op.');
498 }
499 },
500
501 _estimate: function _estimate(oldValue) {
502 if (!oldValue) {
503 return [];
504 } else {
505 var newValue = _.difference(oldValue, this.objects());
506 // If there are saved AV Objects being removed, also remove them.
507 AV._arrayEach(this.objects(), function (obj) {
508 if (obj instanceof AV.Object && obj.id) {
509 newValue = _.reject(newValue, function (other) {
510 return other instanceof AV.Object && other.id === obj.id;
511 });
512 }
513 });
514 return newValue;
515 }
516 }
517 });
518
519 AV.Op._registerDecoder('Remove', function (json) {
520 return new AV.Op.Remove(AV._decode(json.objects));
521 });
522
523 /**
524 * @private
525 * @class
526 * A Relation operation indicates that the field is an instance of
527 * AV.Relation, and objects are being added to, or removed from, that
528 * relation.
529 */
530 AV.Op.Relation = AV.Op._extend(
531 /** @lends AV.Op.Relation.prototype */{
532 _initialize: function _initialize(adds, removes) {
533 this._targetClassName = null;
534
535 var self = this;
536
537 var pointerToId = function pointerToId(object) {
538 if (object instanceof AV.Object) {
539 if (!object.id) {
540 throw new Error("You can't add an unsaved AV.Object to a relation.");
541 }
542 if (!self._targetClassName) {
543 self._targetClassName = object.className;
544 }
545 if (self._targetClassName !== object.className) {
546 throw new Error('Tried to create a AV.Relation with 2 different types: ' + self._targetClassName + ' and ' + object.className + '.');
547 }
548 return object.id;
549 }
550 return object;
551 };
552
553 this.relationsToAdd = _.uniq(_.map(adds, pointerToId));
554 this.relationsToRemove = _.uniq(_.map(removes, pointerToId));
555 },
556
557 /**
558 * Returns an array of unfetched AV.Object that are being added to the
559 * relation.
560 * @return {Array}
561 */
562 added: function added() {
563 var self = this;
564 return _.map(this.relationsToAdd, function (objectId) {
565 var object = AV.Object._create(self._targetClassName);
566 object.id = objectId;
567 return object;
568 });
569 },
570
571 /**
572 * Returns an array of unfetched AV.Object that are being removed from
573 * the relation.
574 * @return {Array}
575 */
576 removed: function removed() {
577 var self = this;
578 return _.map(this.relationsToRemove, function (objectId) {
579 var object = AV.Object._create(self._targetClassName);
580 object.id = objectId;
581 return object;
582 });
583 },
584
585 /**
586 * Returns a JSON version of the operation suitable for sending to AV.
587 * @return {Object}
588 */
589 toJSON: function toJSON() {
590 var adds = null;
591 var removes = null;
592 var self = this;
593 var idToPointer = function idToPointer(id) {
594 return {
595 __type: 'Pointer',
596 className: self._targetClassName,
597 objectId: id
598 };
599 };
600 var pointers = null;
601 if (this.relationsToAdd.length > 0) {
602 pointers = _.map(this.relationsToAdd, idToPointer);
603 adds = { __op: 'AddRelation', objects: pointers };
604 }
605
606 if (this.relationsToRemove.length > 0) {
607 pointers = _.map(this.relationsToRemove, idToPointer);
608 removes = { __op: 'RemoveRelation', objects: pointers };
609 }
610
611 if (adds && removes) {
612 return { __op: 'Batch', ops: [adds, removes] };
613 }
614
615 return adds || removes || {};
616 },
617
618 _mergeWithPrevious: function _mergeWithPrevious(previous) {
619 if (!previous) {
620 return this;
621 } else if (previous instanceof AV.Op.Unset) {
622 throw new Error("You can't modify a relation after deleting it.");
623 } else if (previous instanceof AV.Op.Relation) {
624 if (previous._targetClassName && previous._targetClassName !== this._targetClassName) {
625 throw new Error('Related object must be of class ' + previous._targetClassName + ', but ' + this._targetClassName + ' was passed in.');
626 }
627 var newAdd = _.union(_.difference(previous.relationsToAdd, this.relationsToRemove), this.relationsToAdd);
628 var newRemove = _.union(_.difference(previous.relationsToRemove, this.relationsToAdd), this.relationsToRemove);
629
630 var newRelation = new AV.Op.Relation(newAdd, newRemove);
631 newRelation._targetClassName = this._targetClassName;
632 return newRelation;
633 } else {
634 throw new Error('Op is invalid after previous op.');
635 }
636 },
637
638 _estimate: function _estimate(oldValue, object, key) {
639 if (!oldValue) {
640 var relation = new AV.Relation(object, key);
641 relation.targetClassName = this._targetClassName;
642 } else if (oldValue instanceof AV.Relation) {
643 if (this._targetClassName) {
644 if (oldValue.targetClassName) {
645 if (oldValue.targetClassName !== this._targetClassName) {
646 throw new Error('Related object must be a ' + oldValue.targetClassName + ', but a ' + this._targetClassName + ' was passed in.');
647 }
648 } else {
649 oldValue.targetClassName = this._targetClassName;
650 }
651 }
652 return oldValue;
653 } else {
654 throw new Error('Op is invalid after previous op.');
655 }
656 }
657 });
658
659 AV.Op._registerDecoder('AddRelation', function (json) {
660 return new AV.Op.Relation(AV._decode(json.objects), []);
661 });
662 AV.Op._registerDecoder('RemoveRelation', function (json) {
663 return new AV.Op.Relation([], AV._decode(json.objects));
664 });
665};
\No newline at end of file