ObjectStore.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. define("dojo/data/ObjectStore", ["../_base/lang", "../Evented", "../_base/declare", "../_base/Deferred", "../_base/array",
  2. "../_base/connect", "../regexp"
  3. ], function(lang, Evented, declare, Deferred, array, connect, regexp) {
  4. // module:
  5. // dojo/data/ObjectStore
  6. // summary:
  7. // TODOC
  8. return declare("dojo.data.ObjectStore", [Evented],{
  9. objectStore: null,
  10. constructor: function(options){
  11. // summary:
  12. // A Dojo Data implementation that wraps Dojo object stores for backwards
  13. // compatibility.
  14. // options:
  15. // The configuration information to pass into the data store.
  16. // options.objectStore:
  17. // The object store to use as the source provider for this data store
  18. lang.mixin(this, options);
  19. },
  20. labelProperty: "label",
  21. getValue: function(/*Object*/ item, /*String*/property, /*value?*/defaultValue){
  22. // summary:
  23. // Gets the value of an item's 'property'
  24. //
  25. // item:
  26. // The item to get the value from
  27. // property:
  28. // property to look up value for
  29. // defaultValue:
  30. // the default value
  31. return typeof item.get === "function" ? item.get(property) :
  32. property in item ?
  33. item[property] : defaultValue;
  34. },
  35. getValues: function(item, property){
  36. // summary:
  37. // Gets the value of an item's 'property' and returns
  38. // it. If this value is an array it is just returned,
  39. // if not, the value is added to an array and that is returned.
  40. //
  41. // item: /* object */
  42. // property: /* string */
  43. // property to look up value for
  44. var val = this.getValue(item,property);
  45. return val instanceof Array ? val : val === undefined ? [] : [val];
  46. },
  47. getAttributes: function(item){
  48. // summary:
  49. // Gets the available attributes of an item's 'property' and returns
  50. // it as an array.
  51. //
  52. // item: /* object */
  53. var res = [];
  54. for(var i in item){
  55. if(item.hasOwnProperty(i) && !(i.charAt(0) == '_' && i.charAt(1) == '_')){
  56. res.push(i);
  57. }
  58. }
  59. return res;
  60. },
  61. hasAttribute: function(item,attribute){
  62. // summary:
  63. // Checks to see if item has attribute
  64. //
  65. // item: /* object */
  66. // attribute: /* string */
  67. return attribute in item;
  68. },
  69. containsValue: function(item, attribute, value){
  70. // summary:
  71. // Checks to see if 'item' has 'value' at 'attribute'
  72. //
  73. // item: /* object */
  74. // attribute: /* string */
  75. // value: /* anything */
  76. return array.indexOf(this.getValues(item,attribute),value) > -1;
  77. },
  78. isItem: function(item){
  79. // summary:
  80. // Checks to see if the argument is an item
  81. //
  82. // item: /* object */
  83. // attribute: /* string */
  84. // we have no way of determining if it belongs, we just have object returned from
  85. // service queries
  86. return (typeof item == 'object') && item && !(item instanceof Date);
  87. },
  88. isItemLoaded: function(item){
  89. // summary:
  90. // Checks to see if the item is loaded.
  91. //
  92. // item: /* object */
  93. return item && typeof item.load !== "function";
  94. },
  95. loadItem: function(args){
  96. // summary:
  97. // Loads an item and calls the callback handler. Note, that this will call the callback
  98. // handler even if the item is loaded. Consequently, you can use loadItem to ensure
  99. // that an item is loaded is situations when the item may or may not be loaded yet.
  100. // If you access a value directly through property access, you can use this to load
  101. // a lazy value as well (doesn't need to be an item).
  102. //
  103. // example:
  104. // store.loadItem({
  105. // item: item, // this item may or may not be loaded
  106. // onItem: function(item){
  107. // // do something with the item
  108. // }
  109. // });
  110. var item;
  111. if(typeof args.item.load === "function"){
  112. Deferred.when(args.item.load(), function(result){
  113. item = result; // in synchronous mode this can allow loadItem to return the value
  114. var func = result instanceof Error ? args.onError : args.onItem;
  115. if(func){
  116. func.call(args.scope, result);
  117. }
  118. });
  119. }else if(args.onItem){
  120. // even if it is already loaded, we will use call the callback, this makes it easier to
  121. // use when it is not known if the item is loaded (you can always safely call loadItem).
  122. args.onItem.call(args.scope, args.item);
  123. }
  124. return item;
  125. },
  126. close: function(request){
  127. return request && request.abort && request.abort();
  128. },
  129. fetch: function(args){
  130. // summary:
  131. // See dojo.data.api.Read.fetch
  132. //
  133. args = lang.delegate(args, args && args.queryOptions);
  134. var self = this;
  135. var scope = args.scope || self;
  136. var query = args.query;
  137. if(typeof query == "object"){ // can be null, but that is ignore by for-in
  138. query = lang.delegate(query); // don't modify the original
  139. for(var i in query){
  140. // find any strings and convert them to regular expressions for wildcard support
  141. var required = query[i];
  142. if(typeof required == "string"){
  143. query[i] = RegExp("^" + regexp.escapeString(required, "*?").replace(/\*/g, '.*').replace(/\?/g, '.') + "$", args.ignoreCase ? "mi" : "m");
  144. query[i].toString = (function(original){
  145. return function(){
  146. return original;
  147. }
  148. })(required);
  149. }
  150. }
  151. }
  152. var results = this.objectStore.query(query, args);
  153. Deferred.when(results.total, function(totalCount){
  154. Deferred.when(results, function(results){
  155. if(args.onBegin){
  156. args.onBegin.call(scope, totalCount || results.length, args);
  157. }
  158. if(args.onItem){
  159. for(var i=0; i<results.length;i++){
  160. args.onItem.call(scope, results[i], args);
  161. }
  162. }
  163. if(args.onComplete){
  164. args.onComplete.call(scope, args.onItem ? null : results, args);
  165. }
  166. return results;
  167. }, errorHandler);
  168. }, errorHandler);
  169. function errorHandler(error){
  170. if(args.onError){
  171. args.onError.call(scope, error, args);
  172. }
  173. }
  174. args.abort = function(){
  175. // abort the request
  176. if(results.cancel){
  177. results.cancel();
  178. }
  179. };
  180. if(results.observe){
  181. if(this.observing){
  182. // if we were previously observing, cancel the last time to avoid multiple notifications. Just the best we can do for the impedance mismatch between APIs
  183. this.observing.cancel();
  184. }
  185. this.observing = results.observe(function(object, removedFrom, insertedInto){
  186. if(array.indexOf(self._dirtyObjects, object) == -1){
  187. if(removedFrom == -1){
  188. self.onNew(object);
  189. }
  190. else if(insertedInto == -1){
  191. self.onDelete(object);
  192. }
  193. else{
  194. for(var i in object){
  195. if(i != self.objectStore.idProperty){
  196. self.onSet(object, i, null, object[i]);
  197. }
  198. }
  199. }
  200. }
  201. }, true);
  202. }
  203. this.onFetch(results);
  204. args.store = this;
  205. return args;
  206. },
  207. getFeatures: function(){
  208. // summary:
  209. // return the store feature set
  210. return {
  211. "dojo.data.api.Read": !!this.objectStore.get,
  212. "dojo.data.api.Identity": true,
  213. "dojo.data.api.Write": !!this.objectStore.put,
  214. "dojo.data.api.Notification": true
  215. };
  216. },
  217. getLabel: function(/* item */ item){
  218. // summary:
  219. // See dojo.data.api.Read.getLabel()
  220. if(this.isItem(item)){
  221. return this.getValue(item,this.labelProperty); //String
  222. }
  223. return undefined; //undefined
  224. },
  225. getLabelAttributes: function(/* item */ item){
  226. // summary:
  227. // See dojo.data.api.Read.getLabelAttributes()
  228. return [this.labelProperty]; //array
  229. },
  230. //Identity API Support
  231. getIdentity: function(item){
  232. return this.objectStore.getIdentity ? this.objectStore.getIdentity(item) : item[this.objectStore.idProperty || "id"];
  233. },
  234. getIdentityAttributes: function(item){
  235. // summary:
  236. // returns the attributes which are used to make up the
  237. // identity of an item. Basically returns this.objectStore.idProperty
  238. return [this.objectStore.idProperty];
  239. },
  240. fetchItemByIdentity: function(args){
  241. // summary:
  242. // fetch an item by its identity, by looking in our index of what we have loaded
  243. var item;
  244. Deferred.when(this.objectStore.get(args.identity),
  245. function(result){
  246. item = result;
  247. args.onItem.call(args.scope, result);
  248. },
  249. function(error){
  250. args.onError.call(args.scope, error);
  251. }
  252. );
  253. return item;
  254. },
  255. newItem: function(data, parentInfo){
  256. // summary:
  257. // adds a new item to the store at the specified point.
  258. // Takes two parameters, data, and options.
  259. //
  260. // data: Object
  261. // The data to be added in as an item.
  262. // TODOC: parentInfo
  263. if(parentInfo){
  264. // get the previous value or any empty array
  265. var values = this.getValue(parentInfo.parent,parentInfo.attribute,[]);
  266. // set the new value
  267. values = values.concat([data]);
  268. data.__parent = values;
  269. this.setValue(parentInfo.parent, parentInfo.attribute, values);
  270. }
  271. this._dirtyObjects.push({object:data, save: true});
  272. this.onNew(data);
  273. return data;
  274. },
  275. deleteItem: function(item){
  276. // summary:
  277. // deletes item and any references to that item from the store.
  278. //
  279. // item:
  280. // item to delete
  281. //
  282. // If the desire is to delete only one reference, unsetAttribute or
  283. // setValue is the way to go.
  284. this.changing(item, true);
  285. this.onDelete(item);
  286. },
  287. setValue: function(item, attribute, value){
  288. // summary:
  289. // sets 'attribute' on 'item' to 'value'
  290. var old = item[attribute];
  291. this.changing(item);
  292. item[attribute]=value;
  293. this.onSet(item,attribute,old,value);
  294. },
  295. setValues: function(item, attribute, values){
  296. // summary:
  297. // sets 'attribute' on 'item' to 'value' value
  298. // must be an array.
  299. if(!lang.isArray(values)){
  300. throw new Error("setValues expects to be passed an Array object as its value");
  301. }
  302. this.setValue(item,attribute,values);
  303. },
  304. unsetAttribute: function(item, attribute){
  305. // summary:
  306. // unsets 'attribute' on 'item'
  307. this.changing(item);
  308. var old = item[attribute];
  309. delete item[attribute];
  310. this.onSet(item,attribute,old,undefined);
  311. },
  312. _dirtyObjects: [],
  313. changing: function(object,_deleting){
  314. // summary:
  315. // adds an object to the list of dirty objects. This object
  316. // contains a reference to the object itself as well as a
  317. // cloned and trimmed version of old object for use with
  318. // revert.
  319. object.__isDirty = true;
  320. //if an object is already in the list of dirty objects, don't add it again
  321. //or it will overwrite the premodification data set.
  322. for(var i=0; i<this._dirtyObjects.length; i++){
  323. var dirty = this._dirtyObjects[i];
  324. if(object==dirty.object){
  325. if(_deleting){
  326. // we are deleting, no object is an indicator of deletiong
  327. dirty.object = false;
  328. if(!this._saveNotNeeded){
  329. dirty.save = true;
  330. }
  331. }
  332. return;
  333. }
  334. }
  335. var old = object instanceof Array ? [] : {};
  336. for(i in object){
  337. if(object.hasOwnProperty(i)){
  338. old[i] = object[i];
  339. }
  340. }
  341. this._dirtyObjects.push({object: !_deleting && object, old: old, save: !this._saveNotNeeded});
  342. },
  343. save: function(kwArgs){
  344. // summary:
  345. // Saves the dirty data using object store provider. See dojo.data.api.Write for API.
  346. //
  347. // kwArgs.global:
  348. // This will cause the save to commit the dirty data for all
  349. // ObjectStores as a single transaction.
  350. //
  351. // kwArgs.revertOnError
  352. // This will cause the changes to be reverted if there is an
  353. // error on the save. By default a revert is executed unless
  354. // a value of false is provide for this parameter.
  355. // TODOC: kwArgs pseudo
  356. kwArgs = kwArgs || {};
  357. var result, actions = [];
  358. var savingObjects = [];
  359. var self = this;
  360. var dirtyObjects = this._dirtyObjects;
  361. var left = dirtyObjects.length;// this is how many changes are remaining to be received from the server
  362. try{
  363. connect.connect(kwArgs,"onError",function(){
  364. if(kwArgs.revertOnError !== false){
  365. var postCommitDirtyObjects = dirtyObjects;
  366. dirtyObjects = savingObjects;
  367. self.revert(); // revert if there was an error
  368. self._dirtyObjects = postCommitDirtyObjects;
  369. }
  370. else{
  371. self._dirtyObjects = dirtyObjects.concat(savingObjects);
  372. }
  373. });
  374. if(this.objectStore.transaction){
  375. var transaction = this.objectStore.transaction();
  376. }
  377. for(var i = 0; i < dirtyObjects.length; i++){
  378. var dirty = dirtyObjects[i];
  379. var object = dirty.object;
  380. var old = dirty.old;
  381. delete object.__isDirty;
  382. if(object){
  383. result = this.objectStore.put(object, {overwrite: !!old});
  384. }
  385. else if(typeof old != "undefined"){
  386. result = this.objectStore.remove(this.getIdentity(old));
  387. }
  388. savingObjects.push(dirty);
  389. dirtyObjects.splice(i--,1);
  390. Deferred.when(result, function(value){
  391. if(!(--left)){
  392. if(kwArgs.onComplete){
  393. kwArgs.onComplete.call(kwArgs.scope, actions);
  394. }
  395. }
  396. },function(value){
  397. // on an error we want to revert, first we want to separate any changes that were made since the commit
  398. left = -1; // first make sure that success isn't called
  399. kwArgs.onError.call(kwArgs.scope, value);
  400. });
  401. }
  402. if(transaction){
  403. transaction.commit();
  404. }
  405. }catch(e){
  406. kwArgs.onError.call(kwArgs.scope, value);
  407. }
  408. },
  409. revert: function(kwArgs){
  410. // summary:
  411. // returns any modified data to its original state prior to a save();
  412. //
  413. var dirtyObjects = this._dirtyObjects;
  414. for(var i = dirtyObjects.length; i > 0;){
  415. i--;
  416. var dirty = dirtyObjects[i];
  417. var object = dirty.object;
  418. var old = dirty.old;
  419. if(object && old){
  420. // changed
  421. for(var j in old){
  422. if(old.hasOwnProperty(j) && object[j] !== old[j]){
  423. this.onSet(object, j, object[j], old[j]);
  424. object[j] = old[j];
  425. }
  426. }
  427. for(j in object){
  428. if(!old.hasOwnProperty(j)){
  429. this.onSet(object, j, object[j]);
  430. delete object[j];
  431. }
  432. }
  433. }else if(!old){
  434. // was an addition, remove it
  435. this.onDelete(object);
  436. }else{
  437. // was a deletion, we will add it back
  438. this.onNew(old);
  439. }
  440. delete (object || old).__isDirty;
  441. dirtyObjects.splice(i, 1);
  442. }
  443. },
  444. isDirty: function(item){
  445. // summary:
  446. // returns true if the item is marked as dirty or true if there are any dirty items
  447. if(!item){
  448. return !!this._dirtyObjects.length;
  449. }
  450. return item.__isDirty;
  451. },
  452. //Notifcation Support
  453. onSet: function(){},
  454. onNew: function(){},
  455. onDelete: function(){},
  456. // an extra to get result sets
  457. onFetch: function(results){}
  458. }
  459. );
  460. });