Skip to content

Instantly share code, notes, and snippets.

@etgryphon
Created September 26, 2008 19:30
Show Gist options
  • Save etgryphon/13184 to your computer and use it in GitHub Desktop.
Save etgryphon/13184 to your computer and use it in GitHub Desktop.
// ========================================================================
// SproutCore
// copyright 2006-2008 Sprout Systems, Inc.
// ========================================================================
require('core') ;
SC.URL_ENCODED_FORMAT = 'url-encoded' ;
SC.JSON_FORMAT = 'json';
// The Server object knows how to send requests to the server and how to
// get things back from the server. It automatically handles situations
// such as 304 caching and queuing requests to send to the server later if
// the computer becomes disconnected from the internet.
// The Server object is designed to work with a resource oriented application.
// That is, you do someting like this:
//
// Server.request('resource','verb',{ parameters })
// or
// Server.create('resource',{ parameters })
// Server.refresh('resource',{ parameters })
// Server.update('resource',{ parameters })
// Server.destroy('resource',{ parameters })
//
// parameters include:
// onSuccess -- passes back returned text
// onFailure --
//
//
// SC.Record.refresh
// SC.Record.commit --> create/update
// SC.Record.destroy
SC.Server = SC.Object.extend({
// Set this to the prefix for your app. Server will use this to convert
// record_type properties into recordTypes.
prefix: null,
// Set this string to the format to be used to set your resource and verb.
urlFormat: '/%@/%@',
// Set this string to either rails or json to set the post transport protocol
postFormat: SC.URL_ENCODED_FORMAT,
// Set this string to true when escaping the JSON string is necessary
escapeJSON: true,
// call this in your main to preload any data sent from the server with the
// initial page load.
preload: function(clientData) {
if ((!clientData) || (clientData.size == 0)) return ;
this.refreshRecordsWithData(clientData,SC.Record,null,false);
},
// This is the root method for accessing a server resource. Pass in the
// resource URL, verb name, and any parameters. There are several special-
// purpose parameters used also:
//
// onSuccess -- function invoked when request completes. Expects the format
// didSucceed(status,ajaxRequest,cacheCode,context)
// onFailure -- function invoked when request fails. Same format.
// requestContext -- simply passed back.
// cacheCode -- String indicating the time of the last refresh.
// url -- override the default url building with this url.
//
request: function(resource, action, ids, params, method) {
// Get Settings and Options
if (!params) params = {} ;
var opts = {} ;
var onSuccess = params.onSuccess; delete params.onSuccess;
var onNotModified = params.onNotModified; delete params.onNotModified ;
var onFailure = params.onFailure ; delete params.onFailure ;
var context = params.requestContext ; delete params.requestContext ;
var accept = params.accept ; delete params.accept ;
var cacheCode = params.cacheCode; delete params.cacheCode ;
var url = params.url; delete params.url;
opts.requestHeaders = {'Accept': 'application/json, text/javascript, application/xml, text/xml, text/html, */*'}
if (accept) opts.requestHeaders['Accept'] = accept ;
if (cacheCode) opts.requestHeaders['Sproutit-Cache'] = cacheCode ;
opts.method = method || 'get' ;
if (!url) url = this.urlFor(resource, action, ids, params, opts.method) ;
// handle ids
if (ids && ids.length > 1) {
params.ids = [ids].flatten().join(',') ;
}
// adds a custom HTTP header for remote requests
opts.requestHeaders = {'X-SproutCore-Version' : '1.0'}
// convert parameters.
var parameters = this._toQueryString(params) ;
if (parameters && parameters.length > 0) opts.parameters = parameters ;
var request = null ; //will container the ajax request
// Save callback functions.
opts.onSuccess = function(transport) {
var cacheCode = request.getHeader('Last-Modified') ;
if ((transport.status == '200') && (transport.responseText == '304 Not Modified')) {
if (onNotModified) onNotModified(transport.status, transport, cacheCode,context);
} else {
if (onSuccess) onSuccess(transport.status, transport, cacheCode,context);
}
} ;
opts.onFailure = function(transport) {
var cacheCode = request.getHeader('Last-Modified') ;
if (onFailure) onFailure(transport.status, transport, cacheCode,context);
} ;
console.log('REQUEST: %@ %@'.fmt(opts.method, url)) ;
request = new Ajax.Request(url,opts) ;
},
/**
Generates the URL that is going to be called by this server. Note that you
should only return relative URLs. You can only call resources that are on
the same domain as where this script was downloaded from.
@param {String} resource the URL where the collection of the resource can be queried
@param {String} action the action that should be performed on the resource
@param {Array} ids array of identifiers of your model instances
@param {Array} params parameters that were passed to the SC.Server#request method
@param {String} method the HTTP method that will be used
@returns {String} the URL to use in the request to the backend server
**/
urlFor: function(resource, action, ids, params, method) {
var idPart = (ids && ids.length == 1) ? ids[0] : '';
return this.urlFormat.format(resource, action) + idPart;
},
// RECORD METHODS
// These methods do the basic record changes.
// ..........................................
// LIST
// This is the method called by a collection to get an updated list of
// records.
listFor: function(opts) {
var recordType = opts.recordType ;
var resource = recordType.resourceURL() ;
if (!resource) return false ;
var order = opts.order || 'id' ;
if (!(order instanceof Array)) order = [order] ;
order = order.map(function(str){
return str.decamelize() ; //rubyify
}).join(',') ;
params = {} ;
if (opts.conditions) {
var conditions = this._decamelizeData(opts.conditions) ;
for(var key in conditions) {
params[key] = conditions[key] ;
}
}
params.requestContext = opts ;
params.onSuccess = this._listSuccess.bind(this) ;
params.onNotModified = this._listNotModified.bind(this) ;
params.onFailure = this._listFailure.bind(this) ;
if (opts.cacheCode) params.cacheCode = opts.cacheCode ;
if (opts.offset) params.offset = opts.offset;
if (opts.limit) params.limit = opts.limit ;
if (order) params.order = order ;
this.request(resource, this._listForAction, null, params, this._listMethod) ;
},
_listForAction: 'list',
_listMethod: 'get',
_listSuccess: function(status, transport, cacheCode, context) {
var json = eval('json='+transport.responseText) ;
if (!json) { console.log('invalid json!'); return; }
// first, build any records passed back
if (json.records) {
this.refreshRecordsWithData(json.records,context.recordType,cacheCode,false);
}
// next, convert the list of ids into records.
var recs = (json.ids) ? json.ids.map(function(guid) {
return SC.Store.getRecordFor(guid,context.recordType) ;
}) : [] ;
// now invoke callback
if (context.callback) context.callback(recs,json.count,cacheCode) ;
},
_listNotModified: function(status, transport, cacheCode, context) {
if (context.callback) context.callback(null,null,null) ;
},
_listFailure: function(status, transport, cacheCode, records) {
console.log('listFailed!') ;
},
// ..........................................
// CREATE
// send the records back to create them. added a special parameter to
// the hash for each record, _guid, which will be used onSuccess.
createRecords: function(records) {
if (!records || records.length == 0) return ;
records = this._recordsByResource(records) ; // sort by resource.
for(var resource in records) {
if (resource == '*') continue ;
var curRecords = records[resource] ;
// collect data for records
var server = this ; var context = {} ;
var data = curRecords.map(function(rec) {
var recData = server._decamelizeData(rec.getPropertyData()) ;
recData._guid = rec._guid ;
context[rec._guid] = rec ;
return recData ;
}) ;
// issue request
this.request(resource, this._createAction, null, {
requestContext: context,
onSuccess: this._createSuccess.bind(this),
onFailure: this._createFailure.bind(this),
records: data
}, this._createMethod) ;
}
},
_createAction: 'create',
_createMethod: 'post',
// This method is called when a create is successful. It first goes through
// and assigns the primaryKey to each record.
_createSuccess: function(status, transport, cacheCode, context) {
var json = eval('json='+transport.responseText) ;
if (!(json instanceof Array)) json = [json] ;
// first go through and assign the primaryKey to each record.
if (!context) context = {} ;
json.each(function(data) {
var guid = data._guid ;
var rec = (guid) ? context[guid] : null ;
if (rec) {
var pk = rec.get('primaryKey') ;
var dataKey = (pk == 'guid') ? 'id' : pk.decamelize().toLowerCase().replace(/\-/g,'_') ;
rec.set(pk,data[dataKey]) ;
rec.set('newRecord',false) ;
}
}) ;
// now this method will work so go do it.
this.refreshRecordsWithData(json,context._recordType,cacheCode,true) ;
},
_createFailure: function(status, transport, cacheCode, records) {
console.log('createFailed!') ;
},
// ..........................................
// REFRESH
refreshRecords: function(records) {
if (!records || records.length == 0) return ;
records = this._recordsByResource(records) ; // sort by resource.
for(var resource in records) {
if (resource == '*') continue ;
var curRecords = records[resource] ;
// collect resource ids, sort records into hash, and get cacheCode.
var cacheCode = null ; var ids = [] ; var context = {} ;
var primaryKey = curRecords[0].get('primaryKey') ; // assumes all the same
curRecords.each(function(r) {
cacheCode = cacheCode || r._cacheCode ;
var key = r.get(primaryKey);
if (key) { ids.push(key); context[key] = r; }
});
context._recordType = this._instantiateRecordType(curRecords[0].get('type'), this.prefix, null) ; // default rec type.
//context._recordType = curRecords[0].recordType ; // default rec type.
params = {
requestContext: context,
cacheCode: ((cacheCode=='') ? null : cacheCode),
onSuccess: this._refreshSuccess.bind(this),
onFailure: this._refreshFailure.bind(this)
};
if (ids.length == 1 && curRecords[0].refreshURL) params['url'] = curRecords[0].refreshURL;
// issue request
this.request(resource, this._refreshAction, ids, params, this._refreshMethod) ;
}
},
_refreshAction: 'show',
_refreshMethod: 'get',
// This method is called when a refresh is successful. It expects an array
// of hashes, which it will convert to records.
_refreshSuccess: function(status, transport, cacheCode, context) {
var json = eval('json='+transport.responseText) ;
if (!(json instanceof Array)) json = [json] ;
this.refreshRecordsWithData(json,context._recordType,cacheCode,true) ;
},
_refreshFailure: function(status, transport, cacheCode, records) {
console.log('refreshFailed!') ;
},
// ..........................................
// COMMIT
commitRecords: function(records) {
if (!records || records.length == 0) return ;
records = this._recordsByResource(records) ; // sort by resource.
for(var resource in records) {
if (resource == '*') continue ;
var curRecords = records[resource] ;
// collect data for records
var server = this ;
// start format differences
var data = null;
switch(this.get('postFormat')){
case SC.URL_ENCODED_FORMAT:
data = curRecords.map(function(rec) {
return server._decamelizeData(rec.getPropertyData()) ;
}) ;
break;
case SC.JSON_FORMAT:
// get all records and put them into an array
var objects = [];
for(rec in curRecords){
if (!curRecords.hasOwnProperty(rec)) continue ;
objects.push(curRecords[rec].get('attributes') || {});
}
// convert to JSON and escape if this.escapeJSON is true
if(this.get('escapeJSON')){
data = escape(objects.toJSONString());
} else {
data = objects.toJSONString();
}
break;
default:
break;
}
// end format differences
if (data) {
var ids = [];
if (curRecords.length == 1) {
var primaryKey = curRecords[0].get('primaryKey') ;
var key = curRecords[0].get(primaryKey);
if (key) ids.push(key);
}
params = {
requestContext: records,
onSuccess: this._commitSuccess.bind(this),
onFailure: this._commitFailure.bind(this),
records: data
};
if (ids.length == 1 && curRecords[0].updateURL) params['url'] = curRecords[0].updateURL;
// issue request
this.request(resource, this._commitAction, ids, params, this._commitMethod) ;
}
}
},
_commitAction: 'update',
_commitMethod: 'post',
// This method is called when a refresh is successful. It expects an array
// of hashes, which it will convert to records.
_commitSuccess: function(status, transport, cacheCode, context) {
var json = eval('json='+transport.responseText) ;
if (!(json instanceof Array)) json = [json] ;
this.refreshRecordsWithData(json.records,context._recordType,cacheCode,true) ;
},
_commitFailure: function(status, transport, cacheCode, records) {
console.log('commitFailed!') ;
},
// ..........................................
// DESTROY
destroyRecords: function(records) {
if (!records || records.length == 0) return ;
records = this._recordsByResource(records) ; // sort by resource.
for(var resource in records) {
var curRecords = records[resource] ;
if (resource == '*') {
curRecords.each(function(rec){
rec.set('isDeleted',true) ;
SC.Store.removeRecord(rec) ;
});
continue ;
}
// collect resource ids, sort records into hash, and get cacheCode.
var ids = [] ; var key ;
var primaryKey = curRecords[0].get('primaryKey') ;
curRecords.each(function(rec) {
if ((key = rec.get(primaryKey)) && (!rec.get('newRecord'))) {
ids.push(key) ;
}
rec.set('isDeleted',true) ;
SC.Store.removeRecord(rec) ;
}) ;
// issue request -- we may not have ids to send tho (for ex, if all
// records were newRecords.)
if (ids && ids.length > 0) {
params = {
requestContext: records,
onSuccess: this._destroySuccess.bind(this),
onFailure: this._destroyFailure.bind(this)
};
if (ids.length == 1 && curRecords[0].destroyURL) params['url'] = curRecords[0].destroyURL;
this.request(resource, this._destroyAction, ids, params, this._destroyMethod) ;
}
}
},
_destroyAction: 'destroy',
_destroyMethod: 'post',
_destroySuccess: function(status, transport, cacheCode, records) {
console.log('destroySuccess!') ;
},
_destroyFailure: function(status, transport, cacheCode, records) {
console.log('destroyFailed!') ;
},
// ..........................................
// SUPPORT
// This method is called by the various handlers once they have extracted
// their data.
refreshRecordsWithData: function(dataAry,recordType,cacheCode,loaded) {
var server = this ;
// Loop through the data Array and prepare each element
var prepedDataAry = [];
for (var idx = 0; idx < dataAry.length; idx++)
{
var currElem = server._prepareDataForRecords(dataAry[idx], server, recordType);
if (currElem !== null) prepedDataAry.push(currElem)
}
dataAry = prepedDataAry;
// first, prepare each data item in the Ary.
// dataAry = dataAry.map(function(data) {
// camelize the keys received back.
// data = server._camelizeData(data) ;
// convert the 'id' property to 'guid'
// if (data.id) { data.guid = data.id; delete data.id; }
// find the recordType
// if (data.type) {
// var recordName = data.type.capitalize() ;
// if (server.prefix) {
// for (var prefixLoc = 0; prefixLoc < server.prefix.length; prefixLoc++) {
// var prefixParts = server.prefix[prefixLoc].split('.');
// var namespace = window;
// for (var prefixPartsLoc = 0; prefixPartsLoc < prefixParts.length; prefixPartsLoc++) {
// var namespace = namespace[prefixParts[prefixPartsLoc]] ;
// }
// if (namespace != window) data.recordType = namespace[recordName] ;
// if (data.recordType) break ;
// }
// } else data.recordType = window[recordName] ;
//
// if (!data.recordType) console.log('skipping undefined recordType:'+recordName) ;
// } else data.recordType = recordType ;
//
// data.recordType = server._instantiateRecordType(data.type, server.prefix, recordType);
// if (data.recordType) return data;
// else {
// console.log("Data RecordType could not be instantiated!: "+data.type)
// return null; // could not process.
// }
// }).compact() ;
// now update.
SC.Store.updateRecords(dataAry,server,recordType,loaded) ;
},
// ................................
// PRIVATE METHODS
_prepareDataForRecords: function(data, server, defaultType) {
if (data === null) return data;
// Hand Arrays
var that = this;
if (data instanceof Array)return data.map(function(d){
return that._prepareDataForRecords(d, server, defaultType) ;
}) ;
// handle other objects
if (typeof(data) == "object") {
// camelize the keys received back.
data = server._camelizeData(data) ;
// convert the 'id' property to 'guid'
if (data.id) { data.guid = data.id; delete data.id; }
data.recordType = server._instantiateRecordType(data.type, server.prefix, defaultType);
if (data.recordType) return data;
else {
console.log("Data RecordType could not be instantiated!: "+data.type)
return null; // could not process.
}
}
},
_instantiateRecordType: function(recordType, prefix, defaultType){
if (recordType) {
var recordName = recordType.capitalize() ;
if (prefix) {
for (var prefixLoc = 0; prefixLoc < prefix.length; prefixLoc++) {
var prefixParts = prefix[prefixLoc].split('.');
var namespace = window;
for (var prefixPartsLoc = 0; prefixPartsLoc < prefixParts.length; prefixPartsLoc++) {
var namespace = namespace[prefixParts[prefixPartsLoc]] ;
}
if (namespace !== window) return namespace[recordName] ;
}
} else return window[recordName] ;
} else return defaultType;
},
// places records from array into hash, sorted by resourceURL.
_recordsByResource: function(records) {
var ret = {} ;
records.each(function(rec) {
var recs = ret[rec.resourceURL || '*'] || [] ;
recs.push(rec) ;
ret[rec.resourceURL || '*'] = recs ;
}) ;
return ret ;
},
_camelizeData: function(data) {
if (data == null) return data ;
// handle array
var that = this ;
if (data instanceof Array) return data.map(function(d){
return that._camelizeData(d) ;
}) ;
// handle other objects
if (typeof(data) == "object") {
var ret = {} ;
for(var key in data) {
var value = that._camelizeData(data[key]) ;
if (key == 'id') key = 'guid' ;
ret[key.replace(/_/g,'-').camelize()] = value ;
}
return ret ;
}
// otherwise just return value
return data ;
},
_decamelizeData: function(data) {
if (data == null) return data ;
// handle array
var that = this ;
if (data instanceof Array) return data.map(function(d){
return that._decamelizeData(d) ;
}) ;
// handle other objects
if (typeof(data) == "object") {
var ret = {} ;
for(var key in data) {
var value = that._decamelizeData(data[key]) ;
if (key == 'guid') key = 'id' ;
ret[key.decamelize()] = value ;
}
return ret ;
}
// otherwise just return value
return data ;
},
// converts a string, array, or hash into a query string. root is the
// root string applied to each element key. Used for nesting.
_toQueryString: function(params,rootKey) {
// handle nulls
if (params == null) {
return rootKey + '=';
// handle arrays
} else if (params instanceof Array) {
var ret = [] ;
for(var loc=0;loc<params.length;loc++) {
var key = (rootKey) ? (rootKey + '['+loc+']') : loc ;
ret.push(this._toQueryString(params[loc],key)) ;
}
return ret.join('&') ;
// handle objects
} else if (typeof(params) == "object") {
var ret = [];
for(var cur in params) {
var key = (rootKey) ? (rootKey + '['+cur+']') : cur ;
ret.push(this._toQueryString(params[cur],key)) ;
}
return ret.join('&') ;
// handle other values
} else return [rootKey,params].join('=') ;
}
}) ;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment