loopback/lib/connectors/server.js

450 lines
11 KiB
JavaScript

/*!
* Dependencies.
*/
var assert = require('assert')
, loopback = require('../loopback')
, debug = require('debug')
, path = require('path')
, request = require('browser-request')
, Connector = require('loopback-datasource-juggler').Connector
, util = require('util');
/*!
* Export the ServerConnector class.
*/
module.exports = ServerConnector;
/*!
* Create an instance of the connector with the given `settings`.
*/
function ServerConnector(settings, dataSource) {
Connector.call(this, 'server', settings);
this.settings = settings;
this.dataSource = dataSource;
dataSource.DataAccessObject = dataSource.constructor.DataAccessObject;
settings.base = settings.base || '/';
dataSource.connect = this.connect;
}
util.inherits(ServerConnector, Connector);
ServerConnector.initialize = function(dataSource, callback) {
var connector = dataSource.connector = new ServerConnector(dataSource.settings, dataSource);
var remoteModels = connector.settings.discover;
if(remoteModels) {
remoteModels = remoteModels.sort(function(remoteModel) {
var settings = remoteModel.settings;
var trackChanges = settings && settings.trackChanges;
return trackChanges ? 1 : 0;
});
remoteModels.forEach(connector.buildModel.bind(connector));
}
callback();
}
ServerConnector.prototype.connect = function(callback) {
process.nextTick(function () {
callback && callback(null, self.db);
});
}
ServerConnector.prototype.requestModel = function(model, req, callback) {
var Model = loopback.getModel(model);
var modelPath = '/' + Model.pluralModelName;
var url = path.join(this.settings.base, modelPath, req.url || '');
this.request(url, req, callback);
}
ServerConnector.prototype.requestModelById = function(model, id, req, callback) {
var Model = loopback.getModel(model);
var modelPath = '/' + Model.pluralModelName;
var url = path.join(this.settings.base, modelPath, id.toString(), req.url || '');
this.request(url, req, callback);
}
ServerConnector.prototype.request = function(url, req, callback) {
request({
url: url,
method: req.method || 'GET',
body: req.body,
json: req.json || true
}, function(err, res, body) {
if(res.statusCode >= 400) {
if(res.statusCode === 404 && req.ignoreNotFound) {
return callback && callback(null, null);
}
err = body.error || body;
body = undefined;
}
callback && callback(err, body);
});
}
ServerConnector.prototype.buildModel = function(remoteModel) {
var modelName = remoteModel.modelName;
var dataSource = this.dataSource;
var connector = this;
if(remoteModel.settings && remoteModel.settings.trackChanges) {
remoteModel.settings.trackChanges = false;
}
var Model = loopback.createModel(
modelName,
remoteModel.properties || {},
remoteModel.settings
);
Model.attachTo(dataSource);
return;
if(!Model.defineMethod) {
Model.defineMethod = function defineMethod(method) {
var isStatic = method.fullName.indexOf('.prototype.') === -1;
var scope = isStatic ? Model : Model.prototype;
var methodName = isStatic ? method.name : method.name.replace('prototype.', '');
if(methodName === 'Change') {
return; // skip
}
scope[methodName] = function() {
console.log(method.name);
var callback = arguments[arguments.length - 1];
var ctx = new Context(
connector.settings.base,
remoteModel,
Model,
method,
arguments,
callback
);
ctx.invoke();
};
}
}
remoteModel.methods.forEach(Model.defineMethod.bind(Model));
}
/**
* Create a new model instance for the given data
* @param {String} model The model name
* @param {Object} data The model data
* @param {Function} [callback] The callback function
*/
ServerConnector.prototype.create = function (model, data, callback) {
this.requestModel(model, {
method: 'POST',
body: data
}, callback);
};
/**
* Save the model instance for the given data
* @param {String} model The model name
* @param {Object} data The model data
* @param {Function} [callback] The callback function
*/
ServerConnector.prototype.save = function (model, data, callback) {
var idValue = this.getIdValue(model, data);
if(idValue) {
this.requestModel(model, {
method: 'PUT',
body: data
}, callback);
} else {
this.create(model, data, callback);
}
};
/**
* Check if a model instance exists by id
* @param {String} model The model name
* @param {*} id The id value
* @param {Function} [callback] The callback function
*/
ServerConnector.prototype.exists = function (model, id, callback) {
this.requestModel(model, {
url: '/exists'
}, callback);
};
/**
* Find a model instance by id
* @param {String} model The model name
* @param {*} id The id value
* @param {Function} [callback] The callback function
*/
ServerConnector.prototype.find = function find(model, id, callback) {
this.requestModelById(model, id, {
ignoreNotFound: true
}, callback);
};
/**
* Update if the model instance exists with the same id or create a new instance
*
* @param {String} model The model name
* @param {Object} data The model instance data
* @param {Function} [callback] The callback function
*/
ServerConnector.prototype.updateOrCreate = function updateOrCreate(model, data, callback) {
var self = this;
var idValue = self.getIdValue(model, data);
if (idValue === null || idValue === undefined) {
return this.create(data, callback);
}
this.find(model, idValue, function (err, inst) {
if (err) {
return callback(err);
}
if (inst) {
self.updateAttributes(model, idValue, data, callback);
} else {
self.create(model, data, function (err, id) {
if (err) {
return callback(err);
}
if (id) {
self.setIdValue(model, data, id);
callback(null, data);
} else {
callback(null, null); // wtf?
}
});
}
});
};
/**
* Delete a model instance by id
* @param {String} model The model name
* @param {*} id The id value
* @param [callback] The callback function
*/
ServerConnector.prototype.destroy = function destroy(model, id, callback) {
this.requestModelById(model, id, {
method: 'DELETE',
json: false
}, callback);
};
/**
* Find matching model instances by the filter
*
* @param {String} model The model name
* @param {Object} filter The filter
* @param {Function} [callback] The callback function
*/
ServerConnector.prototype.all = function all(model, filter, callback) {
this.requestModel(model, {
query: {filter: filter}
}, callback);
};
/**
* Delete all instances for the given model
* @param {String} model The model name
* @param {Object} [where] The filter for where
* @param {Function} [callback] The callback function
*/
ServerConnector.prototype.destroyAll = function destroyAll(model, where, callback) {
this.requestModel(model, {
method: 'DELETE',
query: {where: where}
}, callback);
};
/**
* Count the number of instances for the given model
*
* @param {String} model The model name
* @param {Function} [callback] The callback function
* @param {Object} filter The filter for where
*
*/
ServerConnector.prototype.count = function count(model, callback, where) {
this.requestModel(model, {
url: '/count',
query: {where: where}
}, callback);
};
/**
* Update properties for the model instance data
* @param {String} model The model name
* @param {Object} data The model data
* @param {Function} [callback] The callback function
*/
ServerConnector.prototype.updateAttributes = function updateAttrs(model, id, data, callback) {
this.requestModelById(model, id, {
method: 'PUT',
url: '/updateAttributes'
}, callback);
};
function Context(base, meta, model, method, args, callback) {
this.base = base;
this.meta = meta;
this.model = model;
this.method = method;
this.args = this.mapArgs(args);
this.callback = callback;
}
/**
* Build an http request object from the `context`.
* @return {Object} request
*/
Context.prototype.toRequest = function() {
return {
url: this.url(),
query: this.query(),
method: this.verb(),
body: this.body(),
headers: this.headers(),
json: this.isJSON()
}
}
Context.prototype.isJSON = function() {
return true;
}
Context.prototype.url = function() {
var ctx = this;
var args = this.args;
var url = path.join(
this.base,
this.meta.baseRoute.path,
this.route().path
);
// replace url fragments with url params
this.method.accepts.forEach(function(param) {
var argName = param.arg;
var val = args[argName];
if(param && param.http && param.http.source === 'path') {
url = url.replace(':' + argName, val);
}
});
return url;
}
Context.prototype.query = function() {
var accepts = this.method.accepts;
var queryParams;
var ctx = this;
if(accepts && accepts.length) {
accepts.forEach(function(param) {
var http = param.http || {};
var explicit = http.source === 'query';
var implicit = http.source !== 'body' && http.source !== 'url';
if(explicit || implicit) {
queryParams = queryParams || {};
queryParams[param.arg] = ctx.args[param.arg];
}
});
}
return queryParams;
}
Context.prototype.route = function() {
var routes = this.method.routes;
return routes[0] || {path: '/', verb: 'GET'};
}
Context.prototype.verb = function() {
return this.route().verb.toUpperCase();
}
Context.prototype.body = function() {
var accepts = this.method.accepts;
var body;
var ctx = this;
if(accepts && accepts.length) {
accepts.forEach(function(param) {
var http = param.http || {};
var explicit = http.source === 'body';
if(explicit) {
body = ctx.args[param.arg];
}
});
}
return body;
}
Context.prototype.headers = function() {
return {};
}
Context.prototype.mapArgs = function(args) {
var accepts = this.method.accepts || [];
var args = Array.prototype.slice.call(args);
var result = {};
var supportedSources = ['body', 'form', 'query', 'path'];
accepts.forEach(function(param) {
if(param.http && param.http.source) {
// skip explicit unknown sources
if(supportedSources.indexOf(param.http.source) === -1) return;
}
var val = args.shift();
var type = typeof val;
if(Array.isArray(val)) {
type = 'array';
}
// skip all functions
if(type === 'function') return;
switch(param.type) {
case 'any':
case type:
result[param.arg] = val;
break;
default:
// skip this param
args.unshift(val);
break;
}
});
return result;
}
Context.prototype.handleResponse = function(err, res, body) {
// TODO handle `returns` correctly
this.callback.call(this, err, body);
}
Context.prototype.invoke = function() {
var req = this.toRequest();
request(req, this.handleResponse.bind(this));
}