/
store.js
2861 lines (2318 loc) · 92.6 KB
/
store.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
// ==========================================================================
// Project: SproutCore - JavaScript Application Framework
// Copyright: ©2006-2011 Strobe Inc. and contributors.
// Portions ©2008-2011 Apple Inc. All rights reserved.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
sc_require('models/record');
/**
@class
The Store is where you can find all of your dataHashes. Stores can be
chained for editing purposes and committed back one chain level at a time
all the way back to a persistent data source.
Every application you create should generally have its own store objects.
Once you create the store, you will rarely need to work with the store
directly except to retrieve records and collections.
Internally, the store will keep track of changes to your json data hashes
and manage syncing those changes with your data source. A data source may
be a server, local storage, or any other persistent code.
@extends SC.Object
@since SproutCore 1.0
*/
SC.Store = SC.Object.extend( /** @scope SC.Store.prototype */ {
/**
An (optional) name of the store, which can be useful during debugging,
especially if you have multiple nested stores.
@type String
*/
name: null,
/**
An array of all the chained stores that current rely on the receiver
store.
@type Array
*/
nestedStores: null,
/**
The data source is the persistent storage that will provide data to the
store and save changes. You normally will set your data source when you
first create your store in your application.
@type SC.DataSource
*/
dataSource: null,
/**
This type of store is not nested.
@default NO
@type Boolean
*/
isNested: NO,
/**
This type of store is not nested.
@default NO
@type Boolean
*/
commitRecordsAutomatically: NO,
// ..........................................................
// DATA SOURCE SUPPORT
//
/**
Convenience method. Sets the current data source to the passed property.
This will also set the store property on the dataSource to the receiver.
If you are using this from the `core.js` method of your app, you may need to
just pass a string naming your data source class. If this is the case,
then your data source will be instantiated the first time it is requested.
@param {SC.DataSource|String} dataSource the data source
@returns {SC.Store} receiver
*/
from: function(dataSource) {
this.set('dataSource', dataSource);
return this ;
},
// lazily convert data source to real object
_getDataSource: function() {
var ret = this.get('dataSource');
if (typeof ret === SC.T_STRING) {
ret = SC.requiredObjectForPropertyPath(ret);
if (ret.isClass) ret = ret.create();
this.set('dataSource', ret);
}
return ret;
},
/**
Convenience method. Creates a `CascadeDataSource` with the passed
data source arguments and sets the `CascadeDataSource` as the data source
for the receiver.
@param {SC.DataSource...} dataSource one or more data source arguments
@returns {SC.Store} receiver
*/
cascade: function(dataSource) {
var dataSources = SC.A(arguments) ;
dataSource = SC.CascadeDataSource.create({
dataSources: dataSources
});
return this.from(dataSource);
},
// ..........................................................
// STORE CHAINING
//
/**
Returns a new nested store instance that can be used to buffer changes
until you are ready to commit them. When you are ready to commit your
changes, call `commitChanges()` or `destroyChanges()` and then `destroy()`
when you are finished with the chained store altogether.
store = MyApp.store.chain();
.. edit edit edit
store.commitChanges().destroy();
@param {Hash} attrs optional attributes to set on new store
@param {Class} newStoreClass optional the class of the newly-created nested store (defaults to SC.NestedStore)
@returns {SC.NestedStore} new nested store chained to receiver
*/
chain: function(attrs, newStoreClass) {
if (!attrs) attrs = {};
attrs.parentStore = this;
if (newStoreClass) {
// Ensure the passed-in class is a type of nested store.
if (SC.typeOf(newStoreClass) !== 'class') throw new Error("%@ is not a valid class".fmt(newStoreClass));
if (!SC.kindOf(newStoreClass, SC.NestedStore)) throw new Error("%@ is not a type of SC.NestedStore".fmt(newStoreClass));
}
else {
newStoreClass = SC.NestedStore;
}
// Replicate parent records references
attrs.childRecords = this.childRecords ? SC.clone(this.childRecords) : {};
attrs.parentRecords = this.parentRecords ? SC.clone(this.parentRecords) : {};
var ret = newStoreClass.create(attrs),
nested = this.nestedStores;
if (!nested) nested = this.nestedStores = [];
nested.push(ret);
return ret ;
},
/** @private
Called by a nested store just before it is destroyed so that the parent
can remove the store from its list of nested stores.
@returns {SC.Store} receiver
*/
willDestroyNestedStore: function(nestedStore) {
if (this.nestedStores) {
this.nestedStores.removeObject(nestedStore);
}
return this ;
},
/**
Used to determine if a nested store belongs directly or indirectly to the
receiver.
@param {SC.Store} store store instance
@returns {Boolean} YES if belongs
*/
hasNestedStore: function(store) {
while(store && (store !== this)) store = store.get('parentStore');
return store === this ;
},
// ..........................................................
// SHARED DATA STRUCTURES
//
/** @private
JSON data hashes indexed by store key.
*IMPORTANT: Property is not observable*
Shared by a store and its child stores until you make edits to it.
@type Hash
*/
dataHashes: null,
/** @private
The current status of a data hash indexed by store key.
*IMPORTANT: Property is not observable*
Shared by a store and its child stores until you make edits to it.
@type Hash
*/
statuses: null,
/** @private
This array contains the revisions for the attributes indexed by the
storeKey.
*IMPORTANT: Property is not observable*
Revisions are used to keep track of when an attribute hash has been
changed. A store shares the revisions data with its parent until it
starts to make changes to it.
@type Hash
*/
revisions: null,
/**
Array indicates whether a data hash is possibly in use by an external
record for editing. If a data hash is editable then it may be modified
at any time and therefore chained stores may need to clone the
attributes before keeping a copy of them.
Note that this is kept as an array because it will be stored as a dense
array on some browsers, making it faster.
@type Array
*/
editables: null,
/**
A set of storeKeys that need to be committed back to the data source. If
you call `commitRecords()` without passing any other parameters, the keys
in this set will be committed instead.
@type SC.Set
*/
changelog: null,
/**
An array of `SC.Error` objects associated with individual records in the
store (indexed by store keys).
Errors passed form the data source in the call to dataSourceDidError() are
stored here.
@type Array
*/
recordErrors: null,
/**
A hash of `SC.Error` objects associated with queries (indexed by the GUID
of the query).
Errors passed from the data source in the call to
`dataSourceDidErrorQuery()` are stored here.
@type Hash
*/
queryErrors: null,
/**
A hash of child Records and there immediate parents
*/
childRecords: null,
/**
A hash of parent records with registered children
*/
parentRecords: null,
// ..........................................................
// CORE ATTRIBUTE API
//
// The methods in this layer work on data hashes in the store. They do not
// perform any changes that can impact records. Usually you will not need
// to use these methods.
/**
Returns the current edit status of a storekey. May be one of
`EDITABLE` or `LOCKED`. Used mostly for unit testing.
@param {Number} storeKey the store key
@returns {Number} edit status
*/
storeKeyEditState: function(storeKey) {
var editables = this.editables, locks = this.locks;
return (editables && editables[storeKey]) ? SC.Store.EDITABLE : SC.Store.LOCKED ;
},
/**
Returns the data hash for the given `storeKey`. This will also 'lock'
the hash so that further edits to the parent store will no
longer be reflected in this store until you reset.
@param {Number} storeKey key to retrieve
@returns {Hash} data hash or null
*/
readDataHash: function(storeKey) {
return this.dataHashes[storeKey];
},
/**
Returns the data hash for the `storeKey`, cloned so that you can edit
the contents of the attributes if you like. This will do the extra work
to make sure that you only clone the attributes one time.
If you use this method to modify data hash, be sure to call
`dataHashDidChange()` when you make edits to record the change.
@param {Number} storeKey the store key to retrieve
@returns {Hash} the attributes hash
*/
readEditableDataHash: function(storeKey) {
// read the value - if there is no hash just return; nothing to do
var ret = this.dataHashes[storeKey];
if (!ret) return ret ; // nothing to do.
// clone data hash if not editable
var editables = this.editables;
if (!editables) editables = this.editables = [];
if (!editables[storeKey]) {
editables[storeKey] = 1 ; // use number to store as dense array
ret = this.dataHashes[storeKey] = SC.clone(ret, YES);
}
return ret;
},
/**
Reads a property from the hash - cloning it if needed so you can modify
it independently of any parent store. This method is really only well
tested for use with toMany relationships. Although it is public you
generally should not call it directly.
@param {Number} storeKey storeKey of data hash
@param {String} propertyName property to read
@returns {Object} editable property value
*/
readEditableProperty: function(storeKey, propertyName) {
var hash = this.readEditableDataHash(storeKey),
editables = this.editables[storeKey], // get editable info...
ret = hash[propertyName];
// editables must be made into a hash so that we can keep track of which
// properties have already been made editable
if (editables === 1) editables = this.editables[storeKey] = {};
// clone if needed
if (!editables[propertyName]) {
ret = hash[propertyName];
if (ret && ret.isCopyable) ret = hash[propertyName] = ret.copy(YES);
editables[propertyName] = YES ;
}
return ret ;
},
/**
Replaces the data hash for the `storeKey`. This will lock the data hash
and mark them as cloned. This will also call `dataHashDidChange()` for
you.
Note that the hash you set here must be a different object from the
original data hash. Once you make a change here, you must also call
`dataHashDidChange()` to register the changes.
If the data hash does not yet exist in the store, this method will add it.
Pass the optional status to edit the status as well.
@param {Number} storeKey the store key to write
@param {Hash} hash the new hash
@param {String} status the new hash status
@returns {SC.Store} receiver
*/
writeDataHash: function(storeKey, hash, status) {
// update dataHashes and optionally status.
if (hash) this.dataHashes[storeKey] = hash;
if (status) this.statuses[storeKey] = status ;
// also note that this hash is now editable
var editables = this.editables;
if (!editables) editables = this.editables = [];
editables[storeKey] = 1 ; // use number for dense array support
var that = this;
this._propagateToChildren(storeKey, function(storeKey){
that.writeDataHash(storeKey, null, status);
});
return this ;
},
/**
Removes the data hash from the store. This does not imply a deletion of
the record. You could be simply unloading the record. Either way,
removing the dataHash will be synced back to the parent store but not to
the server.
Note that you can optionally pass a new status to go along with this. If
you do not pass a status, it will change the status to `SC.RECORD_EMPTY`
(assuming you just unloaded the record). If you are deleting the record
you may set it to `SC.Record.DESTROYED_CLEAN`.
Be sure to also call `dataHashDidChange()` to register this change.
@param {Number} storeKey
@param {String} status optional new status
@returns {SC.Store} receiver
*/
removeDataHash: function(storeKey, status) {
// don't use delete -- that will allow parent dataHash to come through
this.dataHashes[storeKey] = null;
this.statuses[storeKey] = status || SC.Record.EMPTY;
// hash is gone and therefore no longer editable
var editables = this.editables;
if (editables) editables[storeKey] = 0 ;
return this ;
},
/**
Reads the current status for a storeKey. This will also lock the data
hash. If no status is found, returns `SC.RECORD_EMPTY`.
@param {Number} storeKey the store key
@returns {Number} status
*/
readStatus: function(storeKey) {
// use readDataHash to handle optimistic locking. this could be inlined
// but for now this minimized copy-and-paste code.
this.readDataHash(storeKey);
return this.statuses[storeKey] || SC.Record.EMPTY;
},
/**
Reads the current status for the storeKey without actually locking the
record. Usually you won't need to use this method. It is mostly used
internally.
@param {Number} storeKey the store key
@returns {Number} status
*/
peekStatus: function(storeKey) {
return this.statuses[storeKey] || SC.Record.EMPTY;
},
/**
Writes the current status for a storeKey. If the new status is
`SC.Record.ERROR`, you may also pass an optional error object. Otherwise
this param is ignored.
@param {Number} storeKey the store key
@param {String} newStatus the new status
@param {SC.Error} error optional error object
@returns {SC.Store} receiver
*/
writeStatus: function(storeKey, newStatus) {
// use writeDataHash for now to handle optimistic lock. maximize code
// reuse.
return this.writeDataHash(storeKey, null, newStatus);
},
/**
Call this method whenever you modify some editable data hash to register
with the Store that the attribute values have actually changed. This will
do the book-keeping necessary to track the change across stores including
managing locks.
@param {Number|Array} storeKeys one or more store keys that changed
@param {Number} rev optional new revision number. normally leave null
@param {Boolean} statusOnly (optional) YES if only status changed
@param {String} key that changed (optional)
@returns {SC.Store} receiver
*/
dataHashDidChange: function(storeKeys, rev, statusOnly, key) {
// update the revision for storeKey. Use generateStoreKey() because that
// gaurantees a universally (to this store hierarchy anyway) unique
// key value.
if (!rev) rev = SC.Store.generateStoreKey();
var isArray, len, idx, storeKey;
isArray = SC.typeOf(storeKeys) === SC.T_ARRAY;
if (isArray) {
len = storeKeys.length;
} else {
len = 1;
storeKey = storeKeys;
}
var that = this;
for(idx=0;idx<len;idx++) {
if (isArray) storeKey = storeKeys[idx];
this.revisions[storeKey] = rev;
this._notifyRecordPropertyChange(storeKey, statusOnly, key);
this._propagateToChildren(storeKey, function(storeKey){
that.dataHashDidChange(storeKey, null, statusOnly, key);
});
}
return this ;
},
/** @private
Will push all changes to a the recordPropertyChanges property
and execute `flush()` once at the end of the runloop.
*/
_notifyRecordPropertyChange: function(storeKey, statusOnly, key) {
var records = this.records,
nestedStores = this.get('nestedStores'),
K = SC.Store,
rec, editState, len, idx, store, status, keys;
// pass along to nested stores
len = nestedStores ? nestedStores.length : 0 ;
for(idx=0;idx<len;idx++) {
store = nestedStores[idx];
status = store.peekStatus(storeKey); // important: peek avoids read-lock
editState = store.storeKeyEditState(storeKey);
// when store needs to propagate out changes in the parent store
// to nested stores
if (editState === K.INHERITED) {
store._notifyRecordPropertyChange(storeKey, statusOnly, key);
} else if (status & SC.Record.BUSY) {
// make sure nested store does not have any changes before resetting
if(store.get('hasChanges')) throw K.CHAIN_CONFLICT_ERROR;
store.reset();
}
}
// store info in changes hash and schedule notification if needed.
var changes = this.recordPropertyChanges;
if (!changes) {
changes = this.recordPropertyChanges =
{ storeKeys: SC.CoreSet.create(),
records: SC.CoreSet.create(),
hasDataChanges: SC.CoreSet.create(),
propertyForStoreKeys: {} };
}
changes.storeKeys.add(storeKey);
if (records && (rec=records[storeKey])) {
changes.records.push(storeKey);
// If there are changes other than just the status we need to record
// that information so we do the right thing during the next flush.
// Note that if we're called multiple times before flush and one call
// has `statusOnly=true` and another has `statusOnly=false`, the flush
// will (correctly) operate in `statusOnly=false` mode.
if (!statusOnly) changes.hasDataChanges.push(storeKey);
// If this is a key specific change, make sure that only those
// properties/keys are notified. However, if a previous invocation of
// `_notifyRecordPropertyChange` specified that all keys have changed, we
// need to respect that.
if (key) {
if (!(keys = changes.propertyForStoreKeys[storeKey])) {
keys = changes.propertyForStoreKeys[storeKey] = SC.CoreSet.create();
}
// If it's '*' instead of a set, then that means there was a previous
// invocation that said all keys have changed.
if (keys !== '*') {
keys.add(key);
}
}
else {
// Mark that all properties have changed.
changes.propertyForStoreKeys[storeKey] = '*';
}
}
this.invokeOnce(this.flush);
return this;
},
/**
Delivers any pending changes to materialized records. Normally this
happens once, automatically, at the end of the RunLoop. If you have
updated some records and need to update records immediately, however,
you may call this manually.
@returns {SC.Store} receiver
*/
flush: function() {
if (!this.recordPropertyChanges) return this;
var changes = this.recordPropertyChanges,
storeKeys = changes.storeKeys,
hasDataChanges = changes.hasDataChanges,
records = changes.records,
propertyForStoreKeys = changes.propertyForStoreKeys,
recordTypes = SC.CoreSet.create(),
rec, recordType, statusOnly, idx, len, storeKey, keys;
storeKeys.forEach(function(storeKey) {
if (records.contains(storeKey)) {
statusOnly = hasDataChanges.contains(storeKey) ? NO : YES;
rec = this.records[storeKey];
keys = propertyForStoreKeys ? propertyForStoreKeys[storeKey] : null;
// Are we invalidating all keys? If so, don't pass any to
// storeDidChangeProperties.
if (keys === '*') keys = null;
// remove it so we don't trigger this twice
records.remove(storeKey);
if (rec) rec.storeDidChangeProperties(statusOnly, keys);
}
recordType = SC.Store.recordTypeFor(storeKey);
recordTypes.add(recordType);
}, this);
if (storeKeys.get('length') > 0) this._notifyRecordArrays(storeKeys, recordTypes);
storeKeys.clear();
hasDataChanges.clear();
records.clear();
// Provide full reference to overwrite
this.recordPropertyChanges.propertyForStoreKeys = {};
return this;
},
/**
Resets the store content. This will clear all internal data for all
records, resetting them to an EMPTY state. You generally do not want
to call this method yourself, though you may override it.
@returns {SC.Store} receiver
*/
reset: function() {
// create a new empty data store
this.dataHashes = {} ;
this.revisions = {} ;
this.statuses = {} ;
// also reset temporary objects and errors
this.chainedChanges = this.locks = this.editables = null;
this.changelog = null ;
this.recordErrors = null;
this.queryErrors = null;
var dataSource = this.get('dataSource');
if (dataSource && dataSource.reset) { dataSource.reset(); }
var records = this.records, storeKey;
if (records) {
for(storeKey in records) {
if (!records.hasOwnProperty(storeKey)) continue ;
this._notifyRecordPropertyChange(parseInt(storeKey, 10), NO);
}
}
this.set('hasChanges', NO);
},
/** @private
Called by a nested store on a parent store to commit any changes from the
store. This will copy any changed dataHashes as well as any persistant
change logs.
If the parentStore detects a conflict with the optimistic locking, it will
raise an exception before it makes any changes. If you pass the
force flag then this detection phase will be skipped and the changes will
be applied even if another resource has modified the store in the mean
time.
@param {SC.Store} nestedStore the child store
@param {SC.Set} changes the set of changed store keys
@param {Boolean} force
@returns {SC.Store} receiver
*/
commitChangesFromNestedStore: function(nestedStore, changes, force) {
// first, check for optimistic locking problems
if (!force) this._verifyLockRevisions(changes, nestedStore.locks);
// OK, no locking issues. So let's just copy them changes.
// get local reference to values.
var len = changes.length, i, storeKey, myDataHashes, myStatuses,
myEditables, myRevisions, myParentRecords, myChildRecords,
chDataHashes, chStatuses, chRevisions, chParentRecords, chChildRecords;
myRevisions = this.revisions ;
myDataHashes = this.dataHashes;
myStatuses = this.statuses;
myEditables = this.editables ;
myParentRecords = this.parentRecords ? this.parentRecords : this.parentRecords ={} ;
myChildRecords = this.childRecords ? this.childRecords : this.childRecords = {} ;
// setup some arrays if needed
if (!myEditables) myEditables = this.editables = [] ;
chDataHashes = nestedStore.dataHashes;
chRevisions = nestedStore.revisions ;
chStatuses = nestedStore.statuses;
chParentRecords = nestedStore.parentRecords || {};
chChildRecords = nestedStore.childRecords || {};
for(i=0;i<len;i++) {
storeKey = changes[i];
// now copy changes
myDataHashes[storeKey] = chDataHashes[storeKey];
myStatuses[storeKey] = chStatuses[storeKey];
myRevisions[storeKey] = chRevisions[storeKey];
myParentRecords[storeKey] = chParentRecords[storeKey];
myChildRecords[storeKey] = chChildRecords[storeKey];
myEditables[storeKey] = 0 ; // always make dataHash no longer editable
this._notifyRecordPropertyChange(storeKey, NO);
}
// add any records to the changelog for commit handling
var myChangelog = this.changelog, chChangelog = nestedStore.changelog;
if (chChangelog) {
if (!myChangelog) myChangelog = this.changelog = SC.CoreSet.create();
myChangelog.addEach(chChangelog);
}
this.changelog = myChangelog;
// immediately flush changes to notify records - nested stores will flush
// later on.
if (!this.get('parentStore')) this.flush();
return this ;
},
/** @private
Verifies that the passed lock revisions match the current revisions
in the receiver store. If the lock revisions do not match, then the
store is in a conflict and an exception will be raised.
@param {Array} changes set of changes we are trying to apply
@param {SC.Set} locks the locks to verify
@returns {SC.Store} receiver
*/
_verifyLockRevisions: function(changes, locks) {
var len = changes.length, revs = this.revisions, i, storeKey, lock, rev ;
if (locks && revs) {
for(i=0;i<len;i++) {
storeKey = changes[i];
lock = locks[storeKey] || 1;
rev = revs[storeKey] || 1;
// if the save revision for the item does not match the current rev
// the someone has changed the data hash in this store and we have
// a conflict.
if (lock < rev) throw SC.Store.CHAIN_CONFLICT_ERROR;
}
}
return this ;
},
// ..........................................................
// HIGH-LEVEL RECORD API
//
/**
Finds a single record instance with the specified `recordType` and id or
an array of records matching some query conditions.
Finding a Single Record
---
If you pass a single `recordType` and id, this method will return an
actual record instance. If the record has not been loaded into the store
yet, this method will ask the data source to retrieve it. If no data
source indicates that it can retrieve the record, then this method will
return `null`.
Note that if the record needs to be retrieved from the server, then the
record instance returned from this method will not have any data yet.
Instead it will have a status of `SC.Record.READY_LOADING`. You can
monitor the status property to be notified when the record data is
available for you to use it.
Find a Collection of Records
---
If you pass only a record type or a query object, you can instead find
all records matching a specified set of conditions. When you call
`find()` in this way, it will create a query if needed and pass it to the
data source to fetch the results.
If this is the first time you have fetched the query, then the store will
automatically ask the data source to fetch any records related to it as
well. Otherwise you can refresh the query results at anytime by calling
`refresh()` on the returned `RecordArray`.
You can detect whether a RecordArray is fetching from the server by
checking its status.
Examples
---
Finding a single record:
MyApp.store.find(MyApp.Contact, "23"); // returns MyApp.Contact
Finding all records of a particular type:
MyApp.store.find(MyApp.Contact); // returns SC.RecordArray of contacts
Finding all contacts with first name John:
var query = SC.Query.local(MyApp.Contact, "firstName = %@", "John");
MyApp.store.find(query); // returns SC.RecordArray of contacts
Finding all contacts using a remote query:
var query = SC.Query.remote(MyApp.Contact);
MyApp.store.find(query); // returns SC.RecordArray filled by server
@param {SC.Record|String} recordType the expected record type
@param {String} id the id to load
@returns {SC.Record} record instance or null
*/
find: function(recordType, id) {
// if recordType is passed as string, find object
if (SC.typeOf(recordType)===SC.T_STRING) {
recordType = SC.objectForPropertyPath(recordType);
}
// handle passing a query...
if ((arguments.length === 1) && !(recordType && recordType.get && recordType.get('isRecord'))) {
if (!recordType) throw new Error("SC.Store#find() must pass recordType or query");
if (!recordType.isQuery) {
recordType = SC.Query.local(recordType);
}
return this._findQuery(recordType, YES, YES);
// handle finding a single record
} else {
return this._findRecord(recordType, id);
}
},
/** @private
DEPRECATED used find() instead.
This method will accept a record type or query and return a record array
matching the results. This method was commonly used prior to SproutCore
1.0. It has been deprecated in favor of a single `find()` method instead.
For compatibility, this method will continue to work in SproutCore 1.0 but
it will raise a warning. It will be removed in a future version of
SproutCore.
*/
findAll: function(recordType, conditions, params) {
SC.Logger.warn("SC.Store#findAll() will be removed in a future version of SproutCore. Use SC.Store#find() instead");
if (!recordType || !recordType.isQuery) {
recordType = SC.Query.local(recordType, conditions, params);
}
return this._findQuery(recordType, YES, YES);
},
_findQuery: function(query, createIfNeeded, refreshIfNew) {
// lookup the local RecordArray for this query.
var cache = this._scst_recordArraysByQuery,
key = SC.guidFor(query),
ret, ra ;
if (!cache) cache = this._scst_recordArraysByQuery = {};
ret = cache[key];
// if a RecordArray was not found, then create one and also add it to the
// list of record arrays to update.
if (!ret && createIfNeeded) {
cache[key] = ret = SC.RecordArray.create({ store: this, query: query });
ra = this.get('recordArrays');
if (!ra) this.set('recordArrays', ra = SC.Set.create());
ra.add(ret);
if (refreshIfNew) this.refreshQuery(query);
}
this.flush();
return ret ;
},
_findRecord: function(recordType, id) {
var storeKey ;
// if a record instance is passed, simply use the storeKey. This allows
// you to pass a record from a chained store to get the same record in the
// current store.
if (recordType && recordType.get && recordType.get('isRecord')) {
storeKey = recordType.get('storeKey');
// otherwise, lookup the storeKey for the passed id. look in subclasses
// as well.
} else storeKey = id ? recordType.storeKeyFor(id) : null;
if (storeKey && (this.readStatus(storeKey) === SC.Record.EMPTY)) {
storeKey = this.retrieveRecord(recordType, id);
}
// now we have the storeKey, materialize the record and return it.
return storeKey ? this.materializeRecord(storeKey) : null ;
},
// ..........................................................
// RECORD ARRAY OPERATIONS
//
/**
Called by the record array just before it is destroyed. This will
de-register it from receiving future notifications.
You should never call this method yourself. Instead call `destroy()` on
the `RecordArray` directly.
@param {SC.RecordArray} recordArray the record array
@returns {SC.Store} receiver
*/
recordArrayWillDestroy: function(recordArray) {
var cache = this._scst_recordArraysByQuery,
set = this.get('recordArrays');
if (cache) delete cache[SC.guidFor(recordArray.get('query'))];
if (set) set.remove(recordArray);
return this ;
},
/**
Called by the record array whenever it needs the data source to refresh
its contents. Nested stores will actually just pass this along to the
parent store. The parent store will call `fetch()` on the data source.
You should never call this method yourself. Instead call `refresh()` on
the `RecordArray` directly.
@param {SC.Query} query the record array query to refresh
@returns {SC.Store} receiver
*/
refreshQuery: function(query) {
if (!query) throw new Error("refreshQuery() requires a query");
var cache = this._scst_recordArraysByQuery,
recArray = cache ? cache[SC.guidFor(query)] : null,
source = this._getDataSource();
if (source && source.fetch) {
if (recArray) recArray.storeWillFetchQuery(query);
source.fetch.call(source, this, query);
}
return this ;
},
/** @private
Will ask all record arrays that have been returned from `findAll`
with an `SC.Query` to check their arrays with the new `storeKey`s
@param {SC.IndexSet} storeKeys set of storeKeys that changed
@param {SC.Set} recordTypes
@returns {SC.Store} receiver
*/
_notifyRecordArrays: function(storeKeys, recordTypes) {
var recordArrays = this.get('recordArrays');
if (!recordArrays) return this;
recordArrays.forEach(function(recArray) {
if (recArray) recArray.storeDidChangeStoreKeys(storeKeys, recordTypes);
}, this);
return this ;
},
// ..........................................................