source: Dev/branches/rest-dojo-ui/client/dojox/data/ServiceStore.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

File size: 13.7 KB
Line 
1define(["dojo/_base/declare", "dojo/_base/lang", "dojo/_base/array"],
2  function(declare, lang, array) {
3
4// note that dojox.rpc.Service is not required, you can create your own services
5
6// A ServiceStore is a readonly data store that provides a data.data interface to an RPC service.
7// var myServices = new dojox.rpc.Service(dojo.moduleUrl("dojox.rpc.tests.resources", "test.smd"));
8// var serviceStore = new dojox.data.ServiceStore({service:myServices.ServiceStore});
9//
10// The ServiceStore also supports lazy loading. References can be made to objects that have not been loaded.
11//      For example if a service returned:
12// {"name":"Example","lazyLoadedObject":{"$ref":"obj2"}}
13//
14// And this object has accessed using the dojo.data API:
15// var obj = serviceStore.getValue(myObject,"lazyLoadedObject");
16// The object would automatically be requested from the server (with an object id of "obj2").
17//
18
19return declare("dojox.data.ServiceStore",
20        // ClientFilter is intentionally not required, ServiceStore does not need it, and is more
21        // lightweight without it, but if it is provided, the ServiceStore will use it.
22        lang.getObject("dojox.data.ClientFilter", 0)||null,{
23                service: null,
24                constructor: function(options){
25                        //summary:
26                        //              ServiceStore constructor, instantiate a new ServiceStore
27                        //              A ServiceStore can be configured from a JSON Schema. Queries are just
28                        //              passed through to the underlying services
29                        //
30                        // options:
31                        //              Keyword arguments
32                        // The *schema* parameter
33                        //              This is a schema object for this store. This should be JSON Schema format.
34                        //
35                        // The *service* parameter
36                        //              This is the service object that is used to retrieve lazy data and save results
37                        //              The function should be directly callable with a single parameter of an object id to be loaded
38                        //
39                        // The *idAttribute* parameter
40                        //              Defaults to 'id'. The name of the attribute that holds an objects id.
41                        //              This can be a preexisting id provided by the server.
42                        //              If an ID isn't already provided when an object
43                        //              is fetched or added to the store, the autoIdentity system
44                        //              will generate an id for it and add it to the index.
45                        //
46                        // The *estimateCountFactor* parameter
47                        //              This parameter is used by the ServiceStore to estimate the total count. When
48                        //              paging is indicated in a fetch and the response includes the full number of items
49                        //              requested by the fetch's count parameter, then the total count will be estimated
50                        //              to be estimateCountFactor multiplied by the provided count. If this is 1, then it is assumed that the server
51                        //              does not support paging, and the response is the full set of items, where the
52                        //              total count is equal to the numer of items returned. If the server does support
53                        //              paging, an estimateCountFactor of 2 is a good value for estimating the total count
54                        //              It is also possible to override _processResults if the server can provide an exact
55                        //              total count.
56                        //
57                        // The *syncMode* parameter
58                        //              Setting this to true will set the store to using synchronous calls by default.
59                        //              Sync calls return their data immediately from the calling function, so
60                        //              callbacks are unnecessary. This will only work with a synchronous capable service.
61                        //
62                        // description:
63                        //              ServiceStore can do client side caching and result set updating if
64                        //              dojox.data.ClientFilter is loaded. Do this add:
65                        //      |       dojo.require("dojox.data.ClientFilter")
66                        //              prior to loading the ServiceStore (ClientFilter must be loaded before ServiceStore).
67                        //              To utilize client side filtering with a subclass, you can break queries into
68                        //              client side and server side components by putting client side actions in
69                        //              clientFilter property in fetch calls. For example you could override fetch:
70                        //      |       fetch: function(args){
71                                //      |               // do the sorting and paging on the client side
72                                //      |               args.clientFilter = {start:args.start, count: args.count, sort: args.sort};
73                                //      |               // args.query will be passed to the service object for the server side handling
74                                //      |               return this.inherited(arguments);
75                        //      |       }
76                        //              When extending this class, if you would like to create lazy objects, you can follow
77                        //              the example from dojox.data.tests.stores.ServiceStore:
78                        // |    var lazyItem = {
79                        // |            _loadObject: function(callback){
80                        // |                    this.name="loaded";
81                        // |                    delete this._loadObject;
82                        // |                    callback(this);
83                        // |            }
84                        // |    };
85                        //setup a byId alias to the api call
86                        this.byId=this.fetchItemByIdentity;
87                        this._index = {};
88                        // if the advanced json parser is enabled, we can pass through object updates as onSet events
89                        if(options){
90                                lang.mixin(this,options);
91                        }
92                        // We supply a default idAttribute for parser driven construction, but if no id attribute
93                        //      is supplied, it should be null so that auto identification takes place properly
94                        this.idAttribute = (options && options.idAttribute) || (this.schema && this.schema._idAttr);
95                },
96                schema: null,
97                idAttribute: "id",
98                labelAttribute: "label",
99                syncMode: false,
100                estimateCountFactor: 1,
101                getSchema: function(){
102                        return this.schema;
103                },
104
105                loadLazyValues:true,
106
107                getValue: function(/*Object*/ item, /*String*/property, /*value?*/defaultValue){
108                        // summary:
109                        //      Gets the value of an item's 'property'
110                        //
111                        //      item:
112                        //              The item to get the value from
113                        //      property:
114                        //              property to look up value for
115                        //      defaultValue:
116                        //              the default value
117
118                        var value = item[property];
119                        return value || // return the plain value since it was found;
120                                                (property in item ? // a truthy value was not found, see if we actually have it
121                                                        value : // we do, so we can return it
122                                                        item._loadObject ? // property was not found, maybe because the item is not loaded, we will try to load it synchronously so we can get the property
123                                                                (dojox.rpc._sync = true) && arguments.callee.call(this,dojox.data.ServiceStore.prototype.loadItem({item:item}) || {}, property, defaultValue) : // load the item and run getValue again
124                                                                defaultValue);// not in item -> return default value
125                },
126                getValues: function(item, property){
127                        // summary:
128                        //              Gets the value of an item's 'property' and returns
129                        //              it.     If this value is an array it is just returned,
130                        //              if not, the value is added to an array and that is returned.
131                        //
132                        //      item: /* object */
133                        //      property: /* string */
134                        //              property to look up value for
135
136                        var val = this.getValue(item,property);
137                        return val instanceof Array ? val : val === undefined ? [] : [val];
138                },
139
140                getAttributes: function(item){
141                        // summary:
142                        //      Gets the available attributes of an item's 'property' and returns
143                        //      it as an array.
144                        //
145                        //      item: /* object */
146
147                        var res = [];
148                        for(var i in item){
149                                if(item.hasOwnProperty(i) && !(i.charAt(0) == '_' && i.charAt(1) == '_')){
150                                        res.push(i);
151                                }
152                        }
153                        return res;
154                },
155
156                hasAttribute: function(item,attribute){
157                        // summary:
158                        //              Checks to see if item has attribute
159                        //
160                        //      item: /* object */
161                        //      attribute: /* string */
162                        return attribute in item;
163                },
164
165                containsValue: function(item, attribute, value){
166                        // summary:
167                        //              Checks to see if 'item' has 'value' at 'attribute'
168                        //
169                        //      item: /* object */
170                        //      attribute: /* string */
171                        //      value: /* anything */
172                        return array.indexOf(this.getValues(item,attribute),value) > -1;
173                },
174
175
176                isItem: function(item){
177                        // summary:
178                        //              Checks to see if the argument is an item
179                        //
180                        //      item: /* object */
181                        //      attribute: /* string */
182
183                        // we have no way of determining if it belongs, we just have object returned from
184                        //      service queries
185                        return (typeof item == 'object') && item && !(item instanceof Date);
186                },
187
188                isItemLoaded: function(item){
189                        // summary:
190                        //              Checks to see if the item is loaded.
191                        //
192                        //              item: /* object */
193
194                        return item && !item._loadObject;
195                },
196
197                loadItem: function(args){
198                        // summary:
199                        //              Loads an item and calls the callback handler. Note, that this will call the callback
200                        //              handler even if the item is loaded. Consequently, you can use loadItem to ensure
201                        //              that an item is loaded is situations when the item may or may not be loaded yet.
202                        //              If you access a value directly through property access, you can use this to load
203                        //              a lazy value as well (doesn't need to be an item).
204                        //
205                        //      example:
206                        //              store.loadItem({
207                        //                      item: item, // this item may or may not be loaded
208                        //                      onItem: function(item){
209                        //                              // do something with the item
210                        //                      }
211                        //              });
212
213                        var item;
214                        if(args.item._loadObject){
215                                args.item._loadObject(function(result){
216                                        item = result; // in synchronous mode this can allow loadItem to return the value
217                                        delete item._loadObject;
218                                        var func = result instanceof Error ? args.onError : args.onItem;
219                                        if(func){
220                                                func.call(args.scope, result);
221                                        }
222                                });
223                        }else if(args.onItem){
224                                // even if it is already loaded, we will use call the callback, this makes it easier to
225                                // use when it is not known if the item is loaded (you can always safely call loadItem).
226                                args.onItem.call(args.scope, args.item);
227                        }
228                        return item;
229                },
230                _currentId : 0,
231                _processResults : function(results, deferred){
232                        // this should return an object with the items as an array and the total count of
233                        // items (maybe more than currently in the result set).
234                        // for example:
235                        //      | {totalCount:10, items: [{id:1},{id:2}]}
236
237                        // index the results, assigning ids as necessary
238
239                        if(results && typeof results == 'object'){
240                                var id = results.__id;
241                                if(!id){// if it hasn't been assigned yet
242                                        if(this.idAttribute){
243                                                // use the defined id if available
244                                                id = results[this.idAttribute];
245                                        }else{
246                                                id = this._currentId++;
247                                        }
248                                        if(id !== undefined){
249                                                var existingObj = this._index[id];
250                                                if(existingObj){
251                                                        for(var j in existingObj){
252                                                                delete existingObj[j]; // clear it so we can mixin
253                                                        }
254                                                        results = lang.mixin(existingObj,results);
255                                                }
256                                                results.__id = id;
257                                                this._index[id] = results;
258                                        }
259                                }
260                                for(var i in results){
261                                        results[i] = this._processResults(results[i], deferred).items;
262                                }
263                                var count = results.length;
264                        }
265                        return {totalCount: deferred.request.count == count ? (deferred.request.start || 0) + count * this.estimateCountFactor : count, items: results};
266                },
267                close: function(request){
268                        return request && request.abort && request.abort();
269                },
270                fetch: function(args){
271                        // summary:
272                        //              See dojo.data.api.Read.fetch
273                        //
274                        // The *queryOptions.cache* parameter
275                        //              If true, indicates that the query result should be cached for future use. This is only available
276                        //              if dojox.data.ClientFilter has been loaded before the ServiceStore
277                        //
278                        //      The *syncMode* parameter
279                        //              Indicates that the call should be fetch synchronously if possible (this is not always possible)
280                        //
281                        // The *clientFetch* parameter
282                        //              This is a fetch keyword argument for explicitly doing client side filtering, querying, and paging
283
284                        args = args || {};
285
286                        if("syncMode" in args ? args.syncMode : this.syncMode){
287                                dojox.rpc._sync = true;
288                        }
289                        var self = this;
290
291                        var scope = args.scope || self;
292                        var defResult = this.cachingFetch ? this.cachingFetch(args) : this._doQuery(args);
293                        defResult.request = args;
294                        defResult.addCallback(function(results){
295                                if(args.clientFetch){
296                                        results = self.clientSideFetch({query:args.clientFetch,sort:args.sort,start:args.start,count:args.count},results);
297                                }
298                                var resultSet = self._processResults(results, defResult);
299                                results = args.results = resultSet.items;
300                                if(args.onBegin){
301                                        args.onBegin.call(scope, resultSet.totalCount, args);
302                                }
303                                if(args.onItem){
304                                        for(var i=0; i<results.length;i++){
305                                                args.onItem.call(scope, results[i], args);
306                                        }
307                                }
308                                if(args.onComplete){
309                                        args.onComplete.call(scope, args.onItem ? null : results, args);
310                                }
311                                return results;
312                        });
313                        defResult.addErrback(args.onError && function(err){
314                                return args.onError.call(scope, err, args);
315                        });
316                        args.abort = function(){
317                                // abort the request
318                                defResult.cancel();
319                        };
320                        args.store = this;
321                        return args;
322                },
323                _doQuery: function(args){
324                        var query= typeof args.queryStr == 'string' ? args.queryStr : args.query;
325                        return this.service(query);
326                },
327                getFeatures: function(){
328                        // summary:
329                        //              return the store feature set
330
331                        return {
332                                "dojo.data.api.Read": true,
333                                "dojo.data.api.Identity": true,
334                                "dojo.data.api.Schema": this.schema
335                        };
336                },
337
338                getLabel: function(item){
339                        // summary
340                        //              returns the label for an item. Just gets the "label" attribute.
341                        //
342                        return this.getValue(item,this.labelAttribute);
343                },
344
345                getLabelAttributes: function(item){
346                        // summary:
347                        //              returns an array of attributes that are used to create the label of an item
348                        return [this.labelAttribute];
349                },
350
351                //Identity API Support
352
353
354                getIdentity: function(item){
355                        return item.__id;
356                },
357
358                getIdentityAttributes: function(item){
359                        // summary:
360                        //              returns the attributes which are used to make up the
361                        //              identity of an item.    Basically returns this.idAttribute
362
363                        return [this.idAttribute];
364                },
365
366                fetchItemByIdentity: function(args){
367                        // summary:
368                        //              fetch an item by its identity, by looking in our index of what we have loaded
369                        var item = this._index[(args._prefix || '') + args.identity];
370                        if(item){
371                                // the item exists in the index
372                                if(item._loadObject){
373                                        // we have a handle on the item, but it isn't loaded yet, so we need to load it
374                                        args.item = item;
375                                        return this.loadItem(args);
376                                }else if(args.onItem){
377                                        // it's already loaded, so we can immediately callback
378                                        args.onItem.call(args.scope, item);
379                                }
380                        }else{
381                                // convert the different spellings
382                                return this.fetch({
383                                                query: args.identity,
384                                                onComplete: args.onItem,
385                                                onError: args.onError,
386                                                scope: args.scope
387                                        }).results;
388                        }
389                        return item;
390                }
391
392        }
393);
394});
Note: See TracBrowser for help on using the repository browser.