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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
  }());