1 | define(["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 | |
---|
19 | return 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 | }); |
---|