JsonRestStore.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  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.JsonRestStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
  7. dojo._hasResource["dojox.data.JsonRestStore"] = true;
  8. dojo.provide("dojox.data.JsonRestStore");
  9. dojo.require("dojox.rpc.JsonRest");
  10. dojo.require("dojox.data.ServiceStore");
  11. dojo.declare("dojox.data.JsonRestStore",
  12. dojox.data.ServiceStore,
  13. {
  14. constructor: function(options){
  15. //summary:
  16. // JsonRestStore is a Dojo Data store interface to JSON HTTP/REST web
  17. // storage services that support read and write through GET, PUT, POST, and DELETE.
  18. // options:
  19. // Keyword arguments
  20. //
  21. // The *schema* parameter
  22. // This is a schema object for this store. This should be JSON Schema format.
  23. //
  24. // The *service* parameter
  25. // This is the service object that is used to retrieve lazy data and save results
  26. // The function should be directly callable with a single parameter of an object id to be loaded
  27. // The function should also have the following methods:
  28. // put(id,value) - puts the value at the given id
  29. // post(id,value) - posts (appends) the value at the given id
  30. // delete(id) - deletes the value corresponding to the given id
  31. // Note that it is critical that the service parses responses as JSON.
  32. // If you are using dojox.rpc.Service, the easiest way to make sure this
  33. // happens is to make the responses have a content type of
  34. // application/json. If you are creating your own service, make sure you
  35. // use handleAs: "json" with your XHR requests.
  36. //
  37. // The *target* parameter
  38. // This is the target URL for this Service store. This may be used in place
  39. // of a service parameter to connect directly to RESTful URL without
  40. // using a dojox.rpc.Service object.
  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 *syncMode* parameter
  50. // Setting this to true will set the store to using synchronous calls by default.
  51. // Sync calls return their data immediately from the calling function, so
  52. // callbacks are unnecessary
  53. //
  54. // description:
  55. // The JsonRestStore will cause all saved modifications to be sent to the server using Rest commands (PUT, POST, or DELETE).
  56. // When using a Rest store on a public network, it is important to implement proper security measures to
  57. // control access to resources.
  58. // On the server side implementing a REST interface means providing GET, PUT, POST, and DELETE handlers.
  59. // GET - Retrieve an object or array/result set, this can be by id (like /table/1) or with a
  60. // query (like /table/?name=foo).
  61. // PUT - This should modify a object, the URL will correspond to the id (like /table/1), and the body will
  62. // provide the modified object
  63. // POST - This should create a new object. The URL will correspond to the target store (like /table/)
  64. // and the body should be the properties of the new object. The server's response should include a
  65. // Location header that indicates the id of the newly created object. This id will be used for subsequent
  66. // PUT and DELETE requests. JsonRestStore also includes a Content-Location header that indicates
  67. // the temporary randomly generated id used by client, and this location is used for subsequent
  68. // PUT/DELETEs if no Location header is provided by the server or if a modification is sent prior
  69. // to receiving a response from the server.
  70. // DELETE - This should delete an object by id.
  71. // These articles include more detailed information on using the JsonRestStore:
  72. // http://www.sitepen.com/blog/2008/06/13/restful-json-dojo-data/
  73. // http://blog.medryx.org/2008/07/24/jsonreststore-overview/
  74. //
  75. // example:
  76. // A JsonRestStore takes a REST service or a URL and uses it the remote communication for a
  77. // read/write dojo.data implementation. A JsonRestStore can be created with a simple URL like:
  78. // | new JsonRestStore({target:"/MyData/"});
  79. // example:
  80. // To use a JsonRestStore with a service, you should create a
  81. // service with a REST transport. This can be configured with an SMD:
  82. // | {
  83. // | services: {
  84. // | jsonRestStore: {
  85. // | transport: "REST",
  86. // | envelope: "URL",
  87. // | target: "store.php",
  88. // | contentType:"application/json",
  89. // | parameters: [
  90. // | {name: "location", type: "string", optional: true}
  91. // | ]
  92. // | }
  93. // | }
  94. // | }
  95. // The SMD can then be used to create service, and the service can be passed to a JsonRestStore. For example:
  96. // | var myServices = new dojox.rpc.Service(dojo.moduleUrl("dojox.rpc.tests.resources", "test.smd"));
  97. // | var jsonStore = new dojox.data.JsonRestStore({service:myServices.jsonRestStore});
  98. // example:
  99. // The JsonRestStore also supports lazy loading. References can be made to objects that have not been loaded.
  100. // For example if a service returned:
  101. // | {"name":"Example","lazyLoadedObject":{"$ref":"obj2"}}
  102. // And this object has accessed using the dojo.data API:
  103. // | var obj = jsonStore.getValue(myObject,"lazyLoadedObject");
  104. // The object would automatically be requested from the server (with an object id of "obj2").
  105. //
  106. dojo.connect(dojox.rpc.Rest._index,"onUpdate",this,function(obj,attrName,oldValue,newValue){
  107. var prefix = this.service.servicePath;
  108. if(!obj.__id){
  109. console.log("no id on updated object ", obj);
  110. }else if(obj.__id.substring(0,prefix.length) == prefix){
  111. this.onSet(obj,attrName,oldValue,newValue);
  112. }
  113. });
  114. this.idAttribute = this.idAttribute || 'id';// no options about it, we have to have identity
  115. if(typeof options.target == 'string'){
  116. options.target = options.target.match(/\/$/) || this.allowNoTrailingSlash ? options.target : (options.target + '/');
  117. if(!this.service){
  118. this.service = dojox.rpc.JsonRest.services[options.target] ||
  119. dojox.rpc.Rest(options.target, true);
  120. // create a default Rest service
  121. }
  122. }
  123. dojox.rpc.JsonRest.registerService(this.service, options.target, this.schema);
  124. this.schema = this.service._schema = this.schema || this.service._schema || {};
  125. // wrap the service with so it goes through JsonRest manager
  126. this.service._store = this;
  127. this.service.idAsRef = this.idAsRef;
  128. this.schema._idAttr = this.idAttribute;
  129. var constructor = dojox.rpc.JsonRest.getConstructor(this.service);
  130. var self = this;
  131. this._constructor = function(data){
  132. constructor.call(this, data);
  133. self.onNew(this);
  134. }
  135. this._constructor.prototype = constructor.prototype;
  136. this._index = dojox.rpc.Rest._index;
  137. },
  138. // summary:
  139. // Will load any schemas referenced content-type header or in Link headers
  140. loadReferencedSchema: true,
  141. // summary:
  142. // Treat objects in queries as partially loaded objects
  143. idAsRef: false,
  144. referenceIntegrity: true,
  145. target:"",
  146. // summary:
  147. // Allow no trailing slash on target paths. This is generally discouraged since
  148. // it creates prevents simple scalar values from being used a relative URLs.
  149. // Disabled by default.
  150. allowNoTrailingSlash: false,
  151. //Write API Support
  152. newItem: function(data, parentInfo){
  153. // summary:
  154. // adds a new item to the store at the specified point.
  155. // Takes two parameters, data, and options.
  156. //
  157. // data: /* object */
  158. // The data to be added in as an item.
  159. data = new this._constructor(data);
  160. if(parentInfo){
  161. // get the previous value or any empty array
  162. var values = this.getValue(parentInfo.parent,parentInfo.attribute,[]);
  163. // set the new value
  164. values = values.concat([data]);
  165. data.__parent = values;
  166. this.setValue(parentInfo.parent, parentInfo.attribute, values);
  167. }
  168. return data;
  169. },
  170. deleteItem: function(item){
  171. // summary:
  172. // deletes item and any references to that item from the store.
  173. //
  174. // item:
  175. // item to delete
  176. //
  177. // If the desire is to delete only one reference, unsetAttribute or
  178. // setValue is the way to go.
  179. var checked = [];
  180. var store = dojox.data._getStoreForItem(item) || this;
  181. if(this.referenceIntegrity){
  182. // cleanup all references
  183. dojox.rpc.JsonRest._saveNotNeeded = true;
  184. var index = dojox.rpc.Rest._index;
  185. var fixReferences = function(parent){
  186. var toSplice;
  187. // keep track of the checked ones
  188. checked.push(parent);
  189. // mark it checked so we don't run into circular loops when encountering cycles
  190. parent.__checked = 1;
  191. for(var i in parent){
  192. if(i.substring(0,2) != "__"){
  193. var value = parent[i];
  194. if(value == item){
  195. if(parent != index){ // make sure we are just operating on real objects
  196. if(parent instanceof Array){
  197. // mark it as needing to be spliced, don't do it now or it will mess up the index into the array
  198. (toSplice = toSplice || []).push(i);
  199. }else{
  200. // property, just delete it.
  201. (dojox.data._getStoreForItem(parent) || store).unsetAttribute(parent, i);
  202. }
  203. }
  204. }else{
  205. if((typeof value == 'object') && value){
  206. if(!value.__checked){
  207. // recursively search
  208. fixReferences(value);
  209. }
  210. if(typeof value.__checked == 'object' && parent != index){
  211. // if it is a modified array, we will replace it
  212. (dojox.data._getStoreForItem(parent) || store).setValue(parent, i, value.__checked);
  213. }
  214. }
  215. }
  216. }
  217. }
  218. if(toSplice){
  219. // we need to splice the deleted item out of these arrays
  220. i = toSplice.length;
  221. parent = parent.__checked = parent.concat(); // indicates that the array is modified
  222. while(i--){
  223. parent.splice(toSplice[i], 1);
  224. }
  225. return parent;
  226. }
  227. return null;
  228. };
  229. // start with the index
  230. fixReferences(index);
  231. dojox.rpc.JsonRest._saveNotNeeded = false;
  232. var i = 0;
  233. while(checked[i]){
  234. // remove the checked marker
  235. delete checked[i++].__checked;
  236. }
  237. }
  238. dojox.rpc.JsonRest.deleteObject(item);
  239. store.onDelete(item);
  240. },
  241. changing: function(item,_deleting){
  242. // summary:
  243. // adds an item to the list of dirty items. This item
  244. // contains a reference to the item itself as well as a
  245. // cloned and trimmed version of old item for use with
  246. // revert.
  247. dojox.rpc.JsonRest.changing(item,_deleting);
  248. },
  249. setValue: function(item, attribute, value){
  250. // summary:
  251. // sets 'attribute' on 'item' to 'value'
  252. var old = item[attribute];
  253. var store = item.__id ? dojox.data._getStoreForItem(item) : this;
  254. if(dojox.json.schema && store.schema && store.schema.properties){
  255. // if we have a schema and schema validator available we will validate the property change
  256. dojox.json.schema.mustBeValid(dojox.json.schema.checkPropertyChange(value,store.schema.properties[attribute]));
  257. }
  258. if(attribute == store.idAttribute){
  259. throw new Error("Can not change the identity attribute for an item");
  260. }
  261. store.changing(item);
  262. item[attribute]=value;
  263. if(value && !value.__parent){
  264. value.__parent = item;
  265. }
  266. store.onSet(item,attribute,old,value);
  267. },
  268. setValues: function(item, attribute, values){
  269. // summary:
  270. // sets 'attribute' on 'item' to 'value' value
  271. // must be an array.
  272. if(!dojo.isArray(values)){
  273. throw new Error("setValues expects to be passed an Array object as its value");
  274. }
  275. this.setValue(item,attribute,values);
  276. },
  277. unsetAttribute: function(item, attribute){
  278. // summary:
  279. // unsets 'attribute' on 'item'
  280. this.changing(item);
  281. var old = item[attribute];
  282. delete item[attribute];
  283. this.onSet(item,attribute,old,undefined);
  284. },
  285. save: function(kwArgs){
  286. // summary:
  287. // Saves the dirty data using REST Ajax methods. See dojo.data.api.Write for API.
  288. //
  289. // kwArgs.global:
  290. // This will cause the save to commit the dirty data for all
  291. // JsonRestStores as a single transaction.
  292. //
  293. // kwArgs.revertOnError
  294. // This will cause the changes to be reverted if there is an
  295. // error on the save. By default a revert is executed unless
  296. // a value of false is provide for this parameter.
  297. //
  298. // kwArgs.incrementalUpdates
  299. // For items that have been updated, if this is enabled, the server will be sent a POST request
  300. // with a JSON object containing the changed properties. By default this is
  301. // not enabled, and a PUT is used to deliver an update, and will include a full
  302. // serialization of all the properties of the item/object.
  303. // If this is true, the POST request body will consist of a JSON object with
  304. // only the changed properties. The incrementalUpdates parameter may also
  305. // be a function, in which case it will be called with the updated and previous objects
  306. // and an object update representation can be returned.
  307. //
  308. // kwArgs.alwaysPostNewItems
  309. // If this is true, new items will always be sent with a POST request. By default
  310. // this is not enabled, and the JsonRestStore will send a POST request if
  311. // the item does not include its identifier (expecting server assigned location/
  312. // identifier), and will send a PUT request if the item does include its identifier
  313. // (the PUT will be sent to the URI corresponding to the provided identifier).
  314. if(!(kwArgs && kwArgs.global)){
  315. (kwArgs = kwArgs || {}).service = this.service;
  316. }
  317. if("syncMode" in kwArgs ? kwArgs.syncMode : this.syncMode){
  318. dojox.rpc._sync = true;
  319. }
  320. var actions = dojox.rpc.JsonRest.commit(kwArgs);
  321. this.serverVersion = this._updates && this._updates.length;
  322. return actions;
  323. },
  324. revert: function(kwArgs){
  325. // summary
  326. // returns any modified data to its original state prior to a save();
  327. //
  328. // kwArgs.global:
  329. // This will cause the revert to undo all the changes for all
  330. // JsonRestStores in a single operation.
  331. dojox.rpc.JsonRest.revert(kwArgs && kwArgs.global && this.service);
  332. },
  333. isDirty: function(item){
  334. // summary
  335. // returns true if the item is marked as dirty.
  336. return dojox.rpc.JsonRest.isDirty(item);
  337. },
  338. isItem: function(item, anyStore){
  339. // summary:
  340. // Checks to see if a passed 'item'
  341. // really belongs to this JsonRestStore.
  342. //
  343. // item: /* object */
  344. // The value to test for being an item
  345. // anyStore: /* boolean*/
  346. // If true, this will return true if the value is an item for any JsonRestStore,
  347. // not just this instance
  348. return item && item.__id && (anyStore || this.service == dojox.rpc.JsonRest.getServiceAndId(item.__id).service);
  349. },
  350. _doQuery: function(args){
  351. var query= typeof args.queryStr == 'string' ? args.queryStr : args.query;
  352. var deferred = dojox.rpc.JsonRest.query(this.service,query, args);
  353. var self = this;
  354. if(this.loadReferencedSchema){
  355. deferred.addCallback(function(result){
  356. var contentType = deferred.ioArgs && deferred.ioArgs.xhr && deferred.ioArgs.xhr.getResponseHeader("Content-Type");
  357. var schemaRef = contentType && contentType.match(/definedby\s*=\s*([^;]*)/);
  358. if(contentType && !schemaRef){
  359. schemaRef = deferred.ioArgs.xhr.getResponseHeader("Link");
  360. schemaRef = schemaRef && schemaRef.match(/<([^>]*)>;\s*rel="?definedby"?/);
  361. }
  362. schemaRef = schemaRef && schemaRef[1];
  363. if(schemaRef){
  364. var serviceAndId = dojox.rpc.JsonRest.getServiceAndId((self.target + schemaRef).replace(/^(.*\/)?(\w+:\/\/)|[^\/\.]+\/\.\.\/|^.*\/(\/)/,"$2$3"));
  365. var schemaDeferred = dojox.rpc.JsonRest.byId(serviceAndId.service, serviceAndId.id);
  366. schemaDeferred.addCallbacks(function(newSchema){
  367. dojo.mixin(self.schema, newSchema);
  368. return result;
  369. }, function(error){
  370. console.error(error); // log it, but don't let it cause the main request to fail
  371. return result;
  372. });
  373. return schemaDeferred;
  374. }
  375. return undefined;//don't change anything, and deal with the stupid post-commit lint complaints
  376. });
  377. }
  378. return deferred;
  379. },
  380. _processResults: function(results, deferred){
  381. // index the results
  382. var count = results.length;
  383. // if we don't know the length, and it is partial result, we will guess that it is twice as big, that will work for most widgets
  384. return {totalCount:deferred.fullLength || (deferred.request.count == count ? (deferred.request.start || 0) + count * 2 : count), items: results};
  385. },
  386. getConstructor: function(){
  387. // summary:
  388. // Gets the constructor for objects from this store
  389. return this._constructor;
  390. },
  391. getIdentity: function(item){
  392. var id = item.__clientId || item.__id;
  393. if(!id){
  394. return id;
  395. }
  396. var prefix = this.service.servicePath.replace(/[^\/]*$/,'');
  397. // support for relative or absolute referencing with ids
  398. return id.substring(0,prefix.length) != prefix ? id : id.substring(prefix.length); // String
  399. },
  400. fetchItemByIdentity: function(args){
  401. var id = args.identity;
  402. var store = this;
  403. // if it is an absolute id, we want to find the right store to query
  404. if(id.toString().match(/^(\w*:)?\//)){
  405. var serviceAndId = dojox.rpc.JsonRest.getServiceAndId(id);
  406. store = serviceAndId.service._store;
  407. args.identity = serviceAndId.id;
  408. }
  409. args._prefix = store.service.servicePath.replace(/[^\/]*$/,'');
  410. return store.inherited(arguments);
  411. },
  412. //Notifcation Support
  413. onSet: function(){},
  414. onNew: function(){},
  415. onDelete: function(){},
  416. getFeatures: function(){
  417. // summary:
  418. // return the store feature set
  419. var features = this.inherited(arguments);
  420. features["dojo.data.api.Write"] = true;
  421. features["dojo.data.api.Notification"] = true;
  422. return features;
  423. },
  424. getParent: function(item){
  425. // summary:
  426. // Returns the parent item (or query) for the given item
  427. // item:
  428. // The item to find the parent of
  429. return item && item.__parent;
  430. }
  431. }
  432. );
  433. dojox.data.JsonRestStore.getStore = function(options, Class){
  434. // summary:
  435. // Will retrieve or create a store using the given options (the same options
  436. // that are passed to JsonRestStore constructor. Returns a JsonRestStore instance
  437. // options:
  438. // See the JsonRestStore constructor
  439. // Class:
  440. // Constructor to use (for creating stores from JsonRestStore subclasses).
  441. // This is optional and defaults to JsonRestStore.
  442. if(typeof options.target == 'string'){
  443. options.target = options.target.match(/\/$/) || options.allowNoTrailingSlash ?
  444. options.target : (options.target + '/');
  445. var store = (dojox.rpc.JsonRest.services[options.target] || {})._store;
  446. if(store){
  447. return store;
  448. }
  449. }
  450. return new (Class || dojox.data.JsonRestStore)(options);
  451. };
  452. dojox.data._getStoreForItem = function(item){
  453. if(item.__id){
  454. var serviceAndId = dojox.rpc.JsonRest.getServiceAndId(item.__id);
  455. if(serviceAndId && serviceAndId.service._store){
  456. return serviceAndId.service._store;
  457. }else{
  458. var servicePath = item.__id.toString().match(/.*\//)[0];
  459. return new dojox.data.JsonRestStore({target:servicePath});
  460. }
  461. }
  462. return null;
  463. };
  464. dojox.json.ref._useRefs = true; // Use referencing when identifiable objects are referenced
  465. }