angularjs-rails-resource 1.0.0.pre.2 → 1.0.0.pre.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,117 @@
1
+ (function (undefined) {
2
+ angular.module('rails').factory('RailsResourceSnapshotsMixin', ['RailsResourceInjector', function (RailsResourceInjector) {
3
+ function RailsResourceSnapshotsMixin() {
4
+ }
5
+
6
+ RailsResourceSnapshotsMixin.configure = function (resourceConfig, newConfig) {
7
+ resourceConfig.snapshotSerializer = RailsResourceInjector.getService(newConfig.snapshotSerializer);
8
+ };
9
+
10
+ RailsResourceSnapshotsMixin.extended = function (Resource) {
11
+ Resource.afterResponse(function (result) {
12
+ if (result.hasOwnProperty('$snapshots') && angular.isArray(result.$snapshots)) {
13
+ result.$snapshots.length = 0;
14
+ }
15
+ });
16
+
17
+ Resource.include({
18
+ snapshot: snapshot,
19
+ rollback: rollback,
20
+ rollbackTo: rollbackTo
21
+ });
22
+ };
23
+
24
+ return RailsResourceSnapshotsMixin;
25
+
26
+ /**
27
+ * Stores a copy of this resource in the $snapshots array to allow undoing changes.
28
+ * @param {function} rollbackCallback Optional callback function to be executed after the rollback.
29
+ * @returns {Number} The version of the snapshot created (0-based index)
30
+ */
31
+ function snapshot(rollbackCallback) {
32
+ var config = this.constructor.config,
33
+ copy = (config.snapshotSerializer || config.serializer).serialize(this);
34
+
35
+ // we don't want to store our snapshots in the snapshots because that would make the rollback kind of funny
36
+ // not to mention using more memory for each snapshot.
37
+ delete copy.$snapshots;
38
+ copy.$rollbackCallback = rollbackCallback;
39
+
40
+ if (!this.$snapshots) {
41
+ this.$snapshots = [];
42
+ }
43
+
44
+ this.$snapshots.push(copy);
45
+ return this.$snapshots.length - 1;
46
+ }
47
+
48
+ /**
49
+ * Rolls back the resource to a specific snapshot version (0-based index).
50
+ * All versions after the specified version are removed from the snapshots list.
51
+ *
52
+ * If the version specified is greater than the number of versions then the last snapshot version
53
+ * will be used. If the version is less than 0 then the resource will be rolled back to the first version.
54
+ *
55
+ * If no snapshots are available then the operation will return false.
56
+ *
57
+ * If a rollback callback function was defined then it will be called after the rollback has been completed
58
+ * with "this" assigned to the resource instance.
59
+ *
60
+ * @param {Number|undefined} version The version to roll back to.
61
+ * @returns {Boolean} true if rollback was successful, false otherwise
62
+ */
63
+ function rollbackTo(version) {
64
+ var versions, rollbackCallback,
65
+ config = this.constructor.config,
66
+ snapshots = this.$snapshots,
67
+ snapshotsLength = this.$snapshots ? this.$snapshots.length : 0;
68
+
69
+ // if an invalid snapshot version was specified then don't attempt to do anything
70
+ if (!angular.isArray(snapshots) || snapshotsLength === 0 || !angular.isNumber(version)) {
71
+ return false;
72
+ }
73
+
74
+ versions = snapshots.splice(Math.max(0, Math.min(version, snapshotsLength - 1)));
75
+
76
+ if (!angular.isArray(versions) || versions.length === 0) {
77
+ return false;
78
+ }
79
+
80
+ rollbackCallback = versions[0].$rollbackCallback;
81
+ angular.extend(this, (config.snapshotSerializer || config.serializer).deserialize(versions[0]));
82
+
83
+ // restore special variables
84
+ this.$snapshots = snapshots;
85
+ delete this.$rollbackCallback;
86
+
87
+ if (angular.isFunction(rollbackCallback)) {
88
+ rollbackCallback.call(this);
89
+ }
90
+
91
+ return true;
92
+ }
93
+
94
+ /**
95
+ * Rolls back the resource to a previous snapshot.
96
+ *
97
+ * When numVersions is undefined or 0 then a single version is rolled back.
98
+ * When numVersions exceeds the stored number of snapshots then the resource is rolled back to the first snapshot version.
99
+ * When numVersions is less than 0 then the resource is rolled back to the first snapshot version.
100
+ *
101
+ * @param {Number|undefined} numVersions The number of versions to roll back to. If undefined then
102
+ * @returns {Boolean} true if rollback was successful, false otherwise
103
+ */
104
+ function rollback(numVersions) {
105
+ var snapshotsLength = this.$snapshots ? this.$snapshots.length : 0;
106
+ numVersions = Math.min(numVersions || 1, snapshotsLength);
107
+
108
+ if (numVersions < 0) {
109
+ numVersions = snapshotsLength;
110
+ }
111
+
112
+ this.rollbackTo(this.$snapshots.length - numVersions);
113
+ return true;
114
+ }
115
+
116
+ }]);
117
+ }());
@@ -32,60 +32,86 @@
32
32
  rootWrapping: true,
