JsonRestStore.js 19 KB

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