394 lines
15 KiB
JavaScript
394 lines
15 KiB
JavaScript
|
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
|