/**
* ServerProxy is a superclass of {@link Ext.data.proxy.JsonP JsonPProxy} and
* {@link Ext.data.proxy.Ajax AjaxProxy}, and would not usually be used directly.
* @protected
*/
Ext.define('Ext.data.proxy.Server', {
extend: 'Ext.data.proxy.Proxy',
alias: 'proxy.server',
alternateClassName: 'Ext.data.ServerProxy',
uses: ['Ext.data.Request'],
isRemote: true,
config: {
/**
* @cfg {String} url
* The URL from which to request the data object.
*/
url: '',
/**
* @cfg {String} [pageParam="page"]
* The name of the 'page' parameter to send in a request. Defaults to 'page'. Set this to
* `''` if you don't want to send a page parameter.
*/
pageParam: 'page',
/**
* @cfg {String} [startParam="start"]
* The name of the 'start' parameter to send in a request. Defaults to 'start'. Set this to
* `''` if you don't want to send a start parameter.
*/
startParam: 'start',
/**
* @cfg {String} [limitParam="limit"]
* The name of the 'limit' parameter to send in a request. Defaults to 'limit'. Set this to
* `''` if you don't want to send a limit parameter.
*/
limitParam: 'limit',
/**
* @cfg {String} [groupParam="group"]
* The name of the 'group' parameter to send in a request. Defaults to 'group'. Set this to
* `''` if you don't want to send a group parameter.
*/
groupParam: 'group',
/**
* @cfg {String} [summaryParam="summary"]
* The name of the 'summary' parameter to send in a request.
* Defaults to 'summary'. Set this to `''` if you don't want to send a summary parameter.
*/
summaryParam: 'summary',
/**
* @cfg {String} [groupDirectionParam="groupDir"]
* The name of the direction parameter to send in a request. **This is only used when
* simpleGroupMode is set to true.**
* If this is set to the same value as the {@link #groupParam}, then the group property
* name *and* direction of each grouper is passed as a single, space separated parameter,
* looking like a database `group by` specification.
*
* So if there are multiple groupers, the single group parameter will look like this:
*
* ?group=name%20ASC&group=age%20DESC
*/
groupDirectionParam: 'groupDir',
/**
* @cfg {String} [sortParam="sort"]
* The name of the 'sort' parameter to send in a request. Defaults to 'sort'. Set this to
* `''` if you don't want to send a sort parameter.
*/
sortParam: 'sort',
/**
* @cfg {String} [filterParam="filter"]
* The name of the 'filter' parameter to send in a request. Defaults to 'filter'. Set this
* to `''` if you don't want to send a filter parameter.
*/
filterParam: 'filter',
/**
* @cfg {String} [directionParam="dir"]
* The name of the direction parameter to send in a request. **This is only used when
* simpleSortMode is set to true.**
*
* If this is set to the same value as the {@link #sortParam}, then the sort property name
* *and* direction of each sorter is passed as a single, space separated parameter, looking
* like a database `order by` specification.
*
* So if there are multiple sorters, the single sort parameter will look like this:
*
* ?sort=name%20ASC&sort=age%20DESC
*/
directionParam: 'dir',
/**
* @cfg {String} [idParam="id"]
* The name of the parameter which carries the id of the entity being operated upon.
*/
idParam: 'id',
/**
* @cfg {Boolean} [simpleSortMode=false]
* Enabling simpleSortMode in conjunction with remoteSort will send the sorted field names
* in the parameter named by {@link #sortParam}, and the directions for each sorted field
* in a parameter named by {@link #directionParam}.
*
* In the simplest case, with one Sorter, this will result in HTTP parameters like this:
*
* ?sort=name&dir=ASC
*
* If there are multiple sorters, the parameters will be encoded like this:
*
* ?sort=name&sort=age&dir=ASC&dir=DESC
*/
simpleSortMode: false,
/**
* @cfg {Boolean} [simpleGroupMode=false]
* Enabling simpleGroupMode in conjunction with remoteGroup will only send one group
* property and a direction when a remote group is requested. The
* {@link #groupDirectionParam} and {@link #groupParam} will be sent with the property name
* and either 'ASC' or 'DESC'.
*/
simpleGroupMode: false,
/**
* @cfg {Boolean} [noCache=true]
* Disable caching by adding a unique parameter name to the request. Set to false to allow
* caching. Defaults to true.
*/
noCache: true,
/**
* @cfg {String} [cacheString="_dc"]
* The name of the cache param added to the url when using noCache. Defaults to "_dc".
*/
cacheString: "_dc",
/**
* @cfg {Number} timeout
* The number of milliseconds to wait for a response. Defaults to 30000 milliseconds
* (30 seconds).
*/
timeout: 30000,
/**
* @cfg {Object} api
* Specific urls to call on CRUD action methods "create", "read", "update" and "destroy".
* Defaults to:
*
* api: {
* create : undefined,
* read : undefined,
* update : undefined,
* destroy : undefined
* }
*
* The url is built based upon the action being executed [create|read|update|destroy] using
* the commensurate {@link #api} property, or if undefined default to the configured
* {@link Ext.data.Store}.{@link Ext.data.proxy.Server#url url}.
*
* For example:
*
* api: {
* create : '/controller/new',
* read : '/controller/load',
* update : '/controller/update',
* destroy : '/controller/destroy_action'
* }
*
* If the specific URL for a given CRUD action is undefined, the CRUD action request will
* be directed to the configured {@link Ext.data.proxy.Server#url url}.
*/
api: {
create: undefined,
read: undefined,
update: undefined,
destroy: undefined
},
/**
* @cfg {Object} extraParams
* Extra parameters that will be included on every request. Individual requests with params
* of the same name will override these params when they are in conflict.
*/
extraParams: {}
},
/**
* @event exception
* Fires when the server returns an exception. This event may also be listened
* to in the event that a request has timed out or has been aborted.
* @param {Ext.data.proxy.Proxy} this
* @param {Ext.data.Response} response The response that was received
* @param {Ext.data.operation.Operation} operation The operation that triggered the request
*/
// in a ServerProxy all four CRUD operations are executed in the same manner, so we delegate to
// doRequest in each case
create: function() {
return this.doRequest.apply(this, arguments);
},
read: function() {
return this.doRequest.apply(this, arguments);
},
update: function() {
return this.doRequest.apply(this, arguments);
},
erase: function() {
return this.doRequest.apply(this, arguments);
},
/**
* Sets a value in the underlying {@link #extraParams}.
* @param {String} name The key for the new value
* @param {Object} value The value
*/
setExtraParam: function(name, value) {
var extraParams = this.getExtraParams();
extraParams[name] = value;
this.fireEvent('extraparamschanged', extraParams);
},
updateExtraParams: function(newExtraParams, oldExtraParams) {
this.fireEvent('extraparamschanged', newExtraParams);
},
/**
* Creates an {@link Ext.data.Request Request} object from
* {@link Ext.data.operation.Operation Operation}.
*
* This gets called from doRequest methods in subclasses of Server proxy.
*
* @param {Ext.data.operation.Operation} operation The operation to execute
* @return {Ext.data.Request} The request object
*/
buildRequest: function(operation) {
var me = this,
initialParams = Ext.apply({}, operation.getParams()),
// Clone params right now so that they can be mutated at any point further down the
// call stack
params = Ext.applyIf(initialParams, me.getExtraParams() || {}),
request,
operationId,
idParam;
// copy any sorters, filters etc into the params so they can be sent over the wire
Ext.applyIf(params, me.getParams(operation));
// Set up the entity id parameter according to the configured name.
// This defaults to "id". But TreeStore has a "nodeParam" configuration which
// specifies the id parameter name of the node being loaded.
operationId = operation.getId();
idParam = me.getIdParam();
if (operationId !== undefined && params[idParam] === undefined) {
params[idParam] = operationId;
}
request = new Ext.data.Request({
params: params,
action: operation.getAction(),
records: operation.getRecords(),
url: operation.getUrl(),
operation: operation,
// this is needed by JsonSimlet in order to properly construct responses for
// requests from this proxy
proxy: me
});
request.setUrl(me.buildUrl(request));
/*
* Save the request on the Operation. Operations don't usually care about Request and
* Response data, but in the ServerProxy and any of its subclasses we add both request
* and response as they may be useful for further processing
*/
operation.setRequest(request);
return request;
},
/**
* Processes response, which may involve updating or committing records, each of which
* will inform the owning stores and their interested views. Finally, we may perform
* an additional layout if the data shape has changed.
*
* @protected
*/
processResponse: function(success, operation, request, response) {
var me = this,
exception, reader, resultSet, meta, destroyOp;
// Async callback could have landed at any time, including during and after
// destruction. We don't want to unravel the whole response chain in such case.
if (me.destroying || me.destroyed) {
return;
}
// Processing a response may involve updating or committing many records
// each of which will inform the owning stores, which will ultimately
// inform interested views which will most likely have to do a layout
// assuming that the data shape has changed.
// Bracketing the processing with this event gives owning stores the ability
// to fire their own beginupdate/endupdate events which can be used by interested
// views to suspend layouts.
me.fireEvent('beginprocessresponse', me, response, operation);
if (success === true) {
reader = me.getReader();
if (response.status === 204) {
resultSet = reader.getNullResultSet();
}
else {
resultSet = reader.read(me.extractResponseData(response), {
// If we're doing an update, we want to construct the models ourselves.
recordCreator: operation.getRecordCreator() ||
reader.defaultRecordCreatorFromServer
});
}
if (!operation.$destroyOwner) {
operation.$destroyOwner = me;
destroyOp = true;
}
operation.process(resultSet, request, response);
exception = !operation.wasSuccessful();
}
else {
me.setException(operation, response);
exception = true;
}
// It is possible that exception callback destroyed the store and owning proxy,
// in which case we can't do nothing except punt.
if (me.destroyed) {
if (!operation.destroyed && destroyOp && operation.$destroyOwner === me) {
operation.destroy();
}
return;
}
if (exception) {
me.fireEvent('exception', me, response, operation);
}
// If a JsonReader detected metadata, process it now.
// This will fire the 'metachange' event which the Store processes to fire its own
// 'metachange'
else {
meta = resultSet.getMetadata();
if (meta) {
me.onMetaChange(meta);
}
}
// Ditto
if (me.destroyed) {
if (!operation.destroyed && destroyOp && operation.$destroyOwner === me) {
operation.destroy();
}
return;
}
me.afterRequest(request, success);
// Tell owning store processing has finished.
// It will fire its endupdate event which will cause interested views to
// resume layouts.
me.fireEvent('endprocessresponse', me, response, operation);
if (!operation.destroyed && destroyOp && operation.$destroyOwner === me) {
operation.destroy();
}
},
/**
* Sets up an exception on the operation
* @private
* @param {Ext.data.operation.Operation} operation The operation
* @param {Object} response The response
*/
setException: function(operation, response) {
operation.setException({
status: response.status,
statusText: response.statusText,
response: response
});
},
/**
* @method
* Template method to allow subclasses to specify how to get the response for the reader.
* @template
* @private
* @param {Object} response The server response
* @return {Object} The response data to be used by the reader
*/
extractResponseData: Ext.identityFn,
/**
* Encode any values being sent to the server. Can be overridden in subclasses.
* @protected
* @param {Array} value An array of sorters/filters.
* @return {Object} The encoded value
*/
applyEncoding: function(value) {
return (this.getParamsAsJson && this.getParamsAsJson()) ? value : Ext.encode(value);
},
/**
* Encodes the array of {@link Ext.util.Sorter} objects into a string to be sent in the request
* url. By default, this simply JSON-encodes the sorter data
* @param {Ext.util.Sorter[]} sorters The array of {@link Ext.util.Sorter Sorter} objects
* @param {Boolean} [preventArray=false] Prevents the items from being output as an array.
* @return {String} The encoded sorters
*/
encodeSorters: function(sorters, preventArray) {
var out = [],
length = sorters.length,
i;
for (i = 0; i < length; i++) {
out[i] = sorters[i].serialize();
}
return this.applyEncoding(preventArray ? out[0] : out);
},
/**
* Encodes the array of {@link Ext.util.Filter} objects into a string to be sent in the request
* url. By default, this simply JSON-encodes the filter data
* @param {Ext.util.Filter[]} filters The array of {@link Ext.util.Filter Filter} objects
* @return {String} The encoded filters
*/
encodeFilters: function(filters) {
var out = [],
length = filters.length,
encode, i;
for (i = 0; i < length; i++) {
encode |= filters[i].serializeTo(out);
}
// If any Filters return Objects encapsulating their full state, then the parameters
// needs JSON encoding.
return encode ? this.applyEncoding(out) : out;
},
/**
* @private
* Copy any sorters, filters etc into the params so they can be sent over the wire
*/
getParams: function(operation) {
if (!operation.isReadOperation) {
return {};
}
/* eslint-disable-next-line vars-on-top */
var me = this,
params = {},
groupers = operation.getGroupers(),
grouper = operation.getGrouper(),
sorters = operation.getSorters(),
filters = operation.getFilters(),
page = operation.getPage(),
start = operation.getStart(),
limit = operation.getLimit(),
simpleSortMode = me.getSimpleSortMode(),
simpleGroupMode = me.getSimpleGroupMode(),
pageParam = me.getPageParam(),
startParam = me.getStartParam(),
limitParam = me.getLimitParam(),
groupParam = me.getGroupParam(),
groupDirectionParam = me.getGroupDirectionParam(),
sortParam = me.getSortParam(),
filterParam = me.getFilterParam(),
directionParam = me.getDirectionParam(),
summaries = operation.getSummaries(),
summaryParam = me.getSummaryParam(),
hasGrouper, hasGroupers, index;
if (pageParam && page) {
params[pageParam] = page;
}
if (startParam && (start || start === 0)) {
params[startParam] = start;
}
if (limitParam && limit) {
params[limitParam] = limit;
}
if (summaryParam && summaries && summaries.length > 0) {
params[summaryParam] = me.encodeFields(summaries);
}
hasGrouper = groupParam && grouper;
hasGroupers = groupParam && groupers && groupers.length;
// if there's a grouper then create a groupers array with one item
/* eslint-disable max-len */
if (hasGrouper || hasGroupers) {
// Grouper is a subclass of sorter, so we can just use the sorter method
if (simpleGroupMode) {
if (hasGrouper) {
params[groupParam] = grouper.getProperty();
// Allow for direction to be encoded into the same parameter
if (groupDirectionParam === groupParam) {
params[groupParam] += ' ' + grouper.getDirection();
}
else {
params[groupDirectionParam] = grouper.getDirection();
}
}
else {
for (index = 0; index < groupers.length; index++) {
// Allow for direction to be encoded into the same parameter
if (groupDirectionParam === groupParam) {
params[groupParam] = Ext.Array.push(params[groupParam] || [], groupers[index].getProperty() + ' ' + groupers[index].getDirection());
}
else {
params[groupParam] = Ext.Array.push(params[groupParam] || [], groupers[index].getProperty());
params[groupDirectionParam] = Ext.Array.push(params[groupDirectionParam] || [], groupers[index].getDirection());
}
}
}
}
else {
params[groupParam] = hasGrouper ? me.encodeSorters([grouper], true) : me.encodeSorters(groupers);
}
}
/* eslint-enable max-len */
/* eslint-disable max-len */
if (sortParam && sorters && sorters.length > 0) {
if (simpleSortMode) {
// Group will be included in sorters, so skip sorter 0 if groups
for (index = 0; index < sorters.length; index++) {
// Allow for direction to be encoded into the same parameter
if (directionParam === sortParam) {
params[sortParam] = Ext.Array.push(params[sortParam] || [], sorters[index].getProperty() + ' ' + sorters[index].getDirection());
}
else {
params[sortParam] = Ext.Array.push(params[sortParam] || [], sorters[index].getProperty());
params[directionParam] = Ext.Array.push(params[directionParam] || [], sorters[index].getDirection());
}
}
}
else {
params[sortParam] = me.encodeSorters(sorters);
}
}
/* eslint-enable max-len */
if (filterParam && filters && filters.length > 0) {
params[filterParam] = me.encodeFilters(filters);
}
return params;
},
/**
* Encodes the array of {@link Ext.data.field.Field} objects into a string to
* be sent in the request url. By default, this simply JSON-encodes the fields data
* @param {Ext.data.field.Field[]} fields The array of
* {@link Ext.data.field.Field Field} objects
* @return {String} The encoded fields
* @private
*/
encodeFields: function(fields) {
var out = [],
length = fields.length,
i, field, encodedField, summary;
for (i = 0; i < length; i++) {
field = fields[i];
encodedField = {
name: field.getName()
};
summary = field.getSummary();
if (summary && summary.isAggregator) {
encodedField.summary = summary.type;
out.push(encodedField);
}
}
return this.applyEncoding(out);
},
/**
* Generates a url based on a given Ext.data.Request object. By default, ServerProxy's buildUrl
* will add the cache-buster param to the end of the url. Subclasses may need to perform
* additional modifications to the url.
* @param {Ext.data.Request} request The request object
* @return {String} The url
*/
buildUrl: function(request) {
var me = this,
url = me.getUrl(request);
//<debug>
if (!url) {
Ext.raise("You are using a ServerProxy but have not supplied it with a url.");
}
//</debug>
if (me.getNoCache()) {
url = Ext.urlAppend(
url,
Ext.String.format("{0}={1}", me.getCacheString(), Ext.Date.now())
);
}
return url;
},
/**
* Get the url for the request taking into account the order of priority,
* - The request
* - The api
* - The url
* @private
* @param {Ext.data.Request} request The request
* @return {String} The url
*/
getUrl: function(request) {
var url;
if (request) {
url = request.getUrl() || this.getApi()[request.getAction()];
}
return url ? url : this.callParent();
},
/**
* In ServerProxy subclasses, the {@link #method-create}, {@link #method-read},
* {@link #method-update}, and {@link #method-erase} methods all pass through to doRequest.
* Each ServerProxy subclass must implement the doRequest method - see
* {@link Ext.data.proxy.JsonP} and {@link Ext.data.proxy.Ajax} for examples. This method
* carries the same signature as each of the methods that delegate to it.
*
* @param {Ext.data.operation.Operation} operation The Ext.data.operation.Operation object
* @param {Function} callback The callback function to call when the Operation has completed
* @param {Object} scope The scope in which to execute the callback
*/
doRequest: function(operation, callback, scope) {
//<debug>
Ext.raise("The doRequest function has not been implemented on your " +
"Ext.data.proxy.Server subclass. See src/data/ServerProxy.js for details");
//</debug>
},
/**
* Optional callback function which can be used to clean up after a request has been completed.
* @param {Ext.data.Request} request The Request object
* @param {Boolean} success True if the request was successful
* @protected
* @template
* @method
*/
afterRequest: Ext.emptyFn,
destroy: function() {
var me = this;
me.destroying = true;
// Don't force Reader and Writer creation if they weren't yet instantiated
me.reader = me.writer = Ext.destroy(me.reader, me.writer);
me.callParent();
// This just makes it hard to ask "was destroy() called?":
// me.destroying = false; // removed in 7.0
me.destroyed = true;
}
});