From 9b06ac833abec2dfbb301f53169e8806ec9ad77a Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Wed, 8 Oct 2014 10:10:58 +0200 Subject: [PATCH 01/60] Have ./configure tolerate unknown options --- configure | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/configure b/configure index fef3bc71..052a3502 100755 --- a/configure +++ b/configure @@ -56,9 +56,8 @@ while test -n "$1"; do ENVIRONMENT=`echo "$1" | cut -d= -f2` ;; *) - echo "Unknown option '$1'" >&2 - usage >&2 - exit 1 + echo "Unused option '$1'" >&2 + ;; esac shift done From e9f5c60719b7f1c3cd4021a06425dd6b6fa80f5a Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Wed, 8 Oct 2014 10:31:32 +0200 Subject: [PATCH 02/60] Use a windshaft-specific name for the test user Avoids problems with running cartodb-sql-api tests after windshaft-cartodb tests (as the test database is not cleaned up) --- config/environments/test.js.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/test.js.example b/config/environments/test.js.example index 2937f285..232940a4 100644 --- a/config/environments/test.js.example +++ b/config/environments/test.js.example @@ -49,7 +49,7 @@ var config = { ,postgres: { // Parameters to pass to datasource plugin of mapnik // See http://github.com/mapnik/mapnik/wiki/PostGIS - user: "testpublicuser", + user: "test_windshaft_publicuser", password: "public", host: '127.0.0.1', port: 5432, From 938b6579c0e1315547bd60a90e3c6f552561b119 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Fri, 30 Jan 2015 11:17:29 +0100 Subject: [PATCH 03/60] MapConfig named maps extension specification (draft) --- docs/MapConfig-NamedMaps-extension.md | 56 +++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 docs/MapConfig-NamedMaps-extension.md diff --git a/docs/MapConfig-NamedMaps-extension.md b/docs/MapConfig-NamedMaps-extension.md new file mode 100644 index 00000000..b99ebdaf --- /dev/null +++ b/docs/MapConfig-NamedMaps-extension.md @@ -0,0 +1,56 @@ +# STATUS: DRAFT + +# 1. Purpose + +This specification describes an extension for +[MapConfig 1.3.0](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.3.0.md) version. + + +# 2. Changes over specification + +This extension introduces a new layer type so it's possible to use a named map by its name as a layer. + +## 2.1 Named layers definition + +```javascript +{ + // REQUIRED + // string, `named` is the only supported value + type: "named", + + // REQUIRED + // object, set `named` map layers configuration + options: { + + // REQUIRED + // string, the name for the named map to use + name: "world_borders", + + // OPTIONAL + // object, the replacement values for the named map's template placeholders + // See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Map-API.md#instantiate-1 for more details + config: { + "color": "#000" + }, + + // OPTIONAL + // string array, the authorized tokens in case the named map has auth method set to `token` + // See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Map-API.md#named-maps-1 for more details + auth_tokens: [ + "token1", + "token2" + ] + } +} +``` + +## 2.2 Limitations + +A `named` map will not allow to have `named` type layers inside their templates layergroup's layers definition. + + +# History + +## 1.0.0 + + - Initial version From f099a69df3c59546f15d82ca14739420fec07761 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Fri, 30 Jan 2015 11:27:16 +0100 Subject: [PATCH 04/60] Add limitations for named maps of the user account --- docs/MapConfig-NamedMaps-extension.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/MapConfig-NamedMaps-extension.md b/docs/MapConfig-NamedMaps-extension.md index b99ebdaf..97425b9f 100644 --- a/docs/MapConfig-NamedMaps-extension.md +++ b/docs/MapConfig-NamedMaps-extension.md @@ -46,7 +46,9 @@ This extension introduces a new layer type so it's possible to use a named map b ## 2.2 Limitations -A `named` map will not allow to have `named` type layers inside their templates layergroup's layers definition. +1. A Named Map will not allow to have `named` type layers inside their templates layergroup's layers definition. +2. A `named` layer does not allow Named Maps form other accounts, it's only possible to use Named Maps from the very +same user account. # History From 7a2297325819ea21fee4bc1c9741950e079ede48 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Fri, 30 Jan 2015 12:59:26 +0100 Subject: [PATCH 05/60] Use windshaft's before-layergroup-step branch --- npm-shrinkwrap.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index f2f6d72e..87d48cba 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -178,9 +178,9 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" }, "windshaft": { - "version": "0.35.1", - "from": "https://github.com/CartoDB/Windshaft/tarball/0.35.1", - "resolved": "https://github.com/CartoDB/Windshaft/tarball/0.35.1", + "version": "0.35.2", + "from": "https://github.com/CartoDB/Windshaft/tarball/before-layergroup-step", + "resolved": "https://github.com/CartoDB/Windshaft/tarball/before-layergroup-step", "dependencies": { "chronograph": { "version": "0.1.0", diff --git a/package.json b/package.json index b9d0efda..23d98e60 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "node-varnish": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0", "underscore" : "~1.6.0", "dot": "~1.0.2", - "windshaft": "https://github.com/CartoDB/Windshaft/tarball/0.35.1", + "windshaft": "https://github.com/CartoDB/Windshaft/tarball/before-layergroup-step", "step": "~0.0.5", "request": "~2.9.203", "cartodb-redis": "https://github.com/CartoDB/node-cartodb-redis/tarball/0.11.0", From 5573dfda84e88f3a079c24d79f956ea560a49f2f Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Fri, 30 Jan 2015 15:26:27 +0100 Subject: [PATCH 06/60] Add queue-async dependency --- npm-shrinkwrap.json | 10 +++++----- package.json | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 87d48cba..71666d2b 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -113,6 +113,11 @@ "from": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0", "resolved": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0" }, + "queue-async": { + "version": "1.0.7", + "from": "queue-async@~1.0.7", + "resolved": "https://registry.npmjs.org/queue-async/-/queue-async-1.0.7.tgz" + }, "redis-mpool": { "version": "0.3.0", "from": "https://github.com/CartoDB/node-redis-mpool/tarball/0.3.0", @@ -187,11 +192,6 @@ "from": "chronograph@git://github.com/CartoDB/chronographjs.git#0.1.0", "resolved": "git://github.com/CartoDB/chronographjs.git#0b8c35eee510cfa14a16be24d70533b38ecc1d2d" }, - "queue-async": { - "version": "1.0.7", - "from": "queue-async@~1.0.7", - "resolved": "https://registry.npmjs.org/queue-async/-/queue-async-1.0.7.tgz" - }, "grainstore": { "version": "0.22.1", "from": "https://github.com/CartoDB/grainstore/tarball/0.22.1", diff --git a/package.json b/package.json index 23d98e60..7cbc7e87 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "dot": "~1.0.2", "windshaft": "https://github.com/CartoDB/Windshaft/tarball/before-layergroup-step", "step": "~0.0.5", + "queue-async": "~1.0.7", "request": "~2.9.203", "cartodb-redis": "https://github.com/CartoDB/node-cartodb-redis/tarball/0.11.0", "cartodb-psql": "https://github.com/CartoDB/node-cartodb-psql/tarball/0.4.0", From aecb07b008193a4339ba5610e358659ea9384ef8 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Fri, 30 Jan 2015 15:28:55 +0100 Subject: [PATCH 07/60] Create redis pool in server options when not supplied --- lib/cartodb/server_options.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index a731535b..1c1046a9 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -34,8 +34,10 @@ var REQUEST_QUERY_PARAMS_WHITELIST = [ ]; module.exports = function(redisPool) { - var redisOpts = redisPool ? {pool: redisPool} : global.environment.redis; - var cartoData = require('cartodb-redis')(redisOpts), + redisPool = redisPool + || require('redis-mpool')(_.extend(global.environment.redis, {name: 'windshaft:server_options'})); + + var cartoData = require('cartodb-redis')({ pool: redisPool }), lzmaWorker = new LZMA(), queryTablesApi = new QueryTablesApi(); From e28fe1fdc06be67d0b079efe9b24d83b0d96fc5f Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Fri, 30 Jan 2015 15:30:13 +0100 Subject: [PATCH 08/60] Initialize template maps in server options --- lib/cartodb/cartodb_windshaft.js | 28 +++++++++++++--------------- lib/cartodb/server_options.js | 19 ++++++++++++------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/lib/cartodb/cartodb_windshaft.js b/lib/cartodb/cartodb_windshaft.js index 55e59917..c7e63266 100644 --- a/lib/cartodb/cartodb_windshaft.js +++ b/lib/cartodb/cartodb_windshaft.js @@ -1,11 +1,9 @@ -var _ = require('underscore') - , Step = require('step') - , Windshaft = require('windshaft') - , TemplateMaps = require('./template_maps.js') - , Cache = require('./cache_validator') - , os = require('os') - , HealthCheck = require('./monitoring/health_check') -; +var _ = require('underscore'); +var Step = require('step'); +var Windshaft = require('windshaft'); +var Cache = require('./cache_validator'); +var os = require('os'); +var HealthCheck = require('./monitoring/health_check'); if ( ! process.env['PGAPPNAME'] ) process.env['PGAPPNAME']='cartodb_tiler'; @@ -48,12 +46,6 @@ var CartodbWindshaft = function(serverOptions) { // var template_baseurl = global.environment.base_url_templated || '(?:/maps/named|/tiles/template)'; - var templateMapsOpts = { - max_user_templates: global.environment.maxUserTemplates - }; - var templateMaps = new TemplateMaps(redisPool, templateMapsOpts); - serverOptions.templateMaps = templateMaps; - var SurrogateKeysCache = require('./cache/surrogate_keys_cache'), NamedMapsCacheEntry = require('./cache/model/named_maps_entry'), VarnishHttpCacheBackend = require('./cache/backend/varnish_http'), @@ -158,7 +150,13 @@ var CartodbWindshaft = function(serverOptions) { var TemplateMapsController = require('./controllers/template_maps'), templateMapsController = new TemplateMapsController( - ws, serverOptions, templateMaps, cartoData, template_baseurl, surrogateKeysCache, NamedMapsCacheEntry + ws, + serverOptions, + serverOptions.templateMaps, + cartoData, + template_baseurl, + surrogateKeysCache, + NamedMapsCacheEntry ); templateMapsController.register(ws); diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index 1c1046a9..399f259e 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -1,10 +1,10 @@ -var _ = require('underscore') - , Step = require('step') - , Cache = require('./cache_validator') - , QueryTablesApi = require('./api/query_tables_api') - , crypto = require('crypto') - , LZMA = require('lzma').LZMA - ; +var _ = require('underscore'); +var Step = require('step'); +var Cache = require('./cache_validator'); +var QueryTablesApi = require('./api/query_tables_api'); +var crypto = require('crypto'); +var LZMA = require('lzma').LZMA; +var TemplateMaps = require('./template_maps.js'); // This is for backward compatibility with 1.3.3 if ( _.isUndefined(global.environment.sqlapi.domain) ) { @@ -106,6 +106,11 @@ module.exports = function(redisPool) { // Re-use redisPool me.redis.pool = redisPool; + var templateMaps = new TemplateMaps(redisPool, { + max_user_templates: global.environment.maxUserTemplates + }); + me.templateMaps = templateMaps; + /* This whole block is about generating X-Cache-Channel { */ // TODO: review lifetime of elements of this cache From d0ef87b0cffa8db1baf013795835bdfc4bdf3052 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Fri, 30 Jan 2015 15:31:49 +0100 Subject: [PATCH 09/60] Add a before layergroup creation action to allow first level named maps layer type to be extended as other layers --- .../models/mapconfig_named_layers_adapter.js | 76 +++++++++++++++++++ lib/cartodb/server_options.js | 14 ++++ 2 files changed, 90 insertions(+) create mode 100644 lib/cartodb/models/mapconfig_named_layers_adapter.js diff --git a/lib/cartodb/models/mapconfig_named_layers_adapter.js b/lib/cartodb/models/mapconfig_named_layers_adapter.js new file mode 100644 index 00000000..e75b771b --- /dev/null +++ b/lib/cartodb/models/mapconfig_named_layers_adapter.js @@ -0,0 +1,76 @@ +var queue = require('queue-async'); +var _ = require('underscore'); + +function MapConfigNamedLayersAdapter(templateMaps) { + this.templateMaps = templateMaps; +} + +module.exports = MapConfigNamedLayersAdapter; + +MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, callback) { + var self = this; + + var adaptLayersQueue = queue(layers.length); + + function adaptLayer(layer, callback) { + if (layer.type === 'named') { + + if (!layer.options.name) { + return callback(new Error('Missing Named Map name in layer options')); + } + + var templateName = layer.options.name; + var templateConfigParams = layer.options.config || {}; + var templateAuthTokens = layer.options.auth_tokens; + + self.templateMaps.getTemplate(username, templateName, function(err, template) { + if (err) { + return callback(err); + } + + if (self.templateMaps.isAuthorized(template, templateAuthTokens)) { + var nestedNamedLayers = template.layergroup.layers.filter(function(layer) { + return layer.type === 'named'; + }); + + if (nestedNamedLayers.length > 0) { + var nestedNamedMapsError = new Error('Nested named layers are not allowed'); + // nestedNamedMapsError.http_status = 400; + return callback(nestedNamedMapsError); + } + + try { + var templateLayergroupConfig = self.templateMaps.instance(template, templateConfigParams); + return callback(null, templateLayergroupConfig.layers); + } catch (err) { + return callback(err); + } + } else { + var unauthorizedError = new Error('Unauthorized template instantiation'); + // unauthorizedError.http_status = 403; + return callback(unauthorizedError); + } + }); + } else { + return callback(null, layer); + } + } + + layers.forEach(function(layer) { + adaptLayersQueue.defer(adaptLayer, layer); + }); + + function layersAdaptQueueFinish(err, layers) { + if (err) { + return callback(err); + } + + if (!layers || layers.length === 0) { + return callback(new Error('Missing layers array from layergroup config')); + } + + return callback(null, _.flatten(layers)); + } + + adaptLayersQueue.awaitAll(layersAdaptQueueFinish); +}; diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index 399f259e..99321634 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -5,6 +5,7 @@ var QueryTablesApi = require('./api/query_tables_api'); var crypto = require('crypto'); var LZMA = require('lzma').LZMA; var TemplateMaps = require('./template_maps.js'); +var MapConfigNamedLayersAdapter = require('./models/mapconfig_named_layers_adapter'); // This is for backward compatibility with 1.3.3 if ( _.isUndefined(global.environment.sqlapi.domain) ) { @@ -111,6 +112,8 @@ module.exports = function(redisPool) { }); 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 @@ -303,6 +306,17 @@ module.exports = function(redisPool) { }); }; + me.beforeLayergroupCreate = function(req, requestMapConfig, callback) { + mapConfigNamedLayersAdapter.getLayers(this.userByReq(req), requestMapConfig.layers, function(err, layers) { + if (err) { + return callback(err); + } + + requestMapConfig.layers = layers; + return callback(null, requestMapConfig) + }); + }; + me.afterLayergroupCreate = function(req, mapconfig, response, callback) { var token = response.layergroupid; From 358b2967508ae4bac7c0a1ed89861d0113b594a4 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Fri, 30 Jan 2015 16:50:06 +0100 Subject: [PATCH 10/60] Remove beforeEach and afterEach, in combination with suite they are triggered for every single test even outside of the suite they were invoked in. --- Makefile | 6 ++- .../cache/surrogate_keys_invalidation.js | 47 +++++++++++-------- test/acceptance/health_check.js | 10 ++-- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/Makefile b/Makefile index 795b997c..c033aab0 100644 --- a/Makefile +++ b/Makefile @@ -17,8 +17,10 @@ config/environments/test.js: config.status--test check-local: config/environments/test.js ./run_tests.sh ${RUNTESTFLAGS} \ - test/unit/cartodb/*.js \ - test/acceptance/*.js + test/unit/cartodb/*.js \ + test/unit/cartodb/cache/model/*.js \ + test/acceptance/*.js \ + test/acceptance/cache/*.js check-submodules: PATH="$$PATH:$(srcdir)/node_modules/.bin/"; \ diff --git a/test/acceptance/cache/surrogate_keys_invalidation.js b/test/acceptance/cache/surrogate_keys_invalidation.js index c4bb3818..118c6a63 100644 --- a/test/acceptance/cache/surrogate_keys_invalidation.js +++ b/test/acceptance/cache/surrogate_keys_invalidation.js @@ -16,9 +16,13 @@ var serverOptions = ServerOptions(); suite('templates surrogate keys', function() { - var redisClient, - sqlApiServer, - server; + var sqlApiServer; + var redisClient = redis.createClient(global.environment.redis.port); + + // Enable Varnish purge for tests + serverOptions.varnish_purge_enabled = true; + + var server = new CartodbWindshaft(serverOptions); var templateOwner = 'localhost', templateName = 'acceptance', @@ -45,19 +49,12 @@ suite('templates surrogate keys', function() { expectedBody = { template_id: expectedTemplateId }; suiteSetup(function(done) { - // Enable Varnish purge for tests - serverOptions.varnish_purge_enabled = true; - - server = new CartodbWindshaft(serverOptions); - sqlApiServer = new SqlApiEmulator(global.environment.sqlapi.port, done); - - redisClient = redis.createClient(global.environment.redis.port); }); var surrogateKeysCacheInvalidateFn = SurrogateKeysCache.prototype.invalidate; - beforeEach(function(done) { + function createTemplate(callback) { var postTemplateRequest = { url: '/tiles/template?api_key=1234', method: 'POST', @@ -90,10 +87,10 @@ suite('templates surrogate keys', function() { return true; }, function finish(err) { - done(err); + callback(err); } ); - }); + } test("update template calls surrogate keys invalidation", function(done) { var cacheEntryKey; @@ -104,7 +101,13 @@ suite('templates surrogate keys', function() { }; Step( - function putValidTemplate() { + function createTemplateToUpdate() { + createTemplate(this); + }, + function putValidTemplate(err) { + if (err) { + throw err; + } var updateTemplateRequest = { url: '/tiles/template/' + expectedTemplateId + '/?api_key=1234', method: 'PUT', @@ -163,7 +166,13 @@ suite('templates surrogate keys', function() { }; Step( - function putValidTemplate() { + function createTemplateToDelete() { + createTemplate(this); + }, + function deleteValidTemplate(err) { + if (err) { + throw err; + } var deleteTemplateRequest = { url: '/tiles/template/' + expectedTemplateId + '/?api_key=1234', method: 'DELETE', @@ -199,12 +208,10 @@ suite('templates surrogate keys', function() { ); }); - afterEach(function(done) { - SurrogateKeysCache.prototype.invalidate = surrogateKeysCacheInvalidateFn; - done(); - }); - suiteTeardown(function(done) { + SurrogateKeysCache.prototype.invalidate = surrogateKeysCacheInvalidateFn; + // Enable Varnish purge for tests + serverOptions.varnish_purge_enabled = false; sqlApiServer.close(done); }); diff --git a/test/acceptance/health_check.js b/test/acceptance/health_check.js index af8c361e..b6409574 100644 --- a/test/acceptance/health_check.js +++ b/test/acceptance/health_check.js @@ -7,7 +7,7 @@ var server = new CartodbWindshaft(serverOptions); suite('health checks', function () { - beforeEach(function (done) { + function resetHealthConfig() { global.environment.health = { enabled: true, username: 'localhost', @@ -15,8 +15,7 @@ suite('health checks', function () { x: 0, y: 0 }; - done(); - }); + } var healthCheckRequest = { url: '/health', @@ -27,13 +26,14 @@ suite('health checks', function () { }; test('returns 200 and ok=true with enabled configuration', function (done) { + resetHealthConfig(); + assert.response(server, healthCheckRequest, { status: 200 }, function (res, err) { - console.log(res.body); assert.ok(!err); var parsed = JSON.parse(res.body); @@ -47,6 +47,8 @@ suite('health checks', function () { }); test('fails for invalid user because it is not in redis', function (done) { + resetHealthConfig(); + global.environment.health.username = 'invalid'; assert.response(server, From 8b07156a2dc3317af1716df9d5dbaaaa9e14f20d Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Fri, 30 Jan 2015 16:51:09 +0100 Subject: [PATCH 11/60] Make templateMaps available in cartodb windshaft --- lib/cartodb/cartodb_windshaft.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/cartodb/cartodb_windshaft.js b/lib/cartodb/cartodb_windshaft.js index c7e63266..a1dbe824 100644 --- a/lib/cartodb/cartodb_windshaft.js +++ b/lib/cartodb/cartodb_windshaft.js @@ -23,6 +23,8 @@ var CartodbWindshaft = function(serverOptions) { var cartoData = require('cartodb-redis')({pool: redisPool}); + var templateMaps = serverOptions.templateMaps; + if(serverOptions.cache_enabled) { console.log("cache invalidation enabled, varnish on ", serverOptions.varnish_host, ' ', serverOptions.varnish_port); Cache.init(serverOptions.varnish_host, serverOptions.varnish_port, serverOptions.varnish_secret); @@ -150,13 +152,7 @@ var CartodbWindshaft = function(serverOptions) { var TemplateMapsController = require('./controllers/template_maps'), templateMapsController = new TemplateMapsController( - ws, - serverOptions, - serverOptions.templateMaps, - cartoData, - template_baseurl, - surrogateKeysCache, - NamedMapsCacheEntry + ws, serverOptions, templateMaps, cartoData, template_baseurl, surrogateKeysCache, NamedMapsCacheEntry ); templateMapsController.register(ws); From 87eaeb0074af13c07479d9fd7e71cf2adce45d30 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Fri, 30 Jan 2015 18:57:01 +0100 Subject: [PATCH 12/60] Some integration tests for different cases in named layers type --- Makefile | 1 + .../models/mapconfig_named_layers_adapter.js | 8 +- .../mapconfig_named_layers_adapter.js | 318 ++++++++++++++++++ 3 files changed, 323 insertions(+), 4 deletions(-) create mode 100644 test/integration/mapconfig_named_layers_adapter.js diff --git a/Makefile b/Makefile index c033aab0..32a48859 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ check-local: config/environments/test.js ./run_tests.sh ${RUNTESTFLAGS} \ test/unit/cartodb/*.js \ test/unit/cartodb/cache/model/*.js \ + test/integration/*.js \ test/acceptance/*.js \ test/acceptance/cache/*.js diff --git a/lib/cartodb/models/mapconfig_named_layers_adapter.js b/lib/cartodb/models/mapconfig_named_layers_adapter.js index e75b771b..df5b843b 100644 --- a/lib/cartodb/models/mapconfig_named_layers_adapter.js +++ b/lib/cartodb/models/mapconfig_named_layers_adapter.js @@ -16,7 +16,7 @@ MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, cal if (layer.type === 'named') { if (!layer.options.name) { - return callback(new Error('Missing Named Map name in layer options')); + return callback(new Error('Missing Named Map `name` in layer options')); } var templateName = layer.options.name; @@ -24,8 +24,8 @@ MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, cal var templateAuthTokens = layer.options.auth_tokens; self.templateMaps.getTemplate(username, templateName, function(err, template) { - if (err) { - return callback(err); + if (err || !template) { + return callback(new Error("Template '" + templateName + "' of user '" + username + "' not found")); } if (self.templateMaps.isAuthorized(template, templateAuthTokens)) { @@ -46,7 +46,7 @@ MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, cal return callback(err); } } else { - var unauthorizedError = new Error('Unauthorized template instantiation'); + var unauthorizedError = new Error("Unauthorized '" + templateName + "' template instantiation"); // unauthorizedError.http_status = 403; return callback(unauthorizedError); } diff --git a/test/integration/mapconfig_named_layers_adapter.js b/test/integration/mapconfig_named_layers_adapter.js new file mode 100644 index 00000000..9fd84781 --- /dev/null +++ b/test/integration/mapconfig_named_layers_adapter.js @@ -0,0 +1,318 @@ +var assert = require('assert'); +var RedisPool = require('redis-mpool'); +var TemplateMaps = require('../../lib/cartodb/template_maps.js'); +var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter'); + +var test_helper = require('../support/test_helper'); +var Step = require('step'); +var _ = require('underscore'); + +suite('mapconfig_named_layers_adapter', function() { + + // configure redis pool instance to use in tests + var redisPool = RedisPool(global.environment.redis); + + var templateMaps = new TemplateMaps(redisPool, { + max_user_templates: global.environment.maxUserTemplates + }); + + var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps); + + 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 wadusMapnikLayer = { + type: 'mapnik', + options: { + sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator', + cartocss: '#layer { polygon-fill: <%= polygon_color %>; }', + cartocss_version: '2.3.0' + } + }; + + var username = 'me'; + + 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'] + }, + layergroup: { + layers: [ + wadusLayer + ] + } + }; + + var multipleLayersTemplateName = 'multiple_valid_template'; + var multipleLayersTemplate = { + version: '0.0.1', + name: multipleLayersTemplateName, + auth: { + method: 'token', + valid_tokens: ['valid1', 'valid2'] + }, + "placeholders": { + "polygon_color": { + "type": "css_color", + "default": "green" + }, + "color": { + "type": "css_color", + "default": "red" + } + }, + layergroup: { + layers: [ + wadusMapnikLayer, + wadusLayer + ] + } + }; + + var namedMapLayer = { + type: 'named', + options: { + name: templateName, + config: {}, + auth_tokens: [] + } + }; + + var nestedNamedMapTemplateName = 'auth_valid_template'; + var nestedNamedMapTemplate = { + version: '0.0.1', + name: nestedNamedMapTemplateName, + auth: { + method: 'open' + }, + layergroup: { + layers: [ + namedMapLayer + ] + } + }; + + function makeNamedMapLayerConfig(options) { + return { + version: '1.3.0', + layers: [ + { + type: 'named', + options: options + } + ] + }; + } + + + suiteSetup(function(done) { + templateMaps.addTemplate(username, template, done); + }); + + test('should fail for named map layer with missing name', function(done) { + + + assert.response(server, + healthCheckRequest, + { + status: 200 + }, + function (res, err) { + assert.ok(!err); + + var parsed = JSON.parse(res.body); + + assert.ok(parsed.enabled); + assert.ok(parsed.ok); + + done(); + } + ); + + + + + var missingNamedMapLayerConfig = makeNamedMapLayerConfig({ + config: {} + }); + mapConfigNamedLayersAdapter.getLayers(username, missingNamedMapLayerConfig.layers, function(err, layers) { + assert.ok(err); + assert.ok(!layers); + assert.equal(err.message, 'Missing Named Map `name` in layer options'); + done(); + }); + }); + + test('should fail for non-existing template name', function(done) { + var missingTemplateName = 'wadus'; + var nonExistentNamedMapLayerConfig = makeNamedMapLayerConfig({ + name: missingTemplateName + }); + mapConfigNamedLayersAdapter.getLayers(username, nonExistentNamedMapLayerConfig.layers, function(err, layers) { + assert.ok(err); + assert.ok(!layers); + assert.equal(err.message, "Template '" + missingTemplateName + "' of user '" + username + "' not found"); + done(); + }); + }); + + test('should fail if not properly authorized', function(done) { + templateMaps.addTemplate(username, tokenAuthTemplate, function(err) { + if (err) { + return done(err); + } + + var nonAuthTokensNamedMapLayerConfig = makeNamedMapLayerConfig({ + name: tokenAuthTemplateName + }); + mapConfigNamedLayersAdapter.getLayers(username, nonAuthTokensNamedMapLayerConfig.layers, function(err, layers) { + assert.ok(err); + assert.ok(!layers); + assert.equal(err.message, "Unauthorized '" + tokenAuthTemplateName + "' template instantiation"); + + templateMaps.delTemplate(username, tokenAuthTemplateName, done); + }); + }); + }); + + test('should fail for nested named map layers', function(done) { + templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) { + if (err) { + return done(err); + } + + var nestedNamedMapLayerConfig = makeNamedMapLayerConfig({ + name: nestedNamedMapTemplateName + }); + mapConfigNamedLayersAdapter.getLayers(username, nestedNamedMapLayerConfig.layers, function(err, layers) { + assert.ok(err); + assert.ok(!layers); + assert.equal(err.message, 'Nested named layers are not allowed'); + + templateMaps.delTemplate(username, nestedNamedMapTemplateName, done); + }); + }); + }); + + test('should return an expanded list of layers for a named map layer', function(done) { + var validNamedMapMapLayerConfig = makeNamedMapLayerConfig({ + name: templateName + }); + mapConfigNamedLayersAdapter.getLayers(username, validNamedMapMapLayerConfig.layers, function(err, layers) { + assert.ok(!err); + assert.ok(layers.length, 1); + assert.ok(layers[0].type, 'cartodb'); + done(); + }); + }); + + test('should return on auth=token with valid tokens provided', function(done) { + templateMaps.addTemplate(username, tokenAuthTemplate, function(err) { + if (err) { + return done(err); + } + + var validAuthTokensNamedMapLayerConfig = makeNamedMapLayerConfig({ + name: tokenAuthTemplateName, + auth_tokens: ['valid1'] + }); + mapConfigNamedLayersAdapter.getLayers(username, validAuthTokensNamedMapLayerConfig.layers, function(err, layers) { + assert.ok(!err); + assert.equal(layers.length, 1); + + templateMaps.delTemplate(username, tokenAuthTemplateName, done); + }); + }); + }); + + test('should return an expanded list of layers for a named map layer, multiple layers version', function(done) { + templateMaps.addTemplate(username, multipleLayersTemplate, function(err) { + if (err) { + return done(err); + } + + var multipleLayersNamedMapLayerConfig = makeNamedMapLayerConfig({ + name: multipleLayersTemplateName, + auth_tokens: ['valid2'] + }); + mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, function(err, layers) { + assert.ok(!err); + assert.equal(layers.length, 2); + + assert.equal(layers[0].type, 'mapnik'); + assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: green; }'); + + assert.equal(layers[1].type, 'cartodb'); + assert.equal(layers[1].options.cartocss, '#layer { marker-fill: red; }'); + + templateMaps.delTemplate(username, multipleLayersTemplateName, done); + }); + }); + }); + + test('should replace template params with the given config', function(done) { + templateMaps.addTemplate(username, multipleLayersTemplate, function(err) { + if (err) { + return done(err); + } + + var color = '#cc3300', + polygonColor = '#ff9900'; + + var multipleLayersNamedMapLayerConfig = makeNamedMapLayerConfig({ + name: multipleLayersTemplateName, + config: { + polygon_color: polygonColor, + color: color + }, + auth_tokens: ['valid2'] + }); + mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, function(err, layers) { + assert.ok(!err); + assert.equal(layers.length, 2); + + assert.equal(layers[0].type, 'mapnik'); + assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: ' + polygonColor + '; }'); + + assert.equal(layers[1].type, 'cartodb'); + assert.equal(layers[1].options.cartocss, '#layer { marker-fill: ' + color + '; }'); + + templateMaps.delTemplate(username, multipleLayersTemplateName, done); + }); + }); + }); + + suiteTeardown(function(done) { + templateMaps.delTemplate(username, templateName, done); + }); +}); From 12c5d835c538f3eda50f7f17d61dfba63f9493c5 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Fri, 30 Jan 2015 19:11:54 +0100 Subject: [PATCH 13/60] Fix integration tests as I messed with the filename --- .../mapconfig_named_layers_adapter.js | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/test/integration/mapconfig_named_layers_adapter.js b/test/integration/mapconfig_named_layers_adapter.js index 9fd84781..1402dda6 100644 --- a/test/integration/mapconfig_named_layers_adapter.js +++ b/test/integration/mapconfig_named_layers_adapter.js @@ -108,7 +108,7 @@ suite('mapconfig_named_layers_adapter', function() { } }; - var nestedNamedMapTemplateName = 'auth_valid_template'; + var nestedNamedMapTemplateName = 'nested_template'; var nestedNamedMapTemplate = { version: '0.0.1', name: nestedNamedMapTemplateName, @@ -140,28 +140,6 @@ suite('mapconfig_named_layers_adapter', function() { }); test('should fail for named map layer with missing name', function(done) { - - - assert.response(server, - healthCheckRequest, - { - status: 200 - }, - function (res, err) { - assert.ok(!err); - - var parsed = JSON.parse(res.body); - - assert.ok(parsed.enabled); - assert.ok(parsed.ok); - - done(); - } - ); - - - - var missingNamedMapLayerConfig = makeNamedMapLayerConfig({ config: {} }); From 4949616c4ec1119911ad669d95be40641343e096 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Fri, 30 Jan 2015 19:29:45 +0100 Subject: [PATCH 14/60] Some acceptance tests, http_status = 403 should not happen in adapter, needs improvement --- .../models/mapconfig_named_layers_adapter.js | 2 +- test/acceptance/named_layers.js | 361 ++++++++++++++++++ 2 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 test/acceptance/named_layers.js diff --git a/lib/cartodb/models/mapconfig_named_layers_adapter.js b/lib/cartodb/models/mapconfig_named_layers_adapter.js index df5b843b..0e4c49b3 100644 --- a/lib/cartodb/models/mapconfig_named_layers_adapter.js +++ b/lib/cartodb/models/mapconfig_named_layers_adapter.js @@ -47,7 +47,7 @@ MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, cal } } else { var unauthorizedError = new Error("Unauthorized '" + templateName + "' template instantiation"); - // unauthorizedError.http_status = 403; + unauthorizedError.http_status = 403; return callback(unauthorizedError); } }); diff --git a/test/acceptance/named_layers.js b/test/acceptance/named_layers.js new file mode 100644 index 00000000..e9e1c2b2 --- /dev/null +++ b/test/acceptance/named_layers.js @@ -0,0 +1,361 @@ +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 server = new CartodbWindshaft(serverOptions); + +var SQLAPIEmulator = require('../support/SQLAPIEmu.js'); + + +var RedisPool = require('redis-mpool'); +var TemplateMaps = require('../../lib/cartodb/template_maps.js'); +var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter'); + +var Step = require('step'); +var _ = require('underscore'); + +suite('template_maps', function() { + var sqlapiServer; + + // configure redis pool instance to use in tests + var redisPool = RedisPool(global.environment.redis); + + var templateMaps = new TemplateMaps(redisPool, { + max_user_templates: global.environment.maxUserTemplates + }); + + var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps); + + 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 + ] + } + }; + + function makeNamedMapLayerConfig(options) { + return { + version: '1.3.0', + layers: [ + { + type: 'named', + options: options + } + ] + }; + } + + + suiteSetup(function(done) { + templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) { + if (err) { + return done(err); + } + templateMaps.addTemplate(username, tokenAuthTemplate, function(err) { + if (err) { + return done(err); + } + sqlapiServer = new SQLAPIEmulator(global.environment.sqlapi.port, done); + }); + }); + }); + + test('should fail for non-existing template name', function(done) { + var layergroup = { + version: '1.3.0', + layers: [ + { + type: 'named', + options: { + name: 'nonexistent' + } + } + ] + }; + + Step( + function createLayergroup() { + var next = this; + assert.response(server, + { + url: '/tiles/layergroup', + method: 'POST', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(layergroup) + }, + { + status: 400 + }, + function(res, err) { + next(err, res); + } + ); + }, + function checkLayergroup(err, response) { + if (err) { + throw err; + } + + var parsedBody = JSON.parse(response.body); + assert.deepEqual(parsedBody, { errors: ["Template 'nonexistent' of user 'localhost' not found"] }); + + return null; + }, + function finish(err) { + done(err); + } + ); + }); + + test('should return 403 if not properly authorized', function(done) { + + var layergroup = { + version: '1.3.0', + layers: [ + { + type: 'named', + options: { + name: tokenAuthTemplateName, + config: {}, + auth_tokens: ['token1'] + } + } + ] + }; + + Step( + function createLayergroup() { + var next = this; + assert.response(server, + { + url: '/tiles/layergroup', + method: 'POST', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(layergroup) + }, + { + status: 403 + }, + function(res, err) { + next(err, res); + } + ); + }, + function checkLayergroup(err, response) { + if (err) { + throw err; + } + + var parsedBody = JSON.parse(response.body); + assert.deepEqual( + parsedBody, + { errors: [ "Unauthorized 'auth_valid_template' template instantiation" ] } + ); + + return null; + }, + function finish(err) { + done(err); + } + ); + + }); + + test('should return 200 and layergroup if properly authorized', function(done) { + + var layergroup = { + version: '1.3.0', + layers: [ + { + type: 'named', + options: { + name: tokenAuthTemplateName, + config: {}, + auth_tokens: ['valid1'] + } + } + ] + }; + + Step( + function createLayergroup() { + var next = this; + assert.response(server, + { + url: '/tiles/layergroup', + method: 'POST', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(layergroup) + }, + { + 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); + + return null; + }, + function finish(err) { + done(err); + } + ); + + }); + + test('should return 400 for nested named map layers', function(done) { + + var layergroup = { + version: '1.3.0', + layers: [ + { + type: 'named', + options: { + name: nestedNamedMapTemplateName + } + } + ] + }; + + Step( + function createLayergroup() { + var next = this; + assert.response(server, + { + url: '/tiles/layergroup', + method: 'POST', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(layergroup) + }, + { + status: 400 + }, + function(res, err) { + next(err, res); + } + ); + }, + function checkLayergroup(err, response) { + if (err) { + throw err; + } + + var parsedBody = JSON.parse(response.body); + assert.deepEqual(parsedBody, { errors: [ 'Nested named layers are not allowed' ] }); + + return null; + }, + function finish(err) { + done(err); + } + ); + + }); + + + suiteTeardown(function(done) { + templateMaps.delTemplate(username, nestedNamedMapTemplateName, function(err) { + if (err) { + return done(err); + } + templateMaps.delTemplate(username, tokenAuthTemplateName, function(err) { + if (err) { + return done(err); + } + sqlapiServer.close(done); + }); + }); + }); +}); From 1bb0d8738e53b7bbc6dd25cdaf227c4fd386f641 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 2 Feb 2015 14:38:26 +0100 Subject: [PATCH 15/60] Add test case for layers with private tables --- test/acceptance/named_layers.js | 128 ++++++++++++++++++++++++++++---- 1 file changed, 113 insertions(+), 15 deletions(-) diff --git a/test/acceptance/named_layers.js b/test/acceptance/named_layers.js index e9e1c2b2..0842305c 100644 --- a/test/acceptance/named_layers.js +++ b/test/acceptance/named_layers.js @@ -25,8 +25,6 @@ suite('template_maps', function() { max_user_templates: global.environment.maxUserTemplates }); - var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps); - var wadusLayer = { type: 'cartodb', options: { @@ -103,19 +101,6 @@ suite('template_maps', function() { } }; - function makeNamedMapLayerConfig(options) { - return { - version: '1.3.0', - layers: [ - { - type: 'named', - options: options - } - ] - }; - } - - suiteSetup(function(done) { templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) { if (err) { @@ -344,6 +329,119 @@ suite('template_maps', function() { }); + test('should return 200 and layergroup with private tables', function(done) { + + var privateTableTemplateName = 'private_table_template'; + var privateTableTemplate = { + version: '0.0.1', + name: privateTableTemplateName, + auth: { + method: 'open' + }, + layergroup: { + layers: [ + { + type: 'cartodb', + options: { + sql: 'select * from test_table_private_1', + cartocss: '#layer { marker-fill: #cc3300; }', + cartocss_version: '2.3.0' + } + } + ] + } + }; + + var layergroup = { + version: '1.3.0', + layers: [ + { + type: 'named', + options: { + name: privateTableTemplateName + } + } + ] + }; + + Step( + function createTemplate() { + templateMaps.addTemplate(username, privateTableTemplate, this); + }, + function createLayergroup(err) { + if (err) { + throw err; + } + + var next = this; + assert.response(server, + { + url: '/tiles/layergroup', + method: 'POST', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(layergroup) + }, + { + 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); + + return parsedBody.layergroupid; + }, + function requestTile(err, layergroupId) { + if (err) { + throw err; + } + + assert.response(server, + { + url: '/tiles/layergroup/' + layergroupId + '/0/0/0.png', + method: 'GET', + headers: { + host: 'localhost' + }, + encoding: 'binary' + }, + { + status: 200, + headers: { + 'content-type': 'image/png' + } + }, + function(res, err) { + next(err); + } + ); + }, + function deleteTemplate(err) { + var next = this; + templateMaps.delTemplate(username, privateTableTemplate, function(/*delErr*/) { + // ignore deletion error + next(err); + }); + }, + function finish(err) { + done(err); + } + ); + + }); + suiteTeardown(function(done) { templateMaps.delTemplate(username, nestedNamedMapTemplateName, function(err) { From e4e633cf8612985187950c002e9f021cc8a15922 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 2 Feb 2015 17:44:15 +0100 Subject: [PATCH 16/60] Fix next reference --- test/acceptance/named_layers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/acceptance/named_layers.js b/test/acceptance/named_layers.js index 0842305c..27a4e5c1 100644 --- a/test/acceptance/named_layers.js +++ b/test/acceptance/named_layers.js @@ -408,6 +408,7 @@ suite('template_maps', function() { throw err; } + var next = this; assert.response(server, { url: '/tiles/layergroup/' + layergroupId + '/0/0/0.png', From 58f9f5f7a8e0194a5fa633838f6483290440aff3 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Tue, 3 Feb 2015 14:16:55 +0100 Subject: [PATCH 17/60] Remove unused object, rename suite --- test/acceptance/named_layers.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/acceptance/named_layers.js b/test/acceptance/named_layers.js index 27a4e5c1..ff8f29f9 100644 --- a/test/acceptance/named_layers.js +++ b/test/acceptance/named_layers.js @@ -10,12 +10,11 @@ var SQLAPIEmulator = require('../support/SQLAPIEmu.js'); var RedisPool = require('redis-mpool'); var TemplateMaps = require('../../lib/cartodb/template_maps.js'); -var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter'); var Step = require('step'); var _ = require('underscore'); -suite('template_maps', function() { +suite('named_layers', function() { var sqlapiServer; // configure redis pool instance to use in tests From 2c7bc6adde561b20ed955b905e3c7bcd6795d128 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Wed, 4 Feb 2015 11:30:36 +0100 Subject: [PATCH 18/60] Datasource to give per-layer authentication in named layers Make beforeLayergroupCreate to return a datasource with different authentication for the different layers. - Named layers will get access to private tables in case it's needed Changes in MapConfigNamedLayersAdapter: - It will retrieve the dbAuth params only if named layers are present so there is no extra overhead for normal layers - Rename queue function signature from `callback` to `done` so it is easier to follow the code Add several tests to validate `named` layers authentication --- .../models/mapconfig_named_layers_adapter.js | 78 ++++- lib/cartodb/server_options.js | 4 +- npm-shrinkwrap.json | 12 +- package.json | 2 +- test/acceptance/named_layers.js | 74 ++++- .../mapconfig_named_layers_datasource.js | 297 ++++++++++++++++++ ....js => mapconfig_named_layers_expanded.js} | 35 ++- 7 files changed, 464 insertions(+), 38 deletions(-) create mode 100644 test/integration/mapconfig_named_layers_datasource.js rename test/integration/{mapconfig_named_layers_adapter.js => mapconfig_named_layers_expanded.js} (86%) diff --git a/lib/cartodb/models/mapconfig_named_layers_adapter.js b/lib/cartodb/models/mapconfig_named_layers_adapter.js index 0e4c49b3..ceb9c429 100644 --- a/lib/cartodb/models/mapconfig_named_layers_adapter.js +++ b/lib/cartodb/models/mapconfig_named_layers_adapter.js @@ -1,5 +1,6 @@ var queue = require('queue-async'); var _ = require('underscore'); +var Datasource = require('windshaft').Datasource; function MapConfigNamedLayersAdapter(templateMaps) { this.templateMaps = templateMaps; @@ -7,16 +8,16 @@ function MapConfigNamedLayersAdapter(templateMaps) { module.exports = MapConfigNamedLayersAdapter; -MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, callback) { +MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbMetadata, callback) { var self = this; var adaptLayersQueue = queue(layers.length); - function adaptLayer(layer, callback) { - if (layer.type === 'named') { + function adaptLayer(layer, done) { + if (isNamedTypeLayer(layer)) { if (!layer.options.name) { - return callback(new Error('Missing Named Map `name` in layer options')); + return done(new Error('Missing Named Map `name` in layer options')); } var templateName = layer.options.name; @@ -25,7 +26,7 @@ MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, cal self.templateMaps.getTemplate(username, templateName, function(err, template) { if (err || !template) { - return callback(new Error("Template '" + templateName + "' of user '" + username + "' not found")); + return done(new Error("Template '" + templateName + "' of user '" + username + "' not found")); } if (self.templateMaps.isAuthorized(template, templateAuthTokens)) { @@ -36,41 +37,84 @@ MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, cal if (nestedNamedLayers.length > 0) { var nestedNamedMapsError = new Error('Nested named layers are not allowed'); // nestedNamedMapsError.http_status = 400; - return callback(nestedNamedMapsError); + return done(nestedNamedMapsError); } try { var templateLayergroupConfig = self.templateMaps.instance(template, templateConfigParams); - return callback(null, templateLayergroupConfig.layers); + return done(null, { + datasource: true, + layers: templateLayergroupConfig.layers + }); } catch (err) { - return callback(err); + return done(err); } } else { var unauthorizedError = new Error("Unauthorized '" + templateName + "' template instantiation"); unauthorizedError.http_status = 403; - return callback(unauthorizedError); + return done(unauthorizedError); } }); + } else { - return callback(null, layer); + return done(null, { + datasource: false, + layers: [layer] + }); } } - layers.forEach(function(layer) { - adaptLayersQueue.defer(adaptLayer, layer); - }); + var datasourceBuilder = new Datasource.Builder(); - function layersAdaptQueueFinish(err, layers) { + function layersAdaptQueueFinish(err, layersResults) { if (err) { return callback(err); } - if (!layers || layers.length === 0) { + if (!layersResults || layersResults.length === 0) { return callback(new Error('Missing layers array from layergroup config')); } - return callback(null, _.flatten(layers)); + var layers = [], + currentLayerIndex = 0; + + layersResults.forEach(function(layersResult) { + + layersResult.layers.forEach(function(layer) { + layers.push(layer); + if (layersResult.datasource) { + datasourceBuilder.withLayerDatasource(currentLayerIndex, { + user: dbAuth.dbuser + }); + } + currentLayerIndex++; + }); + + }); + + return callback(null, layers, datasourceBuilder.build()); + } + + + var dbAuth = {}; + + if (_.some(layers, isNamedTypeLayer)) { + // Lazy load dbAuth + dbMetadata.setDBAuth(username, dbAuth, function(err) { + if (err) { + return callback(err); + } + layers.forEach(function(layer) { + adaptLayersQueue.defer(adaptLayer, layer); + }); + adaptLayersQueue.awaitAll(layersAdaptQueueFinish); + }); + } else { + return callback(null, layers, datasourceBuilder.build()); } - adaptLayersQueue.awaitAll(layersAdaptQueueFinish); }; + +function isNamedTypeLayer(layer) { + return layer.type === 'named'; +} diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index 99321634..948349ba 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -307,13 +307,13 @@ module.exports = function(redisPool) { }; me.beforeLayergroupCreate = function(req, requestMapConfig, callback) { - mapConfigNamedLayersAdapter.getLayers(this.userByReq(req), requestMapConfig.layers, function(err, layers) { + mapConfigNamedLayersAdapter.getLayers(this.userByReq(req), requestMapConfig.layers, this, function(err, layers, datasource) { if (err) { return callback(err); } requestMapConfig.layers = layers; - return callback(null, requestMapConfig) + return callback(null, requestMapConfig, datasource) }); }; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 71666d2b..c4fe8e44 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -97,7 +97,7 @@ }, "inherits": { "version": "2.0.1", - "from": "inherits@~2.0.1" + "from": "inherits@2" } } } @@ -184,8 +184,8 @@ }, "windshaft": { "version": "0.35.2", - "from": "https://github.com/CartoDB/Windshaft/tarball/before-layergroup-step", - "resolved": "https://github.com/CartoDB/Windshaft/tarball/before-layergroup-step", + "from": "https://github.com/CartoDB/Windshaft/tarball/mapconfig-per-layer-datasource", + "resolved": "https://github.com/CartoDB/Windshaft/tarball/mapconfig-per-layer-datasource", "dependencies": { "chronograph": { "version": "0.1.0", @@ -193,9 +193,9 @@ "resolved": "git://github.com/CartoDB/chronographjs.git#0b8c35eee510cfa14a16be24d70533b38ecc1d2d" }, "grainstore": { - "version": "0.22.1", - "from": "https://github.com/CartoDB/grainstore/tarball/0.22.1", - "resolved": "https://github.com/CartoDB/grainstore/tarball/0.22.1", + "version": "0.22.2", + "from": "https://github.com/CartoDB/grainstore/tarball/datasource-extend", + "resolved": "https://github.com/CartoDB/grainstore/tarball/datasource-extend", "dependencies": { "carto": { "version": "0.9.5-cdb2", diff --git a/package.json b/package.json index 7cbc7e87..5eb6f11c 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "node-varnish": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0", "underscore" : "~1.6.0", "dot": "~1.0.2", - "windshaft": "https://github.com/CartoDB/Windshaft/tarball/before-layergroup-step", + "windshaft": "https://github.com/CartoDB/Windshaft/tarball/mapconfig-per-layer-datasource", "step": "~0.0.5", "queue-async": "~1.0.7", "request": "~2.9.203", diff --git a/test/acceptance/named_layers.js b/test/acceptance/named_layers.js index ff8f29f9..ec04afbf 100644 --- a/test/acceptance/named_layers.js +++ b/test/acceptance/named_layers.js @@ -109,7 +109,12 @@ suite('named_layers', function() { if (err) { return done(err); } - sqlapiServer = new SQLAPIEmulator(global.environment.sqlapi.port, done); + templateMaps.addTemplate(username, template, function(err) { + if (err) { + return done(err); + } + sqlapiServer = new SQLAPIEmulator(global.environment.sqlapi.port, done); + }); }); }); }); @@ -442,6 +447,66 @@ suite('named_layers', function() { }); + test('should return 403 when private table is accessed from non named layer', function(done) { + + var layergroup = { + version: '1.3.0', + layers: [ + { + type: 'cartodb', + options: { + sql: 'select * from test_table_private_1', + cartocss: '#layer { marker-fill: #cc3300; }', + cartocss_version: '2.3.0' + } + }, + { + type: 'named', + options: { + name: templateName + } + } + ] + }; + + Step( + function createLayergroup() { + var next = this; + assert.response(server, + { + url: '/tiles/layergroup', + method: 'POST', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(layergroup) + }, + { + status: 403 + }, + function(res, err) { + next(err, res); + } + ); + }, + function checkLayergroup(err, response) { + if (err) { + throw err; + } + + var parsedBody = JSON.parse(response.body); + assert.ok(parsedBody.errors[0].match(/permission denied for relation test_table_private_1/)); + + return null; + }, + function finish(err) { + done(err); + } + ); + + }); + suiteTeardown(function(done) { templateMaps.delTemplate(username, nestedNamedMapTemplateName, function(err) { @@ -452,7 +517,12 @@ suite('named_layers', function() { if (err) { return done(err); } - sqlapiServer.close(done); + templateMaps.delTemplate(username, templateName, function(err) { + if (err) { + return done(err); + } + sqlapiServer.close(done); + }); }); }); }); diff --git a/test/integration/mapconfig_named_layers_datasource.js b/test/integration/mapconfig_named_layers_datasource.js new file mode 100644 index 00000000..e529a860 --- /dev/null +++ b/test/integration/mapconfig_named_layers_datasource.js @@ -0,0 +1,297 @@ +var test_helper = require('../support/test_helper'); + +var assert = require('assert'); +var RedisPool = require('redis-mpool'); +var TemplateMaps = require('../../lib/cartodb/template_maps.js'); +var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')(); +var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter'); + +var Step = require('step'); +var _ = require('underscore'); + +// configure redis pool instance to use in tests +var redisPool = RedisPool(global.environment.redis); + +var templateMaps = new TemplateMaps(redisPool, { + max_user_templates: global.environment.maxUserTemplates +}); + +var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps); + +var wadusSql = 'select 1 wadusLayer, null::geometry the_geom_webmercator'; +var wadusLayer = { + type: 'cartodb', + options: { + sql: wadusSql, + cartocss: '#layer { marker-fill: black; }', + cartocss_version: '2.3.0' + } +}; + +var wadusTemplateSql = 'select 1 wadusTemplateLayer, null::geometry the_geom_webmercator'; +var wadusTemplateLayer = { + type: 'cartodb', + options: { + sql: wadusTemplateSql, + cartocss: '#layer { marker-fill: <%= color %>; }', + cartocss_version: '2.3.0' + } +}; + +var wadusMapnikSql = 'select 1 wadusMapnikLayer, null::geometry the_geom_webmercator'; +var wadusMapnikLayer = { + type: 'mapnik', + options: { + sql: wadusMapnikSql, + cartocss: '#layer { polygon-fill: <%= polygon_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: [ + wadusTemplateLayer + ] + } +}; + +var multipleLayersTemplateName = 'multiple_valid_template'; +var multipleLayersTemplate = { + version: '0.0.1', + name: multipleLayersTemplateName, + auth: { + method: 'token', + valid_tokens: ['valid1', 'valid2'] + }, + "placeholders": { + "polygon_color": { + "type": "css_color", + "default": "green" + }, + "color": { + "type": "css_color", + "default": "red" + } + }, + layergroup: { + layers: [ + wadusMapnikLayer, + wadusTemplateLayer + ] + } +}; + +suite('named_layers datasources', function() { + suiteSetup(function(done) { + templateMaps.addTemplate(username, template, function(err) { + if (err) { + return done(err); + } + templateMaps.addTemplate(username, multipleLayersTemplate, done); + }); + }); + + function makeNamedMapLayerConfig(layers) { + return { + version: '1.3.0', + layers: layers + }; + } + + var simpleNamedLayer = { + type: 'named', + options: { + name: templateName + } + }; + + var multipleLayersNamedLayer = { + type: 'named', + options: { + name: multipleLayersTemplateName, + auth_tokens: ['valid2'] + } + }; + + var testScenarios = [ + { + desc: 'without datasource for non-named layers', + config: makeNamedMapLayerConfig([wadusLayer]), + test: function(err, layers, datasource, done) { + assert.ok(!err); + assert.equal(layers.length, 1); + + assert.equal(layers[0].type, 'cartodb'); + assert.equal(layers[0].options.sql, wadusSql); + assert.equal(datasource.getLayerDatasource(0), undefined); + + done(); + } + }, + + { + desc: 'with datasource for the named layer but not for the normal', + config: makeNamedMapLayerConfig([wadusLayer, simpleNamedLayer]), + test: function(err, layers, datasource, done) { + assert.ok(!err); + assert.equal(layers.length, 2); + + assert.equal(layers[0].type, 'cartodb'); + assert.equal(layers[0].options.sql, wadusSql); + assert.equal(datasource.getLayerDatasource(0), undefined); + + assert.equal(layers[1].type, 'cartodb'); + assert.equal(layers[1].options.sql, wadusTemplateSql); + var layerDatasource = datasource.getLayerDatasource(1); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + done(); + } + }, + + { + desc: 'with datasource for the multiple layers in the named but not for the normal', + config: makeNamedMapLayerConfig([wadusLayer, multipleLayersNamedLayer]), + test: function(err, layers, datasource, done) { + assert.ok(!err); + assert.equal(layers.length, 3); + + assert.equal(layers[0].type, 'cartodb'); + assert.equal(layers[0].options.sql, wadusSql); + assert.equal(datasource.getLayerDatasource(0), undefined); + + assert.equal(layers[1].type, 'mapnik'); + assert.equal(layers[1].options.sql, wadusMapnikSql); + var layerDatasource = datasource.getLayerDatasource(1); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + assert.equal(layers[2].type, 'cartodb'); + assert.equal(layers[2].options.sql, wadusTemplateSql); + layerDatasource = datasource.getLayerDatasource(2); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + done(); + } + }, + + { + desc: 'all with datasource because all are named', + config: makeNamedMapLayerConfig([multipleLayersNamedLayer, simpleNamedLayer]), + test: function(err, layers, datasource, done) { + assert.ok(!err); + assert.equal(layers.length, 3); + + assert.equal(layers[0].type, 'mapnik'); + assert.equal(layers[0].options.sql, wadusMapnikSql); + var layerDatasource = datasource.getLayerDatasource(0); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + assert.equal(layers[1].type, 'cartodb'); + assert.equal(layers[1].options.sql, wadusTemplateSql); + layerDatasource = datasource.getLayerDatasource(1); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + assert.equal(layers[2].type, 'cartodb'); + assert.equal(layers[2].options.sql, wadusTemplateSql); + layerDatasource = datasource.getLayerDatasource(2); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + done(); + } + }, + + { + desc: 'with a mix of datasource and no datasource depending if layers are named or not', + config: makeNamedMapLayerConfig([simpleNamedLayer, multipleLayersNamedLayer, wadusLayer, simpleNamedLayer, wadusLayer, multipleLayersNamedLayer]), + test: function(err, layers, datasource, done) { + + assert.ok(!err); + assert.equal(layers.length, 8); + + assert.equal(layers[0].type, 'cartodb'); + assert.equal(layers[0].options.sql, wadusTemplateSql); + var layerDatasource = datasource.getLayerDatasource(0); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + assert.equal(layers[1].type, 'mapnik'); + assert.equal(layers[1].options.sql, wadusMapnikSql); + layerDatasource = datasource.getLayerDatasource(1); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + assert.equal(layers[2].type, 'cartodb'); + assert.equal(layers[2].options.sql, wadusTemplateSql); + layerDatasource = datasource.getLayerDatasource(2); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + assert.equal(layers[3].type, 'cartodb'); + assert.equal(layers[3].options.sql, wadusSql); + assert.equal(datasource.getLayerDatasource(3), undefined); + + assert.equal(layers[4].type, 'cartodb'); + assert.equal(layers[4].options.sql, wadusTemplateSql); + layerDatasource = datasource.getLayerDatasource(4); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + assert.equal(layers[5].type, 'cartodb'); + assert.equal(layers[5].options.sql, wadusSql); + assert.equal(datasource.getLayerDatasource(5), undefined); + + assert.equal(layers[6].type, 'mapnik'); + assert.equal(layers[6].options.sql, wadusMapnikSql); + layerDatasource = datasource.getLayerDatasource(6); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + assert.equal(layers[7].type, 'cartodb'); + assert.equal(layers[7].options.sql, wadusTemplateSql); + layerDatasource = datasource.getLayerDatasource(7); + assert.notEqual(layerDatasource, undefined); + assert.ok(layerDatasource.user); + + done(); + } + } + ]; + + testScenarios.forEach(function(testScenario) { + test('should return a list of layers ' + testScenario.desc, function(done) { + mapConfigNamedLayersAdapter.getLayers(username, testScenario.config.layers, serverOptions, function(err, layers, datasource) { + testScenario.test(err, layers, datasource, done); + }); + }); + }); + + suiteTeardown(function(done) { + templateMaps.delTemplate(username, templateName, function(err) { + if (err) { + return done(err); + } + templateMaps.delTemplate(username, multipleLayersTemplateName, done); + }); + }); +}); diff --git a/test/integration/mapconfig_named_layers_adapter.js b/test/integration/mapconfig_named_layers_expanded.js similarity index 86% rename from test/integration/mapconfig_named_layers_adapter.js rename to test/integration/mapconfig_named_layers_expanded.js index 1402dda6..26b5197a 100644 --- a/test/integration/mapconfig_named_layers_adapter.js +++ b/test/integration/mapconfig_named_layers_expanded.js @@ -1,9 +1,11 @@ +var testHelper = require('../support/test_helper'); + var assert = require('assert'); var RedisPool = require('redis-mpool'); var TemplateMaps = require('../../lib/cartodb/template_maps.js'); +var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')(); var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter'); -var test_helper = require('../support/test_helper'); var Step = require('step'); var _ = require('underscore'); @@ -36,7 +38,7 @@ suite('mapconfig_named_layers_adapter', function() { } }; - var username = 'me'; + var username = 'localhost'; var templateName = 'valid_template'; var template = { @@ -143,10 +145,12 @@ suite('mapconfig_named_layers_adapter', function() { var missingNamedMapLayerConfig = makeNamedMapLayerConfig({ config: {} }); - mapConfigNamedLayersAdapter.getLayers(username, missingNamedMapLayerConfig.layers, function(err, layers) { + mapConfigNamedLayersAdapter.getLayers(username, missingNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { assert.ok(err); assert.ok(!layers); + assert.ok(!datasource); assert.equal(err.message, 'Missing Named Map `name` in layer options'); + done(); }); }); @@ -156,10 +160,12 @@ suite('mapconfig_named_layers_adapter', function() { var nonExistentNamedMapLayerConfig = makeNamedMapLayerConfig({ name: missingTemplateName }); - mapConfigNamedLayersAdapter.getLayers(username, nonExistentNamedMapLayerConfig.layers, function(err, layers) { + mapConfigNamedLayersAdapter.getLayers(username, nonExistentNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { assert.ok(err); assert.ok(!layers); + assert.ok(!datasource); assert.equal(err.message, "Template '" + missingTemplateName + "' of user '" + username + "' not found"); + done(); }); }); @@ -173,9 +179,10 @@ suite('mapconfig_named_layers_adapter', function() { var nonAuthTokensNamedMapLayerConfig = makeNamedMapLayerConfig({ name: tokenAuthTemplateName }); - mapConfigNamedLayersAdapter.getLayers(username, nonAuthTokensNamedMapLayerConfig.layers, function(err, layers) { + mapConfigNamedLayersAdapter.getLayers(username, nonAuthTokensNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { assert.ok(err); assert.ok(!layers); + assert.ok(!datasource); assert.equal(err.message, "Unauthorized '" + tokenAuthTemplateName + "' template instantiation"); templateMaps.delTemplate(username, tokenAuthTemplateName, done); @@ -192,9 +199,10 @@ suite('mapconfig_named_layers_adapter', function() { var nestedNamedMapLayerConfig = makeNamedMapLayerConfig({ name: nestedNamedMapTemplateName }); - mapConfigNamedLayersAdapter.getLayers(username, nestedNamedMapLayerConfig.layers, function(err, layers) { + mapConfigNamedLayersAdapter.getLayers(username, nestedNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { assert.ok(err); assert.ok(!layers); + assert.ok(!datasource); assert.equal(err.message, 'Nested named layers are not allowed'); templateMaps.delTemplate(username, nestedNamedMapTemplateName, done); @@ -206,10 +214,12 @@ suite('mapconfig_named_layers_adapter', function() { var validNamedMapMapLayerConfig = makeNamedMapLayerConfig({ name: templateName }); - mapConfigNamedLayersAdapter.getLayers(username, validNamedMapMapLayerConfig.layers, function(err, layers) { + mapConfigNamedLayersAdapter.getLayers(username, validNamedMapMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { assert.ok(!err); assert.ok(layers.length, 1); assert.ok(layers[0].type, 'cartodb'); + assert.notEqual(datasource.getLayerDatasource(0), undefined); + done(); }); }); @@ -224,9 +234,10 @@ suite('mapconfig_named_layers_adapter', function() { name: tokenAuthTemplateName, auth_tokens: ['valid1'] }); - mapConfigNamedLayersAdapter.getLayers(username, validAuthTokensNamedMapLayerConfig.layers, function(err, layers) { + mapConfigNamedLayersAdapter.getLayers(username, validAuthTokensNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { assert.ok(!err); assert.equal(layers.length, 1); + assert.notEqual(datasource.getLayerDatasource(0), undefined); templateMaps.delTemplate(username, tokenAuthTemplateName, done); }); @@ -243,15 +254,17 @@ suite('mapconfig_named_layers_adapter', function() { name: multipleLayersTemplateName, auth_tokens: ['valid2'] }); - mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, function(err, layers) { + mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { assert.ok(!err); assert.equal(layers.length, 2); assert.equal(layers[0].type, 'mapnik'); assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: green; }'); + assert.notEqual(datasource.getLayerDatasource(0), undefined); assert.equal(layers[1].type, 'cartodb'); assert.equal(layers[1].options.cartocss, '#layer { marker-fill: red; }'); + assert.notEqual(datasource.getLayerDatasource(1), undefined); templateMaps.delTemplate(username, multipleLayersTemplateName, done); }); @@ -275,15 +288,17 @@ suite('mapconfig_named_layers_adapter', function() { }, auth_tokens: ['valid2'] }); - mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, function(err, layers) { + mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { assert.ok(!err); assert.equal(layers.length, 2); assert.equal(layers[0].type, 'mapnik'); assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: ' + polygonColor + '; }'); + assert.notEqual(datasource.getLayerDatasource(0), undefined); assert.equal(layers[1].type, 'cartodb'); assert.equal(layers[1].options.cartocss, '#layer { marker-fill: ' + color + '; }'); + assert.notEqual(datasource.getLayerDatasource(1), undefined); templateMaps.delTemplate(username, multipleLayersTemplateName, done); }); From 5018d32af636e63fb0816834f8219b80e6043e64 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Wed, 4 Feb 2015 18:52:37 +0100 Subject: [PATCH 19/60] Add querytables and cdb_tablemetadata for proper testing its integration --- test/support/prepare_db.sh | 5 ++ test/support/sql/CDB_QueryStatements.sql | 14 +++++ test/support/sql/CDB_QueryTables.sql | 67 ++++++++++++++++++++++++ test/support/sql/windshaft.test.sql | 9 ++++ 4 files changed, 95 insertions(+) create mode 100644 test/support/sql/CDB_QueryStatements.sql create mode 100644 test/support/sql/CDB_QueryTables.sql diff --git a/test/support/prepare_db.sh b/test/support/prepare_db.sh index 8ec0c08a..991b52f9 100755 --- a/test/support/prepare_db.sh +++ b/test/support/prepare_db.sh @@ -78,6 +78,11 @@ if test x"$PREPARE_PGSQL" = xyes; then sed "s/:TESTPASS/${TESTPASS}/" | psql -v ON_ERROR_STOP=1 ${TEST_DB} || exit 1 + curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/cdb/scripts-available/CDB_QueryStatements.sql -o sql/CDB_QueryStatements.sql + curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/cdb/scripts-available/CDB_QueryTables.sql -o sql/CDB_QueryTables.sql + cat sql/CDB_QueryStatements.sql sql/CDB_QueryTables.sql | + psql -v ON_ERROR_STOP=1 ${TEST_DB} || exit 1 + fi if test x"$PREPARE_REDIS" = xyes; then diff --git a/test/support/sql/CDB_QueryStatements.sql b/test/support/sql/CDB_QueryStatements.sql new file mode 100644 index 00000000..bb4d10cf --- /dev/null +++ b/test/support/sql/CDB_QueryStatements.sql @@ -0,0 +1,14 @@ +-- Return an array of statements found in the given query text +-- +-- Regexp curtesy of Hubert Lubaczewski (depesz) +-- Implemented in plpython for performance reasons +-- +CREATE OR REPLACE FUNCTION CDB_QueryStatements(query text) +RETURNS SETOF TEXT AS $$ + import re + pat = re.compile( r'''((?:[^'"$;]+|"[^"]*"|'[^']*'|(\$[^$]*\$).*?\2)+)''', re.DOTALL ) + for match in pat.findall(query): + cleaned = match[0].strip() + if ( cleaned ): + yield cleaned +$$ language 'plpythonu' IMMUTABLE STRICT; diff --git a/test/support/sql/CDB_QueryTables.sql b/test/support/sql/CDB_QueryTables.sql new file mode 100644 index 00000000..2fb9532a --- /dev/null +++ b/test/support/sql/CDB_QueryTables.sql @@ -0,0 +1,67 @@ +-- Return an array of table names scanned by a given query +-- +-- Requires PostgreSQL 9.x+ +-- +CREATE OR REPLACE FUNCTION CDB_QueryTables(query text) +RETURNS name[] +AS $$ +DECLARE + exp XML; + tables NAME[]; + rec RECORD; + rec2 RECORD; +BEGIN + + tables := '{}'; + + FOR rec IN SELECT CDB_QueryStatements(query) q LOOP + + IF NOT ( rec.q ilike 'select %' or rec.q ilike 'with %' ) THEN + --RAISE WARNING 'Skipping %', rec.q; + CONTINUE; + END IF; + + BEGIN + EXECUTE 'EXPLAIN (FORMAT XML, VERBOSE) ' || rec.q INTO STRICT exp; + EXCEPTION WHEN others THEN + -- TODO: if error is 'relation "xxxxxx" does not exist', take xxxxxx as + -- the affected table ? + RAISE WARNING 'CDB_QueryTables cannot explain query: % (%: %)', rec.q, SQLSTATE, SQLERRM; + RAISE EXCEPTION '%', SQLERRM; + CONTINUE; + END; + + -- Now need to extract all values of + + -- RAISE DEBUG 'Explain: %', exp; + + FOR rec2 IN WITH + inp AS ( + SELECT + xpath('//x:Relation-Name/text()', exp, ARRAY[ARRAY['x', 'http://www.postgresql.org/2009/explain']]) as x, + xpath('//x:Relation-Name/../x:Schema/text()', exp, ARRAY[ARRAY['x', 'http://www.postgresql.org/2009/explain']]) as s + ) + SELECT unnest(x)::name as p, unnest(s)::name as sc from inp + LOOP + -- RAISE DEBUG 'tab: %', rec2.p; + -- RAISE DEBUG 'sc: %', rec2.sc; + tables := array_append(tables, (rec2.sc || '.' || rec2.p)::name); + END LOOP; + + -- RAISE DEBUG 'Tables: %', tables; + + END LOOP; + + -- RAISE DEBUG 'Tables: %', tables; + + -- Remove duplicates and sort by name + IF array_upper(tables, 1) > 0 THEN + WITH dist as ( SELECT DISTINCT unnest(tables)::text as p ORDER BY p ) + SELECT array_agg(p) from dist into tables; + END IF; + + --RAISE DEBUG 'Tables: %', tables; + + return tables; +END +$$ LANGUAGE 'plpgsql' VOLATILE STRICT; diff --git a/test/support/sql/windshaft.test.sql b/test/support/sql/windshaft.test.sql index 4ae82bfe..53a7f04b 100644 --- a/test/support/sql/windshaft.test.sql +++ b/test/support/sql/windshaft.test.sql @@ -177,3 +177,12 @@ CREATE TABLE test_table_private_1 ( INSERT INTO test_table_private_1 SELECT * from test_table; GRANT ALL ON TABLE test_table_private_1 TO :TESTUSER; + +CREATE TABLE IF NOT EXISTS + CDB_TableMetadata ( + tabname regclass not null primary key, + updated_at timestamp with time zone not null default now() + ); + +GRANT SELECT ON CDB_TableMetadata TO :PUBLICUSER; +GRANT SELECT ON CDB_TableMetadata TO :TESTUSER; From fbecc11aa5434ef00851fb87fe99c348044c3824 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Wed, 4 Feb 2015 19:01:14 +0100 Subject: [PATCH 20/60] Do not use the SQL API Emulator for testing named layers as it hides an integration issue with cdb_querytables --- test/acceptance/named_layers.js | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/test/acceptance/named_layers.js b/test/acceptance/named_layers.js index ec04afbf..2d4a918c 100644 --- a/test/acceptance/named_layers.js +++ b/test/acceptance/named_layers.js @@ -5,9 +5,6 @@ var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')(); var server = new CartodbWindshaft(serverOptions); -var SQLAPIEmulator = require('../support/SQLAPIEmu.js'); - - var RedisPool = require('redis-mpool'); var TemplateMaps = require('../../lib/cartodb/template_maps.js'); @@ -15,8 +12,6 @@ var Step = require('step'); var _ = require('underscore'); suite('named_layers', function() { - var sqlapiServer; - // configure redis pool instance to use in tests var redisPool = RedisPool(global.environment.redis); @@ -101,6 +96,7 @@ suite('named_layers', function() { }; suiteSetup(function(done) { + global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: true}; templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) { if (err) { return done(err); @@ -110,10 +106,7 @@ suite('named_layers', function() { return done(err); } templateMaps.addTemplate(username, template, function(err) { - if (err) { - return done(err); - } - sqlapiServer = new SQLAPIEmulator(global.environment.sqlapi.port, done); + return done(err); }); }); }); @@ -509,6 +502,7 @@ suite('named_layers', function() { suiteTeardown(function(done) { + global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: false}; templateMaps.delTemplate(username, nestedNamedMapTemplateName, function(err) { if (err) { return done(err); @@ -518,10 +512,7 @@ suite('named_layers', function() { return done(err); } templateMaps.delTemplate(username, templateName, function(err) { - if (err) { - return done(err); - } - sqlapiServer.close(done); + return done(err); }); }); }); From c17af23a4068e86ba7086aa681b6b1de268bfbd9 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Wed, 4 Feb 2015 19:31:20 +0100 Subject: [PATCH 21/60] A non empty datasource from MapConfigNamedLayersAdapter.getLayers means the affected tables can have private tables involved. That implies QueryTablesApi will need the proper user to use CDB_QueryTables. So we store it in a request context to use it in the afterLayergroupCreate call. Tiles for these layergroups will fail to add a X-Cache-Channel header because it won't be possible to use the proper user within those tiles. Ok, they will fail if they are not requested through the same tiler instance because if they are they most likely will reuse the in memory cache. See https://github.com/CartoDB/Windshaft-cartodb/issues/253 --- lib/cartodb/server_options.js | 43 +++++++++++++++++++++++++++++++-- test/acceptance/named_layers.js | 9 ++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index 948349ba..43f33bba 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -312,6 +312,12 @@ module.exports = function(redisPool) { return callback(err); } + if (!datasource.isEmpty()) { + setContext(req, 'queryTablesApiDatasource', _.find(datasource.layersDbParams, function(layerDbParams) { + return !!layerDbParams; + })); + } + requestMapConfig.layers = layers; return callback(null, requestMapConfig, datasource) }); @@ -366,14 +372,16 @@ module.exports = function(redisPool) { Step( function getAffectedTablesAndLastUpdatedTime() { - queryTablesApi.getAffectedTablesAndLastUpdatedTime(usr, { + var queryTablesOpts = { user: req.params.dbuser, pass: req.params.dbpass, host: req.params.dbhost, port: req.params.dbport, dbname: req.params.dbname, api_key: key - }, sql, this); + }; + _.extend(queryTablesOpts, getContext(req, 'queryTablesApiDatasource')); + queryTablesApi.getAffectedTablesAndLastUpdatedTime(usr, queryTablesOpts, sql, this); }, function handleAffectedTablesAndLastUpdatedTime(err, result) { if (req.profiler) req.profiler.done('queryTablesAndLastUpdated'); @@ -873,5 +881,36 @@ module.exports = function(redisPool) { ); }; + /******************************************************************************************************************* + * Private methods + ******************************************************************************************************************/ + + /** + * Handles context for a given Request object + * @param {Object|IncomingMessage} req + * @param {String} key + * @returns {*} + */ + function getContext(req, key) { + return req.context && req.context[key]; + } + + /** + * Handles context for a given Request object + * @param {Object|IncomingMessage} req + * @param {String} key + * @param {*} value + * @returns {*} The previous value + */ + function setContext(req, key, value) { + var previousValue; + if (value) { + req.context = req.context || {}; + previousValue = req.context[key]; + req.context[key] = value; + } + return previousValue; + } + return me; }; diff --git a/test/acceptance/named_layers.js b/test/acceptance/named_layers.js index 2d4a918c..f1a67528 100644 --- a/test/acceptance/named_layers.js +++ b/test/acceptance/named_layers.js @@ -422,10 +422,17 @@ suite('named_layers', function() { } }, function(res, err) { - next(err); + next(err, res); } ); }, + function handleTileResponse(err, res) { + if (err) { + throw err; + } + //assert.ok(res.headers['X-Cache-Channel']); -> https://github.com/CartoDB/Windshaft-cartodb/issues/253 + return true; + }, function deleteTemplate(err) { var next = this; templateMaps.delTemplate(username, privateTableTemplate, function(/*delErr*/) { From 673bd4f3f2c0f6394f7366b849b92866b23c033d Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Wed, 4 Feb 2015 18:52:37 +0100 Subject: [PATCH 22/60] Add querytables and cdb_tablemetadata for proper testing its integration --- test/support/prepare_db.sh | 5 ++ test/support/sql/CDB_QueryStatements.sql | 14 +++++ test/support/sql/CDB_QueryTables.sql | 67 ++++++++++++++++++++++++ test/support/sql/windshaft.test.sql | 9 ++++ 4 files changed, 95 insertions(+) create mode 100644 test/support/sql/CDB_QueryStatements.sql create mode 100644 test/support/sql/CDB_QueryTables.sql diff --git a/test/support/prepare_db.sh b/test/support/prepare_db.sh index 8ec0c08a..991b52f9 100755 --- a/test/support/prepare_db.sh +++ b/test/support/prepare_db.sh @@ -78,6 +78,11 @@ if test x"$PREPARE_PGSQL" = xyes; then sed "s/:TESTPASS/${TESTPASS}/" | psql -v ON_ERROR_STOP=1 ${TEST_DB} || exit 1 + curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/cdb/scripts-available/CDB_QueryStatements.sql -o sql/CDB_QueryStatements.sql + curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/cdb/scripts-available/CDB_QueryTables.sql -o sql/CDB_QueryTables.sql + cat sql/CDB_QueryStatements.sql sql/CDB_QueryTables.sql | + psql -v ON_ERROR_STOP=1 ${TEST_DB} || exit 1 + fi if test x"$PREPARE_REDIS" = xyes; then diff --git a/test/support/sql/CDB_QueryStatements.sql b/test/support/sql/CDB_QueryStatements.sql new file mode 100644 index 00000000..bb4d10cf --- /dev/null +++ b/test/support/sql/CDB_QueryStatements.sql @@ -0,0 +1,14 @@ +-- Return an array of statements found in the given query text +-- +-- Regexp curtesy of Hubert Lubaczewski (depesz) +-- Implemented in plpython for performance reasons +-- +CREATE OR REPLACE FUNCTION CDB_QueryStatements(query text) +RETURNS SETOF TEXT AS $$ + import re + pat = re.compile( r'''((?:[^'"$;]+|"[^"]*"|'[^']*'|(\$[^$]*\$).*?\2)+)''', re.DOTALL ) + for match in pat.findall(query): + cleaned = match[0].strip() + if ( cleaned ): + yield cleaned +$$ language 'plpythonu' IMMUTABLE STRICT; diff --git a/test/support/sql/CDB_QueryTables.sql b/test/support/sql/CDB_QueryTables.sql new file mode 100644 index 00000000..2fb9532a --- /dev/null +++ b/test/support/sql/CDB_QueryTables.sql @@ -0,0 +1,67 @@ +-- Return an array of table names scanned by a given query +-- +-- Requires PostgreSQL 9.x+ +-- +CREATE OR REPLACE FUNCTION CDB_QueryTables(query text) +RETURNS name[] +AS $$ +DECLARE + exp XML; + tables NAME[]; + rec RECORD; + rec2 RECORD; +BEGIN + + tables := '{}'; + + FOR rec IN SELECT CDB_QueryStatements(query) q LOOP + + IF NOT ( rec.q ilike 'select %' or rec.q ilike 'with %' ) THEN + --RAISE WARNING 'Skipping %', rec.q; + CONTINUE; + END IF; + + BEGIN + EXECUTE 'EXPLAIN (FORMAT XML, VERBOSE) ' || rec.q INTO STRICT exp; + EXCEPTION WHEN others THEN + -- TODO: if error is 'relation "xxxxxx" does not exist', take xxxxxx as + -- the affected table ? + RAISE WARNING 'CDB_QueryTables cannot explain query: % (%: %)', rec.q, SQLSTATE, SQLERRM; + RAISE EXCEPTION '%', SQLERRM; + CONTINUE; + END; + + -- Now need to extract all values of + + -- RAISE DEBUG 'Explain: %', exp; + + FOR rec2 IN WITH + inp AS ( + SELECT + xpath('//x:Relation-Name/text()', exp, ARRAY[ARRAY['x', 'http://www.postgresql.org/2009/explain']]) as x, + xpath('//x:Relation-Name/../x:Schema/text()', exp, ARRAY[ARRAY['x', 'http://www.postgresql.org/2009/explain']]) as s + ) + SELECT unnest(x)::name as p, unnest(s)::name as sc from inp + LOOP + -- RAISE DEBUG 'tab: %', rec2.p; + -- RAISE DEBUG 'sc: %', rec2.sc; + tables := array_append(tables, (rec2.sc || '.' || rec2.p)::name); + END LOOP; + + -- RAISE DEBUG 'Tables: %', tables; + + END LOOP; + + -- RAISE DEBUG 'Tables: %', tables; + + -- Remove duplicates and sort by name + IF array_upper(tables, 1) > 0 THEN + WITH dist as ( SELECT DISTINCT unnest(tables)::text as p ORDER BY p ) + SELECT array_agg(p) from dist into tables; + END IF; + + --RAISE DEBUG 'Tables: %', tables; + + return tables; +END +$$ LANGUAGE 'plpgsql' VOLATILE STRICT; diff --git a/test/support/sql/windshaft.test.sql b/test/support/sql/windshaft.test.sql index 4ae82bfe..53a7f04b 100644 --- a/test/support/sql/windshaft.test.sql +++ b/test/support/sql/windshaft.test.sql @@ -177,3 +177,12 @@ CREATE TABLE test_table_private_1 ( INSERT INTO test_table_private_1 SELECT * from test_table; GRANT ALL ON TABLE test_table_private_1 TO :TESTUSER; + +CREATE TABLE IF NOT EXISTS + CDB_TableMetadata ( + tabname regclass not null primary key, + updated_at timestamp with time zone not null default now() + ); + +GRANT SELECT ON CDB_TableMetadata TO :PUBLICUSER; +GRANT SELECT ON CDB_TableMetadata TO :TESTUSER; From 547782eea5ef84de62a529ccf4c96943f19db0f3 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Fri, 6 Feb 2015 12:03:17 +0100 Subject: [PATCH 23/60] Use windshaft's plain-renderer-plus-http-default-plus-per-layer-datasource branch to pick latest developments for medusa --- npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index c4fe8e44..603e8df2 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -184,8 +184,8 @@ }, "windshaft": { "version": "0.35.2", - "from": "https://github.com/CartoDB/Windshaft/tarball/mapconfig-per-layer-datasource", - "resolved": "https://github.com/CartoDB/Windshaft/tarball/mapconfig-per-layer-datasource", + "from": "https://github.com/CartoDB/Windshaft/tarball/plain-renderer-plus-http-default-plus-per-layer-datasource", + "resolved": "https://github.com/CartoDB/Windshaft/tarball/plain-renderer-plus-http-default-plus-per-layer-datasource", "dependencies": { "chronograph": { "version": "0.1.0", diff --git a/package.json b/package.json index 5eb6f11c..762d84df 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "node-varnish": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0", "underscore" : "~1.6.0", "dot": "~1.0.2", - "windshaft": "https://github.com/CartoDB/Windshaft/tarball/mapconfig-per-layer-datasource", + "windshaft": "https://github.com/CartoDB/Windshaft/tarball/plain-renderer-plus-http-default-plus-per-layer-datasource", "step": "~0.0.5", "queue-async": "~1.0.7", "request": "~2.9.203", From e1e925bd9e5a42579eda585a075d7d834416bdc1 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 9 Feb 2015 14:33:17 +0100 Subject: [PATCH 24/60] Run postgresql/sql-api dependant tests against two implementations This time for real. --- test/acceptance/multilayer.js | 3 +-- test/acceptance/server.js | 8 +++----- test/acceptance/templates.js | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/test/acceptance/multilayer.js b/test/acceptance/multilayer.js index aff420d4..3b459ce6 100644 --- a/test/acceptance/multilayer.js +++ b/test/acceptance/multilayer.js @@ -24,8 +24,6 @@ server.setMaxListeners(0); [true, false].forEach(function(cdbQueryTablesFromPostgresEnabledValue) { -global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: cdbQueryTablesFromPostgresEnabledValue}; - suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function() { var redis_client = redis.createClient(global.environment.redis.port); @@ -38,6 +36,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function( var test_database = test_user + '_db'; suiteSetup(function(done){ + global.environment.enabledFeatures = { cdbQueryTablesFromPostgres: cdbQueryTablesFromPostgresEnabledValue }; sqlapi_server = new SQLAPIEmu(global.environment.sqlapi.port, done); }); diff --git a/test/acceptance/server.js b/test/acceptance/server.js index a2e2324e..71a8ba58 100644 --- a/test/acceptance/server.js +++ b/test/acceptance/server.js @@ -20,10 +20,7 @@ server.setMaxListeners(0); [true, false].forEach(function(cdbQueryTablesFromPostgresEnabledValue) { -global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: cdbQueryTablesFromPostgresEnabledValue}; - -suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function() { - +suite('server:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function() { var redis_client = redis.createClient(global.environment.redis.port); var sqlapi_server; @@ -47,8 +44,9 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function( // A couple of styles to use during testing var test_style_black_200 = "#test_table{marker-fill:black;marker-line-color:red;marker-width:10}"; var test_style_black_210 = "#test_table{marker-fill:black;marker-line-color:red;marker-width:20}"; - + suiteSetup(function(done){ + global.environment.enabledFeatures = { cdbQueryTablesFromPostgres: cdbQueryTablesFromPostgresEnabledValue }; sqlapi_server = new SQLAPIEmu(global.environment.sqlapi.port, done); }); diff --git a/test/acceptance/templates.js b/test/acceptance/templates.js index 46c872e2..f0df7414 100644 --- a/test/acceptance/templates.js +++ b/test/acceptance/templates.js @@ -27,9 +27,8 @@ server.setMaxListeners(0); [true, false].forEach(function(cdbQueryTablesFromPostgresEnabledValue) { -global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: cdbQueryTablesFromPostgresEnabledValue}; - suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function() { + serverOptions.channelCache = {}; var redis_client = redis.createClient(global.environment.redis.port); var sqlapi_server; @@ -37,6 +36,7 @@ suite('template_api:postgres=' + cdbQueryTablesFromPostgresEnabledValue, functio var expected_last_updated = new Date(expected_last_updated_epoch).toISOString(); suiteSetup(function(done){ + global.environment.enabledFeatures = { cdbQueryTablesFromPostgres: cdbQueryTablesFromPostgresEnabledValue }; sqlapi_server = new SQLAPIEmu(global.environment.sqlapi.port, done); // TODO: check redis is clean ? }); From d40b15454b86028af273429e35c985db1e28623a Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 9 Feb 2015 14:38:29 +0100 Subject: [PATCH 25/60] Run some tests only if they are using the SQL API --- test/acceptance/multilayer.js | 84 ++++++++++++++++++++--------------- test/acceptance/server.js | 14 ++++-- 2 files changed, 57 insertions(+), 41 deletions(-) diff --git a/test/acceptance/multilayer.js b/test/acceptance/multilayer.js index 3b459ce6..84b1e220 100644 --- a/test/acceptance/multilayer.js +++ b/test/acceptance/multilayer.js @@ -111,15 +111,17 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function( assert.ok(cc); var dbname = test_database; assert.equal(cc.substring(0, dbname.length), dbname); - var jsonquery = cc.substring(dbname.length+1); - var sentquery = JSON.parse(jsonquery); - var expectedQuery = [layergroup.layers[0].options.sql, ';', layergroup.layers[1].options.sql].join(''); - assert.equal(sentquery.q, 'WITH querytables AS ( SELECT * FROM CDB_QueryTables($windshaft$' - + expectedQuery - + '$windshaft$) as tablenames )' - + ' SELECT (SELECT tablenames FROM querytables), EXTRACT(EPOCH FROM max(updated_at)) as max' - + ' FROM CDB_TableMetadata m' - + ' WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])'); + if (!cdbQueryTablesFromPostgresEnabledValue) { // only test if it was using the SQL API + var jsonquery = cc.substring(dbname.length + 1); + var sentquery = JSON.parse(jsonquery); + var expectedQuery = [layergroup.layers[0].options.sql, ';', layergroup.layers[1].options.sql].join(''); + assert.equal(sentquery.q, 'WITH querytables AS ( SELECT * FROM CDB_QueryTables($windshaft$' + + expectedQuery + + '$windshaft$) as tablenames )' + + ' SELECT (SELECT tablenames FROM querytables), EXTRACT(EPOCH FROM max(updated_at)) as max' + + ' FROM CDB_TableMetadata m' + + ' WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])'); + } assert.imageEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.png', IMAGE_EQUALS_HIGHER_TOLERANCE_PER_MIL, function(err, similarity) { @@ -336,7 +338,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function( layers: [ { options: { sql: 'select 1 as cartodb_id, ' - + 'ST_Buffer(!bbox!, -32*greatest(!pixel_width!,!pixel_height!)) as the_geom_webmercator', + + 'ST_Buffer(!bbox!, -32*greatest(!pixel_width!,!pixel_height!)) as the_geom_webmercator from test_table limit 1', cartocss: '#layer { polygon-fill:red; }', cartocss_version: '2.0.1', interactivity: 'cartodb_id' @@ -387,21 +389,23 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function( // Check X-Cache-Channel var cc = res.headers['x-cache-channel']; - assert.ok(cc); + assert.ok(cc); var dbname = test_database; assert.equal(cc.substring(0, dbname.length), dbname); - var jsonquery = cc.substring(dbname.length+1); - var sentquery = JSON.parse(jsonquery); - var expectedQuery = layergroup.layers[0].options.sql - .replace(/!bbox!/g, 'ST_MakeEnvelope(0,0,0,0)') - .replace(/!pixel_width!/g, '1') - .replace(/!pixel_height!/g, '1'); - assert.equal(sentquery.q, 'WITH querytables AS ( SELECT * FROM CDB_QueryTables($windshaft$' - + expectedQuery - + '$windshaft$) as tablenames )' - + ' SELECT (SELECT tablenames FROM querytables), EXTRACT(EPOCH FROM max(updated_at)) as max' - + ' FROM CDB_TableMetadata m' - + ' WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])'); + if (!cdbQueryTablesFromPostgresEnabledValue) { // only test if it was using the SQL API + var jsonquery = cc.substring(dbname.length + 1); + var sentquery = JSON.parse(jsonquery); + var expectedQuery = layergroup.layers[0].options.sql + .replace(/!bbox!/g, 'ST_MakeEnvelope(0,0,0,0)') + .replace(/!pixel_width!/g, '1') + .replace(/!pixel_height!/g, '1'); + assert.equal(sentquery.q, 'WITH querytables AS ( SELECT * FROM CDB_QueryTables($windshaft$' + + expectedQuery + + '$windshaft$) as tablenames )' + + ' SELECT (SELECT tablenames FROM querytables), EXTRACT(EPOCH FROM max(updated_at)) as max' + + ' FROM CDB_TableMetadata m' + + ' WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])'); + } assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err, similarity) { @@ -427,18 +431,20 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function( assert.ok(cc); var dbname = test_database; assert.equal(cc.substring(0, dbname.length), dbname); - var jsonquery = cc.substring(dbname.length+1); - var sentquery = JSON.parse(jsonquery); - var expectedQuery = layergroup.layers[0].options.sql - .replace('!bbox!', 'ST_MakeEnvelope(0,0,0,0)') - .replace('!pixel_width!', '1') - .replace('!pixel_height!', '1'); - assert.equal(sentquery.q, 'WITH querytables AS ( SELECT * FROM CDB_QueryTables($windshaft$' - + expectedQuery - + '$windshaft$) as tablenames )' - + ' SELECT (SELECT tablenames FROM querytables), EXTRACT(EPOCH FROM max(updated_at)) as max' - + ' FROM CDB_TableMetadata m' - + ' WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])'); + if (!cdbQueryTablesFromPostgresEnabledValue) { // only test if it was using the SQL API + var jsonquery = cc.substring(dbname.length + 1); + var sentquery = JSON.parse(jsonquery); + var expectedQuery = layergroup.layers[0].options.sql + .replace('!bbox!', 'ST_MakeEnvelope(0,0,0,0)') + .replace('!pixel_width!', '1') + .replace('!pixel_height!', '1'); + assert.equal(sentquery.q, 'WITH querytables AS ( SELECT * FROM CDB_QueryTables($windshaft$' + + expectedQuery + + '$windshaft$) as tablenames )' + + ' SELECT (SELECT tablenames FROM querytables), EXTRACT(EPOCH FROM max(updated_at)) as max' + + ' FROM CDB_TableMetadata m' + + ' WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])'); + } assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err, similarity) { @@ -1217,8 +1223,10 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function( var parsedBody = JSON.parse(res.body); var token_components = parsedBody.layergroupid.split(':'); expected_token = token_components[0]; - var last_request = sqlapi_server.getLastRequest(); - assert.equal(last_request.method, 'POST'); + if (!cdbQueryTablesFromPostgresEnabledValue) { // only test if it was using the SQL API + var last_request = sqlapi_server.getLastRequest(); + assert.equal(last_request.method, 'POST'); + } return null; }, function cleanup(err) { @@ -1282,6 +1290,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, 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) { @@ -1322,6 +1331,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function( } ); }); + } var layergroupTtlRequest = { url: '/tiles/layergroup?config=' + encodeURIComponent(JSON.stringify({ diff --git a/test/acceptance/server.js b/test/acceptance/server.js index 71a8ba58..9a8b2519 100644 --- a/test/acceptance/server.js +++ b/test/acceptance/server.js @@ -1150,10 +1150,12 @@ suite('server:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function() { assert(cc, 'Missing X-Cache-Channel'); var dbname = test_database; assert.equal(cc.substring(0, dbname.length), dbname); - var jsonquery = cc.substring(dbname.length+1); - var sentquery = JSON.parse(jsonquery); - assert.equal(sentquery.api_key, qo.map_key); - assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$' + qo.sql + '$windshaft$)'); + if (!cdbQueryTablesFromPostgresEnabledValue) { // only test if it was using the SQL API + var jsonquery = cc.substring(dbname.length + 1); + var sentquery = JSON.parse(jsonquery); + assert.equal(sentquery.api_key, qo.map_key); + assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$' + qo.sql + '$windshaft$)'); + } return null; }, function finish(err) { @@ -1162,6 +1164,7 @@ suite('server:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function() { ); }); + if (!cdbQueryTablesFromPostgresEnabledValue) { // only test if it was a post if it's using the SQL API test("passes hostname header to sqlapi", function(done){ var qo = { sql: "SELECT * from gadm4", @@ -1193,7 +1196,9 @@ suite('server:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function() { } ); }); + } + if (!cdbQueryTablesFromPostgresEnabledValue) { // only test if it was using the SQL API test("requests to skip cache on sqlapi error", function(done){ var qo = { sql: "SELECT g.cartodb_id, g.codineprov, t.the_geom_webmercator " @@ -1226,6 +1231,7 @@ suite('server:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function() { } ); }); + } // Zoom is a special variable test("Specifying zoom level in CartoCSS does not need a 'zoom' variable in SQL output", function(done){ From 04af57cab9b10590d4db17b70c763243af001b3d Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 9 Feb 2015 14:38:59 +0100 Subject: [PATCH 26/60] Add some entries to cdb_tablemetadata for tables being used in tests --- test/support/sql/windshaft.test.sql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/support/sql/windshaft.test.sql b/test/support/sql/windshaft.test.sql index 53a7f04b..3ec0071c 100644 --- a/test/support/sql/windshaft.test.sql +++ b/test/support/sql/windshaft.test.sql @@ -184,5 +184,8 @@ CREATE TABLE IF NOT EXISTS updated_at timestamp with time zone not null default now() ); +INSERT INTO CDB_TableMetadata (tabname, updated_at) VALUES ('test_table'::regclass, '2009-02-13T23:31:30.123Z'); +INSERT INTO CDB_TableMetadata (tabname, updated_at) VALUES ('test_table_private_1'::regclass, '2009-02-13T23:31:30.123Z'); + GRANT SELECT ON CDB_TableMetadata TO :PUBLICUSER; GRANT SELECT ON CDB_TableMetadata TO :TESTUSER; From 90b22b271824a834f472e94ced5177b40256dd69 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 9 Feb 2015 14:46:52 +0100 Subject: [PATCH 27/60] QueryTables and last updated_at retrieved with user Move setDBAuth and setDBConn to PgConnection entity - It uses cartodb-redis to retrieve datasource configuration Start using it in ServerOptions, TemplateMaps and QueryTablesApi QueryTablesApi don't receive anymore the connection/credentials - It will always use an authenticated query to retrieve last update - That will allow to query affected private tables last update --- lib/cartodb/api/query_tables_api.js | 79 ++++++++++---- lib/cartodb/backends/pg_connection.js | 96 +++++++++++++++++ lib/cartodb/cartodb_windshaft.js | 9 +- lib/cartodb/controllers/template_maps.js | 7 +- lib/cartodb/server_options.js | 128 +++-------------------- 5 files changed, 183 insertions(+), 136 deletions(-) create mode 100644 lib/cartodb/backends/pg_connection.js diff --git a/lib/cartodb/api/query_tables_api.js b/lib/cartodb/api/query_tables_api.js index bdcf488e..f4795b05 100644 --- a/lib/cartodb/api/query_tables_api.js +++ b/lib/cartodb/api/query_tables_api.js @@ -1,7 +1,10 @@ -var sqlApi = require('../sql/sql_api'), - PSQL = require('cartodb-psql'); +var sqlApi = require('../sql/sql_api'); +var PSQL = require('cartodb-psql'); +var Step = require('step'); -function QueryTablesApi() { +function QueryTablesApi(pgConnection, metadataBackend) { + this.pgConnection = pgConnection; + this.metadataBackend = metadataBackend; } var affectedTableRegexCache = { @@ -14,11 +17,11 @@ var affectedTableRegexCache = { module.exports = QueryTablesApi; -QueryTablesApi.prototype.getAffectedTablesInQuery = function (username, options, sql, callback) { +QueryTablesApi.prototype.getAffectedTablesInQuery = function (username, sql, callback) { var query = 'SELECT CDB_QueryTables($windshaft$' + prepareSql(sql) + '$windshaft$)'; - runQuery(username, options, query, handleAffectedTablesInQueryRows, callback); + this.runQuery(username, query, handleAffectedTablesInQueryRows, callback); }; function handleAffectedTablesInQueryRows(err, rows, callback) { @@ -33,7 +36,7 @@ function handleAffectedTablesInQueryRows(err, rows, callback) { callback(null, tableNames); } -QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (username, options, sql, callback) { +QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (username, sql, callback) { var query = [ 'WITH querytables AS (', @@ -44,7 +47,7 @@ QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (usernam 'WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])' ].join(' '); - runQuery(username, options, query, handleAffectedTablesAndLastUpdatedTimeRows, callback); + this.runQuery(username, query, handleAffectedTablesAndLastUpdatedTimeRows, callback); }; function handleAffectedTablesAndLastUpdatedTimeRows(err, rows, callback) { @@ -68,20 +71,60 @@ function handleAffectedTablesAndLastUpdatedTimeRows(err, rows, callback) { } -function runQuery(username, options, query, queryHandler, callback) { +QueryTablesApi.prototype.runQuery = function(username, query, queryHandler, callback) { + var self = this; + if (shouldQueryPostgresDirectly()) { - var psql = new PSQL(options); - psql.query(query, function(err, resultSet) { - resultSet = resultSet || {}; - var rows = resultSet.rows || []; - queryHandler(err, rows, callback); - }); + + var params = {}; + + Step( + function setAuth() { + self.pgConnection.setDBAuth(username, params, this); + }, + function setConn(err) { + if (err) { + throw err; + } + self.pgConnection.setDBConn(username, params, this); + }, + function executeQuery(err) { + if (err) { + throw err; + } + var psql = new PSQL({ + user: params.dbuser, + pass: params.dbpass, + host: params.dbhost, + port: params.dbport, + dbname: params.dbname + }); + psql.query(query, function(err, resultSet) { + resultSet = resultSet || {}; + var rows = resultSet.rows || []; + queryHandler(err, rows, callback); + }); + } + ); + } else { - sqlApi.query(username, options.api_key, query, function(err, rows) { - queryHandler(err, rows, callback); - }); + + Step( + function getApiKey() { + self.metadataBackend.getUserMapKey(username, this); + }, + function executeQuery(err, apiKey) { + if (err) { + throw err; + } + sqlApi.query(username, apiKey, query, function(err, rows) { + queryHandler(err, rows, callback); + }); + } + ); + } -} +}; function prepareSql(sql) { diff --git a/lib/cartodb/backends/pg_connection.js b/lib/cartodb/backends/pg_connection.js new file mode 100644 index 00000000..37209b98 --- /dev/null +++ b/lib/cartodb/backends/pg_connection.js @@ -0,0 +1,96 @@ +var Step = require('step'); +var _ = require('underscore'); + +function PgConnection(metadataBackend) { + this.metadataBackend = metadataBackend; +} + +module.exports = PgConnection; + + +// Set db authentication parameters to those of the given username +// +// @param username the cartodb username, mapped to a database username +// via CartodbRedis metadata records +// +// @param params the parameters to set auth options into +// added params are: "dbuser" and "dbpassword" +// +// @param callback function(err) +// +PgConnection.prototype.setDBAuth = function(username, params, callback) { + var self = this; + + var user_params = {}; + var auth_user = global.environment.postgres_auth_user; + var auth_pass = global.environment.postgres_auth_pass; + Step( + function getId() { + self.metadataBackend.getUserId(username, this); + }, + function(err, user_id) { + if (err) throw 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; + + self.metadataBackend.getUserDBPass(username, this); + }, + function(err, user_password) { + if (err) throw err; + user_params['user_password'] = user_password; + if ( auth_pass ) { + var dbpass = _.template(auth_pass, user_params); + _.extend(params, {dbpassword:dbpass}); + } + return true; + }, + function finish(err) { + callback(err); + } + ); +}; + +// Set db connection parameters to those for the given username +// +// @param dbowner cartodb username of database owner, +// mapped to a database username +// via CartodbRedis metadata records +// +// @param params the parameters to set connection options into +// added params are: "dbname", "dbhost" +// +// @param callback function(err) +// +PgConnection.prototype.setDBConn = function(dbowner, params, callback) { + var self = this; + // Add default database connection parameters + // if none given + _.defaults(params, { + dbuser: global.environment.postgres.user, + dbpassword: global.environment.postgres.password, + dbhost: global.environment.postgres.host, + dbport: global.environment.postgres.port + }); + Step( + function getConnectionParams() { + self.metadataBackend.getUserDBConnectionParams(dbowner, this); + }, + function extendParams(err, dbParams){ + if (err) throw err; + // we don't want null values or overwrite a non public user + if (params.dbuser != 'publicuser' || !dbParams.dbuser) { + delete dbParams.dbuser; + } + if ( dbParams ) _.extend(params, dbParams); + return null; + }, + function finish(err) { + callback(err); + } + ); +}; diff --git a/lib/cartodb/cartodb_windshaft.js b/lib/cartodb/cartodb_windshaft.js index a5a01620..22bbb074 100644 --- a/lib/cartodb/cartodb_windshaft.js +++ b/lib/cartodb/cartodb_windshaft.js @@ -158,7 +158,14 @@ var CartodbWindshaft = function(serverOptions) { var TemplateMapsController = require('./controllers/template_maps'), templateMapsController = new TemplateMapsController( - ws, serverOptions, templateMaps, cartoData, template_baseurl, surrogateKeysCache, NamedMapsCacheEntry + ws, + serverOptions, + templateMaps, + cartoData, + template_baseurl, + surrogateKeysCache, + NamedMapsCacheEntry, + serverOptions.pgConnection ); templateMapsController.register(ws); diff --git a/lib/cartodb/controllers/template_maps.js b/lib/cartodb/controllers/template_maps.js index db525c7c..5b61ae56 100644 --- a/lib/cartodb/controllers/template_maps.js +++ b/lib/cartodb/controllers/template_maps.js @@ -2,7 +2,7 @@ var Step = require('step'); var _ = require('underscore'); function TemplateMapsController(app, serverOptions, templateMaps, metadataBackend, templateBaseUrl, surrogateKeysCache, - NamedMapsCacheEntry) { + NamedMapsCacheEntry, pgConnection) { this.app = app; this.serverOptions = serverOptions; this.templateMaps = templateMaps; @@ -10,6 +10,7 @@ function TemplateMapsController(app, serverOptions, templateMaps, metadataBacken this.templateBaseUrl = templateBaseUrl; this.surrogateKeysCache = surrogateKeysCache; this.NamedMapsCacheEntry = NamedMapsCacheEntry; + this.pgConnection = pgConnection; } module.exports = TemplateMapsController; @@ -465,11 +466,11 @@ TemplateMapsController.prototype.setDBParams = function(cdbuser, params, callbac var self = this; Step( function setAuth() { - self.serverOptions.setDBAuth(cdbuser, params, this); + self.pgConnection.setDBAuth(cdbuser, params, this); }, function setConn(err) { if ( err ) throw err; - self.serverOptions.setDBConn(cdbuser, params, this); + self.pgConnection.setDBConn(cdbuser, params, this); }, function finish(err) { callback(err); diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index 59e9fc3f..ef53884c 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -1,6 +1,7 @@ var _ = require('underscore'); var Step = require('step'); var QueryTablesApi = require('./api/query_tables_api'); +var PgConnection = require('./backends/pg_connection'); var crypto = require('crypto'); var LZMA = require('lzma').LZMA; @@ -35,7 +36,8 @@ module.exports = function(redisPool) { var redisOpts = redisPool ? {pool: redisPool} : global.environment.redis; var cartoData = require('cartodb-redis')(redisOpts), lzmaWorker = new LZMA(), - queryTablesApi = new QueryTablesApi(); + pgConnection = new PgConnection(cartoData), + queryTablesApi = new QueryTablesApi(pgConnection, cartoData); var rendererConfig = _.defaults(global.environment.renderer || {}, { cache_ttl: 60000, // milliseconds @@ -102,6 +104,9 @@ module.exports = function(redisPool) { // Re-use redisPool me.redis.pool = redisPool; + // Re-use pgConnection + me.pgConnection = pgConnection; + /* This whole block is about generating X-Cache-Channel { */ // TODO: review lifetime of elements of this cache @@ -211,14 +216,7 @@ module.exports = function(redisPool) { if ( req.profiler ) req.profiler.done('getSignerMapKey'); key = data; } - queryTablesApi.getAffectedTablesInQuery(user, { - user: req.params.dbuser, - pass: req.params.dbpass, - host: req.params.dbhost, - port: req.params.dbport, - dbname: req.params.dbname, - api_key: key - }, sql, this); // in addCacheChannel + queryTablesApi.getAffectedTablesInQuery(user, sql, this); // in addCacheChannel }, function finish(err, data) { next(err,data); @@ -329,28 +327,16 @@ module.exports = function(redisPool) { done(); }); - var sql = []; - _.each(mapconfig.layers, function(lyr) { - sql.push(lyr.options.sql); - }); - sql = sql.join(';'); + var sql = mapconfig.layers.map(function(layer) { + return layer.options.sql; + }).join(';'); var dbName = req.params.dbname; - var usr = this.userByReq(req); - var key = req.params.map_key || req.params.api_key; - var cacheKey = dbName + ':' + token; Step( function getAffectedTablesAndLastUpdatedTime() { - queryTablesApi.getAffectedTablesAndLastUpdatedTime(usr, { - user: req.params.dbuser, - pass: req.params.dbpass, - host: req.params.dbhost, - port: req.params.dbport, - dbname: req.params.dbname, - api_key: key - }, sql, this); + queryTablesApi.getAffectedTablesAndLastUpdatedTime(username, sql, this); }, function handleAffectedTablesAndLastUpdatedTime(err, result) { if (req.profiler) req.profiler.done('queryTablesAndLastUpdated'); @@ -405,92 +391,6 @@ module.exports = function(redisPool) { return mat[1]; }; - // Set db authentication parameters to those of the given username - // - // @param username the cartodb username, mapped to a database username - // via CartodbRedis metadata records - // - // @param params the parameters to set auth options into - // added params are: "dbuser" and "dbpassword" - // - // @param callback function(err) - // - me.setDBAuth = function(username, params, callback) { - - var user_params = {}; - var auth_user = global.environment.postgres_auth_user; - var auth_pass = global.environment.postgres_auth_pass; - Step( - function getId() { - cartoData.getUserId(username, this); - }, - function(err, user_id) { - if (err) throw 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; - - cartoData.getUserDBPass(username, this); - }, - function(err, user_password) { - if (err) throw err; - user_params['user_password'] = user_password; - if ( auth_pass ) { - var dbpass = _.template(auth_pass, user_params); - _.extend(params, {dbpassword:dbpass}); - } - return true; - }, - function finish(err) { - callback(err); - } - ); - }; - - // Set db connection parameters to those for the given username - // - // @param dbowner cartodb username of database owner, - // mapped to a database username - // via CartodbRedis metadata records - // - // @param params the parameters to set connection options into - // added params are: "dbname", "dbhost" - // - // @param callback function(err) - // - me.setDBConn = function(dbowner, params, callback) { - // Add default database connection parameters - // if none given - _.defaults(params, { - dbuser: global.environment.postgres.user, - dbpassword: global.environment.postgres.password, - dbhost: global.environment.postgres.host, - dbport: global.environment.postgres.port - }); - Step( - function getConnectionParams() { - cartoData.getUserDBConnectionParams(dbowner, this); - }, - function extendParams(err, dbParams){ - if (err) throw err; - // we don't want null values or overwrite a non public user - if (params.dbuser != 'publicuser' || !dbParams.dbuser) { - delete dbParams.dbuser; - } - if ( dbParams ) _.extend(params, dbParams); - return null; - }, - function finish(err) { - callback(err); - } - ); - }; - - // Check if a request is authorized by a signer // // @param req express request object @@ -589,7 +489,7 @@ module.exports = function(redisPool) { _.extend(req.params, { _authorizedByApiKey: true }); // authorized by api key, login as the given username and stop - that.setDBAuth(user, req.params, function(err) { + pgConnection.setDBAuth(user, req.params, function(err) { callback(err, true); // authorized (or error) }); }, @@ -624,7 +524,7 @@ module.exports = function(redisPool) { // Authorized by "signed_by" ! _.extend(req.params, { _authorizedBySigner: signed_by }); - that.setDBAuth(signed_by, req.params, function(err) { + pgConnection.setDBAuth(signed_by, req.params, function(err) { if (req.profiler) req.profiler.done('setDBAuth'); callback(err, true); // authorized (or error) }); @@ -738,7 +638,7 @@ module.exports = function(redisPool) { }, function getDatabase(err){ if(err) throw err; - that.setDBConn(user, req.params, this); + pgConnection.setDBConn(user, req.params, this); }, function getGeometryType(err){ if (req.profiler) req.profiler.done('setDBConn'); From 81919706ea64ccfb473179cce2b8f3f7baca553c Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 9 Feb 2015 15:08:36 +0100 Subject: [PATCH 28/60] Adds default image placeholder for http renderer to use as fallback --- assets/default-placeholder.png | Bin 0 -> 10618 bytes assets/default-placeholder@2x.png | Bin 0 -> 25658 bytes config/environments/development.js.example | 8 +++++++- config/environments/production.js.example | 8 +++++++- config/environments/staging.js.example | 8 +++++++- config/environments/test.js.example | 8 +++++++- 6 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 assets/default-placeholder.png create mode 100644 assets/default-placeholder@2x.png diff --git a/assets/default-placeholder.png b/assets/default-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..de4079ef7940c9b9bb2cb92d1e28ce6bff4d37c1 GIT binary patch literal 10618 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 E0bB<2RsaA1 literal 0 HcmV?d00001 diff --git a/assets/default-placeholder@2x.png b/assets/default-placeholder@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a52d35135b62df8b74610baf39cb4255aedf5707 GIT binary patch literal 25658 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 WbaeH}`>qIqKkGyG77xvQ68{I(zZCxf literal 0 HcmV?d00001 diff --git a/config/environments/development.js.example b/config/environments/development.js.example index e28c51e6..2c391dd1 100644 --- a/config/environments/development.js.example +++ b/config/environments/development.js.example @@ -94,7 +94,13 @@ var config = { proxy: undefined, // the url for a proxy server whitelist: [ // the whitelist of urlTemplates that can be used 'http://{s}.example.com/{z}/{x}/{y}.png' - ] + ], + // image to use as placeholder when urlTemplate is not in the whitelist + // if provided the http renderer will use it instead of throw an error + fallbackImage: { + type: 'fs', // 'fs' and 'url' supported + src: __dirname + '/../../assets/default-placeholder.png' + } } } ,millstone: { diff --git a/config/environments/production.js.example b/config/environments/production.js.example index 6c4ea756..0d4e999a 100644 --- a/config/environments/production.js.example +++ b/config/environments/production.js.example @@ -88,7 +88,13 @@ var config = { proxy: undefined, // the url for a proxy server whitelist: [ // the whitelist of urlTemplates that can be used 'http://{s}.example.com/{z}/{x}/{y}.png' - ] + ], + // image to use as placeholder when urlTemplate is not in the whitelist + // if provided the http renderer will use it instead of throw an error + fallbackImage: { + type: 'fs', // 'fs' and 'url' supported + src: __dirname + '/../../assets/default-placeholder.png' + } } } ,millstone: { diff --git a/config/environments/staging.js.example b/config/environments/staging.js.example index f8da00db..0c88a9c7 100644 --- a/config/environments/staging.js.example +++ b/config/environments/staging.js.example @@ -88,7 +88,13 @@ var config = { proxy: undefined, // the url for a proxy server whitelist: [ // the whitelist of urlTemplates that can be used 'http://{s}.example.com/{z}/{x}/{y}.png' - ] + ], + // image to use as placeholder when urlTemplate is not in the whitelist + // if provided the http renderer will use it instead of throw an error + fallbackImage: { + type: 'fs', // 'fs' and 'url' supported + src: __dirname + '/../../assets/default-placeholder.png' + } } } ,millstone: { diff --git a/config/environments/test.js.example b/config/environments/test.js.example index 39bf1d15..14b78f61 100644 --- a/config/environments/test.js.example +++ b/config/environments/test.js.example @@ -90,7 +90,13 @@ var config = { 'http://{s}.example.com/{z}/{x}/{y}.png', // for testing purposes 'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png' - ] + ], + // image to use as placeholder when urlTemplate is not in the whitelist + // if provided the http renderer will use it instead of throw an error + fallbackImage: { + type: 'fs', // 'fs' and 'url' supported + src: __dirname + '/../../assets/default-placeholder.png' + } } } ,millstone: { From 40de1a8f8620fb9c7bc20ceb5e4b207132923b25 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 9 Feb 2015 18:34:28 +0100 Subject: [PATCH 29/60] Create CREATE EXTENSION plpythonu for tests --- .travis.yml | 1 + test/support/prepare_db.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2c24498f..e0170152 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ addons: before_install: - sudo apt-get install -y pkg-config libcairo2-dev libjpeg8-dev libgif-dev + - sudo apt-get install postgresql-plpython-9.3 - createdb template_postgis - psql -c "CREATE EXTENSION postgis" template_postgis diff --git a/test/support/prepare_db.sh b/test/support/prepare_db.sh index 991b52f9..8185da4d 100755 --- a/test/support/prepare_db.sh +++ b/test/support/prepare_db.sh @@ -78,6 +78,7 @@ if test x"$PREPARE_PGSQL" = xyes; then sed "s/:TESTPASS/${TESTPASS}/" | psql -v ON_ERROR_STOP=1 ${TEST_DB} || exit 1 + psql -c "CREATE EXTENSION plpythonu;" ${TEST_DB} curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/cdb/scripts-available/CDB_QueryStatements.sql -o sql/CDB_QueryStatements.sql curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/cdb/scripts-available/CDB_QueryTables.sql -o sql/CDB_QueryTables.sql cat sql/CDB_QueryStatements.sql sql/CDB_QueryTables.sql | From 6eec5822f0ee34032a856e4dc0d75794bd193c8d Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 9 Feb 2015 18:34:28 +0100 Subject: [PATCH 30/60] Create CREATE EXTENSION plpythonu for tests --- .travis.yml | 1 + test/support/prepare_db.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index ef58e4f9..77d9f91f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ addons: before_install: - sudo apt-get install -y pkg-config libcairo2-dev libjpeg8-dev libgif-dev + - sudo apt-get install postgresql-plpython-9.3 - createdb template_postgis - psql -c "CREATE EXTENSION postgis" template_postgis diff --git a/test/support/prepare_db.sh b/test/support/prepare_db.sh index 991b52f9..8185da4d 100755 --- a/test/support/prepare_db.sh +++ b/test/support/prepare_db.sh @@ -78,6 +78,7 @@ if test x"$PREPARE_PGSQL" = xyes; then sed "s/:TESTPASS/${TESTPASS}/" | psql -v ON_ERROR_STOP=1 ${TEST_DB} || exit 1 + psql -c "CREATE EXTENSION plpythonu;" ${TEST_DB} curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/cdb/scripts-available/CDB_QueryStatements.sql -o sql/CDB_QueryStatements.sql curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/cdb/scripts-available/CDB_QueryTables.sql -o sql/CDB_QueryTables.sql cat sql/CDB_QueryStatements.sql sql/CDB_QueryTables.sql | From 87638168ff851a17aea5d4ba1f4bccfc7299dfc9 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 9 Feb 2015 18:39:24 +0100 Subject: [PATCH 31/60] apt-get update before apt-get install --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 77d9f91f..bbfbe798 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ addons: postgresql: "9.3" before_install: + - sudo apt-get update - sudo apt-get install -y pkg-config libcairo2-dev libjpeg8-dev libgif-dev - sudo apt-get install postgresql-plpython-9.3 - createdb template_postgis From 8ed864ad18eee5e203bbfcfe4737dd2187e28d7e Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 9 Feb 2015 18:39:24 +0100 Subject: [PATCH 32/60] apt-get update before apt-get install --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 77d9f91f..bbfbe798 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ addons: postgresql: "9.3" before_install: + - sudo apt-get update - sudo apt-get install -y pkg-config libcairo2-dev libjpeg8-dev libgif-dev - sudo apt-get install postgresql-plpython-9.3 - createdb template_postgis From 742a9744ea883cce9a1c486991bc9b008fd3e24d Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 9 Feb 2015 18:56:01 +0100 Subject: [PATCH 33/60] Remove select permission for publicuser --- test/support/sql/windshaft.test.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/support/sql/windshaft.test.sql b/test/support/sql/windshaft.test.sql index 3ec0071c..e174d6b4 100644 --- a/test/support/sql/windshaft.test.sql +++ b/test/support/sql/windshaft.test.sql @@ -187,5 +187,5 @@ CREATE TABLE IF NOT EXISTS INSERT INTO CDB_TableMetadata (tabname, updated_at) VALUES ('test_table'::regclass, '2009-02-13T23:31:30.123Z'); INSERT INTO CDB_TableMetadata (tabname, updated_at) VALUES ('test_table_private_1'::regclass, '2009-02-13T23:31:30.123Z'); -GRANT SELECT ON CDB_TableMetadata TO :PUBLICUSER; +-- GRANT SELECT ON CDB_TableMetadata TO :PUBLICUSER; GRANT SELECT ON CDB_TableMetadata TO :TESTUSER; From 75c4153f9be1ff9f88153b0b91411481ed18e891 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 9 Feb 2015 19:41:38 +0100 Subject: [PATCH 34/60] No need to retrieve api key externally, QueryTablesApi takes care --- lib/cartodb/server_options.js | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index ef53884c..0aadeaa7 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -197,31 +197,8 @@ module.exports = function(redisPool) { } return [req.params.table]; } - var user, key; - var next = this; - Step ( - function findUserKey() { - if ( req.params.hasOwnProperty('_authorizedBySigner') ) { - user = req.params._authorizedBySigner; - cartoData.getUserMapKey(user, this); - } else { - user = that.userByReq(req); - key = req.params.map_key || req.params.api_key; - return null; - } - }, - function getAffected(err, data) { - if ( err ) throw err; - if ( data ) { - if ( req.profiler ) req.profiler.done('getSignerMapKey'); - key = data; - } - queryTablesApi.getAffectedTablesInQuery(user, sql, this); // in addCacheChannel - }, - function finish(err, data) { - next(err,data); - } - ); + + queryTablesApi.getAffectedTablesInQuery(that.userByReq(req), sql, this); // in addCacheChannel }, function buildCacheChannel(err, tableNames) { if ( err ) throw err; From b1b57d6f24712ef9c5e1cbcbaca8a071f2c4d8c3 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Tue, 10 Feb 2015 11:41:22 +0100 Subject: [PATCH 35/60] Regenerate npm-shrinkwrap.json --- npm-shrinkwrap.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 603e8df2..19888ae2 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -97,7 +97,7 @@ }, "inherits": { "version": "2.0.1", - "from": "inherits@2" + "from": "inherits@~2.0.1" } } } @@ -193,9 +193,8 @@ "resolved": "git://github.com/CartoDB/chronographjs.git#0b8c35eee510cfa14a16be24d70533b38ecc1d2d" }, "grainstore": { - "version": "0.22.2", - "from": "https://github.com/CartoDB/grainstore/tarball/datasource-extend", - "resolved": "https://github.com/CartoDB/grainstore/tarball/datasource-extend", + "version": "0.23.0", + "from": "grainstore@~0.23.0", "dependencies": { "carto": { "version": "0.9.5-cdb2", From 0f668aabf12e13979e07ccd32d03fb1b13837d9d Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Wed, 11 Feb 2015 14:48:06 +0100 Subject: [PATCH 36/60] Rely on windshaft's master branch as everything for medusa is already there --- npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 19888ae2..df0abd02 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -184,8 +184,8 @@ }, "windshaft": { "version": "0.35.2", - "from": "https://github.com/CartoDB/Windshaft/tarball/plain-renderer-plus-http-default-plus-per-layer-datasource", - "resolved": "https://github.com/CartoDB/Windshaft/tarball/plain-renderer-plus-http-default-plus-per-layer-datasource", + "from": "https://github.com/CartoDB/Windshaft/tarball/master", + "resolved": "https://github.com/CartoDB/Windshaft/tarball/master", "dependencies": { "chronograph": { "version": "0.1.0", diff --git a/package.json b/package.json index 762d84df..362ea17c 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "node-varnish": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0", "underscore" : "~1.6.0", "dot": "~1.0.2", - "windshaft": "https://github.com/CartoDB/Windshaft/tarball/plain-renderer-plus-http-default-plus-per-layer-datasource", + "windshaft": "https://github.com/CartoDB/Windshaft/tarball/master", "step": "~0.0.5", "queue-async": "~1.0.7", "request": "~2.9.203", From 8d10d0f760ed0bfb58ae5711e93af64376df3ad5 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 16 Feb 2015 10:54:53 +0100 Subject: [PATCH 37/60] Remove draft --- docs/MapConfig-NamedMaps-extension.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/MapConfig-NamedMaps-extension.md b/docs/MapConfig-NamedMaps-extension.md index 97425b9f..a6418174 100644 --- a/docs/MapConfig-NamedMaps-extension.md +++ b/docs/MapConfig-NamedMaps-extension.md @@ -1,5 +1,3 @@ -# STATUS: DRAFT - # 1. Purpose This specification describes an extension for From c6d328ee0708b01894059ef647c3070d1b29268d Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 16 Feb 2015 10:55:38 +0100 Subject: [PATCH 38/60] Upgrades to windshaft 0.36.0 --- npm-shrinkwrap.json | 9 ++++----- package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index df0abd02..8a65a9fa 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -183,9 +183,8 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" }, "windshaft": { - "version": "0.35.2", - "from": "https://github.com/CartoDB/Windshaft/tarball/master", - "resolved": "https://github.com/CartoDB/Windshaft/tarball/master", + "version": "0.36.0", + "from": "windshaft@~0.36.0", "dependencies": { "chronograph": { "version": "0.1.0", @@ -1636,9 +1635,9 @@ "resolved": "https://registry.npmjs.org/connect/-/connect-1.9.2.tgz", "dependencies": { "formidable": { - "version": "1.0.16", + "version": "1.0.17", "from": "formidable@1.0.x", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.16.tgz" + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.17.tgz" } } }, diff --git a/package.json b/package.json index 362ea17c..3c6fa48a 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "node-varnish": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0", "underscore" : "~1.6.0", "dot": "~1.0.2", - "windshaft": "https://github.com/CartoDB/Windshaft/tarball/master", + "windshaft": "~0.36.0", "step": "~0.0.5", "queue-async": "~1.0.7", "request": "~2.9.203", From e08b1ea1a055bb182562720d7a46cc2a4e2a6e8d Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 16 Feb 2015 11:21:22 +0100 Subject: [PATCH 39/60] Update news with changes in branch --- NEWS.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 509dbc9b..e17c8ea6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,15 @@ -1.26.3 -- 2015-mm-dd +1.27.0 -- 2015-mm-dd -------------------- +Announcements: +- Adds default image placeholder for http renderer to use as fallback + +New features: +- `named` layers type, see [MapConfig-NamedMaps-extension](docs/MapConfig-NamedMaps-extension.md) + - Starts using datasource per layer feature from Windshaft ([2c7bc6a](https://github.com/CartoDB/Windshaft-cartodb/commit/2c7bc6adde561b20ed955b905e3c7bcd6795d128)) + +Bugfixes: +- Fixes tests with beforeEach and afterEach triggers 1.26.2 -- 2015-01-28 -------------------- From d311dd4245d08356fc1418f1ad5b7c12f52c984d Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 16 Feb 2015 11:57:53 +0100 Subject: [PATCH 40/60] Use PgConnection to set db auth No need to use request context anymore X-Cache-Channel will be set now even for private tables: fixes #253 --- lib/cartodb/server_options.js | 39 +------------------ test/acceptance/named_layers.js | 2 +- .../mapconfig_named_layers_datasource.js | 5 ++- .../mapconfig_named_layers_expanded.js | 19 ++++----- 4 files changed, 15 insertions(+), 50 deletions(-) diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index a0f1808c..e0011795 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -281,17 +281,11 @@ module.exports = function(redisPool) { }; me.beforeLayergroupCreate = function(req, requestMapConfig, callback) { - mapConfigNamedLayersAdapter.getLayers(this.userByReq(req), requestMapConfig.layers, this, function(err, layers, datasource) { + mapConfigNamedLayersAdapter.getLayers(this.userByReq(req), requestMapConfig.layers, pgConnection, function(err, layers, datasource) { if (err) { return callback(err); } - if (!datasource.isEmpty()) { - setContext(req, 'queryTablesApiDatasource', _.find(datasource.layersDbParams, function(layerDbParams) { - return !!layerDbParams; - })); - } - requestMapConfig.layers = layers; return callback(null, requestMapConfig, datasource) }); @@ -727,36 +721,5 @@ module.exports = function(redisPool) { ); }; - /******************************************************************************************************************* - * Private methods - ******************************************************************************************************************/ - - /** - * Handles context for a given Request object - * @param {Object|IncomingMessage} req - * @param {String} key - * @returns {*} - */ - function getContext(req, key) { - return req.context && req.context[key]; - } - - /** - * Handles context for a given Request object - * @param {Object|IncomingMessage} req - * @param {String} key - * @param {*} value - * @returns {*} The previous value - */ - function setContext(req, key, value) { - var previousValue; - if (value) { - req.context = req.context || {}; - previousValue = req.context[key]; - req.context[key] = value; - } - return previousValue; - } - return me; }; diff --git a/test/acceptance/named_layers.js b/test/acceptance/named_layers.js index f1a67528..1d756a0a 100644 --- a/test/acceptance/named_layers.js +++ b/test/acceptance/named_layers.js @@ -430,7 +430,7 @@ suite('named_layers', function() { if (err) { throw err; } - //assert.ok(res.headers['X-Cache-Channel']); -> https://github.com/CartoDB/Windshaft-cartodb/issues/253 + test_helper.checkCache(res); return true; }, function deleteTemplate(err) { diff --git a/test/integration/mapconfig_named_layers_datasource.js b/test/integration/mapconfig_named_layers_datasource.js index e529a860..14fbc29c 100644 --- a/test/integration/mapconfig_named_layers_datasource.js +++ b/test/integration/mapconfig_named_layers_datasource.js @@ -3,7 +3,7 @@ var test_helper = require('../support/test_helper'); var assert = require('assert'); var RedisPool = require('redis-mpool'); var TemplateMaps = require('../../lib/cartodb/template_maps.js'); -var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')(); +var PgConnection = require(__dirname + '/../../lib/cartodb/backends/pg_connection'); var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter'); var Step = require('step'); @@ -11,6 +11,7 @@ var _ = require('underscore'); // configure redis pool instance to use in tests var redisPool = RedisPool(global.environment.redis); +var pgConnection = new PgConnection(require('cartodb-redis')({ pool: redisPool })); var templateMaps = new TemplateMaps(redisPool, { max_user_templates: global.environment.maxUserTemplates @@ -280,7 +281,7 @@ suite('named_layers datasources', function() { testScenarios.forEach(function(testScenario) { test('should return a list of layers ' + testScenario.desc, function(done) { - mapConfigNamedLayersAdapter.getLayers(username, testScenario.config.layers, serverOptions, function(err, layers, datasource) { + mapConfigNamedLayersAdapter.getLayers(username, testScenario.config.layers, pgConnection, function(err, layers, datasource) { testScenario.test(err, layers, datasource, done); }); }); diff --git a/test/integration/mapconfig_named_layers_expanded.js b/test/integration/mapconfig_named_layers_expanded.js index 26b5197a..72ab67cf 100644 --- a/test/integration/mapconfig_named_layers_expanded.js +++ b/test/integration/mapconfig_named_layers_expanded.js @@ -3,7 +3,7 @@ var testHelper = require('../support/test_helper'); var assert = require('assert'); var RedisPool = require('redis-mpool'); var TemplateMaps = require('../../lib/cartodb/template_maps.js'); -var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')(); +var PgConnection = require(__dirname + '/../../lib/cartodb/backends/pg_connection'); var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter'); var Step = require('step'); @@ -13,6 +13,7 @@ suite('mapconfig_named_layers_adapter', function() { // configure redis pool instance to use in tests var redisPool = RedisPool(global.environment.redis); + var pgConnection = new PgConnection(require('cartodb-redis')({ pool: redisPool })); var templateMaps = new TemplateMaps(redisPool, { max_user_templates: global.environment.maxUserTemplates @@ -145,7 +146,7 @@ suite('mapconfig_named_layers_adapter', function() { var missingNamedMapLayerConfig = makeNamedMapLayerConfig({ config: {} }); - mapConfigNamedLayersAdapter.getLayers(username, missingNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { + mapConfigNamedLayersAdapter.getLayers(username, missingNamedMapLayerConfig.layers, pgConnection, function(err, layers, datasource) { assert.ok(err); assert.ok(!layers); assert.ok(!datasource); @@ -160,7 +161,7 @@ suite('mapconfig_named_layers_adapter', function() { var nonExistentNamedMapLayerConfig = makeNamedMapLayerConfig({ name: missingTemplateName }); - mapConfigNamedLayersAdapter.getLayers(username, nonExistentNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { + mapConfigNamedLayersAdapter.getLayers(username, nonExistentNamedMapLayerConfig.layers, pgConnection, function(err, layers, datasource) { assert.ok(err); assert.ok(!layers); assert.ok(!datasource); @@ -179,7 +180,7 @@ suite('mapconfig_named_layers_adapter', function() { var nonAuthTokensNamedMapLayerConfig = makeNamedMapLayerConfig({ name: tokenAuthTemplateName }); - mapConfigNamedLayersAdapter.getLayers(username, nonAuthTokensNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { + mapConfigNamedLayersAdapter.getLayers(username, nonAuthTokensNamedMapLayerConfig.layers, pgConnection, function(err, layers, datasource) { assert.ok(err); assert.ok(!layers); assert.ok(!datasource); @@ -199,7 +200,7 @@ suite('mapconfig_named_layers_adapter', function() { var nestedNamedMapLayerConfig = makeNamedMapLayerConfig({ name: nestedNamedMapTemplateName }); - mapConfigNamedLayersAdapter.getLayers(username, nestedNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { + mapConfigNamedLayersAdapter.getLayers(username, nestedNamedMapLayerConfig.layers, pgConnection, function(err, layers, datasource) { assert.ok(err); assert.ok(!layers); assert.ok(!datasource); @@ -214,7 +215,7 @@ suite('mapconfig_named_layers_adapter', function() { var validNamedMapMapLayerConfig = makeNamedMapLayerConfig({ name: templateName }); - mapConfigNamedLayersAdapter.getLayers(username, validNamedMapMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { + mapConfigNamedLayersAdapter.getLayers(username, validNamedMapMapLayerConfig.layers, pgConnection, function(err, layers, datasource) { assert.ok(!err); assert.ok(layers.length, 1); assert.ok(layers[0].type, 'cartodb'); @@ -234,7 +235,7 @@ suite('mapconfig_named_layers_adapter', function() { name: tokenAuthTemplateName, auth_tokens: ['valid1'] }); - mapConfigNamedLayersAdapter.getLayers(username, validAuthTokensNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { + mapConfigNamedLayersAdapter.getLayers(username, validAuthTokensNamedMapLayerConfig.layers, pgConnection, function(err, layers, datasource) { assert.ok(!err); assert.equal(layers.length, 1); assert.notEqual(datasource.getLayerDatasource(0), undefined); @@ -254,7 +255,7 @@ suite('mapconfig_named_layers_adapter', function() { name: multipleLayersTemplateName, auth_tokens: ['valid2'] }); - mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { + mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, pgConnection, function(err, layers, datasource) { assert.ok(!err); assert.equal(layers.length, 2); @@ -288,7 +289,7 @@ suite('mapconfig_named_layers_adapter', function() { }, auth_tokens: ['valid2'] }); - mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) { + mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, pgConnection, function(err, layers, datasource) { assert.ok(!err); assert.equal(layers.length, 2); From 0478905689adb88226e9efe5a094998a17528624 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 16 Feb 2015 12:14:24 +0100 Subject: [PATCH 41/60] Release 1.27.0 --- NEWS.md | 3 ++- npm-shrinkwrap.json | 2 +- package.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index e17c8ea6..8b5722c6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -1.27.0 -- 2015-mm-dd +1.27.0 -- 2015-02-16 -------------------- Announcements: @@ -11,6 +11,7 @@ New features: Bugfixes: - Fixes tests with beforeEach and afterEach triggers + 1.26.2 -- 2015-01-28 -------------------- diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 8a65a9fa..c05b2a93 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "windshaft-cartodb", - "version": "1.26.3", + "version": "1.27.0", "dependencies": { "cartodb-psql": { "version": "0.4.0", diff --git a/package.json b/package.json index 3c6fa48a..9278cada 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "windshaft-cartodb", - "version": "1.26.3", + "version": "1.27.0", "description": "A map tile server for CartoDB", "keywords": [ "cartodb" From f295f847d1aa5351dc4bd359f48eedbe2747e1bf Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 16 Feb 2015 12:15:08 +0100 Subject: [PATCH 42/60] Stubs next version --- NEWS.md | 4 ++++ npm-shrinkwrap.json | 2 +- package.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 8b5722c6..fb4fff98 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +1.27.1 -- 2015-mm-dd +-------------------- + + 1.27.0 -- 2015-02-16 -------------------- diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index c05b2a93..0486776b 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "windshaft-cartodb", - "version": "1.27.0", + "version": "1.27.1", "dependencies": { "cartodb-psql": { "version": "0.4.0", diff --git a/package.json b/package.json index 9278cada..e1e0ac47 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "windshaft-cartodb", - "version": "1.27.0", + "version": "1.27.1", "description": "A map tile server for CartoDB", "keywords": [ "cartodb" From 9f7dcc2354393866b556d673de58b299ad834331 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 16 Feb 2015 14:21:56 +0100 Subject: [PATCH 43/60] Update news --- NEWS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NEWS.md b/NEWS.md index fb4fff98..0e10d568 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,10 @@ 1.27.1 -- 2015-mm-dd -------------------- +New features: + - QueryTablesApi will always use an authenticated query to retrieve last update, this allows to query affected private + tables last update (#253) + 1.27.0 -- 2015-02-16 -------------------- From 00542bbc57412f4e658a9d17c3b96a5df741656f Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 16 Feb 2015 14:57:03 +0100 Subject: [PATCH 44/60] Upgrades windshaft to 0.37.0 --- NEWS.md | 3 +++ npm-shrinkwrap.json | 6 +++--- package.json | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/NEWS.md b/NEWS.md index 0e10d568..e516bcc6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,9 @@ 1.27.1 -- 2015-mm-dd -------------------- +Announcements: +- Upgrades windshaft to [0.37.0](https://github.com/CartoDB/Windshaft/releases/tag/0.37.0) + New features: - QueryTablesApi will always use an authenticated query to retrieve last update, this allows to query affected private tables last update (#253) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 0486776b..cf24be60 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -97,7 +97,7 @@ }, "inherits": { "version": "2.0.1", - "from": "inherits@~2.0.1" + "from": "inherits@2" } } } @@ -183,8 +183,8 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" }, "windshaft": { - "version": "0.36.0", - "from": "windshaft@~0.36.0", + "version": "0.37.0", + "from": "windshaft@~0.37.0", "dependencies": { "chronograph": { "version": "0.1.0", diff --git a/package.json b/package.json index e1e0ac47..112e17f4 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "node-varnish": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0", "underscore" : "~1.6.0", "dot": "~1.0.2", - "windshaft": "~0.36.0", + "windshaft": "~0.37.0", "step": "~0.0.5", "queue-async": "~1.0.7", "request": "~2.9.203", From 1f9387bb6830e51d52588c8315a5efe06cf61e12 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Mon, 16 Feb 2015 15:02:39 +0100 Subject: [PATCH 45/60] Rely on published versions --- npm-shrinkwrap.json | 6 +++--- package.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index cf24be60..e1d860dc 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -4,7 +4,7 @@ "dependencies": { "cartodb-psql": { "version": "0.4.0", - "from": "https://github.com/CartoDB/node-cartodb-psql/tarball/0.4.0", + "from": "cartodb-psql@~0.4.0", "resolved": "https://github.com/CartoDB/node-cartodb-psql/tarball/0.4.0", "dependencies": { "pg": { @@ -27,7 +27,7 @@ }, "cartodb-redis": { "version": "0.11.0", - "from": "https://github.com/CartoDB/node-cartodb-redis/tarball/0.11.0", + "from": "cartodb-redis@~0.11.0", "resolved": "https://github.com/CartoDB/node-cartodb-redis/tarball/0.11.0", "dependencies": { "redis-mpool": { @@ -120,7 +120,7 @@ }, "redis-mpool": { "version": "0.3.0", - "from": "https://github.com/CartoDB/node-redis-mpool/tarball/0.3.0", + "from": "redis-mpool@~0.3.0", "resolved": "https://github.com/CartoDB/node-redis-mpool/tarball/0.3.0", "dependencies": { "generic-pool": { diff --git a/package.json b/package.json index 112e17f4..7d7082cd 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,9 @@ "step": "~0.0.5", "queue-async": "~1.0.7", "request": "~2.9.203", - "cartodb-redis": "https://github.com/CartoDB/node-cartodb-redis/tarball/0.11.0", - "cartodb-psql": "https://github.com/CartoDB/node-cartodb-psql/tarball/0.4.0", - "redis-mpool": "https://github.com/CartoDB/node-redis-mpool/tarball/0.3.0", + "cartodb-redis": "~0.11.0", + "cartodb-psql": "~0.4.0", + "redis-mpool": "~0.3.0", "lzma": "~1.3.7", "log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb", "rollbar": "~0.3.13" From 27cda49fd89fdc734e2625755d9d9078a7341590 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Tue, 17 Feb 2015 11:02:29 +0100 Subject: [PATCH 46/60] Release 1.28.0 --- NEWS.md | 2 +- npm-shrinkwrap.json | 13 +++++-------- package.json | 2 +- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/NEWS.md b/NEWS.md index e516bcc6..5dbf7838 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -1.27.1 -- 2015-mm-dd +1.28.0 -- 2015-02-17 -------------------- Announcements: diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index e1d860dc..e727ae48 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "windshaft-cartodb", - "version": "1.27.1", + "version": "1.28.0", "dependencies": { "cartodb-psql": { "version": "0.4.0", @@ -18,8 +18,7 @@ }, "buffer-writer": { "version": "1.0.0", - "from": "buffer-writer@1.0.0", - "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-1.0.0.tgz" + "from": "buffer-writer@1.0.0" } } } @@ -88,8 +87,7 @@ }, "isarray": { "version": "0.0.1", - "from": "isarray@0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + "from": "isarray@0.0.1" }, "string_decoder": { "version": "0.10.31", @@ -97,7 +95,7 @@ }, "inherits": { "version": "2.0.1", - "from": "inherits@2" + "from": "inherits@~2.0.1" } } } @@ -2211,8 +2209,7 @@ }, "isarray": { "version": "0.0.1", - "from": "isarray@0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + "from": "isarray@0.0.1" }, "string_decoder": { "version": "0.10.31", diff --git a/package.json b/package.json index 7d7082cd..7972498a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "windshaft-cartodb", - "version": "1.27.1", + "version": "1.28.0", "description": "A map tile server for CartoDB", "keywords": [ "cartodb" From c65a29acf41a618181484665b3934394a1e8b0ad Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Tue, 17 Feb 2015 11:04:41 +0100 Subject: [PATCH 47/60] Stubs next version --- NEWS.md | 4 ++++ npm-shrinkwrap.json | 2 +- package.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 5dbf7838..c1ce4b84 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +1.28.1 -- 2015-mm-dd +-------------------- + + 1.28.0 -- 2015-02-17 -------------------- diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index e727ae48..8851f75f 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "windshaft-cartodb", - "version": "1.28.0", + "version": "1.28.1", "dependencies": { "cartodb-psql": { "version": "0.4.0", diff --git a/package.json b/package.json index 7972498a..13e5ecb7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "windshaft-cartodb", - "version": "1.28.0", + "version": "1.28.1", "description": "A map tile server for CartoDB", "keywords": [ "cartodb" From 055932d38e5e7343de8c472b883391c96c437ba8 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Tue, 17 Feb 2015 12:19:16 +0100 Subject: [PATCH 48/60] Upgrades windshaft to 0.37.1 --- NEWS.md | 3 +++ npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index c1ce4b84..5ae74889 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,9 @@ 1.28.1 -- 2015-mm-dd -------------------- +Announcements: +- Upgrades windshaft to [0.37.1](https://github.com/CartoDB/Windshaft/releases/tag/0.37.1) + 1.28.0 -- 2015-02-17 -------------------- diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 8851f75f..4ed37847 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -181,8 +181,8 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" }, "windshaft": { - "version": "0.37.0", - "from": "windshaft@~0.37.0", + "version": "0.37.1", + "from": "windshaft@~0.37.1", "dependencies": { "chronograph": { "version": "0.1.0", diff --git a/package.json b/package.json index 13e5ecb7..4a7274a3 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "node-varnish": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0", "underscore" : "~1.6.0", "dot": "~1.0.2", - "windshaft": "~0.37.0", + "windshaft": "~0.37.1", "step": "~0.0.5", "queue-async": "~1.0.7", "request": "~2.9.203", From 2b7bd58fd58910a6f22309181294e4c6a6536f44 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Tue, 17 Feb 2015 12:20:07 +0100 Subject: [PATCH 49/60] Release 1.28.1 --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 5ae74889..57641919 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -1.28.1 -- 2015-mm-dd +1.28.1 -- 2015-02-17 -------------------- Announcements: From d2961430f3b108b64a173d8b686163f594f32deb Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Tue, 17 Feb 2015 12:20:51 +0100 Subject: [PATCH 50/60] Stubs next version --- NEWS.md | 4 ++++ npm-shrinkwrap.json | 2 +- package.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 57641919..32b70baa 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +1.28.2 -- 2015-mm-dd +-------------------- + + 1.28.1 -- 2015-02-17 -------------------- diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 4ed37847..9cf0a9c7 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "windshaft-cartodb", - "version": "1.28.1", + "version": "1.28.2", "dependencies": { "cartodb-psql": { "version": "0.4.0", diff --git a/package.json b/package.json index 4a7274a3..e69c3eaf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "windshaft-cartodb", - "version": "1.28.1", + "version": "1.28.2", "description": "A map tile server for CartoDB", "keywords": [ "cartodb" From 11db363bfbbe7d0bdb780c9ba4ccd0dfdcbdb677 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Tue, 17 Feb 2015 16:07:49 +0100 Subject: [PATCH 51/60] Upgrades windshaft to 0.37.2 --- NEWS.md | 3 +++ npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index 32b70baa..bd61fa86 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,9 @@ 1.28.2 -- 2015-mm-dd -------------------- +Announcements: +- Upgrades windshaft to [0.37.2](https://github.com/CartoDB/Windshaft/releases/tag/0.37.2) + 1.28.1 -- 2015-02-17 -------------------- diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 9cf0a9c7..413f9899 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -181,8 +181,8 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" }, "windshaft": { - "version": "0.37.1", - "from": "windshaft@~0.37.1", + "version": "0.37.2", + "from": "windshaft@~0.37.2", "dependencies": { "chronograph": { "version": "0.1.0", diff --git a/package.json b/package.json index e69c3eaf..5d4ac6b0 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "node-varnish": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0", "underscore" : "~1.6.0", "dot": "~1.0.2", - "windshaft": "~0.37.1", + "windshaft": "~0.37.2", "step": "~0.0.5", "queue-async": "~1.0.7", "request": "~2.9.203", From 9536669053d4980963f87c9fdd627eb44eef2d61 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Tue, 17 Feb 2015 16:16:19 +0100 Subject: [PATCH 52/60] Release 1.28.2 --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index bd61fa86..02036958 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -1.28.2 -- 2015-mm-dd +1.28.2 -- 2015-02-17 -------------------- Announcements: From dc14248de29e15808581ac90e14aaf59b245ac10 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Tue, 17 Feb 2015 16:17:32 +0100 Subject: [PATCH 53/60] Stubs next version --- NEWS.md | 4 ++++ npm-shrinkwrap.json | 2 +- package.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 02036958..c518ebe4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +1.28.3 -- 2015-mm-dd +-------------------- + + 1.28.2 -- 2015-02-17 -------------------- diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 413f9899..c55a6f82 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "windshaft-cartodb", - "version": "1.28.2", + "version": "1.28.3", "dependencies": { "cartodb-psql": { "version": "0.4.0", diff --git a/package.json b/package.json index 5d4ac6b0..6853a6ed 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "windshaft-cartodb", - "version": "1.28.2", + "version": "1.28.3", "description": "A map tile server for CartoDB", "keywords": [ "cartodb" From 2e577343d207eaf42faf05acb43cb23c50e8a200 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Tue, 17 Feb 2015 18:55:34 +0100 Subject: [PATCH 54/60] Update to latest CDB_QueryTables --- test/support/sql/CDB_QueryTables.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/support/sql/CDB_QueryTables.sql b/test/support/sql/CDB_QueryTables.sql index 2fb9532a..cd8b51b4 100644 --- a/test/support/sql/CDB_QueryTables.sql +++ b/test/support/sql/CDB_QueryTables.sql @@ -16,9 +16,9 @@ BEGIN FOR rec IN SELECT CDB_QueryStatements(query) q LOOP - IF NOT ( rec.q ilike 'select %' or rec.q ilike 'with %' ) THEN - --RAISE WARNING 'Skipping %', rec.q; - CONTINUE; + IF NOT ( rec.q ilike 'select%' or rec.q ilike 'with%' ) THEN + --RAISE WARNING 'Skipping %', rec.q; + CONTINUE; END IF; BEGIN From 7f20e296a30291c154abd1c32b882fb02b00f63d Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Tue, 17 Feb 2015 19:28:05 +0100 Subject: [PATCH 55/60] Upgrades windshaft to 0.37.3 and adds a test for named layers and interactivity Closes #263 --- NEWS.md | 3 + npm-shrinkwrap.json | 4 +- package.json | 2 +- test/acceptance/named_layers.js | 128 ++++++++++++++++++++++++++++++++ 4 files changed, 134 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index c518ebe4..6d28c7b8 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,9 @@ 1.28.3 -- 2015-mm-dd -------------------- +Announcements: +- Upgrades windshaft to [0.37.3](https://github.com/CartoDB/Windshaft/releases/tag/0.37.3) + 1.28.2 -- 2015-02-17 -------------------- diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index c55a6f82..be815c5e 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -181,8 +181,8 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" }, "windshaft": { - "version": "0.37.2", - "from": "windshaft@~0.37.2", + "version": "0.37.3", + "from": "windshaft@~0.37.3", "dependencies": { "chronograph": { "version": "0.1.0", diff --git a/package.json b/package.json index 6853a6ed..e2d8e0d1 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "node-varnish": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0", "underscore" : "~1.6.0", "dot": "~1.0.2", - "windshaft": "~0.37.2", + "windshaft": "~0.37.3", "step": "~0.0.5", "queue-async": "~1.0.7", "request": "~2.9.203", diff --git a/test/acceptance/named_layers.js b/test/acceptance/named_layers.js index 1d756a0a..3b52f982 100644 --- a/test/acceptance/named_layers.js +++ b/test/acceptance/named_layers.js @@ -447,6 +447,134 @@ suite('named_layers', function() { }); + test('should return 200 and layergroup with private tables and interactivity', function(done) { + + var privateTableTemplateNameInteractivity = 'private_table_template_interactivity'; + var privateTableTemplate = { + "version": "0.0.1", + "auth": { + "method": "open" + }, + "name": privateTableTemplateNameInteractivity, + "layergroup": { + "layers": [ + { + "type": "cartodb", + "options": { + "attributes": { + "columns": [ + "name" + ], + "id": "cartodb_id" + }, + "cartocss": "#layer { marker-fill: #cc3300; }", + "cartocss_version": "2.3.0", + "interactivity": "cartodb_id", + "sql": "select * from test_table_private_1" + } + } + ] + } + }; + + var layergroup = { + version: '1.3.0', + layers: [ + { + type: 'named', + options: { + name: privateTableTemplateNameInteractivity + } + } + ] + }; + + Step( + function createTemplate() { + templateMaps.addTemplate(username, privateTableTemplate, this); + }, + function createLayergroup(err) { + if (err) { + throw err; + } + + var next = this; + assert.response(server, + { + url: '/tiles/layergroup', + method: 'POST', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(layergroup) + }, + { + 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); + + return parsedBody.layergroupid; + }, + function requestTile(err, layergroupId) { + if (err) { + throw err; + } + + var next = this; + assert.response(server, + { + url: '/tiles/layergroup/' + layergroupId + '/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, privateTableTemplate, function(/*delErr*/) { + // ignore deletion error + next(err); + }); + }, + function finish(err) { + done(err); + } + ); + + }); + test('should return 403 when private table is accessed from non named layer', function(done) { var layergroup = { From ac1f8f84975d48fea67ef238292d9ad90f593002 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Tue, 17 Feb 2015 19:43:17 +0100 Subject: [PATCH 56/60] Release 1.28.3 --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 6d28c7b8..4e844e61 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -1.28.3 -- 2015-mm-dd +1.28.3 -- 2015-02-17 -------------------- Announcements: From 5db7f002e639d10c1df2a60f3376472089015679 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Tue, 17 Feb 2015 19:44:23 +0100 Subject: [PATCH 57/60] Stubs next version --- NEWS.md | 4 ++++ npm-shrinkwrap.json | 2 +- package.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 4e844e61..d4d990fd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +1.28.4 -- 2015-mm-dd +-------------------- + + 1.28.3 -- 2015-02-17 -------------------- diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index be815c5e..ccb84076 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "windshaft-cartodb", - "version": "1.28.3", + "version": "1.28.4", "dependencies": { "cartodb-psql": { "version": "0.4.0", diff --git a/package.json b/package.json index e2d8e0d1..9d827c44 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "windshaft-cartodb", - "version": "1.28.3", + "version": "1.28.4", "description": "A map tile server for CartoDB", "keywords": [ "cartodb" From 27d6d636cf31a1523ec5358219715b111e8fa649 Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Wed, 18 Feb 2015 11:53:56 +0100 Subject: [PATCH 58/60] Upgrades to windshaft 0.37.4 --- NEWS.md | 3 +++ npm-shrinkwrap.json | 4 ++-- package.json | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index d4d990fd..3c43416d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,9 @@ 1.28.4 -- 2015-mm-dd -------------------- +Announcements: +- Upgrades windshaft to [0.37.4](https://github.com/CartoDB/Windshaft/releases/tag/0.37.4) + 1.28.3 -- 2015-02-17 -------------------- diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index ccb84076..990c360f 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -181,8 +181,8 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" }, "windshaft": { - "version": "0.37.3", - "from": "windshaft@~0.37.3", + "version": "0.37.4", + "from": "windshaft@~0.37.4", "dependencies": { "chronograph": { "version": "0.1.0", diff --git a/package.json b/package.json index 9d827c44..7d3c6352 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "node-varnish": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0", "underscore" : "~1.6.0", "dot": "~1.0.2", - "windshaft": "~0.37.3", + "windshaft": "~0.37.4", "step": "~0.0.5", "queue-async": "~1.0.7", "request": "~2.9.203", From 5c2e5c0d05ef375e5b9bea3150f8e35b2cfc641b Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Wed, 18 Feb 2015 11:54:40 +0100 Subject: [PATCH 59/60] Release 1.28.4 --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 3c43416d..8206f497 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -1.28.4 -- 2015-mm-dd +1.28.4 -- 2015-02-18 -------------------- Announcements: From 5da239a2eb1b972ec147e38cca17e94349453c3a Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Wed, 18 Feb 2015 11:55:35 +0100 Subject: [PATCH 60/60] Stubs next version --- NEWS.md | 4 ++++ npm-shrinkwrap.json | 2 +- package.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 8206f497..7939285b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +1.28.5 -- 2015-mm-dd +-------------------- + + 1.28.4 -- 2015-02-18 -------------------- diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 990c360f..2e52be20 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "windshaft-cartodb", - "version": "1.28.4", + "version": "1.28.5", "dependencies": { "cartodb-psql": { "version": "0.4.0", diff --git a/package.json b/package.json index 7d3c6352..806898e7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "windshaft-cartodb", - "version": "1.28.4", + "version": "1.28.5", "description": "A map tile server for CartoDB", "keywords": [ "cartodb"