angularjs-rails-resource 2.2.2 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,8 +9,8 @@
9
9
 
10
10
  RailsResourceSnapshotsMixin.extended = function (Resource) {
11
11
  Resource.intercept('afterResponse', function (result, resource, context) {
12
- if (context && context.hasOwnProperty('$snapshots') && angular.isArray(context.$snapshots)) {
13
- context.$snapshots.length = 0;
12
+ if (context && context.hasOwnProperty('$$snapshots') && angular.isArray(context.$$snapshots)) {
13
+ context.$$snapshots.length = 0;
14
14
  }
15
15
  });
16
16
 
@@ -27,7 +27,7 @@
27
27
 
28
28
  /**
29
29
  * Prepares a copy of the resource to be stored as a snapshot
30
- * @returns {Resource} the copied resource, sans $snapshots
30
+ * @returns {Resource} the copied resource, sans $$snapshots
31
31
  */
32
32
  function _prepSnapshot() {
33
33
  var config = this.constructor.config,
@@ -35,13 +35,13 @@
35
35
 
36
36
  // we don't want to store our snapshots in the snapshots because that would make the rollback kind of funny
37
37
  // not to mention using more memory for each snapshot.
38
- delete copy.$snapshots;
38
+ delete copy.$$snapshots;
39
39
 
40
40
  return copy
41
41
  }
42
42
 
43
43
  /**
44
- * Stores a copy of this resource in the $snapshots array to allow undoing changes.
44
+ * Stores a copy of this resource in the $$snapshots array to allow undoing changes.
45
45
  * @param {function} rollbackCallback Optional callback function to be executed after the rollback.
46
46
  * @returns {Number} The version of the snapshot created (0-based index)
47
47
  */
@@ -50,12 +50,12 @@
50
50
 
51
51
  copy.$rollbackCallback = rollbackCallback;
52
52
 
53
- if (!this.$snapshots) {
54
- this.$snapshots = [];
53
+ if (!this.$$snapshots) {
54
+ this.$$snapshots = [];
55
55
  }
56
56
 
57
- this.$snapshots.push(copy);
58
- return this.$snapshots.length - 1;
57
+ this.$$snapshots.push(copy);
58
+ return this.$$snapshots.length - 1;
59
59
  }
60
60
 
61
61
  /**
@@ -76,8 +76,8 @@
76
76
  function rollbackTo(version) {
77
77
  var versions, rollbackCallback,
78
78
  config = this.constructor.config,
79
- snapshots = this.$snapshots,
80
- snapshotsLength = this.$snapshots ? this.$snapshots.length : 0;
79
+ snapshots = this.$$snapshots,
80
+ snapshotsLength = this.$$snapshots ? this.$$snapshots.length : 0;
81
81
 
82
82
  // if an invalid snapshot version was specified then don't attempt to do anything
83
83
  if (!angular.isArray(snapshots) || snapshotsLength === 0 || !angular.isNumber(version)) {
@@ -94,7 +94,7 @@
94
94
  angular.extend(this, (config.snapshotSerializer || config.serializer).deserialize(versions[0]));
95
95
 
96
96
  // restore special variables
97
- this.$snapshots = snapshots;
97
+ this.$$snapshots = snapshots;
98
98
  delete this.$rollbackCallback;
99
99
 
100
100
  if (angular.isFunction(rollbackCallback)) {
@@ -115,7 +115,7 @@
115
115
  * @returns {Boolean} true if rollback was successful, false otherwise
116
116
  */
117
117
  function rollback(numVersions) {
118
- var snapshotsLength = this.$snapshots ? this.$snapshots.length : 0;
118
+ var snapshotsLength = this.$$snapshots ? this.$$snapshots.length : 0;
119
119
  numVersions = Math.min(numVersions || 1, snapshotsLength);
120
120
 
121
121
  if (numVersions < 0) {
@@ -123,7 +123,7 @@
123
123
  }
124
124
 
125
125
  if (snapshotsLength) {
126
- this.rollbackTo(this.$snapshots.length - numVersions);
126
+ this.rollbackTo(this.$$snapshots.length - numVersions);
127
127
  }
128
128
 
129
129
  return true;
@@ -134,12 +134,12 @@
134
134
  * @returns {Boolean} true if the latest snapshot differs from resource as-is
135
135
  */
136
136
  function unsnappedChanges() {
137
- if (!this.$snapshots) {
137
+ if (!this.$$snapshots) {
138
138
  return true
139
139
  }
140
140
 
141
141
  var copy = this._prepSnapshot(),
142
- latestSnap = this.$snapshots[this.$snapshots.length - 1]
142
+ latestSnap = this.$$snapshots[this.$$snapshots.length - 1]
143
143
 
144
144
  return !angular.equals(copy, latestSnap)
145
145
  }
@@ -108,8 +108,8 @@
108
108
  return this;
109
109
  };
110
110
 
111
- this.$get = ['$http', '$q', 'railsUrlBuilder', 'railsSerializer', 'railsRootWrapper', 'RailsResourceInjector',
112
- function ($http, $q, railsUrlBuilder, railsSerializer, railsRootWrapper, RailsResourceInjector) {
111
+ this.$get = ['$http', '$q', '$timeout', 'railsUrlBuilder', 'railsSerializer', 'railsRootWrapper', 'RailsResourceInjector',
112
+ function ($http, $q, $timeout, railsUrlBuilder, railsSerializer, railsRootWrapper, RailsResourceInjector) {
113
113
 
114
114
  function RailsResource(value) {
115
115
  if (value) {
@@ -118,6 +118,7 @@
118
118
  response = railsRootWrapper.unwrap(response, this.constructor, true);
119
119
  }
120
120
  angular.extend(this, response.data);
121
+ this.constructor.runInterceptorPhase('afterDeserialize', this);
121
122
  }
122
123
  }
123
124
 
@@ -219,6 +220,7 @@
219
220
  this.config.underscoreParams = booleanParam(cfg.underscoreParams, defaultOptions.underscoreParams);
220
221
  this.config.updateMethod = (cfg.updateMethod || defaultOptions.updateMethod).toLowerCase();
221
222
  this.config.fullResponse = booleanParam(cfg.fullResponse, defaultOptions.fullResponse);
223
+ this.config.singular = cfg.singular || defaultOptions.singular;
222
224
 
223
225
  this.config.requestTransformers = cfg.requestTransformers ? cfg.requestTransformers.slice(0) : [];
224
226
  this.config.responseInterceptors = cfg.responseInterceptors ? cfg.responseInterceptors.slice(0) : [];
@@ -313,6 +315,12 @@
313
315
  * * afterResponseError: Interceptors get called when a previous interceptor threw an error or
314
316
  * resolved with a rejection.
315
317
  *
318
+ * Finally, for each deserialized resource including associations, deserialization phases are called.
319
+ *
320
+ * The valid deserialization phases are:
321
+ *
322
+ * * afterDeserialize: Interceptors are called after a resource has been deserialized.
323
+ *
316
324
  * @param {String | Object} interceptor
317
325
  */
318
326
  RailsResource.addInterceptor = function (interceptor) {
@@ -322,7 +330,7 @@
322
330
  /**
323
331
  * Adds an interceptor callback function for the specified phase.
324
332
  * @param {String} phase The interceptor phase, one of:
325
- * beforeRequest, request, beforeResponse, response, afterResponse
333
+ * beforeRequest, request, beforeResponse, response, afterResponse, afterDeserialize
326
334
  * @param fn The function to call.
327
335
  */
328
336
  RailsResource.intercept = function (phase, fn) {
@@ -406,6 +414,16 @@
406
414
  this.intercept('afterResponse', fn);
407
415
  };
408
416
 
417
+ /**
418
+ * Adds interceptor on 'afterDeserialize' phase.
419
+ * @param fn(response data, constructor, context) - response data is either the resource instance returned or an array of resource instances,
420
+ * constructor is the resource class calling the function,
421
+ * context is the resource instance of the calling method (create, update, delete) or undefined if the method was a class method (get, query)
422
+ */
423
+ RailsResource.interceptAfterDeserialize = function (fn) {
424
+ this.intercept('afterDeserialize', fn);
425
+ };
426
+
409
427
  /**
410
428
  * Deprecated, see interceptors
411
429
  * Add a callback to run on response.
@@ -505,6 +523,7 @@
505
523
  };
506
524
 
507
525
  RailsResource.runInterceptorPhase = function (phase, context, promise) {
526
+ promise = promise || $q.resolve(context);
508
527
  var config = this.config, chain = [];
509
528
 
510
529
  forEachDependency(config.interceptors, function (interceptor) {
@@ -542,9 +561,25 @@
542
561
  * has completed.
543
562
  */
544
563
  RailsResource.$http = function (httpConfig, context, resourceConfigOverrides) {
545
- var config = angular.extend(angular.copy(this.config), resourceConfigOverrides || {}),
564
+ var timeoutPromise, promise,
565
+ config = angular.extend(angular.copy(this.config), resourceConfigOverrides || {}),
546
566
  resourceConstructor = config.resourceConstructor,
547
- promise = $q.when(httpConfig);
567
+ abortDeferred = $q.defer();
568
+
569
+ function abortRequest() {
570
+ abortDeferred.resolve();
571
+ }
572
+
573
+ if (httpConfig && httpConfig.timeout) {
574
+ if (httpConfig.timeout > 0) {
575
+ timeoutPromise = $timeout(abortDeferred.resolve, httpConfig.timeout);
576
+ } else if (angular.isFunction(httpConfig.timeout.then)) {
577
+ httpConfig.timeout.then(abortDeferred.resolve);
578
+ }
579
+ }
580
+
581
+ httpConfig = angular.extend({}, httpConfig, {timeout: abortDeferred.promise});
582
+ promise = $q.when(httpConfig);
548
583
 
549
584
  if (!config.skipRequestProcessing) {
550
585
 
@@ -572,9 +607,19 @@
572
607
  });
573
608
 
574
609
  } else {
575
-
576
610
  promise = $http(httpConfig);
611
+ }
577
612
 
613
+ // After the request has completed we need to cancel any pending timeout
614
+ if (timeoutPromise) {
615
+ // not using finally here to stay compatible with angular 1.0
616
+ promise = promise.then(function (result) {
617
+ $timeout.cancel(timeoutPromise);
618
+ return result;
619
+ }, function (error) {
620
+ $timeout.cancel(timeoutPromise);
621
+ return $q.reject(error);
622
+ });
578
623
  }
579
624
 
580
625
  promise = this.runInterceptorPhase('beforeResponse', context, promise).then(function (response) {
@@ -607,9 +652,12 @@
607
652
 
608
653
  promise = this.callAfterResponseInterceptors(promise, context);
609
654
  promise = this.runInterceptorPhase('afterResponse', context, promise);
610
- promise.resource = config.resourceConstructor;
611
- promise.context = context;
612
- return promise;
655
+ promise = this.runInterceptorPhase('afterDeserialize', context, promise);
656
+ return extendPromise(promise, {
657
+ resource: config.resourceConstructor,
658
+ context: context,
659
+ abort: abortRequest
660
+ });
613
661
  };
614
662
 
615
663
  /**
@@ -856,6 +904,16 @@
856
904
  return rejectFn ? rejectFn(rejection, resourceConstructor, context) : $q.reject(rejection);
857
905
  };
858
906
  }
907
+
908
+ function extendPromise(promise, attributes) {
909
+ var oldThen = promise.then;
910
+ promise.then = function (onFulfilled, onRejected, progressBack) {
911
+ var chainedPromise = oldThen.apply(this, arguments);
912
+ return extendPromise(chainedPromise, attributes);
913
+ };
914
+ angular.extend(promise, attributes);
915
+ return promise;
916
+ }
859
917
  }];
860
918
  });
861
919
 
@@ -78,6 +78,7 @@
78
78
  this.preservedAttributes = {};
79
79
  this.customSerializers = {};
80
80
  this.nestedResources = {};
81
+ this.polymorphics = {};
81
82
  this.options = angular.extend({excludeByDefault: false}, defaultOptions, options || {});
82
83
 
83
84
  if (customizer) {
@@ -159,6 +160,25 @@
159
160
  return this;
160
161
  };
161
162
 
163
+ /**
164
+ * Specifies a polymorphic association according to Rails' standards.
165
+ * Polymorphic associations have a <code>{name}_id</code> and <code>{name}_type</code> columns in the database.
166
+ *
167
+ * The <code>{name}_type</code> attribute will specify which resource will be used to serialize and deserialize the data.
168
+ *
169
+ * @param names... {string} Variable number of name parameters
170
+ * @returns {Serializer} this for chaining support
171
+ */
172
+ Serializer.prototype.polymorphic = function () {
173
+ var polymorphics = this.polymorphics;
174
+
175
+ angular.forEach(arguments, function(attributeName) {
176
+ polymorphics[attributeName] = true;
177
+ });
178
+
179
+ return this;
180
+ };
181
+
162
182
  /**
163
183
  * Specifies a custom name mapping for an attribute.
164
184
  * On serializing to JSON the jsonName will be used.
@@ -317,10 +337,17 @@
317
337
  /**
318
338
  * Returns a reference to the nested resource that has been specified for the attribute.
319
339
  * @param attributeName The attribute name
340
+ * @param data the entire object being serialized
320
341
  * @returns {*} undefined if no nested resource has been specified or a reference to the nested resource class
321
342
  */
322
- Serializer.prototype.getNestedResource = function (attributeName) {
323
- return RailsResourceInjector.getDependency(this.nestedResources[attributeName]);
343
+ Serializer.prototype.getNestedResource = function (attributeName, data) {
344
+ var resourceName;
345
+ if (!this.polymorphics[attributeName]) {
346
+ resourceName = this.nestedResources[attributeName];
347
+ } else {
348
+ resourceName = data[attributeName + '_type'];
349
+ }
350
+ return RailsResourceInjector.getDependency(resourceName);
324
351
  };
325
352
 
326
353
  /**
@@ -330,10 +357,11 @@
330
357
  * is used. Custom serializers specified using serializeWith take precedence over the nested resource serializer.
331
358
  *
332
359
  * @param attributeName The attribute name
360
+ * @param data the entire object being serialized
333
361
  * @returns {*} undefined if no custom serializer has been specified or an instance of the Serializer
334
362
  */
335
- Serializer.prototype.getAttributeSerializer = function (attributeName) {
336
- var resource = this.getNestedResource(attributeName),
363
+ Serializer.prototype.getAttributeSerializer = function (attributeName, data) {
364
+ var resource = this.getNestedResource(attributeName, data),
337
365
  serializer = this.customSerializers[attributeName];
338
366
 
339
367
  // custom serializer takes precedence over resource serializer
@@ -376,14 +404,14 @@
376
404
  return result;
377
405
  };
378
406
 
379
- Serializer.prototype.serializeObject = function(result, data){
407
+ Serializer.prototype.serializeObject = function(result, data) {
380
408
 
381
409
 
382
410
  var tthis = this;
383
411
  angular.forEach(data, function (value, key) {
384
412
  // if the value is a function then it can't be serialized to JSON so we'll just skip it
385
413
  if (!angular.isFunction(value)) {
386
- tthis.serializeAttribute(result, key, value);
414
+ tthis.serializeAttribute(result, key, value, data);
387
415
  }
388
416
  });
389
417
  return data;
@@ -393,12 +421,13 @@
393
421
  * Transforms an attribute and its value and stores it on the parent data object. The attribute will be
394
422
  * renamed as needed and the value itself will be serialized as well.
395
423
  *
396
- * @param data The object that the attribute will be added to
424
+ * @param result The object that the attribute will be added to
397
425
  * @param attribute The attribute to transform
398
426
  * @param value The current value of the attribute
427
+ * @param data the entire object being serialized
399
428
  */
400
- Serializer.prototype.serializeAttribute = function (data, attribute, value) {
401
- var serializer = this.getAttributeSerializer(attribute),
429
+ Serializer.prototype.serializeAttribute = function (result, attribute, value, data) {
430
+ var serializer = this.getAttributeSerializer(attribute, data),
402
431
  serializedAttributeName = this.getSerializedAttributeName(attribute);
403
432
 
404
433
  // undefined means the attribute should be excluded from serialization
@@ -406,7 +435,7 @@
406
435
  return;
407
436
  }
408
437
 
409
- data[serializedAttributeName] = serializer ? serializer.serialize(value) : this.serializeData(value);
438
+ result[serializedAttributeName] = serializer ? serializer.serialize(value) : this.serializeData(value);
410
439
  };
411
440
 
412
441
  /**
@@ -432,14 +461,14 @@
432
461
  itemValue = itemValue.call(item, item);
433
462
  }
434
463
 
435
- self.serializeAttribute(item, key, itemValue);
464
+ self.serializeAttribute(item, key, itemValue, data);
436
465
  });
437
466
  } else {
438
467
  if (angular.isFunction(value)) {
439
468
  value = value.call(data, data);
440
469
  }
441
470
 
442
- self.serializeAttribute(result, key, value);
471
+ self.serializeAttribute(result, key, value, data);
443
472
  }
444
473
  });
445
474
  }
@@ -454,9 +483,10 @@
454
483
  *
455
484
  * @param data The object to deserialize
456
485
  * @param Resource (optional) The resource type to deserialize the result into
486
+ * @param triggerPhase (optional) Whether to trigger the afterDeserialize phase
457
487
  * @returns {*} A new object or an instance of Resource populated with deserialized data.
458
488
  */
459
- Serializer.prototype.deserializeData = function (data, Resource) {
489
+ Serializer.prototype.deserializeData = function (data, Resource, triggerPhase) {
460
490
  var result = data,
461
491
  self = this;
462
492
 
@@ -464,7 +494,7 @@
464
494
  result = [];
465
495
 
466
496
  angular.forEach(data, function (value) {
467
- result.push(self.deserializeData(value, Resource));
497
+ result.push(self.deserializeData(value, Resource, triggerPhase));
468
498
  });
469
499
  } else if (angular.isObject(data)) {
470
500
  if (angular.isDate(data)) {
@@ -476,19 +506,23 @@
476
506
  result = new Resource.config.resourceConstructor();
477
507
  }
478
508
 
479
- this.deserializeObject(result, data);
509
+ this.deserializeObject(result, data, triggerPhase);
480
510
 
481
511
  }
482
512
 
483
513
  return result;
484
514
  };
485
515
 
486
- Serializer.prototype.deserializeObject = function (result, data) {
516
+ Serializer.prototype.deserializeObject = function (result, data, triggerPhase) {
487
517
 
488
518
  var tthis = this;
489
519
  angular.forEach(data, function (value, key) {
490
- tthis.deserializeAttribute(result, key, value);
520
+ tthis.deserializeAttribute(result, key, value, data);
491
521
  });
522
+ if (triggerPhase && result.constructor.runInterceptorPhase) {
523
+ result.constructor.runInterceptorPhase('afterDeserialize', result);
524
+ }
525
+
492
526
  return data;
493
527
  };
494
528
 
@@ -497,11 +531,12 @@
497
531
  * Transforms an attribute and its value and stores it on the parent data object. The attribute will be
498
532
  * renamed as needed and the value itself will be deserialized as well.
499
533
  *
500
- * @param data The object that the attribute will be added to
534
+ * @param result The object that the attribute will be added to
501
535
  * @param attribute The attribute to transform
502
536
  * @param value The current value of the attribute
537
+ * @param data the entire object being deserialized
503
538
  */
504
- Serializer.prototype.deserializeAttribute = function (data, attribute, value) {
539
+ Serializer.prototype.deserializeAttribute = function (result, attribute, value, data) {
505
540
  var serializer,
506
541
  NestedResource,
507
542
  attributeName = this.getDeserializedAttributeName(attribute);
@@ -511,14 +546,14 @@
511
546
  return;
512
547
  }
513
548
 
514
- serializer = this.getAttributeSerializer(attributeName);
515
- NestedResource = this.getNestedResource(attributeName);
549
+ serializer = this.getAttributeSerializer(attributeName, data);
550
+ NestedResource = this.getNestedResource(attributeName, data);
516
551
 
517
552
  // preserved attributes are assigned unmodified
518
553
  if (this.preservedAttributes[attributeName]) {
519
- data[attributeName] = value;
554
+ result[attributeName] = value;
520
555
  } else {
521
- data[attributeName] = serializer ? serializer.deserialize(value, NestedResource) : this.deserializeData(value, NestedResource);
556
+ result[attributeName] = serializer ? serializer.deserialize(value, NestedResource, true) : this.deserializeData(value, NestedResource, true);
522
557
  }
523
558
  };
524
559
 
@@ -531,11 +566,12 @@
531
566
  *
532
567
  * @param data The object to deserialize
533
568
  * @param Resource (optional) The resource type to deserialize the result into
569
+ * @param triggerPhase (optional) Whether to trigger the afterDeserialize phase
534
570
  * @returns {*} A new object or an instance of Resource populated with deserialized data
535
571
  */
536
- Serializer.prototype.deserialize = function (data, Resource) {
572
+ Serializer.prototype.deserialize = function (data, Resource, triggerPhase) {
537
573
  // just calls deserializeValue for now so we can more easily add on custom attribute logic for deserialize too
538
- return this.deserializeData(data, Resource);
574
+ return this.deserializeData(data, Resource, triggerPhase);
539
575
  };
540
576
 
541
577
  Serializer.prototype.pluralize = function (value) {
@@ -566,4 +602,4 @@
566
602
  return railsSerializer;
567
603
  }];
568
604
  });
569
- }());
605
+ }());