33
33
  updateMethod: 'put',
34
34
  httpConfig: {},
35
- defaultParams: undefined
35
+ defaultParams: undefined,
36
+ underscoreParams: true,
37
+ extensions: []
36
38
  };
37
39
 
40
+ /**
41
+ * Enables or disables root wrapping by default for RailsResources
42
+ * Defaults to true.
43
+ * @param {boolean} value true to enable root wrapping, false to disable
44
+ * @returns {RailsResourceProvider} The provider instance
45
+ */
38
46
  this.rootWrapping = function (value) {
39
47
  defaultOptions.rootWrapping = value;
40
48
  return this;
41
49
  };
42
50
 
51
+ /**
52
+ * Configures what HTTP operation should be used for update by default for RailsResources.
53
+ * Defaults to 'put'
54
+ * @param value
55
+ * @returns {RailsResourceProvider} The provider instance
56
+ */
43
57
  this.updateMethod = function (value) {
44
58
  defaultOptions.updateMethod = value;
45
59
  return this;
46
60
  };
47
61
 
62
+ /**
63
+ * Configures default HTTP configuration operations for all RailsResources.
64
+ *
65
+ * @param {Object} value See $http for available configuration options.
66
+ * @returns {RailsResourceProvider} The provider instance
67
+ */
48
68
  this.httpConfig = function (value) {
49
69
  defaultOptions.httpConfig = value;
50
70
  return this;
51
71
  };
52
72
 
73
+ /**
74
+ * Configures default HTTP query parameters for all RailsResources.
75
+ *
76
+ * @param {Object} value Object of key/value pairs representing the HTTP query parameters for all HTTP operations.
77
+ * @returns {RailsResourceProvider} The provider instance
78
+ */
53
79
  this.defaultParams = function (value) {
54
80
  defaultOptions.defaultParams = value;
55
81
  return this;
56
82
  };
57
83
 
