Merge pull request #335 from CartoDB/standalone-server

Standalone server using Windshaft as library
This commit is contained in:
Raul Ochoa
2015-09-30 01:13:16 +02:00
154 changed files with 19812 additions and 3840 deletions
+4 -8
View File
@@ -7,8 +7,8 @@
// // Enforcing
// "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
// "camelcase" : false, // true: Identifiers must be in camelCase
// "curly" : true, // true: Require {} for every new block or scope
// "eqeqeq" : true, // true: Require triple equals (===) for comparison
"curly" : true, // true: Require {} for every new block or scope
"eqeqeq" : true, // true: Require triple equals (===) for comparison
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
"freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc.
"immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
@@ -31,7 +31,7 @@
// "maxparams" : false, // {int} Max number of formal params allowed per function
// "maxdepth" : false, // {int} Max depth of nested blocks (within functions)
// "maxstatements" : false, // {int} Max number statements per function
"maxcomplexity" : 8, // {int} Max cyclomatic complexity per function
"maxcomplexity" : 6, // {int} Max cyclomatic complexity per function
"maxlen" : 120, // {int} Max number of characters per line
//
// // Relaxing
@@ -89,10 +89,6 @@
"after": true,
"beforeEach": true,
"afterEach": true,
"it": true,
"suite": true,
"suiteSetup": true,
"test": true,
"suiteTeardown": true
"it": true
}
}
+18 -6
View File
@@ -18,14 +18,26 @@ config.status--test:
config/environments/test.js: config.status--test
./config.status--test
TEST_SUITE := $(shell find test/{acceptance,integration,unit} -name "*.js")
TEST_SUITE_UNIT := $(shell find test/unit -name "*.js")
TEST_SUITE_INTEGRATION := $(shell find test/integration -name "*.js")
TEST_SUITE_ACCEPTANCE := $(shell find test/acceptance -name "*.js")
test: config/environments/test.js
@echo "***tests***"
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} \
test/unit/cartodb/*.js \
test/unit/cartodb/cache/model/*.js \
test/integration/*.js \
test/acceptance/*.js \
test/acceptance/cache/*.js
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} $(TEST_SUITE)
test-unit: config/environments/test.js
@echo "***tests***"
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} $(TEST_SUITE_UNIT)
test-integration: config/environments/test.js
@echo "***tests***"
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} $(TEST_SUITE_INTEGRATION)
test-acceptance: config/environments/test.js
@echo "***tests***"
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} $(TEST_SUITE_ACCEPTANCE)
jshint:
@echo "***jshint***"
+33 -1
View File
@@ -1,9 +1,41 @@
# Changelog
## 2.13.1
## 2.14.0
Released 2015-mm-dd
Summary: this starts using Windshaft as library, it no longer extends old Windshaft server.
New features:
- Named tiles: /api/v1/map/named/:name/:layer/:z/:x/:y.:format
Ported from Windshaft pre-library:
- Almost all acceptance tests, some unit and some integration tests
- Stats + profiler
New features:
- Named maps MapConfig provider
- Base controller with: req2params, send response/error mechanisms
- Authentication/Authorization moves to its own API so it can be reused
- Surrogate-Key headers for named maps and affected tables
Improvements:
- No more fake requests to emulate map config instantiations
- As named maps previews are using named map MapConfigProvider it doesn't need to load the MapConfig
- Controllers using Windshaft's backends to request resources through MapConfig providers
- Express 4.x, as Windshaft no longer provides an HTTP server, here we start using latest major version of Express.
- assert.response implemented using request
- All tests validate there are no unexpected keys in Redis and keys requested to be deleted after a test are present
- Test suite in Makefile generated with `find`
- Image comparison with `mapnik.Image.compare`
- Doesn't emit Named map update event on unmodified templates
TODO:
- Named map provider checks on every request if named map has changed to reload it (actually reset it so MapConfig has to be regenerated). See https://github.com/CartoDB/Windshaft-cartodb/commit/f553efa69e83fdf296154ab1b7b49aa08957c04e. This is done this way because when running the Service in a cluster there is no communication between different instances so when a named map gets updated in one of the them the rest is not aware/notified of the change. In the future there should be a mechanism to synch this changes between instance:
* endpoint
* redis pub/sub
* backdoor
## 2.13.0
-3
View File
@@ -11,8 +11,6 @@ This is the [CartoDB Maps API](http://docs.cartodb.com/cartodb-platform/maps-api
* gets the default geometry type from the cartodb redis store
* allows tiles to be styled individually
* provides a link to varnish high speed cache
* provides a ``infowindow`` endpoint for windshaft (DEPRECATED)
* provides a ``map_metadata`` endpoint for windshaft (DEPRECATED)
* provides a [template maps API](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Template-maps.md)
Requirements
@@ -27,7 +25,6 @@ Requirements
- libcairo2-dev, libpango1.0-dev, libjpeg8-dev and libgif-dev for server side canvas support
- For cache control (optional)
- CartoDB-SQL-API 1.0.0+
- CartoDB 0.9.5+ (for `CDB_QueryTables`)
- Varnish (http://www.varnish-cache.org)
+45 -63
View File
@@ -1,51 +1,58 @@
/*
* Windshaft-CartoDB
* ===============
*
* ./app.js [environment]
*
* environments: [development, production]
*/
var path = require('path');
var fs = require('fs');
var http = require('http');
var https = require('https');
var RedisPool = require('redis-mpool');
var path = require('path');
var fs = require('fs');
var _ = require('underscore');
var ENV;
var ENVIRONMENT;
if ( process.argv[2] ) {
ENV = process.argv[2];
ENVIRONMENT = process.argv[2];
} else if ( process.env.NODE_ENV ) {
ENV = process.env.NODE_ENV;
ENVIRONMENT = process.env.NODE_ENV;
} else {
ENV = 'development';
ENVIRONMENT = 'development';
}
process.env.NODE_ENV = ENV;
var availableEnvironments = {
production: true,
staging: true,
development: true
};
// sanity check
if (ENV != 'development' && ENV != 'production' && ENV != 'staging' ){
console.error("\nnode app.js [environment]");
console.error("environments: development, production, staging\n");
if (!availableEnvironments[ENVIRONMENT]){
console.error('node app.js [environment]');
console.error('environments: %s', Object.keys(availableEnvironments).join(', '));
process.exit(1);
}
process.env.NODE_ENV = ENVIRONMENT;
// set environment specific variables
global.environment = require(__dirname + '/config/environments/' + ENV);
global.environment.api_hostname = require('os').hostname().split('.')[0];
global.environment = require('./config/environments/' + ENVIRONMENT);
global.log4js = require('log4js');
var log4js_config = {
appenders: [],
replaceConsole:true
replaceConsole: true
};
if (global.environment.uv_threadpool_size) {
process.env.UV_THREADPOOL_SIZE = global.environment.uv_threadpool_size;
}
// set global HTTP and HTTPS agent default configurations
// ref https://nodejs.org/api/http.html#http_new_agent_options
var agentOptions = _.defaults(global.environment.httpAgent || {}, {
keepAlive: false,
keepAliveMsecs: 1000,
maxSockets: Infinity,
maxFreeSockets: 256
});
http.globalAgent = new http.Agent(agentOptions);
https.globalAgent = new https.Agent(agentOptions);
if ( global.environment.log_filename ) {
var logdir = path.dirname(global.environment.log_filename);
// See cwd inlog4js.configure call below
@@ -67,64 +74,39 @@ if ( global.environment.log_filename ) {
global.log4js.configure(log4js_config, { cwd: __dirname });
global.logger = global.log4js.getLogger();
var redisOpts = _.defaults(global.environment.redis, {
name: 'windshaft',
unwatchOnRelease: false,
noReadyCheck: true
});
var redisPool = new RedisPool(redisOpts);
// set global HTTP and HTTPS agent default configurations
// ref https://nodejs.org/api/http.html#http_new_agent_options
var agentOptions = _.defaults(global.environment.httpAgent || {}, {
keepAlive: false,
keepAliveMsecs: 1000,
maxSockets: Infinity,
maxFreeSockets: 256
});
http.globalAgent = new http.Agent(agentOptions);
https.globalAgent = new https.Agent(agentOptions);
global.environment.api_hostname = require('os').hostname().split('.')[0];
// Include cartodb_windshaft only _after_ the "global" variable is set
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/28
var cartodbWindshaft = require('./lib/cartodb/cartodb_windshaft'),
serverOptions = require('./lib/cartodb/server_options')(redisPool);
var cartodbWindshaft = require('./lib/cartodb/server');
var serverOptions = require('./lib/cartodb/server_options');
var ws = cartodbWindshaft(serverOptions);
if (global.statsClient) {
redisPool.on('status', function(status) {
var keyPrefix = 'windshaft.redis-pool.' + status.name + '.db' + status.db + '.';
global.statsClient.gauge(keyPrefix + 'count', status.count);
global.statsClient.gauge(keyPrefix + 'unused', status.unused);
global.statsClient.gauge(keyPrefix + 'waiting', status.waiting);
});
setInterval(function() {
var memoryUsage = process.memoryUsage();
Object.keys(memoryUsage).forEach(function(k) {
global.statsClient.gauge('windshaft.memory.' + k, memoryUsage[k]);
});
}, 5000);
}
var server = cartodbWindshaft(serverOptions);
// Maximum number of connections for one process
// 128 is a good number if you have up to 1024 filedescriptors
// 4 is good if you have max 32 filedescriptors
// 1 is good if you have max 16 filedescriptors
ws.maxConnections = global.environment.maxConnections || 128;
var backlog = global.environment.maxConnections || 128;
ws.listen(global.environment.port, global.environment.host);
var listener = server.listen(serverOptions.bind.port, serverOptions.bind.host, backlog);
var version = require("./package").version;
ws.on('listening', function() {
listener.on('listening', function() {
console.log(
"Windshaft tileserver %s started on %s:%s PID=%d (%s)",
version, global.environment.host, global.environment.port, process.pid, ENV
version, serverOptions.bind.host, serverOptions.bind.port, process.pid, ENVIRONMENT
);
});
setInterval(function() {
var memoryUsage = process.memoryUsage();
Object.keys(memoryUsage).forEach(function(k) {
global.statsClient.gauge('windshaft.memory.' + k, memoryUsage[k]);
});
}, 5000);
process.on('SIGHUP', function() {
global.log4js.clearAndShutdownAppenders(function() {
global.log4js.configure(log4js_config);
Vendored
-10
View File
@@ -20,7 +20,6 @@
ENVDIR=config/environments
PGPORT=
SQLAPI_PORT=
MAPNIK_VERSION=
ENVIRONMENT=development
@@ -32,7 +31,6 @@ usage() {
echo "Configuration:"
echo " --help display this help and exit"
echo " --with-pgport=NUM access PostgreSQL server on TCP port NUM [$PGPORT]"
echo " --with-sqlapi-port=NUM access SQL-API server on TCP port NUM [$SQLAPI_PORT]"
echo " --with-mapnik-version=STRING set mapnik version string [$MAPNIK_VERSION]"
echo " --environment=STRING set output environment name [$ENVIRONMENT]"
}
@@ -46,9 +44,6 @@ while test -n "$1"; do
--with-pgport=*)
PGPORT=`echo "$1" | cut -d= -f2`
;;
--with-sqlapi-port=*)
SQLAPI_PORT=`echo "$1" | cut -d= -f2`
;;
--with-mapnik-version=*)
MAPNIK_VERSION=`echo "$1" | cut -d= -f2`
;;
@@ -67,12 +62,8 @@ ENVEX=./${ENVDIR}/${ENVIRONMENT}.js.example
if [ -z "$PGPORT" ]; then
PGPORT=`node -e "console.log(require('${ENVEX}').postgres.port)"`
fi
if [ -z "$SQLAPI_PORT" ]; then
SQLAPI_PORT=`node -e "console.log(require('${ENVEX}').sqlapi.port)"`
fi
echo "PGPORT: $PGPORT"
echo "SQLAPI_PORT: $SQLAPI_PORT"
echo "MAPNIK_VERSION: $MAPNIK_VERSION"
echo "ENVIRONMENT: $ENVIRONMENT"
@@ -82,7 +73,6 @@ echo "Writing $o"
# See http://austinmatzko.com/2008/04/26/sed-multi-line-search-and-replace/
sed -n "1h;1!H;\${;g;s/\(,postgres: {[^}]*port: *'\?\)[^',]*\('\?,\)/\1$PGPORT\2/;p;}" < "${ENVEX}" \
| sed "s/mapnik_version:.*/mapnik_version: '$MAPNIK_VERSION'/" \
| sed -n "1h;1!H;\${;g;s/\(,sqlapi: {[^}]*port: *'\?\)[^',]*\('\?,\)/\1$SQLAPI_PORT\2/;p;}" \
> "$o"
STATUSFILE=config.status--${ENVIRONMENT}
+141
View File
@@ -0,0 +1,141 @@
var assert = require('assert');
var step = require('step');
/**
*
* @param {PgConnection} pgConnection
* @param metadataBackend
* @param {MapStore} mapStore
* @param {TemplateMaps} templateMaps
* @constructor
* @type {AuthApi}
*/
function AuthApi(pgConnection, metadataBackend, mapStore, templateMaps) {
this.pgConnection = pgConnection;
this.metadataBackend = metadataBackend;
this.mapStore = mapStore;
this.templateMaps = templateMaps;
}
module.exports = AuthApi;
// Check if a request is authorized by a signer
//
// @param req express request object
// @param callback function(err, signed_by) signed_by will be
// null if the request is not signed by anyone
// or will be a string cartodb username otherwise.
//
AuthApi.prototype.authorizedBySigner = function(req, callback) {
if ( ! req.params.token || ! req.params.signer ) {
return callback(null, false); // no signer requested
}
var self = this;
var layergroup_id = req.params.token;
var auth_token = req.params.auth_token;
this.mapStore.load(layergroup_id, function(err, mapConfig) {
if (err) {
return callback(err);
}
var authorized = self.templateMaps.isAuthorized(mapConfig.obj().template, auth_token);
return callback(null, authorized);
});
};
// Check if a request is authorized by api_key
//
// @param user
// @param req express request object
// @param callback function(err, authorized)
// NOTE: authorized is expected to be 0 or 1 (integer)
//
AuthApi.prototype.authorizedByAPIKey = function(user, req, callback) {
var givenKey = req.query.api_key || req.query.map_key;
if ( ! givenKey && req.body ) {
// check also in request body
givenKey = req.body.api_key || req.body.map_key;
}
if ( ! givenKey ) {
return callback(null, 0); // no api key, no authorization...
}
var self = this;
step(
function () {
self.metadataBackend.getUserMapKey(user, this);
},
function checkApiKey(err, val){
assert.ifError(err);
return val && givenKey === val;
},
function finish(err, authorized) {
callback(err, authorized);
}
);
};
/**
* Check access authorization
*
* @param req - standard req object. Importantly contains table and host information
* @param callback function(err, allowed) is access allowed not?
*/
AuthApi.prototype.authorize = function(req, callback) {
var self = this;
var user = req.context.user;
step(
function () {
self.authorizedByAPIKey(user, req, this);
},
function checkApiKey(err, authorized){
if (req.profiler) {
req.profiler.done('authorizedByAPIKey');
}
assert.ifError(err);
// if not authorized by api_key, continue
if (!authorized) {
// not authorized by api_key, check if authorized by signer
return self.authorizedBySigner(req, this);
}
// authorized by api key, login as the given username and stop
self.pgConnection.setDBAuth(user, req.params, function(err) {
callback(err, true); // authorized (or error)
});
},
function checkSignAuthorized(err, authorized) {
if (err) {
return callback(err);
}
if ( ! authorized ) {
// request not authorized by signer.
// if no signer name was given, let dbparams and
// PostgreSQL do the rest.
//
if ( ! req.params.signer ) {
return callback(null, true); // authorized so far
}
// if signer name was given, return no authorization
return callback(null, false);
}
self.pgConnection.setDBAuth(user, req.params, function(err) {
if (req.profiler) {
req.profiler.done('setDBAuth');
}
callback(err, true); // authorized (or error)
});
}
);
};
+30 -1
View File
@@ -48,7 +48,7 @@ QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (usernam
function handleAffectedTablesAndLastUpdatedTimeRows(err, rows, callback) {
if (err || rows.length === 0) {
var msg = err.message ? err.message : err;
callback(new Error('could not fetch affected tables and last updated time: ' + msg));
callback(new Error('could not fetch affected tables or last updated time: ' + msg));
return;
}
@@ -65,6 +65,35 @@ function handleAffectedTablesAndLastUpdatedTimeRows(err, rows, callback) {
});
}
QueryTablesApi.prototype.getLastUpdatedTime = function (username, tableNames, callback) {
if (!Array.isArray(tableNames) || tableNames.length === 0) {
return callback(null, 0);
}
var query = [
'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max',
'FROM CDB_TableMetadata m WHERE m.tabname = any (ARRAY[',
tableNames.map(function(t) { return "'" + t + "'::regclass"; }).join(','),
'])'
].join(' ');
this.pgQueryRunner.run(username, query, handleLastUpdatedTimeRows, callback);
};
function handleLastUpdatedTimeRows(err, rows, callback) {
if (err) {
var msg = err.message ? err.message : err;
return callback(new Error('could not fetch affected tables or last updated time: ' + msg));
}
// when the table has not updated_at means it hasn't been changed so a default last_updated is set
var lastUpdated = 0;
if (rows.length !== 0) {
lastUpdated = rows[0].max || 0;
}
return callback(null, lastUpdated*1000);
}
function prepareSql(sql) {
return sql
.replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)')
+2 -3
View File
@@ -39,10 +39,9 @@ TablesExtentApi.prototype.getBounds = function (username, tableNames, callback)
};
function handleBoundsResult(err, rows, callback) {
if (err){
if (err) {
var msg = err.message ? err.message : err;
callback(new Error('could not fetch source tables: ' + msg));
return;
return callback(new Error('could not fetch source tables: ' + msg));
}
var result = null;
if (rows.length > 0) {
+28
View File
@@ -0,0 +1,28 @@
/**
*
* @param metadataBackend
* @param options
* @constructor
* @type {UserLimitsApi}
*/
function UserLimitsApi(metadataBackend, options) {
this.metadataBackend = metadataBackend;
this.options = options || {};
this.options.limits = this.options.limits || {};
}
module.exports = UserLimitsApi;
UserLimitsApi.prototype.getRenderLimits = function (username, callback) {
var self = this;
this.metadataBackend.getTilerRenderLimit(username, function handleTilerLimits(err, renderLimit) {
if (err) {
return callback(err);
}
return callback(null, {
cacheOnTimeout: self.options.limits.cacheOnTimeout || false,
render: renderLimit || self.options.limits.render || 0
});
});
};
+11 -6
View File
@@ -1,3 +1,4 @@
var assert = require('assert');
var step = require('step');
var _ = require('underscore');
@@ -29,19 +30,21 @@ PgConnection.prototype.setDBAuth = function(username, params, callback) {
self.metadataBackend.getUserId(username, this);
},
function(err, user_id) {
if (err) throw err;
assert.ifError(err);
user_params.user_id = user_id;
var dbuser = _.template(auth_user, user_params);
_.extend(params, {dbuser:dbuser});
// skip looking up user_password if postgres_auth_pass
// doesn't contain the "user_password" label
if (!auth_pass || ! auth_pass.match(/\buser_password\b/) ) return null;
if (!auth_pass || ! auth_pass.match(/\buser_password\b/) ) {
return null;
}
self.metadataBackend.getUserDBPass(username, this);
},
function(err, user_password) {
if (err) throw err;
assert.ifError(err);
user_params.user_password = user_password;
if ( auth_pass ) {
var dbpass = _.template(auth_pass, user_params);
@@ -81,12 +84,14 @@ PgConnection.prototype.setDBConn = function(dbowner, params, callback) {
self.metadataBackend.getUserDBConnectionParams(dbowner, this);
},
function extendParams(err, dbParams){
if (err) throw err;
assert.ifError(err);
// we don't want null values or overwrite a non public user
if (params.dbuser != 'publicuser' || !dbParams.dbuser) {
if (params.dbuser !== 'publicuser' || !dbParams.dbuser) {
delete dbParams.dbuser;
}
if ( dbParams ) _.extend(params, dbParams);
if ( dbParams ) {
_.extend(params, dbParams);
}
return null;
},
function finish(err) {
+3 -6
View File
@@ -1,3 +1,4 @@
var assert = require('assert');
var PSQL = require('cartodb-psql');
var step = require('step');
@@ -18,15 +19,11 @@ PgQueryRunner.prototype.run = function(username, query, queryHandler, callback)
self.pgConnection.setDBAuth(username, params, this);
},
function setConn(err) {
if (err) {
throw err;
}
assert.ifError(err);
self.pgConnection.setDBConn(username, params, this);
},
function executeQuery(err) {
if (err) {
throw err;
}
assert.ifError(err);
var psql = new PSQL({
user: params.dbuser,
pass: params.dbpass,
@@ -1,4 +1,6 @@
var assert = require('assert');
var crypto = require('crypto');
var debug = require('debug')('windshaft:templates');
var step = require('step');
var _ = require('underscore');
var dot = require('dot');
@@ -21,7 +23,9 @@ var util = require('util');
//
//
function TemplateMaps(redis_pool, opts) {
if (!(this instanceof TemplateMaps)) return new TemplateMaps();
if (!(this instanceof TemplateMaps)) {
return new TemplateMaps();
}
EventEmitter.call(this);
@@ -76,13 +80,15 @@ o._redisCmd = function(redisFunc, redisArgs, callback) {
that.redis_pool.acquire(db, this);
},
function executeQuery(err, data) {
if ( err ) throw err;
assert.ifError(err);
redisClient = data;
redisArgs.push(this);
redisClient[redisFunc.toUpperCase()].apply(redisClient, redisArgs);
},
function releaseRedisClient(err, data) {
if ( ! _.isUndefined(redisClient) ) that.redis_pool.release(db, redisClient);
if ( ! _.isUndefined(redisClient) ) {
that.redis_pool.release(db, redisClient);
}
callback(err, data);
}
);
@@ -92,7 +98,7 @@ var _reValidNameIdentifier = /^[a-z0-9][0-9a-z_\-]*$/i;
var _reValidPlaceholderIdentifier = /^[a-z][0-9a-z_]*$/i;
// jshint maxcomplexity:15
o._checkInvalidTemplate = function(template) {
if ( template.version != '0.0.1' ) {
if ( template.version !== '0.0.1' ) {
return new Error("Unsupported template version " + template.version);
}
var tplname = template.name;
@@ -131,10 +137,12 @@ o._checkInvalidTemplate = function(template) {
case 'open':
break;
case 'token':
if ( ! _.isArray(auth.valid_tokens) )
if ( ! _.isArray(auth.valid_tokens) ) {
return new Error("Invalid 'token' authentication: missing valid_tokens");
if ( ! auth.valid_tokens.length )
}
if ( ! auth.valid_tokens.length ) {
return new Error("Invalid 'token' authentication: no valid_tokens");
}
break;
default:
return new Error("Unsupported authentication method: " + auth.method);
@@ -214,9 +222,7 @@ o.addTemplate = function(owner, template, callback) {
self._redisCmd('HLEN', [ userTemplatesKey ], this);
},
function installTemplateIfDoesNotExist(err, numberOfTemplates) {
if ( err ) {
throw err;
}
assert.ifError(err);
if ( limit && numberOfTemplates >= limit ) {
throw new Error("User '" + owner + "' reached limit on number of templates " +
"("+ numberOfTemplates + "/" + limit + ")");
@@ -224,9 +230,7 @@ o.addTemplate = function(owner, template, callback) {
self._redisCmd('HSETNX', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
},
function validateInstallation(err, wasSet) {
if ( err ) {
throw err;
}
assert.ifError(err);
if ( ! wasSet ) {
throw new Error("Template '" + templateName + "' of user '" + owner + "' already exists");
}
@@ -259,9 +263,7 @@ o.delTemplate = function(owner, tpl_id, callback) {
self._redisCmd('HDEL', [ self.key_usr_tpl({ owner:owner }), tpl_id ], this);
},
function handleDeletion(err, deleted) {
if (err) {
throw err;
}
assert.ifError(err);
if (!deleted) {
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
}
@@ -306,37 +308,38 @@ o.updTemplate = function(owner, tpl_id, template, callback) {
var templateName = template.name;
if ( tpl_id != templateName ) {
if ( tpl_id !== templateName ) {
return callback(new Error("Cannot update name of a map template ('" + tpl_id + "' != '" + templateName + "')"));
}
var userTemplatesKey = this.key_usr_tpl({ owner:owner });
var previousTemplate = null;
step(
function getExistingTemplate() {
self._redisCmd('HGET', [ userTemplatesKey, tpl_id ], this);
},
function updateTemplate(err, currentTemplate) {
if (err) {
throw err;
}
if (!currentTemplate) {
function updateTemplate(err, _currentTemplate) {
assert.ifError(err);
if (!_currentTemplate) {
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
}
previousTemplate = _currentTemplate;
self._redisCmd('HSET', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
},
function handleTemplateUpdate(err, didSetNewField) {
if (err) {
throw err;
}
assert.ifError(err);
if (didSetNewField) {
console.warn('New template created on update operation');
debug('New template created on update operation');
}
return true;
},
function finish(err) {
if (!err) {
self.emit('update', owner, templateName, template);
if (self.fingerPrint(JSON.parse(previousTemplate)) !== self.fingerPrint(template)) {
self.emit('update', owner, templateName, template);
}
}
callback(err, template);
@@ -372,7 +375,7 @@ o.getTemplate = function(owner, tpl_id, callback) {
self._redisCmd('HGET', [ self.key_usr_tpl({owner:owner}), tpl_id ], this);
},
function parseTemplate(err, tpl_val) {
if ( err ) throw err;
assert.ifError(err);
return JSON.parse(tpl_val);
},
function finish(err, tpl) {
@@ -472,8 +475,12 @@ o.instance = function(template, params) {
var layergroup = JSON.parse(JSON.stringify(template.layergroup));
for (var i=0; i<layergroup.layers.length; ++i) {
var lyropt = layergroup.layers[i].options;
if ( lyropt.cartocss ) lyropt.cartocss = _replaceVars(lyropt.cartocss, all_params);
if ( lyropt.sql) lyropt.sql = _replaceVars(lyropt.sql, all_params);
if ( lyropt.cartocss ) {
lyropt.cartocss = _replaceVars(lyropt.cartocss, all_params);
}
if ( lyropt.sql) {
lyropt.sql = _replaceVars(lyropt.sql, all_params);
}
// Anything else ?
}
+24
View File
@@ -0,0 +1,24 @@
var LruCache = require('lru-cache');
function LayergroupAffectedTables() {
// dbname + layergroupId -> affected tables cache
this.cache = new LruCache({ max: 2000 });
}
module.exports = LayergroupAffectedTables;
LayergroupAffectedTables.prototype.hasAffectedTables = function(dbName, layergroupId) {
return this.cache.has(createKey(dbName, layergroupId));
};
LayergroupAffectedTables.prototype.set = function(dbName, layergroupId, affectedTables) {
this.cache.set(createKey(dbName, layergroupId), affectedTables);
};
LayergroupAffectedTables.prototype.get = function(dbName, layergroupId) {
return this.cache.get(createKey(dbName, layergroupId));
};
function createKey(dbName, layergroupId) {
return dbName + ':' + layergroupId;
}
+24
View File
@@ -0,0 +1,24 @@
var crypto = require('crypto');
function DatabaseTables(dbName, tableNames) {
this.namespace = 't';
this.dbName = dbName;
this.tableNames = tableNames;
}
module.exports = DatabaseTables;
DatabaseTables.prototype.key = function() {
return this.tableNames.map(function(tableName) {
return this.namespace + ':' + shortHashKey(this.dbName + ':' + tableName);
}.bind(this));
};
DatabaseTables.prototype.getCacheChannel = function() {
return this.dbName + ':' + this.tableNames.join(',');
};
function shortHashKey(target) {
return crypto.createHash('sha256').update(target).digest('base64').substring(0,6);
}
+88
View File
@@ -0,0 +1,88 @@
var _ = require('underscore');
var dot = require('dot');
var NamedMapMapConfigProvider = require('../models/mapconfig/named_map_provider');
var MapConfigNamedLayersAdapter = require('../models/mapconfig_named_layers_adapter');
var templateName = require('../backends/template_maps').templateName;
var queue = require('queue-async');
var LruCache = require("lru-cache");
function NamedMapProviderCache(templateMaps, pgConnection, userLimitsApi, queryTablesApi) {
this.templateMaps = templateMaps;
this.pgConnection = pgConnection;
this.userLimitsApi = userLimitsApi;
this.queryTablesApi = queryTablesApi;
this.namedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
this.providerCache = new LruCache({ max: 2000 });
}
module.exports = NamedMapProviderCache;
NamedMapProviderCache.prototype.get = function(user, templateId, config, authToken, params, callback) {
var namedMapKey = createNamedMapKey(user, templateId);
var namedMapProviders = this.providerCache.get(namedMapKey) || {};
var providerKey = createProviderKey(config, authToken, params);
if (!namedMapProviders.hasOwnProperty(providerKey)) {
namedMapProviders[providerKey] = new NamedMapMapConfigProvider(
this.templateMaps,
this.pgConnection,
this.userLimitsApi,
this.queryTablesApi,
this.namedLayersAdapter,
user,
templateId,
config,
authToken,
params
);
this.providerCache.set(namedMapKey, namedMapProviders);
// early exit, if provider did not exist we just return it
return callback(null, namedMapProviders[providerKey]);
}
var namedMapProvider = namedMapProviders[providerKey];
var self = this;
queue(2)
.defer(namedMapProvider.getTemplate.bind(namedMapProvider))
.defer(this.templateMaps.getTemplate.bind(this.templateMaps), user, templateId)
.awaitAll(function templatesQueueDone(err, results) {
if (err) {
return callback(err);
}
// We want to reset provider its template has changed
// Ideally this should be done in a passive mode where this cache gets notified of template changes
var uniqueFingerprints = _.uniq(results.map(self.templateMaps.fingerPrint)).length;
if (uniqueFingerprints > 1) {
namedMapProvider.reset();
}
return callback(null, namedMapProvider);
});
};
NamedMapProviderCache.prototype.invalidate = function(user, templateId) {
this.providerCache.del(createNamedMapKey(user, templateId));
};
function createNamedMapKey(user, templateId) {
return user + ':' + templateName(templateId);
}
var providerKey = '{{=it.authToken}}:{{=it.configHash}}:{{=it.format}}:{{=it.layer}}:{{=it.scale_factor}}';
var providerKeyTpl = dot.template(providerKey);
function createProviderKey(config, authToken, params) {
var tplValues = _.defaults({}, params, {
authToken: authToken || '',
configHash: NamedMapMapConfigProvider.configHash(config),
layer: '',
format: '',
scale_factor: 1
});
return providerKeyTpl(tplValues);
}
+13 -1
View File
@@ -16,9 +16,21 @@ module.exports = SurrogateKeysCache;
* @param cacheObject should respond to `key() -> String` method
*/
SurrogateKeysCache.prototype.tag = function(response, cacheObject) {
response.header('Surrogate-Key', cacheObject.key());
var newKey = cacheObject.key();
response.set('Surrogate-Key', appendSurrogateKey(
response.get('Surrogate-Key'),
Array.isArray(newKey) ? cacheObject.key().join(' ') : newKey
));
};
function appendSurrogateKey(currentKey, newKey) {
if (!!currentKey) {
newKey = currentKey + ' ' + newKey;
}
return newKey;
}
/**
* @param cacheObject should respond to `key() -> String` method
* @param {Function} callback
-217
View File
@@ -1,217 +0,0 @@
var _ = require('underscore');
var step = require('step');
var Windshaft = require('windshaft');
var os = require('os');
var HealthCheck = require('./monitoring/health_check');
var SurrogateKeysCache = require('./cache/surrogate_keys_cache');
var NamedMapsCacheEntry = require('./cache/model/named_maps_entry');
var VarnishHttpCacheBackend = require('./cache/backend/varnish_http');
var FastlyCacheBackend = require('./cache/backend/fastly');
if ( ! process.env.PGAPPNAME )
process.env.PGAPPNAME='cartodb_tiler';
var CartodbWindshaft = function(serverOptions) {
// Perform keyword substitution in statsd
// See https://github.com/CartoDB/Windshaft-cartodb/issues/153
if ( global.environment.statsd ) {
if ( global.environment.statsd.prefix ) {
var host_token = os.hostname().split('.').reverse().join('.');
global.environment.statsd.prefix = global.environment.statsd.prefix.replace(/:host/, host_token);
}
}
var redisPool = serverOptions.redis.pool ||
require('redis-mpool')(_.extend(global.environment.redis, {name: 'windshaft:cartodb'}));
var cartoData = require('cartodb-redis')({pool: redisPool});
var templateMaps = serverOptions.templateMaps;
// This is for Templated maps
//
// "named" is the official, "template" is for backward compatibility up to 1.6.x
//
var template_baseurl = global.environment.base_url_templated || '(?:/maps/named|/tiles/template)';
var surrogateKeysCacheBackends = [];
if (serverOptions.varnish_purge_enabled) {
surrogateKeysCacheBackends.push(
new VarnishHttpCacheBackend(serverOptions.varnish_host, serverOptions.varnish_http_port)
);
}
if (!!serverOptions.fastly.enabled && !!serverOptions.fastly.apiKey && !!serverOptions.fastly.serviceId) {
surrogateKeysCacheBackends.push(
new FastlyCacheBackend(serverOptions.fastly.apiKey, serverOptions.fastly.serviceId)
);
}
var surrogateKeysCache = new SurrogateKeysCache(surrogateKeysCacheBackends);
function invalidateNamedMap (owner, templateName) {
var startTime = Date.now();
surrogateKeysCache.invalidate(new NamedMapsCacheEntry(owner, templateName), function(err) {
var logMessage = JSON.stringify({
username: owner,
type: 'named_map_invalidation',
elapsed: Date.now() - startTime,
error: !!err ? JSON.stringify(err.message) : undefined
});
if (err) {
console.warn(logMessage);
} else {
console.info(logMessage);
}
});
}
['update', 'delete'].forEach(function(eventType) {
templateMaps.on(eventType, invalidateNamedMap);
});
// boot
var ws = new Windshaft.Server(serverOptions);
// Override getVersion to include cartodb-specific versions
var wsversion = ws.getVersion;
ws.getVersion = function() {
var version = wsversion();
version.windshaft_cartodb = require('../../package.json').version;
return version;
};
var ws_sendResponse = ws.sendResponse;
// GET routes for which we don't want to request any caching.
// POST/PUT/DELETE requests are never cached anyway.
var noCacheGETRoutes = [
'/',
'/version',
// See https://github.com/CartoDB/Windshaft-cartodb/issues/176
serverOptions.base_url_mapconfig,
serverOptions.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format',
template_baseurl,
template_baseurl + '/:template_id',
template_baseurl + '/:template_id/jsonp'
];
ws.sendResponse = function(res, args) {
var that = this;
var thatArgs = arguments;
var statusCode;
if ( res._windshaftStatusCode ) {
// Added by our override of sendError
statusCode = res._windshaftStatusCode;
} else {
if ( args.length > 2 ) statusCode = args[2];
else {
statusCode = args[1] || 200;
}
}
var req = res.req;
step (
function addCacheChannel() {
if ( ! req ) {
// having no associated request can happen when
// using fake response objects for testing layergroup
// creation
return false;
}
if ( ! req.params ) {
// service requests (/version, /)
// have no need for an X-Cache-Channel
return false;
}
if ( statusCode != 200 ) {
// We do not want to cache
// unsuccessful responses
return false;
}
if ( _.contains(noCacheGETRoutes, req.route.path) ) {
//console.log("Skipping cache channel in route:\n" + req.route.path);
return false;
}
//console.log("Adding cache channel to route\n" + req.route.path + " not matching any in:\n" +
// mapCreateRoutes.join("\n"));
serverOptions.addCacheChannel(that, req, this);
},
function sendResponse(err/*, added*/) {
if ( err ) console.log(err + err.stack);
ws_sendResponse.apply(that, thatArgs);
return null;
},
function finish(err) {
if ( err ) console.log(err + err.stack);
}
);
};
var ws_sendError = ws.sendError;
ws.sendError = function() {
var res = arguments[0];
var statusCode = arguments[2];
res._windshaftStatusCode = statusCode;
ws_sendError.apply(this, arguments);
};
/*******************************************************************************************************************
* Routing
******************************************************************************************************************/
var NamedMapsController = require('./controllers/named_maps'),
namedMapsController = new NamedMapsController(
ws,
serverOptions,
templateMaps,
cartoData,
template_baseurl,
surrogateKeysCache
);
namedMapsController.register(ws);
var TablesExtentApi = require('./api/tables_extent_api');
var tablesExtentApi = new TablesExtentApi(serverOptions.pgQueryRunner);
var NamedStaticMapsController = require('./controllers/named_static_maps');
var namedStaticMapsController = new NamedStaticMapsController(
ws,
serverOptions,
templateMaps,
ws.staticMapBackend,
surrogateKeysCache,
tablesExtentApi
);
namedStaticMapsController.register(ws);
/*******************************************************************************************************************
* END Routing
******************************************************************************************************************/
var healthCheck = new HealthCheck(global.environment.disabled_file);
ws.get('/health', function(req, res) {
var healthConfig = global.environment.health || {};
if (!!healthConfig.enabled) {
var startTime = Date.now();
healthCheck.check(function(err) {
var ok = !err;
var response = {
enabled: true,
ok: ok,
elapsed: Date.now() - startTime
};
if (err) {
response.err = err.message;
}
res.send(response, ok ? 200 : 503);
});
} else {
res.send({enabled: false, ok: true}, 200);
}
});
return ws;
};
module.exports = CartodbWindshaft;
+250
View File
@@ -0,0 +1,250 @@
var assert = require('assert');
var _ = require('underscore');
var step = require('step');
var debug = require('debug')('windshaft:cartodb');
var LZMA = require('lzma').LZMA;
var lzmaWorker = new LZMA();
// Whitelist query parameters and attach format
var REQUEST_QUERY_PARAMS_WHITELIST = [
'config',
'map_key',
'api_key',
'auth_token',
'callback'
];
function BaseController(authApi, pgConnection) {
this.authApi = authApi;
this.pgConnection = pgConnection;
}
module.exports = BaseController;
// jshint maxcomplexity:9
/**
* Whitelist input and get database name & default geometry type from
* subdomain/user metadata held in CartoDB Redis
* @param req - standard express request obj. Should have host & table
* @param callback
*/
BaseController.prototype.req2params = function(req, callback){
var self = this;
if ( req.query.lzma ) {
// Decode (from base64)
var lzma = new Buffer(req.query.lzma, 'base64')
.toString('binary')
.split('')
.map(function(c) {
return c.charCodeAt(0) - 128;
});
// Decompress
lzmaWorker.decompress(
lzma,
function(result) {
if (req.profiler) {
req.profiler.done('lzma');
}
try {
delete req.query.lzma;
_.extend(req.query, JSON.parse(result));
self.req2params(req, callback);
} catch (err) {
req.profiler.done('req2params');
callback(new Error('Error parsing lzma as JSON: ' + err));
}
}
);
return;
}
req.query = _.pick(req.query, REQUEST_QUERY_PARAMS_WHITELIST);
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
var user = req.context.user;
if ( req.params.token ) {
// Token might match the following patterns:
// - {user}@{tpl_id}@{token}:{cache_buster}
var tksplit = req.params.token.split(':');
req.params.token = tksplit[0];
if ( tksplit.length > 1 ) {
req.params.cache_buster= tksplit[1];
}
tksplit = req.params.token.split('@');
if ( tksplit.length > 1 ) {
req.params.signer = tksplit.shift();
if ( ! req.params.signer ) {
req.params.signer = user;
}
else if ( req.params.signer !== user ) {
var err = new Error(
'Cannot use map signature of user "' + req.params.signer + '" on db of user "' + user + '"'
);
err.http_status = 403;
req.profiler.done('req2params');
callback(err);
return;
}
if ( tksplit.length > 1 ) {
/*var template_hash = */tksplit.shift(); // unused
}
req.params.token = tksplit.shift();
}
}
// bring all query values onto req.params object
_.extend(req.params, req.query);
if (req.profiler) {
req.profiler.done('req2params.setup');
}
step(
function getPrivacy(){
self.authApi.authorize(req, this);
},
function validateAuthorization(err, authorized) {
if (req.profiler) {
req.profiler.done('authorize');
}
assert.ifError(err);
if(!authorized) {
err = new Error("Sorry, you are unauthorized (permission denied)");
err.http_status = 403;
throw err;
}
return null;
},
function getDatabase(err){
assert.ifError(err);
self.pgConnection.setDBConn(user, req.params, this);
},
function finishSetup(err) {
if ( err ) {
req.profiler.done('req2params');
return callback(err, req);
}
// Add default database connection parameters
// if none given
_.defaults(req.params, {
dbuser: global.environment.postgres.user,
dbpassword: global.environment.postgres.password,
dbhost: global.environment.postgres.host,
dbport: global.environment.postgres.port
});
req.profiler.done('req2params');
callback(null, req);
}
);
};
// jshint maxcomplexity:6
// jshint maxcomplexity:9
BaseController.prototype.send = function(req, res, body, status, headers) {
if (req.params.dbhost) {
res.set('X-Served-By-DB-Host', req.params.dbhost);
}
if (req.profiler) {
res.set('X-Tiler-Profiler', req.profiler.toJSONString());
}
if (headers) {
res.set(headers);
}
res.status(status);
if (!Buffer.isBuffer(body) && typeof body === 'object') {
if (req.query && req.query.callback) {
res.jsonp(body);
} else {
res.json(body);
}
} else {
res.send(body);
}
if (req.profiler) {
try {
// May throw due to dns, see
// See http://github.com/CartoDB/Windshaft/issues/166
req.profiler.sendStats();
} catch (err) {
debug("error sending profiling stats: " + err);
}
}
};
// jshint maxcomplexity:6
BaseController.prototype.sendError = function(req, res, err, label) {
label = label || 'UNKNOWN';
var statusCode = findStatusCode(err);
debug('[%s ERROR] -- %d: %s', label, statusCode, err);
// If a callback was requested, force status to 200
if (req.query && req.query.callback) {
statusCode = 200;
}
var errorResponseBody = { errors: [errorMessage(err)] };
this.send(req, res, errorResponseBody, statusCode);
};
function errorMessage(err) {
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
// Strip connection info, if any
return message
// See https://github.com/CartoDB/Windshaft/issues/173
.replace(/Connection string: '[^']*'\n\s/im, '')
// See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644
.replace(/is the server.*encountered/im, 'encountered');
}
module.exports.errorMessage = errorMessage;
function findStatusCode(err) {
var statusCode;
if ( err.http_status ) {
statusCode = err.http_status;
} else {
statusCode = statusFromErrorMessage('' + err);
}
return statusCode;
}
module.exports.findStatusCode = findStatusCode;
function statusFromErrorMessage(errMsg) {
// Find an appropriate statusCode based on message
var statusCode = 400;
if ( -1 !== errMsg.indexOf('permission denied') ) {
statusCode = 403;
}
else if ( -1 !== errMsg.indexOf('authentication failed') ) {
statusCode = 403;
}
else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) {
statusCode = 400;
}
else if ( -1 !== errMsg.indexOf('does not exist') ) {
if ( -1 !== errMsg.indexOf(' role ') ) {
statusCode = 403; // role 'xxx' does not exist
} else {
statusCode = 404;
}
}
return statusCode;
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
Layergroup: require('./layergroup'),
Map: require('./map'),
NamedMaps: require('./named_maps'),
NamedMapsAdmin: require('./named_maps_admin'),
ServerInfo: require('./server_info')
};
+300
View File
@@ -0,0 +1,300 @@
var assert = require('assert');
var step = require('step');
var util = require('util');
var BaseController = require('./base');
var cors = require('../middleware/cors');
var MapStoreMapConfigProvider = require('../models/mapconfig/map_store_provider');
var TablesCacheEntry = require('../cache/model/database_tables_entry');
/**
* @param app
* @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 {QueryTablesApi} queryTablesApi
* @param {LayergroupAffectedTables} layergroupAffectedTables
* @constructor
*/
function LayergroupController(app, authApi, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
surrogateKeysCache, userLimitsApi, queryTablesApi, layergroupAffectedTables) {
BaseController.call(this, authApi, pgConnection);
this.app = app;
this.mapStore = mapStore;
this.tileBackend = tileBackend;
this.previewBackend = previewBackend;
this.attributesBackend = attributesBackend;
this.surrogateKeysCache = surrogateKeysCache;
this.userLimitsApi = userLimitsApi;
this.queryTablesApi = queryTablesApi;
this.layergroupAffectedTables = layergroupAffectedTables;
}
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(), this.tile.bind(this));
app.get(app.base_url_mapconfig + '/:token/:z/:x/:y.:format', cors(), this.tile.bind(this));
app.get(app.base_url_mapconfig + '/:token/:layer/:z/:x/:y.(:format)', cors(), this.layer.bind(this));
app.get(app.base_url_mapconfig + '/:token/:layer/attributes/:fid', cors(), this.attributes.bind(this));
app.get(app.base_url_mapconfig + '/static/center/:token/:z/:lat/:lng/:width/:height.:format', cors(),
this.center.bind(this));
app.get(app.base_url_mapconfig + '/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', cors(),
this.bbox.bind(this));
};
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;
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) {
var tablesCacheEntry = new TablesCacheEntry(dbName, affectedTables);
res.set('X-Cache-Channel', tablesCacheEntry.getCacheChannel());
self.surrogateKeysCache.tag(res, tablesCacheEntry);
}
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");
}
self.queryTablesApi.getAffectedTablesInQuery(user, sql, this); // in addCacheChannel
},
function buildCacheChannel(err, tableNames) {
assert.ifError(err);
self.layergroupAffectedTables.set(dbName, layergroupId, tableNames);
return tableNames;
},
function finish(err, affectedTables) {
callback(err, affectedTables);
}
);
};
+327
View File
@@ -0,0 +1,327 @@
var _ = require('underscore');
var assert = require('assert');
var step = require('step');
var windshaft = require('windshaft');
var util = require('util');
var BaseController = require('./base');
var cors = require('../middleware/cors');
var MapConfig = windshaft.model.MapConfig;
var Datasource = windshaft.model.Datasource;
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
var TablesCacheEntry = require('../cache/model/database_tables_entry');
var MapConfigNamedLayersAdapter = require('../models/mapconfig_named_layers_adapter');
var NamedMapMapConfigProvider = require('../models/mapconfig/named_map_provider');
var CreateLayergroupMapConfigProvider = require('../models/mapconfig/create_layergroup_provider');
/**
* @param app
* @param {AuthApi} authApi
* @param {PgConnection} pgConnection
* @param {TemplateMaps} templateMaps
* @param {MapBackend} mapBackend
* @param metadataBackend
* @param {QueryTablesApi} queryTablesApi
* @param {SurrogateKeysCache} surrogateKeysCache
* @param {UserLimitsApi} userLimitsApi
* @param {LayergroupAffectedTables} layergroupAffectedTables
* @constructor
*/
function MapController(app, authApi, pgConnection, templateMaps, mapBackend, metadataBackend, queryTablesApi,
surrogateKeysCache, userLimitsApi, layergroupAffectedTables) {
BaseController.call(this, authApi, pgConnection);
this.app = app;
this.pgConnection = pgConnection;
this.templateMaps = templateMaps;
this.mapBackend = mapBackend;
this.metadataBackend = metadataBackend;
this.queryTablesApi = queryTablesApi;
this.surrogateKeysCache = surrogateKeysCache;
this.userLimitsApi = userLimitsApi;
this.layergroupAffectedTables = layergroupAffectedTables;
this.namedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
}
util.inherits(MapController, BaseController);
module.exports = MapController;
MapController.prototype.register = function(app) {
app.get(app.base_url_mapconfig, cors(), this.createGet.bind(this));
app.post(app.base_url_mapconfig, cors(), this.createPost.bind(this));
app.get(app.base_url_templated + '/:template_id/jsonp', cors(), this.jsonp.bind(this));
app.post(app.base_url_templated + '/:template_id', cors(), this.instantiate.bind(this));
app.options(app.base_url_mapconfig, cors('Content-Type'));
};
MapController.prototype.createGet = function(req, res){
req.profiler.start('windshaft.createmap_get');
this.create(req, res, function createGet$prepareConfig(err, req) {
assert.ifError(err);
if ( ! req.params.config ) {
throw new Error('layergroup GET needs a "config" parameter');
}
return JSON.parse(req.params.config);
});
};
MapController.prototype.createPost = function(req, res) {
req.profiler.start('windshaft.createmap_post');
this.create(req, res, function createPost$prepareConfig(err, req) {
assert.ifError(err);
if (!req.is('application/json')) {
throw new Error('layergroup POST data must be of type application/json');
}
return req.body;
});
};
MapController.prototype.instantiate = function(req, res) {
if (req.profiler) {
req.profiler.start('windshaft-cartodb.instance_template_post');
}
this.instantiateTemplate(req, res, function prepareTemplateParams(callback) {
if (!req.is('application/json')) {
return callback(new Error('Template POST data must be of type application/json'));
}
return callback(null, req.body);
});
};
MapController.prototype.jsonp = function(req, res) {
if (req.profiler) {
req.profiler.start('windshaft-cartodb.instance_template_get');
}
this.instantiateTemplate(req, res, function prepareJsonTemplateParams(callback) {
var err = null;
if ( req.query.callback === undefined || req.query.callback.length === 0) {
err = new Error('callback parameter should be present and be a function name');
}
var templateParams = {};
if (req.query.config) {
try {
templateParams = JSON.parse(req.query.config);
} catch(e) {
err = new Error('Invalid config parameter, should be a valid JSON');
}
}
return callback(err, templateParams);
});
};
MapController.prototype.create = function(req, res, prepareConfigFn) {
var self = this;
var mapConfig;
step(
function setupParams(){
self.req2params(req, this);
},
prepareConfigFn,
function beforeLayergroupCreate(err, requestMapConfig) {
assert.ifError(err);
var next = this;
self.namedLayersAdapter.getLayers(req.context.user, requestMapConfig.layers, self.pgConnection,
function(err, layers, datasource) {
if (err) {
return next(err);
}
if (layers) {
requestMapConfig.layers = layers;
}
return next(null, requestMapConfig, datasource);
}
);
},
function createLayergroup(err, requestMapConfig, datasource) {
assert.ifError(err);
mapConfig = new MapConfig(requestMapConfig, datasource || Datasource.EmptyDatasource());
self.mapBackend.createLayergroup(
mapConfig, req.params,
new CreateLayergroupMapConfigProvider(mapConfig, req.context.user, self.userLimitsApi, req.params),
this
);
},
function afterLayergroupCreate(err, layergroup) {
assert.ifError(err);
self.afterLayergroupCreate(req, res, mapConfig, layergroup, this);
},
function finish(err, layergroup) {
if (err) {
self.sendError(req, res, err, 'ANONYMOUS LAYERGROUP');
} else {
res.set('X-Layergroup-Id', layergroup.layergroupid);
self.send(req, res, layergroup, 200);
}
}
);
};
MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn) {
var self = this;
var cdbuser = req.context.user;
var mapConfigProvider;
var mapConfig;
step(
function setupParams(){
self.req2params(req, this);
},
function getTemplateParams() {
prepareParamsFn(this);
},
function getTemplate(err, templateParams) {
assert.ifError(err);
mapConfigProvider = new NamedMapMapConfigProvider(
self.templateMaps,
self.pgConnection,
self.userLimitsApi,
self.queryTablesApi,
self.namedLayersAdapter,
cdbuser,
req.params.template_id,
templateParams,
req.query.auth_token,
req.params
);
mapConfigProvider.getMapConfig(this);
},
function createLayergroup(err, mapConfig_, rendererParams/*, context*/) {
assert.ifError(err);
mapConfig = mapConfig_;
self.mapBackend.createLayergroup(
mapConfig, rendererParams,
new CreateLayergroupMapConfigProvider(mapConfig, cdbuser, self.userLimitsApi, rendererParams),
this
);
},
function afterLayergroupCreate(err, layergroup) {
assert.ifError(err);
self.afterLayergroupCreate(req, res, mapConfig, layergroup, this);
},
function finishTemplateInstantiation(err, layergroup) {
if (err) {
self.sendError(req, res, err, 'NAMED MAP LAYERGROUP');
} else {
var templateHash = self.templateMaps.fingerPrint(mapConfigProvider.template).substring(0, 8);
layergroup.layergroupid = cdbuser + '@' + templateHash + '@' + layergroup.layergroupid;
res.set('X-Layergroup-Id', layergroup.layergroupid);
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, mapConfigProvider.getTemplateName()));
self.send(req, res, layergroup, 200);
}
}
);
};
MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, layergroup, callback) {
var self = this;
var username = req.context.user;
var tasksleft = 2; // redis key and affectedTables
var errors = [];
var done = function(err) {
if ( err ) {
errors.push('' + err);
}
if ( ! --tasksleft ) {
err = errors.length ? new Error(errors.join('\n')) : null;
callback(err, layergroup);
}
};
// include in layergroup response the variables in serverMedata
// those variables are useful to send to the client information
// about how to reach this server or information about it
_.extend(layergroup, global.environment.serverMetadata);
// Don't wait for the mapview count increment to
// take place before proceeding. Error will be logged
// asynchronously
this.metadataBackend.incMapviewCount(username, mapconfig.obj().stat_tag, function(err) {
if (req.profiler) {
req.profiler.done('incMapviewCount');
}
if ( err ) {
global.logger.log("ERROR: failed to increment mapview count for user '" + username + "': " + err);
}
done();
});
var sql = mapconfig.getLayers().map(function(layer) {
return layer.options.sql;
}).join(';');
var dbName = req.params.dbname;
var layergroupId = layergroup.layergroupid;
step(
function checkCachedAffectedTables() {
return self.layergroupAffectedTables.hasAffectedTables(dbName, layergroupId);
},
function getAffectedTablesAndLastUpdatedTime(err, hasCache) {
assert.ifError(err);
if (hasCache) {
var next = this;
var affectedTables = self.layergroupAffectedTables.get(dbName, layergroupId);
self.queryTablesApi.getLastUpdatedTime(username, affectedTables, function(err, lastUpdatedTime) {
if (err) {
return next(err);
}
return next(null, { affectedTables: affectedTables, lastUpdatedTime: lastUpdatedTime });
});
} else {
self.queryTablesApi.getAffectedTablesAndLastUpdatedTime(username, sql, this);
}
},
function handleAffectedTablesAndLastUpdatedTime(err, result) {
if (req.profiler) {
req.profiler.done('queryTablesAndLastUpdated');
}
assert.ifError(err);
self.layergroupAffectedTables.set(dbName, layergroupId, result.affectedTables);
// last update for layergroup cache buster
layergroup.layergroupid = layergroup.layergroupid + ':' + result.lastUpdatedTime;
layergroup.last_updated = new Date(result.lastUpdatedTime).toISOString();
if (req.method === 'GET') {
var tableCacheEntry = new TablesCacheEntry(dbName, result.affectedTables);
var ttl = global.environment.varnish.layergroupTtl || 86400;
res.set('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
res.set('Last-Modified', (new Date()).toUTCString());
res.set('X-Cache-Channel', tableCacheEntry.getCacheChannel());
if (result.affectedTables && result.affectedTables.length > 0) {
self.surrogateKeysCache.tag(res, tableCacheEntry);
}
}
return null;
},
function finish(err) {
done(err);
}
);
};
+228 -322
View File
@@ -1,372 +1,278 @@
var step = require('step');
var assert = require('assert');
var _ = require('underscore');
var templateName = require('../template_maps').templateName;
var CdbRequest = require('../models/cdb_request');
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
function NamedMapsController(app, serverOptions, templateMaps, metadataBackend, templateBaseUrl, surrogateKeysCache) {
var util = require('util');
var BaseController = require('./base');
var cors = require('../middleware/cors');
var TablesCacheEntry = require('../cache/model/database_tables_entry');
function NamedMapsController(app, authApi, pgConnection, namedMapProviderCache, tileBackend, previewBackend,
surrogateKeysCache, tablesExtentApi, metadataBackend) {
BaseController.call(this, authApi, pgConnection);
this.app = app;
this.serverOptions = serverOptions;
this.templateMaps = templateMaps;
this.metadataBackend = metadataBackend;
this.templateBaseUrl = templateBaseUrl;
this.namedMapProviderCache = namedMapProviderCache;
this.tileBackend = tileBackend;
this.previewBackend = previewBackend;
this.surrogateKeysCache = surrogateKeysCache;
this.tablesExtentApi = tablesExtentApi;
this.metadataBackend = metadataBackend;
}
util.inherits(NamedMapsController, BaseController);
module.exports = NamedMapsController;
var cdbRequest = new CdbRequest();
NamedMapsController.prototype.register = function(app) {
app.get(this.templateBaseUrl + '/:template_id/jsonp', this.jsonp.bind(this));
app.post(this.templateBaseUrl, this.create.bind(this));
app.put(this.templateBaseUrl + '/:template_id', this.update.bind(this));
app.get(this.templateBaseUrl + '/:template_id', this.retrieve.bind(this));
app.del(this.templateBaseUrl + '/:template_id', this.destroy.bind(this));
app.get(this.templateBaseUrl, this.list.bind(this));
app.options(this.templateBaseUrl + '/:template_id', this.options.bind(this));
app.post(this.templateBaseUrl + '/:template_id', this.instantiate.bind(this));
};
// Add a template
NamedMapsController.prototype.create = function(req, res) {
var self = this;
this.app.doCORS(res);
var cdbuser = cdbRequest.userByReq(req);
step(
function checkPerms(){
self.serverOptions.authorizedByAPIKey(req, this);
},
function addTemplate(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
ifInvalidContentType(req, 'template POST data must be of type application/json');
var cfg = req.body;
self.templateMaps.addTemplate(cdbuser, cfg, this);
},
function prepareResponse(err, tpl_id){
assert.ifError(err);
return { template_id: tpl_id };
},
finishFn(self.app, res, 'POST TEMPLATE')
app.get(app.base_url_templated + '/:template_id/:layer/:z/:x/:y.(:format)', cors(), this.tile.bind(this));
app.get(
app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format', cors(), this.staticMap.bind(this)
);
};
// Update a template
NamedMapsController.prototype.update = function(req, res) {
NamedMapsController.prototype.sendResponse = function(req, res, resource, headers, namedMapProvider) {
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(req.context.user, namedMapProvider.getTemplateName()));
res.set('Content-Type', headers['content-type'] || headers['Content-Type'] || 'image/png');
res.set('Cache-Control', 'public,max-age=7200,must-revalidate');
var self = this;
this.app.doCORS(res);
var cdbuser = cdbRequest.userByReq(req);
var template;
var tpl_id;
var dbName = req.params.dbname;
step(
function checkPerms(){
self.serverOptions.authorizedByAPIKey(req, this);
function getAffectedTablesAndLastUpdatedTime() {
namedMapProvider.getAffectedTablesAndLastUpdatedTime(this);
},
function updateTemplate(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated user can update templated maps');
ifInvalidContentType(req, 'template PUT data must be of type application/json');
template = req.body;
tpl_id = templateName(req.params.template_id);
self.templateMaps.updTemplate(cdbuser, tpl_id, template, this);
},
function prepareResponse(err){
assert.ifError(err);
return { template_id: tpl_id };
},
finishFn(self.app, res, 'PUT TEMPLATE')
);
};
// Get a specific template
NamedMapsController.prototype.retrieve = function(req, res) {
var self = this;
if (req.profiler) {
req.profiler.start('windshaft-cartodb.get_template');
}
this.app.doCORS(res);
var cdbuser = cdbRequest.userByReq(req);
var tpl_id;
step(
function checkPerms(){
self.serverOptions.authorizedByAPIKey(req, this);
},
function updateTemplate(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
tpl_id = templateName(req.params.template_id);
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
},
function prepareResponse(err, tpl_val) {
if ( err ) throw err;
if ( ! tpl_val ) {
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
err.http_status = 404;
throw err;
function sendResponse(err, result) {
req.profiler.done('affectedTables');
if (err) {
global.logger.log('ERROR generating cache channel: ' + err);
}
// auth_id was added by ourselves,
// so we remove it before returning to the user
delete tpl_val.auth_id;
return { template: tpl_val };
},
finishFn(self.app, res, 'GET TEMPLATE')
);
};
if (!result || !!result.affectedTables) {
// we increase cache control as we can invalidate it
res.set('Cache-Control', 'public,max-age=31536000');
// Delete a specific template
NamedMapsController.prototype.destroy = function(req, res) {
var self = this;
var lastModifiedDate;
if (Number.isFinite(result.lastUpdatedTime)) {
lastModifiedDate = new Date(result.lastUpdatedTime);
} else {
lastModifiedDate = new Date();
}
res.set('Last-Modified', lastModifiedDate.toUTCString());
if (req.profiler) {
req.profiler.start('windshaft-cartodb.delete_template');
}
this.app.doCORS(res);
var cdbuser = cdbRequest.userByReq(req);
var tpl_id;
step(
function checkPerms(){
self.serverOptions.authorizedByAPIKey(req, this);
},
function updateTemplate(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated users can delete template maps');
tpl_id = templateName(req.params.template_id);
self.templateMaps.delTemplate(cdbuser, tpl_id, this);
},
function prepareResponse(err/*, tpl_val*/){
if ( err ) throw err;
return { status: 'ok' };
},
finishFn(self.app, res, 'DELETE TEMPLATE', ['', 204])
);
};
// Get a list of owned templates
NamedMapsController.prototype.list = function(req, res) {
var self = this;
if ( req.profiler ) {
req.profiler.start('windshaft-cartodb.get_template_list');
}
this.app.doCORS(res);
var cdbuser = cdbRequest.userByReq(req);
step(
function checkPerms(){
self.serverOptions.authorizedByAPIKey(req, this);
},
function listTemplates(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated user can list templated maps');
self.templateMaps.listTemplates(cdbuser, this);
},
function prepareResponse(err, tpl_ids){
assert.ifError(err);
return { template_ids: tpl_ids };
},
finishFn(self.app, res, 'GET TEMPLATE LIST')
);
};
NamedMapsController.prototype.instantiate = function(req, res) {
var self = this;
if (req.profiler) {
req.profiler.start('windshaft-cartodb.instance_template_post');
}
step(
function() {
ifInvalidContentType(req, 'template POST data must be of type application/json');
self.instantiateTemplate(req, res, req.body, this);
}, function(err, response) {
self.finish_instantiation(err, response, res);
}
);
};
NamedMapsController.prototype.options = function(req, res, next) {
this.app.doCORS(res, "Content-Type");
return next();
};
/**
* jsonp endpoint, allows to instantiate a template with a json call.
* callback query argument is mandatory
*/
NamedMapsController.prototype.jsonp = function(req, res) {
var self = this;
if (req.profiler) {
req.profiler.start('windshaft-cartodb.instance_template_get');
}
step(
function() {
if ( req.query.callback === undefined || req.query.callback.length === 0) {
throw new Error('callback parameter should be present and be a function name');
}
var config = {};
if(req.query.config) {
try {
config = JSON.parse(req.query.config);
} catch(e) {
throw new Error('badformed config parameter, should be a valid JSON');
var tablesCacheEntry = new TablesCacheEntry(dbName, result.affectedTables);
res.set('X-Cache-Channel', tablesCacheEntry.getCacheChannel());
if (result.affectedTables.length > 0) {
self.surrogateKeysCache.tag(res, tablesCacheEntry);
}
}
self.instantiateTemplate(req, res, config, this);
}, function(err, response) {
self.finish_instantiation(err, response, res);
self.send(req, res, resource, 200);
}
);
};
// Instantiate a template
NamedMapsController.prototype.instantiateTemplate = function(req, res, template_params, callback) {
NamedMapsController.prototype.tile = function(req, res) {
var self = this;
this.app.doCORS(res);
var cdbUser = req.context.user;
var template;
var layergroup;
var fakereq; // used for call to createLayergroup
var cdbuser = cdbRequest.userByReq(req);
// Format of template_id: [<template_owner>]@<template_id>
var tpl_id = templateName(req.params.template_id);
var auth_token = req.query.auth_token;
var namedMapProvider;
step(
function getTemplate(){
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
function reqParams() {
self.req2params(req, this);
},
function checkAuthorized(err, templateValue) {
if ( req.profiler ) req.profiler.done('getTemplate');
if ( err ) throw err;
if ( ! templateValue ) {
err = new Error("Template '" + tpl_id + "' of user '" + cdbuser + "' not found");
err.http_status = 404;
throw err;
}
template = templateValue;
var authorized = false;
try {
authorized = self.templateMaps.isAuthorized(template, auth_token);
} catch (err) {
// we catch to add http_status
err.http_status = 403;
throw err;
}
if ( ! authorized ) {
err = new Error('Unauthorized template instanciation');
err.http_status = 403;
throw err;
}
function getNamedMapProvider(err) {
assert.ifError(err);
self.namedMapProviderCache.get(
cdbUser,
req.params.template_id,
req.query.config,
req.query.auth_token,
req.params,
this
);
},
function getTile(err, _namedMapProvider) {
assert.ifError(err);
namedMapProvider = _namedMapProvider;
self.tileBackend.getTile(namedMapProvider, req.params, this);
},
function handleImage(err, tile, headers, stats) {
if (req.profiler) {
req.profiler.done('authorizedByCert');
req.profiler.add(stats);
}
return self.templateMaps.instance(template, template_params);
},
function prepareParams(err, instance){
if ( req.profiler ) req.profiler.done('TemplateMaps_instance');
if ( err ) throw err;
layergroup = instance;
fakereq = {
query: {},
params: {
user: req.params.user
},
headers: _.clone(req.headers),
context: _.clone(req.context),
method: req.method,
res: res,
profiler: req.profiler
};
self.serverOptions.setDBParams(cdbuser, fakereq.params, this);
},
function setApiKey(err){
if ( req.profiler ) req.profiler.done('setDBParams');
if ( err ) throw err;
self.metadataBackend.getUserMapKey(cdbuser, this);
},
function createLayergroup(err, val) {
if ( req.profiler ) req.profiler.done('getUserMapKey');
if ( err ) throw err;
fakereq.params.api_key = val;
self.app.createLayergroup(layergroup, fakereq, this);
},
function prepareResponse(err, layergroup) {
if ( err ) {
return callback(err, { errors: [''+err] });
if (err) {
self.sendError(req, res, err, 'NAMED_MAP_TILE');
} else {
self.sendResponse(req, res, tile, headers, namedMapProvider);
}
var tplhash = self.templateMaps.fingerPrint(template).substring(0,8);
layergroup.layergroupid = cdbuser + '@' + tplhash + '@' + layergroup.layergroupid;
res.header('X-Layergroup-Id', layergroup.layergroupid);
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, template.name));
callback(null, layergroup);
}
);
};
NamedMapsController.prototype.finish_instantiation = function(err, response, res) {
if (err) {
var statusCode = 400;
response = { errors: [''+err] };
if ( ! _.isUndefined(err.http_status) ) {
statusCode = err.http_status;
NamedMapsController.prototype.staticMap = function(req, res) {
var self = this;
var cdbUser = req.context.user;
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
req.params.format = 'png';
req.params.layer = 'all';
var namedMapProvider;
step(
function reqParams() {
self.req2params(req, this);
},
function getNamedMapProvider(err) {
assert.ifError(err);
self.namedMapProviderCache.get(
cdbUser,
req.params.template_id,
req.query.config,
req.query.auth_token,
req.params,
this
);
},
function prepareImageOptions(err, _namedMapProvider) {
assert.ifError(err);
namedMapProvider = _namedMapProvider;
self.getStaticImageOptions(cdbUser, namedMapProvider, this);
},
function getImage(err, imageOpts) {
assert.ifError(err);
var width = +req.params.width;
var height = +req.params.height;
if (!_.isUndefined(imageOpts.zoom) && imageOpts.center) {
self.previewBackend.getImage(
namedMapProvider, format, width, height, imageOpts.zoom, imageOpts.center, this);
} else {
self.previewBackend.getImage(
namedMapProvider, format, width, height, imageOpts.bounds, this);
}
},
function incrementMapViews(err, image, headers, stats) {
assert.ifError(err);
var next = this;
namedMapProvider.getMapConfig(function(mapConfigErr, mapConfig) {
self.metadataBackend.incMapviewCount(cdbUser, mapConfig.obj().stat_tag, function(sErr) {
if (err) {
global.logger.log("ERROR: failed to increment mapview count for user '%s': %s", cdbUser, sErr);
}
next(err, image, headers, stats);
});
});
},
function handleImage(err, image, headers, stats) {
if (req.profiler) {
req.profiler.done('render-' + format);
req.profiler.add(stats || {});
}
if (err) {
self.sendError(req, res, err, 'STATIC_VIZ_MAP');
} else {
self.sendResponse(req, res, image, headers, namedMapProvider);
}
}
this.app.sendError(res, response, statusCode, 'POST INSTANCE TEMPLATE', err);
} else {
this.app.sendResponse(res, [response, 200]);
);
};
var DEFAULT_ZOOM_CENTER = {
zoom: 1,
center: {
lng: 0,
lat: 0
}
};
function finishFn(app, res, description, okResponse) {
return function finish(err, response){
var statusCode = 200;
if (err) {
statusCode = 400;
response = { errors: ['' + err] };
if ( ! _.isUndefined(err.http_status) ) {
statusCode = err.http_status;
NamedMapsController.prototype.getStaticImageOptions = function(cdbUser, namedMapProvider, callback) {
var self = this;
step(
function getTemplate() {
namedMapProvider.getTemplate(this);
},
function handleTemplateView(err, template) {
assert.ifError(err);
if (template.view) {
var zoomCenter = templateZoomCenter(template.view);
if (zoomCenter) {
return zoomCenter;
}
var bounds = templateBounds(template.view);
if (bounds) {
return bounds;
}
}
app.sendError(res, response, statusCode, description, err);
} else {
app.sendResponse(res, okResponse || [response, statusCode]);
return false;
},
function estimateBoundsIfNoImageOpts(err, imageOpts) {
if (imageOpts) {
return imageOpts;
}
var next = this;
namedMapProvider.getAffectedTablesAndLastUpdatedTime(function(err, affectedTablesAndLastUpdate) {
if (err) {
return next(null);
}
var affectedTables = affectedTablesAndLastUpdate.affectedTables || [];
if (affectedTables.length === 0) {
return next(null);
}
self.tablesExtentApi.getBounds(cdbUser, affectedTables, function(err, result) {
return next(null, result);
});
});
},
function returnCallback(err, imageOpts) {
return callback(err, imageOpts || DEFAULT_ZOOM_CENTER);
}
};
);
};
function templateZoomCenter(view) {
if (!_.isUndefined(view.zoom) && view.center) {
return {
zoom: view.zoom,
center: view.center
};
}
return false;
}
function ifUnauthenticated(authenticated, description) {
if (authenticated !== 1) {
var err = new Error(description);
err.http_status = 403;
throw err;
}
}
function ifInvalidContentType(req, description) {
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' ) {
throw new Error(description);
function templateBounds(view) {
if (view.bounds) {
var hasAllBounds = _.every(['west', 'south', 'east', 'north'], function(prop) {
return Number.isFinite(view.bounds[prop]);
});
if (hasAllBounds) {
return {
bounds: {
west: view.bounds.west,
south: view.bounds.south,
east: view.bounds.east,
north: view.bounds.north
}
};
} else {
return false;
}
}
return false;
}
+201
View File
@@ -0,0 +1,201 @@
var step = require('step');
var assert = require('assert');
var templateName = require('../backends/template_maps').templateName;
var util = require('util');
var BaseController = require('./base');
var cors = require('../middleware/cors');
/**
* @param {TemplateMaps} templateMaps
* @param {AuthApi} authApi
* @param {PgConnection} pgConnection
* @constructor
*/
function NamedMapsAdminController(templateMaps, authApi, pgConnection) {
BaseController.call(this, authApi, pgConnection);
this.templateMaps = templateMaps;
this.authApi = authApi;
}
util.inherits(NamedMapsAdminController, BaseController);
module.exports = NamedMapsAdminController;
NamedMapsAdminController.prototype.register = function(app) {
app.post(app.base_url_templated, cors(), this.create.bind(this));
app.put(app.base_url_templated + '/:template_id', cors(), this.update.bind(this));
app.get(app.base_url_templated + '/:template_id', cors(), this.retrieve.bind(this));
app.delete(app.base_url_templated + '/:template_id', cors(), this.destroy.bind(this));
app.get(app.base_url_templated, cors(), this.list.bind(this));
app.options(app.base_url_templated + '/:template_id', cors('Content-Type'));
};
NamedMapsAdminController.prototype.create = function(req, res) {
var self = this;
var cdbuser = req.context.user;
step(
function checkPerms(){
self.authApi.authorizedByAPIKey(cdbuser, req, this);
},
function addTemplate(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
ifInvalidContentType(req, 'template POST data must be of type application/json');
var cfg = req.body;
self.templateMaps.addTemplate(cdbuser, cfg, this);
},
function prepareResponse(err, tpl_id){
assert.ifError(err);
return { template_id: tpl_id };
},
finishFn(self, req, res, 'POST TEMPLATE')
);
};
NamedMapsAdminController.prototype.update = function(req, res) {
var self = this;
var cdbuser = req.context.user;
var template;
var tpl_id;
step(
function checkPerms(){
self.authApi.authorizedByAPIKey(cdbuser, req, this);
},
function updateTemplate(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated user can update templated maps');
ifInvalidContentType(req, 'template PUT data must be of type application/json');
template = req.body;
tpl_id = templateName(req.params.template_id);
self.templateMaps.updTemplate(cdbuser, tpl_id, template, this);
},
function prepareResponse(err){
assert.ifError(err);
return { template_id: tpl_id };
},
finishFn(self, req, res, 'PUT TEMPLATE')
);
};
NamedMapsAdminController.prototype.retrieve = function(req, res) {
var self = this;
if (req.profiler) {
req.profiler.start('windshaft-cartodb.get_template');
}
var cdbuser = req.context.user;
var tpl_id;
step(
function checkPerms(){
self.authApi.authorizedByAPIKey(cdbuser, req, this);
},
function getTemplate(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
tpl_id = templateName(req.params.template_id);
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
},
function prepareResponse(err, tpl_val) {
assert.ifError(err);
if ( ! tpl_val ) {
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
err.http_status = 404;
throw err;
}
// auth_id was added by ourselves,
// so we remove it before returning to the user
delete tpl_val.auth_id;
return { template: tpl_val };
},
finishFn(self, req, res, 'GET TEMPLATE')
);
};
NamedMapsAdminController.prototype.destroy = function(req, res) {
var self = this;
if (req.profiler) {
req.profiler.start('windshaft-cartodb.delete_template');
}
var cdbuser = req.context.user;
var tpl_id;
step(
function checkPerms(){
self.authApi.authorizedByAPIKey(cdbuser, req, this);
},
function deleteTemplate(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated users can delete template maps');
tpl_id = templateName(req.params.template_id);
self.templateMaps.delTemplate(cdbuser, tpl_id, this);
},
function prepareResponse(err/*, tpl_val*/){
assert.ifError(err);
return '';
},
finishFn(self, req, res, 'DELETE TEMPLATE', 204)
);
};
NamedMapsAdminController.prototype.list = function(req, res) {
var self = this;
if ( req.profiler ) {
req.profiler.start('windshaft-cartodb.get_template_list');
}
var cdbuser = req.context.user;
step(
function checkPerms(){
self.authApi.authorizedByAPIKey(cdbuser, req, this);
},
function listTemplates(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated user can list templated maps');
self.templateMaps.listTemplates(cdbuser, this);
},
function prepareResponse(err, tpl_ids){
assert.ifError(err);
return { template_ids: tpl_ids };
},
finishFn(self, req, res, 'GET TEMPLATE LIST')
);
};
function finishFn(controller, req, res, description, status) {
return function finish(err, response){
if (err) {
controller.sendError(req, res, err, description);
} else {
controller.send(req, res, response, status || 200);
}
};
}
function ifUnauthenticated(authenticated, description) {
if (!authenticated) {
var err = new Error(description);
err.http_status = 403;
throw err;
}
}
function ifInvalidContentType(req, description) {
if (!req.is('application/json')) {
throw new Error(description);
}
}
@@ -1,247 +0,0 @@
var step = require('step');
var assert = require('assert');
var templateName = require('../template_maps').templateName;
var CdbRequest = require('../models/cdb_request');
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
var _ = require('underscore');
function NamedStaticMapsController(app, serverOptions, templateMaps, staticMapBackend, surrogateKeysCache,
tablesExtentApi) {
this.app = app;
this.serverOptions = serverOptions;
this.templateMaps = templateMaps;
this.staticMapBackend = staticMapBackend;
this.surrogateKeysCache = surrogateKeysCache;
this.tablesExtentApi = tablesExtentApi;
}
module.exports = NamedStaticMapsController;
var cdbRequest = new CdbRequest();
NamedStaticMapsController.prototype.register = function(app) {
app.get(app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format', this.named.bind(this));
};
NamedStaticMapsController.prototype.named = function(req, res) {
var self = this;
this.app.doCORS(res);
var cdbUser = cdbRequest.userByReq(req);
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
var template;
var layergroupConfig;
var layergroupId;
var fakeReq;
var cacheChannel;
step(
function reqParams() {
self.app.req2params(req, this);
},
function getTemplate(err) {
assert.ifError(err);
self.templateMaps.getTemplate(cdbUser, templateName(req.params.template_id), this);
},
function checkExists(err, tpl) {
assert.ifError(err);
if (!tpl) {
var notFoundErr = new Error(
"Template '" + templateName(req.params.template_id) + "' of user '" + cdbUser + "' not found"
);
notFoundErr.http_status = 404;
throw notFoundErr;
}
return tpl;
},
function checkAuthorized(err, tpl) {
assert.ifError(err);
var authorized = false;
try {
authorized = self.templateMaps.isAuthorized(tpl, req.query.auth_token);
} catch (err) {
// we catch to add http_status
var authorizationFailedErr = new Error('Failed to authorize template');
authorizationFailedErr.http_status = 403;
throw authorizationFailedErr;
}
if ( ! authorized ) {
var unauthorizedErr = new Error('Unauthorized template instantiation');
unauthorizedErr.http_status = 403;
throw unauthorizedErr;
}
return tpl;
},
function prepareParams(err, tpl) {
assert.ifError(err);
template = tpl;
var templateParams = {};
if (req.query.config) {
try {
templateParams = JSON.parse(req.query.config);
} catch (e) {
throw new Error('malformed config parameter, should be a valid JSON');
}
}
return templateParams;
},
function instantiateTemplate(err, templateParams) {
assert.ifError(err);
return self.templateMaps.instance(template, templateParams);
},
function prepareLayergroup(err, layergroup) {
assert.ifError(err);
layergroupConfig = layergroup;
fakeReq = {
query: {},
params: {
user: req.params.user
},
headers: _.clone(req.headers),
context: _.clone(req.context),
method: req.method,
res: res,
profiler: req.profiler
};
self.serverOptions.setDBParams(cdbUser, fakeReq.params, this);
},
function setApiKey(err){
assert.ifError(err);
self.app.createLayergroup(layergroupConfig, fakeReq, this);
},
function prepareResponse(err, layergroup) {
assert.ifError(err);
// added by createLayergroup
cacheChannel = res.header('X-Cache-Channel');
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbUser, template.name));
layergroupId = layergroup.layergroupid.split(":")[0];
return null;
},
function staticImageOptions(err) {
assert.ifError(err);
getStaticImageOptions(template, this);
},
function estimateBounds(err, imageOpts) {
assert.ifError(err);
if (imageOpts) {
return imageOpts;
}
var defaultZoomCenter = {
zoom: 1,
center: {
lng: 0,
lat: 0
}
};
var dbTables = cacheChannel.split(':');
if (dbTables.length <= 1 || dbTables[1].length === 0) {
return defaultZoomCenter;
}
var tableNames = dbTables[1].split(',');
if (tableNames.length === 0) {
return defaultZoomCenter;
}
var next = this;
self.tablesExtentApi.getBounds(cdbUser, tableNames, function(err, result) {
next(null, result || defaultZoomCenter);
});
},
function getImage(err, imageOpts) {
assert.ifError(err);
var staticImageReq = {
headers: _.clone(fakeReq.headers),
params: _.extend(_.clone(fakeReq.params), {
token: layergroupId,
format: req.params.format
})
};
var width = +req.params.width;
var height = +req.params.height;
if (!_.isUndefined(imageOpts.zoom) && imageOpts.center) {
self.staticMapBackend.getImage(staticImageReq, width, height, imageOpts.zoom, imageOpts.center, this);
} else {
self.staticMapBackend.getImage(staticImageReq, width, height, imageOpts.bounds, this);
}
},
function handleImage(err, image, headers, stats) {
if (req.profiler) {
req.profiler.done('render-' + format);
req.profiler.add(stats || {});
}
if (err) {
if (!err.error) {
err.error = err.message;
}
self.app.sendError(res, err, self.app.findStatusCode(err), 'STATIC_VIZ_MAP', err);
} else {
res.setHeader('Content-Type', headers['Content-Type'] || 'image/' + format);
res.setHeader('Cache-Control', 'public,max-age=7200,must-revalidate');
self.app.sendResponse(res, [image, 200]);
}
}
);
};
function getStaticImageOptions(template, callback) {
if (template.view) {
var zoomCenter = templateZoomCenter(template.view);
if (zoomCenter) {
return callback(null, zoomCenter);
}
var bounds = templateBounds(template.view);
if (bounds) {
return callback(null, bounds);
}
}
return callback(null, null);
}
function templateZoomCenter(view) {
if (!_.isUndefined(view.zoom) && view.center) {
return {
zoom: view.zoom,
center: view.center
};
}
return false;
}
function templateBounds(view) {
if (view.bounds) {
var hasAllBounds = _.every(['west', 'south', 'east', 'north'], function(prop) {
return !!view.bounds[prop];
});
if (hasAllBounds) {
return {
bounds: {
west: view.bounds.west,
south: view.bounds.south,
east: view.bounds.east,
north: view.bounds.north
}
};
} else {
return false;
}
}
return false;
}
+56
View File
@@ -0,0 +1,56 @@
var windshaft = require('windshaft');
var HealthCheck = require('../monitoring/health_check');
var WELCOME_MSG = "This is the CartoDB Maps API, " +
"see the documentation at http://docs.cartodb.com/cartodb-platform/maps-api.html";
var versions = {
windshaft: windshaft.version,
grainstore: windshaft.grainstore.version(),
node_mapnik: windshaft.mapnik.version,
mapnik: windshaft.mapnik.versions.mapnik,
windshaft_cartodb: require('../../../package.json').version
};
function ServerInfoController() {
this.healthConfig = global.environment.health || {};
this.healthCheck = new HealthCheck(global.environment.disabled_file);
}
module.exports = ServerInfoController;
ServerInfoController.prototype.register = function(app) {
app.get('/health', this.health.bind(this));
app.get('/', this.welcome.bind(this));
app.get('/version', this.version.bind(this));
};
ServerInfoController.prototype.welcome = function(req, res) {
res.status(200).send(WELCOME_MSG);
};
ServerInfoController.prototype.version = function(req, res) {
res.status(200).send(versions);
};
ServerInfoController.prototype.health = function(req, res) {
if (!!this.healthConfig.enabled) {
var startTime = Date.now();
this.healthCheck.check(function(err) {
var ok = !err;
var response = {
enabled: true,
ok: ok,
elapsed: Date.now() - startTime
};
if (err) {
response.err = err.message;
}
res.status(ok ? 200 : 503).send(response);
});
} else {
res.status(200).send({enabled: false, ok: true});
}
};
+11
View File
@@ -0,0 +1,11 @@
module.exports = function cors(extraHeaders) {
return function(req, res, next) {
var baseHeaders = "X-Requested-With, X-Prototype-Version, X-CSRF-Token";
if(extraHeaders) {
baseHeaders += ", " + extraHeaders;
}
res.set("Access-Control-Allow-Origin", "*");
res.set("Access-Control-Allow-Headers", baseHeaders);
next();
};
};
+2 -3
View File
@@ -14,12 +14,11 @@ CdbRequest.prototype.userByReq = function(req) {
}
var mat = host.match(this.RE_USER_FROM_HOST);
if ( ! mat ) {
console.error("Pattern '" + this.RE_USER_FROM_HOST + "' does not match hostname '" + host + "'");
global.logger.error("Pattern '%s' does not match hostname '%s'", this.RE_USER_FROM_HOST, host);
return;
}
// console.log("Matches: "); console.dir(mat);
if ( mat.length !== 2 ) {
console.error("Pattern '" + this.RE_USER_FROM_HOST + "' gave unexpected matches against '" + host + "': ", mat);
global.logger.error("Pattern '%s' gave unexpected matches against '%s': %s", this.RE_USER_FROM_HOST, host, mat);
return;
}
return mat[1];
+29
View File
@@ -0,0 +1,29 @@
/**
* @param {String} token might match the following pattern: {user}@{tpl_id}@{token}:{cache_buster}
*/
function parse(token) {
var signer, cacheBuster;
var tokenSplit = token.split(':');
token = tokenSplit[0];
if (tokenSplit.length > 1) {
cacheBuster = tokenSplit[1];
}
tokenSplit = token.split('@');
if ( tokenSplit.length > 1 ) {
signer = tokenSplit.shift();
if ( tokenSplit.length > 1 ) {
/*var template_hash = */tokenSplit.shift(); // unused
}
token = tokenSplit.shift();
}
return {
token: token,
signer: signer,
cacheBuster: cacheBuster
};
}
module.exports.parse = parse;
@@ -0,0 +1,48 @@
var assert = require('assert');
var step = require('step');
var MapStoreMapConfigProvider = require('./map_store_provider');
/**
* @param {MapConfig} mapConfig
* @param {String} user
* @param {UserLimitsApi} userLimitsApi
* @param {Object} params
* @constructor
* @type {CreateLayergroupMapConfigProvider}
*/
function CreateLayergroupMapConfigProvider(mapConfig, user, userLimitsApi, params) {
this.mapConfig = mapConfig;
this.user = user;
this.userLimitsApi = userLimitsApi;
this.params = params;
this.cacheBuster = params.cache_buster || 0;
}
module.exports = CreateLayergroupMapConfigProvider;
CreateLayergroupMapConfigProvider.prototype.getMapConfig = function(callback) {
var self = this;
var context = {};
step(
function prepareContextLimits() {
self.userLimitsApi.getRenderLimits(self.user, this);
},
function handleRenderLimits(err, renderLimits) {
assert.ifError(err);
context.limits = renderLimits;
return null;
},
function finish(err) {
return callback(err, self.mapConfig, self.params, context);
}
);
};
CreateLayergroupMapConfigProvider.prototype.getKey = MapStoreMapConfigProvider.prototype.getKey;
CreateLayergroupMapConfigProvider.prototype.getCacheBuster = MapStoreMapConfigProvider.prototype.getCacheBuster;
CreateLayergroupMapConfigProvider.prototype.filter = MapStoreMapConfigProvider.prototype.filter;
CreateLayergroupMapConfigProvider.prototype.createKey = MapStoreMapConfigProvider.prototype.createKey;
@@ -0,0 +1,77 @@
var _ = require('underscore');
var assert = require('assert');
var dot = require('dot');
var step = require('step');
/**
* @param {MapStore} mapStore
* @param {String} user
* @param {UserLimitsApi} userLimitsApi
* @param {Object} params
* @constructor
* @type {MapStoreMapConfigProvider}
*/
function MapStoreMapConfigProvider(mapStore, user, userLimitsApi, params) {
this.mapStore = mapStore;
this.user = user;
this.userLimitsApi = userLimitsApi;
this.params = params;
this.token = params.token;
this.cacheBuster = params.cache_buster || 0;
}
module.exports = MapStoreMapConfigProvider;
MapStoreMapConfigProvider.prototype.getMapConfig = function(callback) {
var self = this;
var context = {};
step(
function prepareContextLimits() {
self.userLimitsApi.getRenderLimits(self.user, this);
},
function handleRenderLimits(err, renderLimits) {
assert.ifError(err);
context.limits = renderLimits;
return null;
},
function loadMapConfig(err) {
assert.ifError(err);
self.mapStore.load(self.token, this);
},
function finish(err, mapConfig) {
return callback(err, mapConfig, self.params, context);
}
);
};
MapStoreMapConfigProvider.prototype.getKey = function() {
return this.createKey(false);
};
MapStoreMapConfigProvider.prototype.getCacheBuster = function() {
return this.cacheBuster;
};
MapStoreMapConfigProvider.prototype.filter = function(key) {
var regex = new RegExp('^' + this.createKey(true) + '.*');
return key && key.match(regex);
};
// Configure bases for cache keys suitable for string interpolation
var baseKey = '{{=it.dbname}}:{{=it.token}}';
var rendererKey = baseKey + ':{{=it.dbuser}}:{{=it.format}}:{{=it.layer}}:{{=it.scale_factor}}';
var baseKeyTpl = dot.template(baseKey);
var rendererKeyTpl = dot.template(rendererKey);
MapStoreMapConfigProvider.prototype.createKey = function(base) {
var tplValues = _.defaults({}, this.params, {
dbname: '',
token: '',
dbuser: '',
format: '',
layer: '',
scale_factor: 1
});
return (base) ? baseKeyTpl(tplValues) : rendererKeyTpl(tplValues);
};
@@ -0,0 +1,266 @@
var _ = require('underscore');
var assert = require('assert');
var crypto = require('crypto');
var dot = require('dot');
var step = require('step');
var MapConfig = require('windshaft').model.MapConfig;
var templateName = require('../../backends/template_maps').templateName;
/**
* @constructor
* @type {NamedMapMapConfigProvider}
*/
function NamedMapMapConfigProvider(templateMaps, pgConnection, userLimitsApi, queryTablesApi, namedLayersAdapter,
owner, templateId, config, authToken, params) {
this.templateMaps = templateMaps;
this.pgConnection = pgConnection;
this.userLimitsApi = userLimitsApi;
this.queryTablesApi = queryTablesApi;
this.namedLayersAdapter = namedLayersAdapter;
this.owner = owner;
this.templateName = templateName(templateId);
this.config = config;
this.authToken = authToken;
this.params = params;
this.cacheBuster = Date.now();
// use template after call to mapConfig
this.template = null;
this.affectedTablesAndLastUpdate = null;
// providing
this.err = null;
this.mapConfig = null;
this.rendererParams = null;
this.context = {};
}
module.exports = NamedMapMapConfigProvider;
NamedMapMapConfigProvider.prototype.getMapConfig = function(callback) {
if (!!this.err || this.mapConfig !== null) {
return callback(this.err, this.mapConfig, this.rendererParams, this.context);
}
var self = this;
var mapConfig = null;
var datasource = null;
var rendererParams;
step(
function getTemplate() {
self.getTemplate(this);
},
function prepareParams(err, tpl) {
assert.ifError(err);
self.template = tpl;
var templateParams = {};
if (self.config) {
try {
templateParams = _.isString(self.config) ? JSON.parse(self.config) : self.config;
} catch (e) {
throw new Error('malformed config parameter, should be a valid JSON');
}
}
return templateParams;
},
function instantiateTemplate(err, templateParams) {
assert.ifError(err);
return self.templateMaps.instance(self.template, templateParams);
},
function prepareLayergroup(err, _mapConfig) {
assert.ifError(err);
var next = this;
self.namedLayersAdapter.getLayers(self.owner, _mapConfig.layers, self.pgConnection,
function(err, layers, datasource) {
if (err) {
return next(err);
}
if (layers) {
_mapConfig.layers = layers;
}
return next(null, _mapConfig, datasource);
}
);
},
function beforeLayergroupCreate(err, _mapConfig, _datasource) {
assert.ifError(err);
mapConfig = _mapConfig;
datasource = _datasource;
rendererParams = _.extend({}, self.params, {
user: self.owner
});
self.setDBParams(self.owner, rendererParams, this);
},
function prepareContextLimits(err) {
assert.ifError(err);
self.userLimitsApi.getRenderLimits(self.owner, this);
},
function cacheAndReturnMapConfig(err, renderLimits) {
self.err = err;
self.mapConfig = (mapConfig === null) ? null : new MapConfig(mapConfig, datasource);
self.rendererParams = rendererParams;
self.context.limits = renderLimits || {};
return callback(self.err, self.mapConfig, self.rendererParams, self.context);
}
);
};
NamedMapMapConfigProvider.prototype.getTemplate = function(callback) {
var self = this;
if (!!this.err || this.template !== null) {
return callback(this.err, this.template);
}
step(
function getTemplate() {
self.templateMaps.getTemplate(self.owner, self.templateName, this);
},
function checkExists(err, tpl) {
assert.ifError(err);
if (!tpl) {
var notFoundErr = new Error(
"Template '" + self.templateName + "' of user '" + self.owner + "' not found"
);
notFoundErr.http_status = 404;
throw notFoundErr;
}
return tpl;
},
function checkAuthorized(err, tpl) {
assert.ifError(err);
var authorized = false;
try {
authorized = self.templateMaps.isAuthorized(tpl, self.authToken);
} catch (err) {
// we catch to add http_status
var authorizationFailedErr = new Error('Failed to authorize template');
authorizationFailedErr.http_status = 403;
throw authorizationFailedErr;
}
if ( ! authorized ) {
var unauthorizedErr = new Error('Unauthorized template instantiation');
unauthorizedErr.http_status = 403;
throw unauthorizedErr;
}
return tpl;
},
function cacheAndReturnTemplate(err, template) {
self.err = err;
self.template = template;
return callback(self.err, self.template);
}
);
};
NamedMapMapConfigProvider.prototype.getKey = function() {
return this.createKey(false);
};
NamedMapMapConfigProvider.prototype.getCacheBuster = function() {
return this.cacheBuster;
};
NamedMapMapConfigProvider.prototype.reset = function() {
this.template = null;
this.affectedTablesAndLastUpdate = null;
this.err = null;
this.mapConfig = null;
this.cacheBuster = Date.now();
};
NamedMapMapConfigProvider.prototype.filter = function(key) {
var regex = new RegExp('^' + this.createKey(true) + '.*');
return key && key.match(regex);
};
// Configure bases for cache keys suitable for string interpolation
var baseKey = '{{=it.dbname}}:{{=it.owner}}:{{=it.templateName}}';
var rendererKey = baseKey + ':{{=it.authToken}}:{{=it.configHash}}:{{=it.format}}:{{=it.layer}}:{{=it.scale_factor}}';
var baseKeyTpl = dot.template(baseKey);
var rendererKeyTpl = dot.template(rendererKey);
NamedMapMapConfigProvider.prototype.createKey = function(base) {
var tplValues = _.defaults({}, this.params, {
dbname: '',
owner: this.owner,
templateName: this.templateName,
authToken: this.authToken || '',
configHash: configHash(this.config),
layer: '',
scale_factor: 1
});
return (base) ? baseKeyTpl(tplValues) : rendererKeyTpl(tplValues);
};
function configHash(config) {
if (!config) {
return '';
}
return crypto.createHash('md5').update(JSON.stringify(config)).digest('hex').substring(0,8);
}
module.exports.configHash = configHash;
NamedMapMapConfigProvider.prototype.setDBParams = function(cdbuser, params, callback) {
var self = this;
step(
function setAuth() {
self.pgConnection.setDBAuth(cdbuser, params, this);
},
function setConn(err) {
assert.ifError(err);
self.pgConnection.setDBConn(cdbuser, params, this);
},
function finish(err) {
callback(err);
}
);
};
NamedMapMapConfigProvider.prototype.getTemplateName = function() {
return this.templateName;
};
NamedMapMapConfigProvider.prototype.getAffectedTablesAndLastUpdatedTime = function(callback) {
var self = this;
if (this.affectedTablesAndLastUpdate !== null) {
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);
self.queryTablesApi.getAffectedTablesAndLastUpdatedTime(self.owner, sql, this);
},
function finish(err, result) {
self.affectedTablesAndLastUpdate = result;
return callback(err, result);
}
);
};
@@ -1,6 +1,6 @@
var queue = require('queue-async');
var _ = require('underscore');
var Datasource = require('windshaft').Datasource;
var Datasource = require('windshaft').model.Datasource;
function MapConfigNamedLayersAdapter(templateMaps) {
this.templateMaps = templateMaps;
@@ -11,6 +11,10 @@ module.exports = MapConfigNamedLayersAdapter;
MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbMetadata, callback) {
var self = this;
if (!layers) {
return callback(null);
}
var adaptLayersQueue = queue(layers.length);
function adaptLayer(layer, done) {
+7 -22
View File
@@ -1,5 +1,4 @@
var fs = require('fs');
var step = require('step');
function HealthCheck(disableFile) {
this.disableFile = disableFile;
@@ -9,26 +8,12 @@ module.exports = HealthCheck;
HealthCheck.prototype.check = function(callback) {
var self = this;
step(
function getManualDisable() {
fs.readFile(self.disableFile, this);
},
function handleDisabledFile(err, data) {
var next = this;
if (err) {
return next();
}
if (!!data) {
err = new Error(data);
err.http_status = 503;
throw err;
}
},
function handleResult(err) {
return callback(err);
fs.readFile(this.disableFile, function handleDisabledFile(err, data) {
var disabledError = null;
if (!err) {
disabledError = new Error(data || 'Unknown error');
disabledError.http_status = 503;
}
);
return callback(disabledError);
});
};
+322
View File
@@ -0,0 +1,322 @@
var express = require('express');
var bodyParser = require('body-parser');
var RedisPool = require('redis-mpool');
var cartodbRedis = require('cartodb-redis');
var _ = require('underscore');
var controller = require('./controllers');
var SurrogateKeysCache = require('./cache/surrogate_keys_cache');
var NamedMapsCacheEntry = require('./cache/model/named_maps_entry');
var VarnishHttpCacheBackend = require('./cache/backend/varnish_http');
var FastlyCacheBackend = require('./cache/backend/fastly');
var StatsClient = require('./stats/client');
var Profiler = require('./stats/profiler_proxy');
var RendererStatsReporter = require('./stats/reporter/renderer');
var windshaft = require('windshaft');
var mapnik = windshaft.mapnik;
var TemplateMaps = require('./backends/template_maps.js');
var QueryTablesApi = require('./api/query_tables_api');
var UserLimitsApi = require('./api/user_limits_api');
var AuthApi = require('./api/auth_api');
var LayergroupAffectedTablesCache = require('./cache/layergroup_affected_tables');
var NamedMapProviderCache = require('./cache/named_map_provider_cache');
var PgQueryRunner = require('./backends/pg_query_runner');
var PgConnection = require('./backends/pg_connection');
var CdbRequest = require('./models/cdb_request');
var cdbRequest = new CdbRequest();
var timeoutErrorTilePath = __dirname + '/../../assets/render-timeout-fallback.png';
var timeoutErrorTile = require('fs').readFileSync(timeoutErrorTilePath, {encoding: null});
module.exports = function(serverOptions) {
// Make stats client globally accessible
global.statsClient = StatsClient.getInstance(serverOptions.statsd);
var redisPool = new RedisPool(_.defaults(global.environment.redis, {
name: 'windshaft:server',
unwatchOnRelease: false,
noReadyCheck: true
}));
redisPool.on('status', function(status) {
var keyPrefix = 'windshaft.redis-pool.' + status.name + '.db' + status.db + '.';
global.statsClient.gauge(keyPrefix + 'count', status.count);
global.statsClient.gauge(keyPrefix + 'unused', status.unused);
global.statsClient.gauge(keyPrefix + 'waiting', status.waiting);
});
var metadataBackend = cartodbRedis({pool: redisPool});
var pgConnection = new PgConnection(metadataBackend);
var pgQueryRunner = new PgQueryRunner(pgConnection);
var queryTablesApi = new QueryTablesApi(pgQueryRunner);
var userLimitsApi = new UserLimitsApi(metadataBackend, {
limits: {
cacheOnTimeout: serverOptions.renderer.mapnik.limits.cacheOnTimeout || false,
render: serverOptions.renderer.mapnik.limits.render || 0
}
});
var templateMaps = new TemplateMaps(redisPool, {
max_user_templates: global.environment.maxUserTemplates
});
var surrogateKeysCache = new SurrogateKeysCache(surrogateKeysCacheBackends(serverOptions));
function invalidateNamedMap (owner, templateName) {
var startTime = Date.now();
surrogateKeysCache.invalidate(new NamedMapsCacheEntry(owner, templateName), function(err) {
var logMessage = JSON.stringify({
username: owner,
type: 'named_map_invalidation',
elapsed: Date.now() - startTime,
error: !!err ? JSON.stringify(err.message) : undefined
});
if (err) {
global.logger.warn(logMessage);
} else {
global.logger.info(logMessage);
}
});
}
['update', 'delete'].forEach(function(eventType) {
templateMaps.on(eventType, invalidateNamedMap);
});
serverOptions.grainstore.mapnik_version = mapnikVersion(serverOptions);
validateOptions(serverOptions);
bootstrapFonts(serverOptions);
// initialize express server
var app = bootstrap(serverOptions);
// Extend windshaft with all the elements of the options object
_.extend(app, serverOptions);
var mapStore = new windshaft.storage.MapStore({
pool: redisPool,
expire_time: serverOptions.grainstore.default_layergroup_ttl
});
var onTileErrorStrategy;
if (global.environment.enabledFeatures.onTileErrorStrategy !== false) {
onTileErrorStrategy = function onTileErrorStrategy$TimeoutTile(err, tile, headers, stats, format, callback) {
if (err && err.message === 'Render timed out' && format === 'png') {
return callback(null, timeoutErrorTile, { 'Content-Type': 'image/png' }, {});
} else {
return callback(err, tile, headers, stats);
}
};
}
var rendererFactory = new windshaft.renderer.Factory({
onTileErrorStrategy: onTileErrorStrategy,
mapnik: {
redisPool: redisPool,
grainstore: serverOptions.grainstore,
mapnik: serverOptions.renderer.mapnik
},
http: serverOptions.renderer.http
});
// initialize render cache
var rendererCacheOpts = _.defaults(serverOptions.renderCache || {}, {
ttl: 60000, // 60 seconds TTL by default
statsInterval: 60000 // reports stats every milliseconds defined here
});
var rendererCache = new windshaft.cache.RendererCache(rendererFactory, rendererCacheOpts);
var rendererStatsReporter = new RendererStatsReporter(rendererCache, rendererCacheOpts.statsInterval);
rendererStatsReporter.start();
var attributesBackend = new windshaft.backend.Attributes(mapStore);
var previewBackend = new windshaft.backend.Preview(rendererCache);
var tileBackend = new windshaft.backend.Tile(rendererCache);
var mapValidatorBackend = new windshaft.backend.MapValidator(tileBackend, attributesBackend);
var mapBackend = new windshaft.backend.Map(rendererCache, mapStore, mapValidatorBackend);
var layergroupAffectedTablesCache = new LayergroupAffectedTablesCache();
app.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
var namedMapProviderCache = new NamedMapProviderCache(templateMaps, pgConnection, userLimitsApi, queryTablesApi);
['update', 'delete'].forEach(function(eventType) {
templateMaps.on(eventType, namedMapProviderCache.invalidate.bind(namedMapProviderCache));
});
var authApi = new AuthApi(pgConnection, metadataBackend, mapStore, templateMaps);
var TablesExtentApi = require('./api/tables_extent_api');
var tablesExtentApi = new TablesExtentApi(pgQueryRunner);
/*******************************************************************************************************************
* Routing
******************************************************************************************************************/
app.all('*', function(req, res, next) {
req.context.user = cdbRequest.userByReq(req);
next();
});
new controller.Layergroup(
app,
authApi,
pgConnection,
mapStore,
tileBackend,
previewBackend,
attributesBackend,
surrogateKeysCache,
userLimitsApi,
queryTablesApi,
layergroupAffectedTablesCache
).register(app);
new controller.Map(
app,
authApi,
pgConnection,
templateMaps,
mapBackend,
metadataBackend,
queryTablesApi,
surrogateKeysCache,
userLimitsApi,
layergroupAffectedTablesCache
).register(app);
new controller.NamedMaps(
app,
authApi,
pgConnection,
namedMapProviderCache,
tileBackend,
previewBackend,
surrogateKeysCache,
tablesExtentApi,
metadataBackend
).register(app);
new controller.NamedMapsAdmin(templateMaps, authApi, pgConnection).register(app);
new controller.ServerInfo().register(app);
/*******************************************************************************************************************
* END Routing
******************************************************************************************************************/
return app;
};
function validateOptions(opts) {
if (!_.isString(opts.base_url) || !_.isString(opts.base_url_mapconfig) || !_.isString(opts.base_url_templated)) {
throw new Error("Must initialise server with: 'base_url'/'base_url_mapconfig'/'base_url_templated' URLs");
}
// Be nice and warn if configured mapnik version is != instaled mapnik version
if (mapnik.versions.mapnik !== opts.grainstore.mapnik_version) {
console.warn('WARNING: detected mapnik version (' + mapnik.versions.mapnik + ')' +
' != configured mapnik version (' + opts.grainstore.mapnik_version + ')');
}
}
function bootstrapFonts(opts) {
// Set carto renderer configuration for MMLStore
opts.grainstore.carto_env = opts.grainstore.carto_env || {};
var cenv = opts.grainstore.carto_env;
cenv.validation_data = cenv.validation_data || {};
if ( ! cenv.validation_data.fonts ) {
mapnik.register_system_fonts();
mapnik.register_default_fonts();
cenv.validation_data.fonts = _.keys(mapnik.fontFiles());
}
}
function bootstrap(opts) {
var app;
if (_.isObject(opts.https)) {
// use https if possible
app = express.createServer(opts.https);
} else {
// fall back to http by default
app = express();
}
app.enable('jsonp callback');
app.disable('x-powered-by');
app.disable('etag');
app.use(bodyParser.json());
app.use(function bootstrap$prepareRequestResponse(req, res, next) {
req.context = req.context || {};
req.profiler = new Profiler({
statsd_client: global.statsClient,
profile: opts.useProfiler
});
if (global.environment && global.environment.api_hostname) {
res.set('X-Served-By-Host', global.environment.api_hostname);
}
next();
});
// temporary measure until we upgrade to newer version expressjs so we can check err.status
app.use(function(err, req, res, next) {
if (err) {
if (err.name === 'SyntaxError') {
res.status(400).json({ errors: [err.name + ': ' + err.message] });
} else {
next(err);
}
} else {
next();
}
});
setupLogger(app, opts);
return app;
}
function setupLogger(app, opts) {
if (global.log4js && opts.log_format) {
var loggerOpts = {
// Allowing for unbuffered logging is mainly
// used to avoid hanging during unit testing.
// TODO: provide an explicit teardown function instead,
// releasing any event handler or timer set by
// this component.
buffer: !opts.unbuffered_logging,
// optional log format
format: opts.log_format
};
app.use(global.log4js.connectLogger(global.log4js.getLogger(), _.defaults(loggerOpts, {level: 'info'})));
}
}
function surrogateKeysCacheBackends(serverOptions) {
var cacheBackends = [];
if (serverOptions.varnish_purge_enabled) {
cacheBackends.push(
new VarnishHttpCacheBackend(serverOptions.varnish_host, serverOptions.varnish_http_port)
);
}
if (serverOptions.fastly &&
!!serverOptions.fastly.enabled && !!serverOptions.fastly.apiKey && !!serverOptions.fastly.serviceId) {
cacheBackends.push(
new FastlyCacheBackend(serverOptions.fastly.apiKey, serverOptions.fastly.serviceId)
);
}
return cacheBackends;
}
function mapnikVersion(opts) {
return opts.grainstore.mapnik_version || mapnik.versions.mapnik;
}
+31 -603
View File
@@ -1,53 +1,34 @@
var os = require('os');
var _ = require('underscore');
var step = require('step');
var LZMA = require('lzma').LZMA;
var assert = require('assert');
var RedisPool = require('redis-mpool');
var QueryTablesApi = require('./api/query_tables_api');
var PgQueryRunner = require('./backends/pg_query_runner');
var PgConnection = require('./backends/pg_connection');
var TemplateMaps = require('./template_maps.js');
var MapConfigNamedLayersAdapter = require('./models/mapconfig_named_layers_adapter');
var CdbRequest = require('./models/cdb_request');
var rendererConfig = _.defaults(global.environment.renderer || {}, {
cache_ttl: 60000, // milliseconds
statsInterval: 60000,
mapnik: {
poolSize: 8,
metatile: 2,
bufferSize: 64,
snapToGrid: false,
clipByBox2d: false,
limits: {}
},
http: {}
});
var timeoutErrorTilePath = __dirname + '/../../assets/render-timeout-fallback.png';
var timeoutErrorTile = require('fs').readFileSync(timeoutErrorTilePath, {encoding: null});
// Perform keyword substitution in statsd
// See https://github.com/CartoDB/Windshaft-cartodb/issues/153
if ( global.environment.statsd ) {
if ( global.environment.statsd.prefix ) {
var host_token = os.hostname().split('.').reverse().join('.');
global.environment.statsd.prefix = global.environment.statsd.prefix.replace(/:host/, host_token);
}
}
// Whitelist query parameters and attach format
var REQUEST_QUERY_PARAMS_WHITELIST = [
'config',
'map_key',
'api_key',
'auth_token',
'callback'
];
module.exports = function(redisPool) {
redisPool = redisPool || new RedisPool(_.extend(global.environment.redis, {name: 'windshaft:server_options'}));
var cartoData = require('cartodb-redis')({ pool: redisPool });
var lzmaWorker = new LZMA();
var pgConnection = new PgConnection(cartoData);
var pgQueryRunner = new PgQueryRunner(pgConnection);
var queryTablesApi = new QueryTablesApi(pgQueryRunner);
var cdbRequest = new CdbRequest();
var rendererConfig = _.defaults(global.environment.renderer || {}, {
cache_ttl: 60000, // milliseconds
statsInterval: 60000,
mapnik: {
poolSize: 8,
metatile: 2,
bufferSize: 64,
snapToGrid: false,
clipByBox2d: false,
limits: {}
},
http: {}
});
var me = {
module.exports = {
bind: {
port: global.environment.port,
host: global.environment.host
},
// This is for inline maps and table maps
base_url: global.environment.base_url_legacy || '/tiles/:table',
@@ -61,6 +42,8 @@ module.exports = function(redisPool) {
//
base_url_mapconfig: global.environment.base_url_detached || '(?:/maps|/tiles/layergroup)',
base_url_templated: global.environment.base_url_templated || '(?:/maps/named|/tiles/template)',
grainstore: {
map: {
// TODO: allow to specify in configuration
@@ -82,7 +65,8 @@ module.exports = function(redisPool) {
torque: rendererConfig.torque,
http: rendererConfig.http
},
redis: global.environment.redis,
// Do not send unwatch on release. See http://github.com/CartoDB/Windshaft-cartodb/issues/161
redis: _.extend(global.environment.redis, {unwatchOnRelease: false}),
enable_cors: global.environment.enable_cors,
varnish_host: global.environment.varnish.host,
varnish_port: global.environment.varnish.port,
@@ -93,560 +77,4 @@ module.exports = function(redisPool) {
cache_enabled: global.environment.cache_enabled,
log_format: global.environment.log_format,
useProfiler: global.environment.useProfiler
};
// Do not send unwatch on release
// See http://github.com/CartoDB/Windshaft-cartodb/issues/161
me.redis.unwatchOnRelease = false;
// Re-use redisPool
me.redis.pool = redisPool;
// Re-use pgConnection
me.pgConnection = pgConnection;
// Re-use pgQueryRunner
me.pgQueryRunner = pgQueryRunner;
var templateMaps = new TemplateMaps(redisPool, {
max_user_templates: global.environment.maxUserTemplates
});
me.templateMaps = templateMaps;
var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
/* This whole block is about generating X-Cache-Channel { */
// TODO: review lifetime of elements of this cache
// NOTE: by-token indices should only be dropped when
// the corresponding layegroup is dropped, because
// we have no SQL after layer creation.
me.channelCache = {};
me.buildCacheChannel = function (dbName, tableNames){
return dbName + ':' + tableNames.join(',');
};
me.generateCacheChannel = function(app, req, callback){
// Build channelCache key
var dbName = req.params.dbname;
var cacheKey = [ dbName, req.params.token ].join(':');
// no token means no tables associated
if (!req.params.token) {
return callback(null, this.buildCacheChannel(dbName, []));
}
step(
function checkCached() {
if ( me.channelCache.hasOwnProperty(cacheKey) ) {
return callback(null, me.channelCache[cacheKey]);
}
return null;
},
function extractSQL(err) {
assert.ifError(err);
// TODO: cached cache channel for token-based access should
// be constructed at renderer cache creation time
// See http://github.com/CartoDB/Windshaft-cartodb/issues/152
if ( ! app.mapStore ) {
throw new Error('missing channel cache for token ' + req.params.token);
}
var mapStore = app.mapStore;
step(
function loadFromStore() {
mapStore.load(req.params.token, this);
},
function getSQL(err, mapConfig) {
if (req.profiler) {
req.profiler.done('mapStore_load');
}
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");
}
queryTablesApi.getAffectedTablesInQuery(cdbRequest.userByReq(req), sql, this); // in addCacheChannel
},
function buildCacheChannel(err, tableNames) {
assert.ifError(err);
if (req.profiler) {
req.profiler.done('affectedTables');
}
var cacheChannel = me.buildCacheChannel(dbName,tableNames);
me.channelCache[cacheKey] = cacheChannel;
return cacheChannel;
},
function finish(err, cacheChannel) {
callback(err, cacheChannel);
}
);
};
// Set the cache chanel info to invalidate the cache on the frontend server
//
// @param req The request object.
// The function will have no effect unless req.res exists.
// It is expected that req.params contains 'table' and 'dbname'
//
// @param cb function(err, channel) will be called when ready.
// the channel parameter will be null if nothing was added
//
me.addCacheChannel = function(app, req, cb) {
// skip non-GET requests, or requests for which there's no response
if ( req.method != 'GET' || ! req.res ) { cb(null, null); return; }
if (req.profiler) {
req.profiler.start('addCacheChannel');
}
var res = req.res;
if ( req.params.token ) {
res.header('Cache-Control', 'public,max-age=31536000'); // 1 year
} else {
var ttl = global.environment.varnish.ttl || 86400;
res.header('Cache-Control', 'no-cache,max-age='+ttl+',must-revalidate, public');
}
// Set Last-Modified header
var lastUpdated;
if ( req.params.cache_buster ) {
// Assuming cache_buster is a timestamp
// FIXME: store lastModified in the cache channel instead
lastUpdated = new Date(parseInt(req.params.cache_buster));
} else {
lastUpdated = new Date();
}
res.header('Last-Modified', lastUpdated.toUTCString());
me.generateCacheChannel(app, req, function(err, channel){
if (req.profiler) {
req.profiler.done('generateCacheChannel');
req.profiler.end();
}
if ( ! err ) {
res.header('X-Cache-Channel', channel);
cb(null, channel);
} else {
console.log('ERROR generating cache channel: ' + ( err.message ? err.message : err ));
// TODO: evaluate if we should bubble up the error instead
cb(null, 'ERROR');
}
});
};
if (global.environment.enabledFeatures.onTileErrorStrategy !== false) {
me.renderer.onTileErrorStrategy = function(err, tile, headers, stats, format, callback) {
if (err && err.message === 'Render timed out' && format === 'png') {
return callback(null, timeoutErrorTile, { 'Content-Type': 'image/png' }, {});
} else {
return callback(err, tile, headers, stats);
}
};
}
me.renderCache.beforeRendererCreate = function(req, callback) {
var user = cdbRequest.userByReq(req);
var rendererOptions = {};
step(
function getLimits(err) {
assert.ifError(err);
cartoData.getTilerRenderLimit(user, this);
},
function handleTilerLimits(err, renderLimit) {
assert.ifError(err);
rendererOptions.limits = {
cacheOnTimeout: rendererConfig.mapnik.limits.cacheOnTimeout || false,
render: renderLimit || rendererConfig.mapnik.limits.render || 0
};
return null;
},
function finish(err) {
if (err) {
return callback(err);
}
return callback(null, rendererOptions);
}
);
};
me.beforeLayergroupCreate = function(req, requestMapConfig, callback) {
mapConfigNamedLayersAdapter.getLayers(cdbRequest.userByReq(req), requestMapConfig.layers, pgConnection,
function(err, layers, datasource) {
if (err) {
return callback(err);
}
requestMapConfig.layers = layers;
return callback(null, requestMapConfig, datasource);
}
);
};
me.afterLayergroupCreate = function(req, mapconfig, response, callback) {
var token = response.layergroupid;
var username = cdbRequest.userByReq(req);
var tasksleft = 2; // redis key and affectedTables
var errors = [];
var done = function(err) {
if ( err ) {
errors.push('' + err);
}
if ( ! --tasksleft ) {
err = errors.length ? new Error(errors.join('\n')) : null;
callback(err);
}
};
// include in layergroup response the variables in serverMedata
// those variables are useful to send to the client information
// about how to reach this server or information about it
var serverMetadata = global.environment.serverMetadata;
if (serverMetadata) {
_.extend(response, serverMetadata);
}
// Don't wait for the mapview count increment to
// take place before proceeding. Error will be logged
// asyncronously
cartoData.incMapviewCount(username, mapconfig.stat_tag, function(err) {
if (req.profiler) {
req.profiler.done('incMapviewCount');
}
if ( err ) {
console.log("ERROR: failed to increment mapview count for user '" + username + "': " + err);
}
done();
});
var sql = mapconfig.layers.map(function(layer) {
return layer.options.sql;
}).join(';');
var dbName = req.params.dbname;
var cacheKey = dbName + ':' + token;
step(
function getAffectedTablesAndLastUpdatedTime() {
queryTablesApi.getAffectedTablesAndLastUpdatedTime(username, sql, this);
},
function handleAffectedTablesAndLastUpdatedTime(err, result) {
if (req.profiler) {
req.profiler.done('queryTablesAndLastUpdated');
}
assert.ifError(err);
var cacheChannel = me.buildCacheChannel(dbName, result.affectedTables);
me.channelCache[cacheKey] = cacheChannel;
// last update for layergroup cache buster
response.layergroupid = response.layergroupid + ':' + result.lastUpdatedTime;
response.last_updated = new Date(result.lastUpdatedTime).toISOString();
var res = req.res;
if (res) {
if (req.method === 'GET') {
var ttl = global.environment.varnish.layergroupTtl || 86400;
res.header('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
res.header('Last-Modified', (new Date()).toUTCString());
res.header('X-Cache-Channel', cacheChannel);
}
res.header('X-Layergroup-Id', response.layergroupid);
}
return null;
},
function finish(err) {
done(err);
}
);
};
/* X-Cache-Channel generation } */
// Check if a request is authorized by a signer
//
// @param req express request object
// @param callback function(err, signed_by) signed_by will be
// null if the request is not signed by anyone
// or will be a string cartodb username otherwise.
//
me.authorizedBySigner = function(req, callback) {
if ( ! req.params.token || ! req.params.signer ) {
return callback(null, null); // no signer requested
}
var signer = req.params.signer;
var layergroup_id = req.params.token;
var auth_token = req.params.auth_token;
var mapStore = req.app.mapStore;
if (!mapStore) {
throw new Error('Unable to retrieve map configuration token');
}
mapStore.load(layergroup_id, function(err, mapConfig) {
if (err) {
return callback(err);
}
var authorized = me.templateMaps.isAuthorized(mapConfig.obj().template, auth_token);
return callback(null, authorized ? signer : null);
});
};
// Check if a request is authorized by api_key
//
// @param req express request object
// @param callback function(err, authorized)
// NOTE: authorized is expected to be 0 or 1 (integer)
//
me.authorizedByAPIKey = function(req, callback)
{
var givenKey = req.query.api_key || req.query.map_key;
if ( ! givenKey && req.body ) {
// check also in request body
givenKey = req.body.api_key || req.body.map_key;
}
if ( ! givenKey ) {
callback(null, 0); // no api key, no authorization...
return;
}
//console.log("given ApiKey: " + givenKey);
var user = cdbRequest.userByReq(req);
step(
function (){
cartoData.getUserMapKey(user, this);
},
function checkApiKey(err, val){
assert.ifError(err);
return ( val && givenKey == val ) ? 1 : 0;
},
function finish(err, authorized) {
callback(err, authorized);
}
);
};
/**
* Check access authorization
*
* @param req - standard req object. Importantly contains table and host information
* @param callback function(err, allowed) is access allowed not?
*/
me.authorize = function(req, callback) {
var that = this;
var user = cdbRequest.userByReq(req);
step(
function (){
that.authorizedByAPIKey(req, this);
},
function checkApiKey(err, authorized){
if (req.profiler) {
req.profiler.done('authorizedByAPIKey');
}
assert.ifError(err);
// if not authorized by api_key, continue
if (authorized !== 1) {
// not authorized by api_key,
// check if authorized by signer
that.authorizedBySigner(req, this);
return;
}
// authorized by api key, login as the given username and stop
pgConnection.setDBAuth(user, req.params, function(err) {
callback(err, true); // authorized (or error)
});
},
function checkSignAuthorized(err, signed_by){
if (err) {
return callback(err);
}
if ( ! signed_by ) {
// request not authorized by signer.
// if no signer name was given, let dbparams and
// PostgreSQL do the rest.
//
if ( ! req.params.signer ) {
callback(null, true); // authorized so far
return;
}
// if signer name was given, return no authorization
callback(null, false);
return;
}
pgConnection.setDBAuth(signed_by, req.params, function(err) {
if (req.profiler) {
req.profiler.done('setDBAuth');
}
callback(err, true); // authorized (or error)
});
}
);
};
me.setDBParams = function(cdbuser, params, callback) {
step(
function setAuth() {
pgConnection.setDBAuth(cdbuser, params, this);
},
function setConn(err) {
if ( err ) throw err;
pgConnection.setDBConn(cdbuser, params, this);
},
function finish(err) {
callback(err);
}
);
};
// jshint maxcomplexity:10
/**
* Whitelist input and get database name & default geometry type from
* subdomain/user metadata held in CartoDB Redis
* @param req - standard express request obj. Should have host & table
* @param callback
*/
me.req2params = function(req, callback){
if ( req.query.lzma ) {
// Decode (from base64)
var lzma = new Buffer(req.query.lzma, 'base64')
.toString('binary')
.split('')
.map(function(c) {
return c.charCodeAt(0) - 128;
});
// Decompress
lzmaWorker.decompress(
lzma,
function(result) {
if (req.profiler) {
req.profiler.done('lzma');
}
try {
delete req.query.lzma;
_.extend(req.query, JSON.parse(result));
me.req2params(req, callback);
} catch (err) {
callback(new Error('Error parsing lzma as JSON: ' + err));
}
}
);
return;
}
req.query = _.pick(req.query, REQUEST_QUERY_PARAMS_WHITELIST);
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
var user = cdbRequest.userByReq(req);
if ( req.params.token ) {
//console.log("Request parameters include token " + req.params.token);
var tksplit = req.params.token.split(':');
req.params.token = tksplit[0];
if ( tksplit.length > 1 ) {
req.params.cache_buster= tksplit[1];
}
tksplit = req.params.token.split('@');
if ( tksplit.length > 1 ) {
req.params.signer = tksplit.shift();
if ( ! req.params.signer ) {
req.params.signer = user;
}
else if ( req.params.signer !== user ) {
var err = new Error('Cannot use map signature of user "' + req.params.signer + '" on database of user "' +
user + '"');
err.http_status = 403;
callback(err);
return;
}
if ( tksplit.length > 1 ) {
/*var template_hash = */tksplit.shift(); // unused
}
req.params.token = tksplit.shift();
//console.log("Request for token " + req.params.token + " with signature from " + req.params.signer);
}
}
// bring all query values onto req.params object
_.extend(req.params, req.query);
if (req.profiler) {
req.profiler.done('req2params.setup');
}
step(
function getPrivacy(){
me.authorize(req, this);
},
function gatekeep(err, authorized){
if (req.profiler) {
req.profiler.done('authorize');
}
assert.ifError(err);
if(!authorized) {
err = new Error("Sorry, you are unauthorized (permission denied)");
err.http_status = 403;
throw err;
}
return null;
},
function getDatabase(err){
assert.ifError(err);
pgConnection.setDBConn(user, req.params, this);
},
function finishSetup(err) {
if ( err ) { callback(err, req); return; }
// Add default database connection parameters
// if none given
_.defaults(req.params, {
dbuser: global.environment.postgres.user,
dbpassword: global.environment.postgres.password,
dbhost: global.environment.postgres.host,
dbport: global.environment.postgres.port
});
callback(null, req);
}
);
};
return me;
};
+73
View File
@@ -0,0 +1,73 @@
var _ = require('underscore');
var debug = require('debug')('windshaft:stats_client');
var StatsD = require('node-statsd').StatsD;
module.exports = {
/**
* Returns an StatsD instance or an stub object that replicates the StatsD public interface so there is no need to
* keep checking if the stats_client is instantiated or not.
*
* The first call to this method implies all future calls will use the config specified in the very first call.
*
* TODO: It's far from ideal to use make this a singleton, improvement desired.
* We proceed this way to be able to use StatsD from several places sharing one single StatsD instance.
*
* @param config Configuration for StatsD, if undefined it will return an stub
* @returns {StatsD|Object}
*/
getInstance: function(config) {
if (!this.instance) {
var instance;
if (config) {
instance = new StatsD(config);
instance.last_error = { msg: '', count: 0 };
instance.socket.on('error', function (err) {
var last_err = instance.last_error;
var last_msg = last_err.msg;
var this_msg = '' + err;
if (this_msg !== last_msg) {
debug("statsd client socket error: " + err);
instance.last_error.count = 1;
instance.last_error.msg = this_msg;
} else {
++last_err.count;
if (!last_err.interval) {
instance.last_error.interval = setInterval(function () {
var count = instance.last_error.count;
if (count > 1) {
debug("last statsd client socket error repeated " + count + " times");
instance.last_error.count = 1;
clearInterval(instance.last_error.interval);
instance.last_error.interval = null;
}
}, 1000);
}
}
});
} else {
var stubFunc = function (stat, value, sampleRate, callback) {
if (_.isFunction(callback)) {
callback(null, 0);
}
};
instance = {
timing: stubFunc,
increment: stubFunc,
decrement: stubFunc,
gauge: stubFunc,
unique: stubFunc,
set: stubFunc,
sendAll: stubFunc,
send: stubFunc
};
}
this.instance = instance;
}
return this.instance;
}
};
+53
View File
@@ -0,0 +1,53 @@
var Profiler = require('step-profiler');
/**
* Proxy to encapsulate node-step-profiler module so there is no need to check if there is an instance
*/
function ProfilerProxy(opts) {
this.profile = !!opts.profile;
this.profiler = null;
if (!!opts.profile) {
this.profiler = new Profiler({statsd_client: opts.statsd_client});
}
}
ProfilerProxy.prototype.done = function(what) {
if (this.profile) {
this.profiler.done(what);
}
};
ProfilerProxy.prototype.end = function() {
if (this.profile) {
this.profiler.end();
}
};
ProfilerProxy.prototype.start = function(what) {
if (this.profile) {
this.profiler.start(what);
}
};
ProfilerProxy.prototype.add = function(what) {
if (this.profile) {
this.profiler.add(what || {});
}
};
ProfilerProxy.prototype.sendStats = function() {
if (this.profile) {
this.profiler.sendStats();
}
};
ProfilerProxy.prototype.toString = function() {
return this.profile ? this.profiler.toString() : "";
};
ProfilerProxy.prototype.toJSONString = function() {
return this.profile ? this.profiler.toJSONString() : "{}";
};
module.exports = ProfilerProxy;
+82
View File
@@ -0,0 +1,82 @@
// - Reports stats about:
// * Total number of renderers
// * For mapnik renderers:
// - the mapnik-pool status: count, unused and waiting
// - the internally cached objects: png and grid
var _ = require('underscore');
function RendererStatsReporter(rendererCache, statsInterval) {
this.rendererCache = rendererCache;
this.statsInterval = statsInterval || 6e4;
this.renderersStatsIntervalId = null;
}
module.exports = RendererStatsReporter;
RendererStatsReporter.prototype.start = function() {
var self = this;
this.renderersStatsIntervalId = setInterval(function() {
var rendererCacheEntries = self.rendererCache.renderers;
if (!rendererCacheEntries) {
return null;
}
global.statsClient.gauge('windshaft.rendercache.count', _.keys(rendererCacheEntries).length);
var renderersStats = _.reduce(rendererCacheEntries, function(_rendererStats, cacheEntry) {
var stats = cacheEntry.renderer && cacheEntry.renderer.getStats && cacheEntry.renderer.getStats();
if (!stats) {
return _rendererStats;
}
_rendererStats.pool.count += stats.pool.count;
_rendererStats.pool.unused += stats.pool.unused;
_rendererStats.pool.waiting += stats.pool.waiting;
_rendererStats.cache.grid += stats.cache.grid;
_rendererStats.cache.png += stats.cache.png;
return _rendererStats;
},
{
pool: {
count: 0,
unused: 0,
waiting: 0
},
cache: {
png: 0,
grid: 0
}
}
);
global.statsClient.gauge('windshaft.mapnik-cache.png', renderersStats.cache.png);
global.statsClient.gauge('windshaft.mapnik-cache.grid', renderersStats.cache.grid);
global.statsClient.gauge('windshaft.mapnik-pool.count', renderersStats.pool.count);
global.statsClient.gauge('windshaft.mapnik-pool.unused', renderersStats.pool.unused);
global.statsClient.gauge('windshaft.mapnik-pool.waiting', renderersStats.pool.waiting);
}, this.statsInterval);
this.rendererCache.on('err', rendererCacheErrorListener);
this.rendererCache.on('gc', gcTimingListener);
};
function rendererCacheErrorListener() {
global.statsClient.increment('windshaft.rendercache.error');
}
function gcTimingListener(gcTime) {
global.statsClient.timing('windshaft.rendercache.gc', gcTime);
}
RendererStatsReporter.prototype.stop = function() {
this.rendererCache.removeListener('err', rendererCacheErrorListener);
this.rendererCache.removeListener('gc', gcTimingListener);
clearInterval(this.renderersStatsIntervalId);
this.renderersStatsIntervalId = null;
};
+1060 -891
View File
File diff suppressed because it is too large Load Diff
+11 -5
View File
@@ -1,7 +1,7 @@
{
"private": true,
"name": "windshaft-cartodb",
"version": "2.13.1",
"version": "2.14.0",
"description": "A map tile server for CartoDB",
"keywords": [
"cartodb"
@@ -22,23 +22,29 @@
"Sandro Santilli <strk@vizzuality.com>"
],
"dependencies": {
"express": "~4.13.3",
"body-parser": "~1.14.0",
"debug": "~2.2.0",
"step-profiler": "~0.2.1",
"node-statsd": "~0.0.7",
"underscore" : "~1.6.0",
"dot": "~1.0.2",
"windshaft": "0.51.0",
"step": "~0.0.5",
"windshaft": "https://github.com/CartoDB/Windshaft/tarball/master",
"step": "~0.0.6",
"queue-async": "~1.0.7",
"request": "~2.9.203",
"request": "~2.62.0",
"cartodb-redis": "~0.13.0",
"cartodb-psql": "~0.4.0",
"fastly-purge": "~1.0.0",
"redis-mpool": "~0.4.0",
"lru-cache": "2.6.5",
"lzma": "~1.3.7",
"log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb"
},
"devDependencies": {
"istanbul": "~0.3.6",
"mocha": "~1.21.4",
"nock": "~1.3.0",
"nock": "~2.11.0",
"jshint": "~2.6.0",
"redis": "~0.8.6",
"strftime": "~0.8.2",
+65 -58
View File
@@ -1,28 +1,28 @@
require(__dirname + '/../../support/test_helper');
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var redis = require('redis');
var step = require('step');
var FastlyPurge = require('fastly-purge');
var _ = require('underscore');
var NamedMapsCacheEntry = require(__dirname + '/../../../lib/cartodb/cache/model/named_maps_entry');
var CartodbWindshaft = require(__dirname + '/../../../lib/cartodb/cartodb_windshaft');
var CartodbWindshaft = require(__dirname + '/../../../lib/cartodb/server');
describe('templates surrogate keys', function() {
var redisClient = redis.createClient(global.environment.redis.port);
var serverOptions = require('../../../lib/cartodb/server_options');
// Enable Varnish purge for tests
var varnishHost = global.environment.varnish.host;
global.environment.varnish.host = '127.0.0.1';
var varnishPurgeEnabled = global.environment.varnish.purge_enabled;
global.environment.varnish.purge_enabled = true;
var varnishHost = serverOptions.varnish_host;
serverOptions.varnish_host = '127.0.0.1';
var varnishPurgeEnabled = serverOptions.varnish_purge_enabled;
serverOptions.varnish_purge_enabled = true;
var fastlyConfig = global.environment.fastly;
var fastlyConfig = serverOptions.fastly;
var FAKE_FASTLY_API_KEY = 'fastly-api-key';
var FAKE_FASTLY_SERVICE_ID = 'fake-service-id';
global.environment.fastly = {
serverOptions.fastly = {
enabled: true,
// the fastly api key
apiKey: FAKE_FASTLY_API_KEY,
@@ -30,35 +30,40 @@ describe('templates surrogate keys', function() {
serviceId: FAKE_FASTLY_SERVICE_ID
};
var serverOptions = require('../../../lib/cartodb/server_options')();
var server = new CartodbWindshaft(serverOptions);
var templateOwner = 'localhost',
templateName = 'acceptance',
expectedTemplateId = templateName,
template = {
version: '0.0.1',
name: templateName,
auth: {
method: 'open'
},
layergroup: {
version: '1.2.0',
layers: [
{
options: {
sql: 'select 1 cartodb_id, null::geometry as the_geom_webmercator',
cartocss: '#layer { marker-fill:blue; }',
cartocss_version: '2.3.0'
}
}
]
}
var templateOwner = 'localhost';
var templateName = 'acceptance';
var expectedTemplateId = templateName;
var template = {
version: '0.0.1',
name: templateName,
auth: {
method: 'open'
},
expectedBody = { template_id: expectedTemplateId };
layergroup: {
version: '1.2.0',
layers: [
{
options: {
sql: 'select 1 cartodb_id, null::geometry as the_geom_webmercator',
cartocss: '#layer { marker-fill:blue; }',
cartocss_version: '2.3.0'
}
}
]
}
};
var templateUpdated = _.extend({}, template, {layergroup: {layers: [{
type: 'plain',
options: {
color: 'red'
}
}]} });
var expectedBody = { template_id: expectedTemplateId };
var varnishHttpUrl = [
'http://', global.environment.varnish.host, ':', global.environment.varnish.http_port
'http://', serverOptions.varnish_host, ':', serverOptions.varnish_http_port
].join('');
var cacheEntryKey = new NamedMapsCacheEntry(templateOwner, templateName).key();
@@ -70,10 +75,10 @@ describe('templates surrogate keys', function() {
after(function(done) {
serverOptions.varnish_purge_enabled = false;
global.environment.varnish.host = varnishHost;
global.environment.varnish.purge_enabled = varnishPurgeEnabled;
serverOptions.varnish_host = varnishHost;
serverOptions.varnish_purge_enabled = varnishPurgeEnabled;
global.environment.fastly = fastlyConfig;
serverOptions.fastly = fastlyConfig;
nock.restore();
done();
@@ -148,7 +153,7 @@ describe('templates surrogate keys', function() {
host: templateOwner,
'Content-Type': 'application/json'
},
data: JSON.stringify(template)
data: JSON.stringify(templateUpdated)
};
var next = this;
assert.response(server,
@@ -157,7 +162,9 @@ describe('templates surrogate keys', function() {
status: 200
},
function(res) {
next(null, res);
setTimeout(function() {
next(null, res);
}, 50);
}
);
},
@@ -177,14 +184,7 @@ describe('templates surrogate keys', function() {
if ( err ) {
return done(err);
}
redisClient.keys("map_*|localhost", function(err, keys) {
if ( err ) {
return done(err);
}
redisClient.del(keys, function(err) {
return done(err);
});
});
testHelper.deleteRedisKeys({'map_tpl|localhost': 0}, done);
}
);
});
@@ -228,7 +228,9 @@ describe('templates surrogate keys', function() {
status: 204
},
function(res) {
next(null, res);
setTimeout(function() {
next(null, res);
}, 50);
}
);
},
@@ -255,6 +257,15 @@ describe('templates surrogate keys', function() {
.matchHeader('Invalidation-Match', invalidationMatchHeader)
.reply(503, '');
var fastlyScope = nock(FastlyPurge.FASTLY_API_ENDPOINT)
.post(fastlyPurgePath)
.matchHeader('Fastly-Key', FAKE_FASTLY_API_KEY)
.matchHeader('Fastly-Soft-Purge', 1)
.matchHeader('Accept', 'application/json')
.reply(200, {
status:'ok'
});
step(
function createTemplateToUpdate() {
createTemplate(this);
@@ -270,7 +281,7 @@ describe('templates surrogate keys', function() {
host: templateOwner,
'Content-Type': 'application/json'
},
data: JSON.stringify(template)
data: JSON.stringify(templateUpdated)
};
var next = this;
assert.response(server,
@@ -279,7 +290,9 @@ describe('templates surrogate keys', function() {
status: 200
},
function(res) {
next(null, res);
setTimeout(function() {
next(null, res);
}, 50);
}
);
},
@@ -291,6 +304,7 @@ describe('templates surrogate keys', function() {
assert.deepEqual(parsedBody, expectedBody);
assert.equal(scope.pendingMocks().length, 0);
assert.equal(fastlyScope.pendingMocks().length, 0);
return null;
},
@@ -298,14 +312,7 @@ describe('templates surrogate keys', function() {
if ( err ) {
return done(err);
}
redisClient.keys("map_*|localhost", function(err, keys) {
if ( err ) {
return done(err);
}
redisClient.del(keys, function(err) {
return done(err);
});
});
testHelper.deleteRedisKeys({'map_tpl|localhost': 0}, done);
}
);
});
+79 -57
View File
@@ -1,24 +1,22 @@
require(__dirname + '/../support/test_helper');
var assert = require('../support/assert');
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
var server = new CartodbWindshaft(serverOptions);
var fs = require('fs');
var metadataBackend = {};
var tilelive = {};
var HealthCheck = require('../../lib/cartodb/monitoring/health_check');
var healthCheck = new HealthCheck(metadataBackend, tilelive);
var assert = require('../support/assert');
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
describe('health checks', function () {
function resetHealthConfig() {
function enableHealthConfig() {
global.environment.health = {
enabled: true,
username: 'localhost',
z: 0,
x: 0,
y: 0
enabled: true
};
}
function disableHealthConfig() {
global.environment.health = {
enabled: false
};
}
@@ -30,65 +28,89 @@ describe('health checks', function () {
}
};
beforeEach(enableHealthConfig);
afterEach(disableHealthConfig);
var RESPONSE_OK = {
status: 200
};
var RESPONSE_FAIL = {
status: 503
};
it('returns 200 and ok=true with enabled configuration', function (done) {
resetHealthConfig();
var server = new CartodbWindshaft(serverOptions);
assert.response(server,
healthCheckRequest,
{
status: 200
},
function (res, err) {
assert.ok(!err);
assert.response(server, healthCheckRequest, RESPONSE_OK, function (res, err) {
assert.ok(!err);
var parsed = JSON.parse(res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.enabled);
assert.ok(parsed.ok);
assert.ok(parsed.enabled);
assert.ok(parsed.ok);
done();
}
);
done();
});
});
it('error if disabled file exists', function(done) {
var fs = require('fs');
var errorMessage = "Maintenance";
var readFileFn = fs.readFile;
fs.readFile = function(filename, callback) {
callback(null, "Maintenance");
};
healthCheck.check(function(err) {
assert.equal(err.message, "Maintenance");
assert.equal(err.http_status, 503);
done();
fs.readFile = readFileFn;
});
});
var readFileFn = fs.readFile;
fs.readFile = function(filename, callback) {
callback(null, errorMessage);
};
var server = new CartodbWindshaft(serverOptions);
assert.response(server, healthCheckRequest, RESPONSE_FAIL, function(res, err) {
fs.readFile = readFileFn;
assert.ok(!err);
var parsed = JSON.parse(res.body);
assert.ok(parsed.enabled);
assert.ok(!parsed.ok);
assert.equal(parsed.err, errorMessage);
done();
});
});
it('error if disabled file exists but has no content', function(done) {
var readFileFn = fs.readFile;
fs.readFile = function(filename, callback) {
callback(null, '');
};
var server = new CartodbWindshaft(serverOptions);
assert.response(server, healthCheckRequest, RESPONSE_FAIL, function(res, err) {
fs.readFile = readFileFn;
assert.ok(!err);
var parsed = JSON.parse(res.body);
assert.ok(parsed.enabled);
assert.ok(!parsed.ok);
assert.equal(parsed.err, 'Unknown error');
done();
});
});
it('not err if disabled file does not exists', function(done) {
resetHealthConfig();
global.environment.disabled_file = '/tmp/ftreftrgtrccre';
global.environment.disabled_file = '/tmp/ftreftrgtrccre';
var server = new CartodbWindshaft(serverOptions);
assert.response(server,
healthCheckRequest,
{
status: 200
},
function (res, err) {
assert.ok(!err);
assert.response(server, healthCheckRequest, RESPONSE_OK, function (res, err) {
assert.ok(!err);
var parsed = JSON.parse(res.body);
var parsed = JSON.parse(res.body);
assert.equal(parsed.enabled, true);
assert.equal(parsed.ok, true);
assert.equal(parsed.enabled, true);
assert.equal(parsed.ok, true);
done();
}
);
done();
});
});
});
+19 -16
View File
@@ -1,37 +1,30 @@
require('../support/test_helper');
var testHelper = require('../support/test_helper');
var assert = require('../support/assert');
var _ = require('underscore');
var redis = require('redis');
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
describe('render limits', function() {
var layergroupUrl = '/api/v1/map';
var redisClient = redis.createClient(global.environment.redis.port);
after(function(done) {
redisClient.keys("map_style|*", function(err, matches) {
redisClient.del(matches, function() {
done();
});
});
});
var server;
var keysToDelete;
beforeEach(function() {
server = new CartodbWindshaft(serverOptions());
keysToDelete = {};
server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
});
var keysToDelete = [];
afterEach(function(done) {
redisClient.DEL(keysToDelete, function() {
keysToDelete = [];
done();
});
testHelper.deleteRedisKeys(keysToDelete, done);
});
var user = 'localhost';
@@ -81,7 +74,7 @@ describe('render limits', function() {
if (err) {
return callback(err);
}
keysToDelete.push(userLimitsKey);
keysToDelete[userLimitsKey] = 5;
return callback();
});
});
@@ -135,6 +128,8 @@ describe('render limits', function() {
function(res) {
var parsed = JSON.parse(res.body);
assert.ok(parsed.layergroupid);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
}
);
@@ -155,6 +150,8 @@ describe('render limits', function() {
status: 200
},
function(res) {
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
assert.response(server,
{
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
@@ -197,6 +194,8 @@ describe('render limits', function() {
status: 200
},
function(res) {
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
assert.response(server,
{
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
@@ -246,6 +245,8 @@ describe('render limits', function() {
function(res) {
var parsed = JSON.parse(res.body);
assert.ok(parsed.layergroupid);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
}
);
@@ -265,6 +266,8 @@ describe('render limits', function() {
status: 200
},
function(res) {
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
assert.response(server,
{
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
+176 -253
View File
@@ -6,32 +6,45 @@ var strftime = require('strftime');
var redis_stats_db = 5;
var helper = require(__dirname + '/../support/test_helper');
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
var windshaft_fixtures = __dirname + '/../../node_modules/windshaft/test/fixtures';
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 20;
var IMAGE_EQUALS_HIGHER_TOLERANCE_PER_MIL = 25;
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions());
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
var TablesCacheEntry = require('../../lib/cartodb/cache/model/database_tables_entry');
['/api/v1/map', '/user/localhost/api/v1/map'].forEach(function(layergroup_url) {
var suiteName = 'multilayer:postgres=layergroup_url=' + layergroup_url;
suite(suiteName, function() {
describe(suiteName, function() {
var keysToDelete;
beforeEach(function() {
keysToDelete = {};
});
afterEach(function(done) {
helper.deleteRedisKeys(keysToDelete, done);
});
var cdbQueryTablesFromPostgresEnabledValue = true;
var redis_client = redis.createClient(global.environment.redis.port);
var expected_last_updated_epoch = 1234567890123; // this is hard-coded into SQLAPIEmu
var expected_last_updated = new Date(expected_last_updated_epoch).toISOString();
var test_user = _.template(global.environment.postgres_auth_user, {user_id:1});
var test_database = test_user + '_db';
test("layergroup with 2 layers, each with its style", function(done) {
it("layergroup with 2 layers, each with its style", function(done) {
var layergroup = {
version: '1.0.0',
@@ -67,17 +80,14 @@ suite(suiteName, function() {
assert.equal(res.statusCode, 200, res.body);
var parsedBody = JSON.parse(res.body);
assert.equal(parsedBody.last_updated, expected_last_updated);
if ( expected_token ) {
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
assert.equal(res.headers['x-layergroup-id'], parsedBody.layergroupid);
}
else expected_token = parsedBody.layergroupid.split(':')[0];
assert.equal(res.headers['x-layergroup-id'], parsedBody.layergroupid);
expected_token = parsedBody.layergroupid;
next(null, res);
});
},
function do_get_tile(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png',
@@ -122,7 +132,7 @@ suite(suiteName, function() {
// See https://github.com/CartoDB/Windshaft-cartodb/issues/170
function do_get_tile_nosignature(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + '/localhost@' + expected_token + ':cb0/0/0/0.png',
@@ -139,7 +149,7 @@ suite(suiteName, function() {
},
function do_get_grid_layer0(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + '/0/0/0/0.grid.json',
@@ -156,7 +166,7 @@ suite(suiteName, function() {
},
function do_get_grid_layer1(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + '/1/0/0/0.grid.json',
@@ -172,26 +182,15 @@ suite(suiteName, function() {
});
},
function finish(err) {
var errors = [];
if ( err ) {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
if ( errors.length ) done(new Error(errors));
else done(null);
});
});
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done(err);
}
);
});
test("should include serverMedata in the response", function(done) {
it("should include serverMedata in the response", function(done) {
global.environment.serverMetadata = { cdn_url : { http:'test', https: 'tests' } };
var layergroup = {
version: '1.0.0',
@@ -217,6 +216,8 @@ suite(suiteName, function() {
},
function do_check_create(err, res) {
var parsed = JSON.parse(res.body);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
assert.ok(_.isEqual(parsed.cdn_url, global.environment.serverMetadata.cdn_url));
done();
}
@@ -224,19 +225,25 @@ suite(suiteName, function() {
});
test("get creation requests has cache", function(done) {
it("get creation requests has cache", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
{ options: {
sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, 5e6, 0) as the_geom_webmercator' +
' from test_table limit 2',
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
cartocss_version: '2.0.1'
} }
]
};
var layergroup = {
version: '1.0.0',
layers: [
{ options: {
sql: 'select cartodb_id, the_geom_webmercator from test_table',
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
cartocss_version: '2.0.1',
interactivity: 'cartodb_id'
} },
{ options: {
sql: 'select cartodb_id, the_geom_webmercator from test_table_2',
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
cartocss_version: '2.0.2',
interactivity: 'cartodb_id'
} }
]
};
var expected_token;
step(
@@ -250,33 +257,26 @@ suite(suiteName, function() {
}, {}, function(res, err) { next(err, res); });
},
function do_check_create(err, res) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(res.statusCode, 200, res.body);
var parsedBody = JSON.parse(res.body);
expected_token = parsedBody.layergroupid.split(':')[0];
helper.checkCache(res);
return null;
},
function finish(err) {
var errors = [];
if ( err ) {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
if ( errors.length ) done(new Error(errors));
else done(null);
});
});
helper.checkSurrogateKey(res, new TablesCacheEntry('test_windshaft_cartodb_user_1_db', [
'public.test_table',
'public.test_table_2'
]).key().join(' '));
keysToDelete['map_cfg|' + expected_token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
}
);
});
test("get creation has no cache if sql is bogus", function(done) {
it("get creation has no cache if sql is bogus", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
@@ -298,7 +298,7 @@ suite(suiteName, function() {
});
});
test("get creation has no cache if cartocss is not valid", function(done) {
it("get creation has no cache if cartocss is not valid", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
@@ -321,7 +321,7 @@ suite(suiteName, function() {
});
});
test("layergroup can hold substitution tokens", function(done) {
it("layergroup can hold substitution tokens", function(done) {
var layergroup = {
version: '1.0.0',
@@ -353,13 +353,15 @@ suite(suiteName, function() {
if ( expected_token ) {
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
}
else expected_token = parsedBody.layergroupid.split(':')[0];
else {
expected_token = parsedBody.layergroupid.split(':')[0];
}
next(null, res);
});
},
function do_get_tile1(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb10/1/0/0.png',
@@ -398,7 +400,7 @@ suite(suiteName, function() {
},
function do_get_tile4(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb11/4/0/0.png',
@@ -437,7 +439,7 @@ suite(suiteName, function() {
},
function do_get_grid1(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + '/0/1/0/0.grid.json',
@@ -454,7 +456,7 @@ suite(suiteName, function() {
},
function do_get_grid4(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + '/0/4/0/0.grid.json',
@@ -463,32 +465,19 @@ suite(suiteName, function() {
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
assert.utfgridEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.grid.json', 2,
function(err/*, similarity*/) {
next(err);
});
assert.utfgridEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.grid.json', 2, next);
});
},
function finish(err) {
var errors = [];
if ( err ) {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
if ( errors.length ) done(new Error(errors));
else done(null);
});
});
keysToDelete['map_cfg|' + expected_token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done(err);
}
);
});
test("layergroup creation raises mapviews counter", function(done) {
it("layergroup creation raises mapviews counter", function(done) {
var layergroup = {
stat_tag: 'random_tag',
version: '1.0.0',
@@ -505,19 +494,22 @@ suite(suiteName, function() {
var redis_stats_client = redis.createClient(global.environment.redis.port);
var expected_token; // will be set on first post and checked on second
var now = strftime("%Y%m%d", new Date());
var errors = [];
step(
function clean_stats()
{
var next = this;
redis_stats_client.select(redis_stats_db, function(err) {
if ( err ) next(err);
else redis_stats_client.del(statskey+':global', next);
if ( err ) {
next(err);
}
else {
redis_stats_client.del(statskey+':global', next);
}
});
},
function do_post_1(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url,
@@ -531,12 +523,12 @@ suite(suiteName, function() {
});
},
function check_global_stats_1(err, val) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(val, 1, "Expected score of " + now + " in " + statskey + ":global to be 1, got " + val);
redis_stats_client.zscore(statskey+':stat_tag:random_tag', now, this);
},
function check_tag_stats_1_do_post_2(err, val) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(val, 1, "Expected score of " + now + " in " + statskey + ":stat_tag:" + layergroup.stat_tag +
" to be 1, got " + val);
var next = this;
@@ -553,39 +545,32 @@ suite(suiteName, function() {
},
function check_global_stats_2(err, val)
{
if ( err ) throw err;
assert.ifError(err);
assert.equal(val, 2, "Expected score of " + now + " in " + statskey + ":global to be 2, got " + val);
redis_stats_client.zscore(statskey+':stat_tag:' + layergroup.stat_tag, now, this);
},
function check_tag_stats_2(err, val)
{
if ( err ) throw err;
assert.ifError(err);
assert.equal(val, 2, "Expected score of " + now + " in " + statskey + ":stat_tag:" + layergroup.stat_tag +
" to be 2, got " + val);
return 1;
},
function cleanup_map_style(err) {
if ( err ) errors.push('' + err);
var next = this;
// trip epoch
expected_token = expected_token.split(':')[0];
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
redis_client.del(matches, next);
});
},
function cleanup_stats(err) {
if ( err ) errors.push('' + err);
redis_client.del([statskey+':global', statskey+':stat_tag:'+layergroup.stat_tag], this);
},
function finish(err) {
if ( err ) errors.push('' + err);
if ( errors.length ) done(new Error(errors.join(',')));
else done(null);
if ( err ) {
return done(err);
}
// trip epoch
expected_token = expected_token.split(':')[0];
keysToDelete['map_cfg|' + expected_token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
keysToDelete[statskey+':stat_tag:'+layergroup.stat_tag] = 5;
done();
}
);
});
test("layergroup creation fails if CartoCSS is bogus", function(done) {
it("layergroup creation fails if CartoCSS is bogus", function(done) {
var layergroup = {
stat_tag: 'random_tag',
version: '1.0.0',
@@ -614,7 +599,7 @@ suite(suiteName, function() {
// Also tests that server doesn't crash:
// see http://github.com/CartoDB/Windshaft-cartodb/issues/109
test("layergroup creation fails if sql is bogus", function(done) {
it("layergroup creation fails if sql is bogus", function(done) {
var layergroup = {
stat_tag: 'random_tag',
version: '1.0.0',
@@ -641,7 +626,7 @@ suite(suiteName, function() {
});
});
test("layergroup with 2 private-table layers", function(done) {
it("layergroup with 2 private-table layers", function(done) {
var layergroup = {
version: '1.0.0',
@@ -678,13 +663,15 @@ suite(suiteName, function() {
if ( expected_token ) {
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
}
else expected_token = parsedBody.layergroupid.split(':')[0];
else {
expected_token = parsedBody.layergroupid.split(':')[0];
}
next(null, res);
});
},
function do_get_tile(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png?map_key=1234',
@@ -705,7 +692,7 @@ suite(suiteName, function() {
},
function do_get_grid_layer0(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + '/0/0/0/0.grid.json?map_key=1234',
@@ -718,7 +705,7 @@ suite(suiteName, function() {
},
function do_get_grid_layer1(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + '/1/0/0/0.grid.json?map_key=1234',
@@ -732,7 +719,7 @@ suite(suiteName, function() {
},
function do_get_tile_unauth(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png',
@@ -748,7 +735,7 @@ suite(suiteName, function() {
},
function do_get_grid_layer0_unauth(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + '/0/0/0/0.grid.json',
@@ -763,7 +750,7 @@ suite(suiteName, function() {
},
function do_get_grid_layer1_unauth(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + '/1/0/0/0.grid.json',
@@ -777,26 +764,16 @@ suite(suiteName, function() {
});
},
function finish(err) {
var errors = [];
if ( err ) {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
if ( errors.length ) done(new Error(errors));
else done(null);
});
});
keysToDelete['map_cfg|' + expected_token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done(err);
}
);
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/152
test("x-cache-channel still works for GETs after tiler restart", function(done) {
it("x-cache-channel still works for GETs after tiler restart", function(done) {
var layergroup = {
version: '1.0.0',
@@ -823,19 +800,21 @@ suite(suiteName, function() {
}, {}, function(res, err) { next(err, res); });
},
function check_post(err, res) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(res.statusCode, 200, res.body);
var parsedBody = JSON.parse(res.body);
assert.equal(parsedBody.last_updated, expected_last_updated);
if ( expected_token ) {
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
}
else expected_token = parsedBody.layergroupid.split(':')[0];
else {
expected_token = parsedBody.layergroupid.split(':')[0];
}
return null;
},
function do_get0(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png?map_key=1234',
@@ -845,7 +824,7 @@ suite(suiteName, function() {
}, {}, function(res, err) { next(err, res); });
},
function do_check0(err, res) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "image/png");
@@ -857,14 +836,14 @@ suite(suiteName, function() {
return null;
},
function do_restart_server(err/*, res*/) {
if ( err ) throw err;
assert.ifError(err);
// hack simulating restart...
server = new CartodbWindshaft(serverOptions());
server = new CartodbWindshaft(serverOptions);
return null;
},
function do_get1(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png?map_key=1234',
@@ -874,7 +853,7 @@ suite(suiteName, function() {
}, {}, function(res, err) { next(err, res); });
},
function do_check1(err, res) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "image/png");
@@ -886,26 +865,16 @@ suite(suiteName, function() {
return null;
},
function finish(err) {
var errors = [];
if ( err ) {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
if ( errors.length ) done(new Error(errors.join(',')));
else done(null);
});
});
keysToDelete['map_cfg|' + expected_token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done(err);
}
);
});
// https://github.com/cartodb/Windshaft-cartodb/issues/81
test("invalid text-name in CartoCSS", function(done) {
it("invalid text-name in CartoCSS", function(done) {
var layergroup = {
version: '1.0.1',
@@ -933,7 +902,7 @@ suite(suiteName, function() {
});
});
test("quotes CartoCSS", function(done) {
it("quotes CartoCSS", function(done) {
var layergroup = {
version: '1.0.1',
@@ -958,12 +927,14 @@ suite(suiteName, function() {
data: JSON.stringify(layergroup)
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
});
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/87
test("exponential notation in CartoCSS filter values", function(done) {
it("exponential notation in CartoCSS filter values", function(done) {
var layergroup = {
version: '1.0.1',
layers: [
@@ -981,12 +952,14 @@ suite(suiteName, function() {
data: JSON.stringify(layergroup)
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
});
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/93
test("accepts unused directives", function(done) {
it("accepts unused directives", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
@@ -1024,7 +997,7 @@ suite(suiteName, function() {
},
function do_get_tile(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png',
@@ -1042,28 +1015,17 @@ suite(suiteName, function() {
});
},
function finish(err) {
var errors = [];
if ( err ) {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
if ( errors.length ) done(new Error(errors));
else done(null);
});
});
keysToDelete['user:localhost:mapviews:global'] = 5;
keysToDelete['map_cfg|' + expected_token] = 0;
done(err);
}
);
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/91
// and https://github.com/CartoDB/Windshaft-cartodb/issues/38
test("tiles for private tables can be fetched with api_key", function(done) {
var errors = [];
it("tiles for private tables can be fetched with api_key", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
@@ -1087,7 +1049,7 @@ suite(suiteName, function() {
}, {}, function(res) { next(null, res); });
},
function check_result(err, res) {
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var parsedBody = JSON.parse(res.body);
@@ -1104,7 +1066,7 @@ suite(suiteName, function() {
},
function do_get_tile(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png?api_key=1234',
@@ -1114,43 +1076,29 @@ suite(suiteName, function() {
}, {}, function(res) { next(null, res); });
},
function check_get_tile(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
return null;
},
function cleanup(err) {
if ( err ) errors.push(err.message);
if ( ! expected_token ) return null;
var next = this;
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
next();
});
});
},
function finish(err) {
if ( err ) {
errors.push(err.message);
console.log("Error: " + err);
}
if ( errors.length ) done(new Error(errors));
else done(null);
if (err) {
return done(err);
}
assert.equal(res.statusCode, 200, res.body);
keysToDelete['user:localhost:mapviews:global'] = 5;
keysToDelete['map_cfg|' + expected_token] = 0;
done(err);
}
);
});
// SQL strings can be of arbitrary length, when using POST
// See https://github.com/CartoDB/Windshaft-cartodb/issues/111
test("sql string can be very long", function(done){
it("sql string can be very long", function(done){
var long_val = 'pretty';
for (var i=0; i<1024; ++i) long_val += ' long';
for (var i=0; i<1024; ++i) {
long_val += ' long';
}
long_val += ' string';
var sql = "SELECT ";
for (i=0; i<16; ++i)
for (i=0; i<16; ++i) {
sql += "'" + long_val + "'::text as pretty_long_field_name_" + i + ", ";
}
sql += "cartodb_id, the_geom_webmercator FROM gadm4 g";
var layergroup = {
version: '1.0.0',
@@ -1162,8 +1110,7 @@ suite(suiteName, function() {
} }
]
};
var errors = [];
var expected_token;
var expected_token;
step(
function do_post()
{
@@ -1178,7 +1125,7 @@ suite(suiteName, function() {
}, {}, function(res) { next(null, res); });
},
function check_result(err, res) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var parsedBody = JSON.parse(res.body);
var token_components = parsedBody.layergroupid.split(':');
@@ -1186,28 +1133,19 @@ suite(suiteName, function() {
return null;
},
function cleanup(err) {
if ( err ) errors.push('' + err);
if ( ! expected_token ) return null;
var next = this;
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
next();
});
});
},
function finish(err) {
if ( err ) errors.push('' + err);
if ( errors.length ) done(new Error(errors.join(',')));
else done(null);
if (err) {
return done(err);
}
keysToDelete['user:localhost:mapviews:global'] = 5;
keysToDelete['map_cfg|' + expected_token] = 0;
done(err);
}
);
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/133
test("MapConfig with mapnik layer and no cartocss", function(done) {
it("MapConfig with mapnik layer and no cartocss", function(done) {
var layergroup = {
version: '1.0.0',
@@ -1232,7 +1170,7 @@ suite(suiteName, function() {
}, {}, function(res, err) { next(err, res); });
},
function check_post(err, res) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, 'Missing "errors" in response: ' + JSON.stringify(parsed));
@@ -1249,7 +1187,7 @@ suite(suiteName, function() {
if (!cdbQueryTablesFromPostgresEnabledValue) { // only test if it was using the SQL API
// See https://github.com/CartoDB/Windshaft-cartodb/issues/167
test("lack of response from sql-api will result in a timeout", function(done) {
it("lack of response from sql-api will result in a timeout", function(done) {
var layergroup = {
version: '1.0.0',
@@ -1274,7 +1212,7 @@ suite(suiteName, function() {
}, {}, function(res, err) { next(err, res); });
},
function check_post(err, res) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, 'Missing "errors" in response: ' + JSON.stringify(parsed));
@@ -1308,25 +1246,29 @@ suite(suiteName, function() {
status: 200
};
test("cache control for layergroup default value", function(done) {
it("cache control for layergroup default value", function(done) {
global.environment.varnish.layergroupTtl = null;
assert.response(server, layergroupTtlRequest, layergroupTtlResponseExpectation,
function(res) {
assert.equal(res.headers['cache-control'], 'public,max-age=86400,must-revalidate');
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
}
);
});
test("cache control for layergroup uses configuration for max-age", function(done) {
it("cache control for layergroup uses configuration for max-age", function(done) {
var layergroupTtl = 300;
global.environment.varnish.layergroupTtl = layergroupTtl;
assert.response(server, layergroupTtlRequest, layergroupTtlResponseExpectation,
function(res) {
assert.equal(res.headers['cache-control'], 'public,max-age=' + layergroupTtl + ',must-revalidate');
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
}
@@ -1334,7 +1276,7 @@ suite(suiteName, function() {
});
test("it's not possible to override authorization with a crafted layergroup", function(done) {
it("it's not possible to override authorization with a crafted layergroup", function(done) {
var layergroup = {
version: '1.0.0',
@@ -1376,25 +1318,6 @@ suite(suiteName, function() {
}
);
});
suiteTeardown(function(done) {
// This test will add map_style records, like
// 'map_style|null|publicuser|my_table',
redis_client.keys("map_style|*", function(err, matches) {
redis_client.del(matches, function() {
redis_client.select(5, function() {
redis_client.keys("user:localhost:mapviews*", function(err, matches) {
redis_client.del(matches, function() {
done();
});
});
});
});
});
});
});
+38 -14
View File
@@ -2,13 +2,13 @@ var testHelper = require('../support/test_helper');
var assert = require('../support/assert');
var redis = require('redis');
var _ = require('underscore');
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
var PgQueryRunner = require('../../lib/cartodb/backends/pg_query_runner');
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
var serverOptions = require('../../lib/cartodb/server_options')();
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
@@ -16,15 +16,14 @@ describe('tests from old api translated to multilayer', function() {
var layergroupUrl = '/api/v1/map';
var redisClient = redis.createClient(global.environment.redis.port);
after(function(done) {
// This test will add map_style records, like
// 'map_style|null|publicuser|my_table',
redisClient.keys("map_style|*", function(err, matches) {
redisClient.del(matches, function() {
done();
});
});
var keysToDelete;
beforeEach(function() {
keysToDelete = {};
});
afterEach(function(done) {
testHelper.deleteRedisKeys(keysToDelete, done);
});
var wadusSql = 'select 1 as cartodb_id, null::geometry as the_geom_webmercator';
@@ -109,6 +108,10 @@ describe('tests from old api translated to multilayer', function() {
var parsed = JSON.parse(res.body);
assert.ok(parsed.layergroupid);
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
}
);
@@ -131,6 +134,10 @@ describe('tests from old api translated to multilayer', function() {
assert.ok(parsed.layergroupid);
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
keysToDelete['user:cartodb250user:mapviews:global'] = 5;
global.environment.postgres.host = backupDBHost;
done();
}
@@ -154,6 +161,9 @@ describe('tests from old api translated to multilayer', function() {
assert.ok(parsed.layergroupid);
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
keysToDelete['user:cartodb250user:mapviews:global'] = 5;
global.environment.postgres_auth_pass = backupDBPass;
done();
}
@@ -185,6 +195,9 @@ describe('tests from old api translated to multilayer', function() {
var parsed = JSON.parse(res.body);
assert.ok(parsed.layergroupid);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
}
);
@@ -251,6 +264,9 @@ describe('tests from old api translated to multilayer', function() {
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
}
);
@@ -316,9 +332,13 @@ describe('tests from old api translated to multilayer', function() {
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
// TODO when affected tables query makes the request to fail layergroup should be removed
keysToDelete['map_cfg|f14693f2d7b6dcf4629724b3d1efe22d'] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
var parsed = JSON.parse(res.body);
assert.deepEqual(parsed, {
errors: ["Error: could not fetch affected tables and last updated time: fake error message"]
errors: ["Error: could not fetch affected tables or last updated time: fake error message"]
});
done();
@@ -340,13 +360,17 @@ describe('tests from old api translated to multilayer', function() {
status: 200
},
function(res) {
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
var runQueryFn = PgQueryRunner.prototype.run;
PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) {
return queryHandler(new Error('failed to query database for affected tables'), [], callback);
};
// reset internal cacheChannel cache
serverOptions.channelCache = {};
server.layergroupAffectedTablesCache.cache.reset();
assert.response(server,
{
+155 -18
View File
@@ -1,12 +1,14 @@
var test_helper = require('../support/test_helper');
var assert = require('../support/assert');
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
var RedisPool = require('redis-mpool');
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
var step = require('step');
@@ -95,7 +97,17 @@ describe('named_layers', function() {
}
};
before(function(done) {
var keysToDelete;
beforeEach(function() {
keysToDelete = {};
});
afterEach(function(done) {
test_helper.deleteRedisKeys(keysToDelete, done);
});
beforeEach(function(done) {
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: true};
templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) {
if (err) {
@@ -112,6 +124,23 @@ describe('named_layers', function() {
});
});
afterEach(function(done) {
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: false};
templateMaps.delTemplate(username, nestedNamedMapTemplateName, function(err) {
if (err) {
return done(err);
}
templateMaps.delTemplate(username, tokenAuthTemplateName, function(err) {
if (err) {
return done(err);
}
templateMaps.delTemplate(username, templateName, function(err) {
return done(err);
});
});
});
});
it('should fail for non-existing template name', function(done) {
var layergroup = {
version: '1.3.0',
@@ -265,6 +294,9 @@ describe('named_layers', function() {
assert.ok(parsedBody.layergroupid);
assert.ok(parsedBody.last_updated);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
return null;
},
function finish(err) {
@@ -398,6 +430,9 @@ describe('named_layers', function() {
assert.ok(parsedBody.layergroupid);
assert.ok(parsedBody.last_updated);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
return parsedBody.layergroupid;
},
function requestTile(err, layergroupId) {
@@ -435,7 +470,7 @@ describe('named_layers', function() {
},
function deleteTemplate(err) {
var next = this;
templateMaps.delTemplate(username, privateTableTemplate, function(/*delErr*/) {
templateMaps.delTemplate(username, privateTableTemplateName, function(/*delErr*/) {
// ignore deletion error
next(err);
});
@@ -526,6 +561,9 @@ describe('named_layers', function() {
assert.ok(parsedBody.layergroupid);
assert.ok(parsedBody.last_updated);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
return parsedBody.layergroupid;
},
function requestTile(err, layergroupId) {
@@ -563,7 +601,7 @@ describe('named_layers', function() {
},
function deleteTemplate(err) {
var next = this;
templateMaps.delTemplate(username, privateTableTemplate, function(/*delErr*/) {
templateMaps.delTemplate(username, privateTableTemplateNameInteractivity, function(/*delErr*/) {
// ignore deletion error
next(err);
});
@@ -707,6 +745,9 @@ describe('named_layers', function() {
assert.equal(parsedBody.metadata.layers[3].type, 'mapnik');
assert.equal(parsedBody.metadata.layers[4].type, 'torque');
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
return null;
},
function finish(err) {
@@ -716,22 +757,118 @@ describe('named_layers', function() {
});
it('should work with named tiles', function(done) {
after(function(done) {
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: false};
templateMaps.delTemplate(username, nestedNamedMapTemplateName, function(err) {
if (err) {
return done(err);
var namedTilesTemplateName = 'named_tiles_template';
var namedTilesTemplate = {
version: '0.0.1',
name: namedTilesTemplateName,
auth: {
method: 'open'
},
layergroup: {
layers: [
namedMapLayer,
{
type: 'mapnik',
options: {
sql: 'select * from test_table_private_1',
cartocss: '#layer { marker-fill: #cc3300; }',
cartocss_version: '2.3.0'
}
}
]
}
templateMaps.delTemplate(username, tokenAuthTemplateName, function(err) {
};
step(
function createTemplate() {
templateMaps.addTemplate(username, namedTilesTemplate, this);
},
function createLayergroup(err) {
if (err) {
return done(err);
throw err;
}
templateMaps.delTemplate(username, templateName, function(err) {
return done(err);
var next = this;
assert.response(server,
{
url: '/api/v1/map/named/' + namedTilesTemplateName + '?api_key=1234',
method: 'POST',
headers: {
host: 'localhost',
'Content-Type': 'application/json'
}
},
{
status: 200
},
function(res, err) {
next(err, res);
}
);
},
function checkLayergroup(err, response) {
if (err) {
throw err;
}
var parsedBody = JSON.parse(response.body);
assert.ok(parsedBody.layergroupid);
assert.ok(parsedBody.last_updated);
assert.equal(parsedBody.metadata.layers[0].type, 'mapnik');
assert.equal(parsedBody.metadata.layers[1].type, 'mapnik');
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
return parsedBody.layergroupid;
},
function requestTile(err, layergroupId) {
if (err) {
throw err;
}
var next = this;
assert.response(server,
{
url: '/api/v1/map/' + layergroupId + '/all/0/0/0.png',
method: 'GET',
headers: {
host: 'localhost'
},
encoding: 'binary'
},
{
status: 200,
headers: {
'content-type': 'image/png'
}
},
function(res, err) {
next(err, res);
}
);
},
function handleTileResponse(err, res) {
if (err) {
throw err;
}
test_helper.checkCache(res);
return true;
},
function deleteTemplate(err) {
var next = this;
templateMaps.delTemplate(username, namedTilesTemplateName, function(/*delErr*/) {
// ignore deletion error
next(err);
});
});
});
},
function finish(err) {
done(err);
}
);
});
});
@@ -0,0 +1,269 @@
var test_helper = require('../support/test_helper');
var RedisPool = require('redis-mpool');
var querystring = require('querystring');
var assert = require('../support/assert');
var mapnik = require('windshaft').mapnik;
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
var NamedMapsCacheEntry = require('../../lib/cartodb/cache/model/named_maps_entry');
describe('named maps authentication', function() {
// configure redis pool instance to use in tests
var redisPool = new RedisPool(global.environment.redis);
var templateMaps = new TemplateMaps(redisPool, {
max_user_templates: global.environment.maxUserTemplates
});
var wadusLayer = {
type: 'cartodb',
options: {
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
cartocss: '#layer { marker-fill: <%= color %>; }',
cartocss_version: '2.3.0'
}
};
var username = 'localhost';
var templateName = 'valid_template';
var template = {
version: '0.0.1',
name: templateName,
auth: {
method: 'open'
},
"placeholders": {
"color": {
"type": "css_color",
"default": "#cc3300"
}
},
layergroup: {
layers: [
wadusLayer
]
}
};
var tokenAuthTemplateName = 'auth_valid_template';
var tokenAuthTemplate = {
version: '0.0.1',
name: tokenAuthTemplateName,
auth: {
method: 'token',
valid_tokens: ['valid1', 'valid2']
},
placeholders: {
color: {
"type": "css_color",
"default": "#cc3300"
}
},
layergroup: {
layers: [
wadusLayer
]
}
};
var namedMapLayer = {
type: 'named',
options: {
name: templateName,
config: {},
auth_tokens: []
}
};
var nestedNamedMapTemplateName = 'nested_template';
var nestedNamedMapTemplate = {
version: '0.0.1',
name: nestedNamedMapTemplateName,
auth: {
method: 'open'
},
layergroup: {
layers: [
namedMapLayer
]
}
};
beforeEach(function (done) {
templateMaps.addTemplate(username, nestedNamedMapTemplate, function (err) {
if (err) {
return done(err);
}
templateMaps.addTemplate(username, tokenAuthTemplate, function (err) {
if (err) {
return done(err);
}
templateMaps.addTemplate(username, template, function (err) {
return done(err);
});
});
});
});
afterEach(function (done) {
templateMaps.delTemplate(username, nestedNamedMapTemplateName, function (err) {
if (err) {
return done(err);
}
templateMaps.delTemplate(username, tokenAuthTemplateName, function (err) {
if (err) {
return done(err);
}
templateMaps.delTemplate(username, templateName, function (err) {
return done(err);
});
});
});
});
function getNamedTile(name, z, x, y, options, callback) {
var url = '/api/v1/map/named/' + name + '/all/' + [z,x,y].join('/') + '.png';
if (options.params) {
url = url + '?' + querystring.stringify(options.params);
}
var requestOptions = {
url: url,
method: 'GET',
headers: {
host: username
},
encoding: 'binary'
};
var statusCode = options.status || 200;
var expectedResponse = {
status: statusCode,
headers: {
'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8'
}
};
assert.response(server,
requestOptions,
expectedResponse,
function (res, err) {
var img;
if (!err && res.headers['content-type'] === 'image/png') {
img = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
}
return callback(err, res, img);
}
);
}
describe('tiles', function() {
it('should return a 404 error for nonexistent template name', function (done) {
var nonexistentName = 'nonexistent';
getNamedTile(nonexistentName, 0, 0, 0, { status: 404 }, function(err, res) {
assert.ok(!err);
assert.deepEqual(
JSON.parse(res.body),
{ errors: ["Template '" + nonexistentName + "' of user '" + username + "' not found"] }
);
done();
});
});
it('should return 403 if not properly authorized', function(done) {
getNamedTile(tokenAuthTemplateName, 0, 0, 0, { status: 403 }, function(err, res) {
assert.ok(!err);
assert.deepEqual(JSON.parse(res.body), { errors: ['Unauthorized template instantiation'] });
done();
});
});
it('should return 200 if properly authorized', function(done) {
getNamedTile(tokenAuthTemplateName, 0, 0, 0, { params: { auth_token: 'valid1' } }, function(err, res, img) {
assert.equal(img.width(), 256);
assert.equal(img.height(), 256);
assert.ok(!err);
test_helper.checkSurrogateKey(res, new NamedMapsCacheEntry(username, tokenAuthTemplateName).key());
done();
});
});
});
function getStaticMap(name, options, callback) {
var url = '/api/v1/map/static/named/' + name + '/640/480.png';
if (options.params) {
url = url + '?' + querystring.stringify(options.params);
}
var requestOptions = {
url: url,
method: 'GET',
headers: {
host: username
},
encoding: 'binary'
};
var statusCode = options.status || 200;
var expectedResponse = {
status: statusCode,
headers: {
'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8'
}
};
assert.response(server,
requestOptions,
expectedResponse,
function (res, err) {
var img;
if (!err && res.headers['content-type'] === 'image/png') {
img = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
}
return callback(err, res, img);
}
);
}
describe('static maps', function() {
it('should return a 404 error for nonexistent template name', function (done) {
var nonexistentName = 'nonexistent';
getStaticMap(nonexistentName, { status: 404 }, function(err, res) {
assert.ok(!err);
assert.deepEqual(
JSON.parse(res.body),
{ errors: ["Template '" + nonexistentName + "' of user '" + username + "' not found"] }
);
done();
});
});
it('should return 403 if not properly authorized', function(done) {
getStaticMap(tokenAuthTemplateName, { status: 403 }, function(err, res) {
assert.ok(!err);
assert.deepEqual(JSON.parse(res.body), { errors: ['Unauthorized template instantiation'] });
done();
});
});
it('should return 200 if properly authorized', function(done) {
getStaticMap(tokenAuthTemplateName, { params: { auth_token: 'valid1' } }, function(err, res, img) {
assert.ok(!err);
assert.equal(img.width(), 640);
assert.equal(img.height(), 480);
test_helper.checkSurrogateKey(res, new NamedMapsCacheEntry(username, tokenAuthTemplateName).key());
test_helper.deleteRedisKeys({'user:localhost:mapviews:global': 5}, done);
});
});
});
});
+140
View File
@@ -0,0 +1,140 @@
require('../support/test_helper');
var RedisPool = require('redis-mpool');
var assert = require('../support/assert');
var mapnik = require('windshaft').mapnik;
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
describe('named maps provider cache', function() {
// configure redis pool instance to use in tests
var redisPool = new RedisPool(global.environment.redis);
var templateMaps = new TemplateMaps(redisPool, {
max_user_templates: global.environment.maxUserTemplates
});
var username = 'localhost';
var templateName = 'template_with_color';
var IMAGE_TOLERANCE = 20;
function createTemplate(color) {
return {
version: '0.0.1',
name: templateName,
auth: {
method: 'open'
},
placeholders: {
color: {
type: "css_color",
default: color
}
},
layergroup: {
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced',
cartocss: '#layer { marker-fill: <%= color %>; marker-line-color: <%= color %>; }',
cartocss_version: '2.3.0'
}
}
]
}
};
}
afterEach(function (done) {
templateMaps.delTemplate(username, templateName, done);
});
function getNamedTile(options, callback) {
if (!callback) {
callback = options;
options = {};
}
var url = '/api/v1/map/named/' + templateName + '/all/' + [0,0,0].join('/') + '.png';
var requestOptions = {
url: url,
method: 'GET',
headers: {
host: username
},
encoding: 'binary'
};
var statusCode = options.statusCode || 200;
var expectedResponse = {
status: statusCode,
headers: {
'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8'
}
};
assert.response(server, requestOptions, expectedResponse, function (res, err) {
var img;
if (statusCode === 200) {
img = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
}
return callback(err, res, img);
});
}
function previewFixture(color) {
return './test/fixtures/provider/populated_places_simple_reduced-' + color + '.png';
}
var colors = ['red', 'red', 'green', 'blue'];
colors.forEach(function(color) {
it('should return an image estimating its bounds based on dataset', function (done) {
templateMaps.addTemplate(username, createTemplate(color), function (err) {
if (err) {
return done(err);
}
getNamedTile(function(err, res, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture(color), IMAGE_TOLERANCE, done);
});
});
});
});
it('should fail to use template from named map provider after template deletion', function (done) {
var color = 'black';
templateMaps.addTemplate(username, createTemplate(color), function (err) {
if (err) {
return done(err);
}
getNamedTile(function(err, res, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture(color), IMAGE_TOLERANCE, function(err) {
assert.ok(!err);
templateMaps.delTemplate(username, templateName, function (err) {
assert.ok(!err);
getNamedTile({ statusCode: 404 }, function(err, res) {
assert.ok(!err);
assert.deepEqual(
JSON.parse(res.body),
{ errors: ["Template 'template_with_color' of user 'localhost' not found"] }
);
// add template again so it's clean in afterEach
templateMaps.addTemplate(username, createTemplate(color), done);
});
});
});
});
});
});
});
+166
View File
@@ -0,0 +1,166 @@
var testHelper = require('../support/test_helper');
var RedisPool = require('redis-mpool');
var assert = require('../support/assert');
var mapnik = require('windshaft').mapnik;
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
describe('named maps static view', function() {
// configure redis pool instance to use in tests
var redisPool = new RedisPool(global.environment.redis);
var templateMaps = new TemplateMaps(redisPool, {
max_user_templates: global.environment.maxUserTemplates
});
var username = 'localhost';
var templateName = 'template_with_view';
var IMAGE_TOLERANCE = 20;
function createTemplate(view) {
return {
version: '0.0.1',
name: templateName,
auth: {
method: 'open'
},
placeholders: {
color: {
type: "css_color",
default: "#cc3300"
}
},
view: view,
layergroup: {
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced',
cartocss: '#layer { marker-fill: <%= color %>; }',
cartocss_version: '2.3.0'
}
}
]
}
};
}
afterEach(function (done) {
templateMaps.delTemplate(username, templateName, done);
});
function getStaticMap(callback) {
var url = '/api/v1/map/static/named/' + templateName + '/640/480.png';
var requestOptions = {
url: url,
method: 'GET',
headers: {
host: username
},
encoding: 'binary'
};
var expectedResponse = {
status: 200,
headers: {
'Content-Type': 'image/png'
}
};
// this could be removed once named maps are invalidated, otherwise you hits the cache
var server = new CartodbWindshaft(serverOptions);
assert.response(server, requestOptions, expectedResponse, function (res, err) {
testHelper.deleteRedisKeys({'user:localhost:mapviews:global': 5}, function() {
return callback(err, mapnik.Image.fromBytes(new Buffer(res.body, 'binary')));
});
});
}
function previewFixture(version) {
return './test/fixtures/previews/populated_places_simple_reduced-' + version + '.png';
}
it('should return an image estimating its bounds based on dataset', function (done) {
templateMaps.addTemplate(username, createTemplate(), function (err) {
if (err) {
return done(err);
}
getStaticMap(function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('estimated'), IMAGE_TOLERANCE, done);
});
});
});
it('should return an image using view zoom + center', function (done) {
var view = {
zoom: 4,
center: {
lng: 40,
lat: 20
}
};
templateMaps.addTemplate(username, createTemplate(view), function (err) {
if (err) {
return done(err);
}
getStaticMap(function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), IMAGE_TOLERANCE, done);
});
});
});
it('should return an image using view bounds', function (done) {
var view = {
bounds: {
west: 0,
south: 0,
east: 45,
north: 45
}
};
templateMaps.addTemplate(username, createTemplate(view), function (err) {
if (err) {
return done(err);
}
getStaticMap(function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('bounds'), IMAGE_TOLERANCE, done);
});
});
});
it('should return an image using view zoom + center when bounds are also present', function (done) {
var view = {
bounds: {
west: 0,
south: 0,
east: 45,
north: 45
},
zoom: 4,
center: {
lng: 40,
lat: 20
}
};
templateMaps.addTemplate(username, createTemplate(view), function (err) {
if (err) {
return done(err);
}
getStaticMap(function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), IMAGE_TOLERANCE, done);
});
});
});
});
+110
View File
@@ -0,0 +1,110 @@
var test_helper = require('../support/test_helper');
var RedisPool = require('redis-mpool');
var querystring = require('querystring');
var assert = require('../support/assert');
var mapnik = require('windshaft').mapnik;
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
var NamedMapsCacheEntry = require('../../lib/cartodb/cache/model/named_maps_entry');
describe('named maps preview stats', function() {
var redisPool = new RedisPool(global.environment.redis);
var templateMaps = new TemplateMaps(redisPool, {
max_user_templates: global.environment.maxUserTemplates
});
var username = 'localhost';
var statTag = 'wadus_viz';
var templateName = 'with_stats';
var template = {
version: '0.0.1',
name: templateName,
auth: {
method: 'open'
},
"placeholders": {
"color": {
"type": "css_color",
"default": "#cc3300"
}
},
layergroup: {
stat_tag: statTag,
layers: [
{
type: 'cartodb',
options: {
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
cartocss: '#layer { marker-fill: <%= color %>; }',
cartocss_version: '2.3.0'
}
}
]
}
};
beforeEach(function (done) {
templateMaps.addTemplate(username, template, done);
});
afterEach(function (done) {
templateMaps.delTemplate(username, templateName, done);
});
function getStaticMap(name, options, callback) {
var url = '/api/v1/map/static/named/' + name + '/640/480.png';
if (options.params) {
url = url + '?' + querystring.stringify(options.params);
}
var requestOptions = {
url: url,
method: 'GET',
headers: {
host: username
},
encoding: 'binary'
};
var statusCode = options.status || 200;
var expectedResponse = {
status: statusCode,
headers: {
'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8'
}
};
assert.response(server,
requestOptions,
expectedResponse,
function (res, err) {
var img;
if (!err && res.headers['content-type'] === 'image/png') {
img = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
}
return callback(err, res, img);
}
);
}
it('should return 200 if properly authorized', function(done) {
getStaticMap(templateName, { params: { auth_token: 'valid1' } }, function(err, res, img) {
assert.ok(!err);
assert.equal(img.width(), 640);
assert.equal(img.height(), 480);
test_helper.checkSurrogateKey(res, new NamedMapsCacheEntry(username, templateName).key());
var redisKeysToDelete = { 'user:localhost:mapviews:global': 5 };
redisKeysToDelete['user:localhost:mapviews:stat_tag:' + statTag] = 5;
test_helper.deleteRedisKeys(redisKeysToDelete, done);
});
});
});
-189
View File
@@ -1,189 +0,0 @@
var test_helper = require('../support/test_helper');
var RedisPool = require('redis-mpool');
var querystring = require('querystring');
var assert = require('../support/assert');
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
var server = new CartodbWindshaft(serverOptions);
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
var NamedMapsCacheEntry = require('../../lib/cartodb/cache/model/named_maps_entry');
describe('named static maps', function() {
// configure redis pool instance to use in tests
var redisPool = new RedisPool(global.environment.redis);
var templateMaps = new TemplateMaps(redisPool, {
max_user_templates: global.environment.maxUserTemplates
});
var wadusLayer = {
type: 'cartodb',
options: {
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
cartocss: '#layer { marker-fill: <%= color %>; }',
cartocss_version: '2.3.0'
}
};
var username = 'localhost';
var templateName = 'valid_template';
var template = {
version: '0.0.1',
name: templateName,
auth: {
method: 'open'
},
"placeholders": {
"color": {
"type": "css_color",
"default": "#cc3300"
}
},
layergroup: {
layers: [
wadusLayer
]
}
};
var tokenAuthTemplateName = 'auth_valid_template';
var tokenAuthTemplate = {
version: '0.0.1',
name: tokenAuthTemplateName,
auth: {
method: 'token',
valid_tokens: ['valid1', 'valid2']
},
placeholders: {
color: {
"type": "css_color",
"default": "#cc3300"
}
},
layergroup: {
layers: [
wadusLayer
]
}
};
var namedMapLayer = {
type: 'named',
options: {
name: templateName,
config: {},
auth_tokens: []
}
};
var nestedNamedMapTemplateName = 'nested_template';
var nestedNamedMapTemplate = {
version: '0.0.1',
name: nestedNamedMapTemplateName,
auth: {
method: 'open'
},
layergroup: {
layers: [
namedMapLayer
]
}
};
before(function (done) {
templateMaps.addTemplate(username, nestedNamedMapTemplate, function (err) {
if (err) {
return done(err);
}
templateMaps.addTemplate(username, tokenAuthTemplate, function (err) {
if (err) {
return done(err);
}
templateMaps.addTemplate(username, template, function (err) {
return done(err);
});
});
});
});
after(function (done) {
templateMaps.delTemplate(username, nestedNamedMapTemplateName, function (err) {
if (err) {
return done(err);
}
templateMaps.delTemplate(username, tokenAuthTemplateName, function (err) {
if (err) {
return done(err);
}
templateMaps.delTemplate(username, templateName, function (err) {
return done(err);
});
});
});
});
function getStaticMap(name, options, callback) {
var url = '/api/v1/map/static/named/' + name + '/640/480.png';
if (options.params) {
url = url + '?' + querystring.stringify(options.params);
}
var requestOptions = {
url: url,
method: 'GET',
headers: {
host: username
}
};
console.log(url);
var statusCode = options.status || 200;
var expectedResponse = {
status: statusCode,
headers: {
'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8'
}
};
assert.response(server,
requestOptions,
expectedResponse,
function (res, err) {
return callback(err, res);
}
);
}
it('should return a 404 error for nonexistent template name', function (done) {
var nonexistentName = 'nonexistent';
getStaticMap(nonexistentName, { status: 404 }, function(err, res) {
assert.ok(!err);
var parsed = JSON.parse(res.body);
assert.equal(parsed.error, "Template '" + nonexistentName + "' of user '" + username + "' not found");
done();
});
});
it('should return 403 if not properly authorized', function(done) {
getStaticMap(tokenAuthTemplateName, { status: 403 }, function(err, res) {
assert.ok(!err);
var parsed = JSON.parse(res.body);
assert.equal(parsed.error, 'Unauthorized template instantiation');
done();
});
});
it('should return 200 if properly authorized', function(done) {
getStaticMap(tokenAuthTemplateName, { params: { auth_token: 'valid1' } }, function(err, res) {
assert.ok(!err);
test_helper.checkSurrogateKey(res, new NamedMapsCacheEntry(username, tokenAuthTemplateName).key());
done();
});
});
});
+303
View File
@@ -0,0 +1,303 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var step = require('step');
var cartodbServer = require('../../../lib/cartodb/server');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
describe('attributes', function() {
var server = cartodbServer(PortedServerOptions);
server.setMaxListeners(0);
var test_mapconfig_1 = {
version: '1.1.0',
layers: [
{ type: 'mapnik', options: {
sql: "select 1 as id, 'SRID=4326;POINT(0 0)'::geometry as the_geom",
cartocss: '#style { }',
cartocss_version: '2.0.1'
} },
{ type: 'mapnik', options: {
sql: "select 1 as i, 6 as n, 'SRID=4326;POINT(0 0)'::geometry as the_geom",
attributes: { id:'i', columns: ['n'] },
cartocss: '#style { }',
cartocss_version: '2.0.1'
} }
]
};
function checkCORSHeaders(res) {
assert.equal(
res.headers['access-control-allow-headers'],
'X-Requested-With, X-Prototype-Version, X-CSRF-Token'
);
assert.equal(res.headers['access-control-allow-origin'], '*');
}
var keysToDelete;
beforeEach(function() {
keysToDelete = {};
});
afterEach(function(done) {
testHelper.deleteRedisKeys(keysToDelete, done);
});
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
it("can only be fetched from layer having an attributes spec", function(done) {
var expected_token;
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(test_mapconfig_1)
}, {}, function(res, err) { next(err, res); });
},
function checkPost(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
// CORS headers should be sent with response
// from layergroup creation via POST
checkCORSHeaders(res);
var parsedBody = JSON.parse(res.body);
if ( expected_token ) {
assert.deepEqual(parsedBody, {layergroupid: expected_token, layercount: 2});
} else {
expected_token = parsedBody.layergroupid;
}
return null;
},
function do_get_attr_0(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/attributes/1',
method: 'GET'
}, {}, function(res, err) { next(err, res); });
},
function check_error_0(err, res) {
assert.ifError(err);
assert.equal(
res.statusCode,
400,
res.statusCode + ( res.statusCode !== 200 ? (': ' + res.body) : '' )
);
var parsed = JSON.parse(res.body);
assert.equal(parsed.errors[0], "Layer 0 has no exposed attributes");
return null;
},
function do_get_attr_1(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/1/attributes/1',
method: 'GET'
}, {}, function(res, err) { next(err, res); });
},
function check_attr_1(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.deepEqual(parsed, {"n":6});
return null;
},
function do_get_attr_1_404(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/1/attributes/-666',
method: 'GET'
}, {}, function(res, err) { next(err, res); });
},
function check_attr_1_404(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 404, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors);
var msg = parsed.errors[0];
assert.ok(msg.match(/0 features.*identified by fid -666/), msg);
return null;
},
function finish(err) {
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done(err);
}
);
});
// See https://github.com/CartoDB/Windshaft/issues/131
it("are checked at map creation time", function(done) {
// clone the mapconfig test
var mapconfig = JSON.parse(JSON.stringify(test_mapconfig_1));
// append unexistant attribute name
mapconfig.layers[1].options.sql = 'SELECT * FROM test_table';
mapconfig.layers[1].options.attributes.id = 'unexistant';
mapconfig.layers[1].options.attributes.columns = ['cartodb_id'];
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(mapconfig)
}, {}, function(res, err) { next(err, res); });
},
function checkPost(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 404, res.statusCode + ': ' + (res.statusCode===200?'...':res.body));
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors);
assert.equal(parsed.errors.length, 1);
var msg = parsed.errors[0];
assert.equal(msg, 'column "unexistant" does not exist');
return null;
},
function finish(err) {
done(err);
}
);
});
it("can be used with jsonp", function(done) {
var expected_token;
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(test_mapconfig_1)
}, {}, function(res, err) { next(err, res); });
},
function checkPost(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
// CORS headers should be sent with response
// from layergroup creation via POST
checkCORSHeaders(res);
var parsedBody = JSON.parse(res.body);
if ( expected_token ) {
assert.deepEqual(parsedBody, {layergroupid: expected_token, layercount: 2});
} else {
expected_token = parsedBody.layergroupid;
}
return null;
},
function do_get_attr_0(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token +
'/0/attributes/1?callback=test',
method: 'GET'
}, {}, function(res, err) { next(err, res); });
},
function check_error_0(err, res) {
assert.ifError(err);
// jsonp errors should be returned with HTTP status 200
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
assert.equal(
res.body,
'/**/ typeof test === \'function\' && test({"errors":["Layer 0 has no exposed attributes"]});'
);
return null;
},
function do_get_attr_1(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/1/attributes/1',
method: 'GET'
}, {}, function(res, err) { next(err, res); });
},
function check_attr_1(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.deepEqual(parsed, {"n":6});
return null;
},
function finish(err) {
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done(err);
}
);
});
// Test that you cannot write to the database from an attributes tile request
//
// Test for http://github.com/CartoDB/Windshaft/issues/130
//
it("database access is read-only", function(done) {
// clone the mapconfig test
var mapconfig = JSON.parse(JSON.stringify(test_mapconfig_1));
mapconfig.layers[1].options.sql +=
", test_table_inserter(st_setsrid(st_point(0,0),4326),'write') as w";
mapconfig.layers[1].options.attributes.columns.push('w');
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(mapconfig)
}, {}, function(res, err) { next(err, res); });
},
function checkPost(err, res) {
assert.ifError(err);
// TODO: should be 403 Forbidden
assert.equal(res.statusCode, 400, res.statusCode + ': ' + (res.statusCode===200?'...':res.body));
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors);
assert.equal(parsed.errors.length, 1);
var msg = parsed.errors[0];
assert.equal(msg, "cannot execute INSERT in a read-only transaction");
return null;
},
function finish(err) {
done(err);
}
);
});
});
+105
View File
@@ -0,0 +1,105 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('blend png renderer', function() {
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
var IMAGE_TOLERANCE_PER_MIL = 20;
function plainTorqueMapConfig(plainColor) {
return {
version: '1.2.0',
layers: [
{
type: 'plain',
options: {
color: plainColor
}
},
{
type: 'torque',
options: {
sql: "SELECT * FROM populated_places_simple_reduced " +
"where the_geom && ST_MakeEnvelope(-90, 0, 90, 65)",
cartocss: [
'Map {',
' buffer-size:0;',
' -torque-frame-count:1;',
' -torque-animation-duration:30;',
' -torque-time-attribute:"cartodb_id";',
' -torque-aggregation-function:"count(cartodb_id)";',
' -torque-resolution:1;',
' -torque-data-aggregation:linear;',
'}',
'#populated_places_simple_reduced{',
' comp-op: multiply;',
' marker-fill-opacity: 1;',
' marker-line-color: #FFF;',
' marker-line-width: 0;',
' marker-line-opacity: 1;',
' marker-type: rectangle;',
' marker-width: 3;',
' marker-fill: #FFCC00;',
'}'
].join(' '),
cartocss_version: '2.3.0'
}
}
]
};
}
var testScenarios = [
{
tile: {
z: 2,
x: 2,
y: 1,
layer: 'all',
format: 'png'
},
plainColor: 'white'
},
{
tile: {
z: 2,
x: 1,
y: 1,
layer: 'all',
format: 'png'
},
plainColor: '#339900'
}
];
function blendPngFixture(zxy) {
return './test/fixtures/blend/blend-plain-torque-' + zxy.join('.') + '.png';
}
testScenarios.forEach(function(testScenario) {
var tileRequest = testScenario.tile;
var zxy = [tileRequest.z, tileRequest.x, tileRequest.y];
it('tile all/' + zxy.join('/') + '.png', function (done) {
testClient.getTileLayer(plainTorqueMapConfig(testScenario.plainColor), tileRequest, function(err, res) {
assert.imageEqualsFile(res.body, blendPngFixture(zxy), IMAGE_TOLERANCE_PER_MIL, function(err) {
assert.ok(!err);
done();
});
});
});
});
});
+166
View File
@@ -0,0 +1,166 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var fs = require('fs');
var http = require('http');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('blend layer filtering', function() {
var IMG_TOLERANCE_PER_MIL = 20;
var httpRendererResourcesServer;
var req2paramsFn;
before(function(done) {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
// Start a server to test external resources
httpRendererResourcesServer = http.createServer( function(request, response) {
var filename = __dirname + '/../../fixtures/http/light_nolabels-1-0-0.png';
fs.readFile(filename, {encoding: 'binary'}, function(err, file) {
response.writeHead(200);
response.write(file, "binary");
response.end();
});
});
httpRendererResourcesServer.listen(8033, done);
});
after(function(done) {
BaseController.prototype.req2params = req2paramsFn;
httpRendererResourcesServer.close(done);
});
var mapConfig = {
version: '1.2.0',
layers: [
{
type: 'plain',
options: {
color: '#fabada'
}
},
{
type: 'http',
options: {
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
subdomains: ['abcd']
}
},
{
type: 'mapnik',
options: {
sql: 'SELECT * FROM populated_places_simple_reduced',
cartocss: '#layer { marker-fill:red; } #layer { marker-width: 2; }',
cartocss_version: '2.3.0',
geom_column: 'the_geom'
}
},
{
type: 'torque',
options: {
sql: "SELECT * FROM populated_places_simple_reduced",
cartocss: [
'Map {',
' buffer-size:0;',
' -torque-frame-count:1;',
' -torque-animation-duration:30;',
' -torque-time-attribute:"cartodb_id";',
' -torque-aggregation-function:"count(cartodb_id)";',
' -torque-resolution:1;',
' -torque-data-aggregation:linear;',
'}',
'#populated_places_simple_reduced{',
' comp-op: multiply;',
' marker-fill-opacity: 1;',
' marker-line-color: #FFF;',
' marker-line-width: 0;',
' marker-line-opacity: 1;',
' marker-type: rectangle;',
' marker-width: 3;',
' marker-fill: #FFCC00;',
'}'
].join(' '),
cartocss_version: '2.3.0'
}
},
{
type: 'http',
options: {
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
subdomains: ['abcd']
}
},
{
type: 'torque',
options: {
sql: "SELECT * FROM populated_places_simple_reduced " +
"where the_geom && ST_MakeEnvelope(-90, 0, 90, 65)",
cartocss: [
'Map {',
' buffer-size:0;',
' -torque-frame-count:1;',
' -torque-animation-duration:30;',
' -torque-time-attribute:"cartodb_id";',
' -torque-aggregation-function:"count(cartodb_id)";',
' -torque-resolution:1;',
' -torque-data-aggregation:linear;',
'}',
'#populated_places_simple_reduced{',
' comp-op: multiply;',
' marker-fill-opacity: 1;',
' marker-line-color: #FFF;',
' marker-line-width: 0;',
' marker-line-opacity: 1;',
' marker-type: rectangle;',
' marker-width: 3;',
' marker-fill: #FFCC00;',
'}'
].join(' '),
cartocss_version: '2.3.0'
}
}
]
};
var filteredLayersSuite = [
[2, 2],
[0, 1],
[0, 2],
[1, 2],
[2, 1], // ordering doesn't matter
[0, 3],
[1, 3],
[1, 2, 5],
[1, 2, 3, 4]
];
function blendPngFixture(layers) {
return './test/fixtures/blend/blend-filtering-layers-' + layers.join('.') + '-zxy-1.0.0.png';
}
filteredLayersSuite.forEach(function(filteredLayers) {
var layerFilter = filteredLayers.join(',');
var tileRequest = {
z: 1,
x: 0,
y: 0,
layer: layerFilter,
format: 'png'
};
it('should filter on ' + layerFilter + '/1/0/0.png', function (done) {
testClient.getTileLayer(mapConfig, tileRequest, function(err, res) {
assert.imageEqualsFile(res.body, blendPngFixture(filteredLayers), IMG_TOLERANCE_PER_MIL, function(err) {
assert.ok(!err);
done();
});
});
});
});
});
@@ -0,0 +1,147 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var fs = require('fs');
var http = require('http');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('blend http fallback', function() {
var IMG_TOLERANCE_PER_MIL = 20;
var httpRendererResourcesServer;
var req2paramsFn;
before(function(done) {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
// Start a server to test external resources
httpRendererResourcesServer = http.createServer( function(request, response) {
if (request.url.match(/^\/error404\//)) {
response.writeHead(404);
response.end();
} else {
var filename = __dirname + '/../../fixtures/http/light_nolabels-1-0-0.png';
if (request.url.match(/^\/dark\//)) {
filename = __dirname + '/../../fixtures/http/dark_nolabels-1-0-0.png';
}
fs.readFile(filename, {encoding: 'binary'}, function(err, file) {
response.writeHead(200);
response.write(file, "binary");
response.end();
});
}
});
httpRendererResourcesServer.listen(8033, done);
});
after(function(done) {
BaseController.prototype.req2params = req2paramsFn;
httpRendererResourcesServer.close(done);
});
var mapConfig = {
version: '1.2.0',
layers: [
{
type: 'plain', // <- 0
options: {
color: '#fabada'
}
},
{
type: 'http', // <- 1
options: {
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
subdomains: ['light']
}
},
{
type: 'http', // <- 2
options: {
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
subdomains: ['dark']
}
},
{
type: 'http', // <- 3
options: {
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
subdomains: ['error404']
}
},
{
type: 'mapnik', // <- 4
options: {
sql: 'SELECT * FROM populated_places_simple_reduced',
cartocss: '#layer { marker-fill:red; } #layer { marker-width: 2; }',
cartocss_version: '2.3.0',
geom_column: 'the_geom'
}
}
]
};
var filteredLayersSuite = [
['all'], // layers displayed: 2 + 4, skipping 3 as it fails
[0, 4],
[0, 3], // skips layer 3 as it fails
[1, 2],
[1, 3],
[2, 3],
[3, 4]
];
function blendPngFixture(layers) {
return './test/fixtures/blend/http_fallback/blend-layers-' + layers.join('.') + '-zxy-1.0.0.png';
}
filteredLayersSuite.forEach(function(filteredLayers) {
var layerFilter = filteredLayers.join(',');
var tileRequest = {
z: 1,
x: 0,
y: 0,
layer: layerFilter,
format: 'png'
};
it('should fallback on http error while blending layers ' + layerFilter + '/1/0/0.png', function (done) {
testClient.getTileLayer(mapConfig, tileRequest, function(err, res) {
assert.imageEqualsFile(res.body, blendPngFixture(filteredLayers), IMG_TOLERANCE_PER_MIL, function(err) {
assert.ok(!err, err);
done();
});
});
});
});
it('should keep failing when http layer is requested individually', function(done) {
var tileRequest = {
z: 1,
x: 0,
y: 0,
layer: 3,
format: 'png'
};
var expectedResponse = {
status: 400,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
testClient.getTileLayer(mapConfig, tileRequest, expectedResponse, function(err, res) {
assert.ok(!err);
var parsedBody = JSON.parse(res.body);
assert.deepEqual(parsedBody, {
errors: [
"Unable to fetch http tile: http://127.0.0.1:8033/error404/1/0/0.png [404]"
]
});
done();
});
});
});
@@ -0,0 +1,95 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var serverOptions = require('./support/ported_server_options');
var fs = require('fs');
var http = require('http');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe.skip('blend http client timeout', function() {
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
var mapConfig = {
version: '1.3.0',
layers: [
{
type: 'http',
options: {
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
subdomains: ['light']
}
},
{
type: 'mapnik',
options: {
sql: 'SELECT * FROM populated_places_simple_reduced',
cartocss: '#layer { marker-fill:red; } #layer { marker-width: 2; }',
cartocss_version: '2.3.0',
geom_column: 'the_geom'
}
}
]
};
var oldHttpRendererTimeout;
var httpRendererTimeout = 100;
var slowHttpRendererResourcesServer;
before(function(done) {
oldHttpRendererTimeout = serverOptions.renderer.http.timeout;
serverOptions.renderer.http.timeout = httpRendererTimeout;
// Start a server to test external resources
slowHttpRendererResourcesServer = http.createServer( function(request, response) {
setTimeout(function() {
var filename = __dirname + '/../fixtures/http/light_nolabels-1-0-0.png';
fs.readFile(filename, {encoding: 'binary'}, function(err, file) {
response.writeHead(200);
response.write(file, "binary");
response.end();
});
}, httpRendererTimeout * 2);
});
slowHttpRendererResourcesServer.listen(8033, done);
});
after(function(done) {
serverOptions.renderer.http.timeout = oldHttpRendererTimeout;
slowHttpRendererResourcesServer.close(done);
});
it('should fail to render when http layer times out', function(done) {
var options = {
statusCode: 400,
contentType: 'application/json; charset=utf-8',
serverOptions: serverOptions
};
testClient.withLayergroup(mapConfig, options, function(err, requestTile, finish) {
var tileUrl = '/all/0/0/0.png';
requestTile(tileUrl, options, function(err, res) {
assert.ok(!err);
var parsedBody = JSON.parse(res.body);
assert.ok(parsedBody.errors);
assert.ok(parsedBody.errors.length);
assert.equal(parsedBody.errors[0], 'Unable to fetch http tile: http://127.0.0.1:8033/light/0/0/0.png');
finish(function(finishErr) {
done(err || finishErr);
});
});
});
});
});
@@ -0,0 +1,129 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var fs = require('fs');
var PortedServerOptions = require('./support/ported_server_options');
var http = require('http');
var testClient = require('./support/test_client');
var nock = require('nock');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('external resources', function() {
var res_serv; // resources server
var res_serv_status = { numrequests:0 }; // status of resources server
var res_serv_port = 8033; // FIXME: make configurable ?
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 25;
var req2paramsFn;
before(function(done) {
nock.enableNetConnect('127.0.0.1');
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
// Start a server to test external resources
res_serv = http.createServer( function(request, response) {
++res_serv_status.numrequests;
var filename = __dirname + '/../../fixtures/markers' + request.url;
fs.readFile(filename, "binary", function(err, file) {
if ( err ) {
response.writeHead(404, {'Content-Type': 'text/plain'});
response.write("404 Not Found\n");
} else {
response.writeHead(200);
response.write(file, "binary");
}
response.end();
});
});
res_serv.listen(res_serv_port, done);
});
after(function(done) {
BaseController.prototype.req2params = req2paramsFn;
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
// Close the resources server
res_serv.close(done);
});
function imageCompareFn(fixture, done) {
return function(err, res) {
if (err) {
return done(err);
}
assert.imageEqualsFile(res.body, './test/fixtures/' + fixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, done);
};
}
it("basic external resource", function(done) {
var circleStyle = "#test_table_3 { marker-file: url('http://127.0.0.1:" + res_serv_port +
"/circle.svg'); marker-transform:'scale(0.2)'; }";
testClient.getTile(testClient.defaultTableMapConfig('test_table_3', circleStyle), 13, 4011, 3088,
imageCompareFn('test_table_13_4011_3088_svg1.png', done));
});
it("different external resource", function(done) {
var squareStyle = "#test_table_3 { marker-file: url('http://127.0.0.1:" + res_serv_port +
"/square.svg'); marker-transform:'scale(0.2)'; }";
testClient.getTile(testClient.defaultTableMapConfig('test_table_3', squareStyle), 13, 4011, 3088,
imageCompareFn('test_table_13_4011_3088_svg2.png', done));
});
// See http://github.com/CartoDB/Windshaft/issues/107
it("external resources get localized on renderer creation if not locally cached", function(done) {
var options = {
serverOptions: PortedServerOptions
};
var externalResourceStyle = "#test_table_3{marker-file: url('http://127.0.0.1:" + res_serv_port +
"/square.svg'); marker-transform:'scale(0.2)'; }";
var externalResourceMapConfig = testClient.defaultTableMapConfig('test_table_3', externalResourceStyle);
testClient.createLayergroup(externalResourceMapConfig, options, function() {
var externalResourceRequestsCount = res_serv_status.numrequests;
testClient.createLayergroup(externalResourceMapConfig, options, function() {
assert.equal(res_serv_status.numrequests, externalResourceRequestsCount);
// reset resources cache
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
externalResourceMapConfig = testClient.defaultTableMapConfig('test_table_3 ', externalResourceStyle);
testClient.createLayergroup(externalResourceMapConfig, options, function() {
assert.equal(res_serv_status.numrequests, externalResourceRequestsCount + 1);
done();
});
});
});
});
it("referencing unexistant external resources returns an error", function(done) {
var url = "http://127.0.0.1:" + res_serv_port + "/notfound.png";
var style = "#test_table_3{marker-file: url('" + url + "'); marker-transform:'scale(0.2)'; }";
var mapConfig = testClient.defaultTableMapConfig('test_table_3', style);
testClient.createLayergroup(mapConfig, { statusCode: 400 }, function(err, res) {
assert.deepEqual(JSON.parse(res.body), {
errors: ["Unable to download '" + url + "' for 'style0' (server returned 404)"]
});
done();
});
});
});
@@ -0,0 +1 @@
{"grid":[" "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," !!! "," !!!!!! "," !!!!!!! "," !!!!!!!! "," !!!!!!!!! "," !!!!!!!! "," !!!!!!! "," !!!!!! "," !!! "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "],"keys":["","2"],"data":{"2":{"cartodb_id":2}}}
@@ -0,0 +1 @@
{"grid":[" "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," !! "," !!! "," !! "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "],"keys":["","2"],"data":{"2":{"cartodb_id":4}}}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

+96
View File
@@ -0,0 +1,96 @@
require('../../support/test_helper');
var fs = require('fs');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var serverOptions = require('./support/ported_server_options');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe.skip('render limits', function() {
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 25;
var limitsConfig;
var onTileErrorStrategy;
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
limitsConfig = serverOptions.renderer.mapnik.limits;
serverOptions.renderer.mapnik.limits = {
render: 50,
cacheOnTimeout: false
};
onTileErrorStrategy = serverOptions.renderer.onTileErrorStrategy;
serverOptions.renderer.onTileErrorStrategy = function(err, tile, headers, stats, format, callback) {
callback(err, tile, headers, stats);
};
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
serverOptions.renderer.mapnik.limits = limitsConfig;
serverOptions.renderer.onTileErrorStrategy = onTileErrorStrategy;
});
var slowQuery = 'select pg_sleep(1), * from test_table limit 2';
var slowQueryMapConfig = testClient.singleLayerMapConfig(slowQuery);
it('slow query/render returns with 400 status', function(done) {
var options = {
statusCode: 400,
serverOptions: serverOptions
};
testClient.createLayergroup(slowQueryMapConfig, options, function(err, res) {
assert.deepEqual(JSON.parse(res.body), { errors: ["Render timed out"] });
done();
});
});
it('uses onTileErrorStrategy to handle error and modify response', function(done) {
serverOptions.renderer.onTileErrorStrategy = function(err, tile, headers, stats, format, callback) {
var fixture = __dirname + '/../../fixtures/limits/fallback.png';
fs.readFile(fixture, {encoding: 'binary'}, function(err, img) {
callback(null, img, {'Content-Type': 'image/png'}, {});
});
};
var options = {
statusCode: 200,
contentType: 'image/png',
serverOptions: serverOptions
};
testClient.createLayergroup(slowQueryMapConfig, options, function(err, res) {
var parsed = JSON.parse(res.body);
assert.ok(parsed.layergroupid);
done();
});
});
it('returns a fallback tile that was modified via onTileErrorStrategy', function(done) {
var fixtureImage = './test/fixtures/limits/fallback.png';
serverOptions.renderer.onTileErrorStrategy = function(err, tile, headers, stats, format, callback) {
fs.readFile(fixtureImage, {encoding: null}, function(err, img) {
callback(null, img, {'Content-Type': 'image/png'}, {});
});
};
var options = {
statusCode: 200,
contentType: 'image/png',
serverOptions: serverOptions
};
testClient.withLayergroup(slowQueryMapConfig, options, function(err, requestTile, finish) {
var tileUrl = '/0/0/0.png';
requestTile(tileUrl, options, function(err, res) {
assert.imageEqualsFile(res.body, fixtureImage, IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
finish(function(finishErr) {
done(err || finishErr);
});
});
});
});
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,465 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var step = require('step');
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var testClient = require('./support/test_client');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('multilayer error cases', function() {
var server = cartodbServer(ServerOptions);
server.setMaxListeners(0);
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
it("post layergroup with wrong Content-Type", function(done) {
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded' }
}, {}, function(res) {
assert.equal(res.statusCode, 400, res.body);
var parsedBody = JSON.parse(res.body);
assert.deepEqual(parsedBody, {"errors":["layergroup POST data must be of type application/json"]});
done();
});
});
it("post layergroup with no layers", function(done) {
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' }
}, {}, function(res) {
assert.equal(res.statusCode, 400, res.body);
var parsedBody = JSON.parse(res.body);
assert.deepEqual(parsedBody, {"errors":["Missing layers array from layergroup config"]});
done();
});
});
it("post layergroup jsonp errors are returned with 200 status", function(done) {
assert.response(server, {
url: '/database/windshaft_test/layergroup?callback=test',
method: 'POST',
headers: {'Content-Type': 'application/json' }
}, {}, function(res) {
assert.equal(res.statusCode, 200);
assert.equal(
res.body,
'/**/ typeof test === \'function\' && test({"errors":["Missing layers array from layergroup config"]});'
);
done();
});
});
// See https://github.com/CartoDB/Windshaft/issues/154
it("mapnik tokens cannot be used with attributes service", function(done) {
var layergroup = {
version: '1.1.0',
layers: [
{ options: {
sql: 'select cartodb_id, 1 as n, the_geom, !bbox! as b from test_table limit 1',
cartocss: '#layer { marker-fill:red }',
cartocss_version: '2.0.1',
attributes: { id:'cartodb_id', columns:['n'] },
geom_column: 'the_geom'
} }
]
};
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json; charset=utf-8' },
data: JSON.stringify(layergroup)
}, {}, function(res, err) { next(err, res); });
},
function do_check(err, res) {
assert.equal(res.statusCode, 400, res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors);
assert.equal(parsed.errors.length, 1);
var msg = parsed.errors[0];
assert.ok(msg.match(/Attribute service cannot be activated/), msg);
return null;
},
function finish(err) {
done(err);
}
);
});
it("layergroup with no cartocss_version", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
{ options: {
sql: 'select cartodb_id, ST_Translate(the_geom, 50, 0) as the_geom from test_table limit 2',
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
geom_column: 'the_geom'
} }
]
};
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) {
assert.equal(res.statusCode, 400, res.body);
var parsedBody = JSON.parse(res.body);
assert.deepEqual(parsedBody, {errors:["Missing cartocss_version for layer 0 options"]});
done();
});
});
it("sql/cartocss combination errors", function(done) {
var layergroup = {
version: '1.0.1',
layers: [{ options: {
sql: "select 1 as i, 'LINESTRING(0 0, 1 0)'::geometry as the_geom",
cartocss_version: '2.0.2',
cartocss: '#layer [missing=1] { line-width:16; }',
geom_column: 'the_geom'
}}]
};
ServerOptions.afterLayergroupCreateCalls = 0;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) {
try {
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
// See http://github.com/CartoDB/Windshaft/issues/159
assert.equal(ServerOptions.afterLayergroupCreateCalls, 0);
var parsed = JSON.parse(res.body);
assert.ok(parsed);
assert.equal(parsed.errors.length, 1);
var error = parsed.errors[0];
assert.ok(error.match(/column "missing" does not exist/m), error);
// cannot check for error starting with style0 until a new enough mapnik
// is used: https://github.com/mapnik/mapnik/issues/1924
//assert.ok(error.match(/^style0/), "Error doesn't start with style0: " + error);
// TODO: check which layer introduced the problem ?
done();
} catch (err) { done(err); }
});
});
it("sql/interactivity combination error", function(done) {
var layergroup = {
version: '1.0.1',
layers: [
{ options: {
sql: "select 1 as i, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom",
cartocss_version: '2.0.2',
cartocss: '#layer { line-width:16; }',
interactivity: 'i',
geom_column: 'the_geom'
}},
{ options: {
sql: "select 1 as i, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom",
cartocss_version: '2.0.2',
cartocss: '#layer { line-width:16; }',
geom_column: 'the_geom'
}},
{ options: {
sql: "select 1 as i, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom",
cartocss_version: '2.0.2',
cartocss: '#layer { line-width:16; }',
interactivity: 'missing',
geom_column: 'the_geom'
}}
]
};
ServerOptions.afterLayergroupCreateCalls = 0;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) {
try {
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
// See http://github.com/CartoDB/Windshaft/issues/159
assert.equal(ServerOptions.afterLayergroupCreateCalls, 0);
var parsed = JSON.parse(res.body);
assert.ok(parsed);
assert.equal(parsed.errors.length, 1);
var error = parsed.errors[0];
assert.ok(error.match(/column "missing" does not exist/m), error);
// TODO: check which layer introduced the problem ?
done();
} catch (err) { done(err); }
});
});
it("blank CartoCSS error", function(done) {
var layergroup = {
version: '1.0.1',
layers: [
{ options: {
sql: "select 1 as i, 'LINESTRING(0 0, 1 0)'::geometry as the_geom",
cartocss_version: '2.0.2',
cartocss: '#style { line-width:16 }',
interactivity: 'i',
geom_column: 'the_geom'
}},
{ options: {
sql: "select 1 as i, 'LINESTRING(0 0, 1 0)'::geometry as the_geom",
cartocss_version: '2.0.2',
cartocss: '',
interactivity: 'i',
geom_column: 'the_geom'
}}
]
};
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) {
try {
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed);
assert.equal(parsed.errors.length, 1);
var error = parsed.errors[0];
assert.ok(error.match(/^style1: CartoCSS is empty/), error);
done();
} catch (err) { done(err); }
});
});
it("Invalid mapnik-geometry-type CartoCSS error", function(done) {
var layergroup = {
version: '1.0.1',
layers: [
{ options: {
sql: "select 1 as i, 'LINESTRING(0 0, 1 0)'::geometry as the_geom",
cartocss_version: '2.0.2',
cartocss: '#style [mapnik-geometry-type=bogus] { line-width:16 }',
geom_column: 'the_geom'
}},
{ options: {
sql: "select 1 as i, 'LINESTRING(0 0, 1 0)'::geometry as the_geom",
cartocss_version: '2.0.2',
cartocss: '#style [mapnik-geometry-type=bogus] { line-width:16 }',
geom_column: 'the_geom'
}}
]
};
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) {
try {
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed);
assert.equal(parsed.errors.length, 1);
var error = parsed.errors[0];
// carto-0.9.3 used to say "Failed to parse expression",
// carto-0.9.5 says "not a valid keyword"
assert.ok(error.match(/^style0:.*(Failed|not a valid)/), error);
// TODO: check which layer introduced the problem ?
done();
} catch (err) { done(err); }
});
});
it("post'ing style with non existent column in filter returns 400 with error", function(done) {
var layergroup = {
version: '1.0.1',
layers: [
{ options: {
sql: 'select * from test_table limit 1',
cartocss: '#test_table::outline[address="one"], [address="two"] { marker-fill: red; }',
cartocss_version: '2.0.2',
interactivity: [ 'cartodb_id' ],
geom_column: 'the_geom'
} },
{ options: {
sql: 'select * from test_big_poly limit 1',
cartocss: '#test_big_poly { marker-fill:blue }',
cartocss_version: '2.0.2',
interactivity: [ 'cartodb_id' ],
geom_column: 'the_geom'
} }
]
};
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) {
assert.equal(res.statusCode, 400, res.body);
var parsed = JSON.parse(res.body);
assert.equal(parsed.errors.length, 1);
var error = parsed.errors[0];
assert.ok(error.match(/column "address" does not exist/m), error);
done();
});
});
// See https://github.com/Vizzuality/Windshaft/issues/31
it('bogus sql raises 400 status code', function(done) {
var bogusSqlMapConfig = testClient.singleLayerMapConfig('BOGUS FROM test_table');
testClient.createLayergroup(bogusSqlMapConfig, { statusCode: 400 }, function(err, res) {
assert.ok(/syntax error/.test(res.body), "Unexpected error: " + res.body);
done();
});
});
it('bogus sql raises 200 status code for jsonp', function(done) {
var bogusSqlMapConfig = testClient.singleLayerMapConfig('bogus');
var options = {
method: 'GET',
callbackName: 'test',
headers: {
'Content-Type': 'text/javascript; charset=utf-8'
}
};
testClient.createLayergroup(bogusSqlMapConfig, options, function(err, res) {
assert.ok(
/^\/\*\*\/ typeof test === 'function' && test\(/.test(res.body),
"Body start expected callback name: " + res.body
);
assert.ok(/syntax error/.test(res.body), "Unexpected error: " + res.body);
done();
});
});
it('query not selecting the_geom raises 200 status code for jsonp instead of 404', function(done) {
var noGeomMapConfig = testClient.singleLayerMapConfig('select null::geometry the_geom_wadus');
var options = {
method: 'GET',
callbackName: 'test',
headers: {
'Content-Type': 'text/javascript; charset=utf-8'
}
};
testClient.createLayergroup(noGeomMapConfig, options, function(err, res) {
assert.ok(
/^\/\*\*\/ typeof test === 'function' && test\(/.test(res.body),
"Body start expected callback name: " + res.body
);
assert.ok(/column.*does not exist/.test(res.body), "Unexpected error: " + res.body);
done();
});
});
it("query with no geometry field returns 400 status", function(done){
var noGeometrySqlMapConfig = testClient.singleLayerMapConfig('SELECT 1');
testClient.createLayergroup(noGeometrySqlMapConfig, { statusCode: 400 }, function(err, res) {
assert.ok(/column.*does not exist/.test(res.body), "Unexpected error: " + res.body);
done();
});
});
it("bogus style should raise 400 status", function(done){
var bogusStyleMapConfig = testClient.defaultTableMapConfig('test_table', '#test_table{xxxxx;}');
testClient.createLayergroup(bogusStyleMapConfig, { method: 'GET', statusCode: 400 }, done);
});
var defaultErrorExpectedResponse = {
status: 400,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
it('should raise 400 error for out of bounds layer index', function(done){
var mapConfig = testClient.singleLayerMapConfig('select * from test_table', null, null, 'name');
testClient.getGrid(mapConfig, 1, 13, 4011, 3088, defaultErrorExpectedResponse, function(err, res) {
assert.deepEqual(JSON.parse(res.body), { errors: ["Layer '1' not found in layergroup"] });
done();
});
});
////////////////////////////////////////////////////////////////////
//
// OPTIONS LAYERGROUP
//
////////////////////////////////////////////////////////////////////
it("nonexistent layergroup token error", function(done) {
step(
function do_get_tile(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/deadbeef/0/0/0/0.grid.json',
method: 'GET',
encoding: 'binary'
}, {}, function(res, err) { next(err, res); });
},
function checkResponse(err, res) {
assert.ifError(err);
// FIXME: should be 404
assert.equal(res.statusCode, 400, res.statusCode + ':' + res.body);
var parsed = JSON.parse(res.body);
assert.deepEqual(parsed, {"errors": ["Invalid or nonexistent map configuration token 'deadbeef'"]});
return null;
},
function finish(err) {
done(err);
}
);
});
it('error 400 on json syntax error', function(done) {
var layergroup = {
version: '1.0.1',
layers: [
{
options: {
sql: 'select the_geom from test_table limit 1',
cartocss: '#layer { marker-fill:red }'
}
}
]
};
assert.response(server,
{
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json; charset=utf-8' },
data: '{' + JSON.stringify(layergroup)
},
{
status: 400
},
function(res) {
var parsedBody = JSON.parse(res.body);
assert.deepEqual(parsedBody, { errors: ['SyntaxError: Unexpected token {'] });
done();
}
);
});
});
@@ -0,0 +1,377 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var _ = require('underscore');
var cartodbServer = require('../../../lib/cartodb/server');
var getLayerTypeFn = require('windshaft').model.MapConfig.prototype.getType;
var PortedServerOptions = require('./support/ported_server_options');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('multilayer interactivity and layers order', function() {
var server = cartodbServer(PortedServerOptions);
server.setMaxListeners(0);
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
function layerType(layer) {
return layer.type || 'undefined';
}
function testInteractivityLayersOrderScenario(testScenario) {
it(testScenario.desc, function(done) {
var layergroup = {
version: '1.3.0',
layers: testScenario.layers
};
PortedServerOptions.afterLayergroupCreateCalls = 0;
assert.response(server,
{
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(layergroup)
},
{
//status: 200, don't use status here to have a more meaningful error message
headers: {
'content-type': 'application/json; charset=utf-8'
}
},
function(response) {
assert.equal(
response.statusCode,
200,
'Expected status code 200, got ' + response.statusCode +
'\n\tResponse body: ' + response.body +
'\n\tLayer types: ' + layergroup.layers.map(layerType).join(', ')
);
// assert.equal(PortedServerOptions.afterLayergroupCreateCalls, 1);
var layergroupResponse = JSON.parse(response.body);
assert.ok(layergroupResponse);
var layergroupId = layergroupResponse.layergroupid;
assert.ok(layergroupId);
assert.equal(layergroupResponse.metadata.layers.length, layergroup.layers.length);
// check layers metadata at least match in number
var layersMetadata = layergroupResponse.metadata.layers;
assert.equal(layersMetadata.length, layergroup.layers.length);
for (var i = 0, len = layersMetadata.length; i < len; i++) {
assert.equal(
getLayerTypeFn(layersMetadata[i].type),
getLayerTypeFn(layergroup.layers[i].type)
);
}
// check torque metadata at least match in number
var torqueLayers = layergroup.layers.filter(function(layer) { return layer.type === 'torque'; });
if (torqueLayers.length) {
assert.equal(Object.keys(layergroupResponse.metadata.torque).length, torqueLayers.length);
}
var keysToDelete = {
'user:localhost:mapviews:global': 5
};
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
testHelper.deleteRedisKeys(keysToDelete, done);
}
);
});
}
var cartocssVersion = '2.3.0';
var cartocss = '#layer { line-width:16; }';
var sql = "select 1 as i, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom";
var sqlWadus = "select 1 as wadus, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom";
var httpLayer = {
type: 'http',
options: {
urlTemplate: 'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png',
subdomains: ['a','b','c']
}
};
var torqueLayer = {
type: 'torque',
options: {
sql: "select 1 id, '1970-01-02'::date d, 'POINT(0 0)'::geometry the_geom_webmercator",
cartocss: [
"Map {",
"-torque-frame-count:2;",
"-torque-resolution:3;",
"-torque-time-attribute:d;",
"-torque-aggregation-function:'count(id)';",
"}"
].join(' '),
cartocss_version: '2.0.1'
}
};
var mapnikLayer = {
type: 'mapnik',
options: {
sql: sql,
cartocss_version: cartocssVersion,
cartocss: cartocss,
geom_column: 'the_geom'
}
};
var mapnikInteractivityLayer = {
type: 'mapnik',
options: {
sql: sql,
cartocss_version: cartocssVersion,
cartocss: cartocss,
interactivity: 'i',
geom_column: 'the_geom'
}
};
var cartodbLayer = {
type: 'cartodb',
options: {
sql: sql,
cartocss_version: cartocssVersion,
cartocss: cartocss,
geom_column: 'the_geom'
}
};
var cartodbInteractivityLayer = {
type: 'cartodb',
options: {
sql: sql,
cartocss_version: cartocssVersion,
cartocss: cartocss,
interactivity: 'i',
geom_column: 'the_geom'
}
};
var cartodbWadusInteractivityLayer = {
type: 'cartodb',
options: {
sql: sqlWadus,
cartocss_version: cartocssVersion,
cartocss: cartocss,
interactivity: 'wadus',
geom_column: 'the_geom'
}
};
var noTypeLayer = {
options: {
sql: sql,
cartocss_version: cartocssVersion,
cartocss: cartocss,
geom_column: 'the_geom'
}
};
var noTypeInteractivityLayer = {
options: {
sql: sql,
cartocss_version: cartocssVersion,
cartocss: cartocss,
interactivity: 'i',
geom_column: 'the_geom'
}
};
var allLayers = [
httpLayer,
torqueLayer,
mapnikLayer,
mapnikInteractivityLayer,
cartodbLayer,
cartodbInteractivityLayer,
cartodbWadusInteractivityLayer,
noTypeLayer,
noTypeInteractivityLayer
];
var testScenarios = [
{
desc: 'one layer, no interactivity',
layers: [cartodbLayer]
},
{
desc: 'one layer with interactivity',
layers: [cartodbInteractivityLayer]
},
{
desc: 'two layers, interactivity mix',
layers: [
cartodbLayer,
cartodbInteractivityLayer
]
},
{
desc: 'two layers, different interactivity columns',
layers: [
cartodbWadusInteractivityLayer,
cartodbInteractivityLayer
]
},
{
desc: 'mix of no interactivity with interactivity',
layers: [
cartodbInteractivityLayer,
cartodbLayer,
cartodbWadusInteractivityLayer
]
},
{
desc: 'interactivity layers + torque',
layers: [
cartodbWadusInteractivityLayer,
cartodbInteractivityLayer,
torqueLayer
]
},
{
desc: 'torque + interactivity layers',
layers: [
torqueLayer,
cartodbWadusInteractivityLayer,
cartodbInteractivityLayer
]
},
{
desc: 'interactivity + torque + interactivity',
layers: [
cartodbInteractivityLayer,
torqueLayer,
cartodbInteractivityLayer
]
},
{
desc: 'http + mix of [no] interactivity layers',
layers: [
httpLayer,
cartodbInteractivityLayer,
cartodbLayer,
cartodbInteractivityLayer
]
},
{
desc: 'http + interactivity + torque + interactivity',
layers: [
httpLayer,
cartodbInteractivityLayer,
torqueLayer,
cartodbInteractivityLayer
]
},
{
desc: 'http + interactivity + torque + no interactivity + torque + interactivity',
layers: [
httpLayer,
cartodbInteractivityLayer,
torqueLayer,
cartodbLayer,
torqueLayer,
cartodbWadusInteractivityLayer
]
},
{
desc: 'mapnik type – two layers, interactivity mix',
layers: [
mapnikLayer,
mapnikInteractivityLayer
]
},
{
desc: 'mapnik type – mix of no interactivity with interactivity',
layers: [
cartodbInteractivityLayer,
cartodbLayer,
mapnikInteractivityLayer
]
},
{
desc: 'mapnik type – interactivity layers + torque',
layers: [
mapnikInteractivityLayer,
cartodbInteractivityLayer,
torqueLayer
]
},
{
desc: 'mapnik type – http + interactivity + torque + interactivity',
layers: [
httpLayer,
mapnikInteractivityLayer,
torqueLayer,
cartodbInteractivityLayer
]
},
{
desc: 'no type – two layers, interactivity mix',
layers: [
noTypeLayer,
noTypeInteractivityLayer
]
},
{
desc: 'no type – mix of no interactivity with interactivity',
layers: [
noTypeInteractivityLayer,
noTypeLayer,
mapnikInteractivityLayer
]
},
{
desc: 'no type – interactivity layers + torque',
layers: [
noTypeLayer,
noTypeInteractivityLayer,
torqueLayer
]
},
{
desc: 'no type – http + interactivity + torque + interactivity',
layers: [
httpLayer,
noTypeInteractivityLayer,
torqueLayer,
noTypeInteractivityLayer
]
}
];
var chaosScenariosSize = 25;
var chaosScenarios = [];
for (var i = 0; i < chaosScenariosSize; i++) {
// Underscore.js' sample method uses Fisher-Yates shuffle internally, see http://bost.ocks.org/mike/shuffle/
var randomLayers = _.sample(allLayers, _.random(1, allLayers.length));
chaosScenarios.push({
desc: 'chaos scenario – layer types: ' + randomLayers.map(layerType).join(', '),
layers: randomLayers
});
}
testScenarios.forEach(testInteractivityLayersOrderScenario);
chaosScenarios.forEach(testInteractivityLayersOrderScenario);
});
+158
View File
@@ -0,0 +1,158 @@
var testHelper =require('../../support/test_helper');
var assert = require('../../support/assert');
var step = require('step');
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
describe('raster', function() {
var server = cartodbServer(ServerOptions);
server.setMaxListeners(0);
function checkCORSHeaders(res) {
assert.equal(res.headers['access-control-allow-headers'], 'X-Requested-With, X-Prototype-Version, X-CSRF-Token');
assert.equal(res.headers['access-control-allow-origin'], '*');
}
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 2;
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
it("can render raster for valid mapconfig", function(done) {
var mapconfig = {
version: '1.2.0',
layers: [
{ type: 'mapnik', options: {
sql: "select ST_AsRaster(" +
" ST_MakeEnvelope(-100,-40, 100, 40, 4326), " +
" 1.0, -1.0, '8BUI', 127) as rst",
geom_column: 'rst',
geom_type: 'raster',
cartocss: '#layer { raster-opacity:1.0 }',
cartocss_version: '2.0.1'
} }
]
};
var expected_token;
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(mapconfig)
}, {}, function(res, err) { next(err, res); });
},
function checkPost(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
// CORS headers should be sent with response
// from layergroup creation via POST
checkCORSHeaders(res);
var parsedBody = JSON.parse(res.body);
if ( expected_token ) {
assert.deepEqual(parsedBody, {layergroupid: expected_token, layercount: 2});
} else {
expected_token = parsedBody.layergroupid;
}
return null;
},
function do_get_tile(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/0/0.png',
method: 'GET',
encoding: 'binary'
}, {}, function(res, err) { next(err, res); });
},
function check_response(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.body);
assert.deepEqual(res.headers['content-type'], "image/png");
var next = this;
assert.imageEqualsFile(res.body,
'./test/fixtures/raster_gray_rect.png',
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
try {
assert.ifError(err);
next();
} catch (err) { next(err); }
});
},
function finish(err) {
if (err) {
return done(err);
}
var keysToDelete = {
'user:localhost:mapviews:global': 5
};
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
testHelper.deleteRedisKeys(keysToDelete, done);
}
);
});
it("raster geom type does not allow interactivity", function(done) {
var mapconfig = {
version: '1.2.0',
layers: [
{
type: 'cartodb',
options: {
sql: [
"select 1 id,",
"ST_AsRaster(ST_MakeEnvelope(-100, -40, 100, 40, 4326), 1.0, -1.0, '8BUI', 127) as rst"
].join(' '),
geom_column: 'rst',
geom_type: 'raster',
cartocss: '#layer { raster-opacity: 1.0 }',
cartocss_version: '2.0.1',
interactivity: 'id'
}
}
]
};
assert.response(server,
{
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(mapconfig)
},
{
status: 400
},
function(res, err) {
assert.ok(!err);
checkCORSHeaders(res);
var parsedBody = JSON.parse(res.body);
assert.deepEqual(parsedBody, { errors: [ 'Mapnik raster layers do not support interactivity' ] });
done();
}
);
});
});
+64
View File
@@ -0,0 +1,64 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var ServerOptions = require('./support/ported_server_options');
var testClient = require('./support/test_client');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('regressions', function() {
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
});
// See https://github.com/Vizzuality/Windshaft/issues/65
it("#65 catching non-Error exception doesn't kill the backend", function(done) {
var mapConfig = testClient.defaultTableMapConfig('test_table');
testClient.withLayergroup(mapConfig, function(err, requestTile, finish) {
var options = {
statusCode: 400,
contentType: 'application/json; charset=utf-8'
};
requestTile('/0/0/0.png?testUnexpectedError=1', options, function(err, res) {
assert.deepEqual(JSON.parse(res.body), { "errors": ["test unexpected error"] });
finish(done);
});
});
});
// Test that you cannot write to the database from a tile request
//
// See http://github.com/CartoDB/Windshaft/issues/130
// [x] Needs a fix on the mapnik side: https://github.com/mapnik/mapnik/pull/2143
//
it("#130 database access is read-only", function(done) {
var writeSqlMapConfig = testClient.singleLayerMapConfig(
'select st_point(0,0) as the_geom, * from test_table_inserter(st_setsrid(st_point(0,0),4326),\'write\')'
);
var expectedResponse = {
status: 400,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
testClient.getTile(writeSqlMapConfig, 0, 0, 0, expectedResponse, function(err, res) {
var parsedBody = JSON.parse(res.body);
assert.ok(parsedBody.errors);
assert.equal(parsedBody.errors.length, 1);
assert.ok(parsedBody.errors[0].match(/read-only transaction/), 'read-only error message expected');
done();
});
});
});
+138
View File
@@ -0,0 +1,138 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var mapnik = require('windshaft').mapnik;
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
describe('retina support', function() {
var layergroupId = null;
var server = cartodbServer(ServerOptions);
server.setMaxListeners(0);
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
var keysToDelete;
beforeEach(function(done) {
keysToDelete = {'user:localhost:mapviews:global': 5};
var retinaSampleMapConfig = {
version: '1.2.0',
layers: [
{
options: {
sql: 'SELECT * FROM populated_places_simple_reduced',
cartocss: '#layer { marker-fill:red; } #layer { marker-width: 2; }',
cartocss_version: '2.3.0',
geom_column: 'the_geom'
}
}
]
};
assert.response(server,
{
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(retinaSampleMapConfig)
},
{
},
function (res, err) {
assert.ok(!err, 'Failed to create layergroup');
layergroupId = JSON.parse(res.body).layergroupid;
done();
}
);
});
afterEach(function(done) {
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
testHelper.deleteRedisKeys(keysToDelete, done);
});
function testRetinaImage(scaleFactor, responseHead, assertFn) {
assert.response(server,
{
url: '/database/windshaft_test/layergroup/' + layergroupId + '/0/0/0' + scaleFactor + '.png',
method: 'GET',
encoding: 'binary'
},
responseHead,
assertFn
);
}
function testValidImageDimmensions(scaleFactor, imageSize, done) {
testRetinaImage(scaleFactor,
{
status: 200,
headers: {
'Content-Type': 'image/png'
}
},
function(res, err) {
assert.ok(!err, 'Failed to request 0/0/0' + scaleFactor + '.png tile');
var image = new mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
assert.equal(image.width(), imageSize);
assert.equal(image.height(), imageSize);
done();
}
);
}
it('image dimensions when scale factor is not defined', function(done) {
testValidImageDimmensions('', 256, done);
});
it('image dimensions when scale factor = @1x', function(done) {
testValidImageDimmensions('@1x', 256, done);
});
it('image dimensions when scale factor = @2x', function(done) {
testValidImageDimmensions('@2x', 512, done);
});
it('error when scale factor is not enabled', function(done) {
var scaleFactor = '@4x';
testRetinaImage(scaleFactor,
{
status: 404,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
},
function(res, err) {
assert.ok(!err, 'Failed to request 0/0/0' + scaleFactor + '.png tile');
assert.deepEqual(JSON.parse(res.body), { errors: ["Tile with specified resolution not found"] } );
done();
}
);
});
});
+170
View File
@@ -0,0 +1,170 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var testClient = require('./support/test_client');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('server', function() {
var server = cartodbServer(ServerOptions);
server.setMaxListeners(0);
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
});
////////////////////////////////////////////////////////////////////
//
// GET INVALID
//
////////////////////////////////////////////////////////////////////
it("get call to server returns 200", function(done){
assert.response(server, {
url: '/',
method: 'GET'
},{
// FIXME: shouldn't this be a 404 ?
status: 200
}, function() { done(); } );
});
////////////////////////////////////////////////////////////////////
//
// GET VERSION
//
////////////////////////////////////////////////////////////////////
it("get /version returns versions", function(done){
assert.response(server, {
url: '/version',
method: 'GET'
},{
status: 200
}, function(res) {
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('windshaft'), "No 'windshaft' version in " + parsed);
assert.ok(parsed.hasOwnProperty('grainstore'), "No 'grainstore' version in " + parsed);
assert.ok(parsed.hasOwnProperty('node_mapnik'), "No 'node_mapnik' version in " + parsed);
assert.ok(parsed.hasOwnProperty('mapnik'), "No 'mapnik' version in " + parsed);
// TODO: check actual versions ?
done();
});
});
////////////////////////////////////////////////////////////////////
//
// GET GRID
//
////////////////////////////////////////////////////////////////////
it("grid jsonp", function(done){
var mapConfig = testClient.singleLayerMapConfig('select * from test_table', null, null, 'name');
testClient.getGridJsonp(mapConfig, 0, 13, 4011, 3088, 'jsonp_test', function(err, res) {
assert.equal(res.statusCode, 200, res.body);
assert.deepEqual(res.headers['content-type'], 'text/javascript; charset=utf-8');
var didRunJsonCallback = false;
var response = {};
// jshint ignore:start
function jsonp_test(body) {
response = body;
didRunJsonCallback = true;
}
eval(res.body);
// jshint ignore:end
assert.ok(didRunJsonCallback);
assert.utfgridEqualsFile(response, './test/fixtures/test_table_13_4011_3088.grid.json', 2, done);
});
});
it("get'ing a json with default style and single interactivity should return a grid", function(done){
var mapConfig = testClient.singleLayerMapConfig('select * from test_table', null, null, 'name');
testClient.getGrid(mapConfig, 0, 13, 4011, 3088, function(err, res) {
var expected_json = {
"1":{"name":"Hawai"},
"2":{"name":"El Estocolmo"},
"3":{"name":"El Rey del Tallarín"},
"4":{"name":"El Lacón"},
"5":{"name":"El Pico"}
};
assert.deepEqual(JSON.parse(res.body).data, expected_json);
done();
});
});
it("get'ing a json with default style and no interactivity should return an error", function(done){
var mapConfig = testClient.singleLayerMapConfig('select * from test_table');
var expectedResponse = {
status: 400,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
testClient.getGrid(mapConfig, 0, 13, 4011, 3088, expectedResponse, function(err, res) {
assert.deepEqual(JSON.parse(res.body), {"errors":["Tileset has no interactivity"]});
done();
});
});
it("get grid jsonp error is returned with 200 status", function(done){
var mapConfig = testClient.singleLayerMapConfig('select * from test_table');
var expectedResponse = {
status: 200,
headers: {
'Content-Type': 'text/javascript; charset=utf-8'
}
};
testClient.getGridJsonp(mapConfig, 0, 13, 4011, 3088, 'test', expectedResponse, function(err, res) {
assert.ok(res.body.match(/"errors":/), 'missing error in response: ' + res.body);
done();
});
});
// See http://github.com/Vizzuality/Windshaft/issues/50
it("get'ing a json with no data should return an empty grid", function(done){
var query = 'select * from test_table limit 0';
var mapConfig = testClient.singleLayerMapConfig(query, null, null, 'name');
testClient.getGrid(mapConfig, 0, 13, 4011, 3088, function(err, res) {
assert.utfgridEqualsFile(res.body, './test/fixtures/test_table_13_4011_3088_empty.grid.json', 2, done);
});
});
// Another test for http://github.com/Vizzuality/Windshaft/issues/50
it("get'ing a json with no data but interactivity should return an empty grid", function(done){
var query = 'SELECT * FROM test_table limit 0';
var mapConfig = testClient.singleLayerMapConfig(query, null, null, 'cartodb_id');
testClient.getGrid(mapConfig, 0, 13, 4011, 3088, function(err, res) {
assert.utfgridEqualsFile(res.body, './test/fixtures/test_table_13_4011_3088_empty.grid.json', 2, done);
});
});
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/67
it("get'ing a solid grid while changing interactivity fields", function(done){
var query = 'SELECT * FROM test_big_poly';
var style211 = "#test_big_poly{polygon-fill:blue;}"; // for solid
var mapConfigName = testClient.singleLayerMapConfig(query, style211, null, 'name');
testClient.getGrid(mapConfigName, 0, 3, 2, 2, function(err, res) {
var expected_data = { "1":{"name":"west"} };
assert.deepEqual(JSON.parse(res.body).data, expected_data);
var mapConfigCartodbId = testClient.singleLayerMapConfig(query, style211, null, 'cartodb_id');
testClient.getGrid(mapConfigCartodbId, 0, 3, 2, 2, function(err, res) {
var expected_data = { "1":{"cartodb_id":"1"} };
assert.deepEqual(JSON.parse(res.body).data, expected_data);
done();
});
});
});
});
+261
View File
@@ -0,0 +1,261 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var mapnik = require('windshaft').mapnik;
var semver = require('semver');
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var testClient = require('./support/test_client');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('server_gettile', function() {
var server = cartodbServer(ServerOptions);
server.setMaxListeners(0);
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 25;
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
});
function imageCompareFn(fixture, done) {
return function(err, res) {
if (err) {
return done(err);
}
assert.imageEqualsFile(res.body, './test/fixtures/' + fixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, done);
};
}
////////////////////////////////////////////////////////////////////
//
// GET TILE
// --{
////////////////////////////////////////////////////////////////////
it("get'ing a tile with default style should return an expected tile", function(done){
testClient.getTile(testClient.defaultTableMapConfig('test_table'), 13, 4011, 3088,
imageCompareFn('test_table_13_4011_3088.png', done)
);
});
it("response of get tile can be served by renderer cache", function(done) {
var tileUrl = '/13/4011/3088.png';
var lastXwc;
var mapConfig = testClient.defaultTableMapConfig('test_table');
testClient.withLayergroup(mapConfig, function (err, requestTile, finish) {
requestTile(tileUrl, function (err, res) {
var xwc = res.headers['x-windshaft-cache'];
assert.ok(xwc);
assert.ok(xwc > 0);
lastXwc = xwc;
requestTile(tileUrl, function (err, res) {
var xwc = res.headers['x-windshaft-cache'];
assert.ok(xwc);
assert.ok(xwc > 0);
assert.ok(xwc >= lastXwc);
requestTile(tileUrl + '?cache_buster=wadus', function (err, res) {
var xwc = res.headers['x-windshaft-cache'];
assert.ok(!xwc);
finish(done);
});
});
});
});
});
it("should not choke when queries end with a semicolon", function(done){
testClient.getTile(testClient.singleLayerMapConfig('SELECT * FROM test_table limit 2;'), 0, 0, 0, done);
});
it("should not choke when sql ends with a semicolon and some blanks", function(done){
testClient.getTile(testClient.singleLayerMapConfig('SELECT * FROM test_table limit 2; \t\n'), 0, 0, 0, done);
});
it("should not strip quoted semicolons within an sql query", function(done){
testClient.getTile(
testClient.singleLayerMapConfig("SELECT * FROM test_table where name != ';\n'"), 0, 0, 0, done
);
});
it("getting two tiles with same configuration uses renderer cache", function(done) {
var imageFixture = './test/fixtures/test_table_13_4011_3088_styled.png';
var tileUrl = '/13/4011/3088.png';
var mapConfig = testClient.defaultTableMapConfig(
'test_table',
'#test_table{marker-fill: blue;marker-line-color: black;}'
);
function validateLayergroup(res) {
// cache is hit because we create a renderer to validate the map config
assert.ok(!res.headers.hasOwnProperty('x-windshaft-cache'), "Did hit renderer cache on first time");
}
testClient.withLayergroup(mapConfig, validateLayergroup, function(err, requestTile, finish) {
requestTile(tileUrl, function(err, res) {
assert.ok(res.headers.hasOwnProperty('x-windshaft-cache'), "Did not hit renderer cache on second time");
assert.ok(res.headers['x-windshaft-cache'] >= 0);
assert.imageEqualsFile(res.body, imageFixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
finish(function(finishErr) {
done(err || finishErr);
});
});
});
});
});
var test_style_black_200 = "#test_table{marker-fill:black;marker-line-color:black;marker-width:5}";
var test_style_black_210 = "#test_table{marker-fill:black;marker-line-color:black;marker-width:10}";
it("get'ing a tile with url specified 2.0.0 style should return an expected tile", function(done){
testClient.getTile(testClient.defaultTableMapConfig('test_table', test_style_black_200, '2.0.0'),
13, 4011, 3088, imageCompareFn('test_table_13_4011_3088_styled_black.png', done));
});
it("get'ing a tile with url specified 2.1.0 style should return an expected tile", function(done){
testClient.getTile(testClient.defaultTableMapConfig('test_table', test_style_black_210, '2.1.0'),
13, 4011, 3088, imageCompareFn('test_table_13_4011_3088_styled_black.png', done));
});
// See http://github.com/CartoDB/Windshaft/issues/99
it("unused directives are tolerated", function(done){
var style = "#test_table{point-transform: 'scale(100)';}";
var sql = "SELECT 1 as cartodb_id, 'SRID=4326;POINT(0 0)'::geometry as the_geom";
testClient.getTile(testClient.singleLayerMapConfig(sql, style), 0, 0, 0,
imageCompareFn('test_default_mapnik_point.png', done));
});
// See http://github.com/CartoDB/Windshaft/issues/100
var test_strictness = function(done) {
var nonStrictMapConfig = testClient.singleLayerMapConfig(
"SELECT 1 as cartodb_id, 'SRID=3857;POINT(666 666)'::geometry as the_geom",
"#test_table{point-transform: 'scale(100)';}"
);
testClient.withLayergroup(nonStrictMapConfig, function(err, requestTile, finish) {
var options = {
statusCode: 400,
contentType: 'application/json; charset=utf-8'
};
requestTile('/0/0/0.png?strict=1', options, function() {
finish(done);
});
});
};
var test_strict_lbl = "unused directives are not tolerated if strict";
if ( semver.satisfies(mapnik.versions.mapnik, '2.3.x') ) {
// Strictness handling changed in 2.3.x, possibly a bug: see http://github.com/mapnik/mapnik/issues/2301
it.skip('[skipped due to http://github.com/mapnik/mapnik/issues/2301]' + test_strict_lbl, test_strictness);
}
else {
it(test_strict_lbl, test_strictness);
}
it('high cpu regression with mapnik <2.3.x', function(done) {
var sql = [
"SELECT 'my polygon name here' AS name,",
"st_envelope(st_buffer(st_transform(",
"st_setsrid(st_makepoint(-26.6592894004,49.7990296995),4326),3857),10000000)) AS the_geom",
"FROM generate_series(-6,6) x",
"UNION ALL",
"SELECT 'my marker name here' AS name,",
" st_transform(st_setsrid(st_makepoint(49.6042060319,-49.0522997372),4326),3857) AS the_geom",
"FROM generate_series(-6,6) x"
].join(' ');
var style = [
'#test_table {marker-fill:#ff7;',
' marker-max-error:0.447492761618;',
' marker-line-opacity:0.659371340628;',
' marker-allow-overlap:true;',
' polygon-fill:green;',
' marker-spacing:0.0;',
' marker-width:4.0;',
' marker-height:18.0;',
' marker-opacity:0.942312062822;',
' line-color:green;',
' line-gamma:0.945973211092;',
' line-cap:square;',
' polygon-opacity:0.12576055992;',
' marker-type:arrow;',
' polygon-gamma:0.46354913107;',
' line-dasharray:33,23;',
' line-join:bevel;',
' marker-placement:line;',
' line-width:1.0;',
' marker-line-color:#ff7;',
' line-opacity:0.39403752154;',
' marker-line-width:3.0;',
'}'
].join('');
testClient.getTile(testClient.singleLayerMapConfig(sql, style), 13, 4011, 3088, done);
});
// https://github.com/CartoDB/Windshaft-cartodb/issues/316
it('should return errors with better formatting', function(done) {
var mapConfig = {
"version": "1.0.1",
"minzoom": 0,
"maxzoom": 20,
"layers": [
{
"type": 'mapnik',
"options": {
"cartocss_version": '2.1.1',
"sql": "SELECT null::geometry AS the_geom",
"cartocss": [
'@water: #cdd2d4;',
'Map {',
'\tbackground-color: @water;',
'\tbufferz-size: 256;',
'}',
'@landmass_fill: lighten(#e3e3dc, 8%);'
].join('\n')
}
},
{
"type": 'mapnik',
"options": {
"cartocss_version": '2.1.1',
"sql": "SELECT the_geom FROM false_background_zoomed('!scale_denominator!', !bbox!) AS _",
"cartocss": [
'#false_background {',
'\tpolygon-fill: @landmass_fill;',
'}'
].join('\n')
}
}
]
};
var options = {
statusCode: 400
};
testClient.createLayergroup(mapConfig, options, function(err, res, parsedBody) {
assert.ok(parsedBody.errors);
// more assertions when errors is populated with better format
done();
});
});
});
@@ -0,0 +1,192 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var _ = require('underscore');
var fs = require('fs');
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 85;
describe('server_png8_format', function() {
var serverOptionsPng32 = ServerOptions;
serverOptionsPng32.grainstore = _.clone(ServerOptions.grainstore);
serverOptionsPng32.grainstore.mapnik_tile_format = 'png32';
var serverPng32 = new cartodbServer(serverOptionsPng32);
serverPng32.setMaxListeners(0);
var serverOptionsPng8 = ServerOptions;
serverOptionsPng8.grainstore = _.clone(ServerOptions.grainstore);
serverOptionsPng8.grainstore.mapnik_tile_format = 'png8:m=h';
var serverPng8 = new cartodbServer(serverOptionsPng8);
serverPng8.setMaxListeners(0);
var layergroupId;
var req2paramsFn;
before(function(done) {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
var testPngFilesDir = __dirname + '/../../results/png';
fs.readdirSync(testPngFilesDir)
.filter(function(fileName) {
return /.*\.png$/.test(fileName);
})
.map(function(fileName) {
return testPngFilesDir + '/' + fileName;
})
.forEach(fs.unlinkSync);
done();
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
var keysToDelete;
beforeEach(function() {
keysToDelete = {
'user:localhost:mapviews:global': 5
};
});
afterEach(function(done) {
testHelper.deleteRedisKeys(keysToDelete, done);
});
function testOutputForPng32AndPng8(desc, tile, callback) {
var bufferPng32,
bufferPng8;
it(desc + '; tile: ' + JSON.stringify(tile), function(done){
assert.response(
serverPng32,
{
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(layergroup)
},
{
status: 200
},
function(res, err) {
if (err) {
return done(err);
}
layergroupId = JSON.parse(res.body).layergroupid;
var tilePartialUrl = _.template('/<%= z %>/<%= x %>/<%= y %>.png', tile);
var requestPayload = {
url: '/database/windshaft_test/layergroup/' + layergroupId + tilePartialUrl,
method: 'GET',
encoding: 'binary'
};
var requestHeaders = {
status: 200,
headers: {
'Content-Type': 'image/png'
}
};
assert.response(serverPng32, requestPayload, requestHeaders, function(responsePng32) {
assert.equal(responsePng32.headers['content-type'], "image/png");
bufferPng32 = responsePng32.body;
assert.response(serverPng8, requestPayload, requestHeaders, function(responsePng8) {
assert.equal(responsePng8.headers['content-type'], "image/png");
bufferPng8 = responsePng8.body;
assert.ok(bufferPng8.length < bufferPng32.length);
assert.imageBuffersAreEqual(bufferPng32, bufferPng8, IMAGE_EQUALS_TOLERANCE_PER_MIL,
function(err, imagePaths, similarity) {
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
callback(err, imagePaths, similarity, done);
}
);
});
});
}
);
});
}
var currentLevel = 3,
allLevelTiles = [],
maxLevelTile = Math.pow(2, currentLevel);
for (var i = 0; i < maxLevelTile; i++) {
for (var j = 0; j < maxLevelTile; j++) {
allLevelTiles.push({
z: currentLevel,
x: i,
y: j
});
}
}
var layergroup = {
version: '1.3.0',
layers: [
{
options: {
sql: 'SELECT * FROM populated_places_simple_reduced',
cartocss: [
'#populated_places_simple_reduced {',
'marker-fill: #FFCC00;',
'marker-width: 10;',
'marker-line-color: #FFF;',
'marker-line-width: 1.5;',
'marker-line-opacity: 1;',
'marker-fill-opacity: 0.9;',
'marker-comp-op: multiply;',
'marker-type: ellipse;',
'marker-placement: point;',
'marker-allow-overlap: true;',
'marker-clip: false;',
'}'
].join(' '),
cartocss_version: '2.0.1'
}
}
]
};
var allImagePaths = [],
similarities = [];
allLevelTiles.forEach(function(tile) {
testOutputForPng32AndPng8('intensity visualization', tile, function(err, imagePaths, similarity, done) {
allImagePaths.push(imagePaths);
similarities.push(similarity);
var transformPaths = [];
for (var i = 0, len = allImagePaths.length; i < len; i++) {
if (similarities[i] > 0.075) {
transformPaths.push({
passive: allImagePaths[i][0],
active: allImagePaths[i][1],
similarity: similarities[i]
});
}
}
var output = 'handleResults(' + JSON.stringify(transformPaths) + ');';
fs.writeFileSync('test/results/png/results.js', output);
assert.ifError(err);
done();
});
});
});
+154
View File
@@ -0,0 +1,154 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var http = require('http');
var fs = require('fs');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('static_maps', function() {
var validUrlTemplate = 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png';
var invalidUrlTemplate = 'http://127.0.0.1:8033/INVALID/{z}/{x}/{y}.png';
var httpRendererResourcesServer;
var req2paramsFn;
before(function(done) {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
// Start a server to test external resources
httpRendererResourcesServer = http.createServer( function(request, response) {
var filename = __dirname + '/../../fixtures/http/basemap.png';
fs.readFile(filename, {encoding: 'binary'}, function(err, file) {
response.writeHead(200);
response.write(file, "binary");
response.end();
});
});
httpRendererResourcesServer.listen(8033, done);
});
after(function(done) {
BaseController.prototype.req2params = req2paramsFn;
httpRendererResourcesServer.close(done);
});
function staticMapConfig(urlTemplate, cartocss) {
return {
version: '1.2.0',
layers: [
{
type: 'http',
options: {
urlTemplate: urlTemplate,
subdomains: ['abcd']
}
},
{
type: 'mapnik',
options: {
sql: 'SELECT * FROM populated_places_simple_reduced',
cartocss: cartocss || '#layer { marker-fill:red; } #layer { marker-width: 2; }',
cartocss_version: '2.3.0'
}
}
]
};
}
var zoom = 3,
lat = 0,
lon = 0,
width = 400,
height = 300;
it('center image', function (done) {
var mapConfig = staticMapConfig(validUrlTemplate);
testClient.getStaticCenter(mapConfig, zoom, lat, lon, width, height, function(err, res, image) {
if (err) {
return done(err);
}
assert.equal(image.width(), width);
assert.equal(image.height(), height);
done();
});
});
it('center image with invalid basemap', function (done) {
var mapConfig = staticMapConfig(invalidUrlTemplate);
testClient.getStaticCenter(mapConfig, zoom, lat, lon, width, height, function(err, res, image) {
if (err) {
return done(err);
}
assert.equal(image.width(), width);
assert.equal(image.height(), height);
done();
});
});
var west = -90,
south = -45,
east = 90,
north = 45,
bbWidth = 640,
bbHeight = 480;
it('bbox', function (done) {
var mapConfig = staticMapConfig(validUrlTemplate);
testClient.getStaticBbox(mapConfig, west, south, east, north, bbWidth, bbHeight, function(err, res, image) {
if (err) {
return done(err);
}
assert.equal(image.width(), bbWidth);
assert.equal(image.height(), bbHeight);
done();
});
});
it('should not fail for coordinates out of range', function (done) {
var outOfRangeHeight = 3000;
var mapConfig = staticMapConfig(validUrlTemplate);
testClient.getStaticCenter(mapConfig, 1, lat, lon, width, outOfRangeHeight, function(err, res, image) {
if (err) {
return done(err);
}
assert.equal(image.width(), width);
assert.equal(image.height(), outOfRangeHeight);
done();
});
});
it('should keep failing for other errors', function (done) {
var invalidStyleForZoom = '#layer { marker-fill:red; } #layer[zoom='+zoom+'] { marker-width: [wadus] * 2; }';
var mapConfig = staticMapConfig(validUrlTemplate, invalidStyleForZoom);
var expectedResponse = {
statusCode: 400,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
testClient.getStaticCenter(mapConfig, zoom, lat, lon, width, height, expectedResponse, function(err, res) {
assert.ok(!err);
var parsedBody = JSON.parse(res.body);
assert.ok(parsedBody.errors);
assert.ok(parsedBody.errors.length);
assert.ok(parsedBody.errors[0].match(/column \"wadus\" does not exist/));
done();
});
});
});
@@ -0,0 +1,84 @@
var _ = require('underscore');
var serverOptions = require('../../../../lib/cartodb/server_options');
var LayergroupToken = require('../../../../lib/cartodb/models/layergroup_token');
var mapnik = require('windshaft').mapnik;
module.exports = _.extend({}, serverOptions, {
base_url: '/database/:dbname/table/:table',
base_url_mapconfig: '/database/:dbname/layergroup',
grainstore: {
datasource: {
geometry_field: 'the_geom',
srid: 4326
},
cachedir: global.environment.millstone.cache_basedir,
mapnik_version: global.environment.mapnik_version || mapnik.versions.mapnik,
gc_prob: 0 // run the garbage collector at each invocation
},
renderer: {
mapnik: {
poolSize: 4,//require('os').cpus().length,
metatile: 1,
bufferSize: 64,
snapToGrid: false,
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
scale_factors: [1, 2],
limits: {
render: 0,
cacheOnTimeout: true
}
},
http: {
timeout: 5000,
whitelist: ['http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png'],
fallbackImage: {
type: 'fs',
src: __dirname + '/../../test/fixtures/http/basemap.png'
}
}
},
redis: global.environment.redis,
enable_cors: global.environment.enable_cors,
unbuffered_logging: true, // for smoother teardown from tests
log_format: null, // do not log anything
afterLayergroupCreateCalls: 0,
useProfiler: true,
req2params: function(req, callback){
if ( req.query.testUnexpectedError ) {
return callback('test unexpected error');
}
// this is in case you want to test sql parameters eg ...png?sql=select * from my_table limit 10
req.params = _.extend({}, req.params);
if (req.params.token) {
req.params.token = LayergroupToken.parse(req.params.token).token;
}
_.extend(req.params, req.query);
req.params.user = 'localhost';
req.context = {user: 'localhost'};
req.params.dbhost = global.environment.postgres.host;
req.params.dbport = req.params.dbport || global.environment.postgres.port;
req.params.dbuser = 'test_windshaft_publicuser';
if (req.params.dbname !== 'windshaft_test2') {
req.params.dbuser = 'test_windshaft_cartodb_user_1';
}
req.params.dbname = 'test_windshaft_cartodb_user_1_db';
// increment number of calls counter
global.req2params_calls = global.req2params_calls ? global.req2params_calls + 1 : 1;
// send the finished req object on
callback(null,req);
},
afterLayergroupCreate: function(req, cfg, res, callback) {
res.layercount = cfg.layers.length;
// config.afterLayergroupCreateCalls++;
callback(null);
}
});
@@ -0,0 +1,488 @@
var testHelper = require('../../../support/test_helper');
var LayergroupToken = require('../../../../lib/cartodb/models/layergroup_token');
var step = require('step');
var assert = require('../../../support/assert');
var _ = require('underscore');
var querystring = require('querystring');
var mapnik = require('windshaft').mapnik;
var CartodbServer = require('../../../../lib/cartodb/server');
var PortedServerOptions = require('./ported_server_options');
var DEFAULT_POINT_STYLE = [
'#layer {',
' marker-fill: #FF6600;',
' marker-opacity: 1;',
' marker-width: 16;',
' marker-line-color: white;',
' marker-line-width: 3;',
' marker-line-opacity: 0.9;',
' marker-placement: point;',
' marker-type: ellipse;',
' marker-allow-overlap: true;',
'}'
].join('');
module.exports = {
createLayergroup: createLayergroup,
withLayergroup: withLayergroup,
singleLayerMapConfig: singleLayerMapConfig,
defaultTableMapConfig: defaultTableMapConfig,
getStaticBbox: getStaticBbox,
getStaticCenter: getStaticCenter,
getGrid: getGrid,
getGridJsonp: getGridJsonp,
getTorque: getTorque,
getTile: getTile,
getTileLayer: getTileLayer
};
var server = new CartodbServer(PortedServerOptions);
server.setMaxListeners(0);
var jsonContentType = 'application/json; charset=utf-8';
var jsContentType = 'text/javascript; charset=utf-8';
var pngContentType = 'image/png';
function createLayergroup(layergroupConfig, options, callback) {
if (!callback) {
callback = options;
options = {
method: 'POST',
statusCode: 200
};
}
var expectedResponse = {
status: options.statusCode || 200,
headers: options.headers || {
'Content-Type': 'application/json; charset=utf-8'
}
};
step(
function requestLayergroup() {
var next = this;
var request = layergroupRequest(layergroupConfig, options.method, options.callbackName, options.params);
assert.response(serverInstance(options), request, expectedResponse, function (res, err) {
next(err, res);
});
},
function validateLayergroup(err, res) {
assert.ok(!err, 'Failed to request layergroup');
var parsedBody;
var layergroupid;
if (options.callbackName) {
global[options.callbackName] = function(layergroup) {
layergroupid = layergroup.layergroupid;
};
// jshint ignore:start
eval(res.body);
// jshint ignore:end
delete global[options.callbackName];
} else {
parsedBody = JSON.parse(res.body);
layergroupid = parsedBody.layergroupid;
if (layergroupid) {
layergroupid = LayergroupToken.parse(layergroupid).token;
}
}
if (layergroupid) {
var keysToDelete = {
'user:localhost:mapviews:global': 5
};
var redisKey = 'map_cfg|' + layergroupid;
keysToDelete[redisKey] = 0;
testHelper.deleteRedisKeys(keysToDelete, function() {
return callback(err, res, parsedBody);
});
} else {
return callback(err, res, parsedBody);
}
}
);
}
function serverInstance(options) {
if (options.server) {
return options.server;
}
if (options.serverOptions) {
var otherServer = new CartodbServer(options.serverOptions);
otherServer.req2params = options.serverOptions.req2params;
otherServer.setMaxListeners(0);
return otherServer;
}
return server;
}
function layergroupRequest(layergroupConfig, method, callbackName, extraParams) {
method = method || 'POST';
var request = {
url: '/database/windshaft_test/layergroup',
headers: {
'Content-Type': 'application/json'
}
};
var urlParams = _.extend({}, extraParams);
if (callbackName) {
urlParams.callback = callbackName;
}
if (method.toUpperCase() === 'GET') {
request.method = 'GET';
urlParams.config = JSON.stringify(layergroupConfig);
} else {
request.method = 'POST';
request.data = JSON.stringify(layergroupConfig);
}
if (Object.keys(urlParams).length) {
request.url += '?' + querystring.stringify(urlParams);
}
return request;
}
function singleLayerMapConfig(sql, cartocss, cartocssVersion, interactivity) {
return {
version: '1.3.0',
layers: [
{
type: 'mapnik',
options: {
sql: sql,
cartocss: cartocss || DEFAULT_POINT_STYLE,
cartocss_version: cartocssVersion || '2.3.0',
interactivity: interactivity,
geom_column: 'the_geom'
}
}
]
};
}
function defaultTableMapConfig(tableName, cartocss, cartocssVersion, interactivity) {
return singleLayerMapConfig(defaultTableQuery(tableName), cartocss, cartocssVersion, interactivity);
}
function defaultTableQuery(tableName) {
return _.template('SELECT * FROM <%= tableName %>', {tableName: tableName});
}
function getStaticBbox(layergroupConfig, west, south, east, north, width, height, expectedResponse, callback) {
if (!callback) {
callback = expectedResponse;
expectedResponse = pngContentType;
}
var url = [
'static',
'bbox',
'<%= layergroupid %>',
[west, south, east, north].join(','),
width,
height
].join('/') + '.png';
return getGeneric(layergroupConfig, url, expectedResponse, callback);
}
function getStaticCenter(layergroupConfig, zoom, lat, lon, width, height, expectedResponse, callback) {
if (!callback) {
callback = expectedResponse;
expectedResponse = pngContentType;
}
var url = [
'static',
'center',
'<%= layergroupid %>',
zoom,
lat,
lon,
width,
height
].join('/') + '.png';
return getGeneric(layergroupConfig, url, expectedResponse, callback);
}
function getGrid(layergroupConfig, layer, z, x, y, expectedResponse, callback) {
if (!callback) {
callback = expectedResponse;
expectedResponse = jsonContentType;
}
var options = {
layer: layer,
z: z,
x: x,
y: y,
format: 'grid.json'
};
return getLayer(layergroupConfig, options, expectedResponse, callback);
}
function getGridJsonp(layergroupConfig, layer, z, x, y, jsonpCallbackName, expectedResponse, callback) {
if (!callback) {
callback = expectedResponse;
expectedResponse = jsContentType;
}
var options = {
layer: layer,
z: z,
x: x,
y: y,
format: 'grid.json',
jsonpCallbackName: jsonpCallbackName
};
return getLayer(layergroupConfig, options, expectedResponse, callback);
}
function getTorque(layergroupConfig, layer, z, x, y, expectedResponse, callback) {
if (!callback) {
callback = expectedResponse;
expectedResponse = jsonContentType;
}
var options = {
layer: layer,
z: z,
x: x,
y: y,
format: 'torque.json'
};
return getLayer(layergroupConfig, options, expectedResponse, callback);
}
function getTile(layergroupConfig, z, x, y, expectedResponse, callback) {
if (!callback) {
callback = expectedResponse;
expectedResponse = pngContentType;
}
var options = {
z: z,
x: x,
y: y,
format: 'png'
};
return getLayer(layergroupConfig, options, expectedResponse, callback);
}
function getTileLayer(layergroupConfig, options, expectedResponse, callback) {
if (!callback) {
callback = expectedResponse;
expectedResponse = pngContentType;
}
return getLayer(layergroupConfig, options, expectedResponse, callback);
}
function getLayer(layergroupConfig, options, expectedResponse, callback) {
return getGeneric(layergroupConfig, tileUrlStrategy(options), expectedResponse, callback);
}
function tileUrlStrategy(options) {
var urlLayerPattern = [
'<%= layer %>',
'<%= z %>',
'<%= x %>',
'<%= y %>'
].join('/') + '.<%= format %>';
if (options.jsonpCallbackName) {
urlLayerPattern += '?callback=<%= jsonpCallbackName %>';
}
var urlNoLayerPattern = [
'<%= z %>',
'<%= x %>',
'<%= y %>'
].join('/') + '.<%= format %>';
var urlTemplate = _.template((options.layer === undefined) ? urlNoLayerPattern : urlLayerPattern);
options.format = options.format || 'png';
return '<%= layergroupid %>/' + urlTemplate(_.defaults(options, { z: 0, x: 0, y: 0, layer: 0 }));
}
function getGeneric(layergroupConfig, url, expectedResponse, callback) {
if (_.isString(expectedResponse)) {
expectedResponse = {
status: 200,
headers: {
'Content-Type': expectedResponse
}
};
}
var contentType = expectedResponse.headers['Content-Type'];
var layergroupid = null;
step(
function requestLayergroup() {
var next = this;
var request = {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(layergroupConfig)
};
var expectedResponse = {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
assert.response(server, request, expectedResponse, function (res, err) {
next(err, res);
});
},
function validateLayergroup(err, res) {
assert.ok(!err, 'Failed to create layergroup');
var parsedBody = JSON.parse(res.body);
layergroupid = parsedBody.layergroupid;
assert.ok(layergroupid);
return res;
},
function requestTile(err, res) {
assert.ok(!err, 'Invalid layergroup response: ' + res.body);
var next = this;
var finalUrl = '/database/windshaft_test/layergroup/' + _.template(url, {
layergroupid: layergroupid
});
var request = {
url: finalUrl,
method: 'GET'
};
if (contentType === pngContentType) {
request.encoding = 'binary';
}
assert.response(server, request, expectedResponse, function (res, err) {
next(err, res);
});
},
function validateTile(err, res) {
assert.ok(!err, 'Failed to get tile');
var img;
if (contentType === pngContentType) {
img = new mapnik.Image.fromBytesSync(new Buffer(res.body, 'binary'));
}
var keysToDelete = {
'user:localhost:mapviews:global': 5
};
var redisKey = 'map_cfg|' + LayergroupToken.parse(layergroupid).token;
keysToDelete[redisKey] = 0;
testHelper.deleteRedisKeys(keysToDelete, function() {
return callback(err, res, img);
});
}
);
}
function withLayergroup(layergroupConfig, options, callback) {
var validationLayergroupFn = function() {};
if (!callback) {
callback = options;
options = {};
}
if (_.isFunction(options)) {
validationLayergroupFn = options;
options = {};
}
var layergroupExpectedResponse = {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
step(
function requestLayergroup() {
var next = this;
var request = layergroupRequest(layergroupConfig, 'POST');
assert.response(serverInstance(options), request, layergroupExpectedResponse, function (res, err) {
next(err, res);
});
},
function validateLayergroup(err, res) {
assert.ok(!err, 'Failed to request layergroup');
var parsedBody = JSON.parse(res.body);
var layergroupid = parsedBody.layergroupid;
assert.ok(layergroupid, 'No layergroup was created');
validationLayergroupFn(res);
function requestTile(layergroupUrl, options, callback) {
if (!callback) {
callback = options;
options = {
statusCode: 200,
contentType: pngContentType
};
}
var baseUrlTpl = '/database/windshaft_test/layergroup/<%= layergroupid %>';
var finalUrl = _.template(baseUrlTpl, { layergroupid: layergroupid }) + layergroupUrl;
var request = {
url: finalUrl,
method: 'GET'
};
if (options.contentType === pngContentType) {
request.encoding = 'binary';
}
var tileExpectedResponse = {
status: options.statusCode || 200,
headers: {
'Content-Type': options.contentType || pngContentType
}
};
assert.response(serverInstance(options), request, tileExpectedResponse, function (res, err) {
callback(err, res);
});
}
function finish(done) {
var keysToDelete = {
'user:localhost:mapviews:global': 5
};
var redisKey = 'map_cfg|' + LayergroupToken.parse(layergroupid).token;
keysToDelete[redisKey] = 0;
testHelper.deleteRedisKeys(keysToDelete, done);
}
return callback(err, requestTile, finish);
}
);
}
+436
View File
@@ -0,0 +1,436 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var _ = require('underscore');
var step = require('step');
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
describe('torque', function() {
var server = cartodbServer(ServerOptions);
server.setMaxListeners(0);
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
var keysToDelete;
beforeEach(function() {
keysToDelete = {};
});
afterEach(function(done) {
testHelper.deleteRedisKeys(keysToDelete, done);
});
function checkCORSHeaders(res) {
assert.equal(res.headers['access-control-allow-headers'], 'X-Requested-With, X-Prototype-Version, X-CSRF-Token');
assert.equal(res.headers['access-control-allow-origin'], '*');
}
it("missing required property from torque layer", function(done) {
var layergroup = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: 'select cartodb_id, the_geom from test_table',
geom_column: 'the_geom',
srid: 4326,
cartocss: 'Map { marker-fill:blue; }'
} }
]
};
step(
function do_post1()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) { next(null, res); });
},
function checkResponse(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, parsed);
var error = parsed.errors[0];
assert.equal(error,
"Missing required property '-torque-frame-count' in torque layer CartoCSS");
return null;
},
function do_post2(err)
{
assert.ifError(err);
var next = this;
var css = 'Map { -torque-frame-count: 2; }';
layergroup.layers[0].options.cartocss = css;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) { next(null, res); });
},
function checkResponse2(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, parsed);
var error = parsed.errors[0];
assert.equal(error,
"Missing required property '-torque-resolution' in torque layer CartoCSS");
return null;
},
function do_post3(err)
{
assert.ifError(err);
var next = this;
var css = 'Map { -torque-frame-count: 2; -torque-resolution: 3; }';
layergroup.layers[0].options.cartocss = css;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) { next(null, res); });
},
function checkResponse3(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, parsed);
var error = parsed.errors[0];
assert.equal(error,
"Missing required property '-torque-aggregation-function' in torque layer CartoCSS");
return null;
},
function finish(err) {
done(err);
}
);
});
// See http://github.com/CartoDB/Windshaft/issues/150
it.skip("unquoted property in torque layer", function(done) {
var layergroup = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: 'select updated_at as d, cartodb_id as id, the_geom from test_table',
geom_column: 'the_geom',
srid: 4326,
cartocss: 'Map { -torque-frame-count:2; -torque-resolution:3; -torque-time-attribute:"d"; ' +
'-torque-aggregation-function:count(id); }'
} }
]
};
step(
function do_post1()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) { next(null, res); });
},
function checkResponse(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, parsed);
var error = parsed.errors[0];
assert.equal(error, "Something meaningful here");
return null;
},
function finish(err) {
done(err);
}
);
});
it("can render tile for valid mapconfig", function(done) {
var mapconfig = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: "select 1 as id, '1970-01-02'::date as d, 'POINT(0 0)'::geometry as the_geom UNION select 2, " +
"'1970-01-01'::date, 'POINT(1 1)'::geometry",
geom_column: 'the_geom',
cartocss: 'Map { -torque-frame-count:2; -torque-resolution:3; -torque-time-attribute:d; ' +
'-torque-aggregation-function:\'count(id)\'; }',
cartocss_version: '2.0.1'
} }
]
};
var expected_token;
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(mapconfig)
}, {}, function(res, err) { next(err, res); });
},
function checkPost(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
// CORS headers should be sent with response
// from layergroup creation via POST
checkCORSHeaders(res);
var parsedBody = JSON.parse(res.body);
if ( expected_token ) {
assert.deepEqual(parsedBody, {layergroupid: expected_token, layercount: 2});
} else {
expected_token = parsedBody.layergroupid;
}
var meta = parsedBody.metadata;
assert.ok(!_.isUndefined(meta),
'No metadata in torque MapConfig creation response: ' + res.body);
var tm = meta.torque;
assert.ok(tm,
'No "torque" in metadata:' + JSON.stringify(meta));
var tm0 = tm[0];
assert.ok(tm0,
'No layer 0 in "torque" in metadata:' + JSON.stringify(tm));
var expectedTorqueMetadata = {"start":0,"end":86400000,"data_steps":2,"column_type":"date"};
assert.deepEqual(tm0, expectedTorqueMetadata);
assert.deepEqual(meta.layers[0].meta, expectedTorqueMetadata);
return null;
},
function do_get_tile(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/0/0.png',
method: 'GET',
encoding: 'binary'
}, {}, function(res, err) { next(err, res); });
},
function check_mapnik_error_1(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ( res.statusCode !== 200 ? (': ' + res.body) : '' ));
var parsed = JSON.parse(res.body);
assert.equal(parsed.errors.length, 1);
assert.equal(parsed.errors[0], "No 'mapnik' layers in MapConfig");
return null;
},
function do_get_grid0(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/0/0/0.grid.json',
method: 'GET'
}, {}, function(res, err) { next(err, res); });
},
function check_mapnik_error_2(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ( res.statusCode !== 200 ? (': ' + res.body) : '' ));
var parsed = JSON.parse(res.body);
assert.equal(parsed.errors.length, 1);
assert.equal(parsed.errors[0], "Unsupported format grid.json");
return null;
},
function do_get_torque0(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/0/0/0.json.torque',
method: 'GET'
}, {}, function(res, err) { next(err, res); });
},
function check_torque0_response(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
var tile_content = [{"x__uint8":43,"y__uint8":43,"vals__uint8":[1,1],"dates__uint16":[0,1]}];
var parsed = JSON.parse(res.body);
assert.deepEqual(tile_content, parsed);
return null;
},
function do_get_torque0_1(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/0/0/0.torque.json',
method: 'GET'
}, {}, function(res, err) { next(err, res); });
},
function check_torque0_response_1(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
var tile_content = [{"x__uint8":43,"y__uint8":43,"vals__uint8":[1,1],"dates__uint16":[0,1]}];
var parsed = JSON.parse(res.body);
assert.deepEqual(tile_content, parsed);
return null;
},
function finish(err) {
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done(err);
}
);
});
// Test that you cannot write to the database from a torque tile request
//
// Test for http://github.com/CartoDB/Windshaft/issues/130
//
it("database access is read-only", function(done) {
var mapconfig = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: "select 'SRID=3857;POINT(0 0)'::geometry as g, now() as d,* from " +
"test_table_inserter(st_setsrid(st_point(0,0),4326),'write')",
geom_column: 'g',
cartocss: 'Map { -torque-frame-count:2; -torque-resolution:3; -torque-time-attribute:d; ' +
'-torque-aggregation-function:\'count(*)\'; }',
cartocss_version: '2.0.1'
} }
]
};
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(mapconfig)
}, {}, function(res, err) { next(err, res); });
},
function checkPost(err, res) {
assert.ifError(err);
// TODO: should be 403 Forbidden
assert.equal(res.statusCode, 400, res.statusCode + ': ' + (res.statusCode===200?'...':res.body));
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors);
assert.equal(parsed.errors.length, 1);
var msg = parsed.errors[0];
assert.equal(msg, "TorqueRenderer: cannot execute INSERT in a read-only transaction");
return null;
},
function finish(err) {
done(err);
}
);
});
// See http://github.com/CartoDB/Windshaft/issues/164
it("gives a 500 on database connection refused", function(done) {
var mapconfig = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: "select 1 as id, '1970-01-03'::date as d, 'POINT(0 0)'::geometry as the_geom UNION select 2, " +
"'1970-01-01'::date, 'POINT(1 1)'::geometry",
geom_column: 'the_geom',
cartocss: 'Map { -torque-frame-count:2; -torque-resolution:3; -torque-time-attribute:d; ' +
'-torque-aggregation-function:\'count(id)\'; }',
cartocss_version: '2.0.1'
} }
]
};
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup?dbport=1234567',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(mapconfig)
}, {}, function(res, err) { next(err, res); });
},
function checkPost(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 500, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, parsed);
var error = parsed.errors[0];
assert.equal(error, "TorqueRenderer: cannot connect to the database");
return null;
},
function finish(err) {
done(err);
}
);
});
it("checks types for torque-specific styles", function(done) {
var wrongStyle = ["Map {",
"-torque-frame-count:512;",
"-torque-animation-duration:30;",
"-torque-time-attribute:'cartodb_id';",
"-torque-aggregation-function:count(cartodb_id);", //unquoted aggregation function
"-torque-resolution:4;",
"-torque-data-aggregation:linear;",
"}"].join(" ");
var layergroup = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: 'select cartodb_id, the_geom from test_table',
geom_column: 'the_geom',
srid: 4326,
cartocss: wrongStyle
} }
]
};
step(
function request(){
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) { next(null, res); });
},
function checkResponse(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, parsed);
var error = parsed.errors[0];
assert.equal(error,
"Unexpected type for property '-torque-aggregation-function', expected string");
done();
return null;
}
);
});
});
+459
View File
@@ -0,0 +1,459 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
describe('torque boundary points', function() {
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
var layergroupIdToDelete = null;
beforeEach(function() {
layergroupIdToDelete = null;
});
afterEach(function(done) {
var keysToDelete = {
'user:localhost:mapviews:global': 5
};
var mapKey = 'map_cfg|' + LayergroupToken.parse(layergroupIdToDelete).token;
keysToDelete[mapKey] = 0;
testHelper.deleteRedisKeys(keysToDelete, done);
});
var server = cartodbServer(ServerOptions);
server.setMaxListeners(0);
var boundaryPointsMapConfig = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: "WITH p AS ( " +
" SELECT '1970-01-02'::date as d, " +
// r1 is pixel resolution at zoom level 1
// s1 is tile size at zoom level 1
" 1e-40 as o, 78271.517578125 as r1, 20037508.5 as s1 )" +
" SELECT 1 as i, d, ST_MakePoint(0,0) as ug FROM p" +
" UNION ALL " +
"SELECT 2, d, ST_MakePoint(-o,-o) FROM p" +
" UNION ALL " +
"SELECT 3, d, ST_MakePoint(-o,o) FROM p" +
" UNION ALL " +
"SELECT 4, d, ST_MakePoint(o,-o) FROM p" +
" UNION ALL " +
"SELECT 5, d, ST_MakePoint(o,o) FROM p" +
" UNION ALL " +
"SELECT 6, d, ST_MakePoint(-r1,r1) FROM p" +
" UNION ALL " +
"SELECT 7, d, ST_MakePoint(-r1/2,r1) FROM p" +
" UNION ALL " +
"SELECT 8, d, ST_MakePoint(-r1,-r1) FROM p" +
" UNION ALL " +
// this is discarded, being on the right boundary
"SELECT 9, d, ST_MakePoint(s1,0) FROM p" +
" UNION ALL " +
// this is discarded, being on the top boundary
"SELECT 10, d, ST_MakePoint(0,s1) FROM p" +
" UNION ALL " +
// this is discarded, rounding to the right boundary
"SELECT 11, d, ST_MakePoint(s1-r1/2,0) FROM p" +
" UNION ALL " +
// this is discarded, rounding to the top boundary
"SELECT 12, d, ST_MakePoint(0,s1-r1/2) FROM p",
geom_column: 'ug',
cartocss: 'Map { ' +
'-torque-frame-count:2; ' +
'-torque-resolution:1; ' +
'-torque-time-attribute:"d"; ' +
'-torque-data-aggregation:linear; ' +
'-torque-aggregation-function:"count(i)"; }',
cartocss_version: '1.0.1'
} }
]
};
var tileRequests = [
{
desc: '0/0/0',
repr: [
' ------',
'| |',
'| |',
' ------'
],
z: 0,
x: 0,
y: 0,
expects: [
{
x__uint8: 255,
y__uint8: 128,
vals__uint8: [{
v: 2,
d: 'i=9 and i=11'
}],
dates__uint16: [0]
},
{
x__uint8: 128,
y__uint8: 128,
vals__uint8: [{
v: 8,
d: 'i=[1..8]'
}],
dates__uint16: [0]
},
{
x__uint8: 128,
y__uint8: 255,
vals__uint8: [{
v: 2,
d: 'i=10 and i=12'
}],
dates__uint16: [0]
}
]
},
{
desc: '1/0/0',
repr: [
'*00 | 10',
'----+----',
'01 | 11'
],
z: 1,
x: 0,
y: 0,
expects: [
{
x__uint8: 255,
y__uint8: 0,
vals__uint8: [{v: 2, d: 'i=6 (-r1,r1) and i=7(-r1/2,r1)'}],
dates__uint16: [0]
},
{
x__uint8: 255,
y__uint8: 1,
vals__uint8: [{v: 2, d: 'i=1 and i=3'}],
dates__uint16: [0]
},
{
x__uint8: 255,
y__uint8: 255,
vals__uint8: [{v: 2, d: 'i=10 and i=12'}],
dates__uint16: [0]
}
]
},
{
desc: '1/1/0',
repr: [
'00 | 10*',
'----+----',
'01 | 11'
],
z: 1,
x: 1,
y: 0,
expects: [
{
x__uint8: 255,
y__uint8: 0,
vals__uint8: [{v: 2, d: 'i=9 and i=11'}],
dates__uint16: [0]
},
{
x__uint8: 0,
y__uint8: 0,
vals__uint8: [{v: 2, d: 'i=1 and i=5'}],
dates__uint16: [0]
},
{
x__uint8: 0,
y__uint8: 255,
vals__uint8: [{v: 2, d: 'i=10 and i=12'}],
dates__uint16: [0]
}
]
},
{
desc: '1/0/1',
repr: [
'00 | 10',
'----+----',
'*01 | 11'
],
z: 1,
x: 0,
y: 1,
expects: [
{
x__uint8: 255,
y__uint8: 255,
vals__uint8: [{v: 3, d: 'i=1, i=2 and i=8'}],
dates__uint16: [0]
}
]
},
{
desc: '1/1/1',
repr: [
'00 | 10',
'----+----',
'01 | 11*'
],
z: 1,
x: 1,
y: 1,
expects: [
{
x__uint8: 255,
y__uint8: 255,
vals__uint8: [{
v: 2,
d: 'i=9 and i=11'
}],
dates__uint16: [0]
},
{
x__uint8: 0,
y__uint8: 255,
vals__uint8: [{
v: 2,
d: 'i=1 and i=4'
}],
dates__uint16: [0]
}
]
}
];
tileRequests.forEach(function(tileRequest) {
// See https://github.com/CartoDB/Windshaft/issues/186
var desc = 'handles ' + tileRequest.desc + '.json.torque\n\n\t' + tileRequest.repr.join('\n\t') + '\n\n';
it(desc, function (done) {
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(boundaryPointsMapConfig)
}, {}, function (res, err) {
assert.ok(!err, 'Failed to create layergroup');
var parsedBody = JSON.parse(res.body);
var expected_token = parsedBody.layergroupid;
layergroupIdToDelete = expected_token;
var partialUrl = tileRequest.z + '/' + tileRequest.x + '/' + tileRequest.y;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/' + partialUrl + '.json.torque',
method: 'GET'
}, {}, function (res, err) {
assert.ok(!err, 'Failed to get json');
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
var parsed = JSON.parse(res.body);
var i = 0;
tileRequest.expects.forEach(function(expected) {
assert.equal(
parsed[i].x__uint8,
expected.x__uint8,
parsed[i].x__uint8 + ' == ' + expected.x__uint8 +
'\nRESULT\n------' +
'\n' + JSON.stringify(parsed, null, 4) +
'\nEXPECTED\n--------' +
'\n' + JSON.stringify(tileRequest.expects, null, 4)
);
assert.equal(
parsed[i].y__uint8,
expected.y__uint8,
parsed[i].y__uint8 + ' == ' + expected.y__uint8 +
'\nRESULT\n------' +
'\n' + JSON.stringify(parsed, null, 4) +
'\nEXPECTED\n--------' +
'\n' + JSON.stringify(tileRequest.expects, null, 4)
);
var j = 0;
expected.vals__uint8.forEach(function(val) {
assert.equal(
parsed[i].vals__uint8[j],
val.v,
'desc: ' + val.d +
'Number of points got=' + parsed.length + '; ' +
'expected=' + tileRequest.expects.length +
'\n\tindex=' + i +
'\n\tvals__uint8 index=' + j +
'\n\tgot=' + parsed[i].vals__uint8[j] +
'\n\texpected=' + val.v +
'\nRESULT\n------' +
'\n' + JSON.stringify(parsed, null, 4) +
'\nEXPECTED\n--------' +
'\n' + JSON.stringify(tileRequest.expects, null, 4));
j++;
});
i++;
});
assert.equal(
parsed.length,
tileRequest.expects.length,
'Number of points did not match ' +
'got=' + parsed.length + '; ' +
'expected=' + tileRequest.expects.length +
'\nRESULT\n------' +
'\n' + JSON.stringify(parsed, null, 4) +
'\nEXPECTED\n--------' +
'\n' + JSON.stringify(tileRequest.expects, null, 4));
done();
});
});
});
});
it('regression london point', function(done) {
var londonPointMapConfig = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: "SELECT " +
"1 as i, " +
"'1970-01-02'::date as d, " +
"ST_MakePoint(-11309.9155492599,6715342.44989312) g",
geom_column: 'g',
cartocss: 'Map { ' +
'-torque-frame-count:2; ' +
'-torque-resolution:1; ' +
'-torque-time-attribute:"d"; ' +
'-torque-data-aggregation:linear; ' +
'-torque-aggregation-function:"count(i)"; }',
cartocss_version: '1.0.1'
} }
]
};
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(londonPointMapConfig)
}, {}, function (res, err) {
assert.ok(!err, 'Failed to create layergroup');
var parsedBody = JSON.parse(res.body);
var layergroupId = parsedBody.layergroupid;
layergroupIdToDelete = layergroupId;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + layergroupId + '/0/2/1/1.json.torque',
method: 'GET'
}, {}, function (res, err) {
assert.ok(!err, 'Failed to request torque.json');
var parsed = JSON.parse(res.body);
assert.deepEqual(parsed, [{
x__uint8: 255,
y__uint8: 172,
vals__uint8: [1],
dates__uint16: [0]
}]);
done();
});
});
});
it('should consider resolution for least value in query', function(done) {
var londonPointMapConfig = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: "" +
"SELECT " +
"0 as i, " +
"st_transform('0101000020E6100000FABD3AB4B5031C400581A80ECC2F4940'::geometry, 3857) as g " +
"UNION ALL " +
"SELECT " +
"2 as i, " +
"st_transform('0101000020E61000006739E30EAE031C406625C0C3C72F4940'::geometry, 3857) as g " +
"UNION ALL " +
"SELECT " +
"3 as i, " +
"st_transform('0101000020E6100000E26DB8A2A7031C40C8BAA5C2C52F4940'::geometry, 3857) as g",
geom_column: 'g',
cartocss: 'Map { ' +
'-torque-frame-count:1; ' +
'-torque-animation-duration:30;' +
'-torque-time-attribute:"i"; ' +
'-torque-aggregation-function:"count(i)"; ' +
'-torque-resolution:2; ' +
'-torque-data-aggregation: cumulative; }',
cartocss_version: '1.0.1'
} }
]
};
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(londonPointMapConfig)
}, {}, function (res, err) {
assert.ok(!err, 'Failed to create layergroup');
var parsedBody = JSON.parse(res.body);
var layergroupId = parsedBody.layergroupid;
layergroupIdToDelete = layergroupId;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + layergroupId + '/0/13/4255/2765.json.torque',
method: 'GET'
}, {}, function (res, err) {
assert.ok(!err, 'Failed to request torque.json');
var parsed = JSON.parse(res.body);
assert.deepEqual(parsed, [
{
x__uint8: 47,
y__uint8: 127,
vals__uint8: [2],
dates__uint16: [0]
},
{
x__uint8: 48,
y__uint8: 127,
vals__uint8: [1],
dates__uint16: [0]
}
]);
done();
});
});
});
});
+155
View File
@@ -0,0 +1,155 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('torque png renderer', function() {
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
var IMAGE_TOLERANCE_PER_MIL = 20;
var torquePngPointsMapConfig = {
version: '1.2.0',
layers: [
{
type: 'torque',
options: {
sql: "SELECT * FROM populated_places_simple_reduced where the_geom" +
" && ST_MakeEnvelope(-90, 0, 90, 65)",
cartocss: [
'Map {',
' buffer-size:0;',
' -torque-frame-count:1;',
' -torque-animation-duration:30;',
' -torque-time-attribute:"cartodb_id";',
' -torque-aggregation-function:"count(cartodb_id)";',
' -torque-resolution:1;',
' -torque-data-aggregation:linear;',
'}',
'#populated_places_simple_reduced{',
' comp-op: multiply;',
' marker-fill-opacity: 1;',
' marker-line-color: #FFF;',
' marker-line-width: 0;',
' marker-line-opacity: 1;',
' marker-type: rectangle;',
' marker-width: 3;',
' marker-fill: #FFCC00;',
'}'
].join(' '),
cartocss_version: '2.3.0'
}
}
]
};
var tileRequests = [
{
z: 2,
x: 2,
y: 1,
layer: 0,
format: 'torque.png'
},
{
z: 2,
x: 1,
y: 1,
layer: 0,
format: 'torque.png'
}
];
function torquePngFixture(zxy) {
return './test/fixtures/torque/populated_places_simple_reduced-' + zxy.join('.') + '.png';
}
tileRequests.forEach(function(tileRequest) {
var z = tileRequest.z,
x = tileRequest.x,
y = tileRequest.y;
var zxy = [z, x, y];
it('tile ' + zxy.join('/') + '.torque.png', function (done) {
testClient.getTileLayer(torquePngPointsMapConfig, tileRequest, function(err, res) {
assert.imageEqualsFile(res.body, torquePngFixture(zxy), IMAGE_TOLERANCE_PER_MIL, function(err) {
assert.ok(!err);
done();
});
});
});
});
var mapConfigTorqueOffset = {
version: '1.3.0',
layers: [
{
type: 'torque',
options: {
sql: "SELECT * FROM populated_places_simple_reduced",
cartocss: [
'Map {',
'buffer-size:0;',
'-torque-frame-count:1;',
'-torque-animation-duration:30;',
'-torque-time-attribute:"cartodb_id";',
'-torque-aggregation-function:"count(cartodb_id)";',
'-torque-resolution:2;',
'-torque-data-aggregation:linear;',
'}',
'',
'#populated_places_simple_reduced{',
' comp-op: lighter;',
' marker-fill-opacity: 0.9;',
' marker-line-color: #2167AB;',
' marker-line-width: 5;',
' marker-line-opacity: 1;',
' marker-type: ellipse;',
' marker-width: 6;',
' marker-fill: #FF9900;',
'}',
'#populated_places_simple_reduced[frame-offset=1] {',
' marker-width:8;',
' marker-fill-opacity:0.45; ',
'}',
'#populated_places_simple_reduced[frame-offset=2] {',
' marker-width:10;',
' marker-fill-opacity:0.225; ',
'}'
].join(' '),
cartocss_version: '2.3.0'
}
}
]
};
it('torque static map with offset', function(done) {
var w = 600,
h = 400;
testClient.getStaticBbox(mapConfigTorqueOffset, -170, -87, 170, 87, w, h, function(err, res, img) {
if (err) {
return done(err);
}
assert.equal(img.width(), w);
assert.equal(img.height(), h);
done();
});
});
});
@@ -0,0 +1,97 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('torque tiles at 0,0 point', function() {
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
/*
Tiles are represented as in:
---------
| TL | TR |
|--(0,0)--|
| BL | BR |
---------
*/
var tiles = [
{
what: 'tl',
x: 3,
y: 3,
expects: []
},
{
what: 'tr',
x: 4,
y: 3,
expects: []
},
{
what: 'bl',
x: 3,
y: 4,
expects: [{"x__uint8":1,"y__uint8":1,"vals__uint8":[1],"dates__uint16":[0]}]
},
{
what: 'br',
x: 4,
y: 4,
expects: [{"x__uint8":0,"y__uint8":1,"vals__uint8":[1],"dates__uint16":[0]}]
}
];
tiles.forEach(function(tile) {
it(tile.what, function(done) {
var query = 'select 1 cartodb_id,' +
' ST_Transform(ST_SetSRID(ST_MakePoint(0, 0), 4326), 3857) the_geom_webmercator';
var mapConfig = {
version: '1.3.0',
layers: [
{
type: 'torque',
options: {
sql: query,
cartocss: [
'Map {',
' -torque-time-attribute: "cartodb_id";',
' -torque-aggregation-function: "count(cartodb_id)";',
' -torque-frame-count: 1;',
' -torque-animation-duration: 15;',
' -torque-resolution: 128',
'}',
'#layer {',
' marker-fill: #fff;',
' marker-fill-opacity: 0.4;',
' marker-width: 1;',
'}'
].join(' '),
cartocss_version: '2.3.0'
}
}
]
};
testClient.getTorque(mapConfig, 0, 3, tile.x, tile.y, function(err, res) {
assert.deepEqual(JSON.parse(res.body), tile.expects);
done();
});
});
});
});
+131
View File
@@ -0,0 +1,131 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('wrap x coordinate', function() {
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
describe('renders correct tile', function() {
var IMG_TOLERANCE_PER_MIL = 20;
function plainTorqueMapConfig(plainColor) {
return {
version: '1.2.0',
layers: [
{
type: 'plain',
options: {
color: plainColor
}
},
{
type: 'torque',
options: {
sql: "SELECT * FROM populated_places_simple_reduced " +
"where the_geom && ST_MakeEnvelope(-90, 0, 90, 65)",
cartocss: [
'Map {',
' buffer-size:0;',
' -torque-frame-count:1;',
' -torque-animation-duration:30;',
' -torque-time-attribute:"cartodb_id";',
' -torque-aggregation-function:"count(cartodb_id)";',
' -torque-resolution:1;',
' -torque-data-aggregation:linear;',
'}',
'#populated_places_simple_reduced{',
' comp-op: multiply;',
' marker-fill-opacity: 1;',
' marker-line-color: #FFF;',
' marker-line-width: 0;',
' marker-line-opacity: 1;',
' marker-type: rectangle;',
' marker-width: 3;',
' marker-fill: #FFCC00;',
'}'
].join(' '),
cartocss_version: '2.3.0'
}
}
]
};
}
var testScenarios = [
{
tile: {
z: 2,
x: -2,
y: 1,
layer: 'all',
format: 'png'
},
fixture: {
z: 2,
x: 2,
y: 1
},
plainColor: 'white'
},
{
tile: {
z: 2,
x: -3,
y: 1,
layer: 'all',
format: 'png'
},
fixture: {
z: 2,
x: 1,
y: 1
},
plainColor: '#339900'
}
];
function blendPngFixture(zxy) {
return './test/fixtures/blend/blend-plain-torque-' + zxy.join('.') + '.png';
}
testScenarios.forEach(function(testScenario) {
var tileRequest = testScenario.tile;
var zxy = [tileRequest.z, tileRequest.x, tileRequest.y];
var fixtureZxy = [testScenario.fixture.z, testScenario.fixture.x, testScenario.fixture.y];
it('tile all/' + zxy.join('/') + '.png', function (done) {
testClient.getTileLayer(plainTorqueMapConfig(testScenario.plainColor), tileRequest, function(err, res) {
assert.imageEqualsFile(res.body, blendPngFixture(fixtureZxy), IMG_TOLERANCE_PER_MIL, function(err) {
assert.ok(!err);
done();
});
});
});
});
});
describe('mapnik', function() {
it("can get a tile with negative x coordinate", function(done){
testClient.getTile(testClient.defaultTableMapConfig('test_table'), 2, -2, 1, function(err, res, img) {
assert.ok(!err);
assert.ok(img);
assert.equal(img.width(), 256);
assert.equal(img.height(), 256);
done();
});
});
});
});
+8 -14
View File
@@ -4,16 +4,16 @@ var assert = require('../support/assert');
var querystring = require('querystring');
var step = require('step');
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
var serverOptions = require('../../lib/cartodb/server_options')();
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
suite('server', function() {
describe('server', function() {
// TODO: I guess this should be a 404 instead...
test("get call to server returns 200", function(done){
it("get call to server returns 200", function(done){
step(
function doGet() {
var next = this;
@@ -23,7 +23,7 @@ suite('server', function() {
},{}, function(res, err) { next(err,res); });
},
function doCheck(err, res) {
if ( err ) throw err;
assert.ifError(err);
assert.ok(res.statusCode, 200);
var cc = res.headers['x-cache-channel'];
assert.ok(!cc);
@@ -35,7 +35,7 @@ suite('server', function() {
);
});
test("get call to server returns 200", function(done){
it("get call to server returns 200", function(done){
assert.response(server, {
url: '/version',
method: 'GET'
@@ -44,25 +44,19 @@ suite('server', function() {
}, function(res) {
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('windshaft_cartodb'), "No 'windshaft_cartodb' version in " + parsed);
console.log("Windshaft-cartodb: " + parsed.windshaft_cartodb);
assert.ok(parsed.hasOwnProperty('windshaft'), "No 'windshaft' version in " + parsed);
console.log("Windshaft: " + parsed.windshaft);
assert.ok(parsed.hasOwnProperty('grainstore'), "No 'grainstore' version in " + parsed);
console.log("Grainstore: " + parsed.grainstore);
assert.ok(parsed.hasOwnProperty('node_mapnik'), "No 'node_mapnik' version in " + parsed);
console.log("Node-mapnik: " + parsed.node_mapnik);
assert.ok(parsed.hasOwnProperty('mapnik'), "No 'mapnik' version in " + parsed);
console.log("Mapnik: " + parsed.mapnik);
// TODO: check actual versions ?
done();
});
});
});
suite('server old_api', function() {
describe('server old_api', function() {
// See https://github.com/CartoDB/Windshaft-cartodb/issues/115
test.skip("get'ing tile with not-strictly-valid style", function(done) {
it.skip("get'ing tile with not-strictly-valid style", function(done) {
var style = querystring.stringify({style: '#test_table{line-color:black}}', style_version: '2.0.0'});
assert.response(server, {
headers: {host: 'localhost'},
File diff suppressed because it is too large Load Diff
+24 -12
View File
@@ -1,15 +1,26 @@
require('../support/test_helper');
var testHelper = require('../support/test_helper');
var assert = require('../support/assert');
var qs = require('querystring');
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
var serverOptions = require('../../lib/cartodb/server_options')();
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
describe('get requests x-cache-channel', function() {
var keysToDelete;
beforeEach(function() {
keysToDelete = {};
});
afterEach(function(done) {
testHelper.deleteRedisKeys(keysToDelete, done);
});
var statusOkResponse = {
status: 200
};
@@ -99,7 +110,10 @@ describe('get requests x-cache-channel', function() {
if (err) {
return callback(err);
}
callback(null, JSON.parse(res.body).layergroupid);
var layergroupId = JSON.parse(res.body).layergroupid;
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
callback(null, layergroupId, res);
}
);
}
@@ -107,12 +121,10 @@ describe('get requests x-cache-channel', function() {
describe('header should be present', function() {
it('/api/v1/map Map instantiation', function(done) {
assert.response(
server,
layergroupRequest,
statusOkResponse,
validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table')
);
var testFn = validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table');
withLayergroupId(function(err, layergroupId, res) {
testFn(res);
});
});
it ('/api/v1/map/:token/:z/:x/:y@:scale_factor?x.:format Mapnik retina tiles', function(done) {
@@ -219,7 +231,7 @@ describe('get requests x-cache-channel', function() {
var templateName = 'x_cache';
before(function(done) {
beforeEach(function(done) {
var template = {
version: '0.0.1',
name: templateName,
@@ -249,7 +261,7 @@ describe('get requests x-cache-channel', function() {
);
});
after(function(done) {
afterEach(function(done) {
assert.response(
server,
{
-81
View File
@@ -1,81 +0,0 @@
#!/usr/bin/env node
// small benchmark to execute with nodejs
var http = require('http')
if ( process.argv.length < 3 ) {
console.error("Usage: " + process.argv[1] + " <baseurl> [<map_key>]");
process.exit(1);
}
var baseurl = process.argv[2];
console.log("Baseurl is " + baseurl);
var baseurl_comps = baseurl.match(/(https?:\/\/)?([^:\/]*)(:([^\/]*))?(\/.*).*/);
var options = {
host: baseurl_comps[2],
port: baseurl_comps[4] ? baseurl_comps[4] : 8181,
path: baseurl_comps[5] + '/{z}/{x}/{y}.png?cache_buster=0'
};
if ( process.argv.length > 3 ) {
options.path += '&map_key=' + process.argv[3];
}
console.dir(options);
function randInt(min, max) {
return min + Math.floor(Math.random()*(max- min +1));
}
var start_time = new Date().getTime();
function end() {
var end_time = new Date().getTime();
var t = (end_time - start_time)/1000;
console.log("ok: ", ok)
console.log("error: ", error)
console.log("time: ", t);
console.log("req/s: ", ok/t);
}
var N = 1000
var ok = 0;
var error = 0;
function fail(msg) {
console.log(msg);
++ error;
if ( error + ok === N ) end();
}
function pass() {
++ ok;
if ( error + ok === N ) end();
}
for(var i = 0; i < N; ++i) {
var opt = {
host: options.host,
port: options.port,
path: new String(options.path)
};
opt.path = opt.path.replace('{z}', 2).replace('{x}', randInt(0, 3)).replace('{y}', randInt(0, 3));
//console.log(opt.path)
http.get(opt, function(res) {
res.body = '';
res.on('data', function(chunk) {
// Save only first chunk, to reduce cost of the operation
if ( res.body.length == 0 ) res.body += chunk;
});
res.on('end', function() {
if ( res.statusCode == 200 ) pass();
else {
fail(res.statusCode + ' http://' + opt.host + ':' + opt.port + opt.path + ' ' + res.body);
}
});
}).on('error', function(e) {
fail('unknown (http) error');
});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Some files were not shown because too many files have changed in this diff Show More