dojo.provide("dojox.data.JsonRestStore"); dojo.require("dojox.data.ServiceStore"); dojo.require("dojox.rpc.JsonRest"); dojo.declare("dojox.data.JsonRestStore", dojox.data.ServiceStore, { constructor: function(options){ //summary: // JsonRestStore is a Dojo Data store interface to JSON HTTP/REST web // storage services that support read and write through GET, PUT, POST, and DELETE. // options: // Keyword arguments // // The *schema* parameter // This is a schema object for this store. This should be JSON Schema format. // // The *service* parameter // This is the service object that is used to retrieve lazy data and save results // The function should be directly callable with a single parameter of an object id to be loaded // The function should also have the following methods: // put(id,value) - puts the value at the given id // post(id,value) - posts (appends) the value at the given id // delete(id) - deletes the value corresponding to the given id // Note that it is critical that the service parses responses as JSON. // If you are using dojox.rpc.Service, the easiest way to make sure this // happens is to make the responses have a content type of // application/json. If you are creating your own service, make sure you // use handleAs: "json" with your XHR requests. // // The *target* parameter // This is the target URL for this Service store. This may be used in place // of a service parameter to connect directly to RESTful URL without // using a dojox.rpc.Service object. // // The *idAttribute* parameter // Defaults to 'id'. The name of the attribute that holds an objects id. // This can be a preexisting id provided by the server. // If an ID isn't already provided when an object // is fetched or added to the store, the autoIdentity system // will generate an id for it and add it to the index. // // The *syncMode* parameter // Setting this to true will set the store to using synchronous calls by default. // Sync calls return their data immediately from the calling function, so // callbacks are unnecessary // // description: // The JsonRestStore will cause all saved modifications to be sent to the server using Rest commands (PUT, POST, or DELETE). // When using a Rest store on a public network, it is important to implement proper security measures to // control access to resources. // On the server side implementing a REST interface means providing GET, PUT, POST, and DELETE handlers. // GET - Retrieve an object or array/result set, this can be by id (like /table/1) or with a // query (like /table/?name=foo). // PUT - This should modify a object, the URL will correspond to the id (like /table/1), and the body will // provide the modified object // POST - This should create a new object. The URL will correspond to the target store (like /table/) // and the body should be the properties of the new object. The server's response should include a // Location header that indicates the id of the newly created object. This id will be used for subsequent // PUT and DELETE requests. JsonRestStore also includes a Content-Location header that indicates // the temporary randomly generated id used by client, and this location is used for subsequent // PUT/DELETEs if no Location header is provided by the server or if a modification is sent prior // to receiving a response from the server. // DELETE - This should delete an object by id. // These articles include more detailed information on using the JsonRestStore: // http://www.sitepen.com/blog/2008/06/13/restful-json-dojo-data/ // http://blog.medryx.org/2008/07/24/jsonreststore-overview/ // // example: // A JsonRestStore takes a REST service or a URL and uses it the remote communication for a // read/write dojo.data implementation. A JsonRestStore can be created with a simple URL like: // | new JsonRestStore({target:"/MyData/"}); // example: // To use a JsonRestStore with a service, you should create a // service with a REST transport. This can be configured with an SMD: // | { // | services: { // | jsonRestStore: { // | transport: "REST", // | envelope: "URL", // | target: "store.php", // | contentType:"application/json", // | parameters: [ // | {name: "location", type: "string", optional: true} // | ] // | } // | } // | } // The SMD can then be used to create service, and the service can be passed to a JsonRestStore. For example: // | var myServices = new dojox.rpc.Service(dojo.moduleUrl("dojox.rpc.tests.resources", "test.smd")); // | var jsonStore = new dojox.data.JsonRestStore({service:myServices.jsonRestStore}); // example: // The JsonRestStore also supports lazy loading. References can be made to objects that have not been loaded. // For example if a service returned: // | {"name":"Example","lazyLoadedObject":{"$ref":"obj2"}} // And this object has accessed using the dojo.data API: // | var obj = jsonStore.getValue(myObject,"lazyLoadedObject"); // The object would automatically be requested from the server (with an object id of "obj2"). // dojo.connect(dojox.rpc.Rest._index,"onUpdate",this,function(obj,attrName,oldValue,newValue){ var prefix = this.service.servicePath; if(!obj.__id){ console.log("no id on updated object ", obj); }else if(obj.__id.substring(0,prefix.length) == prefix){ this.onSet(obj,attrName,oldValue,newValue); } }); this.idAttribute = this.idAttribute || 'id';// no options about it, we have to have identity //setup a byId alias to the api call if(typeof options.target == 'string' && !this.service){ this.service = dojox.rpc.Rest(this.target,true); // create a default Rest service } dojox.rpc.JsonRest.registerService(this.service, options.target, this.schema); this.schema = this.service._schema = this.schema || this.service._schema || {}; // wrap the service with so it goes through JsonRest manager this.service._store = this; this.schema._idAttr = this.idAttribute; var constructor = dojox.rpc.JsonRest.getConstructor(this.service); var self = this; this._constructor = function(data){ constructor.call(this, data); self.onNew(this); } this._constructor.prototype = constructor.prototype; this._index = dojox.rpc.Rest._index; //given a url, load json data from as the store }, referenceIntegrity: true, target:"", //Write API Support newItem: function(data, parentInfo){ // summary: // adds a new item to the store at the specified point. // Takes two parameters, data, and options. // // data: /* object */ // The data to be added in as an item. data = new this._constructor(data); if(parentInfo){ // get the previous value or any empty array var values = this.getValue(parentInfo.parent,parentInfo.attribute,[]); // set the new value this.setValue(parentInfo.parent,parentInfo.attribute,values.concat([data])); } return data; }, deleteItem: function(item){ // summary: // deletes item and any references to that item from the store. // // item: // item to delete // // If the desire is to delete only one reference, unsetAttribute or // setValue is the way to go. var checked = []; var store = dojox.data._getStoreForItem(item) || this; if(this.referenceIntegrity){ // cleanup all references dojox.rpc.JsonRest._saveNotNeeded = true; var index = dojox.rpc.Rest._index; var fixReferences = function(parent){ var toSplice; // keep track of the checked ones checked.push(parent); // mark it checked so we don't run into circular loops when encountering cycles parent.__checked = 1; for(var i in parent){ var value = parent[i]; if(value == item){ if(parent != index){ // make sure we are just operating on real objects if(parent instanceof Array){ // mark it as needing to be spliced, don't do it now or it will mess up the index into the array (toSplice = toSplice || []).push(i); }else{ // property, just delete it. (dojox.data._getStoreForItem(parent) || store).unsetAttribute(parent, i); } } }else{ if((typeof value == 'object') && value){ if(!value.__checked){ // recursively search fixReferences(value); } if(typeof value.__checked == 'object' && parent != index){ // if it is a modified array, we will replace it (dojox.data._getStoreForItem(parent) || store).setValue(parent, i, value.__checked); } } } } if(toSplice){ // we need to splice the deleted item out of these arrays i = toSplice.length; parent = parent.__checked = parent.concat(); // indicates that the array is modified while(i--){ parent.splice(toSplice[i], 1); } return parent; } return null; }; // start with the index fixReferences(index); dojox.rpc.JsonRest._saveNotNeeded = false; var i = 0; while(checked[i]){ // remove the checked marker delete checked[i++].__checked; } } dojox.rpc.JsonRest.deleteObject(item); store.onDelete(item); }, changing: function(item,_deleting){ // summary: // adds an item to the list of dirty items. This item // contains a reference to the item itself as well as a // cloned and trimmed version of old item for use with // revert. dojox.rpc.JsonRest.changing(item,_deleting); }, setValue: function(item, attribute, value){ // summary: // sets 'attribute' on 'item' to 'value' var old = item[attribute]; var store = item.__id ? dojox.data._getStoreForItem(item) : this; if(dojox.json.schema && store.schema && store.schema.properties){ // if we have a schema and schema validator available we will validate the property change dojox.json.schema.mustBeValid(dojox.json.schema.checkPropertyChange(value,store.schema.properties[attribute])); } if(attribute == store.idAttribute){ throw new Error("Can not change the identity attribute for an item"); } store.changing(item); item[attribute]=value; store.onSet(item,attribute,old,value); }, setValues: function(item, attribute, values){ // summary: // sets 'attribute' on 'item' to 'value' value // must be an array. if(!dojo.isArray(values)){ throw new Error("setValues expects to be passed an Array object as its value"); } this.setValue(item,attribute,values); }, unsetAttribute: function(item, attribute){ // summary: // unsets 'attribute' on 'item' this.changing(item); var old = item[attribute]; delete item[attribute]; this.onSet(item,attribute,old,undefined); }, save: function(kwArgs){ // summary: // Saves the dirty data using REST Ajax methods. See dojo.data.api.Write for API. // // kwArgs.global: // This will cause the save to commit the dirty data for all // JsonRestStores as a single transaction. if(!(kwArgs && kwArgs.global)){ (kwArgs = kwArgs || {}).service = this.service; } var actions = dojox.rpc.JsonRest.commit(kwArgs); this.serverVersion = this._updates && this._updates.length; return actions; }, revert: function(kwArgs){ // summary // returns any modified data to its original state prior to a save(); // // kwArgs.global: // This will cause the revert to undo all the changes for all // JsonRestStores in a single operation. var dirtyObjects = dojox.rpc.JsonRest.getDirtyObjects().concat([]); while (dirtyObjects.length>0){ var d = dirtyObjects.pop(); var store = dojox.data._getStoreForItem(d.object || d.old); if(!d.object){ // was a deletion, we will add it back store.onNew(d.old); }else if(!d.old){ // was an addition, remove it store.onDelete(d.object); }else{ // find all the properties that were modified for(var i in d.object){ if(d.object[i] != d.old[i]){ store.onSet(d.object, i, d.object[i], d.old[i]); } } } } dojox.rpc.JsonRest.revert(kwArgs && kwArgs.global && this.service); }, isDirty: function(item){ // summary // returns true if the item is marked as dirty. return dojox.rpc.JsonRest.isDirty(item); }, isItem: function(item, anyStore){ // summary: // Checks to see if a passed 'item' // really belongs to this JsonRestStore. // // item: /* object */ // The value to test for being an item // anyStore: /* boolean*/ // If true, this will return true if the value is an item for any JsonRestStore, // not just this instance return item && item.__id && (anyStore || this.service == dojox.rpc.JsonRest.getServiceAndId(item.__id).service); }, _doQuery: function(args){ var query= typeof args.queryStr == 'string' ? args.queryStr : args.query; return dojox.rpc.JsonRest.query(this.service,query, args); }, _processResults: function(results, deferred){ // index the results var count = results.length; // 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 return {totalCount:deferred.fullLength || (deferred.request.count == count ? (deferred.request.start || 0) + count * 2 : count), items: results}; }, getConstructor: function(){ // summary: // Gets the constructor for objects from this store return this._constructor; }, getIdentity: function(item){ var id = item.__clientId || item.__id; if(!id){ return id; } var prefix = this.service.servicePath; // support for relative or absolute referencing with ids return id.substring(0,prefix.length) != prefix ? id : id.substring(prefix.length); // String }, fetchItemByIdentity: function(args){ var id = args.identity; var store = this; // if it is an absolute id, we want to find the right store to query if(id.toString().match(/^(\w*:)?\//)){ var serviceAndId = dojox.rpc.JsonRest.getServiceAndId(id); store = serviceAndId.service._store; args.identity = serviceAndId.id; } args._prefix = store.service.servicePath; return store.inherited(arguments); }, //Notifcation Support onSet: function(){}, onNew: function(){}, onDelete: function(){}, getFeatures: function(){ // summary: // return the store feature set var features = this.inherited(arguments); features["dojo.data.api.Write"] = true; features["dojo.data.api.Notification"] = true; return features; } } ); dojox.data._getStoreForItem = function(item){ if(item.__id){ var servicePath = item.__id.toString().match(/.*\//)[0]; var service = dojox.rpc.JsonRest.services[servicePath]; return service ? service._store : new dojox.data.JsonRestStore({target:servicePath}); } return null; }; dojox.json.ref._useRefs = true; // Use referencing when identifiable objects are referenced