ObjectStore.js 16 KB

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