diff options
Diffstat (limited to 'js/dojo/dojox/data/JsonRestStore.js')
| -rw-r--r-- | js/dojo/dojox/data/JsonRestStore.js | 510 |
1 files changed, 510 insertions, 0 deletions
diff --git a/js/dojo/dojox/data/JsonRestStore.js b/js/dojo/dojox/data/JsonRestStore.js new file mode 100644 index 0000000..27e16f6 --- /dev/null +++ b/js/dojo/dojox/data/JsonRestStore.js @@ -0,0 +1,510 @@ +//>>built +define("dojox/data/JsonRestStore", ["dojo/_base/lang", "dojo/_base/declare", "dojo/_base/connect", "dojox/rpc/Rest", + "dojox/rpc/JsonRest", "dojox/json/schema", "dojox/data/ServiceStore"], + function(lang, declare, connect, rpcRest, rpcJsonRest, jsonSchema, ServiceStore) { + +/*===== +var ServiceStore = dojox.data.ServiceStore; +=====*/ + +var JsonRestStore = declare("dojox.data.JsonRestStore", 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"). + // + + connect.connect(rpcRest._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 + + if(typeof options.target == 'string'){ + options.target = options.target.match(/\/$/) || this.allowNoTrailingSlash ? options.target : (options.target + '/'); + if(!this.service){ + this.service = rpcJsonRest.services[options.target] || + rpcRest(options.target, true); + // create a default Rest service + } + } + + rpcJsonRest.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.service.idAsRef = this.idAsRef; + this.schema._idAttr = this.idAttribute; + var constructor = rpcJsonRest.getConstructor(this.service); + var self = this; + this._constructor = function(data){ + constructor.call(this, data); + self.onNew(this); + } + this._constructor.prototype = constructor.prototype; + this._index = rpcRest._index; + }, + + // summary: + // Will load any schemas referenced content-type header or in Link headers + loadReferencedSchema: true, + // summary: + // Treat objects in queries as partially loaded objects + idAsRef: false, + referenceIntegrity: true, + target:"", + // summary: + // Allow no trailing slash on target paths. This is generally discouraged since + // it creates prevents simple scalar values from being used a relative URLs. + // Disabled by default. + allowNoTrailingSlash: false, + //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 + values = values.concat([data]); + data.__parent = values; + this.setValue(parentInfo.parent, parentInfo.attribute, values); + } + 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 = dataExtCfg._getStoreForItem(item) || this; + if(this.referenceIntegrity){ + // cleanup all references + rpcJsonRest._saveNotNeeded = true; + var index = rpcRest._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){ + if(i.substring(0,2) != "__"){ + 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. + (dataExtCfg._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 + (dataExtCfg._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); + rpcJsonRest._saveNotNeeded = false; + var i = 0; + while(checked[i]){ + // remove the checked marker + delete checked[i++].__checked; + } + } + rpcJsonRest.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. + rpcJsonRest.changing(item,_deleting); + }, + cancelChanging : function(object){ + // summary: + // Removes an object from the list of dirty objects + // This will prevent that object from being saved to the server on the next save + // object: + // The item to cancel changes on + if(!object.__id){ + return; + } + dirtyObjects = dirty=rpcJsonRest.getDirtyObjects(); + for(var i=0; i<dirtyObjects.length; i++){ + var dirty = dirtyObjects[i]; + if(object==dirty.object){ + dirtyObjects.splice(i, 1); + return; + } + } + + }, + + setValue: function(item, attribute, value){ + // summary: + // sets 'attribute' on 'item' to 'value' + + var old = item[attribute]; + var store = item.__id ? dataExtCfg._getStoreForItem(item) : this; + if(jsonSchema && store.schema && store.schema.properties){ + // if we have a schema and schema validator available we will validate the property change + jsonSchema.mustBeValid(jsonSchema.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; + if(value && !value.__parent){ + value.__parent = item; + } + store.onSet(item,attribute,old,value); + }, + setValues: function(item, attribute, values){ + // summary: + // sets 'attribute' on 'item' to 'value' value + // must be an array. + + + if(!lang.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. + // + // kwArgs.revertOnError + // This will cause the changes to be reverted if there is an + // error on the save. By default a revert is executed unless + // a value of false is provide for this parameter. + // + // kwArgs.incrementalUpdates + // For items that have been updated, if this is enabled, the server will be sent a POST request + // with a JSON object containing the changed properties. By default this is + // not enabled, and a PUT is used to deliver an update, and will include a full + // serialization of all the properties of the item/object. + // If this is true, the POST request body will consist of a JSON object with + // only the changed properties. The incrementalUpdates parameter may also + // be a function, in which case it will be called with the updated and previous objects + // and an object update representation can be returned. + // + // kwArgs.alwaysPostNewItems + // If this is true, new items will always be sent with a POST request. By default + // this is not enabled, and the JsonRestStore will send a POST request if + // the item does not include its identifier (expecting server assigned location/ + // identifier), and will send a PUT request if the item does include its identifier + // (the PUT will be sent to the URI corresponding to the provided identifier). + + if(!(kwArgs && kwArgs.global)){ + (kwArgs = kwArgs || {}).service = this.service; + } + if("syncMode" in kwArgs ? kwArgs.syncMode : this.syncMode){ + rpcConfig._sync = true; + } + + var actions = rpcJsonRest.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. + rpcJsonRest.revert(kwArgs && kwArgs.global && this.service); + }, + + isDirty: function(item){ + // summary + // returns true if the item is marked as dirty. + return rpcJsonRest.isDirty(item, this); + }, + 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 == rpcJsonRest.getServiceAndId(item.__id).service); + }, + _doQuery: function(args){ + var query= typeof args.queryStr == 'string' ? args.queryStr : args.query; + var deferred = rpcJsonRest.query(this.service,query, args); + var self = this; + if(this.loadReferencedSchema){ + deferred.addCallback(function(result){ + var contentType = deferred.ioArgs && deferred.ioArgs.xhr && deferred.ioArgs.xhr.getResponseHeader("Content-Type"); + var schemaRef = contentType && contentType.match(/definedby\s*=\s*([^;]*)/); + if(contentType && !schemaRef){ + schemaRef = deferred.ioArgs.xhr.getResponseHeader("Link"); + schemaRef = schemaRef && schemaRef.match(/<([^>]*)>;\s*rel="?definedby"?/); + } + schemaRef = schemaRef && schemaRef[1]; + if(schemaRef){ + var serviceAndId = rpcJsonRest.getServiceAndId((self.target + schemaRef).replace(/^(.*\/)?(\w+:\/\/)|[^\/\.]+\/\.\.\/|^.*\/(\/)/,"$2$3")); + var schemaDeferred = rpcJsonRest.byId(serviceAndId.service, serviceAndId.id); + schemaDeferred.addCallbacks(function(newSchema){ + lang.mixin(self.schema, newSchema); + return result; + }, function(error){ + console.error(error); // log it, but don't let it cause the main request to fail + return result; + }); + return schemaDeferred; + } + return undefined;//don't change anything, and deal with the stupid post-commit lint complaints + }); + } + return deferred; + }, + _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.replace(/[^\/]*$/,''); + // 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 = rpcJsonRest.getServiceAndId(id); + store = serviceAndId.service._store; + args.identity = serviceAndId.id; + } + args._prefix = store.service.servicePath.replace(/[^\/]*$/,''); + 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; + }, + + getParent: function(item){ + // summary: + // Returns the parent item (or query) for the given item + // item: + // The item to find the parent of + + return item && item.__parent; + } + + + } +); +JsonRestStore.getStore = function(options, Class){ + // summary: + // Will retrieve or create a store using the given options (the same options + // that are passed to JsonRestStore constructor. Returns a JsonRestStore instance + // options: + // See the JsonRestStore constructor + // Class: + // Constructor to use (for creating stores from JsonRestStore subclasses). + // This is optional and defaults to JsonRestStore. + if(typeof options.target == 'string'){ + options.target = options.target.match(/\/$/) || options.allowNoTrailingSlash ? + options.target : (options.target + '/'); + var store = (rpcJsonRest.services[options.target] || {})._store; + if(store){ + return store; + } + } + return new (Class || JsonRestStore)(options); +}; + +var dataExtCfg = lang.getObject("dojox.data",true); +dataExtCfg._getStoreForItem = function(item){ + if(item.__id){ + var serviceAndId = rpcJsonRest.getServiceAndId(item.__id); + if(serviceAndId && serviceAndId.service._store){ + return serviceAndId.service._store; + }else{ + var servicePath = item.__id.toString().match(/.*\//)[0]; + return new JsonRestStore({target:servicePath}); + } + } + return null; +}; +var jsonRefConfig = lang.getObject("dojox.json.ref", true); +jsonRefConfig._useRefs = true; // Use referencing when identifiable objects are referenced + +return JsonRestStore; +}); |
