summaryrefslogtreecommitdiff
path: root/js/dojo/dojox/data/JsonRestStore.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/dojo/dojox/data/JsonRestStore.js')
-rw-r--r--js/dojo/dojox/data/JsonRestStore.js510
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;
+});