Merge pull request #915 from CartoDB/unify-headers-middlewared

Unify headers middlewares
This commit is contained in:
Daniel 2018-03-27 12:38:23 +02:00 committed by GitHub
commit f2a7953d9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 593 additions and 489 deletions

View File

@ -6,12 +6,20 @@ var queue = require('queue-async');
var LruCache = require("lru-cache"); var LruCache = require("lru-cache");
function NamedMapProviderCache(templateMaps, pgConnection, metadataBackend, userLimitsApi, mapConfigAdapter) { function NamedMapProviderCache(
templateMaps,
pgConnection,
metadataBackend,
userLimitsApi,
mapConfigAdapter,
affectedTablesCache
) {
this.templateMaps = templateMaps; this.templateMaps = templateMaps;
this.pgConnection = pgConnection; this.pgConnection = pgConnection;
this.metadataBackend = metadataBackend; this.metadataBackend = metadataBackend;
this.userLimitsApi = userLimitsApi; this.userLimitsApi = userLimitsApi;
this.mapConfigAdapter = mapConfigAdapter; this.mapConfigAdapter = mapConfigAdapter;
this.affectedTablesCache = affectedTablesCache;
this.providerCache = new LruCache({ max: 2000 }); this.providerCache = new LruCache({ max: 2000 });
} }
@ -30,6 +38,7 @@ NamedMapProviderCache.prototype.get = function(user, templateId, config, authTok
this.metadataBackend, this.metadataBackend,
this.userLimitsApi, this.userLimitsApi,
this.mapConfigAdapter, this.mapConfigAdapter,
this.affectedTablesCache,
user, user,
templateId, templateId,
config, config,

View File

@ -9,6 +9,7 @@ const authorize = require('../middleware/authorize');
const dbConnSetup = require('../middleware/db-conn-setup'); const dbConnSetup = require('../middleware/db-conn-setup');
const rateLimit = require('../middleware/rate-limit'); const rateLimit = require('../middleware/rate-limit');
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit; const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
const cacheControlHeader = require('../middleware/cache-control-header');
const sendResponse = require('../middleware/send-response'); const sendResponse = require('../middleware/send-response');
function AnalysesController(pgConnection, authApi, userLimitsApi) { function AnalysesController(pgConnection, authApi, userLimitsApi) {
@ -37,7 +38,7 @@ AnalysesController.prototype.register = function (app) {
getDataFromQuery({ queryTemplate: catalogQueryTpl, key: 'catalog' }), getDataFromQuery({ queryTemplate: catalogQueryTpl, key: 'catalog' }),
getDataFromQuery({ queryTemplate: tablesQueryTpl, key: 'tables' }), getDataFromQuery({ queryTemplate: tablesQueryTpl, key: 'tables' }),
prepareResponse(), prepareResponse(),
setCacheControlHeader(), cacheControlHeader({ ttl: 10, revalidate: true }),
sendResponse(), sendResponse(),
unauthorizedError() unauthorizedError()
); );
@ -112,13 +113,6 @@ function prepareResponse () {
}; };
} }
function setCacheControlHeader () {
return function setCacheControlHeaderMiddleware (req, res, next) {
res.set('Cache-Control', 'public,max-age=10,must-revalidate');
next();
};
}
function unauthorizedError () { function unauthorizedError () {
return function unathorizedErrorMiddleware(err, req, res, next) { return function unathorizedErrorMiddleware(err, req, res, next) {
if (err.message.match(/permission\sdenied/)) { if (err.message.match(/permission\sdenied/)) {

View File

@ -9,11 +9,14 @@ const dbConnSetup = require('../middleware/db-conn-setup');
const authorize = require('../middleware/authorize'); const authorize = require('../middleware/authorize');
const rateLimit = require('../middleware/rate-limit'); const rateLimit = require('../middleware/rate-limit');
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit; const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
const cacheControlHeader = require('../middleware/cache-control-header');
const cacheChannelHeader = require('../middleware/cache-channel-header');
const surrogateKeyHeader = require('../middleware/surrogate-key-header');
const lastModifiedHeader = require('../middleware/last-modified-header');
const sendResponse = require('../middleware/send-response'); const sendResponse = require('../middleware/send-response');
const DataviewBackend = require('../backends/dataview'); const DataviewBackend = require('../backends/dataview');
const AnalysisStatusBackend = require('../backends/analysis-status'); const AnalysisStatusBackend = require('../backends/analysis-status');
const MapStoreMapConfigProvider = require('../models/mapconfig/provider/map-store-provider'); const MapStoreMapConfigProvider = require('../models/mapconfig/provider/map-store-provider');
const QueryTables = require('cartodb-query-tables');
const SUPPORTED_FORMATS = { const SUPPORTED_FORMATS = {
grid_json: true, grid_json: true,
json_torque: true, json_torque: true,
@ -44,7 +47,7 @@ function LayergroupController(
attributesBackend, attributesBackend,
surrogateKeysCache, surrogateKeysCache,
userLimitsApi, userLimitsApi,
layergroupAffectedTables, layergroupAffectedTablesCache,
analysisBackend, analysisBackend,
authApi authApi
) { ) {
@ -55,7 +58,7 @@ function LayergroupController(
this.attributesBackend = attributesBackend; this.attributesBackend = attributesBackend;
this.surrogateKeysCache = surrogateKeysCache; this.surrogateKeysCache = surrogateKeysCache;
this.userLimitsApi = userLimitsApi; this.userLimitsApi = userLimitsApi;
this.layergroupAffectedTables = layergroupAffectedTables; this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
this.dataviewBackend = new DataviewBackend(analysisBackend); this.dataviewBackend = new DataviewBackend(analysisBackend);
this.analysisStatusBackend = new AnalysisStatusBackend(); this.analysisStatusBackend = new AnalysisStatusBackend();
@ -65,10 +68,10 @@ function LayergroupController(
module.exports = LayergroupController; module.exports = LayergroupController;
LayergroupController.prototype.register = function(app) { LayergroupController.prototype.register = function(app) {
const { base_url_mapconfig: mapconfigBasePath } = app; const { base_url_mapconfig: mapConfigBasePath } = app;
app.get( app.get(
`${mapconfigBasePath}/:token/:z/:x/:y@:scale_factor?x.:format`, `${mapConfigBasePath}/:token/:z/:x/:y@:scale_factor?x.:format`,
cors(), cors(),
cleanUpQueryParams(), cleanUpQueryParams(),
locals(), locals(),
@ -78,13 +81,17 @@ LayergroupController.prototype.register = function(app) {
credentials(), credentials(),
authorize(this.authApi), authorize(this.authApi),
dbConnSetup(this.pgConnection), dbConnSetup(this.pgConnection),
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi), createMapStoreMapConfigProvider(
this.mapStore,
this.userLimitsApi,
this.pgConnection,
this.layergroupAffectedTablesCache
),
getTile(this.tileBackend, 'map_tile'), getTile(this.tileBackend, 'map_tile'),
setCacheControlHeader(), cacheControlHeader(),
setLastModifiedHeader(), cacheChannelHeader(),
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore), surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
setCacheChannelHeader(), lastModifiedHeader(),
setSurrogateKeyHeader(this.surrogateKeysCache),
incrementSuccessMetrics(global.statsClient), incrementSuccessMetrics(global.statsClient),
sendResponse(), sendResponse(),
incrementErrorMetrics(global.statsClient), incrementErrorMetrics(global.statsClient),
@ -93,7 +100,7 @@ LayergroupController.prototype.register = function(app) {
); );
app.get( app.get(
`${mapconfigBasePath}/:token/:z/:x/:y.:format`, `${mapConfigBasePath}/:token/:z/:x/:y.:format`,
cors(), cors(),
cleanUpQueryParams(), cleanUpQueryParams(),
locals(), locals(),
@ -103,13 +110,17 @@ LayergroupController.prototype.register = function(app) {
credentials(), credentials(),
authorize(this.authApi), authorize(this.authApi),
dbConnSetup(this.pgConnection), dbConnSetup(this.pgConnection),
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi), createMapStoreMapConfigProvider(
this.mapStore,
this.userLimitsApi,
this.pgConnection,
this.layergroupAffectedTablesCache
),
getTile(this.tileBackend, 'map_tile'), getTile(this.tileBackend, 'map_tile'),
setCacheControlHeader(), cacheControlHeader(),
setLastModifiedHeader(), cacheChannelHeader(),
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore), surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
setCacheChannelHeader(), lastModifiedHeader(),
setSurrogateKeyHeader(this.surrogateKeysCache),
incrementSuccessMetrics(global.statsClient), incrementSuccessMetrics(global.statsClient),
sendResponse(), sendResponse(),
incrementErrorMetrics(global.statsClient), incrementErrorMetrics(global.statsClient),
@ -118,7 +129,7 @@ LayergroupController.prototype.register = function(app) {
); );
app.get( app.get(
`${mapconfigBasePath}/:token/:layer/:z/:x/:y.(:format)`, `${mapConfigBasePath}/:token/:layer/:z/:x/:y.(:format)`,
distinguishLayergroupFromStaticRoute(), distinguishLayergroupFromStaticRoute(),
cors(), cors(),
cleanUpQueryParams(), cleanUpQueryParams(),
@ -129,13 +140,17 @@ LayergroupController.prototype.register = function(app) {
credentials(), credentials(),
authorize(this.authApi), authorize(this.authApi),
dbConnSetup(this.pgConnection), dbConnSetup(this.pgConnection),
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi), createMapStoreMapConfigProvider(
this.mapStore,
this.userLimitsApi,
this.pgConnection,
this.layergroupAffectedTablesCache
),
getTile(this.tileBackend, 'maplayer_tile'), getTile(this.tileBackend, 'maplayer_tile'),
setCacheControlHeader(), cacheControlHeader(),
setLastModifiedHeader(), cacheChannelHeader(),
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore), surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
setCacheChannelHeader(), lastModifiedHeader(),
setSurrogateKeyHeader(this.surrogateKeysCache),
incrementSuccessMetrics(global.statsClient), incrementSuccessMetrics(global.statsClient),
sendResponse(), sendResponse(),
incrementErrorMetrics(global.statsClient), incrementErrorMetrics(global.statsClient),
@ -144,7 +159,7 @@ LayergroupController.prototype.register = function(app) {
); );
app.get( app.get(
`${mapconfigBasePath}/:token/:layer/attributes/:fid`, `${mapConfigBasePath}/:token/:layer/attributes/:fid`,
cors(), cors(),
cleanUpQueryParams(), cleanUpQueryParams(),
locals(), locals(),
@ -154,20 +169,24 @@ LayergroupController.prototype.register = function(app) {
credentials(), credentials(),
authorize(this.authApi), authorize(this.authApi),
dbConnSetup(this.pgConnection), dbConnSetup(this.pgConnection),
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi), createMapStoreMapConfigProvider(
this.mapStore,
this.userLimitsApi,
this.pgConnection,
this.layergroupAffectedTablesCache
),
getFeatureAttributes(this.attributesBackend), getFeatureAttributes(this.attributesBackend),
setCacheControlHeader(), cacheControlHeader(),
setLastModifiedHeader(), cacheChannelHeader(),
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore), surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
setCacheChannelHeader(), lastModifiedHeader(),
setSurrogateKeyHeader(this.surrogateKeysCache),
sendResponse() sendResponse()
); );
const forcedFormat = 'png'; const forcedFormat = 'png';
app.get( app.get(
`${mapconfigBasePath}/static/center/:token/:z/:lat/:lng/:width/:height.:format`, `${mapConfigBasePath}/static/center/:token/:z/:lat/:lng/:width/:height.:format`,
cors(), cors(),
cleanUpQueryParams(['layer']), cleanUpQueryParams(['layer']),
locals(), locals(),
@ -177,18 +196,23 @@ LayergroupController.prototype.register = function(app) {
credentials(), credentials(),
authorize(this.authApi), authorize(this.authApi),
dbConnSetup(this.pgConnection), dbConnSetup(this.pgConnection),
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi, forcedFormat), createMapStoreMapConfigProvider(
this.mapStore,
this.userLimitsApi,
this.pgConnection,
this.layergroupAffectedTablesCache,
forcedFormat
),
getPreviewImageByCenter(this.previewBackend), getPreviewImageByCenter(this.previewBackend),
setCacheControlHeader(), cacheControlHeader(),
setLastModifiedHeader(), cacheChannelHeader(),
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore), surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
setCacheChannelHeader(), lastModifiedHeader(),
setSurrogateKeyHeader(this.surrogateKeysCache),
sendResponse() sendResponse()
); );
app.get( app.get(
`${mapconfigBasePath}/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format`, `${mapConfigBasePath}/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format`,
cors(), cors(),
cleanUpQueryParams(['layer']), cleanUpQueryParams(['layer']),
locals(), locals(),
@ -198,13 +222,18 @@ LayergroupController.prototype.register = function(app) {
credentials(), credentials(),
authorize(this.authApi), authorize(this.authApi),
dbConnSetup(this.pgConnection), dbConnSetup(this.pgConnection),
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi, forcedFormat), createMapStoreMapConfigProvider(
this.mapStore,
this.userLimitsApi,
this.pgConnection,
this.layergroupAffectedTablesCache,
forcedFormat
),
getPreviewImageByBoundingBox(this.previewBackend), getPreviewImageByBoundingBox(this.previewBackend),
setCacheControlHeader(), cacheControlHeader(),
setLastModifiedHeader(), cacheChannelHeader(),
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore), surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
setCacheChannelHeader(), lastModifiedHeader(),
setSurrogateKeyHeader(this.surrogateKeysCache),
sendResponse() sendResponse()
); );
@ -227,7 +256,7 @@ LayergroupController.prototype.register = function(app) {
]; ];
app.get( app.get(
`${mapconfigBasePath}/:token/dataview/:dataviewName`, `${mapConfigBasePath}/:token/dataview/:dataviewName`,
cors(), cors(),
cleanUpQueryParams(allowedDataviewQueryParams), cleanUpQueryParams(allowedDataviewQueryParams),
locals(), locals(),
@ -237,18 +266,22 @@ LayergroupController.prototype.register = function(app) {
credentials(), credentials(),
authorize(this.authApi), authorize(this.authApi),
dbConnSetup(this.pgConnection), dbConnSetup(this.pgConnection),
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi), createMapStoreMapConfigProvider(
this.mapStore,
this.userLimitsApi,
this.pgConnection,
this.layergroupAffectedTablesCache
),
getDataview(this.dataviewBackend), getDataview(this.dataviewBackend),
setCacheControlHeader(), cacheControlHeader(),
setLastModifiedHeader(), cacheChannelHeader(),
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore), surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
setCacheChannelHeader(), lastModifiedHeader(),
setSurrogateKeyHeader(this.surrogateKeysCache),
sendResponse() sendResponse()
); );
app.get( app.get(
`${mapconfigBasePath}/:token/:layer/widget/:dataviewName`, `${mapConfigBasePath}/:token/:layer/widget/:dataviewName`,
cors(), cors(),
cleanUpQueryParams(allowedDataviewQueryParams), cleanUpQueryParams(allowedDataviewQueryParams),
locals(), locals(),
@ -258,18 +291,22 @@ LayergroupController.prototype.register = function(app) {
credentials(), credentials(),
authorize(this.authApi), authorize(this.authApi),
dbConnSetup(this.pgConnection), dbConnSetup(this.pgConnection),
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi), createMapStoreMapConfigProvider(
this.mapStore,
this.userLimitsApi,
this.pgConnection,
this.layergroupAffectedTablesCache
),
getDataview(this.dataviewBackend), getDataview(this.dataviewBackend),
setCacheControlHeader(), cacheControlHeader(),
setLastModifiedHeader(), cacheChannelHeader(),
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore), surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
setCacheChannelHeader(), lastModifiedHeader(),
setSurrogateKeyHeader(this.surrogateKeysCache),
sendResponse() sendResponse()
); );
app.get( app.get(
`${mapconfigBasePath}/:token/dataview/:dataviewName/search`, `${mapConfigBasePath}/:token/dataview/:dataviewName/search`,
cors(), cors(),
cleanUpQueryParams(allowedDataviewQueryParams), cleanUpQueryParams(allowedDataviewQueryParams),
locals(), locals(),
@ -279,18 +316,22 @@ LayergroupController.prototype.register = function(app) {
credentials(), credentials(),
authorize(this.authApi), authorize(this.authApi),
dbConnSetup(this.pgConnection), dbConnSetup(this.pgConnection),
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi), createMapStoreMapConfigProvider(
this.mapStore,
this.userLimitsApi,
this.pgConnection,
this.layergroupAffectedTablesCache
),
dataviewSearch(this.dataviewBackend), dataviewSearch(this.dataviewBackend),
setCacheControlHeader(), cacheControlHeader(),
setLastModifiedHeader(), cacheChannelHeader(),
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore), surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
setCacheChannelHeader(), lastModifiedHeader(),
setSurrogateKeyHeader(this.surrogateKeysCache),
sendResponse() sendResponse()
); );
app.get( app.get(
`${mapconfigBasePath}/:token/:layer/widget/:dataviewName/search`, `${mapConfigBasePath}/:token/:layer/widget/:dataviewName/search`,
cors(), cors(),
cleanUpQueryParams(allowedDataviewQueryParams), cleanUpQueryParams(allowedDataviewQueryParams),
locals(), locals(),
@ -300,18 +341,22 @@ LayergroupController.prototype.register = function(app) {
credentials(), credentials(),
authorize(this.authApi), authorize(this.authApi),
dbConnSetup(this.pgConnection), dbConnSetup(this.pgConnection),
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi), createMapStoreMapConfigProvider(
this.mapStore,
this.userLimitsApi,
this.pgConnection,
this.layergroupAffectedTablesCache
),
dataviewSearch(this.dataviewBackend), dataviewSearch(this.dataviewBackend),
setCacheControlHeader(), cacheControlHeader(),
setLastModifiedHeader(), cacheChannelHeader(),
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore), surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
setCacheChannelHeader(), lastModifiedHeader(),
setSurrogateKeyHeader(this.surrogateKeysCache),
sendResponse() sendResponse()
); );
app.get( app.get(
`${mapconfigBasePath}/:token/analysis/node/:nodeId`, `${mapConfigBasePath}/:token/analysis/node/:nodeId`,
cors(), cors(),
cleanUpQueryParams(), cleanUpQueryParams(),
locals(), locals(),
@ -367,7 +412,13 @@ function getRequestParams(locals) {
return params; return params;
} }
function createMapStoreMapConfigProvider (mapStore, userLimitsApi, forcedFormat = null) { function createMapStoreMapConfigProvider (
mapStore,
userLimitsApi,
pgConnection,
affectedTablesCache,
forcedFormat = null
) {
return function createMapStoreMapConfigProviderMiddleware (req, res, next) { return function createMapStoreMapConfigProviderMiddleware (req, res, next) {
const { user } = res.locals; const { user } = res.locals;
@ -378,7 +429,14 @@ function createMapStoreMapConfigProvider (mapStore, userLimitsApi, forcedFormat
params.layer = params.layer || 'all'; params.layer = params.layer || 'all';
} }
res.locals.mapConfigProvider = new MapStoreMapConfigProvider(mapStore, user, userLimitsApi, params); res.locals.mapConfigProvider = new MapStoreMapConfigProvider(
mapStore,
user,
userLimitsApi,
pgConnection,
affectedTablesCache,
params
);
next(); next();
}; };
@ -553,110 +611,6 @@ function getPreviewImageByBoundingBox (previewBackend) {
}; };
} }
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, mapStore) {
return function getAffectedTablesMiddleware (req, res, next) {
const { user, dbname, token } = res.locals;
if (layergroupAffectedTables.hasAffectedTables(dbname, token)) {
res.locals.affectedTables = layergroupAffectedTables.get(dbname, token);
return next();
}
mapStore.load(token, (err, mapconfig) => {
if (err) {
global.logger.warn('ERROR generating cache channel:', err);
return next();
}
const 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`);
});
}
});
const sql = queries.length ? queries.join(';') : null;
if (!sql) {
global.logger.warn('ERROR generating cache channel:' +
' this request doesn\'t need an X-Cache-Channel generated');
return next();
}
pgConnection.getConnection(user, (err, connection) => {
if (err) {
global.logger.warn('ERROR generating cache channel:', err);
return next();
}
QueryTables.getAffectedTablesFromQuery(connection, sql, (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) { function incrementSuccessMetrics (statsClient) {
return function incrementSuccessMetricsMiddleware (req, res, next) { return function incrementSuccessMetricsMiddleware (req, res, next) {
const formatStat = parseFormat(req.params.format); const formatStat = parseFormat(req.params.format);

View File

@ -2,7 +2,6 @@ const _ = require('underscore');
const windshaft = require('windshaft'); const windshaft = require('windshaft');
const MapConfig = windshaft.model.MapConfig; const MapConfig = windshaft.model.MapConfig;
const Datasource = windshaft.model.Datasource; const Datasource = windshaft.model.Datasource;
const QueryTables = require('cartodb-query-tables');
const ResourceLocator = require('../models/resource-locator'); const ResourceLocator = require('../models/resource-locator');
const cors = require('../middleware/cors'); const cors = require('../middleware/cors');
const user = require('../middleware/user'); const user = require('../middleware/user');
@ -12,8 +11,11 @@ const layergroupToken = require('../middleware/layergroup-token');
const credentials = require('../middleware/credentials'); const credentials = require('../middleware/credentials');
const dbConnSetup = require('../middleware/db-conn-setup'); const dbConnSetup = require('../middleware/db-conn-setup');
const authorize = require('../middleware/authorize'); const authorize = require('../middleware/authorize');
const cacheControlHeader = require('../middleware/cache-control-header');
const cacheChannelHeader = require('../middleware/cache-channel-header');
const surrogateKeyHeader = require('../middleware/surrogate-key-header');
const lastModifiedHeader = require('../middleware/last-modified-header');
const sendResponse = require('../middleware/send-response'); const sendResponse = require('../middleware/send-response');
const NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
const NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider'); const NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider');
const CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/create-layergroup-provider'); const CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/create-layergroup-provider');
const LayergroupMetadata = require('../utils/layergroup-metadata'); const LayergroupMetadata = require('../utils/layergroup-metadata');
@ -64,15 +66,15 @@ function MapController (
module.exports = MapController; module.exports = MapController;
MapController.prototype.register = function(app) { MapController.prototype.register = function(app) {
const { base_url_mapconfig: mapconfigBasePath, base_url_templated: templateBasePath } = app; const { base_url_mapconfig: mapConfigBasePath, base_url_templated: templateBasePath } = app;
app.get( app.get(
`${mapconfigBasePath}`, `${mapConfigBasePath}`,
this.composeCreateMapMiddleware(RATE_LIMIT_ENDPOINTS_GROUPS.ANONYMOUS) this.composeCreateMapMiddleware(RATE_LIMIT_ENDPOINTS_GROUPS.ANONYMOUS)
); );
app.post( app.post(
`${mapconfigBasePath}`, `${mapConfigBasePath}`,
this.composeCreateMapMiddleware(RATE_LIMIT_ENDPOINTS_GROUPS.ANONYMOUS) this.composeCreateMapMiddleware(RATE_LIMIT_ENDPOINTS_GROUPS.ANONYMOUS)
); );
@ -88,7 +90,7 @@ MapController.prototype.register = function(app) {
this.composeCreateMapMiddleware(RATE_LIMIT_ENDPOINTS_GROUPS.NAMED, useTemplate) this.composeCreateMapMiddleware(RATE_LIMIT_ENDPOINTS_GROUPS.NAMED, useTemplate)
); );
app.options(app.base_url_mapconfig, cors('Content-Type')); app.options(`${mapConfigBasePath}`, cors('Content-Type'));
}; };
MapController.prototype.composeCreateMapMiddleware = function (endpointGroup, useTemplate = false) { MapController.prototype.composeCreateMapMiddleware = function (endpointGroup, useTemplate = false) {
@ -113,12 +115,11 @@ MapController.prototype.composeCreateMapMiddleware = function (endpointGroup, us
this.getCreateMapMiddlewares(useTemplate), this.getCreateMapMiddlewares(useTemplate),
incrementMapViewCount(this.metadataBackend), incrementMapViewCount(this.metadataBackend),
augmentLayergroupData(), augmentLayergroupData(),
getAffectedTables(this.pgConnection, this.layergroupAffectedTables), cacheControlHeader({ ttl: global.environment.varnish.layergroupTtl || 86400, revalidate: true }),
setCacheChannelHeader(), cacheChannelHeader(),
setSurrogateKeyHeader(this.surrogateKeysCache), surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
setLastModified(), lastModifiedHeader({ now: true }),
setLastUpdatedTimeToLayergroup(), setLastUpdatedTimeToLayergroup(),
setCacheControl(),
setLayerStats(this.pgConnection, this.statsBackend), setLayerStats(this.pgConnection, this.statsBackend),
setLayergroupIdHeader(this.templateMaps ,useTemplateHash), setLayergroupIdHeader(this.templateMaps ,useTemplateHash),
setDataviewsAndWidgetsUrlsToLayergroupMetadata(this.layergroupMetadata), setDataviewsAndWidgetsUrlsToLayergroupMetadata(this.layergroupMetadata),
@ -140,16 +141,27 @@ MapController.prototype.getCreateMapMiddlewares = function (useTemplate) {
this.pgConnection, this.pgConnection,
this.metadataBackend, this.metadataBackend,
this.userLimitsApi, this.userLimitsApi,
this.mapConfigAdapter this.mapConfigAdapter,
this.layergroupAffectedTables
), ),
instantiateLayergroup(this.mapBackend, this.userLimitsApi) instantiateLayergroup(
this.mapBackend,
this.userLimitsApi,
this.pgConnection,
this.layergroupAffectedTables
)
]; ];
} }
return [ return [
checkCreateLayergroup(), checkCreateLayergroup(),
prepareAdapterMapConfig(this.mapConfigAdapter), prepareAdapterMapConfig(this.mapConfigAdapter),
createLayergroup (this.mapBackend, this.userLimitsApi) createLayergroup (
this.mapBackend,
this.userLimitsApi,
this.pgConnection,
this.layergroupAffectedTables
)
]; ];
}; };
@ -220,17 +232,25 @@ function checkCreateLayergroup () {
}; };
} }
function getTemplate (templateMaps, pgConnection, metadataBackend, userLimitsApi, mapConfigAdapter) { function getTemplate (
templateMaps,
pgConnection,
metadataBackend,
userLimitsApi,
mapConfigAdapter,
affectedTablesCache
) {
return function getTemplateMiddleware (req, res, next) { return function getTemplateMiddleware (req, res, next) {
const templateParams = req.body; const templateParams = req.body;
const { user } = res.locals; const { user } = res.locals;
const mapconfigProvider = new NamedMapMapConfigProvider( const mapConfigProvider = new NamedMapMapConfigProvider(
templateMaps, templateMaps,
pgConnection, pgConnection,
metadataBackend, metadataBackend,
userLimitsApi, userLimitsApi,
mapConfigAdapter, mapConfigAdapter,
affectedTablesCache,
user, user,
req.params.template_id, req.params.template_id,
templateParams, templateParams,
@ -238,15 +258,15 @@ function getTemplate (templateMaps, pgConnection, metadataBackend, userLimitsApi
res.locals res.locals
); );
mapconfigProvider.getMapConfig((err, mapconfig, rendererParams) => { mapConfigProvider.getMapConfig((err, mapConfig, rendererParams) => {
req.profiler.done('named.getMapConfig'); req.profiler.done('named.getMapConfig');
if (err) { if (err) {
return next(err); return next(err);
} }
res.locals.mapconfig = mapconfig; res.locals.mapConfig = mapConfig;
res.locals.rendererParams = rendererParams; res.locals.rendererParams = rendererParams;
res.locals.mapconfigProvider = mapconfigProvider; res.locals.mapConfigProvider = mapConfigProvider;
next(); next();
}); });
@ -289,38 +309,51 @@ function prepareAdapterMapConfig (mapConfigAdapter) {
}; };
} }
function createLayergroup (mapBackend, userLimitsApi) { function createLayergroup (mapBackend, userLimitsApi, pgConnection, affectedTablesCache) {
return function createLayergroupMiddleware (req, res, next) { return function createLayergroupMiddleware (req, res, next) {
const requestMapConfig = req.body; const requestMapConfig = req.body;
const { context, user } = res.locals; const { context, user } = res.locals;
const datasource = context.datasource || Datasource.EmptyDatasource(); const datasource = context.datasource || Datasource.EmptyDatasource();
const mapconfig = new MapConfig(requestMapConfig, datasource); const mapConfig = new MapConfig(requestMapConfig, datasource);
const mapconfigProvider = const mapConfigProvider = new CreateLayergroupMapConfigProvider(
new CreateLayergroupMapConfigProvider(mapconfig, user, userLimitsApi, res.locals); mapConfig,
user,
userLimitsApi,
pgConnection,
affectedTablesCache,
res.locals
);
res.locals.mapconfig = mapconfig; res.locals.mapConfig = mapConfig;
res.locals.analysesResults = context.analysesResults; res.locals.analysesResults = context.analysesResults;
mapBackend.createLayergroup(mapconfig, res.locals, mapconfigProvider, (err, layergroup) => { mapBackend.createLayergroup(mapConfig, res.locals, mapConfigProvider, (err, layergroup) => {
req.profiler.done('createLayergroup'); req.profiler.done('createLayergroup');
if (err) { if (err) {
return next(err); return next(err);
} }
res.body = layergroup; res.body = layergroup;
res.locals.mapConfigProvider = mapConfigProvider;
next(); next();
}); });
}; };
} }
function instantiateLayergroup (mapBackend, userLimitsApi) { function instantiateLayergroup (mapBackend, userLimitsApi, pgConnection, affectedTablesCache) {
return function instantiateLayergroupMiddleware (req, res, next) { return function instantiateLayergroupMiddleware (req, res, next) {
const { user, mapconfig, rendererParams } = res.locals; const { user, mapConfig, rendererParams } = res.locals;
const mapconfigProvider = const mapConfigProvider = new CreateLayergroupMapConfigProvider(
new CreateLayergroupMapConfigProvider(mapconfig, user, userLimitsApi, rendererParams); mapConfig,
user,
userLimitsApi,
pgConnection,
affectedTablesCache,
rendererParams
);
mapBackend.createLayergroup(mapconfig, rendererParams, mapconfigProvider, (err, layergroup) => { mapBackend.createLayergroup(mapConfig, rendererParams, mapConfigProvider, (err, layergroup) => {
req.profiler.done('createLayergroup'); req.profiler.done('createLayergroup');
if (err) { if (err) {
return next(err); return next(err);
@ -328,12 +361,11 @@ function instantiateLayergroup (mapBackend, userLimitsApi) {
res.body = layergroup; res.body = layergroup;
const { mapconfigProvider } = res.locals; const { mapConfigProvider } = res.locals;
res.locals.analysesResults = mapconfigProvider.analysesResults; res.locals.analysesResults = mapConfigProvider.analysesResults;
res.locals.template = mapconfigProvider.template; res.locals.template = mapConfigProvider.template;
res.locals.templateName = mapconfigProvider.getTemplateName(); res.locals.context = mapConfigProvider.context;
res.locals.context = mapconfigProvider.context;
next(); next();
}); });
@ -342,10 +374,10 @@ function instantiateLayergroup (mapBackend, userLimitsApi) {
function incrementMapViewCount (metadataBackend) { function incrementMapViewCount (metadataBackend) {
return function incrementMapViewCountMiddleware(req, res, next) { return function incrementMapViewCountMiddleware(req, res, next) {
const { mapconfig, user } = res.locals; const { mapConfig, user } = res.locals;
// Error won't blow up, just be logged. // Error won't blow up, just be logged.
metadataBackend.incMapviewCount(user, mapconfig.obj().stat_tag, (err) => { metadataBackend.incMapviewCount(user, mapConfig.obj().stat_tag, (err) => {
req.profiler.done('incMapviewCount'); req.profiler.done('incMapviewCount');
if (err) { if (err) {
@ -370,98 +402,33 @@ function augmentLayergroupData () {
}; };
} }
function getAffectedTables (pgConnection, layergroupAffectedTables) { function setLastUpdatedTimeToLayergroup () {
return function getAffectedTablesMiddleware (req, res, next) { return function setLastUpdatedTimeToLayergroupMiddleware (req, res, next) {
const { dbname, user, mapconfig } = res.locals; const { mapConfigProvider, analysesResults } = res.locals;
const layergroup = res.body; const layergroup = res.body;
pgConnection.getConnection(user, (err, connection) => { mapConfigProvider.getAffectedTables((err, affectedTables) => {
if (err) { if (err) {
return next(err); return next(err);
} }
const sql = []; if (!affectedTables) {
mapconfig.getLayers().forEach(function(layer) { return next();
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) => { var lastUpdateTime = affectedTables.getLastUpdatedAt();
req.profiler.done('getAffectedTablesFromQuery');
if (err) {
return next(err);
}
// feed affected tables cache so it can be reused from, for instance, layergroup controller lastUpdateTime = getLastUpdatedTime(analysesResults, lastUpdateTime) || lastUpdateTime;
layergroupAffectedTables.set(dbname, layergroup.layergroupId, affectedTables);
res.locals.affectedTables = affectedTables; // last update for layergroup cache buster
layergroup.layergroupid = layergroup.layergroupid + ':' + lastUpdateTime;
layergroup.last_updated = new Date(lastUpdateTime).toISOString();
next(); next();
});
}); });
}; };
} }
function setCacheChannelHeader () {
return function setCacheChannelHeaderMiddleware (req, res, next) {
const { affectedTables } = res.locals;
if (req.method === 'GET') {
res.set('X-Cache-Channel', affectedTables.getCacheChannel());
}
next();
};
}
function setSurrogateKeyHeader (surrogateKeysCache) {
return function setSurrogateKeyHeaderMiddleware(req, res, next) {
const { affectedTables, user, templateName } = res.locals;
if (req.method === 'GET' && affectedTables.tables && affectedTables.tables.length > 0) {
surrogateKeysCache.tag(res, affectedTables);
}
if (templateName) {
surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, templateName));
}
next();
};
}
function setLastModified () {
return function setLastModifiedMiddleware (req, res, next) {
if (req.method === 'GET') {
res.set('Last-Modified', (new Date()).toUTCString());
}
next();
};
}
function setLastUpdatedTimeToLayergroup () {
return function setLastUpdatedTimeToLayergroupMiddleware (req, res, next) {
const { affectedTables, analysesResults } = res.locals;
const layergroup = res.body;
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) { function getLastUpdatedTime(analysesResults, lastUpdateTime) {
if (!Array.isArray(analysesResults)) { if (!Array.isArray(analysesResults)) {
return lastUpdateTime; return lastUpdateTime;
@ -475,20 +442,9 @@ function getLastUpdatedTime(analysesResults, lastUpdateTime) {
}, lastUpdateTime); }, lastUpdateTime);
} }
function setCacheControl () {
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();
};
}
function setLayerStats (pgConnection, statsBackend) { function setLayerStats (pgConnection, statsBackend) {
return function setLayerStatsMiddleware(req, res, next) { return function setLayerStatsMiddleware(req, res, next) {
const { user, mapconfig } = res.locals; const { user, mapConfig } = res.locals;
const layergroup = res.body; const layergroup = res.body;
pgConnection.getConnection(user, (err, connection) => { pgConnection.getConnection(user, (err, connection) => {
@ -496,7 +452,7 @@ function setLayerStats (pgConnection, statsBackend) {
return next(err); return next(err);
} }
statsBackend.getStats(mapconfig, connection, function(err, layersStats) { statsBackend.getStats(mapConfig, connection, function(err, layersStats) {
if (err) { if (err) {
return next(err); return next(err);
} }
@ -531,10 +487,10 @@ function setLayergroupIdHeader (templateMaps, useTemplateHash) {
function setDataviewsAndWidgetsUrlsToLayergroupMetadata (layergroupMetadata) { function setDataviewsAndWidgetsUrlsToLayergroupMetadata (layergroupMetadata) {
return function setDataviewsAndWidgetsUrlsToLayergroupMetadataMiddleware (req, res, next) { return function setDataviewsAndWidgetsUrlsToLayergroupMetadataMiddleware (req, res, next) {
const { user, mapconfig } = res.locals; const { user, mapConfig } = res.locals;
const layergroup = res.body; const layergroup = res.body;
layergroupMetadata.addDataviewsAndWidgetsUrls(user, layergroup, mapconfig.obj()); layergroupMetadata.addDataviewsAndWidgetsUrls(user, layergroup, mapConfig.obj());
next(); next();
}; };
@ -553,10 +509,10 @@ function setAnalysesMetadataToLayergroup (layergroupMetadata, includeQuery) {
function setTurboCartoMetadataToLayergroup (layergroupMetadata) { function setTurboCartoMetadataToLayergroup (layergroupMetadata) {
return function setTurboCartoMetadataToLayergroupMiddleware (req, res, next) { return function setTurboCartoMetadataToLayergroupMiddleware (req, res, next) {
const { mapconfig, context } = res.locals; const { mapConfig, context } = res.locals;
const layergroup = res.body; const layergroup = res.body;
layergroupMetadata.addTurboCartoContextMetadata(layergroup, mapconfig.obj(), context); layergroupMetadata.addTurboCartoContextMetadata(layergroup, mapConfig.obj(), context);
next(); next();
}; };
@ -564,10 +520,10 @@ function setTurboCartoMetadataToLayergroup (layergroupMetadata) {
function setAggregationMetadataToLayergroup (layergroupMetadata) { function setAggregationMetadataToLayergroup (layergroupMetadata) {
return function setAggregationMetadataToLayergroupMiddleware (req, res, next) { return function setAggregationMetadataToLayergroupMiddleware (req, res, next) {
const { mapconfig, context } = res.locals; const { mapConfig, context } = res.locals;
const layergroup = res.body; const layergroup = res.body;
layergroupMetadata.addAggregationContextMetadata(layergroup, mapconfig.obj(), context); layergroupMetadata.addAggregationContextMetadata(layergroup, mapConfig.obj(), context);
next(); next();
}; };
@ -575,10 +531,10 @@ function setAggregationMetadataToLayergroup (layergroupMetadata) {
function setTilejsonMetadataToLayergroup (layergroupMetadata) { function setTilejsonMetadataToLayergroup (layergroupMetadata) {
return function augmentLayergroupTilejsonMiddleware (req, res, next) { return function augmentLayergroupTilejsonMiddleware (req, res, next) {
const { user, mapconfig } = res.locals; const { user, mapConfig } = res.locals;
const layergroup = res.body; const layergroup = res.body;
layergroupMetadata.addTileJsonMetadata(layergroup, user, mapconfig); layergroupMetadata.addTileJsonMetadata(layergroup, user, mapConfig);
next(); next();
}; };
@ -589,10 +545,10 @@ function augmentError (options) {
return function augmentErrorMiddleware (err, req, res, next) { return function augmentErrorMiddleware (err, req, res, next) {
req.profiler.done('error'); req.profiler.done('error');
const { mapconfig } = res.locals; const { mapConfig } = res.locals;
if (addContext) { if (addContext) {
err = Number.isFinite(err.layerIndex) ? populateError(err, mapconfig) : err; err = Number.isFinite(err.layerIndex) ? populateError(err, mapConfig) : err;
} }
err.label = label; err.label = label;

View File

@ -1,4 +1,3 @@
const NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
const cors = require('../middleware/cors'); const cors = require('../middleware/cors');
const user = require('../middleware/user'); const user = require('../middleware/user');
const locals = require('../middleware/locals'); const locals = require('../middleware/locals');
@ -7,6 +6,10 @@ const layergroupToken = require('../middleware/layergroup-token');
const credentials = require('../middleware/credentials'); const credentials = require('../middleware/credentials');
const dbConnSetup = require('../middleware/db-conn-setup'); const dbConnSetup = require('../middleware/db-conn-setup');
const authorize = require('../middleware/authorize'); const authorize = require('../middleware/authorize');
const cacheControlHeader = require('../middleware/cache-control-header');
const cacheChannelHeader = require('../middleware/cache-channel-header');
const surrogateKeyHeader = require('../middleware/surrogate-key-header');
const lastModifiedHeader = require('../middleware/last-modified-header');
const sendResponse = require('../middleware/send-response'); const sendResponse = require('../middleware/send-response');
const vectorError = require('../middleware/vector-error'); const vectorError = require('../middleware/vector-error');
const rateLimit = require('../middleware/rate-limit'); const rateLimit = require('../middleware/rate-limit');
@ -28,8 +31,8 @@ function getRequestParams(locals) {
const params = Object.assign({}, locals); const params = Object.assign({}, locals);
delete params.template; delete params.template;
delete params.affectedTablesAndLastUpdate; delete params.affectedTables;
delete params.namedMapProvider; delete params.mapConfigProvider;
delete params.allowedQueryParams; delete params.allowedQueryParams;
return params; return params;
@ -77,16 +80,15 @@ NamedMapsController.prototype.register = function(app) {
namedMapProviderCache: this.namedMapProviderCache, namedMapProviderCache: this.namedMapProviderCache,
label: 'NAMED_MAP_TILE' label: 'NAMED_MAP_TILE'
}), }),
getAffectedTables(),
getTile({ getTile({
tileBackend: this.tileBackend, tileBackend: this.tileBackend,
label: 'NAMED_MAP_TILE' label: 'NAMED_MAP_TILE'
}), }),
setSurrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
setCacheChannelHeader(),
setLastModifiedHeader(),
setCacheControlHeader(),
setContentTypeHeader(), setContentTypeHeader(),
cacheControlHeader(),
cacheChannelHeader(),
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
lastModifiedHeader(),
sendResponse(), sendResponse(),
vectorError() vectorError()
); );
@ -106,7 +108,6 @@ NamedMapsController.prototype.register = function(app) {
namedMapProviderCache: this.namedMapProviderCache, namedMapProviderCache: this.namedMapProviderCache,
label: 'STATIC_VIZ_MAP', forcedFormat: 'png' label: 'STATIC_VIZ_MAP', forcedFormat: 'png'
}), }),
getAffectedTables(),
getTemplate({ label: 'STATIC_VIZ_MAP' }), getTemplate({ label: 'STATIC_VIZ_MAP' }),
prepareLayerFilterFromPreviewLayers({ prepareLayerFilterFromPreviewLayers({
namedMapProviderCache: this.namedMapProviderCache, namedMapProviderCache: this.namedMapProviderCache,
@ -114,12 +115,12 @@ NamedMapsController.prototype.register = function(app) {
}), }),
getStaticImageOptions({ tablesExtentApi: this.tablesExtentApi }), getStaticImageOptions({ tablesExtentApi: this.tablesExtentApi }),
getImage({ previewBackend: this.previewBackend, label: 'STATIC_VIZ_MAP' }), getImage({ previewBackend: this.previewBackend, label: 'STATIC_VIZ_MAP' }),
incrementMapViews({ metadataBackend: this.metadataBackend }),
setSurrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
setCacheChannelHeader(),
setLastModifiedHeader(),
setCacheControlHeader(),
setContentTypeHeader(), setContentTypeHeader(),
incrementMapViews({ metadataBackend: this.metadataBackend }),
cacheControlHeader(),
cacheChannelHeader(),
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
lastModifiedHeader(),
sendResponse() sendResponse()
); );
}; };
@ -143,25 +144,7 @@ function getNamedMapProvider ({ namedMapProviderCache, label, forcedFormat = nul
return next(err); return next(err);
} }
res.locals.namedMapProvider = namedMapProvider; res.locals.mapConfigProvider = namedMapProvider;
next();
});
};
}
function getAffectedTables () {
return function getAffectedTables (req, res, next) {
const { namedMapProvider } = res.locals;
namedMapProvider.getAffectedTablesAndLastUpdatedTime((err, affectedTablesAndLastUpdate) => {
req.profiler.done('affectedTables');
if (err) {
return next(err);
}
res.locals.affectedTablesAndLastUpdate = affectedTablesAndLastUpdate;
next(); next();
}); });
@ -170,9 +153,9 @@ function getAffectedTables () {
function getTemplate ({ label }) { function getTemplate ({ label }) {
return function getTemplateMiddleware (req, res, next) { return function getTemplateMiddleware (req, res, next) {
const { namedMapProvider } = res.locals; const { mapConfigProvider } = res.locals;
namedMapProvider.getTemplate((err, template) => { mapConfigProvider.getTemplate((err, template) => {
if (err) { if (err) {
err.label = label; err.label = label;
return next(err); return next(err);
@ -220,7 +203,7 @@ function prepareLayerFilterFromPreviewLayers ({ namedMapProviderCache, label })
return next(err); return next(err);
} }
res.locals.namedMapProvider = provider; res.locals.mapConfigProvider = provider;
next(); next();
}); });
@ -229,9 +212,9 @@ function prepareLayerFilterFromPreviewLayers ({ namedMapProviderCache, label })
function getTile ({ tileBackend, label }) { function getTile ({ tileBackend, label }) {
return function getTileMiddleware (req, res, next) { return function getTileMiddleware (req, res, next) {
const { namedMapProvider, format } = res.locals; const { mapConfigProvider, format } = res.locals;
tileBackend.getTile(namedMapProvider, req.params, (err, tile, headers, stats) => { tileBackend.getTile(mapConfigProvider, req.params, (err, tile, headers, stats) => {
req.profiler.add(stats); req.profiler.add(stats);
req.profiler.done('render-' + format); req.profiler.done('render-' + format);
@ -253,7 +236,7 @@ function getTile ({ tileBackend, label }) {
function getStaticImageOptions ({ tablesExtentApi }) { function getStaticImageOptions ({ tablesExtentApi }) {
return function getStaticImageOptionsMiddleware(req, res, next) { return function getStaticImageOptionsMiddleware(req, res, next) {
const { user, namedMapProvider, template } = res.locals; const { user, mapConfigProvider, template } = res.locals;
const imageOpts = getImageOptions(res.locals, template); const imageOpts = getImageOptions(res.locals, template);
@ -264,18 +247,18 @@ function getStaticImageOptions ({ tablesExtentApi }) {
res.locals.imageOpts = DEFAULT_ZOOM_CENTER; res.locals.imageOpts = DEFAULT_ZOOM_CENTER;
namedMapProvider.getAffectedTablesAndLastUpdatedTime((err, affectedTablesAndLastUpdate) => { mapConfigProvider.getAffectedTables((err, affectedTables) => {
if (err) { if (err) {
return next(); return next();
} }
var affectedTables = affectedTablesAndLastUpdate.tables || []; var tables = affectedTables.tables || [];
if (affectedTables.length === 0) { if (tables.length === 0) {
return next(); return next();
} }
tablesExtentApi.getBounds(user, affectedTables, (err, bounds) => { tablesExtentApi.getBounds(user, tables, (err, bounds) => {
if (err) { if (err) {
return next(); return next();
} }
@ -355,7 +338,7 @@ function getImageOptionsFromBoundingBox (bbox = '') {
function getImage({ previewBackend, label }) { function getImage({ previewBackend, label }) {
return function getImageMiddleware (req, res, next) { return function getImageMiddleware (req, res, next) {
const { imageOpts, namedMapProvider } = res.locals; const { imageOpts, mapConfigProvider } = res.locals;
const { zoom, center, bounds } = imageOpts; const { zoom, center, bounds } = imageOpts;
let { width, height } = req.params; let { width, height } = req.params;
@ -366,7 +349,7 @@ function getImage({ previewBackend, label }) {
const format = req.params.format === 'jpg' ? 'jpeg' : 'png'; const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
if (zoom !== undefined && center) { if (zoom !== undefined && center) {
return previewBackend.getImage(namedMapProvider, format, width, height, zoom, center, return previewBackend.getImage(mapConfigProvider, format, width, height, zoom, center,
(err, image, headers, stats) => { (err, image, headers, stats) => {
req.profiler.add(stats); req.profiler.add(stats);
@ -385,7 +368,7 @@ function getImage({ previewBackend, label }) {
}); });
} }
previewBackend.getImage(namedMapProvider, format, width, height, bounds, (err, image, headers, stats) => { previewBackend.getImage(mapConfigProvider, format, width, height, bounds, (err, image, headers, stats) => {
req.profiler.add(stats); req.profiler.add(stats);
req.profiler.done('render-' + format); req.profiler.done('render-' + format);
@ -405,15 +388,23 @@ function getImage({ previewBackend, label }) {
}; };
} }
function setContentTypeHeader () {
return function setContentTypeHeaderMiddleware(req, res, next) {
res.set('Content-Type', res.get('content-type') || res.get('Content-Type') || 'image/png');
next();
};
}
function incrementMapViewsError (ctx) { function incrementMapViewsError (ctx) {
return `ERROR: failed to increment mapview count for user '${ctx.user}': ${ctx.err}`; return `ERROR: failed to increment mapview count for user '${ctx.user}': ${ctx.err}`;
} }
function incrementMapViews ({ metadataBackend }) { function incrementMapViews ({ metadataBackend }) {
return function incrementMapViewsMiddleware(req, res, next) { return function incrementMapViewsMiddleware(req, res, next) {
const { user, namedMapProvider } = res.locals; const { user, mapConfigProvider } = res.locals;
namedMapProvider.getMapConfig((err, mapConfig) => { mapConfigProvider.getMapConfig((err, mapConfig) => {
if (err) { if (err) {
global.logger.log(incrementMapViewsError({ user, err })); global.logger.log(incrementMapViewsError({ user, err }));
return next(); return next();
@ -461,73 +452,3 @@ function templateBounds(view) {
} }
return false; return false;
} }
function setSurrogateKeyHeader ({ surrogateKeysCache }) {
return function setSurrogateKeyHeaderMiddleware(req, res, next) {
const { user, namedMapProvider, affectedTablesAndLastUpdate } = res.locals;
surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, namedMapProvider.getTemplateName()));
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
if (affectedTablesAndLastUpdate.tables.length > 0) {
surrogateKeysCache.tag(res, affectedTablesAndLastUpdate);
}
}
next();
};
}
function setCacheChannelHeader () {
return function setCacheChannelHeaderMiddleware (req, res, next) {
const { affectedTablesAndLastUpdate } = res.locals;
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
res.set('X-Cache-Channel', affectedTablesAndLastUpdate.getCacheChannel());
}
next();
};
}
function setLastModifiedHeader () {
return function setLastModifiedHeaderMiddleware(req, res, next) {
const { affectedTablesAndLastUpdate } = res.locals;
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
var lastModifiedDate;
if (Number.isFinite(affectedTablesAndLastUpdate.lastUpdatedTime)) {
lastModifiedDate = new Date(affectedTablesAndLastUpdate.getLastUpdatedAt());
} else {
lastModifiedDate = new Date();
}
res.set('Last-Modified', lastModifiedDate.toUTCString());
}
next();
};
}
function setCacheControlHeader () {
return function setCacheControlHeaderMiddleware(req, res, next) {
const { affectedTablesAndLastUpdate } = res.locals;
res.set('Cache-Control', 'public,max-age=7200,must-revalidate');
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
// we increase cache control as we can invalidate it
res.set('Cache-Control', 'public,max-age=31536000');
}
next();
};
}
function setContentTypeHeader () {
return function setContentTypeHeaderMiddleware(req, res, next) {
res.set('Content-Type', res.get('content-type') || res.get('Content-Type') || 'image/png');
next();
};
}

View File

@ -0,0 +1,24 @@
module.exports = function setCacheChannelHeader () {
return function setCacheChannelHeaderMiddleware (req, res, next) {
if (req.method !== 'GET') {
return next();
}
const { mapConfigProvider } = res.locals;
mapConfigProvider.getAffectedTables((err, affectedTables) => {
if (err) {
global.logger.warn('ERROR generating Cache Channel Header:', err);
return next();
}
if (!affectedTables) {
return next();
}
res.set('X-Cache-Channel', affectedTables.getCacheChannel());
next();
});
};
};

View File

@ -0,0 +1,19 @@
const ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365;
module.exports = function setCacheControlHeader ({ ttl = ONE_YEAR_IN_SECONDS, revalidate = false } = {}) {
return function setCacheControlHeaderMiddleware (req, res, next) {
if (req.method !== 'GET') {
return next();
}
const directives = [ 'public', `max-age=${ttl}` ];
if (revalidate) {
directives.push('must-revalidate');
}
res.set('Cache-Control', directives.join(','));
next();
};
};

View File

@ -0,0 +1,45 @@
module.exports = function setLastModifiedHeader ({ now = false } = {}) {
return function setLastModifiedHeaderMiddleware(req, res, next) {
if (req.method !== 'GET') {
return next();
}
const { mapConfigProvider, cache_buster } = res.locals;
if (cache_buster) {
const cacheBuster = parseInt(cache_buster, 10);
const lastModifiedDate = Number.isFinite(cacheBuster) ? new Date(cacheBuster) : new Date();
res.set('Last-Modified', lastModifiedDate.toUTCString());
return next();
}
// REVIEW: to keep 100% compatibility with maps controller
if (now) {
res.set('Last-Modified', new Date().toUTCString());
return next();
}
mapConfigProvider.getAffectedTables((err, affectedTables) => {
if (err) {
global.logger.warn('ERROR generating Last Modified Header:', err);
return next();
}
if (!affectedTables) {
res.set('Last-Modified', new Date().toUTCString());
return next();
}
const lastUpdatedAt = affectedTables.getLastUpdatedAt();
const lastModifiedDate = Number.isFinite(lastUpdatedAt) ? new Date(lastUpdatedAt) : new Date();
res.set('Last-Modified', lastModifiedDate.toUTCString());
next();
});
};
};

View File

@ -0,0 +1,31 @@
const NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
const NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider');
module.exports = function setSurrogateKeyHeader ({ surrogateKeysCache }) {
return function setSurrogateKeyHeaderMiddleware(req, res, next) {
const { user, mapConfigProvider } = res.locals;
if (mapConfigProvider instanceof NamedMapMapConfigProvider) {
surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, mapConfigProvider.getTemplateName()));
}
if (req.method !== 'GET') {
return next();
}
mapConfigProvider.getAffectedTables((err, affectedTables) => {
if (err) {
global.logger.warn('ERROR generating Surrogate Key Header:', err);
return next();
}
if (!affectedTables || !affectedTables.tables || affectedTables.tables.length === 0) {
return next();
}
surrogateKeysCache.tag(res, affectedTables);
next();
});
};
};

View File

@ -2,6 +2,7 @@ var assert = require('assert');
var step = require('step'); var step = require('step');
var MapStoreMapConfigProvider = require('./map-store-provider'); var MapStoreMapConfigProvider = require('./map-store-provider');
const QueryTables = require('cartodb-query-tables');
/** /**
* @param {MapConfig} mapConfig * @param {MapConfig} mapConfig
@ -11,10 +12,13 @@ var MapStoreMapConfigProvider = require('./map-store-provider');
* @constructor * @constructor
* @type {CreateLayergroupMapConfigProvider} * @type {CreateLayergroupMapConfigProvider}
*/ */
function CreateLayergroupMapConfigProvider(mapConfig, user, userLimitsApi, params) {
function CreateLayergroupMapConfigProvider(mapConfig, user, userLimitsApi, pgConnection, affectedTablesCache, params) {
this.mapConfig = mapConfig; this.mapConfig = mapConfig;
this.user = user; this.user = user;
this.userLimitsApi = userLimitsApi; this.userLimitsApi = userLimitsApi;
this.pgConnection = pgConnection;
this.affectedTablesCache = affectedTablesCache;
this.params = params; this.params = params;
this.cacheBuster = params.cache_buster || 0; this.cacheBuster = params.cache_buster || 0;
} }
@ -23,7 +27,13 @@ module.exports = CreateLayergroupMapConfigProvider;
CreateLayergroupMapConfigProvider.prototype.getMapConfig = function(callback) { CreateLayergroupMapConfigProvider.prototype.getMapConfig = function(callback) {
var self = this; var self = this;
if (this.mapConfig && this.params && this.context) {
return callback(null, this.mapConfig, this.params, this.context);
}
var context = {}; var context = {};
step( step(
function prepareContextLimits() { function prepareContextLimits() {
self.userLimitsApi.getRenderLimits(self.user, self.params.api_key, this); self.userLimitsApi.getRenderLimits(self.user, self.params.api_key, this);
@ -31,6 +41,7 @@ CreateLayergroupMapConfigProvider.prototype.getMapConfig = function(callback) {
function handleRenderLimits(err, renderLimits) { function handleRenderLimits(err, renderLimits) {
assert.ifError(err); assert.ifError(err);
context.limits = renderLimits; context.limits = renderLimits;
self.context = context;
return null; return null;
}, },
function finish(err) { function finish(err) {
@ -46,3 +57,52 @@ CreateLayergroupMapConfigProvider.prototype.getCacheBuster = MapStoreMapConfigPr
CreateLayergroupMapConfigProvider.prototype.filter = MapStoreMapConfigProvider.prototype.filter; CreateLayergroupMapConfigProvider.prototype.filter = MapStoreMapConfigProvider.prototype.filter;
CreateLayergroupMapConfigProvider.prototype.createKey = MapStoreMapConfigProvider.prototype.createKey; CreateLayergroupMapConfigProvider.prototype.createKey = MapStoreMapConfigProvider.prototype.createKey;
CreateLayergroupMapConfigProvider.prototype.getAffectedTables = function (callback) {
this.getMapConfig((err, mapConfig) => {
if (err) {
return callback(err);
}
const { dbname } = this.params;
const token = mapConfig.id();
if (this.affectedTablesCache.hasAffectedTables(dbname, token)) {
const affectedTables = this.affectedTablesCache.get(dbname, token);
return callback(null, affectedTables);
}
const queries = [];
this.mapConfig.getLayers().forEach(layer => {
queries.push(layer.options.sql);
if (layer.options.affected_tables) {
layer.options.affected_tables.map(table => {
queries.push(`SELECT * FROM ${table} LIMIT 0`);
});
}
});
const sql = queries.length ? queries.join(';') : null;
if (!sql) {
return callback();
}
this.pgConnection.getConnection(this.user, (err, connection) => {
if (err) {
return callback(err);
}
QueryTables.getAffectedTablesFromQuery(connection, sql, (err, affectedTables) => {
if (err) {
return callback(err);
}
this.affectedTablesCache.set(dbname, token, affectedTables);
callback(null, affectedTables);
});
});
});
};

View File

@ -2,6 +2,7 @@ var _ = require('underscore');
var assert = require('assert'); var assert = require('assert');
var dot = require('dot'); var dot = require('dot');
var step = require('step'); var step = require('step');
const QueryTables = require('cartodb-query-tables');
/** /**
* @param {MapStore} mapStore * @param {MapStore} mapStore
@ -11,20 +12,30 @@ var step = require('step');
* @constructor * @constructor
* @type {MapStoreMapConfigProvider} * @type {MapStoreMapConfigProvider}
*/ */
function MapStoreMapConfigProvider(mapStore, user, userLimitsApi, params) { function MapStoreMapConfigProvider(mapStore, user, userLimitsApi, pgConnection, affectedTablesCache, params) {
this.mapStore = mapStore; this.mapStore = mapStore;
this.user = user; this.user = user;
this.userLimitsApi = userLimitsApi; this.userLimitsApi = userLimitsApi;
this.params = params; this.pgConnection = pgConnection;
this.affectedTablesCache = affectedTablesCache;
this.token = params.token; this.token = params.token;
this.cacheBuster = params.cache_buster || 0; this.cacheBuster = params.cache_buster || 0;
this.mapConfig = null;
this.params = params;
this.context = null;
} }
module.exports = MapStoreMapConfigProvider; module.exports = MapStoreMapConfigProvider;
MapStoreMapConfigProvider.prototype.getMapConfig = function(callback) { MapStoreMapConfigProvider.prototype.getMapConfig = function(callback) {
var self = this; var self = this;
if (this.mapConfig !== null) {
return callback(null, this.mapConfig, this.params, this.context);
}
var context = {}; var context = {};
step( step(
function prepareContextLimits() { function prepareContextLimits() {
self.userLimitsApi.getRenderLimits(self.user, self.params.api_key, this); self.userLimitsApi.getRenderLimits(self.user, self.params.api_key, this);
@ -39,6 +50,8 @@ MapStoreMapConfigProvider.prototype.getMapConfig = function(callback) {
self.mapStore.load(self.token, this); self.mapStore.load(self.token, this);
}, },
function finish(err, mapConfig) { function finish(err, mapConfig) {
self.mapConfig = mapConfig;
self.context = context;
return callback(err, mapConfig, self.params, context); return callback(err, mapConfig, self.params, context);
} }
); );
@ -74,4 +87,54 @@ MapStoreMapConfigProvider.prototype.createKey = function(base) {
scale_factor: 1 scale_factor: 1
}); });
return (base) ? baseKeyTpl(tplValues) : rendererKeyTpl(tplValues); return (base) ? baseKeyTpl(tplValues) : rendererKeyTpl(tplValues);
}; };
MapStoreMapConfigProvider.prototype.getAffectedTables = function(callback) {
this.getMapConfig((err, mapConfig) => {
if (err) {
return callback(err);
}
const { dbname } = this.params;
const token = mapConfig.id();
if (this.affectedTablesCache.hasAffectedTables(dbname, token)) {
const affectedTables = this.affectedTablesCache.get(dbname, token);
return callback(null, affectedTables);
}
const queries = [];
mapConfig.getLayers().forEach(layer => {
queries.push(layer.options.sql);
if (layer.options.affected_tables) {
layer.options.affected_tables.map(table => {
queries.push(`SELECT * FROM ${table} LIMIT 0`);
});
}
});
const sql = queries.length ? queries.join(';') : null;
if (!sql) {
return callback();
}
this.pgConnection.getConnection(this.user, (err, connection) => {
if (err) {
return callback(err);
}
QueryTables.getAffectedTablesFromQuery(connection, sql, (err, affectedTables) => {
if (err) {
return callback(err);
}
this.affectedTablesCache.set(dbname, token, affectedTables);
callback(err, affectedTables);
});
});
});
};

View File

@ -11,8 +11,19 @@ var QueryTables = require('cartodb-query-tables');
* @constructor * @constructor
* @type {NamedMapMapConfigProvider} * @type {NamedMapMapConfigProvider}
*/ */
function NamedMapMapConfigProvider(templateMaps, pgConnection, metadataBackend, userLimitsApi, mapConfigAdapter, function NamedMapMapConfigProvider(
owner, templateId, config, authToken, params) { templateMaps,
pgConnection,
metadataBackend,
userLimitsApi,
mapConfigAdapter,
affectedTablesCache,
owner,
templateId,
config,
authToken,
params
) {
this.templateMaps = templateMaps; this.templateMaps = templateMaps;
this.pgConnection = pgConnection; this.pgConnection = pgConnection;
this.metadataBackend = metadataBackend; this.metadataBackend = metadataBackend;
@ -30,7 +41,7 @@ function NamedMapMapConfigProvider(templateMaps, pgConnection, metadataBackend,
// use template after call to mapConfig // use template after call to mapConfig
this.template = null; this.template = null;
this.affectedTablesAndLastUpdate = null; this.affectedTablesCache = affectedTablesCache;
// providing // providing
this.err = null; this.err = null;
@ -189,7 +200,7 @@ NamedMapMapConfigProvider.prototype.getCacheBuster = function() {
NamedMapMapConfigProvider.prototype.reset = function() { NamedMapMapConfigProvider.prototype.reset = function() {
this.template = null; this.template = null;
this.affectedTablesAndLastUpdate = null; this.affectedTables = null;
this.err = null; this.err = null;
this.mapConfig = null; this.mapConfig = null;
@ -251,39 +262,51 @@ NamedMapMapConfigProvider.prototype.getTemplateName = function() {
return this.templateName; return this.templateName;
}; };
NamedMapMapConfigProvider.prototype.getAffectedTablesAndLastUpdatedTime = function(callback) { NamedMapMapConfigProvider.prototype.getAffectedTables = function(callback) {
var self = this; this.getMapConfig((err, mapConfig) => {
if (err) {
if (this.affectedTablesAndLastUpdate !== null) { return callback(err);
return callback(null, this.affectedTablesAndLastUpdate);
}
step(
function getMapConfig() {
self.getMapConfig(this);
},
function getSql(err, mapConfig) {
assert.ifError(err);
return mapConfig.getLayers().map(function(layer) {
return layer.options.sql;
}).join(';');
},
function getAffectedTables(err, sql) {
assert.ifError(err);
step(
function getConnection() {
self.pgConnection.getConnection(self.owner, this);
},
function getAffectedTables(err, connection) {
assert.ifError(err);
QueryTables.getAffectedTablesFromQuery(connection, sql, this);
},
this
);
},
function finish(err, result) {
self.affectedTablesAndLastUpdate = result;
return callback(err, result);
} }
);
const { dbname } = this.rendererParams;
const token = mapConfig.id();
if (this.affectedTablesCache.hasAffectedTables(dbname, token)) {
const affectedTables = this.affectedTablesCache.get(dbname, token);
return callback(null, affectedTables);
}
const queries = [];
mapConfig.getLayers().forEach(layer => {
queries.push(layer.options.sql);
if (layer.options.affected_tables) {
layer.options.affected_tables.map(table => {
queries.push(`SELECT * FROM ${table} LIMIT 0`);
});
}
});
const sql = queries.length ? queries.join(';') : null;
if (!sql) {
return callback();
}
this.pgConnection.getConnection(this.owner, (err, connection) => {
if (err) {
return callback(err);
}
QueryTables.getAffectedTablesFromQuery(connection, sql, (err, affectedTables) => {
if (err) {
return callback(err);
}
this.affectedTablesCache.set(dbname, token, affectedTables);
callback(err, affectedTables);
});
});
});
}; };

View File

@ -200,7 +200,8 @@ module.exports = function(serverOptions) {
pgConnection, pgConnection,
metadataBackend, metadataBackend,
userLimitsApi, userLimitsApi,
mapConfigAdapter mapConfigAdapter,
layergroupAffectedTablesCache
); );
['update', 'delete'].forEach(function(eventType) { ['update', 'delete'].forEach(function(eventType) {

View File

@ -1272,6 +1272,8 @@ describe(suiteName, function() {
it("cache control for layergroup default value", function(done) { it("cache control for layergroup default value", function(done) {
global.environment.varnish.layergroupTtl = null; global.environment.varnish.layergroupTtl = null;
var server = new CartodbWindshaft(serverOptions);
assert.response(server, layergroupTtlRequest, layergroupTtlResponseExpectation, assert.response(server, layergroupTtlRequest, layergroupTtlResponseExpectation,
function(res) { function(res) {
assert.equal(res.headers['cache-control'], 'public,max-age=86400,must-revalidate'); assert.equal(res.headers['cache-control'], 'public,max-age=86400,must-revalidate');
@ -1287,6 +1289,8 @@ describe(suiteName, function() {
var layergroupTtl = 300; var layergroupTtl = 300;
global.environment.varnish.layergroupTtl = layergroupTtl; global.environment.varnish.layergroupTtl = layergroupTtl;
var server = new CartodbWindshaft(serverOptions);
assert.response(server, layergroupTtlRequest, layergroupTtlResponseExpectation, assert.response(server, layergroupTtlRequest, layergroupTtlResponseExpectation,
function(res) { function(res) {
assert.equal(res.headers['cache-control'], 'public,max-age=' + layergroupTtl + ',must-revalidate'); assert.equal(res.headers['cache-control'], 'public,max-age=' + layergroupTtl + ',must-revalidate');