alloy-sync-restapi
Version:
RestAPI Sync Adapter for Titanium Alloy
460 lines (387 loc) • 11.9 kB
JavaScript
/**
* Rest API Adapter for Titanium Alloy
* @author Mads Møller
* @version 1.1.12
* Copyright Napp ApS
* www.napp.dk
*/
function S4() {
return ((1 + Math.random()) * 65536 | 0).toString(16).substring(1);
}
function guid() {
return S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4();
}
function InitAdapter(config) {
return {};
}
function apiCall(_options, _callback) {
if (Ti.Network.online) {
var xhr = Ti.Network.createHTTPClient({
timeout : _options.timeout || 7000
});
xhr.onload = function() {
var responseJSON, success = (this.status <= 304) ? "ok" : "error", status = true, error;
// save the eTag for future reference
if (_options.eTagEnabled && success) {
setETag(_options.url, xhr.getResponseHeader('ETag'));
}
// we dont want to parse the JSON on a empty response
if (this.status != 304 && this.status != 204) {
// parse JSON
try {
if (this.responseText) responseJSON = JSON.parse(this.responseText);
} catch (e) {
Ti.API.error('[REST API] apiCall PARSE ERROR: ' + e.message);
Ti.API.error('[REST API] apiCall PARSE ERROR: ' + this.responseText);
status = false;
error = e.message;
}
}
_callback({
success : status,
status : success,
code : this.status,
data : error,
responseText : this.responseText || null,
responseJSON : responseJSON || null
});
cleanup();
};
//Handle error
xhr.onerror = function(e) {
var responseJSON, error;
try {
responseJSON = JSON.parse(this.responseText);
} catch (e) {
error = e.message;
}
_callback({
success : false,
status : "error",
code : this.status,
error : e.error,
data : error,
responseText : this.responseText,
responseJSON : responseJSON || null
});
Ti.API.error('[REST API] apiCall ERROR: ' + this.responseText);
Ti.API.error('[REST API] apiCall ERROR CODE: ' + this.status);
Ti.API.error('[REST API] apiCall ERROR MSG: ' + e.error);
Ti.API.error('[REST API] apiCall ERROR URL: ' + _options.url);
cleanup();
};
if (_options.beforeOpen) {
_options.beforeOpen(xhr);
}
//Prepare the request
xhr.open(_options.type, _options.url);
// headers
for (var header in _options.headers) {
// use value or function to return value
xhr.setRequestHeader(header, _.isFunction(_options.headers[header]) ? _options.headers[header]() : _options.headers[header]);
}
if (_options.beforeSend) {
_options.beforeSend(xhr);
}
if (_options.eTagEnabled) {
var etag = getETag(_options.url);
etag && xhr.setRequestHeader('IF-NONE-MATCH', etag);
}
if (_options.type != 'GET' && !_.isEmpty(_options.data)) {
xhr.send(_options.data);
} else {
xhr.send();
}
} else {
// we are offline
_callback({
success : false,
status : "offline",
offline : true,
responseText : null
});
}
/**
* Clean up the request
*/
function cleanup() {
xhr = null;
_options = null;
_callback = null;
error = null;
responseJSON = null;
}
}
function Sync(method, model, opts) {
model.idAttribute = model.config.adapter.idAttribute || "id";
// Debug mode
var DEBUG = model.config.debug;
// eTag enabled
var eTagEnabled = model.config.eTagEnabled;
// Used for custom parsing of the response data
var parentNode = model.config.parentNode;
// REST - CRUD
var methodMap = {
'create' : 'POST',
'read' : 'GET',
'update' : 'PUT',
'delete' : 'DELETE'
};
var type = methodMap[method];
var params = _.extend({}, opts);
params.type = opts.requestMethod || type;
//set default headers
params.headers = params.headers || {};
// Send our own custom headers
if (model.config.hasOwnProperty("headers")) {
var configHeaders = model.config.headers;
if (_.isFunction(configHeaders)) {
configHeaders = configHeaders();
}
for (var header in configHeaders) {
params.headers[header] = configHeaders[header];
}
}
// We need to ensure that we have a base url.
if (!params.url) {
params.url = (model.config.URL || model.url());
if (!params.url) {
Ti.API.error("[REST API] ERROR: NO BASE URL");
return;
}
}
// Extend the provided url params with those from the model config
if (_.isObject(params.urlparams) || model.config.URLPARAMS) {
params.urlparams = params.urlparams || {};
_.extend(params.urlparams, _.isFunction(model.config.URLPARAMS) ? model.config.URLPARAMS() : model.config.URLPARAMS);
}
// For older servers, emulate JSON by encoding the request into an HTML-form.
if (Alloy.Backbone.emulateJSON) {
params.contentType = 'application/x-www-form-urlencoded';
params.processData = true;
params.data = params.data ? {
model : params.data
} : {};
}
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
// And an `X-HTTP-Method-Override` header.
if (Alloy.Backbone.emulateHTTP) {
if (type === 'PUT' || type === 'DELETE') {
if (Alloy.Backbone.emulateJSON)
params.data._method = type;
params.type = 'POST';
params.beforeSend = function(xhr) {
params.headers['X-HTTP-Method-Override'] = type;
};
}
}
//json data transfers
if (!params.data && model && (method == 'create' || method == 'update')) {
params.headers['Content-Type'] = 'application/json';
}
logger(DEBUG, "REST METHOD", method);
switch(method) {
case 'create' :
// convert to string for API call
params.data = JSON.stringify(model.toJSON());
logger(DEBUG, "create options", params);
apiCall(params, function(_response) {
if (_response.success) {
var data = parseJSON(DEBUG, _response, parentNode, model);
//Rest API should return a new model id.
if (data[model.idAttribute] === undefined) {
//if not - create one
data[model.idAttribute] = guid();
}
params.success(data, JSON.stringify(data));
model.trigger("fetch");
// fire event
} else {
Alloy.Backbone.VERSION === '0.9.2' ? params.error(_response.responseJSON, _response.responseText) : params.error(_response.responseText);
Ti.API.error('[REST API] CREATE ERROR: ');
Ti.API.error(_response);
}
});
break;
case 'read':
if (model.id) {
params.url = params.url + '/' + model.id;
}
if (params.search) {
// search mode
params.url = params.url + "/search/" + Ti.Network.encodeURIComponent(params.search);
}
if (params.urlparams) {
// build url with parameters
params.url = encodeData(params.urlparams, params.url);
}
if ( ! params.urlparams && params.type !== "POST" && params.data) {
// If we have set optional parameters on the request we should use it
// when params.urlparams fails/is empty.
params.url = encodeData(params.data, params.url);
}
if (eTagEnabled) {
params.eTagEnabled = true;
}
logger(DEBUG, "read options", params);
apiCall(params, function(_response) {
if (_response.success) {
var data = parseJSON(DEBUG, _response, parentNode, model);
var values = [];
if (!_.isArray(data)) {
data = [data];
}
var length = 0;
for (var i in data) {
var item = {};
item = data[i];
if (item && item[model.idAttribute] === undefined) {
item[model.idAttribute] = guid();
}
values.push(item);
length++;
}
params.success((length === 1) ? values[0] : values, _response.responseText);
model.trigger("fetch");
} else {
Alloy.Backbone.VERSION === '0.9.2' ? params.error(model, _response.responseText) : params.error(_response.responseText);
Ti.API.error('[REST API] READ ERROR: ');
Ti.API.error(_response);
}
});
break;
case 'update' :
if (!model.id) {
params.error(null, "MISSING MODEL ID");
Ti.API.error("[REST API] ERROR: MISSING MODEL ID");
return;
}
// setup the url & data
if (_.indexOf(params.url, "?") == -1) {
params.url = params.url + '/' + model.id;
} else {
var str = params.url.split("?");
params.url = str[0] + '/' + model.id + "?" + str[1];
}
if (params.urlparams) {
params.url = encodeData(params.urlparams, params.url);
}
params.data = JSON.stringify(model.toJSON());
logger(DEBUG, "update options", params);
apiCall(params, function(_response) {
if (_response.success) {
var data = parseJSON(DEBUG, _response, parentNode, model);
params.success(data, JSON.stringify(data));
model.trigger("fetch");
} else {
Alloy.Backbone.VERSION === '0.9.2' ? params.error(model, _response.responseText) : params.error(_response.responseText);
Ti.API.error('[REST API] UPDATE ERROR: ');
Ti.API.error(_response);
}
});
break;
case 'delete' :
if (!model.id) {
params.error(null, "MISSING MODEL ID");
Ti.API.error("[REST API] ERROR: MISSING MODEL ID");
return;
}
//params.url = params.url + '/' + model.id;
if (_.indexOf(params.url, "?") == -1) {
params.url = params.url + '/' + model.id;
} else {
var str = params.url.split("?");
params.url = str[0] + '/' + model.id + "?" + str[1];
}
if (params.urlparams) {
params.url = encodeData(params.urlparams, params.url);
}
logger(DEBUG, "delete options", params);
apiCall(params, function(_response) {
if (_response.success) {
var data = parseJSON(DEBUG, _response, parentNode, model);
params.success(null, _response.responseText);
model.trigger("fetch");
} else {
Alloy.Backbone.VERSION === '0.9.2' ? params.error(model, _response.responseText) : params.error(_response.responseText);
Ti.API.error('[REST API] DELETE ERROR: ');
Ti.API.error(_response);
}
});
break;
}
}
/////////////////////////////////////////////
// HELPERS
/////////////////////////////////////////////
function logger(DEBUG, message, data) {
if (DEBUG) {
Ti.API.debug("[REST API] " + message);
if (data) {
Ti.API.debug( typeof data === 'object' ? JSON.stringify(data, null, '\t') : data);
}
}
}
function parseJSON(DEBUG, _response, parentNode, model) {
var data = _response.responseJSON;
if (!_.isUndefined(parentNode)) {
data = _.isFunction(parentNode) ? parentNode(data, _response, model) : traverseProperties(data, parentNode);
}
logger(DEBUG, "server response", _response);
return data;
}
function traverseProperties(object, string) {
var explodedString = string.split('.');
for ( i = 0, l = explodedString.length; i < l; i++) {
object = object[explodedString[i]];
}
return object;
}
function encodeData(obj, url) {
var str = [];
for (var p in obj) {
str.push(Ti.Network.encodeURIComponent(p) + "=" + Ti.Network.encodeURIComponent(obj[p]));
}
if (_.indexOf(url, "?") == -1) {
return url + "?" + str.join("&");
} else {
return url + "&" + str.join("&");
}
}
/**
* Get the ETag for the given url
* @param {Object} url
*/
function getETag(url) {
var obj = Ti.App.Properties.getObject("NAPP_REST_ADAPTER", {});
var data = obj[url];
return data || null;
}
/**
* Set the ETag for the given url
* @param {Object} url
* @param {Object} eTag
*/
function setETag(url, eTag) {
if (eTag && url) {
var obj = Ti.App.Properties.getObject("NAPP_REST_ADAPTER", {});
obj[url] = eTag;
Ti.App.Properties.setObject("NAPP_REST_ADAPTER", obj);
}
}
//we need underscore
var _ = require("alloy/underscore")._;
//until this issue is fixed: https://jira.appcelerator.org/browse/TIMOB-11752
var Alloy = require("alloy"), Backbone = Alloy.Backbone;
module.exports.sync = Sync;
module.exports.beforeModelCreate = function(config, name) {
config = config || {};
InitAdapter(config);
return config;
};
module.exports.afterModelCreate = function(Model, name) {
Model = Model || {};
Model.prototype.config.Model = Model;
Model.prototype.idAttribute = Model.prototype.config.adapter.idAttribute;
return Model;
};