ServiceStore.js 14 KB

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