source: Dev/trunk/src/client/dojox/data/JsonRestStore.js @ 532

Last change on this file since 532 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

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