diff --git a/lib/cartodb/controllers/layergroup.js b/lib/cartodb/controllers/layergroup.js index dc6f85ff..d9551c9c 100644 --- a/lib/cartodb/controllers/layergroup.js +++ b/lib/cartodb/controllers/layergroup.js @@ -385,8 +385,6 @@ LayergroupController.prototype.staticMap = function(req, res, width, height, zoo }; LayergroupController.prototype.sendResponse = function(req, res, body, status, headers) { - var self = this; - req.profiler.done('res'); res.set('Cache-Control', 'public,max-age=31536000'); @@ -401,43 +399,48 @@ LayergroupController.prototype.sendResponse = function(req, res, body, status, h } res.set('Last-Modified', lastUpdated.toUTCString()); - var dbName = res.locals.dbname; - step( - function getAffectedTables() { - self.getAffectedTables(res.locals.user, dbName, res.locals.token, this); - }, - function sendResponse(err, affectedTables) { - req.profiler.done('affectedTables'); - if (err) { - global.logger.warn('ERROR generating cache channel: ' + err); - } - if (!!affectedTables) { - res.set('X-Cache-Channel', affectedTables.getCacheChannel()); - self.surrogateKeysCache.tag(res, affectedTables); - } - - if (headers) { - res.set(headers); - } - - res.status(status); - - if (!Buffer.isBuffer(body) && typeof body === 'object') { - if (req.query && req.query.callback) { - res.jsonp(body); - } else { - res.json(body); - } - } else { - res.send(body); - } + this.setCacheChannel(req, res, (err) => { + if (err) { + global.logger.warn('ERROR generating cache channel: ' + err); } - ); + if (headers) { + res.set(headers); + } + + res.status(status); + + if (!Buffer.isBuffer(body) && typeof body === 'object') { + if (req.query && req.query.callback) { + res.jsonp(body); + } else { + res.json(body); + } + } else { + res.send(body); + } + }); +}; + +LayergroupController.prototype.setCacheChannel = function(req, res, callback) { + const { dbname, user, token } = res.locals; + + this.getAffectedTables(user, dbname, token, (err, affectedTables) => { + req.profiler.done('affectedTables'); + if (err) { + return callback(err); + } + + if (!!affectedTables) { + res.set('X-Cache-Channel', affectedTables.getCacheChannel()); + this.surrogateKeysCache.tag(res, affectedTables); + } + + callback(); + }); }; LayergroupController.prototype.getAffectedTables = function(user, dbName, layergroupId, callback) { - if (this.layergroupAffectedTables.hasAffectedTables(dbName, layergroupId)) { return callback(null, this.layergroupAffectedTables.get(dbName, layergroupId)); } diff --git a/lib/cartodb/controllers/map.js b/lib/cartodb/controllers/map.js index f1bbe55e..4c2d6764 100644 --- a/lib/cartodb/controllers/map.js +++ b/lib/cartodb/controllers/map.js @@ -1,6 +1,4 @@ var _ = require('underscore'); -var assert = require('assert'); -var step = require('step'); var windshaft = require('windshaft'); var QueryTables = require('cartodb-query-tables'); @@ -51,365 +49,328 @@ function MapController(prepareContext, pgConnection, templateMaps, mapBackend, m module.exports = MapController; MapController.prototype.register = function(app) { - app.get( - app.base_url_mapconfig, - cors(), - userMiddleware, - this.prepareContext, - this.createGet.bind(this) - ); - app.post( - app.base_url_mapconfig, - cors(), - userMiddleware, - this.prepareContext, - this.createPost.bind(this) - ); - app.get( - app.base_url_templated + '/:template_id/jsonp', - cors(), - userMiddleware, - this.prepareContext, - this.jsonp.bind(this) - ); - app.post( - app.base_url_templated + '/:template_id', - cors(), - userMiddleware, - this.prepareContext, - this.instantiate.bind(this) - ); + const { base_url_mapconfig, base_url_templated } = app; + const useTemplate = true; + + app.get(base_url_mapconfig, this.composeCreateMapMiddleware()); + app.post(base_url_mapconfig, this.composeCreateMapMiddleware()); + app.get(`${base_url_templated}/:template_id/jsonp`, this.composeCreateMapMiddleware(useTemplate)); + app.post(`${base_url_templated}/:template_id`, this.composeCreateMapMiddleware(useTemplate)); app.options(app.base_url_mapconfig, cors('Content-Type')); }; -MapController.prototype.createGet = function(req, res, next){ - req.profiler.start('windshaft.createmap_get'); +MapController.prototype.composeCreateMapMiddleware = function (useTemplate = false) { + const isTemplateInstantiation = useTemplate; + const useTemplateHash = useTemplate; + const includeQuery = !useTemplate; + const label = useTemplate ? 'NAMED MAP LAYERGROUP' : 'ANONYMOUS LAYERGROUP'; + const addContext = !useTemplate; - this.create(req, res, function createGet$prepareConfig(req, config) { - if ( ! config ) { - throw new Error('layergroup GET needs a "config" parameter'); - } - return JSON.parse(config); - }, next); + return [ + cors(), + userMiddleware, + this.prepareContext, + this.initProfiler(isTemplateInstantiation), + this.checkJsonContentType(), + useTemplate ? this.checkInstantiteLayergroup() : this.checkCreateLayergroup(), + useTemplate ? this.getTemplate() : this.prepareAdapterMapConfig(), + useTemplate ? this.instantiateLayergroup() : this.createLayergroup(), + this.incrementMapViewCount(), + this.augmentLayergroupData(), + this.getAffectedTables(), + this.setCacheChannel(), + this.setLastModified(), + this.setLastUpdatedTimeToLayergroup(), + this.setCacheControl(), + this.setLayerStats(), + this.setLayergroupIdHeader(useTemplateHash), + this.setDataviewsAndWidgetsUrlsToLayergroupMetadata(), + this.setAnalysesMetadataToLayergroup(includeQuery), + this.setTurboCartoMetadataToLayergroup(), + this.setSurrogateKeyHeader(), + this.sendResponse(), + this.augmentError({ label, addContext }) + ]; }; -MapController.prototype.createPost = function(req, res, next) { - req.profiler.start('windshaft.createmap_post'); +MapController.prototype.initProfiler = function (isTemplateInstantiation) { + const operation = isTemplateInstantiation ? 'instance_template' : 'createmap'; - 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); + return function initProfilerMiddleware (req, res, next) { + req.profiler.start(`windshaft-cartodb.${operation}_${req.method.toLowerCase()}`); + 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')); +MapController.prototype.checkJsonContentType = function () { + return function checkJsonContentTypeMiddleware(req, res, next) { + if (req.method === 'POST' && !req.is('application/json')) { + return next(new Error('POST data must be of type application/json')); } - return callback(null, req.body); - }, next); + + next(); + }; }; -MapController.prototype.jsonp = function(req, res, next) { - req.profiler.start('windshaft-cartodb.instance_template_get'); +MapController.prototype.checkInstantiteLayergroup = function () { + return function checkInstantiteLayergroupMiddleware(req, res, next) { + if (req.method === 'GET') { + const { callback, config } = req.query; - 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'); - } + if (callback === undefined || callback.length === 0) { + return next(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'); + if (config) { + try { + req.body = JSON.parse(config); + } catch(e) { + return next(new Error('Invalid config parameter, should be a valid JSON')); + } } } - return callback(err, templateParams); - }, next); + return next(); + }; }; -MapController.prototype.create = function(req, res, prepareConfigFn, next) { - var self = this; +MapController.prototype.checkCreateLayergroup = function () { + return function checkCreateLayergroupMiddleware (req, res, next) { + if (req.method === 'GET') { + const { config } = res.locals; - var mapConfig; + if (!config) { + return next(new Error('layergroup GET needs a "config" parameter')); + } - var context = {}; + try { + req.body = JSON.parse(config); + } catch (err) { + return next(err); + } + } - step( - function prepareConfig () { - const requestMapConfig = prepareConfigFn(req, res.locals.config); - return requestMapConfig; - }, - function prepareAdapterMapConfig(err, requestMapConfig) { - assert.ifError(err); - context.analysisConfiguration = { - user: res.locals.user, - db: { - host: res.locals.dbhost, - port: res.locals.dbport, - dbname: res.locals.dbname, - user: res.locals.dbuser, - pass: res.locals.dbpassword - }, - batch: { - username: res.locals.user, - apiKey: res.locals.api_key - } - }; - self.mapConfigAdapter.getMapConfig(res.locals.user, requestMapConfig, res.locals, context, this); - }, - function createLayergroup(err, requestMapConfig) { - assert.ifError(err); - var datasource = context.datasource || Datasource.EmptyDatasource(); - mapConfig = new MapConfig(requestMapConfig, datasource); - self.mapBackend.createLayergroup( - mapConfig, - res.locals, - new CreateLayergroupMapConfigProvider(mapConfig, res.locals.user, self.userLimitsApi, res.locals), - this - ); - }, - function afterLayergroupCreate(err, layergroup) { - assert.ifError(err); - self.afterLayergroupCreate(req, res, mapConfig, layergroup, context.analysesResults, this); - }, - function finish(err, layergroup) { + return next(); + }; +}; + +MapController.prototype.getTemplate = function () { + return function getTemplateMiddleware (req, res, next) { + const templateParams = req.body; + const { user } = res.locals; + + const mapconfigProvider = new NamedMapMapConfigProvider( + this.templateMaps, + this.pgConnection, + this.metadataBackend, + this.userLimitsApi, + this.mapConfigAdapter, + user, + req.params.template_id, + templateParams, + res.locals.auth_token, + res.locals + ); + + mapconfigProvider.getMapConfig((err, mapconfig, rendererParams) => { 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.locals.mapconfig = mapconfig; + res.locals.rendererParams = rendererParams; + res.locals.mapconfigProvider = mapconfigProvider; - 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; -} - -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]; - } - return layer; + next(); }); - } -} - -MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn, next) { - var self = this; - - 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, - self.userLimitsApi, - self.mapConfigAdapter, - cdbuser, - req.params.template_id, - templateParams, - res.locals.auth_token, - res.locals - ); - mapConfigProvider.getMapConfig(this); - }, - function createLayergroup(err, mapConfig_, rendererParams) { - assert.ifError(err); - mapConfig = mapConfig_; - self.mapBackend.createLayergroup( - mapConfig, rendererParams, - new CreateLayergroupMapConfigProvider(mapConfig, cdbuser, self.userLimitsApi, rendererParams), - this - ); - }, - function afterLayergroupCreate(err, layergroup) { - assert.ifError(err); - self.afterLayergroupCreate(req, res, mapConfig, layergroup, - mapConfigProvider.analysesResults, - 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; - - var _mapConfig = mapConfig.obj(); - self.addDataviewsAndWidgetsUrls(cdbuser, layergroup, _mapConfig); - self.addAnalysesMetadata(cdbuser, layergroup, mapConfigProvider.analysesResults); - addContextMetadata(layergroup, _mapConfig, mapConfigProvider.context); - - 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); - } - } - } - ); + }.bind(this); }; -MapController.prototype.afterLayergroupCreate = -function(req, res, mapconfig, layergroup, analysesResults, callback) { - var self = this; +MapController.prototype.prepareAdapterMapConfig = function () { + return function prepareAdapterMapConfigMiddleware(req, res, next) { + const requestMapConfig = req.body; + const { user, dbhost, dbport, dbname, dbuser, dbpassword, api_key } = res.locals; - 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'); - }); - } - }); - - var dbName = res.locals.dbname; - var layergroupId = layergroup.layergroupid; - var dbConnection; - - step( - function getPgConnection() { - self.pgConnection.getConnection(username, this); - }, - function getAffectedTablesAndLastUpdatedTime(err, connection) { - assert.ifError(err); - 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 - 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; - res.set('Cache-Control', 'public,max-age='+ttl+',must-revalidate'); - res.set('Last-Modified', (new Date()).toUTCString()); - res.set('X-Cache-Channel', result.getCacheChannel()); - if (result.tables && result.tables.length > 0) { - self.surrogateKeysCache.tag(res, result); + const context = { + analysisConfiguration: { + user, + db: { + host: dbhost, + port: dbport, + dbname: dbname, + user: dbuser, + pass: dbpassword + }, + batch: { + username: user, + apiKey: api_key } } + }; - return null; - }, - function fetchLayersStats(err) { - assert.ifError(err); - var next = this; - self.statsBackend.getStats(mapconfig, dbConnection, function(err, layersStats) { + this.mapConfigAdapter.getMapConfig(user, requestMapConfig, res.locals, context, (err, requestMapConfig) => { + if (err) { + return next(err); + } + + req.body = requestMapConfig; + res.locals.context = context; + + next(); + }); + }.bind(this); +}; + +MapController.prototype.createLayergroup = function () { + return function createLayergroupMiddleware (req, res, next) { + const requestMapConfig = req.body; + const { context, user } = res.locals; + const datasource = context.datasource || Datasource.EmptyDatasource(); + const mapconfig = new MapConfig(requestMapConfig, datasource); + const mapconfigProvider = + new CreateLayergroupMapConfigProvider(mapconfig, user, this.userLimitsApi, res.locals); + + res.locals.mapconfig = mapconfig; + res.locals.analysesResults = context.analysesResults; + + this.mapBackend.createLayergroup(mapconfig, res.locals, mapconfigProvider, (err, layergroup) => { + if (err) { + return next(err); + } + + res.locals.layergroup = layergroup; + + next(); + }); + }.bind(this); +}; + +MapController.prototype.instantiateLayergroup = function () { + return function instantiateLayergroupMiddleware (req, res, next) { + const { user, mapconfig, rendererParams } = res.locals; + const mapconfigProvider = + new CreateLayergroupMapConfigProvider(mapconfig, user, this.userLimitsApi, rendererParams); + + this.mapBackend.createLayergroup(mapconfig, rendererParams, mapconfigProvider, (err, layergroup) => { + if (err) { + return next(err); + } + + res.locals.layergroup = layergroup; + + const { mapconfigProvider } = res.locals; + + res.locals.analysesResults = mapconfigProvider.analysesResults; + res.locals.template = mapconfigProvider.template; + res.locals.templateName = mapconfigProvider.getTemplateName(); + res.locals.context = mapconfigProvider.context; + + next(); + }); + }.bind(this); +}; + +MapController.prototype.incrementMapViewCount = function () { + return function incrementMapViewCountMiddleware(req, res, next) { + const { mapconfig, user } = res.locals; + + // Error won't blow up, just be logged. + this.metadataBackend.incMapviewCount(user, mapconfig.obj().stat_tag, (err) => { + req.profiler.done('incMapviewCount'); + + if (err) { + global.logger.log(`ERROR: failed to increment mapview count for user '${user}': ${err.message}`); + } + + next(); + }); + }.bind(this); +}; + +MapController.prototype.augmentLayergroupData = function () { + return function augmentLayergroupDataMiddleware (req, res, next) { + const { layergroup } = res.locals; + + // 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); + + next(); + }; +}; + +MapController.prototype.getAffectedTables = function () { + return function getAffectedTablesMiddleware (req, res, next) { + const { dbname, layergroup, user, mapconfig } = res.locals; + + this.pgConnection.getConnection(user, (err, connection) => { + if (err) { + return next(err); + } + + const 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'); + }); + } + }); + + QueryTables.getAffectedTablesFromQuery(connection, sql.join(';'), (err, affectedTables) => { if (err) { return next(err); } - if (layersStats.length > 0) { - layergroup.metadata.layers.forEach(function (layer, index) { - layer.meta.stats = layersStats[index]; - }); - } - return next(); + + // feed affected tables cache so it can be reused from, for instance, layergroup controller + this.layergroupAffectedTables.set(dbname, layergroup.layergroupId, affectedTables); + + res.locals.affectedTables = affectedTables; + + next(); }); - }, - function finish(err) { - done(err); + }); + }.bind(this); +}; + +MapController.prototype.setCacheChannel = function () { + return function setCacheChannelMiddleware (req, res, next) { + const { affectedTables } = res.locals; + + if (req.method === 'GET') { + res.set('X-Cache-Channel', affectedTables.getCacheChannel()); } - ); + + next(); + }; +}; + +MapController.prototype.setLastModified = function () { + return function setLastModifiedMiddleware (req, res, next) { + if (req.method === 'GET') { + res.set('Last-Modified', (new Date()).toUTCString()); + } + + next(); + }; +}; + +MapController.prototype.setLastUpdatedTimeToLayergroup = function () { + return function setLastUpdatedTimeToLayergroupMiddleware (req, res, next) { + const { affectedTables, layergroup, analysesResults } = res.locals; + + var lastUpdateTime = affectedTables.getLastUpdatedAt(); + + lastUpdateTime = getLastUpdatedTime(analysesResults, lastUpdateTime) || lastUpdateTime; + + // last update for layergroup cache buster + layergroup.layergroupid = layergroup.layergroupid + ':' + lastUpdateTime; + layergroup.last_updated = new Date(lastUpdateTime).toISOString(); + + next(); + }; }; function getLastUpdatedTime(analysesResults, lastUpdateTime) { @@ -425,34 +386,66 @@ function getLastUpdatedTime(analysesResults, lastUpdateTime) { }, lastUpdateTime); } -MapController.prototype.addAnalysesMetadata = function(username, layergroup, analysesResults, includeQuery) { - includeQuery = includeQuery || false; - analysesResults = analysesResults || []; - layergroup.metadata.analyses = []; +MapController.prototype.setCacheControl = function () { + return function setCacheControlMiddleware (req, res, next) { + if (req.method === 'GET') { + var ttl = global.environment.varnish.layergroupTtl || 86400; + res.set('Cache-Control', 'public,max-age='+ttl+',must-revalidate'); + } - 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 = { - 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; + next(); + }; +}; + +MapController.prototype.setLayerStats = function () { + return function setLayerStatsMiddleware(req, res, next) { + const { user, mapconfig, layergroup } = res.locals; + + this.pgConnection.getConnection(user, (err, connection) => { + if (err) { + return next(err); + } + + this.statsBackend.getStats(mapconfig, connection, function(err, layersStats) { + if (err) { + return next(err); } - return nodesIdMap; - }.bind(this), {}) + if (layersStats.length > 0) { + layergroup.metadata.layers.forEach(function (layer, index) { + layer.meta.stats = layersStats[index]; + }); + } + + next(); + }); }); - }.bind(this)); + }.bind(this); +}; + +MapController.prototype.setLayergroupIdHeader = function (useTemplateHash) { + return function setLayergroupIdHeaderMiddleware (req, res, next) { + const { layergroup, user, template } = res.locals; + + if (useTemplateHash) { + var templateHash = this.templateMaps.fingerPrint(template).substring(0, 8); + layergroup.layergroupid = `${user}@${templateHash}@${layergroup.layergroupid}`; + } + + res.set('X-Layergroup-Id', layergroup.layergroupid); + + next(); + }.bind(this); +}; + +MapController.prototype.setDataviewsAndWidgetsUrlsToLayergroupMetadata = function () { + return function setDataviewsAndWidgetsUrlsToLayergroupMetadataMiddleware (req, res, next) { + const { layergroup, user, mapconfig } = res.locals; + + this.addDataviewsAndWidgetsUrls(user, layergroup, mapconfig.obj()); + + next(); + }.bind(this); }; // TODO this should take into account several URL patterns @@ -491,3 +484,129 @@ MapController.prototype.addWidgetsUrl = function(username, layergroup, mapConfig }.bind(this)); } }; + +MapController.prototype.setAnalysesMetadataToLayergroup = function (includeQuery) { + return function setAnalysesMetadataToLayergroupMiddleware (req, res, next) { + const { layergroup, user, analysesResults = [] } = res.locals; + + this.addAnalysesMetadata(user, layergroup, analysesResults, includeQuery); + + next(); + }.bind(this); +}; + +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 = { + 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)); +}; + +MapController.prototype.setTurboCartoMetadataToLayergroup = function () { + return function setTurboCartoMetadataToLayergroupMiddleware (req, res, next) { + const { layergroup, mapconfig, context } = res.locals; + + addContextMetadata(layergroup, mapconfig.obj(), context); + + next(); + }; +}; + +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]; + } + return layer; + }); + } +} + +MapController.prototype.setSurrogateKeyHeader = function () { + return function setSurrogateKeyHeaderMiddleware(req, res, next) { + const { affectedTables, user, templateName } = res.locals; + + if (req.method === 'GET' && affectedTables.tables && affectedTables.tables.length > 0) { + this.surrogateKeysCache.tag(res, affectedTables); + } + + if (templateName) { + this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, templateName)); + } + + next(); + }.bind(this); +}; + +MapController.prototype.sendResponse = function () { + return function sendResponseMiddleware (req, res) { + const { layergroup } = res.locals; + + res.status(200); + + if (req.query && req.query.callback) { + res.jsonp(layergroup); + } else { + res.json(layergroup); + } + }; +}; + +MapController.prototype.augmentError = function (options) { + const { addContext = false, label = 'MAPS CONTROLLER' } = options; + + return function augmentErrorMiddleware (err, req, res, next) { + const { mapconfig } = res.locals; + + if (addContext) { + err = Number.isFinite(err.layerIndex) ? populateError(err, mapconfig) : err; + } + + err.label = label; + + next(err); + }; +}; + +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; +} diff --git a/lib/cartodb/controllers/named_maps.js b/lib/cartodb/controllers/named_maps.js index 55790bbf..ffea03a2 100644 --- a/lib/cartodb/controllers/named_maps.js +++ b/lib/cartodb/controllers/named_maps.js @@ -39,43 +39,56 @@ NamedMapsController.prototype.register = function(app) { ); }; -NamedMapsController.prototype.sendResponse = function(req, res, body, headers, namedMapProvider) { +NamedMapsController.prototype.sendResponse = function(req, res, body, headers) { + const { namedMapProvider } = res.locals; + this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(res.locals.user, namedMapProvider.getTemplateName())); res.set('Content-Type', headers['content-type'] || headers['Content-Type'] || 'image/png'); res.set('Cache-Control', 'public,max-age=7200,must-revalidate'); - var self = this; - - step( - function getAffectedTablesAndLastUpdatedTime() { - namedMapProvider.getAffectedTablesAndLastUpdatedTime(this); - }, - function sendResponse(err, result) { - req.profiler.done('affectedTables'); - if (err) { - global.logger.log('ERROR generating cache channel: ' + err); - } - if (!result || !!result.tables) { - // we increase cache control as we can invalidate it - res.set('Cache-Control', 'public,max-age=31536000'); - - var lastModifiedDate; - if (Number.isFinite(result.lastUpdatedTime)) { - lastModifiedDate = new Date(result.getLastUpdatedAt()); - } else { - lastModifiedDate = new Date(); - } - res.set('Last-Modified', lastModifiedDate.toUTCString()); - - res.set('X-Cache-Channel', result.getCacheChannel()); - if (result.tables.length > 0) { - self.surrogateKeysCache.tag(res, result); - } - } - res.status(200); - res.send(body); + this.setCacheChannel(req, res, (err) => { + if (err) { + global.logger.log('ERROR generating cache channel: ' + err); } - ); + + res.status(200); + res.send(body); + }); +}; + +NamedMapsController.prototype.setCacheChannel = function(req, res, callback) { + const { namedMapProvider } = res.locals; + + this.getAffectedTables(namedMapProvider, (err, result) => { + + req.profiler.done('affectedTables'); + if (err) { + global.logger.log('ERROR generating cache channel: ' + err); + } + if (!result || !!result.tables) { + // we increase cache control as we can invalidate it + res.set('Cache-Control', 'public,max-age=31536000'); + + var lastModifiedDate; + if (Number.isFinite(result.lastUpdatedTime)) { + lastModifiedDate = new Date(result.getLastUpdatedAt()); + } else { + lastModifiedDate = new Date(); + } + res.set('Last-Modified', lastModifiedDate.toUTCString()); + + res.set('X-Cache-Channel', result.getCacheChannel()); + if (result.tables.length > 0) { + this.surrogateKeysCache.tag(res, result); + } + } + + callback(); + }); +}; + +NamedMapsController.prototype.getAffectedTables = function(namedMapProvider, callback) { + namedMapProvider.getAffectedTablesAndLastUpdatedTime(callback); }; NamedMapsController.prototype.tile = function(req, res, next) { @@ -97,7 +110,7 @@ NamedMapsController.prototype.tile = function(req, res, next) { }, function getTile(err, _namedMapProvider) { assert.ifError(err); - namedMapProvider = _namedMapProvider; + res.locals.namedMapProvider = namedMapProvider = _namedMapProvider; self.tileBackend.getTile(namedMapProvider, req.params, this); }, function handleImage(err, tile, headers, stats) { @@ -106,7 +119,7 @@ NamedMapsController.prototype.tile = function(req, res, next) { err.label = 'NAMED_MAP_TILE'; next(err); } else { - self.sendResponse(req, res, tile, headers, namedMapProvider); + self.sendResponse(req, res, tile, headers); } } ); @@ -136,7 +149,7 @@ NamedMapsController.prototype.staticMap = function(req, res, next) { function prepareLayerVisibility(err, _namedMapProvider) { assert.ifError(err); - namedMapProvider = _namedMapProvider; + res.locals.namedMapProvider = namedMapProvider = _namedMapProvider; self.prepareLayerFilterFromPreviewLayers(cdbUser, req, res.locals, namedMapProvider, this); }, @@ -179,7 +192,7 @@ NamedMapsController.prototype.staticMap = function(req, res, next) { err.label = 'STATIC_VIZ_MAP'; next(err); } else { - self.sendResponse(req, res, image, headers, namedMapProvider); + self.sendResponse(req, res, image, headers); } } ); diff --git a/test/acceptance/ported/multilayer_error_cases.js b/test/acceptance/ported/multilayer_error_cases.js index 92ce0b1f..9efd7c7b 100644 --- a/test/acceptance/ported/multilayer_error_cases.js +++ b/test/acceptance/ported/multilayer_error_cases.js @@ -28,7 +28,7 @@ describe('multilayer error cases', function() { }, {}, function(res) { assert.equal(res.statusCode, 400, res.body); var parsedBody = JSON.parse(res.body); - assert.deepEqual(parsedBody.errors, ["layergroup POST data must be of type application/json"]); + assert.deepEqual(parsedBody.errors, ["POST data must be of type application/json"]); done(); }); });