Merge pull request #260 from CartoDB/medusa-improvements

Medusa improvements
This commit is contained in:
Raul Ochoa 2015-02-16 11:34:23 +01:00
commit afa625e3d2
24 changed files with 1571 additions and 63 deletions

View File

@ -2,7 +2,9 @@ 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
- psql -c "CREATE EXTENSION postgis" template_postgis

View File

@ -18,7 +18,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/cache/model/*.js \
test/integration/*.js \
test/acceptance/*.js \
test/acceptance/cache/*.js
check-submodules:
PATH="$$PATH:$(srcdir)/node_modules/.bin/"; \

11
NEWS.md
View File

@ -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
--------------------

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -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: {

View File

@ -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: {

View File

@ -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: {

View File

@ -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: {

View File

@ -0,0 +1,56 @@
# 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
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
## 1.0.0
- Initial version

View File

@ -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';
@ -25,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);
@ -48,12 +48,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'),

View File

@ -0,0 +1,120 @@
var queue = require('queue-async');
var _ = require('underscore');
var Datasource = require('windshaft').Datasource;
function MapConfigNamedLayersAdapter(templateMaps) {
this.templateMaps = templateMaps;
}
module.exports = MapConfigNamedLayersAdapter;
MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbMetadata, callback) {
var self = this;
var adaptLayersQueue = queue(layers.length);
function adaptLayer(layer, done) {
if (isNamedTypeLayer(layer)) {
if (!layer.options.name) {
return done(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 || !template) {
return done(new Error("Template '" + templateName + "' of user '" + username + "' not found"));
}
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 done(nestedNamedMapsError);
}
try {
var templateLayergroupConfig = self.templateMaps.instance(template, templateConfigParams);
return done(null, {
datasource: true,
layers: templateLayergroupConfig.layers
});
} catch (err) {
return done(err);
}
} else {
var unauthorizedError = new Error("Unauthorized '" + templateName + "' template instantiation");
unauthorizedError.http_status = 403;
return done(unauthorizedError);
}
});
} else {
return done(null, {
datasource: false,
layers: [layer]
});
}
}
var datasourceBuilder = new Datasource.Builder();
function layersAdaptQueueFinish(err, layersResults) {
if (err) {
return callback(err);
}
if (!layersResults || layersResults.length === 0) {
return callback(new Error('Missing layers array from layergroup config'));
}
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());
}
};
function isNamedTypeLayer(layer) {
return layer.type === 'named';
}

View File

@ -3,6 +3,8 @@ var Step = require('step');
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) ) {
@ -32,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();
@ -102,6 +106,13 @@ 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;
var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
/* This whole block is about generating X-Cache-Channel { */
// TODO: review lifetime of elements of this cache
@ -294,6 +305,23 @@ module.exports = function(redisPool) {
});
};
me.beforeLayergroupCreate = function(req, requestMapConfig, callback) {
mapConfigNamedLayersAdapter.getLayers(this.userByReq(req), requestMapConfig.layers, this, 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)
});
};
me.afterLayergroupCreate = function(req, mapconfig, response, callback) {
var token = response.layergroupid;
@ -343,14 +371,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');
@ -822,5 +852,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;
};

24
npm-shrinkwrap.json generated
View File

@ -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",
@ -178,24 +183,17 @@
"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.36.0",
"from": "windshaft@~0.36.0",
"dependencies": {
"chronograph": {
"version": "0.1.0",
"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",
"resolved": "https://github.com/CartoDB/grainstore/tarball/0.22.1",
"version": "0.23.0",
"from": "grainstore@~0.23.0",
"dependencies": {
"carto": {
"version": "0.9.5-cdb2",
@ -1637,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"
}
}
},

View File

@ -25,8 +25,9 @@
"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": "~0.36.0",
"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",

View File

@ -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);
});

View File

@ -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,

View File

@ -0,0 +1,527 @@
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 RedisPool = require('redis-mpool');
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
var Step = require('step');
var _ = require('underscore');
suite('named_layers', 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 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
]
}
};
suiteSetup(function(done) {
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: true};
templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) {
if (err) {
return done(err);
}
templateMaps.addTemplate(username, tokenAuthTemplate, function(err) {
if (err) {
return done(err);
}
templateMaps.addTemplate(username, template, function(err) {
return done(err);
});
});
});
});
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);
}
);
});
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;
}
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;
}
//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*/) {
// 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 = {
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) {
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: false};
templateMaps.delTemplate(username, nestedNamedMapTemplateName, function(err) {
if (err) {
return done(err);
}
templateMaps.delTemplate(username, tokenAuthTemplateName, function(err) {
if (err) {
return done(err);
}
templateMaps.delTemplate(username, templateName, function(err) {
return done(err);
});
});
});
});
});

View File

@ -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);
});
});
});

View File

@ -0,0 +1,311 @@
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 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 = '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']
},
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 = '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, template, done);
});
test('should fail for named map layer with missing name', function(done) {
var missingNamedMapLayerConfig = makeNamedMapLayerConfig({
config: {}
});
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();
});
});
test('should fail for non-existing template name', function(done) {
var missingTemplateName = 'wadus';
var nonExistentNamedMapLayerConfig = makeNamedMapLayerConfig({
name: missingTemplateName
});
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();
});
});
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, 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);
});
});
});
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, 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);
});
});
});
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, 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();
});
});
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, serverOptions, function(err, layers, datasource) {
assert.ok(!err);
assert.equal(layers.length, 1);
assert.notEqual(datasource.getLayerDatasource(0), undefined);
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, 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);
});
});
});
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, 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);
});
});
});
suiteTeardown(function(done) {
templateMaps.delTemplate(username, templateName, done);
});
});

View File

@ -78,6 +78,12 @@ 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 |
psql -v ON_ERROR_STOP=1 ${TEST_DB} || exit 1
fi
if test x"$PREPARE_REDIS" = xyes; then

View File

@ -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;

View File

@ -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 <Relation-Name>
-- 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;

View File

@ -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;