Windshaft-cartodb/lib/cartodb/controllers/map.js

494 lines
18 KiB
JavaScript
Raw Normal View History

var _ = require('underscore');
var assert = require('assert');
var step = require('step');
var windshaft = require('windshaft');
var QueryTables = require('cartodb-query-tables');
var ResourceLocator = require('../models/resource-locator');
2015-07-08 19:27:56 +08:00
var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');
2015-07-08 19:27:56 +08:00
var MapConfig = windshaft.model.MapConfig;
var Datasource = windshaft.model.Datasource;
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
var NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider');
var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/create-layergroup-provider');
/**
* @param {AuthApi} authApi
* @param {PgConnection} pgConnection
* @param {TemplateMaps} templateMaps
* @param {MapBackend} mapBackend
* @param metadataBackend
* @param {SurrogateKeysCache} surrogateKeysCache
* @param {UserLimitsApi} userLimitsApi
* @param {LayergroupAffectedTables} layergroupAffectedTables
2016-05-24 05:35:42 +08:00
* @param {MapConfigAdapter} mapConfigAdapter
* @param {StatsBackend} statsBackend
* @constructor
*/
function MapController(prepareContext, pgConnection, templateMaps, mapBackend, metadataBackend,
2017-05-09 00:42:40 +08:00
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, mapConfigAdapter,
statsBackend) {
this.pgConnection = pgConnection;
this.templateMaps = templateMaps;
this.mapBackend = mapBackend;
this.metadataBackend = metadataBackend;
this.surrogateKeysCache = surrogateKeysCache;
2015-07-11 01:10:55 +08:00
this.userLimitsApi = userLimitsApi;
this.layergroupAffectedTables = layergroupAffectedTables;
2016-05-24 05:35:42 +08:00
this.mapConfigAdapter = mapConfigAdapter;
this.resourceLocator = new ResourceLocator(global.environment);
2017-05-09 00:42:40 +08:00
this.statsBackend = statsBackend;
this.prepareContext = prepareContext;
}
module.exports = MapController;
MapController.prototype.register = function(app) {
app.get(
app.base_url_mapconfig,
cors(),
userMiddleware,
2017-09-22 23:56:47 +08:00
this.prepareContext,
this.createGet.bind(this)
);
app.post(
app.base_url_mapconfig,
cors(),
userMiddleware,
2017-09-22 23:56:47 +08:00
this.prepareContext,
this.createPost.bind(this)
);
app.get(
app.base_url_templated + '/:template_id/jsonp',
cors(),
userMiddleware,
2017-09-22 23:56:47 +08:00
this.prepareContext,
this.jsonp.bind(this)
);
app.post(
app.base_url_templated + '/:template_id',
cors(),
userMiddleware,
2017-09-22 23:56:47 +08:00
this.prepareContext,
this.instantiate.bind(this)
);
app.options(app.base_url_mapconfig, cors('Content-Type'));
};
MapController.prototype.createGet = function(req, res, next){
req.profiler.start('windshaft.createmap_get');
2017-09-29 18:54:21 +08:00
this.create(req, res, function createGet$prepareConfig(req, config) {
if ( ! config ) {
throw new Error('layergroup GET needs a "config" parameter');
}
2017-09-29 18:54:21 +08:00
return JSON.parse(config);
}, next);
};
MapController.prototype.createPost = function(req, res, next) {
req.profiler.start('windshaft.createmap_post');
this.create(req, res, function createPost$prepareConfig(req) {
if (!req.is('application/json')) {
throw new Error('layergroup POST data must be of type application/json');
}
return req.body;
}, next);
};
MapController.prototype.instantiate = function(req, res, next) {
req.profiler.start('windshaft-cartodb.instance_template_post');
this.instantiateTemplate(req, res, function prepareTemplateParams(callback) {
if (!req.is('application/json')) {
return callback(new Error('Template POST data must be of type application/json'));
}
return callback(null, req.body);
}, next);
};
MapController.prototype.jsonp = function(req, res, next) {
req.profiler.start('windshaft-cartodb.instance_template_get');
this.instantiateTemplate(req, res, function prepareJsonTemplateParams(callback) {
var err = null;
if ( req.query.callback === undefined || req.query.callback.length === 0) {
err = new Error('callback parameter should be present and be a function name');
}
var templateParams = {};
if (req.query.config) {
try {
templateParams = JSON.parse(req.query.config);
} catch(e) {
err = new Error('Invalid config parameter, should be a valid JSON');
}
}
return callback(err, templateParams);
}, next);
};
MapController.prototype.create = function(req, res, prepareConfigFn, next) {
var self = this;
var mapConfig;
2016-05-23 22:20:42 +08:00
var context = {};
step(
function prepareConfig () {
2017-09-29 18:54:21 +08:00
const requestMapConfig = prepareConfigFn(req, res.locals.config);
return requestMapConfig;
},
2016-05-24 00:59:23 +08:00
function prepareAdapterMapConfig(err, requestMapConfig) {
assert.ifError(err);
2016-05-23 22:44:14 +08:00
context.analysisConfiguration = {
2017-10-03 23:47:57 +08:00
user: res.locals.user,
db: {
2017-09-29 18:54:21 +08:00
host: res.locals.dbhost,
port: res.locals.dbport,
dbname: res.locals.dbname,
user: res.locals.dbuser,
pass: res.locals.dbpassword
},
batch: {
2017-10-03 23:47:57 +08:00
username: res.locals.user,
2017-09-29 18:54:21 +08:00
apiKey: res.locals.api_key
}
};
2017-10-03 23:47:57 +08:00
self.mapConfigAdapter.getMapConfig(res.locals.user, requestMapConfig, res.locals, context, this);
2016-01-20 02:31:43 +08:00
},
2016-05-24 01:09:57 +08:00
function createLayergroup(err, requestMapConfig) {
assert.ifError(err);
2016-05-24 01:09:57 +08:00
var datasource = context.datasource || Datasource.EmptyDatasource();
mapConfig = new MapConfig(requestMapConfig, datasource);
2015-07-11 01:10:55 +08:00
self.mapBackend.createLayergroup(
mapConfig,
2017-10-02 18:07:35 +08:00
res.locals,
2017-10-03 23:47:57 +08:00
new CreateLayergroupMapConfigProvider(mapConfig, res.locals.user, self.userLimitsApi, res.locals),
2015-07-11 01:10:55 +08:00
this
);
},
function afterLayergroupCreate(err, layergroup) {
assert.ifError(err);
2017-05-18 17:51:12 +08:00
self.afterLayergroupCreate(req, res, mapConfig, layergroup, context.analysesResults, this);
},
function finish(err, layergroup) {
if (err) {
err = Number.isFinite(err.layerIndex) ? populateError(err, mapConfig) : err;
err.label = 'ANONYMOUS LAYERGROUP';
return next(err);
}
var analysesResults = context.analysesResults || [];
self.addDataviewsAndWidgetsUrls(res.locals.user, layergroup, mapConfig.obj());
self.addAnalysesMetadata(res.locals.user, layergroup, analysesResults, true);
addContextMetadata(layergroup, mapConfig.obj(), context);
res.set('X-Layergroup-Id', layergroup.layergroupid);
res.status(200);
if (req.query && req.query.callback) {
res.jsonp(layergroup);
} else {
res.json(layergroup);
}
}
);
};
function populateError(err, mapConfig) {
var error = new Error(err.message);
error.http_status = err.http_status;
if (!err.http_status && err.message.indexOf('column "the_geom_webmercator" does not exist') >= 0) {
error.http_status = 400;
}
error.type = 'layer';
error.subtype = err.message.indexOf('Postgis Plugin') >= 0 ? 'query' : undefined;
error.layer = {
id: mapConfig.getLayerId(err.layerIndex),
index: err.layerIndex,
type: mapConfig.layerType(err.layerIndex)
};
return error;
}
2016-09-20 22:09:21 +08:00
function addContextMetadata(layergroup, mapConfig, context) {
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
if (context.turboCarto && Array.isArray(context.turboCarto.layers)) {
layer.meta.cartocss_meta = context.turboCarto.layers[layerIndex];
}
2016-09-20 22:09:21 +08:00
return layer;
});
}
}
MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn, next) {
var self = this;
2017-10-03 23:47:57 +08:00
var cdbuser = res.locals.user;
var mapConfigProvider;
var mapConfig;
step(
function getTemplateParams() {
prepareParamsFn(this);
},
function getTemplate(err, templateParams) {
assert.ifError(err);
mapConfigProvider = new NamedMapMapConfigProvider(
self.templateMaps,
self.pgConnection,
self.metadataBackend,
2015-07-11 01:10:55 +08:00
self.userLimitsApi,
2016-05-24 05:29:06 +08:00
self.mapConfigAdapter,
cdbuser,
req.params.template_id,
templateParams,
2017-09-29 18:54:21 +08:00
res.locals.auth_token,
res.locals
);
mapConfigProvider.getMapConfig(this);
},
2017-05-18 17:51:12 +08:00
function createLayergroup(err, mapConfig_, rendererParams) {
assert.ifError(err);
mapConfig = mapConfig_;
2015-07-11 01:10:55 +08:00
self.mapBackend.createLayergroup(
mapConfig, rendererParams,
new CreateLayergroupMapConfigProvider(mapConfig, cdbuser, self.userLimitsApi, rendererParams),
this
);
},
function afterLayergroupCreate(err, layergroup) {
assert.ifError(err);
2017-05-10 00:24:24 +08:00
self.afterLayergroupCreate(req, res, mapConfig, layergroup,
mapConfigProvider.analysesResults,
2017-05-18 17:51:12 +08:00
this);
},
function finishTemplateInstantiation(err, layergroup) {
if (err) {
err.label = 'NAMED MAP LAYERGROUP';
next(err);
} else {
var templateHash = self.templateMaps.fingerPrint(mapConfigProvider.template).substring(0, 8);
layergroup.layergroupid = cdbuser + '@' + templateHash + '@' + layergroup.layergroupid;
2016-09-20 22:09:21 +08:00
var _mapConfig = mapConfig.obj();
self.addDataviewsAndWidgetsUrls(cdbuser, layergroup, _mapConfig);
self.addAnalysesMetadata(cdbuser, layergroup, mapConfigProvider.analysesResults);
2016-09-20 22:09:21 +08:00
addContextMetadata(layergroup, _mapConfig, mapConfigProvider.context);
2015-09-17 08:03:09 +08:00
res.set('X-Layergroup-Id', layergroup.layergroupid);
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, mapConfigProvider.getTemplateName()));
res.status(200);
if (req.query && req.query.callback) {
res.jsonp(layergroup);
} else {
res.json(layergroup);
}
}
}
);
};
2017-05-10 00:24:24 +08:00
MapController.prototype.afterLayergroupCreate =
2017-05-18 17:51:12 +08:00
function(req, res, mapconfig, layergroup, analysesResults, callback) {
var self = this;
2017-10-03 23:47:57 +08:00
var username = res.locals.user;
var tasksleft = 2; // redis key and affectedTables
var errors = [];
var done = function(err) {
if ( err ) {
errors.push('' + err);
}
if ( ! --tasksleft ) {
err = errors.length ? new Error(errors.join('\n')) : null;
callback(err, layergroup);
}
};
// include in layergroup response the variables in serverMedata
// those variables are useful to send to the client information
// about how to reach this server or information about it
_.extend(layergroup, global.environment.serverMetadata);
// Don't wait for the mapview count increment to
// take place before proceeding. Error will be logged
// asynchronously
this.metadataBackend.incMapviewCount(username, mapconfig.obj().stat_tag, function(err) {
req.profiler.done('incMapviewCount');
if ( err ) {
global.logger.log("ERROR: failed to increment mapview count for user '" + username + "': " + err);
}
done();
});
var sql = [];
mapconfig.getLayers().forEach(function(layer) {
sql.push(layer.options.sql);
if (layer.options.affected_tables) {
layer.options.affected_tables.map(function(table) {
sql.push('SELECT * FROM ' + table + ' LIMIT 0');
});
}
});
2017-09-29 18:54:21 +08:00
var dbName = res.locals.dbname;
var layergroupId = layergroup.layergroupid;
2017-05-09 00:42:40 +08:00
var dbConnection;
step(
2016-03-04 02:01:58 +08:00
function getPgConnection() {
self.pgConnection.getConnection(username, this);
},
2016-03-04 02:01:58 +08:00
function getAffectedTablesAndLastUpdatedTime(err, connection) {
assert.ifError(err);
2017-05-09 00:42:40 +08:00
dbConnection = connection;
QueryTables.getAffectedTablesFromQuery(dbConnection, sql.join(';'), this);
},
function handleAffectedTablesAndLastUpdatedTime(err, result) {
req.profiler.done('queryTablesAndLastUpdated');
assert.ifError(err);
// feed affected tables cache so it can be reused from, for instance, layergroup controller
2016-02-22 18:40:25 +08:00
self.layergroupAffectedTables.set(dbName, layergroupId, result);
var lastUpdateTime = result.getLastUpdatedAt();
lastUpdateTime = getLastUpdatedTime(analysesResults, lastUpdateTime) || lastUpdateTime;
// last update for layergroup cache buster
layergroup.layergroupid = layergroup.layergroupid + ':' + lastUpdateTime;
layergroup.last_updated = new Date(lastUpdateTime).toISOString();
if (req.method === 'GET') {
var ttl = global.environment.varnish.layergroupTtl || 86400;
2015-09-17 08:03:09 +08:00
res.set('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
res.set('Last-Modified', (new Date()).toUTCString());
2016-02-22 18:40:25 +08:00
res.set('X-Cache-Channel', result.getCacheChannel());
if (result.tables && result.tables.length > 0) {
self.surrogateKeysCache.tag(res, result);
}
}
return null;
},
function fetchLayersStats(err) {
2017-05-09 00:42:40 +08:00
assert.ifError(err);
var next = this;
self.statsBackend.getStats(mapconfig, dbConnection, function(err, layersStats) {
2017-05-09 00:42:40 +08:00
if (err) {
return next(err);
}
if (layersStats.length > 0) {
layergroup.metadata.layers.forEach(function (layer, index) {
layer.meta.stats = layersStats[index];
});
2017-05-09 00:42:40 +08:00
}
return next();
});
},
function finish(err) {
done(err);
}
);
};
function getLastUpdatedTime(analysesResults, lastUpdateTime) {
if (!Array.isArray(analysesResults)) {
return lastUpdateTime;
}
return analysesResults.reduce(function(lastUpdateTime, analysis) {
return analysis.getNodes().reduce(function(lastNodeUpdatedAtTime, node) {
var nodeUpdatedAtDate = node.getUpdatedAt();
var nodeUpdatedTimeAt = (nodeUpdatedAtDate && nodeUpdatedAtDate.getTime()) || 0;
return nodeUpdatedTimeAt > lastNodeUpdatedAtTime ? nodeUpdatedTimeAt : lastNodeUpdatedAtTime;
}, lastUpdateTime);
}, lastUpdateTime);
}
MapController.prototype.addAnalysesMetadata = function(username, layergroup, analysesResults, includeQuery) {
includeQuery = includeQuery || false;
analysesResults = analysesResults || [];
layergroup.metadata.analyses = [];
analysesResults.forEach(function(analysis) {
var nodes = analysis.getNodes();
layergroup.metadata.analyses.push({
nodes: nodes.reduce(function(nodesIdMap, node) {
if (node.params.id) {
var nodeResource = layergroup.layergroupid + '/analysis/node/' + node.id();
var nodeRepr = {
2016-04-12 00:49:43 +08:00
status: node.getStatus(),
url: this.resourceLocator.getUrls(username, nodeResource)
};
if (includeQuery) {
nodeRepr.query = node.getQuery();
}
if (node.getStatus() === 'failed') {
nodeRepr.error_message = node.getErrorMessage();
}
nodesIdMap[node.params.id] = nodeRepr;
}
return nodesIdMap;
}.bind(this), {})
});
}.bind(this));
};
2016-06-02 20:14:11 +08:00
// TODO this should take into account several URL patterns
MapController.prototype.addDataviewsAndWidgetsUrls = function(username, layergroup, mapConfig) {
this.addDataviewsUrls(username, layergroup, mapConfig);
this.addWidgetsUrl(username, layergroup, mapConfig);
};
2016-06-02 20:14:11 +08:00
MapController.prototype.addDataviewsUrls = function(username, layergroup, mapConfig) {
2016-03-19 01:09:17 +08:00
layergroup.metadata.dataviews = layergroup.metadata.dataviews || {};
var dataviews = mapConfig.dataviews || {};
Object.keys(dataviews).forEach(function(dataviewName) {
var resource = layergroup.layergroupid + '/dataview/' + dataviewName;
layergroup.metadata.dataviews[dataviewName] = {
url: this.resourceLocator.getUrls(username, resource)
2016-03-19 01:09:17 +08:00
};
}.bind(this));
};
2016-03-19 01:09:17 +08:00
MapController.prototype.addWidgetsUrl = function(username, layergroup, mapConfig) {
2016-06-02 20:14:11 +08:00
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
2016-06-02 20:14:11 +08:00
var mapConfigLayer = mapConfig.layers[layerIndex];
if (mapConfigLayer.options && mapConfigLayer.options.widgets) {
layer.widgets = layer.widgets || {};
Object.keys(mapConfigLayer.options.widgets).forEach(function(widgetName) {
var resource = layergroup.layergroupid + '/' + layerIndex + '/widget/' + widgetName;
2016-06-02 20:14:11 +08:00
layer.widgets[widgetName] = {
type: mapConfigLayer.options.widgets[widgetName].type,
url: this.resourceLocator.getUrls(username, resource)
2016-06-02 20:14:11 +08:00
};
}.bind(this));
}
return layer;
}.bind(this));
}
};