source: Dev/branches/rest-dojo-ui/client/dojox/data/JsonRestStore.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

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