58
- this.$get = ['$http', '$q', 'railsUrlBuilder', 'railsSerializer', 'railsRootWrappingTransformer', 'railsRootWrappingInterceptor', 'RailsResourceInjector',
59
- function ($http, $q, railsUrlBuilder, railsSerializer, railsRootWrappingTransformer, railsRootWrappingInterceptor, RailsResourceInjector) {
60
-
61
- function appendPath(url, path) {
62
- if (path) {
63
- if (path[0] !== '/') {
64
- url += '/';
65
- }
66
-
67
- url += path;
68
- }
69
-
70
- return url;
71
- }
72
-
73
- function forEachDependency(list, callback) {
74
- var dependency;
75
-
76
- for (var i = 0, len = list.length; i < len; i++) {
77
- dependency = list[i];
84
+ /**
85
+ * Configures whether or not underscore query parameters
86
+ * @param {boolean} value true to underscore. Defaults to true.
87
+ * @returns {RailsResourceProvider} The provider instance
88
+ */
89
+ this.underscoreParams = function (value) {
90
+ defaultOptions.underscoreParams = value;
91
+ return this;
92
+ };
78
93
 
79
- if (angular.isString(dependency)) {
80
- dependency = list[i] = RailsResourceInjector.getDependency(dependency);
81
- }
94
+ /**
95
+ * List of RailsResource extensions to include by default.
96
+ *
97
+ * @param {...string} extensions One or more extension names to include
98
+ * @returns {*}
99
+ */
100
+ this.extensions = function () {
101
+ defaultOptions.extensions = [];
102
+ angular.forEach(arguments, function (value) {
103
+ defaultOptions.extensions = defaultOptions.extensions.concat(value);
104
+ });
105
+ return this;
106
+ };
82
107
 
83
- callback(dependency);
84
- }
85
- }
108
+ this.$get = ['$http', '$q', 'railsUrlBuilder', 'railsSerializer', 'railsRootWrappingTransformer', 'railsRootWrappingInterceptor', 'RailsResourceInjector', 'RailsInflector',
109
+ function ($http, $q, railsUrlBuilder, railsSerializer, railsRootWrappingTransformer, railsRootWrappingInterceptor, RailsResourceInjector, RailsInflector) {
86
110
 
87
111
  function RailsResource(value) {
88
112
  var instance = this;
113
+ this.$snapshots = [];
114
+
89
115
  if (value) {
90
116
  var immediatePromise = function (data) {
91
117
  return {
@@ -96,7 +122,7 @@
96
122
  this.response = callback(this.response, this.resource, this.context);
97
123
  return immediatePromise(this.response);
98
124
  }
99
- }
125
+ };
100
126
  };
101
127
 
102
128
  var data = this.constructor.callInterceptors(immediatePromise({data: value}), this).response.data;
@@ -104,24 +130,87 @@
104
130
  }
105
131
  }
106
132
 
107
- RailsResource.extend = function (child) {
108
- // Extend logic copied from CoffeeScript generated code
109
- var __hasProp = {}.hasOwnProperty, parent = this;
110
- for (var key in parent) {
111
- if (__hasProp.call(parent, key)) child[key] = parent[key];
133
+ /**
134
+ * Extends the RailsResource to the child constructor function making the child constructor a subclass of
135
+ * RailsResource. This is modeled off of CoffeeScript's class extend function. All RailsResource
136
+ * class properties defined are copied to the child class and the child's prototype chain is configured
137
+ * to allow instances of the child class to have all of the instance methods of RailsResource.
138
+ *
139
+ * Like CoffeeScript, a __super__ property is set on the child class to the parent resource's prototype chain.
140
+ * This is done to allow subclasses to extend the functionality of instance methods and still
141
+ * call back to the original method using:
142
+ *
143
+ * Class.__super__.method.apply(this, arguments);
144
+ *
145
+ * @param {function} child Child constructor function
146
+ * @returns {function} Child constructor function
147
+ */
148
+ RailsResource.extendTo = function (child) {
149
+ angular.forEach(this, function (value, key) {
150
+ child[key] = value;
151
+ });
152
+
153
+ if (angular.isArray(this.$modules)) {
154
+ child.$modules = this.$modules.slice(0);
112
155
  }
113
156
 
114
157
  function ctor() {
115
158
  this.constructor = child;
116
159
  }
117
160
 
118
- ctor.prototype = parent.prototype;
161
+ ctor.prototype = this.prototype;
119
162
  child.prototype = new ctor();
120
- child.__super__ = parent.prototype;
163
+ child.__super__ = this.prototype;
121
164
  return child;
122
165
  };
123
166
 
124
- // allow calling configure multiple times to set configuration options and override values from inherited resources
167
+ /**
168
+ * Copies a mixin's properties to the resource.
169
+ *
170
+ * If module is a String then we it will be loaded using Angular's dependency injection. If the name is
171
+ * not valid then Angular will throw an error.
172
+ *
173
+ * @param {...String|function|Object} mixins The mixin or name of the mixin to add.
174
+ * @returns {RailsResource} this
175
+ */
176
+ RailsResource.extend = function () {
177
+ angular.forEach(arguments, function (mixin) {
178
+ addMixin(this, this, mixin, function (Resource, mixin) {
179
+ if (angular.isFunction(mixin.extended)) {
180
+ mixin.extended(Resource);
181
+ }
182
+ });
183
+ }, this);
184
+
185
+ return this;
186
+ };
187
+
188
+ /**
189
+ * Copies a mixin's properties to the resource's prototype chain.
190
+ *
191
+ * If module is a String then we it will be loaded using Angular's dependency injection. If the name is
192
+ * not valid then Angular will throw an error.
193
+ *
194
+ * @param {...String|function|Object} mixins The mixin or name of the mixin to add
195
+ * @returns {RailsResource} this
196
+ */
197
+ RailsResource.include = function () {
198
+ angular.forEach(arguments, function (mixin) {
199
+ addMixin(this, this.prototype, mixin, function (Resource, mixin) {
200
+ if (angular.isFunction(mixin.included)) {
201
+ mixin.included(Resource);
202
+ }
203
+ });
204
+ }, this);
205
+
206
+ return this;
207
+ };
208
+
209
+ /**
210
+ * Sets configuration options. This method may be called multiple times to set additional options or to
211
+ * override previous values (such as the case with inherited resources).
212
+ * @param cfg
213
+ */
125
214
  RailsResource.configure = function (cfg) {
126
215
  cfg = cfg || {};
127
216
 
@@ -131,22 +220,18 @@
131
220
 
132
221
  this.config = {};
133
222
  this.config.url = cfg.url;
134
- this.config.rootWrapping = cfg.rootWrapping === undefined ? defaultOptions.rootWrapping : cfg.rootWrapping; // using undefined check because config.rootWrapping || true would be true when config.rootWrapping === false
223
+ this.config.rootWrapping = booleanParam(cfg.rootWrapping, defaultOptions.rootWrapping); // using undefined check because config.rootWrapping || true would be true when config.rootWrapping === false
135
224
  this.config.httpConfig = cfg.httpConfig || defaultOptions.httpConfig;
136
225
  this.config.httpConfig.headers = angular.extend({'Accept': 'application/json', 'Content-Type': 'application/json'}, this.config.httpConfig.headers || {});
137
226
  this.config.defaultParams = cfg.defaultParams || defaultOptions.defaultParams;
227
+ this.config.underscoreParams = booleanParam(cfg.underscoreParams, defaultOptions.underscoreParams);
138
228
  this.config.updateMethod = (cfg.updateMethod || defaultOptions.updateMethod).toLowerCase();
139
229
 
140
230
  this.config.requestTransformers = cfg.requestTransformers ? cfg.requestTransformers.slice(0) : [];
141
231
  this.config.responseInterceptors = cfg.responseInterceptors ? cfg.responseInterceptors.slice(0) : [];
142
232
  this.config.afterResponseInterceptors = cfg.afterResponseInterceptors ? cfg.afterResponseInterceptors.slice(0) : [];
143
233
 
144
- // strings and functions are not considered objects by angular.isObject()
145
- if (angular.isObject(cfg.serializer)) {
146
- this.config.serializer = cfg.serializer;
147
- } else {
148
- this.config.serializer = RailsResourceInjector.createService(cfg.serializer || railsSerializer());
149
- }
234
+ this.config.serializer = RailsResourceInjector.getService(cfg.serializer || railsSerializer());
150
235
 
151
236
  this.config.name = this.config.serializer.underscore(cfg.name);
152
237
 
@@ -157,10 +242,20 @@
157
242
 
158
243
  this.config.urlBuilder = railsUrlBuilder(this.config.url);
159
244
  this.config.resourceConstructor = this;
160
- };
161
245
 
162
- RailsResource.configure({});
246
+ this.extend.apply(this, loadExtensions((cfg.extensions || []).concat(defaultOptions.extensions)));
163
247
 
248
+ angular.forEach(this.$mixins, function (mixin) {
249
+ if (angular.isFunction(mixin.configure)) {
250
+ mixin.configure(this.config, cfg);
251
+ }
252
+ }, this);
253
+ };
254
+
255
+ /**
256
+ * Configures the URL for the resource.
257
+ * @param {String|function} url The url string or function.
258
+ */
164
259
  RailsResource.setUrl = function (url) {
165
260
  this.configure({url: url});
166
261
  };
@@ -276,18 +371,41 @@
276
371
  return this.callAfterInterceptors(promise);
277
372
  };
278
373
 
374
+ /**
375
+ * Processes query parameters before request. You can override to modify
376
+ * the query params or return a new object.
377
+ *
378
+ * @param {Object} queryParams - The query parameters for the request
379
+ * @returns {Object} The query parameters for the request
380
+ */
381
+ RailsResource.processParameters = function (queryParams) {
382
+ var newParams = {};
383
+
384
+ if (angular.isObject(queryParams) && this.config.underscoreParams) {
385
+ angular.forEach(queryParams, function (v, k) {
386
+ newParams[this.config.serializer.underscore(k)] = v;
387
+ }, this);
388
+
389
+ return newParams;
390
+ }
391
+
392
+ return queryParams;
393
+ };
394
+
279
395
  RailsResource.getParameters = function (queryParams) {
280
396
  var params;
281
397
 
282
398
  if (this.config.defaultParams) {
283
- params = this.config.defaultParams;
399
+ // we need to clone it so we don't modify it when we add the additional
400
+ // query params below
401
+ params = angular.copy(this.config.defaultParams);
284
402
  }
285
403
 
286
404
  if (angular.isObject(queryParams)) {
287
405
  params = angular.extend(params || {}, queryParams);
288
406
  }
289
407
 
290
- return params;
408
+ return this.processParameters(params);
291
409
  };
292
410
 
293
411
  RailsResource.getHttpConfig = function (queryParams) {
@@ -388,7 +506,8 @@
388
506
  };
389
507
 
390
508
  RailsResource.prototype.isNew = function () {
391
- return this.id == null;
509
+ return angular.isUndefined(this.id) ||
510
+ this.id === null;
392
511
  };
393
512
 
394
513
  RailsResource.prototype.save = function () {
@@ -399,11 +518,11 @@
399
518
  }
400
519
  };
401
520
 
402
- RailsResource['$delete'] = function (url) {
521
+ RailsResource.$delete = function (url) {
403
522
  return this.processResponse($http['delete'](url, this.getHttpConfig()));
404
523
  };
405
524
 
406
- RailsResource.prototype['$delete'] = function (url) {
525
+ RailsResource.prototype.$delete = function (url) {
407
526
  return this.processResponse($http['delete'](url, this.constructor.getHttpConfig()));
408
527
  };
409
528
 
@@ -413,6 +532,74 @@
413
532
  };
414
533
 
415
534
  return RailsResource;
535
+
536
+ function appendPath(url, path) {
537
+ if (path) {
538
+ if (path[0] !== '/') {
539
+ url += '/';
540
+ }
541
+
542
+ url += path;
543
+ }
544
+
545
+ return url;
546
+ }
547
+
548
+ function forEachDependency(list, callback) {
549
+ var dependency;
550
+
551
+ for (var i = 0, len = list.length; i < len; i++) {
552
+ dependency = list[i];
553
+
554
+ if (angular.isString(dependency)) {
555
+ dependency = list[i] = RailsResourceInjector.getDependency(dependency);
556
+ }
557
+
558
+ callback(dependency);
559
+ }
560
+ }
561
+
562
+ function addMixin(Resource, destination, mixin, callback) {
563
+ var excludedKeys = ['included', 'extended,', 'configure'];
564
+
565
+ if (!Resource.$mixins) {
566
+ Resource.$mixins = [];
567
+ }
568
+
569
+ if (angular.isString(mixin)) {
570
+ mixin = RailsResourceInjector.getDependency(mixin);
571
+ }
572
+
573
+ if (mixin && Resource.$mixins.indexOf(mixin) === -1) {
574
+ angular.forEach(mixin, function (value, key) {
575
+ if (excludedKeys.indexOf(key) === -1) {
576
+ destination[key] = value;
577
+ }
578
+ });
579
+
580
+ Resource.$mixins.push(mixin);
581
+
582
+ if (angular.isFunction(callback)) {
583
+ callback(Resource, mixin);
584
+ }
585
+ }
586
+ }
587
+
588
+ function loadExtensions(extensions) {
589
+ var modules = [];
590
+
591
+ angular.forEach(extensions, function (extensionName) {
592
+ extensionName = 'RailsResource' + extensionName.charAt(0).toUpperCase() + extensionName.slice(1) + 'Mixin';
593
+
594
+ modules.push(RailsResourceInjector.getDependency(extensionName));
595
+ });
596
+
597
+ return modules;
598
+ }
599
+
600
+ function booleanParam(value, defaultValue) {
601
+ return angular.isUndefined(value) ? defaultValue : value;
602
+ }
416
603
  }];
417
604
  });
418
605
 
@@ -422,11 +609,11 @@
422
609
  Resource.__super__.constructor.apply(this, arguments);
423
610
  }
424
611
 
425
- RailsResource.extend(Resource);
612
+ RailsResource.extendTo(Resource);
426
613
  Resource.configure(config);
427
614
 
428
615
  return Resource;
429
- }
616
+ };
430
617
  }]);
431
618
 
432
619
  }());