ServiceStore.js 14 KB

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