/** * @class ExtMVC.model.plugin.adapter.RESTAdapter * @extends ExtMVC.model.plugin.adapter.Abstract * An adapter which hooks into a RESTful server side API for its data storage. This is the recommended * adapter to use on MVC applications. *

Usage

* Say we have a User model defined:

  ExtMVC.model.define("User", {
    fields: [{name: 'id', type: 'int'}, {name: 'name', type: 'string'}]
  });
  var user = new User({id: 1, name: 'Saul Tigh'});
* If this model uses the REST Adapter, the following methods are made available to it. Each fires the AJAX request * indicated in the comment next to it:

  user.destroy(); //DELETE /users/1
  user.save(); //PUT /users/1 with {id: 1, name: 'Saul Tigh'} as the request payload
  user.update({name: 'Bill Adama'})l //PUT /users/1/ with {id: 1, name: 'Bill Adama'} as the request payload
* In addition, the following methods are made available to the User class object:

User.destroy(10); //DELETE /users/1

User.find(10, {
  success: function(instance) {
    console.log("Asyncronously loaded User 10 from /users/10 using GET")
  },
  failure: function() {
    console.log('Called if user 10 could not be found');
  }
}); //GET /users/10

User.create({name: 'Gaius Baltar'}, {
  success: function(instance) {
    console.log('Gaius was created');
  },
  failure: function(errors) {
    console.log('Called if Gaius could not be created');
    console.log(errors);
  }
}); //POST /users

User.build({name: 'Felix Gaeta'}); //same as new User({name: 'Felix Gaeta'});
*/ ExtMVC.model.plugin.adapter.RESTAdapter = Ext.extend(ExtMVC.model.plugin.adapter.Abstract, { /** * @property createMethod * @type String * The HTTP verb to use when creating a new instance (defaults to 'POST') */ createMethod: 'POST', /** * @property readMethod * @type String * The HTTP verb to use when reading data from the server (e.g. in find requests). Defaults to 'GET' */ readMethod: 'GET', /** * @property updateMethod * @type String * The HTTP verb to use when updating an existing instance (defaults to 'PUT') */ updateMethod: 'PUT', /** * @property destroyMethod * @type String * The HTTP verb to use when destroying an instance (defaults to 'DELETE') */ destroyMethod: 'DELETE', /** * @property proxyType * @type Function * The type of Data Proxy to use (defaults to Ext.data.HttpProxy) */ proxyType: Ext.data.HttpProxy, /** * Performs the actual save request. Uses POST for new records, PUT when updating existing ones */ doSave: function(instance, options) { if (typeof instance == 'undefined') throw new Error('No instance provided to REST Adapter save'); options = options || {}; var successFn = options.success || Ext.emptyFn, failureFn = options.failure || Ext.emptyFn; delete options.success; delete options.failure; Ext.Ajax.request( Ext.apply({ url : this.instanceUrl(instance), method : instance.newRecord() ? this.createMethod : this.updateMethod, params : this.buildPostData(instance), success: function(instance, userCallback, scope) { var scope = scope || this; return function(response, options) { var jsonPath = instance.modelName.underscore(), jsonData = Ext.decode(response.responseText)[jsonPath]; for (var key in jsonData) { instance.set(key, jsonData[key]); } userCallback.call(scope, instance); }; }(instance, successFn, options.scope), failure: function(instance, userCallback, scope) { var scope = options.scope || this; return function(response, options) { failureFn.call(scope, instance); }; }(instance, failureFn, options.scope) }, options) ); }, /** * Callback for save AJAX request. By default this reads server response data and populates the instance * if the request was successful, adds errors if not * @private */ afterSave: function() { }, /** * Performs the actual find request. * @private * @param {Object} conditions An object containing find conditions. If a primaryKey is set this will be used * to build the url for that particular instance, otherwise the collection url will be used * @param {Object} options Callbacks (use callback, success and failure) */ doFind: function(conditions, options, constructor) { conditions = conditions || {}; options = options || {}; //if primary key is given, perform a single search var single = (conditions.primaryKey !== undefined), url = options.url || this.findUrl(conditions, constructor); Ext.applyIf(options, { conditions: conditions, scope : this }); var findMethod = single ? this.doSingleFind : this.doCollectionFind; return findMethod.call(this, url, options, constructor); }, /** * Performs an HTTP DELETE request using Ext.Ajax.request * @private * @param {ExtMVC.model.Base} instance The model instance to destroy * @param {Object} options Options object passed to Ext.Ajax.request * @return {Number} The Ajax transaction ID */ doDestroy: function(instance, options, constructor) { var options = options || {}; if (typeof instance == 'undefined') throw new Error('No instance provided to REST Adapter destroy'); if (!(instance instanceof Ext.data.Record)) { var id = parseInt(instance, 10); instance = new constructor(); instance.set(constructor.prototype.primaryKey, id); } //if we were passed a success function, save it here so that we can call it with the instance later var successFn = options.success || Ext.emptyFn; delete options.success; return Ext.Ajax.request( Ext.applyIf(options, { method: this.destroyMethod, url: this.instanceUrl(instance), success: function() { successFn.call(options.scope || this, instance); } }) ); }, /** * Loads a single instance of a model via an Ext.Ajax.request * @private * @param {String} url The url to load from * @param {Object} options Options passed to Ext.Ajax.request * @param {Function} constructor The constructor function used to instantiate the model instance * @return {Number} The transaction ID of the Ext.Ajax.request */ doSingleFind: function(url, options, constructor) { //store references to user callbacks as we need to overwrite them in the request var optionsCallback = options.callback, successCallback = options.success, failureCallback = options.failure; delete options.callback; delete options.success; delete options.failure; //need to make a local reference here as scope inside the Ext.data.Request block may not be 'this' var decodeFunction = this.decodeSingleLoadResponse; //helper function to cut down repetition in Ajax request callback var callIf = function(callback, args) { if (typeof callback == 'function') callback.apply(options.scope, args); }; Ext.Ajax.request( Ext.apply(options, { callback: function(opts, success, response) { if (success === true) { var instance = new constructor(decodeFunction(response.responseText, constructor)); callIf(successCallback, [instance, opts, response]); } else callIf(failureCallback, arguments); //call the generic callback passed into options callIf(optionsCallback, arguments); } }, this.buildProxyConfig(url)) ); }, /** * @property storeConfig * @type Object * Default properties assigned to the Ext.data.Store used in find requests */ storeConfig: { autoLoad : true, remoteSort: false }, /** * Specialised find for dealing with collections. Returns an Ext.data.Store * @private * @param {String} url The url to load the collection from * @param {Object} options Options passed to the Store constructor * @param {Function} constructor The constructor function used to instantiate the model instance * @return {Ext.data.Store} A Store with the appropriate configuration to load this collection */ doCollectionFind: function(url, options, constructor) { Ext.applyIf(options, this.storeConfig); if (options.conditions != undefined) { Ext.applyIf(options, { baseParams: options.conditions }); } return new Ext.data.Store( Ext.applyIf(options, { reader : constructor.prototype.getReader(), proxy : new this.proxyType(this.buildProxyConfig(url)) }) ); }, /** * Calculates the unique REST URL for a given model instance * @param {ExtMVC.model.Base} instance The model instance * @return {String} The url associated with this instance */ instanceUrl: function(instance) { if (instance.newRecord()) { return String.format("/{0}", instance.tableName); } else { return String.format("/{0}/{1}", instance.tableName, instance.get(instance.primaryKey)); } }, /** * Calculates the REST URL for a given model collection. By default this just returns / followed by the table name * @param {Function} constructor The model constructor function */ collectionUrl: function(constructor) { return String.format("/{0}", constructor.prototype.tableName); }, /** * Returns configuration data to be used by the DataProxy when loading records. Override to provide your own config * @param {String} url The url the proxy should use. This is typically calculated elsewhere so must be provided * @return {Object} Configuration for the proxy */ buildProxyConfig: function(url) { return { url: url, method: this.readMethod }; }, /** * Creates a params object suitable for sending as POST data to the server * @param {ExtMVC.model.Base} instance The models instance to build post data for * @return {Object} Params object to send to the server */ buildPostData: function(instance) { var data = {}, prefix = instance.modelName.underscore(); for (key in instance.data) { data[prefix + '[' + key + ']'] = instance.data[key]; } return data; }, /** * Decodes response text received from the server as the result of requesting data for a single record. * By default this expects the data to be in the form {"model_name": {"key": "value", "key2", "value 2"}} * and would return an object like {"key": "value", "key2", "value 2"} * @param {String} responseText The raw response text * @param {Function} constructor The constructor used to construct model instances. Useful for access to the prototype * @return {Object} Decoded data suitable for use in a model constructor */ decodeSingleLoadResponse: function(responseText, constructor) { var tname = constructor.prototype.tableName; return Ext.decode(responseText)[tname]; }, //private findUrl: function(conditions, constructor) { if (typeof(conditions) == 'object' && conditions.primaryKey) { //find by ID var instance = new constructor({}); instance.set(instance.primaryKey, conditions.primaryKey); delete conditions.primaryKey; return this.instanceUrl(instance); } else { //find by conditions return this.collectionUrl(constructor); } } });