diff --git a/lib/cartodb/controllers/map.js b/lib/cartodb/controllers/map.js index 2d26cf5d..ea443253 100644 --- a/lib/cartodb/controllers/map.js +++ b/lib/cartodb/controllers/map.js @@ -74,356 +74,386 @@ MapController.prototype.composeCreateMapMiddleware = function ({ useTemplate = f cors(), userMiddleware, this.prepareContext, - useTemplate ? checkIntantiteLayergroup : checkCreateLayergroup, - useTemplate ? this.getTemplate.bind(this) : this.prepareAdapterMapConfig.bind(this), - useTemplate ? this.instantiateLayergroup.bind(this) : this.createLayergroup.bind(this), - this.incrementMapViewCount.bind(this), - this.augmentLayergroupData, - this.getAffectedTables.bind(this), - this.setCacheChannel, - this.setLastUpdatedTimeToLayergroup, - this.setCacheControl, - this.setLayerStats.bind(this), + useTemplate ? checkIntantiteLayergroup() : checkCreateLayergroup(), + useTemplate ? this.getTemplate() : this.prepareAdapterMapConfig(), + useTemplate ? this.instantiateLayergroup() : this.createLayergroup(), + this.incrementMapViewCount(), + this.augmentLayergroupData(), + this.getAffectedTables(), + this.setCacheChannel(), + this.setLastUpdatedTimeToLayergroup(), + this.setCacheControl(), + this.setLayerStats(), this.setLayergroupIdHeaderBuilder(useTemplateHash), - this.setDataviewsAndWidgetsUrlsToLayergroupMetadata.bind(this), - this.setAnalysesMetadataToLayergroupBuilder(includeQuery), - this.setTurboCartoMetadataToLayergroup, - this.setSurrogateKeyHeader.bind(this), - sendResponse, + this.setDataviewsAndWidgetsUrlsToLayergroupMetadata(), + this.setAnalysesMetadataToLayergroup(includeQuery), + this.setTurboCartoMetadataToLayergroup(), + this.setSurrogateKeyHeader(), + sendResponse(), augmentError({ label, addContext }) ]; }; -function checkCreateLayergroup (req, res, next) { - req.profiler.start(`windshaft.createmap_${req.method.toLowerCase()}`); - if (req.method === 'POST' && !req.is('application/json')) { - return next(new Error('layergroup POST data must be of type application/json')); - } +function checkCreateLayergroup () { + return function checkCreateLayergroupMiddleware (req, res, next) { + req.profiler.start(`windshaft.createmap_${req.method.toLowerCase()}`); - if (req.method === 'GET') { - const { config } = res.locals; - - if (!config) { - return next(new Error('layergroup GET needs a "config" parameter')); + if (req.method === 'POST' && !req.is('application/json')) { + return next(new Error('layergroup POST data must be of type application/json')); } - try { - req.body = JSON.parse(config); - } catch (err) { - return next(err); - } - } + if (req.method === 'GET') { + const { config } = res.locals; - return next(); -} + if (!config) { + return next(new Error('layergroup GET needs a "config" parameter')); + } -function checkIntantiteLayergroup(req, res, next) { - // jshint maxcomplexity: 7 - req.profiler.start(`windshaft-cartodb.instance_template_${req.method.toLowerCase()}`); - - if (req.method === 'POST' && !req.is('application/json')) { - return next(new Error('Template POST data must be of type application/json')); - } - - if (req.method === 'GET') { - const { callback, config } = req.query; - - if (callback === undefined || callback.length === 0) { - return next(new Error('callback parameter should be present and be a function name')); - } - - if (config) { try { req.body = JSON.parse(config); - } catch(e) { - return next(new Error('Invalid config parameter, should be a valid JSON')); + } catch (err) { + return next(err); } } - } - return next(); -} - -MapController.prototype.prepareAdapterMapConfig = function (req, res, next) { - const requestMapConfig = req.body; - const { user, dbhost, dbport, dbname, dbuser, dbpassword, api_key } = res.locals; - - const context = { - analysisConfiguration: { - user, - db: { - host: dbhost, - port: dbport, - dbname: dbname, - user: dbuser, - pass: dbpassword - }, - batch: { - username: user, - apiKey: api_key - } - } + return next(); }; - - this.mapConfigAdapter.getMapConfig(user, requestMapConfig, res.locals, context, (err, requestMapConfig) => { - if (err) { - return next(err); - } - - req.body = requestMapConfig; - res.locals.context = context; - - next(); - }); -}; - -MapController.prototype.createLayergroup = function(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(); - }); -}; - -MapController.prototype.getTemplate = function(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) { - return next(err); - } - - res.locals.mapconfig = mapconfig; - res.locals.rendererParams = rendererParams; - res.locals.mapconfigProvider = mapconfigProvider; - - next(); - }); -}; - -MapController.prototype.instantiateLayergroup = function(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; - - // TODO: Do not provide shortcuts - 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(); - }); -}; - -function sendResponse (req, res) { - const { layergroup } = res.locals; - - res.status(200); - - if (req.query && req.query.callback) { - res.jsonp(layergroup); - } else { - res.json(layergroup); - } } -MapController.prototype.incrementMapViewCount = function (req, res, callback) { - const { mapconfig, user } = res.locals; +function checkIntantiteLayergroup () { + return function checkIntantiteLayergroupMiddleware(req, res, next) { + // jshint maxcomplexity: 7 + req.profiler.start(`windshaft-cartodb.instance_template_${req.method.toLowerCase()}`); - // 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}`); + if (req.method === 'POST' && !req.is('application/json')) { + return next(new Error('Template POST data must be of type application/json')); } - callback(); - }); -}; + if (req.method === 'GET') { + const { callback, config } = req.query; -MapController.prototype.augmentLayergroupData = function (req, res, callback) { - 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); - - callback(); -}; - -MapController.prototype.getAffectedTables = function (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'); - }); + if (callback === undefined || callback.length === 0) { + return next(new Error('callback parameter should be present and be a function name')); } - }); - QueryTables.getAffectedTablesFromQuery(connection, sql.join(';'), (err, affectedTables) => { + if (config) { + try { + req.body = JSON.parse(config); + } catch(e) { + return next(new Error('Invalid config parameter, should be a valid JSON')); + } + } + } + + return next(); + }; +} + +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; + + const context = { + analysisConfiguration: { + user, + db: { + host: dbhost, + port: dbport, + dbname: dbname, + user: dbuser, + pass: dbpassword + }, + batch: { + username: user, + apiKey: api_key + } + } + }; + + this.mapConfigAdapter.getMapConfig(user, requestMapConfig, res.locals, context, (err, requestMapConfig) => { if (err) { return next(err); } - // 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; + req.body = requestMapConfig; + res.locals.context = context; next(); }); - }); + }.bind(this); }; -MapController.prototype.setCacheChannel = function (req, res, callback) { - const { affectedTables } = res.locals; +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); - if (req.method === 'GET') { - res.set('Last-Modified', (new Date()).toUTCString()); - res.set('X-Cache-Channel', affectedTables.getCacheChannel()); - } + res.locals.mapconfig = mapconfig; + res.locals.analysesResults = context.analysesResults; - callback(); + this.mapBackend.createLayergroup(mapconfig, res.locals, mapconfigProvider, (err, layergroup) => { + if (err) { + return next(err); + } + + res.locals.layergroup = layergroup; + + next(); + }); + }.bind(this); }; -MapController.prototype.setLastUpdatedTimeToLayergroup = function (req, res, callback) { - const { affectedTables, layergroup, analysesResults } = res.locals; +MapController.prototype.getTemplate = function () { + return function getTemplateMiddleware (req, res, next) { + const templateParams = req.body; + const { user } = res.locals; - var lastUpdateTime = affectedTables.getLastUpdatedAt(); + 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 + ); - lastUpdateTime = getLastUpdatedTime(analysesResults, lastUpdateTime) || lastUpdateTime; + mapconfigProvider.getMapConfig((err, mapconfig, rendererParams) => { + if (err) { + return next(err); + } - // last update for layergroup cache buster - layergroup.layergroupid = layergroup.layergroupid + ':' + lastUpdateTime; - layergroup.last_updated = new Date(lastUpdateTime).toISOString(); + res.locals.mapconfig = mapconfig; + res.locals.rendererParams = rendererParams; + res.locals.mapconfigProvider = mapconfigProvider; - callback(); + next(); + }); + }.bind(this); }; -MapController.prototype.setCacheControl = function (req, res, callback) { - if (req.method === 'GET') { - var ttl = global.environment.varnish.layergroupTtl || 86400; - res.set('Cache-Control', 'public,max-age='+ttl+',must-revalidate'); - } +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); - callback(); + this.mapBackend.createLayergroup(mapconfig, rendererParams, mapconfigProvider, (err, layergroup) => { + if (err) { + return next(err); + } + + res.locals.layergroup = layergroup; + + // TODO: Do not provide shortcuts + 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.setLayerStats = function (req, res, callback) { - const { user, mapconfig, layergroup } = res.locals; +function sendResponse() { + return function sendResponseMiddleware (req, res) { + const { layergroup } = res.locals; - this.pgConnection.getConnection(user, (err, connection) => { - if (err) { - return callback(err); + res.status(200); + + if (req.query && req.query.callback) { + res.jsonp(layergroup); + } else { + res.json(layergroup); + } + }; +} + +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); + } + + // 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(); + }); + }); + }.bind(this); +}; + +MapController.prototype.setCacheChannel = function () { + return function setCacheChannelMiddleware (req, res, next) { + const { affectedTables } = res.locals; + + if (req.method === 'GET') { + res.set('Last-Modified', (new Date()).toUTCString()); + res.set('X-Cache-Channel', affectedTables.getCacheChannel()); } - this.statsBackend.getStats(mapconfig, connection, function(err, layersStats) { + 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(); + }; +}; + +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'); + } + + 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 callback(err); + return next(err); } - if (layersStats.length > 0) { - layergroup.metadata.layers.forEach(function (layer, index) { - layer.meta.stats = layersStats[index]; - }); - } + this.statsBackend.getStats(mapconfig, connection, function(err, layersStats) { + if (err) { + return next(err); + } - callback(); + if (layersStats.length > 0) { + layergroup.metadata.layers.forEach(function (layer, index) { + layer.meta.stats = layersStats[index]; + }); + } + + next(); + }); }); - }); + }.bind(this); }; MapController.prototype.setLayergroupIdHeaderBuilder = function (useTemplateHash) { - const self = this; - return function setLayergroupIdHeader(req, res, callback) { + return function setLayergroupIdHeaderMiddleware (req, res, next) { const { layergroup, user, template } = res.locals; if (useTemplateHash) { - var templateHash = self.templateMaps.fingerPrint(template).substring(0, 8); + var templateHash = this.templateMaps.fingerPrint(template).substring(0, 8); layergroup.layergroupid = `${user}@${templateHash}@${layergroup.layergroupid}`; } res.set('X-Layergroup-Id', layergroup.layergroupid); - callback(); - }; + next(); + }.bind(); }; -MapController.prototype.setDataviewsAndWidgetsUrlsToLayergroupMetadata = function (req, res, callback) { - const { layergroup, user, mapconfig } = res.locals; +MapController.prototype.setDataviewsAndWidgetsUrlsToLayergroupMetadata = function () { + return function setDataviewsAndWidgetsUrlsToLayergroupMetadataMiddleware (req, res, next) { + const { layergroup, user, mapconfig } = res.locals; - this.addDataviewsAndWidgetsUrls(user, layergroup, mapconfig.obj()); + this.addDataviewsAndWidgetsUrls(user, layergroup, mapconfig.obj()); - callback(); + next(); + }.bind(this); }; -MapController.prototype.setAnalysesMetadataToLayergroupBuilder = function (includeQuery) { - const self = this; - - return function setAnalysesMetadataToLayergroup (req, res, callback) { +MapController.prototype.setAnalysesMetadataToLayergroup = function (includeQuery) { + return function setAnalysesMetadataToLayergroupMiddleware (req, res, next) { const { layergroup, user, analysesResults = [] } = res.locals; - self.addAnalysesMetadata(user, layergroup, analysesResults, includeQuery); + this.addAnalysesMetadata(user, layergroup, analysesResults, includeQuery); - callback(); - }; + next(); + }.bind(this); }; -MapController.prototype.setTurboCartoMetadataToLayergroup = function (req, res, callback) { - const { layergroup, mapconfig, context } = res.locals; +MapController.prototype.setTurboCartoMetadataToLayergroup = function () { + return function (req, res, next) { + const { layergroup, mapconfig, context } = res.locals; - addContextMetadata(layergroup, mapconfig.obj(), context); + addContextMetadata(layergroup, mapconfig.obj(), context); - callback(); + next(); + }; }; function addContextMetadata(layergroup, mapConfig, context) { @@ -437,18 +467,20 @@ function addContextMetadata(layergroup, mapConfig, context) { } } -MapController.prototype.setSurrogateKeyHeader = function (req, res, callback) { - const { affectedTables, user, templateName } = res.locals; +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 (req.method === 'GET' && affectedTables.tables && affectedTables.tables.length > 0) { + this.surrogateKeysCache.tag(res, affectedTables); + } - if (templateName) { - this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, templateName)); - } + if (templateName) { + this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, templateName)); + } - callback(); + next(); + }.bind(this); }; function getLastUpdatedTime(analysesResults, lastUpdateTime) { @@ -534,7 +566,7 @@ MapController.prototype.addWidgetsUrl = function(username, layergroup, mapConfig function augmentError (options) { const { addContext = false, label = 'MAPS CONTROLLER' } = options; - return function mapError (err, req, res, next) { + return function augmentErrorMiddleware (err, req, res, next) { const { mapconfig } = res.locals; if (addContext) {