Instead of making all params available in all endpoints, we control what endpoints allow what extra params. Dataviews endpoints should be migrated to this.
435 lines
14 KiB
JavaScript
435 lines
14 KiB
JavaScript
var assert = require('assert');
|
|
var step = require('step');
|
|
|
|
var util = require('util');
|
|
var BaseController = require('./base');
|
|
|
|
var cors = require('../middleware/cors');
|
|
var userMiddleware = require('../middleware/user');
|
|
var allowQueryParams = require('../middleware/allow-query-params');
|
|
|
|
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');
|
|
|
|
/**
|
|
* @param {AuthApi} authApi
|
|
* @param {PgConnection} pgConnection
|
|
* @param {MapStore} mapStore
|
|
* @param {TileBackend} tileBackend
|
|
* @param {PreviewBackend} previewBackend
|
|
* @param {AttributesBackend} attributesBackend
|
|
* @param {SurrogateKeysCache} surrogateKeysCache
|
|
* @param {UserLimitsApi} userLimitsApi
|
|
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
|
* @param {AnalysisBackend} analysisBackend
|
|
* @constructor
|
|
*/
|
|
function LayergroupController(authApi, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
|
|
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, analysisBackend) {
|
|
BaseController.call(this, authApi, pgConnection);
|
|
|
|
this.pgConnection = pgConnection;
|
|
this.mapStore = mapStore;
|
|
this.tileBackend = tileBackend;
|
|
this.previewBackend = previewBackend;
|
|
this.attributesBackend = attributesBackend;
|
|
this.surrogateKeysCache = surrogateKeysCache;
|
|
this.userLimitsApi = userLimitsApi;
|
|
this.layergroupAffectedTables = layergroupAffectedTables;
|
|
|
|
this.dataviewBackend = new DataviewBackend(analysisBackend);
|
|
this.analysisStatusBackend = new AnalysisStatusBackend();
|
|
}
|
|
|
|
util.inherits(LayergroupController, BaseController);
|
|
|
|
module.exports = LayergroupController;
|
|
|
|
|
|
LayergroupController.prototype.register = function(app) {
|
|
app.get(app.base_url_mapconfig +
|
|
'/:token/:z/:x/:y@:scale_factor?x.:format', cors(), userMiddleware,
|
|
this.tile.bind(this));
|
|
|
|
app.get(app.base_url_mapconfig +
|
|
'/:token/:z/:x/:y.:format', cors(), userMiddleware,
|
|
this.tile.bind(this));
|
|
|
|
app.get(app.base_url_mapconfig +
|
|
'/:token/:layer/:z/:x/:y.(:format)', cors(), userMiddleware,
|
|
this.layer.bind(this));
|
|
|
|
app.get(app.base_url_mapconfig +
|
|
'/:token/:layer/attributes/:fid', cors(), userMiddleware,
|
|
this.attributes.bind(this));
|
|
|
|
app.get(app.base_url_mapconfig +
|
|
'/static/center/:token/:z/:lat/:lng/:width/:height.:format',
|
|
cors(), userMiddleware, allowQueryParams(['layer']),
|
|
this.center.bind(this));
|
|
|
|
app.get(app.base_url_mapconfig +
|
|
'/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format',
|
|
cors(), userMiddleware, allowQueryParams(['layer']),
|
|
this.bbox.bind(this));
|
|
|
|
// Undocumented/non-supported API endpoint methods.
|
|
// Use at your own peril.
|
|
app.get(app.base_url_mapconfig +
|
|
'/:token/dataview/:dataviewName', cors(), userMiddleware,
|
|
this.dataview.bind(this));
|
|
app.get(app.base_url_mapconfig +
|
|
'/:token/:layer/widget/:dataviewName', cors(), userMiddleware,
|
|
this.dataview.bind(this));
|
|
|
|
app.get(app.base_url_mapconfig +
|
|
'/:token/dataview/:dataviewName/search', cors(), userMiddleware,
|
|
this.dataviewSearch.bind(this));
|
|
app.get(app.base_url_mapconfig +
|
|
'/:token/:layer/widget/:dataviewName/search', cors(), userMiddleware,
|
|
this.dataviewSearch.bind(this));
|
|
|
|
app.get(app.base_url_mapconfig +
|
|
'/:token/analysis/node/:nodeId', cors(), userMiddleware,
|
|
this.analysisNodeStatus.bind(this));
|
|
};
|
|
|
|
LayergroupController.prototype.analysisNodeStatus = function(req, res) {
|
|
var self = this;
|
|
|
|
step(
|
|
function setupParams() {
|
|
self.req2params(req, this);
|
|
},
|
|
function retrieveNodeStatus(err) {
|
|
assert.ifError(err);
|
|
self.analysisStatusBackend.getNodeStatus(req.params, this);
|
|
},
|
|
function finish(err, nodeStatus, stats) {
|
|
req.profiler.add(stats || {});
|
|
|
|
if (err) {
|
|
self.sendError(req, res, err, 'GET NODE STATUS');
|
|
} else {
|
|
self.sendResponse(req, res, nodeStatus, 200, {
|
|
'Cache-Control': 'public,max-age=5',
|
|
'Last-Modified': new Date().toUTCString()
|
|
});
|
|
}
|
|
}
|
|
);
|
|
};
|
|
|
|
LayergroupController.prototype.dataview = function(req, res) {
|
|
var self = this;
|
|
|
|
step(
|
|
function setupParams() {
|
|
self.req2params(req, this);
|
|
},
|
|
function retrieveDataview(err) {
|
|
assert.ifError(err);
|
|
|
|
var mapConfigProvider = new MapStoreMapConfigProvider(
|
|
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
|
);
|
|
self.dataviewBackend.getDataview(mapConfigProvider, req.context.user, req.params, this);
|
|
},
|
|
function finish(err, dataview, stats) {
|
|
req.profiler.add(stats || {});
|
|
|
|
if (err) {
|
|
self.sendError(req, res, err, 'GET DATAVIEW');
|
|
} else {
|
|
self.sendResponse(req, res, dataview, 200);
|
|
}
|
|
}
|
|
);
|
|
|
|
};
|
|
|
|
LayergroupController.prototype.dataviewSearch = function(req, res) {
|
|
var self = this;
|
|
|
|
step(
|
|
function setupParams() {
|
|
self.req2params(req, this);
|
|
},
|
|
function searchDataview(err) {
|
|
assert.ifError(err);
|
|
|
|
var mapConfigProvider = new MapStoreMapConfigProvider(
|
|
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
|
);
|
|
self.dataviewBackend.search(mapConfigProvider, req.context.user, req.params, this);
|
|
},
|
|
function finish(err, searchResult, stats) {
|
|
req.profiler.add(stats || {});
|
|
|
|
if (err) {
|
|
self.sendError(req, res, err, 'GET DATAVIEW SEARCH');
|
|
} else {
|
|
self.sendResponse(req, res, searchResult, 200);
|
|
}
|
|
}
|
|
);
|
|
|
|
};
|
|
|
|
LayergroupController.prototype.attributes = function(req, res) {
|
|
var self = this;
|
|
|
|
req.profiler.start('windshaft.maplayer_attribute');
|
|
|
|
step(
|
|
function setupParams() {
|
|
self.req2params(req, this);
|
|
},
|
|
function retrieveFeatureAttributes(err) {
|
|
assert.ifError(err);
|
|
|
|
var mapConfigProvider = new MapStoreMapConfigProvider(
|
|
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
|
);
|
|
self.attributesBackend.getFeatureAttributes(mapConfigProvider, req.params, false, this);
|
|
},
|
|
function finish(err, tile, stats) {
|
|
req.profiler.add(stats || {});
|
|
|
|
if (err) {
|
|
self.sendError(req, res, err, 'GET ATTRIBUTES');
|
|
} else {
|
|
self.sendResponse(req, res, tile, 200);
|
|
}
|
|
}
|
|
);
|
|
|
|
};
|
|
|
|
// Gets a tile for a given token and set of tile ZXY coords. (OSM style)
|
|
LayergroupController.prototype.tile = function(req, res) {
|
|
req.profiler.start('windshaft.map_tile');
|
|
this.tileOrLayer(req, res);
|
|
};
|
|
|
|
// Gets a tile for a given token, layer set of tile ZXY coords. (OSM style)
|
|
LayergroupController.prototype.layer = function(req, res, next) {
|
|
if (req.params.token === 'static') {
|
|
return next();
|
|
}
|
|
req.profiler.start('windshaft.maplayer_tile');
|
|
this.tileOrLayer(req, res);
|
|
};
|
|
|
|
LayergroupController.prototype.tileOrLayer = function (req, res) {
|
|
var self = this;
|
|
|
|
step(
|
|
function mapController$prepareParams() {
|
|
self.req2params(req, this);
|
|
},
|
|
function mapController$getTileOrGrid(err) {
|
|
assert.ifError(err);
|
|
self.tileBackend.getTile(
|
|
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
|
|
req.params, this
|
|
);
|
|
},
|
|
function mapController$finalize(err, tile, headers, stats) {
|
|
req.profiler.add(stats);
|
|
self.finalizeGetTileOrGrid(err, req, res, tile, headers);
|
|
}
|
|
);
|
|
};
|
|
|
|
// 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) {
|
|
var supportedFormats = {
|
|
grid_json: true,
|
|
json_torque: true,
|
|
torque_json: true,
|
|
png: true
|
|
};
|
|
|
|
var formatStat = 'invalid';
|
|
if (req.params.format) {
|
|
var format = req.params.format.replace('.', '_');
|
|
if (supportedFormats[format]) {
|
|
formatStat = format;
|
|
}
|
|
}
|
|
|
|
if (err) {
|
|
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
|
var errMsg = err.message ? ( '' + err.message ) : ( '' + err );
|
|
|
|
// 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;
|
|
|
|
this.sendError(req, res, err, 'TILE RENDER');
|
|
global.statsClient.increment('windshaft.tiles.error');
|
|
global.statsClient.increment('windshaft.tiles.' + formatStat + '.error');
|
|
} else {
|
|
this.sendResponse(req, res, tile, 200, headers);
|
|
global.statsClient.increment('windshaft.tiles.success');
|
|
global.statsClient.increment('windshaft.tiles.' + formatStat + '.success');
|
|
}
|
|
};
|
|
|
|
LayergroupController.prototype.bbox = function(req, res) {
|
|
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
|
|
});
|
|
};
|
|
|
|
LayergroupController.prototype.center = function(req, res) {
|
|
this.staticMap(req, res, +req.params.width, +req.params.height, +req.params.z, {
|
|
lng: +req.params.lng,
|
|
lat: +req.params.lat
|
|
});
|
|
};
|
|
|
|
LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center) {
|
|
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
|
req.params.layer = 'all';
|
|
req.params.format = 'png';
|
|
|
|
var self = this;
|
|
|
|
step(
|
|
function reqParams() {
|
|
self.req2params(req, this);
|
|
},
|
|
function getImage(err) {
|
|
assert.ifError(err);
|
|
if (center) {
|
|
self.previewBackend.getImage(
|
|
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
|
|
format, width, height, zoom, center, this);
|
|
} else {
|
|
self.previewBackend.getImage(
|
|
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
|
|
format, width, height, zoom /* bounds */, this);
|
|
}
|
|
},
|
|
function handleImage(err, image, headers, stats) {
|
|
req.profiler.done('render-' + format);
|
|
req.profiler.add(stats || {});
|
|
|
|
if (err) {
|
|
self.sendError(req, res, err, 'STATIC_MAP');
|
|
} 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 (req.params.cache_buster) {
|
|
// Assuming cache_buster is a timestamp
|
|
lastUpdated = new Date(parseInt(req.params.cache_buster));
|
|
} else {
|
|
lastUpdated = new Date();
|
|
}
|
|
res.set('Last-Modified', lastUpdated.toUTCString());
|
|
|
|
var dbName = req.params.dbname;
|
|
step(
|
|
function getAffectedTables() {
|
|
self.getAffectedTables(req.context.user, dbName, req.params.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);
|
|
}
|
|
self.send(req, res, body, status, headers);
|
|
}
|
|
);
|
|
|
|
};
|
|
|
|
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()
|
|
.map(function(lyr) {
|
|
return lyr.options.sql;
|
|
})
|
|
.filter(function(sql) {
|
|
return !!sql;
|
|
});
|
|
|
|
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
|
|
);
|
|
};
|