From a1e024e228973b4d6b4fa2260e9e4e485d54847d Mon Sep 17 00:00:00 2001 From: Javier Goizueta Date: Wed, 18 May 2016 17:49:09 +0200 Subject: [PATCH 1/6] Fix dataview problem for bbox with no query rewrite data Fixes #457 --- lib/cartodb/backends/dataview.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/cartodb/backends/dataview.js b/lib/cartodb/backends/dataview.js index 44db254e..89b811bb 100644 --- a/lib/cartodb/backends/dataview.js +++ b/lib/cartodb/backends/dataview.js @@ -149,7 +149,9 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param bbox: params.bbox } }; - queryRewriteData = _.extend(queryRewriteData, { bbox_filter: bbox_filter_definition }); + if ( queryRewriteData ) { + queryRewriteData = _.extend(queryRewriteData, { bbox_filter: bbox_filter_definition }); + } } var dataviewFactory = DataviewFactoryWithOverviews.getFactory( From 5989ab344d450155f71885e8e32d6a6b59b156ac Mon Sep 17 00:00:00 2001 From: Javier Goizueta Date: Wed, 18 May 2016 18:02:08 +0200 Subject: [PATCH 2/6] Add test to detect problem #457 --- test/acceptance/dataviews/overviews.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/acceptance/dataviews/overviews.js b/test/acceptance/dataviews/overviews.js index 9acda126..0ab7e8bd 100644 --- a/test/acceptance/dataviews/overviews.js +++ b/test/acceptance/dataviews/overviews.js @@ -58,6 +58,21 @@ describe('dataviews using tables without overviews', function() { }); }); + it("should admit a bbox", function(done) { + var params = { + bbox: "-170,-80,170,80" + }; + var testClient = new TestClient(nonOverviewsMapConfig); + testClient.getDataview('country_places_count', params, function(err, formula_result) { + if (err) { + return done(err); + } + assert.deepEqual(formula_result, { operation: 'count', result: 7253, nulls: 0, type: 'formula' }); + + testClient.drain(done); + }); + }); + describe('filters', function() { describe('category', function () { From 9206b1a1b502bf0d032de8cfc0c740d4a273233f Mon Sep 17 00:00:00 2001 From: Javier Goizueta Date: Wed, 18 May 2016 18:16:32 +0200 Subject: [PATCH 3/6] Fix dataviews/overviews tests and add some new cases --- test/acceptance/dataviews/overviews.js | 161 ++++++++++++++++--------- 1 file changed, 106 insertions(+), 55 deletions(-) diff --git a/test/acceptance/dataviews/overviews.js b/test/acceptance/dataviews/overviews.js index 0ab7e8bd..601124a7 100644 --- a/test/acceptance/dataviews/overviews.js +++ b/test/acceptance/dataviews/overviews.js @@ -92,6 +92,23 @@ describe('dataviews using tables without overviews', function() { testClient.drain(done); }); }); + + it("should expose a filtered formula and admit a bbox", function (done) { + var params = { + filters: { + dataviews: {country_categories: {accept: ['CAN']}} + }, + bbox: "-170,-80,170,80" + }; + var testClient = new TestClient(nonOverviewsMapConfig); + testClient.getDataview('country_places_count', params, function (err, formula_result) { + if (err) { + return done(err); + } + assert.deepEqual(formula_result, { operation: 'count', result: 254, nulls: 0, type: 'formula' }); + testClient.drain(done); + }); + }); }); }); @@ -233,16 +250,32 @@ describe('dataviews using tables with overviews', function() { }); }); + it("should admit a bbox", function(done) { + var params = { + bbox: "-170,-80,170,80" + }; + var testClient = new TestClient(overviewsMapConfig); + testClient.getDataview('test_sum', params, function(err, formula_result) { + if (err) { + return done(err); + } + assert.deepEqual(formula_result, {"operation":"sum","result":15,"nulls":0,"type":"formula"}); + + testClient.drain(done); + }); + }); + describe('filters', function() { describe('category', function () { - it("should expose a filtered formula", function (done) { - var params = { - filters: { - dataviews: {test_categories: {accept: ['Hawai']}} - } - }; + var params = { + filters: { + dataviews: {test_categories: {accept: ['Hawai']}} + } + }; + + it("should expose a filtered sum formula", function (done) { var testClient = new TestClient(overviewsMapConfig); testClient.getDataview('test_sum', params, function (err, formula_result) { if (err) { @@ -251,56 +284,74 @@ describe('dataviews using tables with overviews', function() { assert.deepEqual(formula_result, {"operation":"sum","result":1,"nulls":0,"type":"formula"}); testClient.drain(done); }); - - it("should expose an avg formula", function(done) { - var testClient = new TestClient(overviewsMapConfig); - testClient.getDataview('test_avg', { own_filter: 0 }, function(err, formula_result) { - if (err) { - return done(err); - } - assert.deepEqual(formula_result, {"operation":"avg","result":1,"nulls":0,"type":"formula"}); - - testClient.drain(done); - }); - }); - - it("should expose a count formula", function(done) { - var testClient = new TestClient(overviewsMapConfig); - testClient.getDataview('test_count', { own_filter: 0 }, function(err, formula_result) { - if (err) { - return done(err); - } - assert.deepEqual(formula_result, {"operation":"count","result":1,"nulls":0,"type":"formula"}); - - testClient.drain(done); - }); - }); - - it("should expose a max formula", function(done) { - var testClient = new TestClient(overviewsMapConfig); - testClient.getDataview('test_max', { own_filter: 0 }, function(err, formula_result) { - if (err) { - return done(err); - } - assert.deepEqual(formula_result, {"operation":"max","result":1,"nulls":0,"type":"formula"}); - - testClient.drain(done); - }); - }); - - it("should expose a min formula", function(done) { - var testClient = new TestClient(overviewsMapConfig); - testClient.getDataview('test_min', { own_filter: 0 }, function(err, formula_result) { - if (err) { - return done(err); - } - assert.deepEqual(formula_result, {"operation":"min","result":1,"nulls":0,"type":"formula"}); - - testClient.drain(done); - }); - }); - }); + + it("should expose a filtered avg formula", function(done) { + var testClient = new TestClient(overviewsMapConfig); + testClient.getDataview('test_avg', params, function(err, formula_result) { + if (err) { + return done(err); + } + assert.deepEqual(formula_result, {"operation":"avg","result":1,"nulls":0,"type":"formula"}); + + testClient.drain(done); + }); + }); + + it("should expose a filtered count formula", function(done) { + var testClient = new TestClient(overviewsMapConfig); + testClient.getDataview('test_count', params, function(err, formula_result) { + if (err) { + return done(err); + } + assert.deepEqual(formula_result, {"operation":"count","result":1,"nulls":0,"type":"formula"}); + + testClient.drain(done); + }); + }); + + it("should expose a filterd max formula", function(done) { + var testClient = new TestClient(overviewsMapConfig); + testClient.getDataview('test_max', params, function(err, formula_result) { + if (err) { + return done(err); + } + assert.deepEqual(formula_result, {"operation":"max","result":1,"nulls":0,"type":"formula"}); + + testClient.drain(done); + }); + }); + + it("should expose a filterd min formula", function(done) { + var testClient = new TestClient(overviewsMapConfig); + testClient.getDataview('test_min', params, function(err, formula_result) { + if (err) { + return done(err); + } + assert.deepEqual(formula_result, {"operation":"min","result":1,"nulls":0,"type":"formula"}); + + testClient.drain(done); + }); + }); + + it("should expose a filtered sum formula with bbox", function (done) { + var bboxparams = { + filters: { + dataviews: {test_categories: {accept: ['Hawai']}} + }, + bbox: "-170,-80,170,80" + }; + var testClient = new TestClient(overviewsMapConfig); + testClient.getDataview('test_sum', bboxparams, function (err, formula_result) { + if (err) { + return done(err); + } + assert.deepEqual(formula_result, {"operation":"sum","result":1,"nulls":0,"type":"formula"}); + testClient.drain(done); + }); + }); + + }); }); From 2a06405a58b7fae3fa2b4d72d7df739112c2f5d3 Mon Sep 17 00:00:00 2001 From: Javier Goizueta Date: Wed, 18 May 2016 18:21:17 +0200 Subject: [PATCH 4/6] Move definition to the scope where it's needed --- lib/cartodb/backends/dataview.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/cartodb/backends/dataview.js b/lib/cartodb/backends/dataview.js index 89b811bb..b7bb6d0b 100644 --- a/lib/cartodb/backends/dataview.js +++ b/lib/cartodb/backends/dataview.js @@ -139,17 +139,17 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param if (params.bbox) { var bboxFilter = new BBoxFilter({column: 'the_geom', srid: 4326}, {bbox: params.bbox}); query = bboxFilter.sql(query); - var bbox_filter_definition = { - type: 'bbox', - options: { - column: 'the_geom', - srid: 4326, - }, - params: { - bbox: params.bbox - } - }; if ( queryRewriteData ) { + var bbox_filter_definition = { + type: 'bbox', + options: { + column: 'the_geom', + srid: 4326, + }, + params: { + bbox: params.bbox + } + }; queryRewriteData = _.extend(queryRewriteData, { bbox_filter: bbox_filter_definition }); } } From 289ffbbedcd3e579d5a510e2a681c9499294a6f6 Mon Sep 17 00:00:00 2001 From: Javier Goizueta Date: Thu, 19 May 2016 12:33:41 +0200 Subject: [PATCH 5/6] Release 2.43.1 --- NEWS.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index fde7f461..7e495c7d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,7 +2,10 @@ ## 2.43.1 -Released 2016-mm-dd +Released 2016-05-19 + +Bug fixes: + - Dataview error when bbox present without query rewrite data #458 ## 2.43.0 From 2e79781711d14627a1dba97fc87bfb7c3f5976ca Mon Sep 17 00:00:00 2001 From: Raul Ochoa Date: Thu, 19 May 2016 13:32:32 +0200 Subject: [PATCH 6/6] Adds support for sql wrap in all layers Previously it was only working for analyses ones. --- NEWS.md | 7 +++ lib/cartodb/controllers/map.js | 6 ++ .../adapter/sql-wrap-mapconfig-adapter.js | 25 +++++++++ .../models/mapconfig/named_map_provider.js | 6 ++ npm-shrinkwrap.json | 2 +- package.json | 2 +- test/acceptance/sql-wrap.js | 52 ++++++++++++++++++ test/fixtures/sql-wrap-usa-filter.png | Bin 0 -> 4197 bytes 8 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 lib/cartodb/models/mapconfig/adapter/sql-wrap-mapconfig-adapter.js create mode 100644 test/acceptance/sql-wrap.js create mode 100644 test/fixtures/sql-wrap-usa-filter.png diff --git a/NEWS.md b/NEWS.md index 7e495c7d..9eeb06e3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,12 @@ # Changelog +## 2.44.0 + +Released 2016-mm-dd + +Adds support for sql wrap in all layers + + ## 2.43.1 Released 2016-05-19 diff --git a/lib/cartodb/controllers/map.js b/lib/cartodb/controllers/map.js index feaf6fa5..1f8f25bb 100644 --- a/lib/cartodb/controllers/map.js +++ b/lib/cartodb/controllers/map.js @@ -19,6 +19,7 @@ var MapConfigNamedLayersAdapter = require('../models/mapconfig_named_layers_adap var AnalysisMapConfigAdapter = require('../models/analysis-mapconfig-adapter'); var NamedMapMapConfigProvider = require('../models/mapconfig/named_map_provider'); var CreateLayergroupMapConfigProvider = require('../models/mapconfig/create_layergroup_provider'); +var SqlWrapMapConfigAdapter = require('../models/mapconfig/adapter/sql-wrap-mapconfig-adapter'); /** * @param {AuthApi} authApi @@ -52,6 +53,7 @@ function MapController(authApi, pgConnection, templateMaps, mapBackend, metadata this.analysisMapConfigAdapter = new AnalysisMapConfigAdapter(analysisBackend); this.namedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps); this.overviewsAdapter = overviewsAdapter; + this.sqlWrapMapConfigAdapter = new SqlWrapMapConfigAdapter(); } util.inherits(MapController, BaseController); @@ -139,6 +141,10 @@ MapController.prototype.create = function(req, res, prepareConfigFn) { self.req2params(req, this); }, prepareConfigFn, + function prepareSqlWrap(err, requestMapConfig) { + assert.ifError(err); + self.sqlWrapMapConfigAdapter.getMapConfig(requestMapConfig, this); + }, function prepareAnalysisLayers(err, requestMapConfig) { assert.ifError(err); var analysisConfiguration = { diff --git a/lib/cartodb/models/mapconfig/adapter/sql-wrap-mapconfig-adapter.js b/lib/cartodb/models/mapconfig/adapter/sql-wrap-mapconfig-adapter.js new file mode 100644 index 00000000..6b9a5130 --- /dev/null +++ b/lib/cartodb/models/mapconfig/adapter/sql-wrap-mapconfig-adapter.js @@ -0,0 +1,25 @@ +function SqlWrapMapConfigAdapter() { +} + +module.exports = SqlWrapMapConfigAdapter; + + +SqlWrapMapConfigAdapter.prototype.getMapConfig = function(requestMapConfig, callback) { + if (requestMapConfig && Array.isArray(requestMapConfig.layers)) { + requestMapConfig.layers = requestMapConfig.layers.map(function(layer) { + if (layer.options) { + var sqlQueryWrap = layer.options.sql_wrap; + if (sqlQueryWrap) { + var layerSql = layer.options.sql; + if (layerSql) { + layer.options.sql_raw = layerSql; + layer.options.sql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, layerSql); + } + } + } + return layer; + }); + } + + return callback(null, requestMapConfig); +}; diff --git a/lib/cartodb/models/mapconfig/named_map_provider.js b/lib/cartodb/models/mapconfig/named_map_provider.js index e285b2b7..e1ad6fea 100644 --- a/lib/cartodb/models/mapconfig/named_map_provider.js +++ b/lib/cartodb/models/mapconfig/named_map_provider.js @@ -6,6 +6,7 @@ var step = require('step'); var MapConfig = require('windshaft').model.MapConfig; var templateName = require('../../backends/template_maps').templateName; var QueryTables = require('cartodb-query-tables'); +var SqlWrapMapConfigAdapter = require('./adapter/sql-wrap-mapconfig-adapter'); /** * @constructor @@ -22,6 +23,7 @@ function NamedMapMapConfigProvider(templateMaps, pgConnection, metadataBackend, this.turboCartoAdapter = turboCartoAdapter; this.analysisMapConfigAdapter = analysisMapConfigAdapter; this.overviewsAdapter = overviewsAdapter; + this.sqlWrapMapConfigAdapter = new SqlWrapMapConfigAdapter(); this.owner = owner; this.templateName = templateName(templateId); @@ -95,6 +97,10 @@ NamedMapMapConfigProvider.prototype.getMapConfig = function(callback) { assert.ifError(err); return self.templateMaps.instance(self.template, templateParams); }, + function prepareSqlWrap(err, requestMapConfig) { + assert.ifError(err); + self.sqlWrapMapConfigAdapter.getMapConfig(requestMapConfig, this); + }, function prepareAnalysisLayers(err, requestMapConfig) { assert.ifError(err); var analysisConfiguration = { diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index c3b9529d..04f931c5 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "windshaft-cartodb", - "version": "2.42.3", + "version": "2.44.0", "dependencies": { "body-parser": { "version": "1.14.2", diff --git a/package.json b/package.json index ecf28729..c426011b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "windshaft-cartodb", - "version": "2.43.1", + "version": "2.44.0", "description": "A map tile server for CartoDB", "keywords": [ "cartodb" diff --git a/test/acceptance/sql-wrap.js b/test/acceptance/sql-wrap.js new file mode 100644 index 00000000..7b3c25d8 --- /dev/null +++ b/test/acceptance/sql-wrap.js @@ -0,0 +1,52 @@ +require('../support/test_helper'); + +var assert = require('../support/assert'); +var TestClient = require('../support/test-client'); + +describe('sql-wrap', function() { + + afterEach(function(done) { + if (this.testClient) { + this.testClient.drain(done); + } else { + return done(); + } + }); + + it('should use sql_wrap from layer options', function(done) { + var mapConfig = { + version: '1.5.0', + layers: [ + { + "type": "cartodb", + "options": { + "sql": "SELECT * FROM populated_places_simple_reduced", + "sql_wrap": "SELECT * FROM (<%= sql %>) _w WHERE adm0_a3 = 'USA'", + "cartocss": [ + "#points {", + " marker-fill-opacity: 1;", + " marker-line-color: #FFF;", + " marker-line-width: 0.5;", + " marker-line-opacity: 1;", + " marker-placement: point;", + " marker-type: ellipse;", + " marker-width: 8;", + " marker-fill: red;", + " marker-allow-overlap: true;", + "}" + ].join('\n'), + "cartocss_version": "2.3.0" + } + } + ] + }; + + this.testClient = new TestClient(mapConfig, 1234); + this.testClient.getTile(0, 0, 0, function(err, tile, img) { + assert.ok(!err, err); + var fixtureImg = './test/fixtures/sql-wrap-usa-filter.png'; + assert.imageIsSimilarToFile(img, fixtureImg, 20, done); + }); + }); + +}); diff --git a/test/fixtures/sql-wrap-usa-filter.png b/test/fixtures/sql-wrap-usa-filter.png new file mode 100644 index 0000000000000000000000000000000000000000..30bd4eb341b814a73a88b99177b2f0461d811189 GIT binary patch literal 4197 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 literal 0 HcmV?d00001