-
Notifications
You must be signed in to change notification settings - Fork 34
/
base.js
1406 lines (1252 loc) · 55.6 KB
/
base.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// HELPER METHODS
//
// String.prototype.downcase
//
// Shorthand for toLowerCase()
String.prototype.downcase = function() {
return this.toLowerCase();
}
angular
.module('ActiveResource')
.provider('ARCache', function() {
this.$get = function() {
function Cache() {
// function cache(instance, primaryKey)
//
// @param {instance} - Model instance to store in the model's cache
//
// If the instance has an ID, add it to the cache of its constructor. E.g.:
// sensor => {id: 1, name: "Johnny's Window"}
// sensor.constructor = Sensor
//
// expect(Sensor.cached[1]).toEqual(sensor);
Object.defineProperty(this, 'cache', {
enumerable: false,
value: function(instance, primaryKey) {
if (instance && instance[primaryKey] !== undefined) {
instance.constructor.cached[instance[primaryKey]] = instance;
}
}
});
// function isEmpty()
//
// True/false cache is empty
Object.defineProperty(this, 'isEmpty', {
enumerable: false,
value: function() {
return !!(!Object.keys(this).length);
}
});
// function length()
//
// Length of cache, since cache is object so it has no length
// property by default
Object.defineProperty(this, 'length', {
enumerable: false,
value: function() {
return Object.keys(this).length;
}
});
// function where(terms)
//
// @param {terms} - Search terms used to find instances in the cache
//
// Returns all cached instances that match the given terms
Object.defineProperty(this, 'where', {
enumerable: false,
value: function(terms) {
if (Object.keys(terms).length == 0) terms = undefined;
return _.where(this, terms, this);
}
});
};
return Cache;
};
});
angular
.module('ActiveResource')
.provider('ARSerializer', function() {
this.$get = ['json', 'ARMixin', 'ARAssociations', 'ARHelpers', 'ARDeferred',
function(json, mixin, Associations, Helpers, deferred) {
function Serializer() {
// function serialize(instance)
//
// @param instance {object} - Instance to serialize
//
// Transform associations to foreign keys; a parsable, non-circular JSON structure
// ready to be sent over the wire.
this.serialize = function(instance, options) {
var obj = foreignkeyify(instance);
return json.serialize(obj, options);
};
// function deserialize(httpResponse, instance, options)
//
// @param httpResponse {object} - The data received in an http response
//
// @param instance {object} - An optional instance to update using the data received
//
// @param options {object} - Additional options to further refine deserialization
//
// Deserialize takes an http response, and by default loads all associations for any
// foreign keys on the response it receives (eager loading). Optionally, deserialize
// can be set to lazy-load (lazy: true), which will load no associations, or
// to over-eager load (overEager: true), which will also load all associations found
// on the associated instances (careful: this can pull down a huge amount of your database,
// and issue many http requests).
this.deserialize = function(httpResponse, instance, options) {
var json, options;
if (httpResponse && httpResponse.data) json = httpResponse.data;
else json = httpResponse;
if (!options) options = {lazy: true};
if (responseContainsForeignKeys(json, instance)) {
return setAssociationsAndUpdate(instance, json, options);
} else {
return updateLocalInstance(instance, json, options)
.then(function(response) { instance = response; return deferred(instance); });
}
};
// function foreignkeyify (instance)
//
// @param instance {object} - A model instance
//
// Takes all associations and transforms the necessary ones into foreign keys
function foreignkeyify(instance) {
var json = mixin({}, instance, false);
var associations = Associations.get(instance);
_.each(associations.belongsTo, function(association) {
var foreignKeyName = association.foreignKey;
var associatedName = Helpers.getClassNameFor(association);
var associatedInstance = instance[associatedName];
if (!associatedInstance) return;
var primaryKeyName = Helpers.getPrimaryKeyFor(associatedInstance);
var foreignkey = associatedInstance[primaryKeyName];
json[foreignKeyName] = foreignkey;
json[associatedName] = undefined;
});
return json;
};
// function responseContainsForeignKeys (response, instance)
//
// True/false - Response contains foreign keys
function responseContainsForeignKeys(response, instance) {
var answer = false;
var associations = Associations.get(instance);
_.each(associations.belongsTo, function(foreignRel) {
var foreignKey = foreignRel.foreignKey;
if (response[foreignKey] || response == foreignKey) answer = true;
});
return answer;
};
};
function updateLocalInstance(instance, response, options) {
if (options && options.update == false) return deferred(instance);
instance.update(response);
var primaryKey = Helpers.getPrimaryKeyFor(instance);
instance.constructor.cached.cache(instance, primaryKey);
instance.validations.updateInstance(instance);
if (!options.lazy) return eagerLoad(instance).then(finishUpdate);
return finishUpdate();
function finishUpdate() {
instance.establishBelongsTo();
return deferred(instance);
};
};
function setAssociationsAndUpdate(instance, response, options) {
if (options && options.update == false) options.update = true;
var associationsToUpdate = [];
var associations = Associations.get(instance);
if (associations.belongsTo.length >= 1) {
_.each(associations.belongsTo, function(foreignRel) {
if (!response[foreignRel.foreignKey]) return;
var association = foreignRel.klass;
var associatedName = association.name.camelize();
var foreignKey = foreignRel.foreignKey;
var query = response[foreignKey];
// Unless overEager is set, only eagerly load one level of associations.
var queryOptions = {};
for (var i in options) { queryOptions[i] = options[i]; }
if (!options.overEager) queryOptions.lazy = true;
associationsToUpdate.push(function(callback) {
foreignRel.klass.find(query, queryOptions).then(function(association) {
response[associatedName] = association;
delete response[foreignKey];
callback(null, response);
})
});
});
}
async.series(associationsToUpdate, function(err, response) {
response = _.first(response);
updateLocalInstance(instance, response, options);
});
return deferred(instance);
};
function eagerLoad(instance) {
var queries = [];
var associations = Associations.get(instance);
var dependentList = [associations.hasMany, associations.hasOne];
_.each(dependentList, function(dependentsArray) {
_.each(dependentsArray, function(association) {
var dependent = Associations.getDependents(association, instance);
var foreignKey = dependent.foreignKey;
var query = {};
var primaryKey = Helpers.getPrimaryKeyFor(instance);
query[foreignKey] = instance[primaryKey];
queries.push(function(callback) {
association.klass.where(query, {lazy: true}).then(function(response) {
_.each(response, function(associ) {
if (_.include(associations.hasMany, association)) {
var name = association.klass.name.pluralize().camelize();
instance[name].nodupush(associ);
} else {
var name = association.klass.name.singularize().camelize();
if (!instance[name]) instance[name] = associ;
}
callback(null, instance);
});
});
});
});
});
async.series(queries, function(err, callback) {});
return deferred(instance);
};
return Serializer;
}];
});
angular
.module('ActiveResource')
.provider('ARHelpers', function() {
this.$get = function() {
// Non-duplicating push. Will not add an instance to an array if it is already
// a member.
Object.defineProperty(Array.prototype, 'nodupush', {
enumerable: false,
configurable: true,
value: function(val) {
if (!_.include(this, val)) this.push(val);
}
});
return {
getClassNameFor: function(association) {
return association.klass.name.camelize();
},
getPrimaryKeyFor: function(classOrInstance) {
if (classOrInstance.constructor == 'Function') return classOrInstance.primaryKey;
else return classOrInstance.constructor.primaryKey;
}
};
};
});
angular
.module('ActiveResource')
.provider('ARMixin', function() {
this.$get = function() {
return function(receiver, giver, excludeFunctions) {
if (giver.constructor.name == 'Function') {
giver = new giver();
}
for (var i in giver) {
function mixinProp() {
if (!receiver.hasOwnProperty(i)) {
(function() {
var local;
Object.defineProperty(receiver, i, {
enumerable: true,
get: function() { return local; },
set: function(val) { local = val; }
});
})();
receiver[i] = giver[i];
}
}
if (excludeFunctions) {
if (typeof giver[i] !== 'function') {
mixinProp();
}
} else {
mixinProp();
}
}
return receiver;
};
};
});
angular
.module('ActiveResource')
.provider('ARDeferred', function() {
this.$get = ['$q', function($q) {
// function deferred(instance, error)
//
// @param {instance} - An instance to wrap in a deferred object
// @param {error} - Error to return
//
// Returns an object or error wrapped in a deferred. Responds to then() method. Shortcut
// for establishing these boilerplate lines.
return function deferred(instance, error) {
var deferred = $q.defer();
if (error) deferred.reject(error);
else deferred.resolve(instance);
return deferred.promise;
};
}];
});
angular
.module('ActiveResource')
.provider('ARQuerystring', function() {
this.$get = function() {
var querystring = {
stringify: function(object) {
var string = '';
_.map(object, function(val, key) {
if (string.length == 0) string += key + '=' + val;
else string += '&' + key + '=' + val;
});
return string;
},
parse: function(string) {
}
};
return querystring;
};
});
angular
.module('ActiveResource')
.provider('ARParameterize', function() {
this.$get = function() {
return function(url, object) {
if (!url) return;
if (!object) return url;
return url.replace(/\:\_*[A-Za-z]+/g, function(param) {
param = param.replace(/\:*/, '');
return object[param];
});
}
};
});
angular
.module('ActiveResource')
.provider('URLify', function() {
this.$get = ['ARParameterize', 'ARQuerystring', function(parameterize, querystring) {
return function(url, terms) {
if (!url) return;
if (!terms) return url;
var qs = '';
if (querystring.stringify(terms)) qs = '?' + querystring.stringify(terms);
if (url.match(/\[\:[A-Za-z]+\]/)) url = url.replace(/\[\:[A-Za-z]+\]/, qs);
else if (url.match(/\:\_*[A-Za-z]+/)) url = parameterize(url, terms);
return url;
};
}];
});
angular
.module('ActiveResource')
.provider('ARGET', function() {
this.$get = ['$http', 'ARDeferred', 'ARAssociations', 'ARHelpers', 'URLify', '$q',
function($http, deferred, Associations, Helpers, URLify, $q) {
function resolveSingleGET(data, terms, options) {
if (data && data.length >= 1) {
if (options.noInstanceEndpoint) return _.first(_.where(data, terms));
else return _.first(data);
}
return data;
};
function resolveMultiGET(data, terms, options) {
return data;
};
function transformSearchTermsToForeignKeys(instance, terms) {
var associatedInstance, propertyName;
var associations = Associations.get(instance);
if (!associations) return;
associations = associations.belongsTo;
_.each(associations, function(association) {
if (terms[association.propertyName]) {
associatedInstance = terms[association.propertyName];
propertyName = association.propertyName;
var foreignKey = association.foreignKey;
var primaryKey = Helpers.getPrimaryKeyFor(terms[association.propertyName]);
terms[foreignKey] = associatedInstance[primaryKey];
if (terms[foreignKey]) delete terms[association.propertyName];
}
});
return [associatedInstance, terms, propertyName];
};
function getAllParams(url) {
var params = [];
url.replace(/\:[a-zA-Z_]+/g, function(param) { params.push(param); });
params = _.map(params, function(param) { return param.slice(1); });
return params
};
function queryableByParams(url, terms) {
var params = getAllParams(url);
var truth = true;
_.each(params, function(param) {
if (terms[param] === undefined) truth = false;
});
_.each(terms, function(value, termName) {
if (!_.include(params, termName)) truth = false;
});
return truth;
};
return function generateGET(instance, url, terms, options) {
var instanceAndTerms = transformSearchTermsToForeignKeys(instance, terms);
var associatedInstance, terms, propertyName;
if (instanceAndTerms) {
associatedInstance = instanceAndTerms[0];
terms = instanceAndTerms[1];
propertyName = instanceAndTerms[2];
}
var config = {};
if (queryableByParams(url, terms)) {
url = URLify(url, terms);
} else if(Object.keys(terms).length) {
url = url.replace(/\/\:[a-zA-Z_]+/g, '').replace(/\:[a-zA-Z_]+/g, '');
config.params = terms;
}
if(options.api === false) {
var deferred = $q.defer();
deferred.resolve(options.cached);
return deferred.promise;
}
return $http.get(url, config).then(function(response) {
var data = response.data;
if (propertyName && associatedInstance) {
if (data && data.push) {
_.each(data, function(datum) { datum[propertyName] = associatedInstance; });
} else {
data[propertyName] = associatedInstance;
}
}
if (options.multi) return resolveMultiGET(data, terms, options);
else return resolveSingleGET(data, terms, options);
});
};
}];
});
angular
.module('ActiveResource')
.provider('ARBase', function() {
this.$get = ['ARAPI', 'ARCollection', 'ARAssociation', 'ARAssociations',
'ARCache', 'ARSerializer', 'AREventable', 'ARValidations', '$http', '$q',
'$injector', 'ARDeferred', 'ARGET', 'ARMixin', 'URLify', 'ARHelpers',
function(API, Collection, Association, Associations,
Cache, Serializer, Eventable, Validations, $http, $q,
$injector, deferred, GET, mixin, URLify, Helpers) {
function Base() {
var _this = this;
_this.watchedCollections = [];
// By default, the primary key is set to 'id'. It can be overridden using the
// Model.instance#primaryKey method. This local variable is used by the other methods
// to set the correct data and construct API requests.
var primaryKey = 'id';
Object.defineProperty(_this, 'primaryKey', {
configurable: true,
get: function() { return primaryKey; },
set: function(key) { primaryKey = key; this.api.updatePrimaryKey(primaryKey); }
});
// @ASSOCIATIONS
// We use an associations object to store the hasMany and belongsTo associations for each
// model. These are stored on associations.hasMany and associations.belongsTo respectively.
var associations = new Associations(_this);
// Dependents to destroy when the primary resource is destroyed. Set with
// _this.dependentDestroy(dependents)
var dependentDestroy = [];
// @API
// Instantiates a new ActiveResource::API, which comes with methods for setting the
// URLs used by functions like $save, $create, $delete, and $update. See
// ActiveResource::API for more details.
this.api = new API(this);
// @EVENT EMITTER
// Make models event-driven
mixin(_this, Eventable);
// @MODEL CACHE
//
// Creates a cache for the model. The cache is used by methods like Model#find and
// Model#where, to first check whether or not an instance with a given primary key
// already exists on the client before querying the backend for it. Model#find will not
// query the backend if it finds an instance in the cache. Model#where will combine
// both the cached instances and those it retrieved from the backend.
//
// The cache is also used to ensure model instances are the same object across the
// application. In different providers or directives, if two objects are meant to be
// the exact same object (===), as represented by the primary key, then they must be
// the exact same object in order for Angular's dirty checking functionality to
// work as expected.
if (!_this.cached) _this.cached = new Cache();
// @MODEL CACHE
//
// function cacheInstance(instance)
//
// A wrapper for cached.cache, which passes in the primary key that has been
// set on the instance. Puts the instance in the cache.
function cacheInstance(instance) {
_this.cached.cache(instance, primaryKey);
};
function findCachedMatching(terms) {
return _.where(_this.cached, terms, _this);
};
// @SERIALIZER
//
serializer = new Serializer();
_this.prototype.toJSON = function(options) {
return _this.prototype.serialize.call(this, options);
};
_this.prototype.serialize = function(options) {
return serializer.serialize(this, options);
};
//
// Model.instance#$save
//
// Persists an instance of a model to the backend via the API. A convention used
// in ActiveResource is that methods prefaced with `$` interact with the backend.
//
// Calls the createURL defined on the API of the model. The createURL can either
// be set via Model.api.set('http://defaulturl.com') or overridden specifically
// by setting Model.api.createURL = 'http://myoverriddenURL.com'
//
// The API should respond with either a representation of the same resource, or
// an error.
//
// If a representation of the resource is received, Model.instance calls
// Model.instance#update passing in the data received from the server. If the
// resource has a hasMany relationship, and receives a representation of its child
// resources, the child resources will also be updated.
//
// To avoid having to call $scope.$apply with nested resources, nested resources
// call up to the highest-level resource to perform the $save. The $save still only
// calls the resource-in-question, and not its parent or parent's parent, but the
// parent is being actively watched for $http requests, while the child is not
// when created via the nested structure (e.g. $scope.system.sensors.new())
_this.prototype.$save = function(instance, url, put) {
if (!instance) instance = this;
// If passed with no arguments, we attempt to parse what is meant by the $save.
// If the instance contains a primary key, it should save to the updateURL as
// a PUT request. Otherwise, it should be a new instance saved to the createURL.
if (instance && instance[primaryKey] && !url) {
url = URLify(_this.api.updateURL, instance);
if (put !== false) put = true;
} else if (!url) {
url = _this.api.createURL;
}
_this.emit('$save:called', instance);
var json = serializer.serialize(instance);
if (instance.$invalid) {
_this.emit('$save:fail', instance);
return deferred(null, instance);
}
if (put) method = 'put';
else method = 'post';
return $http[method](url, json)
.then(function(response) {
return serializer.deserialize(response, instance).then(function(instance) {
_this.emit('$save:complete', {data: response, instance: instance});
return deferred(instance);
});
});
};
// Model.instance#$update(data)
//
// @param data {object} - Optional data to use to update the instance
//
// Updates the instance, and then persists the instance to the database via the
// $save method. Notice that methods prefaced with a dollar sign ($update, $save,
// $create, and $delete),perform unsafe API interactions, like PUT, POST, and DELETE.
//
// Model.instance#update below is distinct from $update, because it only works with the
// in-memory copy of the data, and does not attempt to persist the changes to the API.
_this.prototype.$update = function(data) {
var instance = this;
_this.emit('$update:called', {instance: instance, data: data});
var url = _this.api.updateURL;
if (!url) return;
if (url.match(/\:\w+/)) url = url.replace(/\:\w+/, this[primaryKey]);
if (data) {
return instance.update(data).then(function(response) {
instance = response;
return save();
});
} else {
return save();
}
function save() {
return instance.$save(instance, url, 'put');
}
};
// Model.instance#update
//
// Resource representations may be received many times over during the course of a
// session in a single page application. Whenever a new representation is received
// from the server, if a model instance of that representation already exists, it
// should be updated across the application.
//
// Model.instance#update receives server representations and updates the appropriate
// model objects with them. If an instance has a has many relationship to another model,
// and the representation received includes a reference to the has many relationship,
// the data on that reference will be used to update the foreign relationship.
//
// Update ensures random properties are not set on the instance. Only properties
// defined in the body of the constructor or via Object.defineProperty are considered
// "settable" via the model, although Javascript normally will allow you to set any
// property on any object using a setter. To ensure the sanctity of your data, use
// instance#update to set properties.
_this.prototype.update = function(data) {
_this.emit('update:called', {data: data, instance: this});
for (var attr in data) {
if (instanceHasManyOf(attr)) updateHasManyRelationship.call(this, data[attr], attr);
else if (instanceHasOneOf(attr)) updateHasOneRelationship.call(this, data[attr], attr);
else if (instanceBelongsTo(attr)) updateBelongsRelationship.call(this, data[attr], attr)
else if (isSettableProperty(attr)) this[attr] = data[attr];
}
var instance = this;
return serializer.deserialize(data, instance, {lazy: true, update: false})
.then(function(response) {
_this.emit('update:complete', {data: data, instance: instance});
return deferred(instance);
});
};
// Model#$create
//
// When a model calls $create, a new instance is built using the arguments passed in,
// and immediately saved. This calls Model.instance#$save, which will attempt to persist
// the instance to the backend. If the backend returns success, the new instance is added to
// the cache and returned.
//
// System.$create({placement: 'window'}).then(function(response) { system = response; });
//
// Model.$create is equivalent to calling Model.new().save()
_this.$create = function(data) {
if (!data) data = {};
_this.emit('$create:called', data);
var instance = _this.new(data);
instance.establishBelongsTo();
return instance.$save().then(function(response) {
instance = response;
cacheInstance(instance);
return deferred(instance);
});
};
// Model#new(data)
//
// @param {data} - JSON data used to instantiate a new instance of the model.
//
// New creates a new instance of the model. If an id is passed in, new first checks
// whether or not an object is stored in the cache with that id; if it is, it is returned.
// The new instance is added to the cache. If the instance has any hasMany relationships
// associated with it, those relationships are instantiated via an empty ActiveResource::Collection.
// The new collection associates this instance with it, so that calling:
//
// system.sensors.new()
//
// Associates the sensor with the system. E.g.:
//
// var sensor = system.sensors.new()
// expect(sensor.system).toEqual(system);
//
_this.new = function(data) {
if (!data) data = {};
_this.emit('new:called', data);
if (typeof data == 'Number') data = argify(data);
if (data && this.cached[data[primaryKey]]) return this.cached[data[primaryKey]];
_this.prototype.integer = function(propertyName) {
var instance = this;
var validations = {};
validations[propertyName] = {integer: {ignore: /\,/g } };
instance.validates(validations);
instance[propertyName] = data[propertyName];
};
_this.prototype.number = function(propertyName) {
var instance = this;
var validations = {};
validations[propertyName] = {numericality: {ignore: /\,/g } };
instance.validates(validations);
instance[propertyName] = data[propertyName];
};
_this.prototype.boolean = function(propertyName) {
var instance = this;
var validations = {};
validations[propertyName] = {boolean: true};
instance.validates(validations);
instance[propertyName] = data[propertyName];
};
_this.prototype.string = function(propertyName) {
var instance = this;
var validations = {};
validations[propertyName] = {string: true};
instance.validates(validations);
instance[propertyName] = data[propertyName];
};
// Instance#computedProperty(name, valueFn, dependents)
//
// @param name {string} - The name of the property to be computed from other properties
//
// @param valueFn {func} - The function used to compute the new property from the others
//
// @param dependents {string | array} - The name of the property or list of the properties that this
// property depends upon.
//
// Example:
//
// function Tshirt(attributes) {
// this.number('price');
//
// this.computedProperty('salePrice', function() {
// return this.price - (this.price * 0.2);
// }, 'price');
//
// this.computedProperty('superSalePrice', function() {
// return this.price - this.salePrice;
// }, ['price', 'salePrice']);
// }
//
// The computed property function creates configurable getters and setters (that can thus be reconfigured).
// In the first example, the price setter calls the salePrice setter whenever it updates. In the second
// example, the salePrice setter continues to be called by the price setter, and additionally calls the
// superSalePrice setter afterward.
//
// This chainability allows us to create complex inter-dependencies, where an update to one property
// updates many others. In order to all this to occur, we use the `__lookupSetter__` function to retrieve
// the value of the previous setter.
_this.prototype.computedProperty = function(name, valueFn, dependents) {
var instance = this;
if (!dependents) dependents = [];
if (!dependents.push) dependents = [dependents];
var local2;
Object.defineProperty(instance, name, {
enumerable: true,
configurable: true,
get: function() { return local2; },
set: function() {
local2 = valueFn.apply(instance);
return local2;
}
});
_.each(dependents, function(dependent) {
var local;
var previousSetter = instance.__lookupSetter__(dependent);
var dependentVal = instance[dependent];
Object.defineProperty(instance, dependent, {
enumerable: true,
configurable: true,
get: function() { return local; },
set: function(val) {
if (val !== undefined && val != 'set') local = val;
if (previousSetter) {
if (local == val) previousSetter();
else local = previousSetter();
}
instance[name] = 'set';
return local;
}
});
if (data && data[dependent]) instance[dependent] = data[dependent];
else instance[dependent] = dependentVal;
});
};
var instance = new this(data);
setPrimaryKey(instance, data);
cacheInstance(instance);
_.each(associations.belongsTo, function(model) {
var name = nameOfBelongsToModel(model);
if (data && data[name] !== undefined) {
instance[name] = data[name];
};
});
// Add any data passed to the hasMany relationships
_.each(associations.hasMany, function(collection) {
var name = collection.klass.name.pluralize().camelize();
instance[name][this.name.camelize()] = instance;
if (data[name] !== undefined) addNewDataToCollection(data, name, instance);
}, this);
_.each(associations.hasOne, function(rel) {
var name = rel.propertyName;
if (data[rel.propertyName] !== undefined) addNewDataToHasOne(data, name, instance, rel);
});
addValidations(instance);
_this.emit('new:complete', instance);
return instance;
};
// Model#where(terms, options)
//
// @param {terms} - JSON terms used to find all instances of an object matching specific parameters
//
// Used to find all instances of a model matching specific parameters:
//
// System.where({placement: "window"})
//
// Returns a collection of system instances where the placement attribute is set to "window"
_this.where = function(terms, options) {
// Generate start event
_this.emit('where:called', terms);
// Normalize variables
if (typeof terms != 'object') throw 'Argument to where must be an object';
if (!options) options = {lazy: false, overEager: false, api: true};
var cached = _this.cached.where(terms);
options.cached = cached;
options.multi = true;
var url = _this.api.indexURL || _this.api.showURL;
// Generate a GET request for all instances matching the given params, deserialize each
// into the appropriate class, and return the found collection
return GET(_this, url, terms, options).then(function(json) {
var results = [];
for (var i in json) {
var instance = _this.new(json[i]);
results.push(instance);
serializer.deserialize(json[i], instance, options);
}
// Watch all collections that get assigned out as variables
_this.watchedCollections.push(results);
_this.emit('where:complete', {instance: results, data: json});
return results;
});
};
// Model#all()
//
// Returns all instances of a model. Equivalent to Model#where({})
_this.all = function(options) {
// Generate start event
_this.emit('all:called');
return _this.where({}, options).then(function(results) {
var deferred = $q.defer();
deferred.resolve(results);
_this.emit('all:complete', results);
return deferred.promise;
});
};
// Model#find(terms, options)
//
// @param {terms} - JSON terms used to find a single instance of the model matching the given
// parameters
// @param {options} - Options include:
// * Lazy: Whether or not to lazy-load options.
//
// Used to find the first instance of a model that matches the parameters given:
//
// System.find({id: 1})
//
// Returns the system with an id of 1. By default, find eager-loads associated models. Passing
// the lazy option will cause find not to query for associated models.
_this.find = function(terms, options) {
var cached;
// Emit start event
_this.emit('find:called', terms);
// Normalize variables
if (typeof terms == 'number' || typeof terms == 'string') terms = argify(terms);
if (typeof terms != 'object') throw 'Argument to find must be an object';
if (!options) options = {lazy: false};
if (!options.forceGET) cached = _.first(_this.cached.where(terms));
var url = _this.api.showURL || _this.api.indexURL;
// If no instance is found in the cache, generate a GET request, and return the
// found instance, deserialized into the appropriate class
if (cached !== undefined) {
_this.emit('find:complete', {instance: cached, data: cached, message: 'Backend not queried. Found in cache'});
return deferred(cached);
} else {
return GET(_this, url, terms, options).then(function(json) {
var instance = _this.new(json);
_this.emit('find:complete', {instance: instance, data: json});
return serializer.deserialize(json, instance, options);
});
}
};
// Model.instance#$delete(terms)
//
// @param {terms} - JSON terms used to delete
//
_this.prototype.$delete = function() {
var instance = this;
_this.emit('$delete:called', this);
var queryterms = {};
var config = {};
var url = _this.api.deleteURL;
queryterms[primaryKey] = instance[primaryKey];
// if user has provided an attr in their deleteURL definition
// then we URLify the deleteURL. Else we pass in params as
if(_this.api.deleteURL.indexOf('/:') !== -1) {
url = URLify(_this.api.deleteURL, queryterms);
} else {
config = {params: queryterms};
}
return $http.delete(url, config).then(function(response) {
if (response.status == 200) {
removeFromWatchedCollections(instance);
_this.emit('$delete:complete', {data: response, instance: instance});
if (dependentDestroy.length >= 1) return destroyDependents(instance);
unlinkAssociations(instance);
delete _this.cached[instance[primaryKey]];
}
});
};
function removeFromWatchedCollections(instance) {
_.each(_this.watchedCollections, function(watchedCollection) {
_.remove(watchedCollection, instance);
});
}
// function instanceIsAssociatedWith(instance, association)
//
// @param {instance} - The instance in question
// @param {association} - The name of the associated model
//
// Checks whether or not an instance is associated with an instance of another model.
// In the event a "Sensor" model belongs to a "System" model, returns true if an instance
// of sensor contains a property called "system" that is an instance of the System model.