diff --git a/lib/cartodb/controllers/layergroup.js b/lib/cartodb/controllers/layergroup.js index 3365e270..182fcff5 100644 --- a/lib/cartodb/controllers/layergroup.js +++ b/lib/cartodb/controllers/layergroup.js @@ -1,20 +1,22 @@ -var assert = require('assert'); -var step = require('step'); - -var cors = require('../middleware/cors'); -var userMiddleware = require('../middleware/user'); -var allowQueryParams = require('../middleware/allow-query-params'); -var vectorError = require('../middleware/vector-error'); - -var DataviewBackend = require('../backends/dataview'); -var AnalysisStatusBackend = require('../backends/analysis-status'); - -var MapStoreMapConfigProvider = require('../models/mapconfig/provider/map-store-provider'); - -var QueryTables = require('cartodb-query-tables'); +const cors = require('../middleware/cors'); +const userMiddleware = require('../middleware/user'); +const allowQueryParams = require('../middleware/allow-query-params'); +const vectorError = require('../middleware/vector-error'); +const DataviewBackend = require('../backends/dataview'); +const AnalysisStatusBackend = require('../backends/analysis-status'); +const MapStoreMapConfigProvider = require('../models/mapconfig/provider/map-store-provider'); +const QueryTables = require('cartodb-query-tables'); +const SUPPORTED_FORMATS = { + grid_json: true, + json_torque: true, + torque_json: true, + png: true, + png32: true, + mvt: true +}; /** - * @param {AuthApi} authApi + * @param {prepareContext} prepareContext * @param {PgConnection} pgConnection * @param {MapStore} mapStore * @param {TileBackend} tileBackend @@ -46,64 +48,119 @@ function LayergroupController(prepareContext, pgConnection, mapStore, tileBacken module.exports = LayergroupController; LayergroupController.prototype.register = function(app) { + const { base_url_mapconfig: basePath } = app; + app.get( - app.base_url_mapconfig + '/:token/:z/:x/:y@:scale_factor?x.:format', + `${basePath}/:token/:z/:x/:y@:scale_factor?x.:format`, cors(), userMiddleware(), this.prepareContext, - this.tile.bind(this), + createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi), + getTile(this.tileBackend, 'map_tile'), + setCacheControlHeader(), + setLastModifiedHeader(), + getAffectedTables(this.layergroupAffectedTables, this.pgConnection), + setCacheChannelHeader(), + setSurrogateKeyHeader(this.surrogateKeysCache), + incrementSuccessMetrics(global.statsClient), + sendResponse(), + incrementErrorMetrics(global.statsClient), + tileError(), vectorError() ); app.get( - app.base_url_mapconfig + '/:token/:z/:x/:y.:format', + `${basePath}/:token/:z/:x/:y.:format`, cors(), userMiddleware(), this.prepareContext, - this.tile.bind(this), + createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi), + getTile(this.tileBackend, 'map_tile'), + setCacheControlHeader(), + setLastModifiedHeader(), + getAffectedTables(this.layergroupAffectedTables, this.pgConnection), + setCacheChannelHeader(), + setSurrogateKeyHeader(this.surrogateKeysCache), + incrementSuccessMetrics(global.statsClient), + sendResponse(), + incrementErrorMetrics(global.statsClient), + tileError(), vectorError() ); app.get( - app.base_url_mapconfig + '/:token/:layer/:z/:x/:y.(:format)', + `${basePath}/:token/:layer/:z/:x/:y.(:format)`, + distinguishLayergroupFromStaticRoute(), cors(), userMiddleware(), - validateLayerRouteMiddleware, this.prepareContext, - this.layer.bind(this), + createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi), + getTile(this.tileBackend, 'maplayer_tile'), + setCacheControlHeader(), + setLastModifiedHeader(), + getAffectedTables(this.layergroupAffectedTables, this.pgConnection), + setCacheChannelHeader(), + setSurrogateKeyHeader(this.surrogateKeysCache), + incrementSuccessMetrics(global.statsClient), + sendResponse(), + incrementErrorMetrics(global.statsClient), + tileError(), vectorError() ); app.get( - app.base_url_mapconfig + '/:token/:layer/attributes/:fid', + `${basePath}/:token/:layer/attributes/:fid`, cors(), userMiddleware(), this.prepareContext, - this.attributes.bind(this) + createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi), + getFeatureAttributes(this.attributesBackend), + setCacheControlHeader(), + setLastModifiedHeader(), + getAffectedTables(this.layergroupAffectedTables, this.pgConnection), + setCacheChannelHeader(), + setSurrogateKeyHeader(this.surrogateKeysCache), + sendResponse() ); + const forcedFormat = 'png'; + app.get( - app.base_url_mapconfig + '/static/center/:token/:z/:lat/:lng/:width/:height.:format', + `${basePath}/static/center/:token/:z/:lat/:lng/:width/:height.:format`, cors(), userMiddleware(), allowQueryParams(['layer']), this.prepareContext, - this.center.bind(this) + createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi, forcedFormat), + getPreviewImageByCenter(this.previewBackend), + setCacheControlHeader(), + setLastModifiedHeader(), + getAffectedTables(this.layergroupAffectedTables, this.pgConnection), + setCacheChannelHeader(), + setSurrogateKeyHeader(this.surrogateKeysCache), + sendResponse() ); app.get( - app.base_url_mapconfig + '/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', + `${basePath}/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format`, cors(), userMiddleware(), allowQueryParams(['layer']), this.prepareContext, - this.bbox.bind(this) + createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi, forcedFormat), + getPreviewImageByBoundingBox(this.previewBackend), + setCacheControlHeader(), + setLastModifiedHeader(), + getAffectedTables(this.layergroupAffectedTables, this.pgConnection), + setCacheChannelHeader(), + setSurrogateKeyHeader(this.surrogateKeysCache), + sendResponse() ); // Undocumented/non-supported API endpoint methods. // Use at your own peril. - var allowedDataviewQueryParams = [ + const allowedDataviewQueryParams = [ 'filters', // json 'own_filter', // 0, 1 'no_filters', // 0, 1 @@ -119,395 +176,459 @@ LayergroupController.prototype.register = function(app) { ]; app.get( - app.base_url_mapconfig + '/:token/dataview/:dataviewName', + `${basePath}/:token/dataview/:dataviewName`, cors(), userMiddleware(), allowQueryParams(allowedDataviewQueryParams), this.prepareContext, - this.dataview.bind(this) + createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi), + getDataview(this.dataviewBackend), + setCacheControlHeader(), + setLastModifiedHeader(), + getAffectedTables(this.layergroupAffectedTables, this.pgConnection), + setCacheChannelHeader(), + setSurrogateKeyHeader(this.surrogateKeysCache), + sendResponse() ); app.get( - app.base_url_mapconfig + '/:token/:layer/widget/:dataviewName', + `${basePath}/:token/:layer/widget/:dataviewName`, cors(), userMiddleware(), allowQueryParams(allowedDataviewQueryParams), this.prepareContext, - this.dataview.bind(this) + createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi), + getDataview(this.dataviewBackend), + setCacheControlHeader(), + setLastModifiedHeader(), + getAffectedTables(this.layergroupAffectedTables, this.pgConnection), + setCacheChannelHeader(), + setSurrogateKeyHeader(this.surrogateKeysCache), + sendResponse() ); app.get( - app.base_url_mapconfig + '/:token/dataview/:dataviewName/search', + `${basePath}/:token/dataview/:dataviewName/search`, cors(), userMiddleware(), allowQueryParams(allowedDataviewQueryParams), this.prepareContext, - this.dataviewSearch.bind(this) + createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi), + dataviewSearch(this.dataviewBackend), + setCacheControlHeader(), + setLastModifiedHeader(), + getAffectedTables(this.layergroupAffectedTables, this.pgConnection), + setCacheChannelHeader(), + setSurrogateKeyHeader(this.surrogateKeysCache), + sendResponse() ); app.get( - app.base_url_mapconfig + '/:token/:layer/widget/:dataviewName/search', + `${basePath}/:token/:layer/widget/:dataviewName/search`, cors(), userMiddleware(), allowQueryParams(allowedDataviewQueryParams), this.prepareContext, - this.dataviewSearch.bind(this) + createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi), + dataviewSearch(this.dataviewBackend), + setCacheControlHeader(), + setLastModifiedHeader(), + getAffectedTables(this.layergroupAffectedTables, this.pgConnection), + setCacheChannelHeader(), + setSurrogateKeyHeader(this.surrogateKeysCache), + sendResponse() ); app.get( - app.base_url_mapconfig + '/:token/analysis/node/:nodeId', + `${basePath}/:token/analysis/node/:nodeId`, cors(), userMiddleware(), this.prepareContext, - this.analysisNodeStatus.bind(this) + analysisNodeStatus(this.analysisStatusBackend), + sendResponse() ); }; -LayergroupController.prototype.analysisNodeStatus = function(req, res, next) { - var self = this; +function distinguishLayergroupFromStaticRoute () { + return function distinguishLayergroupFromStaticRouteMiddleware(req, res, next) { + if (req.params.token === 'static') { + return next('route'); + } - step( - function retrieveNodeStatus() { - self.analysisStatusBackend.getNodeStatus(res.locals, this); - }, - function finish(err, nodeStatus, stats) { - req.profiler.add(stats || {}); + next(); + }; +} + +function analysisNodeStatus (analysisStatusBackend) { + return function analysisNodeStatusMiddleware(req, res, next) { + analysisStatusBackend.getNodeStatus(res.locals, (err, nodeStatus, stats = {}) => { + req.profiler.add(stats); if (err) { err.label = 'GET NODE STATUS'; - next(err); - } else { - self.sendResponse(req, res, nodeStatus, 200, { - 'Cache-Control': 'public,max-age=5', - 'Last-Modified': new Date().toUTCString() - }); + return next(err); } + + res.set({ + 'Cache-Control': 'public,max-age=5', + 'Last-Modified': new Date().toUTCString() + }); + + res.body = nodeStatus; + + next(); + }); + }; +} + +function getRequestParams(locals) { + const params = Object.assign({}, locals); + + delete params.mapConfigProvider; + delete params.allowedQueryParams; + + return params; +} + +function createMapStoreMapConfigProvider (mapStore, userLimitsApi, forcedFormat = null) { + return function createMapStoreMapConfigProviderMiddleware (req, res, next) { + const { user } = res.locals; + + const params = getRequestParams(res.locals); + + if (forcedFormat) { + params.format = forcedFormat; + params.layer = params.layer || 'all'; } - ); -}; -LayergroupController.prototype.dataview = function(req, res, next) { - var self = this; + const mapConfigProvider = new MapStoreMapConfigProvider(mapStore, user, userLimitsApi, params); - step( - function retrieveDataview() { - var mapConfigProvider = new MapStoreMapConfigProvider( - self.mapStore, res.locals.user, self.userLimitsApi, res.locals - ); - self.dataviewBackend.getDataview( - mapConfigProvider, - res.locals.user, - res.locals, - this - ); - }, - function finish(err, dataview, stats) { - req.profiler.add(stats || {}); + mapConfigProvider.getMapConfig((err, mapconfig) => { + if (err) { + return next(err); + } + + res.locals.mapConfigProvider = mapConfigProvider; + res.locals.mapconfig = mapconfig; + + next(); + }); + }; +} + +function getDataview (dataviewBackend) { + return function getDataviewMiddleware (req, res, next) { + const { user, mapConfigProvider } = res.locals; + const params = getRequestParams(res.locals); + + dataviewBackend.getDataview(mapConfigProvider, user, params, (err, dataview, stats = {}) => { + req.profiler.add(stats); if (err) { err.label = 'GET DATAVIEW'; - next(err); - } else { - self.sendResponse(req, res, dataview, 200); + return next(err); } - } - ); -}; -LayergroupController.prototype.dataviewSearch = function(req, res, next) { - var self = this; + res.body = dataview; - step( - function searchDataview() { - var mapConfigProvider = new MapStoreMapConfigProvider( - self.mapStore, res.locals.user, self.userLimitsApi, res.locals - ); - self.dataviewBackend.search(mapConfigProvider, res.locals.user, req.params.dataviewName, res.locals, this); - }, - function finish(err, searchResult, stats) { - req.profiler.add(stats || {}); + next(); + }); + }; +} + +function dataviewSearch (dataviewBackend) { + return function dataviewSearchMiddleware (req, res, next) { + const { user, dataviewName, mapConfigProvider } = res.locals; + const params = getRequestParams(res.locals); + + dataviewBackend.search(mapConfigProvider, user, dataviewName, params, (err, searchResult, stats = {}) => { + req.profiler.add(stats); if (err) { err.label = 'GET DATAVIEW SEARCH'; - next(err); - } else { - self.sendResponse(req, res, searchResult, 200); + return next(err); } - } - ); -}; + res.body = searchResult; -LayergroupController.prototype.attributes = function(req, res, next) { - var self = this; + next(); + }); + }; +} - req.profiler.start('windshaft.maplayer_attribute'); +function getFeatureAttributes (attributesBackend) { + return function getFeatureAttributesMiddleware (req, res, next) { + req.profiler.start('windshaft.maplayer_attribute'); - step( - function retrieveFeatureAttributes() { - var mapConfigProvider = new MapStoreMapConfigProvider( - self.mapStore, res.locals.user, self.userLimitsApi, res.locals - ); - self.attributesBackend.getFeatureAttributes(mapConfigProvider, res.locals, false, this); - }, - function finish(err, tile, stats) { - req.profiler.add(stats || {}); + const { mapConfigProvider } = res.locals; + const params = getRequestParams(res.locals); + + attributesBackend.getFeatureAttributes(mapConfigProvider, params, false, (err, tile, stats = {}) => { + req.profiler.add(stats); if (err) { err.label = 'GET ATTRIBUTES'; - next(err); - } else { - self.sendResponse(req, res, tile, 200); + return next(err); } - } - ); -}; + res.body = tile; -// Gets a tile for a given token and set of tile ZXY coords. (OSM style) -LayergroupController.prototype.tile = function(req, res, next) { - req.profiler.start('windshaft.map_tile'); - this.tileOrLayer(req, res, next); -}; - -// Gets a tile for a given token, layer set of tile ZXY coords. (OSM style) -LayergroupController.prototype.layer = function(req, res, next) { - req.profiler.start('windshaft.maplayer_tile'); - this.tileOrLayer(req, res, next); -}; - -LayergroupController.prototype.tileOrLayer = function (req, res, next) { - var self = this; - - step( - function mapController$getTileOrGrid() { - self.tileBackend.getTile( - new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals), - res.locals, this - ); - }, - function mapController$finalize(err, tile, headers, stats) { - req.profiler.add(stats); - self.finalizeGetTileOrGrid(err, req, res, tile, headers, next); - } - ); -}; - -function getStatusCode(tile, format){ - return tile.length===0 && format==='mvt'? 204:200; + next(); + }); + }; } -// This function is meant for being called as the very last -// step by all endpoints serving tiles or grids -LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers, next) { - var supportedFormats = { - grid_json: true, - json_torque: true, - torque_json: true, - png: true, - png32: true, - mvt: true - }; +function getStatusCode(tile, format){ + return tile.length === 0 && format === 'mvt'? 204 : 200; +} - var formatStat = 'invalid'; - if (req.params.format) { - var format = req.params.format.replace('.', '_'); - if (supportedFormats[format]) { - formatStat = format; - } - } +function parseFormat (format = '') { + const prettyFormat = format.replace('.', '_'); + return SUPPORTED_FORMATS[prettyFormat] ? prettyFormat : 'invalid'; +} - if (err) { - // See https://github.com/Vizzuality/Windshaft-cartodb/issues/68 - var errMsg = err.message ? ( '' + err.message ) : ( '' + err ); +function getTile (tileBackend, profileLabel = 'tile') { + return function getTileMiddleware (req, res, next) { + req.profiler.start(`windshaft.${profileLabel}`); - // Rewrite mapnik parsing errors to start with layer number - var matches = errMsg.match("(.*) in style 'layer([0-9]+)'"); - if (matches) { - errMsg = 'style'+matches[2]+': ' + matches[1]; - } - err.message = errMsg; + const { mapConfigProvider } = res.locals; + const params = getRequestParams(res.locals); - err.label = 'TILE RENDER'; - next(err); - - global.statsClient.increment('windshaft.tiles.error'); - global.statsClient.increment('windshaft.tiles.' + formatStat + '.error'); - } else { - this.sendResponse(req, res, tile, getStatusCode(tile, formatStat), headers); - global.statsClient.increment('windshaft.tiles.success'); - global.statsClient.increment('windshaft.tiles.' + formatStat + '.success'); - } -}; - -LayergroupController.prototype.bbox = function(req, res, next) { - this.staticMap(req, res, +req.params.width, +req.params.height, { - west: +req.params.west, - north: +req.params.north, - east: +req.params.east, - south: +req.params.south - }, null, next); -}; - -LayergroupController.prototype.center = function(req, res, next) { - this.staticMap(req, res, +req.params.width, +req.params.height, +req.params.z, { - lng: +req.params.lng, - lat: +req.params.lat - }, next); -}; - -LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center, next) { - var format = req.params.format === 'jpg' ? 'jpeg' : 'png'; - // We force always the tile to be generated using PNG because - // is the only format we support by now - res.locals.format = 'png'; - res.locals.layer = res.locals.layer || 'all'; - - var self = this; - - step( - function getImage() { - if (center) { - self.previewBackend.getImage( - new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals), - format, width, height, zoom, center, this); - } else { - self.previewBackend.getImage( - new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals), - format, width, height, zoom /* bounds */, this); - } - }, - function handleImage(err, image, headers, stats) { - req.profiler.done('render-' + format); - req.profiler.add(stats || {}); + tileBackend.getTile(mapConfigProvider, params, (err, tile, headers, stats = {}) => { + req.profiler.add(stats); if (err) { - err.label = 'STATIC_MAP'; - next(err); - } else { - res.set('Content-Type', headers['Content-Type'] || 'image/' + format); - self.sendResponse(req, res, image, 200); - } - } - ); -}; - -LayergroupController.prototype.sendResponse = function(req, res, body, status, headers) { - var self = this; - - req.profiler.done('res'); - - res.set('Cache-Control', 'public,max-age=31536000'); - - // Set Last-Modified header - var lastUpdated; - if (res.locals.cache_buster) { - // Assuming cache_buster is a timestamp - lastUpdated = new Date(parseInt(res.locals.cache_buster)); - } else { - lastUpdated = new Date(); - } - 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); + return next(err); } if (headers) { res.set(headers); } - res.status(status); + const formatStat = parseFormat(req.params.format); - if (!Buffer.isBuffer(body) && typeof body === 'object') { - if (req.query && req.query.callback) { - res.jsonp(body); - } else { - res.json(body); - } - } else { - res.send(body); - } - } - ); -}; + res.statusCode = getStatusCode(tile, formatStat); + res.body = tile; -LayergroupController.prototype.getAffectedTables = function(user, dbName, layergroupId, callback) { - - if (this.layergroupAffectedTables.hasAffectedTables(dbName, layergroupId)) { - return callback(null, this.layergroupAffectedTables.get(dbName, layergroupId)); - } - - var self = this; - step( - function extractSQL() { - step( - function loadFromStore() { - self.mapStore.load(layergroupId, this); - }, - function getSQL(err, mapConfig) { - assert.ifError(err); - - var queries = []; - mapConfig.getLayers().forEach(function(layer) { - queries.push(layer.options.sql); - if (layer.options.affected_tables) { - layer.options.affected_tables.map(function(table) { - queries.push('SELECT * FROM ' + table + ' LIMIT 0'); - }); - } - }); - - return queries.length ? queries.join(';') : null; - }, - this - ); - }, - function findAffectedTables(err, sql) { - assert.ifError(err); - - if ( ! sql ) { - throw new Error("this request doesn't need an X-Cache-Channel generated"); - } - - step( - function getConnection() { - self.pgConnection.getConnection(user, this); - }, - function getAffectedTables(err, connection) { - assert.ifError(err); - - QueryTables.getAffectedTablesFromQuery(connection, sql, this); - }, - this - ); - }, - function buildCacheChannel(err, tables) { - assert.ifError(err); - self.layergroupAffectedTables.set(dbName, layergroupId, tables); - - return tables; - }, - callback - ); -}; - - -function validateLayerRouteMiddleware(req, res, next) { - if (req.params.token === 'static') { - return next('route'); - } - - next(); + next(); + }); + }; +} + +function getPreviewImageByCenter (previewBackend) { + return function getPreviewImageByCenterMiddleware (req, res, next) { + const width = +req.params.width; + const height = +req.params.height; + const zoom = +req.params.z; + const center = { + lng: +req.params.lng, + lat: +req.params.lat + }; + + const format = req.params.format === 'jpg' ? 'jpeg' : 'png'; + const { mapConfigProvider: provider } = res.locals; + + previewBackend.getImage(provider, format, width, height, zoom, center, (err, image, headers, stats = {}) => { + req.profiler.done(`render-${format}`); + req.profiler.add(stats); + + if (err) { + err.label = 'STATIC_MAP'; + return next(err); + } + + if (headers) { + res.set(headers); + } + + res.set('Content-Type', headers['Content-Type'] || `image/${format}`); + + res.body = image; + + next(); + }); + }; +} + +function getPreviewImageByBoundingBox (previewBackend) { + return function getPreviewImageByBoundingBoxMiddleware (req, res, next) { + const width = +req.params.width; + const height = +req.params.height; + const bounds = { + west: +req.params.west, + north: +req.params.north, + east: +req.params.east, + south: +req.params.south + }; + const format = req.params.format === 'jpg' ? 'jpeg' : 'png'; + const { mapConfigProvider: provider } = res.locals; + + previewBackend.getImage(provider, format, width, height, bounds, (err, image, headers, stats = {}) => { + req.profiler.done(`render-${format}`); + req.profiler.add(stats); + + if (err) { + err.label = 'STATIC_MAP'; + return next(err); + } + + if (headers) { + res.set(headers); + } + + res.set('Content-Type', headers['Content-Type'] || `image/${format}`); + + res.body = image; + + next(); + }); + }; +} + +function setLastModifiedHeader () { + return function setLastModifiedHeaderMiddleware (req, res, next) { + let { cache_buster: cacheBuster } = res.locals; + + cacheBuster = parseInt(cacheBuster, 10); + + const lastUpdated = res.locals.cache_buster ? new Date(cacheBuster) : new Date(); + + res.set('Last-Modified', lastUpdated.toUTCString()); + + next(); + }; +} + +function setCacheControlHeader () { + return function setCacheControlHeaderMiddleware (req, res, next) { + res.set('Cache-Control', 'public,max-age=31536000'); + + next(); + }; +} + +function getAffectedTables (layergroupAffectedTables, pgConnection) { + return function getAffectedTablesMiddleware (req, res, next) { + const { user, dbname, token, mapconfig } = res.locals; + + if (layergroupAffectedTables.hasAffectedTables(dbname, token)) { + res.locals.affectedTables = layergroupAffectedTables.get(dbname, token); + return next(); + } + + pgConnection.getConnection(user, (err, connection) => { + if (err) { + global.logger.warn('ERROR generating cache channel:', err); + return next(); + } + + 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) => { + req.profiler.done('getAffectedTablesFromQuery'); + if (err) { + global.logger.warn('ERROR generating cache channel: ', err); + return next(); + } + + // feed affected tables cache so it can be reused from, for instance, map controller + layergroupAffectedTables.set(dbname, token, affectedTables); + + res.locals.affectedTables = affectedTables; + + next(); + }); + }); + }; +} + +function setCacheChannelHeader () { + return function setCacheChannelHeaderMiddleware (req, res, next) { + const { affectedTables } = res.locals; + + if (affectedTables) { + res.set('X-Cache-Channel', affectedTables.getCacheChannel()); + } + + next(); + }; +} + +function setSurrogateKeyHeader (surrogateKeysCache) { + return function setSurrogateKeyHeaderMiddleware (req, res, next) { + const { affectedTables } = res.locals; + + if (affectedTables) { + surrogateKeysCache.tag(res, affectedTables); + } + + next(); + }; +} + +function incrementSuccessMetrics (statsClient) { + return function incrementSuccessMetricsMiddleware (req, res, next) { + const formatStat = parseFormat(req.params.format); + + statsClient.increment('windshaft.tiles.success'); + statsClient.increment(`windshaft.tiles.${formatStat}.success`); + + next(); + }; +} + +function sendResponse () { + return function sendResponseMiddleware (req, res) { + req.profiler.done('res'); + + res.status(res.statusCode || 200); + + if (!Buffer.isBuffer(res.body) && typeof res.body === 'object') { + if (req.query && req.query.callback) { + res.jsonp(res.body); + } else { + res.json(res.body); + } + } else { + res.send(res.body); + } + }; +} + +function incrementErrorMetrics (statsClient) { + return function incrementErrorMetricsMiddleware (err, req, res, next) { + const formatStat = parseFormat(req.params.format); + + statsClient.increment('windshaft.tiles.error'); + statsClient.increment(`windshaft.tiles.${formatStat}.error`); + + next(err); + }; +} + +function tileError () { + return function tileErrorMiddleware (err, req, res, next) { + // See https://github.com/Vizzuality/Windshaft-cartodb/issues/68 + let errMsg = err.message ? ( '' + err.message ) : ( '' + err ); + + // Rewrite mapnik parsing errors to start with layer number + const matches = errMsg.match("(.*) in style 'layer([0-9]+)'"); + + if (matches) { + errMsg = `style${matches[2]}: ${matches[1]}`; + } + + err.message = errMsg; + err.label = 'TILE RENDER'; + + next(err); + }; } diff --git a/test/unit/cartodb/ported/tile_stats.test.js b/test/unit/cartodb/ported/tile_stats.test.js deleted file mode 100644 index a2d30cd0..00000000 --- a/test/unit/cartodb/ported/tile_stats.test.js +++ /dev/null @@ -1,90 +0,0 @@ -require('../../../support/test_helper.js'); - -var assert = require('assert'); - -var LayergroupController = require('../../../../lib/cartodb/controllers/layergroup'); - -describe('tile stats', function() { - - beforeEach(function () { - this.statsClient = global.statsClient; - }); - - afterEach(function() { - global.statsClient = this.statsClient; - }); - - it('finalizeGetTileOrGrid does not call statsClient when format is not supported', function() { - var expectedCalls = 2, // it will call increment once for the general error - invalidFormat = 'png2', - invalidFormatRegexp = new RegExp('invalid'), - formatMatched = false; - mockStatsClientGetInstance({ - increment: function(label) { - formatMatched = formatMatched || !!label.match(invalidFormatRegexp); - expectedCalls--; - } - }); - - var layergroupController = new LayergroupController(); - - var reqMock = { - profiler: { toJSONString:function() {} }, - params: { - format: invalidFormat - } - }; - var resMock = { - status: function() { return this; }, - set: function() {}, - json: function() {}, - jsonp: function() {}, - send: function() {} - }; - - var next = function () {}; - layergroupController.finalizeGetTileOrGrid('Unsupported format png2', reqMock, resMock, null, null, next); - - assert.ok(formatMatched, 'Format was never matched in increment method'); - assert.equal(expectedCalls, 0, 'Unexpected number of calls to increment method'); - }); - - it('finalizeGetTileOrGrid calls statsClient when format is supported', function() { - var expectedCalls = 2, // general error + format error - validFormat = 'png', - validFormatRegexp = new RegExp(validFormat), - formatMatched = false; - mockStatsClientGetInstance({ - increment: function(label) { - formatMatched = formatMatched || !!label.match(validFormatRegexp); - expectedCalls--; - } - }); - var reqMock = { - profiler: { toJSONString:function() {} }, - params: { - format: validFormat - } - }; - var resMock = { - status: function() { return this; }, - set: function() {}, - json: function() {}, - jsonp: function() {}, - send: function() {} - }; - - var layergroupController = new LayergroupController(); - - var next = function () {}; - layergroupController.finalizeGetTileOrGrid('Another error happened', reqMock, resMock, null, null, next); - - assert.ok(formatMatched, 'Format was never matched in increment method'); - assert.equal(expectedCalls, 0, 'Unexpected number of calls to increment method'); - }); - - function mockStatsClientGetInstance(instance) { - global.statsClient = Object.assign(global.statsClient, instance); - } - -});