Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47217909f6 | ||
|
|
3a0a366230 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -8,6 +8,6 @@ tools/munin/windshaft.conf
|
||||
logs/
|
||||
pids/
|
||||
redis.pid
|
||||
*.log
|
||||
test.log
|
||||
npm-debug.log
|
||||
coverage/
|
||||
.DS_Store
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
|
||||
// "eqnull" : false, // true: Tolerate use of `== null`
|
||||
// "es5" : false, // true: Allow ES5 syntax (ex: getters and setters)
|
||||
"esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
// "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
// "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
|
||||
// // (ex: `for each`, multiple try/catch, function expression…)
|
||||
// "evil" : false, // true: Tolerate use of `eval` and `new Function()`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
1. Test (make clean all check), fix if broken before proceeding
|
||||
2. Ensure proper version in package.json
|
||||
3. Ensure NEWS section exists for the new version, review it, add release date
|
||||
4. If there are modified dependencies in package.json, update them with `yarn upgrade {{package_name}}@{{version}}`
|
||||
4. Recreate yarn.lock with: `yarn upgrade`
|
||||
5. Commit package.json, yarn.lock, NEWS
|
||||
6. git tag -a Major.Minor.Patch # use NEWS section as content
|
||||
7. Stub NEWS/package for next version
|
||||
|
||||
@@ -5,7 +5,7 @@ Make sure that you have the requirements needed. These are
|
||||
|
||||
- Core
|
||||
- Node.js >=6.9.x
|
||||
- yarn >=0.27.5 <1.0.0
|
||||
- yarn >=0.21.3
|
||||
- PostgreSQL >8.3.x, PostGIS >1.5.x
|
||||
- Redis >2.4.0 (http://www.redis.io)
|
||||
- Mapnik >3.x. See [Installing Mapnik](https://github.com/CartoDB/Windshaft#installing-mapnik).
|
||||
|
||||
206
NEWS.md
206
NEWS.md
@@ -1,212 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## 4.0.0
|
||||
Released 2017-10-04
|
||||
|
||||
Backward incompatible changes:
|
||||
- Removes `list` dataview type.
|
||||
|
||||
Announcements:
|
||||
- Upgrades body-parser to 1.18.2.
|
||||
- Upgrades express to 4.16.0.
|
||||
- Upgrades debug to 3.1.0.
|
||||
- Upgrades request to 2.83.0.
|
||||
- Upgrades turbo-carto to [0.20.1](https://github.com/CartoDB/turbo-carto/releases/tag/0.20.1)
|
||||
- Upgrades cartodb-psql to [0.10.2](https://github.com/CartoDB/node-cartodb-psql/releases/tag/0.10.2).
|
||||
- Upgrades camshaft to [0.59.2](https://github.com/CartoDB/camshaft/releases/tag/0.59.2).
|
||||
- Upgrades windshaft to [3.3.3](https://github.com/CartoDB/windshaft/releases/tag/3.3.3).
|
||||
- Upgrades yarn minimum version requirement to v0.27.5
|
||||
|
||||
|
||||
## 3.13.0
|
||||
Released 2017-10-02
|
||||
- Upgrades camshaft, cartodb-query-tables, and turbo-carto: better support for query variables.
|
||||
|
||||
Bugfixes:
|
||||
- Bounding box parameter ignored in static named maps #735.
|
||||
- camhaft 0.59.1 fixes duplicate columns in aggregate-intersection analysis
|
||||
|
||||
## 3.12.10
|
||||
Released 2017-09-18
|
||||
- Upgrades windshaft to [3.3.2](https://github.com/CartoDB/windshaft/releases/tag/3.3.2).
|
||||
|
||||
## 3.12.9
|
||||
Released 2017-09-07
|
||||
|
||||
Bug fixes:
|
||||
- Do not use distinct when calculating quantiles. #743
|
||||
|
||||
## 3.12.8
|
||||
Released 2017-09-07
|
||||
|
||||
Bug fixes:
|
||||
- Integer out of range in date histograms. (https://github.com/CartoDB/support/issues/962)
|
||||
|
||||
## 3.12.7
|
||||
## 3.6.7
|
||||
Released 2017-09-01
|
||||
|
||||
- Upgrades camshaft to [0.58.1](https://github.com/CartoDB/camshaft/releases/tag/0.58.1).
|
||||
|
||||
|
||||
## 3.12.6
|
||||
Released 2017-08-31
|
||||
|
||||
- Upgrades camshaft to [0.58.0](https://github.com/CartoDB/camshaft/releases/tag/0.58.0).
|
||||
|
||||
|
||||
## 3.12.5
|
||||
Released 2017-08-24
|
||||
|
||||
- Upgrades camshaft to [0.57.0](https://github.com/CartoDB/camshaft/releases/tag/0.57.0).
|
||||
|
||||
|
||||
## 3.12.4
|
||||
Released 2017-08-23
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.56.0](https://github.com/CartoDB/camshaft/releases/tag/0.56.0).
|
||||
|
||||
## 3.12.3
|
||||
Released 2017-08-22
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.55.8](https://github.com/CartoDB/camshaft/releases/tag/0.55.8).
|
||||
|
||||
## 3.12.2
|
||||
Released 2017-08-16
|
||||
|
||||
Bug fixes:
|
||||
- Polygon count problems #725.
|
||||
|
||||
|
||||
## 3.12.1
|
||||
Released 2017-08-13
|
||||
- Upgrades cartodb-psql to [0.10.1](https://github.com/CartoDB/node-cartodb-psql/releases/tag/0.10.1).
|
||||
- Upgrades windshaft to [3.3.1](https://github.com/CartoDB/windshaft/releases/tag/3.3.1).
|
||||
- Upgrades camshaft to [0.55.7](https://github.com/CartoDB/camshaft/releases/tag/0.55.7).
|
||||
|
||||
|
||||
## 3.12.0
|
||||
Released 2017-08-10
|
||||
|
||||
Announcements:
|
||||
- Apply max tile response time for requests to layergoup, tiles, static maps, attributes and dataviews services #717.
|
||||
- Upgrades windshaft to [3.3.0](https://github.com/CartoDB/windshaft/releases/tag/3.3.0).
|
||||
- Upgrades cartodb-redis to [0.14.0](https://github.com/CartoDB/node-cartodb-redis/releases/tag/0.14.0).
|
||||
|
||||
|
||||
## 3.11.0
|
||||
Released 2017-08-08
|
||||
|
||||
Announcements:
|
||||
- Allow to override with any aggregation for histograms instantiated w/o aggregation.
|
||||
|
||||
Bug fixes:
|
||||
- Apply timezone after truncating the minimun date for each bin to calculate timestamps in time-series.
|
||||
- Support timestamp with timezones to calculate the number of bins in time-series.
|
||||
- Fixed issue related to name collision while building time-series query.
|
||||
|
||||
|
||||
## 3.10.1
|
||||
Released 2017-08-04
|
||||
|
||||
Bug fixes:
|
||||
- Exclude Infinities & NaNs from ramps #719.
|
||||
- Fixed issue in time-series when aggregation starts at 1970-01-01 (epoch) #720.
|
||||
|
||||
|
||||
## 3.10.0
|
||||
Released 2017-08-03
|
||||
|
||||
Announcements:
|
||||
- Improve time-series dataview, now supports date aggregations (e.g: daily, weekly, monthly, etc.) and timezones (UTC by default) #698.
|
||||
- Support special numeric values (±Infinity, NaN) for json responses #706
|
||||
|
||||
|
||||
## 3.9.8
|
||||
Released 2017-07-21
|
||||
|
||||
- Upgrades windshaft to [3.2.2](https://github.com/CartoDB/windshaft/releases/tag/3.2.2).
|
||||
|
||||
|
||||
## 3.9.7
|
||||
Released 2017-07-20
|
||||
|
||||
Bug fixes:
|
||||
- Respond with 204 (No content) when vector tile has no data #712
|
||||
|
||||
Announcements:
|
||||
- Upgrades turbo-carto to [0.19.2](https://github.com/CartoDB/turbo-carto/releases/tag/0.19.2)
|
||||
|
||||
|
||||
## 3.9.6
|
||||
Released 2017-07-11
|
||||
|
||||
- Dataviews: support for aggregation in search results #708
|
||||
|
||||
|
||||
## 3.9.5
|
||||
Released 2017-06-27
|
||||
|
||||
- Dataviews: support special numeric values (±Infinity, NaN) #700
|
||||
|
||||
|
||||
## 3.9.4
|
||||
Released 2017-06-22
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.55.6](https://github.com/CartoDB/camshaft/releases/tag/0.55.6).
|
||||
|
||||
## 3.9.3
|
||||
Released 2017-06-16
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.55.5](https://github.com/CartoDB/camshaft/releases/tag/0.55.5).
|
||||
|
||||
## 3.9.2
|
||||
Released 2017-06-16
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.55.4](https://github.com/CartoDB/camshaft/releases/tag/0.55.4).
|
||||
|
||||
## 3.9.1
|
||||
Released 2017-06-06
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.55.3](https://github.com/CartoDB/camshaft/releases/tag/0.55.3).
|
||||
|
||||
|
||||
## 3.9.0
|
||||
Released 2017-05-31
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [3.2.1](https://github.com/CartoDB/windshaft/releases/tag/3.2.1).
|
||||
- Add support to retrieve info about layer stats in map instantiation.
|
||||
- Upgrades camshaft to [0.55.2](https://github.com/CartoDB/camshaft/releases/tag/0.55.2).
|
||||
- Remove promise polyfill from turbo-carto adapter
|
||||
|
||||
|
||||
## 3.8.0
|
||||
Released 2017-05-22
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.55.0](https://github.com/CartoDB/camshaft/releases/tag/0.55.0).
|
||||
- Upgrades turbo-carto to [0.19.1](https://github.com/CartoDB/turbo-carto/releases/tag/0.19.1)
|
||||
|
||||
|
||||
## 3.7.1
|
||||
Released 2017-05-18
|
||||
|
||||
Bug fixes:
|
||||
- Fix buffersize assignment when is not defined in requested mapconfig.
|
||||
|
||||
|
||||
## 3.7.0
|
||||
Released 2017-05-18
|
||||
|
||||
Announcements:
|
||||
- Manage multiple values of buffer-size for different formats
|
||||
- Upgrades windshaft to [3.2.0](https://github.com/CartoDB/windshaft/releases/tag/3.2.0).
|
||||
- Adding a new cartodb-psql fixing a pg vulnerability.
|
||||
|
||||
|
||||
## 3.6.6
|
||||
|
||||
8
app.js
8
app.js
@@ -2,20 +2,14 @@ var http = require('http');
|
||||
var https = require('https');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
|
||||
var _ = require('underscore');
|
||||
var semver = require('semver');
|
||||
|
||||
// jshint undef:false
|
||||
var log = console.log.bind(console);
|
||||
var logError = console.error.bind(console);
|
||||
// jshint undef:true
|
||||
|
||||
var nodejsVersion = process.versions.node;
|
||||
if (!semver.satisfies(nodejsVersion, '>=6.9.0')) {
|
||||
logError(`Node version ${nodejsVersion} is not supported, please use Node.js 6.9 or higher.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var argv = require('yargs')
|
||||
.usage('Usage: $0 <environment> [options]')
|
||||
.help('h')
|
||||
|
||||
@@ -324,7 +324,8 @@ var config = {
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: true
|
||||
layerMetadata: true
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -324,7 +324,7 @@ var config = {
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: false
|
||||
layerMetadata: false
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -324,7 +324,7 @@ var config = {
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: true
|
||||
layerMetadata: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -318,7 +318,7 @@ var config = {
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: true
|
||||
layerMetadata: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,13 +8,50 @@ This specification describes an extension for
|
||||
|
||||
This extension depends on Analyses extension. It extends MapConfig with a new attribute: `dataviews`.
|
||||
|
||||
It makes possible to get tabular data from analysis nodes: aggregated lists, aggregations, and histograms.
|
||||
It makes possible to get tabular data from analysis nodes: lists, aggregated lists, aggregations, and histograms.
|
||||
|
||||
## 2.1. Dataview types
|
||||
|
||||
### List
|
||||
|
||||
A list is a simple result set per row where is possible to retrieve several columns from the original layer query.
|
||||
|
||||
Definition
|
||||
```
|
||||
{
|
||||
// REQUIRED
|
||||
// string, `type` the list type
|
||||
“type”: “list”,
|
||||
// REQUIRED
|
||||
// object, `options` dataview params
|
||||
“options”: {
|
||||
// REQUIRED
|
||||
// array, `columns` to select for the list
|
||||
“columns”: [“name”, “description”]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Expected output
|
||||
```
|
||||
{
|
||||
"type": "list",
|
||||
"rows": [
|
||||
{
|
||||
"{columnName1}": "val1",
|
||||
"{columnName2}": 100
|
||||
},
|
||||
{
|
||||
"{columnName1}": "val2",
|
||||
"{columnName2}": 200
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Aggregation
|
||||
|
||||
An aggregation is a list with aggregated results by a column and a given aggregation function.
|
||||
An aggregation is very similar to a list but results are aggregated by a column and a given aggregation function.
|
||||
|
||||
Definition
|
||||
```
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
var step = require('step');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param metadataBackend
|
||||
@@ -15,65 +13,16 @@ function UserLimitsApi(metadataBackend, options) {
|
||||
|
||||
module.exports = UserLimitsApi;
|
||||
|
||||
UserLimitsApi.prototype.getRenderLimits = function (username, apiKey, callback) {
|
||||
UserLimitsApi.prototype.getRenderLimits = function (username, callback) {
|
||||
var self = this;
|
||||
|
||||
var limits = {
|
||||
cacheOnTimeout: self.options.limits.cacheOnTimeout || false,
|
||||
render: self.options.limits.render || 0
|
||||
};
|
||||
|
||||
self.getTimeoutRenderLimit(username, apiKey, function (err, timeoutRenderLimit) {
|
||||
this.metadataBackend.getTilerRenderLimit(username, function handleTilerLimits(err, renderLimit) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (timeoutRenderLimit && timeoutRenderLimit.render) {
|
||||
if (Number.isFinite(timeoutRenderLimit.render)) {
|
||||
limits.render = timeoutRenderLimit.render;
|
||||
}
|
||||
}
|
||||
|
||||
return callback(null, limits);
|
||||
return callback(null, {
|
||||
cacheOnTimeout: self.options.limits.cacheOnTimeout || false,
|
||||
render: renderLimit || self.options.limits.render || 0
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
UserLimitsApi.prototype.getTimeoutRenderLimit = function (username, apiKey, callback) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function isAuthorized() {
|
||||
var next = this;
|
||||
|
||||
if (!apiKey) {
|
||||
return next(null, false);
|
||||
}
|
||||
|
||||
self.metadataBackend.getUserMapKey(username, function (err, userApiKey) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return next(null, userApiKey === apiKey);
|
||||
});
|
||||
},
|
||||
function getUserTimeoutRenderLimits(err, authorized) {
|
||||
var next = this;
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
self.metadataBackend.getUserTimeoutRenderLimits(username, function (err, timeoutRenderLimit) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
next(null, {
|
||||
render: authorized ? timeoutRenderLimit.render : timeoutRenderLimit.renderPublic
|
||||
});
|
||||
});
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,19 +43,53 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
|
||||
ownFilter = !!ownFilter;
|
||||
|
||||
var query = (ownFilter) ? dataviewDefinition.sql.own_filter_on : dataviewDefinition.sql.own_filter_off;
|
||||
var sourceId = dataviewDefinition.source.id; // node.id
|
||||
var layer = _.find(mapConfig.obj().layers, function(l) {
|
||||
return l.options.source && (l.options.source.id === sourceId);
|
||||
});
|
||||
var queryRewriteData = layer && layer.options.query_rewrite_data;
|
||||
if (queryRewriteData && dataviewDefinition.node.type === 'source') {
|
||||
queryRewriteData = _.extend({}, queryRewriteData, {
|
||||
filters: dataviewDefinition.node.filters,
|
||||
unfiltered_query: dataviewDefinition.sql.own_filter_on
|
||||
});
|
||||
}
|
||||
|
||||
if (params.bbox) {
|
||||
var bboxFilter = new BBoxFilter({column: 'the_geom_webmercator', srid: 3857}, {bbox: params.bbox});
|
||||
query = bboxFilter.sql(query);
|
||||
if ( queryRewriteData ) {
|
||||
var bbox_filter_definition = {
|
||||
type: 'bbox',
|
||||
options: {
|
||||
column: 'the_geom_webmercator',
|
||||
srid: 3857
|
||||
},
|
||||
params: {
|
||||
bbox: params.bbox
|
||||
}
|
||||
};
|
||||
queryRewriteData = _.extend(queryRewriteData, { bbox_filter: bbox_filter_definition });
|
||||
}
|
||||
}
|
||||
|
||||
var queryRewriteData = getQueryRewriteData(mapConfig, dataviewDefinition, params);
|
||||
|
||||
var dataviewFactory = DataviewFactoryWithOverviews.getFactory(
|
||||
overviewsQueryRewriter, queryRewriteData, { bbox: params.bbox }
|
||||
);
|
||||
|
||||
var overrideParams = _.reduce(_.pick(params, 'start', 'end', 'bins'),
|
||||
function castNumbers(overrides, val, k) {
|
||||
if (!Number.isFinite(+val)) {
|
||||
throw new Error('Invalid number format for parameter \'' + k + '\'');
|
||||
}
|
||||
overrides[k] = +val;
|
||||
return overrides;
|
||||
},
|
||||
{ownFilter: ownFilter}
|
||||
);
|
||||
|
||||
var dataview = dataviewFactory.getDataview(query, dataviewDefinition);
|
||||
dataview.getResult(pg, getOverrideParams(params, ownFilter), this);
|
||||
dataview.getResult(pg, overrideParams, this);
|
||||
},
|
||||
function returnCallback(err, result) {
|
||||
return callback(err, result);
|
||||
@@ -63,56 +97,6 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
|
||||
);
|
||||
};
|
||||
|
||||
function getQueryRewriteData(mapConfig, dataviewDefinition, params) {
|
||||
var sourceId = dataviewDefinition.source.id; // node.id
|
||||
var layer = _.find(mapConfig.obj().layers, function(l) {
|
||||
return l.options.source && (l.options.source.id === sourceId);
|
||||
});
|
||||
var queryRewriteData = layer && layer.options.query_rewrite_data;
|
||||
if (queryRewriteData && dataviewDefinition.node.type === 'source') {
|
||||
queryRewriteData = _.extend({}, queryRewriteData, {
|
||||
filters: dataviewDefinition.node.filters,
|
||||
unfiltered_query: dataviewDefinition.sql.own_filter_on
|
||||
});
|
||||
}
|
||||
|
||||
if (params.bbox && queryRewriteData) {
|
||||
var bbox_filter_definition = {
|
||||
type: 'bbox',
|
||||
options: {
|
||||
column: 'the_geom_webmercator',
|
||||
srid: 3857
|
||||
},
|
||||
params: {
|
||||
bbox: params.bbox
|
||||
}
|
||||
};
|
||||
queryRewriteData = _.extend(queryRewriteData, { bbox_filter: bbox_filter_definition });
|
||||
}
|
||||
|
||||
return queryRewriteData;
|
||||
}
|
||||
|
||||
function getOverrideParams(params, ownFilter) {
|
||||
var overrideParams = _.reduce(_.pick(params, 'start', 'end', 'bins', 'offset'),
|
||||
function castNumbers(overrides, val, k) {
|
||||
if (!Number.isFinite(+val)) {
|
||||
throw new Error('Invalid number format for parameter \'' + k + '\'');
|
||||
}
|
||||
overrides[k] = +val;
|
||||
return overrides;
|
||||
},
|
||||
{ownFilter: ownFilter}
|
||||
);
|
||||
|
||||
// validation will be delegated to the proper dataview
|
||||
if (params.aggregation !== undefined) {
|
||||
overrideParams.aggregation = params.aggregation;
|
||||
}
|
||||
|
||||
return overrideParams;
|
||||
}
|
||||
|
||||
DataviewBackend.prototype.search = function (mapConfigProvider, user, params, callback) {
|
||||
var dataviewName = params.dataviewName;
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
function EmptyLayerStats(types) {
|
||||
this._types = types || {};
|
||||
}
|
||||
|
||||
EmptyLayerStats.prototype.is = function (type) {
|
||||
return this._types[type] ? this._types[type] : false;
|
||||
};
|
||||
|
||||
EmptyLayerStats.prototype.getStats =
|
||||
function (layer, dbConnection, callback) {
|
||||
setImmediate(function() {
|
||||
callback(null, {});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = EmptyLayerStats;
|
||||
@@ -1,23 +0,0 @@
|
||||
var LayerStats = require('./layer-stats');
|
||||
var EmptyLayerStats = require('./empty-layer-stats');
|
||||
var MapnikLayerStats = require('./mapnik-layer-stats');
|
||||
var TorqueLayerStats = require('./torque-layer-stats');
|
||||
|
||||
module.exports = function LayerStatsFactory(type) {
|
||||
var layerStatsIterator = [];
|
||||
var selectedType = type || 'ALL';
|
||||
|
||||
if (selectedType === 'ALL') {
|
||||
layerStatsIterator.push(new EmptyLayerStats({ http: true, plain: true }));
|
||||
layerStatsIterator.push(new MapnikLayerStats());
|
||||
layerStatsIterator.push(new TorqueLayerStats());
|
||||
} else if (selectedType === 'mapnik') {
|
||||
layerStatsIterator.push(new EmptyLayerStats({ http: true, plain: true, torque: true }));
|
||||
layerStatsIterator.push(new MapnikLayerStats());
|
||||
} else if (selectedType === 'torque') {
|
||||
layerStatsIterator.push(new EmptyLayerStats({ http: true, plain: true, mapnik: true }));
|
||||
layerStatsIterator.push(new TorqueLayerStats());
|
||||
}
|
||||
|
||||
return new LayerStats(layerStatsIterator);
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
var queue = require('queue-async');
|
||||
|
||||
function LayerStats(layerStatsIterator) {
|
||||
this.layerStatsIterator = layerStatsIterator;
|
||||
}
|
||||
|
||||
LayerStats.prototype.getStats = function (mapConfig, dbConnection, callback) {
|
||||
var self = this;
|
||||
var stats = [];
|
||||
|
||||
if (!mapConfig.getLayers().length) {
|
||||
return callback(null, stats);
|
||||
}
|
||||
var metaQueue = queue(mapConfig.getLayers().length);
|
||||
mapConfig.getLayers().forEach(function (layer, layerId) {
|
||||
var layerType = mapConfig.layerType(layerId);
|
||||
|
||||
for (var i = 0; i < self.layerStatsIterator.length; i++) {
|
||||
if (self.layerStatsIterator[i].is(layerType)) {
|
||||
var getStats = self.layerStatsIterator[i].getStats.bind(self.layerStatsIterator[i]);
|
||||
metaQueue.defer(getStats, layer, dbConnection);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
metaQueue.awaitAll(function (err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!results) {
|
||||
return callback(null, null);
|
||||
}
|
||||
|
||||
mapConfig.getLayers().forEach(function (layer, layerIndex) {
|
||||
stats[layerIndex] = results[layerIndex];
|
||||
});
|
||||
|
||||
return callback(err, stats);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
module.exports = LayerStats;
|
||||
@@ -1,28 +0,0 @@
|
||||
var queryUtils = require('../../utils/query-utils');
|
||||
|
||||
function MapnikLayerStats () {
|
||||
this._types = {
|
||||
mapnik: true,
|
||||
cartodb: true
|
||||
};
|
||||
}
|
||||
|
||||
MapnikLayerStats.prototype.is = function (type) {
|
||||
return this._types[type] ? this._types[type] : false;
|
||||
};
|
||||
|
||||
MapnikLayerStats.prototype.getStats =
|
||||
function (layer, dbConnection, callback) {
|
||||
var queryRowCountSql = queryUtils.getQueryRowCount(layer.options.sql);
|
||||
// This query would gather stats for postgresql table if not exists
|
||||
dbConnection.query(queryRowCountSql, function (err, res) {
|
||||
if (err) {
|
||||
return callback(null, {estimatedFeatureCount: -1});
|
||||
} else {
|
||||
// We decided that the relation is 1 row == 1 feature
|
||||
return callback(null, {estimatedFeatureCount: res.rows[0].rows});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = MapnikLayerStats;
|
||||
@@ -1,16 +0,0 @@
|
||||
function TorqueLayerStats() {
|
||||
this._types = {
|
||||
torque: true
|
||||
};
|
||||
}
|
||||
|
||||
TorqueLayerStats.prototype.is = function (type) {
|
||||
return this._types[type] ? this._types[type] : false;
|
||||
};
|
||||
|
||||
TorqueLayerStats.prototype.getStats =
|
||||
function (layer, dbConnection, callback) {
|
||||
return callback(null, {});
|
||||
};
|
||||
|
||||
module.exports = TorqueLayerStats;
|
||||
@@ -1,16 +0,0 @@
|
||||
var layerStats = require('./layer-stats/factory');
|
||||
|
||||
function StatsBackend() {
|
||||
}
|
||||
|
||||
module.exports = StatsBackend;
|
||||
|
||||
StatsBackend.prototype.getStats = function(mapConfig, dbConnection, callback) {
|
||||
var enabledFeatures = global.environment.enabledFeatures;
|
||||
var layerStatsEnabled = enabledFeatures ? enabledFeatures.layerStats: false;
|
||||
if (layerStatsEnabled) {
|
||||
layerStats().getStats(mapConfig, dbConnection, callback);
|
||||
} else {
|
||||
return callback(null, []);
|
||||
}
|
||||
};
|
||||
@@ -296,7 +296,7 @@ TemplateMaps.prototype.delTemplate = function(owner, tpl_id, callback) {
|
||||
// @param callback function(err)
|
||||
//
|
||||
TemplateMaps.prototype.updTemplate = function(owner, tpl_id, template, callback) {
|
||||
|
||||
|
||||
var self = this;
|
||||
|
||||
template = templateDefaults(template);
|
||||
@@ -430,17 +430,13 @@ var _reNumber = /^([-+]?[\d\.]?\d+([eE][+-]?\d+)?)$/,
|
||||
_reCSSColorVal = /^#[0-9a-fA-F]{3,6}$/;
|
||||
|
||||
function _replaceVars (str, params) {
|
||||
// Construct regular expressions for each param
|
||||
//return _.template(str, params); // lazy way, possibly dangerous
|
||||
// Construct regular expressions for each param
|
||||
Object.keys(params).forEach(function(k) {
|
||||
str = str.replace(new RegExp("<%=\\s*" + k + "\\s*%>", "g"), params[k]);
|
||||
});
|
||||
return str;
|
||||
}
|
||||
|
||||
function isObject(val) {
|
||||
return ( _.isObject(val) && !_.isArray(val) && !_.isFunction(val));
|
||||
}
|
||||
|
||||
TemplateMaps.prototype.instance = function(template, params) {
|
||||
var all_params = {};
|
||||
var phold = template.placeholders || {};
|
||||
@@ -478,13 +474,6 @@ TemplateMaps.prototype.instance = function(template, params) {
|
||||
|
||||
// NOTE: we're deep-cloning the layergroup here
|
||||
var layergroup = JSON.parse(JSON.stringify(template.layergroup));
|
||||
|
||||
if (layergroup.buffersize && isObject(layergroup.buffersize)) {
|
||||
Object.keys(layergroup.buffersize).forEach(function(k) {
|
||||
layergroup.buffersize[k] = parseInt(_replaceVars(layergroup.buffersize[k], all_params), 10);
|
||||
});
|
||||
}
|
||||
|
||||
for (var i=0; i<layergroup.layers.length; ++i) {
|
||||
var lyropt = layergroup.layers[i].options;
|
||||
|
||||
|
||||
@@ -10,21 +10,15 @@ function createTemplate(method) {
|
||||
'max({{=it._column}}) max_val,',
|
||||
'avg({{=it._column}}) avg_val,',
|
||||
method,
|
||||
'FROM ({{=it._sql}}) _table_sql WHERE {{=it._column}} IS NOT NULL',
|
||||
'AND',
|
||||
' {{=it._column}} != \'infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'-infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'NaN\'::float'
|
||||
'FROM ({{=it._sql}}) _table_sql WHERE {{=it._column}} IS NOT NULL'
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
var methods = {
|
||||
quantiles: 'CDB_QuantileBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as quantiles',
|
||||
quantiles: 'CDB_QuantileBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as quantiles',
|
||||
equal: 'CDB_EqualIntervalBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as equal',
|
||||
jenks: 'CDB_JenksBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as jenks',
|
||||
headtails: 'CDB_HeadsTailsBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as headtails'
|
||||
jenks: 'CDB_JenksBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as jenks',
|
||||
headtails: 'CDB_HeadsTailsBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as headtails'
|
||||
};
|
||||
|
||||
var methodTemplates = Object.keys(methods).reduce(function(methodTemplates, methodName) {
|
||||
|
||||
@@ -4,6 +4,9 @@ var _ = require('underscore');
|
||||
var step = require('step');
|
||||
var debug = require('debug')('windshaft:cartodb');
|
||||
|
||||
var LZMA = require('lzma').LZMA;
|
||||
var lzmaWorker = new LZMA();
|
||||
|
||||
// Whitelist query parameters and attach format
|
||||
var REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'config',
|
||||
@@ -14,8 +17,16 @@ var REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'zoom',
|
||||
'lon',
|
||||
'lat',
|
||||
// analysis
|
||||
'filters' // json
|
||||
// widgets & filters
|
||||
'filters', // json
|
||||
'own_filter', // 0, 1
|
||||
'bbox', // w,s,e,n
|
||||
'bins', // number
|
||||
'start', // number
|
||||
'end', // number
|
||||
'column_type', // string
|
||||
// widgets search
|
||||
'q'
|
||||
];
|
||||
|
||||
function BaseController(authApi, pgConnection) {
|
||||
@@ -25,7 +36,7 @@ function BaseController(authApi, pgConnection) {
|
||||
|
||||
module.exports = BaseController;
|
||||
|
||||
// jshint maxcomplexity:8
|
||||
// jshint maxcomplexity:10
|
||||
/**
|
||||
* Whitelist input and get database name & default geometry type from
|
||||
* subdomain/user metadata held in CartoDB Redis
|
||||
@@ -35,6 +46,35 @@ module.exports = BaseController;
|
||||
BaseController.prototype.req2params = function(req, callback){
|
||||
var self = this;
|
||||
|
||||
if ( req.query.lzma ) {
|
||||
|
||||
// Decode (from base64)
|
||||
var lzma = new Buffer(req.query.lzma, 'base64')
|
||||
.toString('binary')
|
||||
.split('')
|
||||
.map(function(c) {
|
||||
return c.charCodeAt(0) - 128;
|
||||
});
|
||||
|
||||
|
||||
// Decompress
|
||||
lzmaWorker.decompress(
|
||||
lzma,
|
||||
function(result) {
|
||||
req.profiler.done('lzma');
|
||||
try {
|
||||
delete req.query.lzma;
|
||||
_.extend(req.query, JSON.parse(result));
|
||||
self.req2params(req, callback);
|
||||
} catch (err) {
|
||||
req.profiler.done('req2params');
|
||||
callback(new Error('Error parsing lzma as JSON: ' + err));
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var allowedQueryParams = REQUEST_QUERY_PARAMS_WHITELIST;
|
||||
if (Array.isArray(req.context.allowedQueryParams)) {
|
||||
allowedQueryParams = allowedQueryParams.concat(req.context.allowedQueryParams);
|
||||
@@ -155,19 +195,12 @@ BaseController.prototype.send = function(req, res, body, status, headers) {
|
||||
|
||||
BaseController.prototype.sendError = function(req, res, err, label) {
|
||||
var allErrors = Array.isArray(err) ? err : [err];
|
||||
|
||||
allErrors = populateTimeoutErrors(allErrors);
|
||||
|
||||
label = label || 'UNKNOWN';
|
||||
err = allErrors[0] || new Error(label);
|
||||
allErrors[0] = err;
|
||||
|
||||
var statusCode = findStatusCode(err);
|
||||
|
||||
if (err.message === 'Tile does not exist' && req.params.format === 'mvt') {
|
||||
statusCode = 204;
|
||||
}
|
||||
|
||||
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
|
||||
|
||||
// If a callback was requested, force status to 200
|
||||
@@ -192,18 +225,6 @@ function stripConnectionInfo(message) {
|
||||
.replace(/is the server.*encountered/im, 'encountered');
|
||||
}
|
||||
|
||||
var ERROR_INFO_TO_EXPOSE = {
|
||||
message: true,
|
||||
layer: true,
|
||||
type: true,
|
||||
analysis: true,
|
||||
subtype: true
|
||||
};
|
||||
|
||||
function shouldBeExposed (prop) {
|
||||
return !!ERROR_INFO_TO_EXPOSE[prop];
|
||||
}
|
||||
|
||||
function errorMessage(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
@@ -222,7 +243,7 @@ function errorMessageWithContext(err) {
|
||||
|
||||
for (var prop in err) {
|
||||
// type & message are properties from Error's prototype and will be skipped
|
||||
if (err.hasOwnProperty(prop) && shouldBeExposed(prop)) {
|
||||
if (err.hasOwnProperty(prop)) {
|
||||
error[prop] = err[prop];
|
||||
}
|
||||
}
|
||||
@@ -264,38 +285,5 @@ function statusFromErrorMessage(errMsg) {
|
||||
statusCode = 404;
|
||||
}
|
||||
}
|
||||
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
function isRenderTimeoutError (err) {
|
||||
return err.message === 'Render timed out';
|
||||
}
|
||||
|
||||
function isDatasourceTimeoutError (err) {
|
||||
return err.message && err.message.match(/canceling statement due to statement timeout/i);
|
||||
}
|
||||
|
||||
function isTimeoutError (err) {
|
||||
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
|
||||
}
|
||||
|
||||
function populateTimeoutErrors (errors) {
|
||||
return errors.map(function (error) {
|
||||
if (isRenderTimeoutError(error)) {
|
||||
error.subtype = 'render';
|
||||
}
|
||||
|
||||
if (isDatasourceTimeoutError(error)) {
|
||||
error.subtype = 'datasource';
|
||||
}
|
||||
|
||||
if (isTimeoutError(error)) {
|
||||
error.message = 'You are over platform\'s limits. Please contact us to know more details';
|
||||
error.type = 'limit';
|
||||
error.http_status = 429;
|
||||
}
|
||||
|
||||
return error;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,51 +79,19 @@ LayergroupController.prototype.register = function(app) {
|
||||
|
||||
// Undocumented/non-supported API endpoint methods.
|
||||
// Use at your own peril.
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/dataview/:dataviewName', cors(), userMiddleware,
|
||||
this.dataview.bind(this));
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/widget/:dataviewName', cors(), userMiddleware,
|
||||
this.dataview.bind(this));
|
||||
|
||||
var allowedDataviewQueryParams = [
|
||||
'filters', // json
|
||||
'own_filter', // 0, 1
|
||||
'bbox', // w,s,e,n
|
||||
'start', // number
|
||||
'end', // number
|
||||
'column_type', // string
|
||||
'bins', // number
|
||||
'aggregation', //string
|
||||
'offset', // number
|
||||
'q' // widgets search
|
||||
];
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/dataview/:dataviewName',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.dataview.bind(this)
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:layer/widget/:dataviewName',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.dataview.bind(this)
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/dataview/:dataviewName/search',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.dataviewSearch.bind(this)
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:layer/widget/:dataviewName/search',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.dataviewSearch.bind(this)
|
||||
);
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/dataview/:dataviewName/search', cors(), userMiddleware,
|
||||
this.dataviewSearch.bind(this));
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/widget/:dataviewName/search', cors(), userMiddleware,
|
||||
this.dataviewSearch.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/analysis/node/:nodeId', cors(), userMiddleware,
|
||||
@@ -285,9 +253,7 @@ LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, t
|
||||
grid_json: true,
|
||||
json_torque: true,
|
||||
torque_json: true,
|
||||
png: true,
|
||||
png32: true,
|
||||
mvt: true
|
||||
png: true
|
||||
};
|
||||
|
||||
var formatStat = 'invalid';
|
||||
|
||||
@@ -20,7 +20,6 @@ var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
var NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider');
|
||||
var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/create-layergroup-provider');
|
||||
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
* @param {PgConnection} pgConnection
|
||||
@@ -31,12 +30,10 @@ var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/cr
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @param {MapConfigAdapter} mapConfigAdapter
|
||||
* @param {StatsBackend} statsBackend
|
||||
* @constructor
|
||||
*/
|
||||
function MapController(authApi, pgConnection, templateMaps, mapBackend, metadataBackend,
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, mapConfigAdapter,
|
||||
statsBackend) {
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, mapConfigAdapter) {
|
||||
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
@@ -50,8 +47,6 @@ function MapController(authApi, pgConnection, templateMaps, mapBackend, metadata
|
||||
|
||||
this.mapConfigAdapter = mapConfigAdapter;
|
||||
this.resourceLocator = new ResourceLocator(global.environment);
|
||||
|
||||
this.statsBackend = statsBackend;
|
||||
}
|
||||
|
||||
util.inherits(MapController, BaseController);
|
||||
@@ -219,6 +214,7 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
|
||||
|
||||
var mapConfigProvider;
|
||||
var mapConfig;
|
||||
|
||||
step(
|
||||
function setupParams(){
|
||||
self.req2params(req, this);
|
||||
@@ -253,9 +249,7 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
|
||||
},
|
||||
function afterLayergroupCreate(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup,
|
||||
mapConfigProvider.analysesResults,
|
||||
this);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup, mapConfigProvider.analysesResults, this);
|
||||
},
|
||||
function finishTemplateInstantiation(err, layergroup) {
|
||||
if (err) {
|
||||
@@ -278,8 +272,7 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
|
||||
);
|
||||
};
|
||||
|
||||
MapController.prototype.afterLayergroupCreate =
|
||||
function(req, res, mapconfig, layergroup, analysesResults, callback) {
|
||||
MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, layergroup, analysesResults, callback) {
|
||||
var self = this;
|
||||
|
||||
var username = req.context.user;
|
||||
@@ -325,7 +318,6 @@ function(req, res, mapconfig, layergroup, analysesResults, callback) {
|
||||
|
||||
var dbName = req.params.dbname;
|
||||
var layergroupId = layergroup.layergroupid;
|
||||
var dbConnection;
|
||||
|
||||
step(
|
||||
function getPgConnection() {
|
||||
@@ -333,8 +325,7 @@ function(req, res, mapconfig, layergroup, analysesResults, callback) {
|
||||
},
|
||||
function getAffectedTablesAndLastUpdatedTime(err, connection) {
|
||||
assert.ifError(err);
|
||||
dbConnection = connection;
|
||||
QueryTables.getAffectedTablesFromQuery(dbConnection, sql.join(';'), this);
|
||||
QueryTables.getAffectedTablesFromQuery(connection, sql.join(';'), this);
|
||||
},
|
||||
function handleAffectedTablesAndLastUpdatedTime(err, result) {
|
||||
req.profiler.done('queryTablesAndLastUpdated');
|
||||
@@ -361,21 +352,6 @@ function(req, res, mapconfig, layergroup, analysesResults, callback) {
|
||||
|
||||
return null;
|
||||
},
|
||||
function fetchLayersStats(err) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
self.statsBackend.getStats(mapconfig, dbConnection, function(err, layersStats) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (layersStats.length > 0) {
|
||||
layergroup.metadata.layers.forEach(function (layer, index) {
|
||||
layer.meta.stats = layersStats[index];
|
||||
});
|
||||
}
|
||||
return next();
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
|
||||
@@ -32,8 +32,7 @@ NamedMapsController.prototype.register = function(app) {
|
||||
this.tile.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/static/named/:template_id/:width/:height.:format', cors(), userMiddleware,
|
||||
allowQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
|
||||
'/static/named/:template_id/:width/:height.:format', cors(), userMiddleware, allowQueryParams(['layer']),
|
||||
this.staticMap.bind(this));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var LZMA = require('lzma').LZMA;
|
||||
|
||||
var lzmaWorker = new LZMA();
|
||||
|
||||
module.exports = function lzmaMiddleware(req, res, next) {
|
||||
if (!req.query.hasOwnProperty('lzma')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Decode (from base64)
|
||||
var lzma = new Buffer(req.query.lzma, 'base64')
|
||||
.toString('binary')
|
||||
.split('')
|
||||
.map(function(c) {
|
||||
return c.charCodeAt(0) - 128;
|
||||
});
|
||||
|
||||
// Decompress
|
||||
lzmaWorker.decompress(lzma, function(result) {
|
||||
try {
|
||||
delete req.query.lzma;
|
||||
Object.assign(req.query, JSON.parse(result));
|
||||
next();
|
||||
} catch (err) {
|
||||
next(new Error('Error parsing lzma as JSON: ' + err));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,178 +1,71 @@
|
||||
const BaseDataview = require('./base');
|
||||
const debug = require('debug')('windshaft:dataview:aggregation');
|
||||
var _ = require('underscore');
|
||||
var BaseWidget = require('./base');
|
||||
var debug = require('debug')('windshaft:widget:aggregation');
|
||||
|
||||
const filteredQueryTpl = ctx => `
|
||||
filtered_source AS (
|
||||
SELECT *
|
||||
FROM (${ctx.query}) _cdb_filtered_source
|
||||
${ctx.aggregationColumn && ctx.isFloatColumn ? `
|
||||
WHERE
|
||||
${ctx.aggregationColumn} != 'infinity'::float
|
||||
AND
|
||||
${ctx.aggregationColumn} != '-infinity'::float
|
||||
AND
|
||||
${ctx.aggregationColumn} != 'NaN'::float` :
|
||||
''
|
||||
}
|
||||
)
|
||||
`;
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
const summaryQueryTpl = ctx => `
|
||||
summary AS (
|
||||
SELECT
|
||||
count(1) AS count,
|
||||
sum(CASE WHEN ${ctx.column} IS NULL THEN 1 ELSE 0 END) AS nulls_count
|
||||
${ctx.isFloatColumn ? `,
|
||||
sum(
|
||||
CASE
|
||||
WHEN ${ctx.aggregationColumn} = 'infinity'::float OR ${ctx.aggregationColumn} = '-infinity'::float
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
) AS infinities_count,
|
||||
sum(CASE WHEN ${ctx.aggregationColumn} = 'NaN'::float THEN 1 ELSE 0 END) AS nans_count` :
|
||||
''
|
||||
}
|
||||
FROM (${ctx.query}) _cdb_aggregation_nulls
|
||||
)
|
||||
`;
|
||||
var summaryQueryTpl = dot.template([
|
||||
'summary AS (',
|
||||
' SELECT',
|
||||
' count(1) AS count,',
|
||||
' sum(CASE WHEN {{=it._column}} IS NULL THEN 1 ELSE 0 END) AS nulls_count',
|
||||
' FROM ({{=it._query}}) _cdb_aggregation_nulls',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
const rankedCategoriesQueryTpl = ctx => `
|
||||
categories AS(
|
||||
SELECT
|
||||
${ctx.column} AS category,
|
||||
${ctx.aggregationFn} AS value,
|
||||
row_number() OVER (ORDER BY ${ctx.aggregationFn} desc) as rank
|
||||
FROM filtered_source
|
||||
${ctx.aggregationColumn !== null ? `WHERE ${ctx.aggregationColumn} IS NOT NULL` : ''}
|
||||
GROUP BY ${ctx.column}
|
||||
ORDER BY 2 DESC
|
||||
)
|
||||
`;
|
||||
var rankedCategoriesQueryTpl = dot.template([
|
||||
'categories AS(',
|
||||
' SELECT {{=it._column}} AS category, {{=it._aggregation}} AS value,',
|
||||
' row_number() OVER (ORDER BY {{=it._aggregation}} desc) as rank',
|
||||
' FROM ({{=it._query}}) _cdb_aggregation_all',
|
||||
' {{?it._aggregationColumn!==null}}WHERE {{=it._aggregationColumn}} IS NOT NULL{{?}}',
|
||||
' GROUP BY {{=it._column}}',
|
||||
' ORDER BY 2 DESC',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
const categoriesSummaryMinMaxQueryTpl = () => `
|
||||
categories_summary_min_max AS(
|
||||
SELECT
|
||||
max(value) max_val,
|
||||
min(value) min_val
|
||||
FROM categories
|
||||
)
|
||||
`;
|
||||
var categoriesSummaryMinMaxQueryTpl = dot.template([
|
||||
'categories_summary_min_max AS(',
|
||||
' SELECT max(value) max_val, min(value) min_val',
|
||||
' FROM categories',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
const categoriesSummaryCountQueryTpl = ctx => `
|
||||
categories_summary_count AS(
|
||||
SELECT count(1) AS categories_count
|
||||
FROM (
|
||||
SELECT ${ctx.column} AS category
|
||||
FROM (${ctx.query}) _cdb_categories
|
||||
GROUP BY ${ctx.column}
|
||||
) _cdb_categories_count
|
||||
)
|
||||
`;
|
||||
var categoriesSummaryCountQueryTpl = dot.template([
|
||||
'categories_summary_count AS(',
|
||||
' SELECT count(1) AS categories_count',
|
||||
' FROM (',
|
||||
' SELECT {{=it._column}} AS category',
|
||||
' FROM ({{=it._query}}) _cdb_categories',
|
||||
' GROUP BY {{=it._column}}',
|
||||
' ) _cdb_categories_count',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
const specialNumericValuesColumns = () => `, nans_count, infinities_count`;
|
||||
var rankedAggregationQueryTpl = dot.template([
|
||||
'SELECT CAST(category AS text), value, false as agg, nulls_count, min_val, max_val, count, categories_count',
|
||||
' FROM categories, summary, categories_summary_min_max, categories_summary_count',
|
||||
' WHERE rank < {{=it._limit}}',
|
||||
'UNION ALL',
|
||||
'SELECT \'Other\' category, {{=it._aggregationFn}}(value) as value, true as agg, nulls_count, min_val, max_val,',
|
||||
' count, categories_count',
|
||||
' FROM categories, summary, categories_summary_min_max, categories_summary_count',
|
||||
' WHERE rank >= {{=it._limit}}',
|
||||
'GROUP BY nulls_count, min_val, max_val, count, categories_count'
|
||||
].join('\n'));
|
||||
|
||||
const rankedAggregationQueryTpl = ctx => `
|
||||
SELECT
|
||||
CAST(category AS text),
|
||||
value,
|
||||
false as agg,
|
||||
nulls_count,
|
||||
min_val,
|
||||
max_val,
|
||||
count,
|
||||
categories_count
|
||||
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
||||
FROM categories, summary, categories_summary_min_max, categories_summary_count
|
||||
WHERE rank < ${ctx.limit}
|
||||
UNION ALL
|
||||
SELECT
|
||||
'Other' category,
|
||||
${ctx.aggregation !== 'count' ? ctx.aggregation : 'sum'}(value) as value,
|
||||
true as agg,
|
||||
nulls_count,
|
||||
min_val,
|
||||
max_val,
|
||||
count,
|
||||
categories_count
|
||||
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
||||
FROM categories, summary, categories_summary_min_max, categories_summary_count
|
||||
WHERE rank >= ${ctx.limit}
|
||||
GROUP BY
|
||||
nulls_count,
|
||||
min_val,
|
||||
max_val,
|
||||
count,
|
||||
categories_count
|
||||
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
||||
`;
|
||||
var aggregationQueryTpl = dot.template([
|
||||
'SELECT CAST({{=it._column}} AS text) AS category, {{=it._aggregation}} AS value, false as agg,',
|
||||
' nulls_count, min_val, max_val, count, categories_count',
|
||||
'FROM ({{=it._query}}) _cdb_aggregation_all, summary, categories_summary_min_max, categories_summary_count',
|
||||
'GROUP BY category, nulls_count, min_val, max_val, count, categories_count',
|
||||
'ORDER BY value DESC'
|
||||
].join('\n'));
|
||||
|
||||
const aggregationQueryTpl = ctx => `
|
||||
SELECT
|
||||
CAST(${ctx.column} AS text) AS category,
|
||||
${ctx.aggregationFn} AS value,
|
||||
false as agg,
|
||||
nulls_count,
|
||||
min_val,
|
||||
max_val,
|
||||
count,
|
||||
categories_count
|
||||
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
||||
FROM (${ctx.query}) _cdb_aggregation_all, summary, categories_summary_min_max, categories_summary_count
|
||||
GROUP BY
|
||||
category,
|
||||
nulls_count,
|
||||
min_val,
|
||||
max_val,
|
||||
count,
|
||||
categories_count
|
||||
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
||||
ORDER BY value DESC
|
||||
`;
|
||||
var CATEGORIES_LIMIT = 6;
|
||||
|
||||
const aggregationFnQueryTpl = ctx => `${ctx.aggregation}(${ctx.aggregationColumn})`;
|
||||
|
||||
const aggregationDataviewQueryTpl = ctx => `
|
||||
WITH
|
||||
${filteredQueryTpl(ctx)},
|
||||
${summaryQueryTpl(ctx)},
|
||||
${rankedCategoriesQueryTpl(ctx)},
|
||||
${categoriesSummaryMinMaxQueryTpl(ctx)},
|
||||
${categoriesSummaryCountQueryTpl(ctx)}
|
||||
${!!ctx.override.ownFilter ? `${aggregationQueryTpl(ctx)}` : `${rankedAggregationQueryTpl(ctx)}`}
|
||||
`;
|
||||
|
||||
const filterCategoriesQueryTpl = ctx => `
|
||||
SELECT
|
||||
${ctx.column} AS category,
|
||||
${ctx.value} AS value
|
||||
FROM (${ctx.query}) _cdb_aggregation_search
|
||||
WHERE CAST(${ctx.column} as text) ILIKE ${ctx.userQuery}
|
||||
GROUP BY ${ctx.column}
|
||||
`;
|
||||
|
||||
const searchQueryTpl = ctx => `
|
||||
WITH
|
||||
search_unfiltered AS (
|
||||
${ctx.searchUnfiltered}
|
||||
),
|
||||
search_filtered AS (
|
||||
${ctx.searchFiltered}
|
||||
),
|
||||
search_union AS (
|
||||
SELECT * FROM search_unfiltered
|
||||
UNION ALL
|
||||
SELECT * FROM search_filtered
|
||||
)
|
||||
SELECT category, sum(value) AS value
|
||||
FROM search_union
|
||||
GROUP BY category
|
||||
ORDER BY value desc
|
||||
`;
|
||||
|
||||
const CATEGORIES_LIMIT = 6;
|
||||
|
||||
const VALID_OPERATIONS = {
|
||||
var VALID_OPERATIONS = {
|
||||
count: [],
|
||||
sum: ['aggregationColumn'],
|
||||
avg: ['aggregationColumn'],
|
||||
@@ -180,7 +73,7 @@ const VALID_OPERATIONS = {
|
||||
max: ['aggregationColumn']
|
||||
};
|
||||
|
||||
const TYPE = 'aggregation';
|
||||
var TYPE = 'aggregation';
|
||||
|
||||
/**
|
||||
{
|
||||
@@ -191,150 +84,210 @@ const TYPE = 'aggregation';
|
||||
}
|
||||
}
|
||||
*/
|
||||
module.exports = class Aggregation extends BaseDataview {
|
||||
constructor (query, options = {}, queries = {}) {
|
||||
super();
|
||||
|
||||
this._checkOptions(options);
|
||||
|
||||
this.query = query;
|
||||
this.queries = queries;
|
||||
this.column = options.column;
|
||||
this.aggregation = options.aggregation;
|
||||
this.aggregationColumn = options.aggregationColumn;
|
||||
this._isFloatColumn = null;
|
||||
function Aggregation(query, options) {
|
||||
if (!_.isString(options.column)) {
|
||||
throw new Error('Aggregation expects `column` in widget options');
|
||||
}
|
||||
|
||||
_checkOptions (options) {
|
||||
if (typeof options.column !== 'string') {
|
||||
throw new Error(`Aggregation expects 'column' in dataview options`);
|
||||
}
|
||||
|
||||
if (typeof options.aggregation !== 'string') {
|
||||
throw new Error(`Aggregation expects 'aggregation' operation in dataview options`);
|
||||
}
|
||||
|
||||
if (!VALID_OPERATIONS[options.aggregation]) {
|
||||
throw new Error(`Aggregation does not support '${options.aggregation}' operation`);
|
||||
}
|
||||
|
||||
const requiredOptions = VALID_OPERATIONS[options.aggregation];
|
||||
const missingOptions = requiredOptions.filter(requiredOption => !options.hasOwnProperty(requiredOption));
|
||||
|
||||
if (missingOptions.length > 0) {
|
||||
throw new Error(
|
||||
`Aggregation '${options.aggregation}' is missing some options: ${missingOptions.join(',')}`
|
||||
);
|
||||
}
|
||||
if (!_.isString(options.aggregation)) {
|
||||
throw new Error('Aggregation expects `aggregation` operation in widget options');
|
||||
}
|
||||
|
||||
sql (psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
if (this._shouldCheckColumnType()) {
|
||||
this._isFloatColumn = false;
|
||||
this.getColumnType(psql, this.aggregationColumn, this.queries.no_filters, (err, type) => {
|
||||
if (!err && !!type) {
|
||||
this._isFloatColumn = type.float;
|
||||
}
|
||||
this.sql(psql, override, callback);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const aggregationSql = aggregationDataviewQueryTpl({
|
||||
override: override,
|
||||
query: this.query,
|
||||
column: this.column,
|
||||
aggregation: this.aggregation,
|
||||
aggregationColumn: this.aggregation !== 'count' ? this.aggregationColumn : null,
|
||||
aggregationFn: aggregationFnQueryTpl({
|
||||
aggregation: this.aggregation,
|
||||
aggregationColumn: this.aggregationColumn || 1
|
||||
}),
|
||||
isFloatColumn: this._isFloatColumn,
|
||||
limit: CATEGORIES_LIMIT
|
||||
});
|
||||
|
||||
debug(aggregationSql);
|
||||
|
||||
return callback(null, aggregationSql);
|
||||
if (!VALID_OPERATIONS[options.aggregation]) {
|
||||
throw new Error("Aggregation does not support '" + options.aggregation + "' operation");
|
||||
}
|
||||
|
||||
_shouldCheckColumnType () {
|
||||
return this.aggregationColumn && this._isFloatColumn === null;
|
||||
var requiredOptions = VALID_OPERATIONS[options.aggregation];
|
||||
var missingOptions = _.difference(requiredOptions, Object.keys(options));
|
||||
if (missingOptions.length > 0) {
|
||||
throw new Error(
|
||||
"Aggregation '" + options.aggregation + "' is missing some options: " + missingOptions.join(',')
|
||||
);
|
||||
}
|
||||
|
||||
format (result) {
|
||||
const {
|
||||
count = 0,
|
||||
nulls_count = 0,
|
||||
nans_count = 0,
|
||||
infinities_count = 0,
|
||||
min_val = 0,
|
||||
max_val = 0,
|
||||
categories_count = 0
|
||||
} = result.rows[0] || {};
|
||||
BaseWidget.apply(this);
|
||||
|
||||
return {
|
||||
aggregation: this.aggregation,
|
||||
count: count,
|
||||
nulls: nulls_count,
|
||||
nans: nans_count,
|
||||
infinities: infinities_count,
|
||||
min: min_val,
|
||||
max: max_val,
|
||||
categoriesCount: categories_count,
|
||||
categories: result.rows.map(({ category, value, agg }) => ({ category, value, agg }))
|
||||
};
|
||||
this.query = query;
|
||||
this.column = options.column;
|
||||
this.aggregation = options.aggregation;
|
||||
this.aggregationColumn = options.aggregationColumn;
|
||||
}
|
||||
|
||||
Aggregation.prototype = new BaseWidget();
|
||||
Aggregation.prototype.constructor = Aggregation;
|
||||
|
||||
module.exports = Aggregation;
|
||||
|
||||
Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
search (psql, userQuery, callback) {
|
||||
const escapedUserQuery = psql.escapeLiteral(`%${userQuery}%`);
|
||||
const value = this.aggregation !== 'count' && this.aggregationColumn ?
|
||||
`${this.aggregation}(${this.aggregationColumn})` :
|
||||
'count(1)';
|
||||
var _query = this.query;
|
||||
|
||||
// TODO unfiltered will be wrong as filters are already applied at this point
|
||||
const query = searchQueryTpl({
|
||||
searchUnfiltered: filterCategoriesQueryTpl({
|
||||
query: this.query,
|
||||
column: this.column,
|
||||
value: '0',
|
||||
userQuery: escapedUserQuery
|
||||
}),
|
||||
searchFiltered: filterCategoriesQueryTpl({
|
||||
query: this.query,
|
||||
column: this.column,
|
||||
value: value,
|
||||
userQuery: escapedUserQuery
|
||||
var aggregationSql;
|
||||
|
||||
if (!!override.ownFilter) {
|
||||
aggregationSql = [
|
||||
this.getCategoriesCTESql(_query, this.column, this.aggregation, this.aggregationColumn),
|
||||
aggregationQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql(),
|
||||
_limit: CATEGORIES_LIMIT
|
||||
})
|
||||
].join('\n');
|
||||
} else {
|
||||
aggregationSql = [
|
||||
this.getCategoriesCTESql(_query, this.column, this.aggregation, this.aggregationColumn),
|
||||
rankedAggregationQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregationFn: this.aggregation !== 'count' ? this.aggregation : 'sum',
|
||||
_limit: CATEGORIES_LIMIT
|
||||
})
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
debug(aggregationSql);
|
||||
|
||||
return callback(null, aggregationSql);
|
||||
};
|
||||
|
||||
Aggregation.prototype.getCategoriesCTESql = function(query, column, aggregation, aggregationColumn) {
|
||||
return [
|
||||
"WITH",
|
||||
[
|
||||
summaryQueryTpl({
|
||||
_query: query,
|
||||
_column: column
|
||||
}),
|
||||
rankedCategoriesQueryTpl({
|
||||
_query: query,
|
||||
_column: column,
|
||||
_aggregation: this.getAggregationSql(),
|
||||
_aggregationColumn: aggregation !== 'count' ? aggregationColumn : null
|
||||
}),
|
||||
categoriesSummaryMinMaxQueryTpl({
|
||||
_query: query,
|
||||
_column: column
|
||||
}),
|
||||
categoriesSummaryCountQueryTpl({
|
||||
_query: query,
|
||||
_column: column
|
||||
})
|
||||
].join(',\n')
|
||||
].join('\n');
|
||||
};
|
||||
|
||||
var aggregationFnQueryTpl = dot.template('{{=it._aggregationFn}}({{=it._aggregationColumn}})');
|
||||
Aggregation.prototype.getAggregationSql = function() {
|
||||
return aggregationFnQueryTpl({
|
||||
_aggregationFn: this.aggregation,
|
||||
_aggregationColumn: this.aggregationColumn || 1
|
||||
});
|
||||
};
|
||||
|
||||
Aggregation.prototype.format = function(result) {
|
||||
var categories = [];
|
||||
var count = 0;
|
||||
var nulls = 0;
|
||||
var minValue = 0;
|
||||
var maxValue = 0;
|
||||
var categoriesCount = 0;
|
||||
|
||||
|
||||
if (result.rows.length) {
|
||||
var firstRow = result.rows[0];
|
||||
count = firstRow.count;
|
||||
nulls = firstRow.nulls_count;
|
||||
minValue = firstRow.min_val;
|
||||
maxValue = firstRow.max_val;
|
||||
categoriesCount = firstRow.categories_count;
|
||||
|
||||
result.rows.forEach(function(row) {
|
||||
categories.push(_.omit(row, 'count', 'nulls_count', 'min_val', 'max_val', 'categories_count'));
|
||||
});
|
||||
|
||||
debug(query);
|
||||
|
||||
psql.query(query, (err, result) => {
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
}
|
||||
|
||||
return callback(null, {type: this.getType(), categories: result.rows });
|
||||
}, true); // use read-only transaction
|
||||
}
|
||||
|
||||
getType () {
|
||||
return TYPE;
|
||||
}
|
||||
return {
|
||||
aggregation: this.aggregation,
|
||||
count: count,
|
||||
nulls: nulls,
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
categoriesCount: categoriesCount,
|
||||
categories: categories
|
||||
};
|
||||
};
|
||||
|
||||
toString () {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
var filterCategoriesQueryTpl = dot.template([
|
||||
'SELECT {{=it._column}} AS category, {{=it._value}} AS value',
|
||||
'FROM ({{=it._query}}) _cdb_aggregation_search',
|
||||
'WHERE CAST({{=it._column}} as text) ILIKE {{=it._userQuery}}',
|
||||
'GROUP BY {{=it._column}}'
|
||||
].join('\n'));
|
||||
|
||||
var searchQueryTpl = dot.template([
|
||||
'WITH',
|
||||
'search_unfiltered AS (',
|
||||
' {{=it._searchUnfiltered}}',
|
||||
'),',
|
||||
'search_filtered AS (',
|
||||
' {{=it._searchFiltered}}',
|
||||
'),',
|
||||
'search_union AS (',
|
||||
' SELECT * FROM search_unfiltered',
|
||||
' UNION ALL',
|
||||
' SELECT * FROM search_filtered',
|
||||
')',
|
||||
'SELECT category, sum(value) AS value',
|
||||
'FROM search_union',
|
||||
'GROUP BY category',
|
||||
'ORDER BY value desc'
|
||||
].join('\n'));
|
||||
|
||||
|
||||
Aggregation.prototype.search = function(psql, userQuery, callback) {
|
||||
var self = this;
|
||||
|
||||
var _userQuery = psql.escapeLiteral('%' + userQuery + '%');
|
||||
|
||||
// TODO unfiltered will be wrong as filters are already applied at this point
|
||||
var query = searchQueryTpl({
|
||||
_searchUnfiltered: filterCategoriesQueryTpl({
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_aggregation: this.aggregation
|
||||
});
|
||||
}
|
||||
_value: '0',
|
||||
_userQuery: _userQuery
|
||||
}),
|
||||
_searchFiltered: filterCategoriesQueryTpl({
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_value: 'count(1)',
|
||||
_userQuery: _userQuery
|
||||
})
|
||||
});
|
||||
|
||||
psql.query(query, function(err, result) {
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
}
|
||||
|
||||
return callback(null, {type: self.getType(), categories: result.rows });
|
||||
}, true); // use read-only transaction
|
||||
};
|
||||
|
||||
Aggregation.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
|
||||
Aggregation.prototype.toString = function() {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_aggregation: this.aggregation
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,59 +1,26 @@
|
||||
const FLOAT_OIDS = {
|
||||
700: true,
|
||||
701: true,
|
||||
1700: true
|
||||
};
|
||||
function BaseDataview() {}
|
||||
|
||||
const DATE_OIDS = {
|
||||
1082: true,
|
||||
1114: true,
|
||||
1184: true
|
||||
};
|
||||
module.exports = BaseDataview;
|
||||
|
||||
const columnTypeQueryTpl = ctx => `SELECT pg_typeof(${ctx.column})::oid FROM (${ctx.query}) _cdb_column_type limit 1`;
|
||||
BaseDataview.prototype.getResult = function(psql, override, callback) {
|
||||
var self = this;
|
||||
this.sql(psql, override, function(err, query) {
|
||||
psql.query(query, function(err, result) {
|
||||
|
||||
function getPGTypeName (pgType) {
|
||||
return {
|
||||
float: FLOAT_OIDS.hasOwnProperty(pgType),
|
||||
date: DATE_OIDS.hasOwnProperty(pgType)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = class BaseDataview {
|
||||
getResult (psql, override, callback) {
|
||||
this.sql(psql, override, (err, query) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
return callback(err, result);
|
||||
}
|
||||
|
||||
psql.query(query, (err, result) => {
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
}
|
||||
result = self.format(result, override);
|
||||
result.type = self.getType();
|
||||
|
||||
result = this.format(result, override);
|
||||
result.type = this.getType();
|
||||
return callback(null, result);
|
||||
|
||||
return callback(null, result);
|
||||
}, true); // use read-only transaction
|
||||
});
|
||||
|
||||
}, true); // use read-only transaction
|
||||
});
|
||||
}
|
||||
|
||||
search (psql, userQuery, callback) {
|
||||
return callback(null, this.format({ rows: [] }));
|
||||
}
|
||||
|
||||
getColumnType (psql, column, query, callback) {
|
||||
const readOnlyTransaction = true;
|
||||
const columnTypeQuery = columnTypeQueryTpl({ column, query });
|
||||
|
||||
psql.query(columnTypeQuery, (err, result) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
const pgType = result.rows[0].pg_typeof;
|
||||
callback(null, getPGTypeName(pgType));
|
||||
}, readOnlyTransaction);
|
||||
}
|
||||
};
|
||||
|
||||
BaseDataview.prototype.search = function(psql, userQuery, callback) {
|
||||
return callback(null, this.format({ rows: [] }));
|
||||
};
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
const dataviews = require('./');
|
||||
var dataviews = require('./');
|
||||
|
||||
module.exports = class DataviewFactory {
|
||||
static get dataviews() {
|
||||
return Object.keys(dataviews).reduce((allDataviews, dataviewClassName) => {
|
||||
allDataviews[dataviewClassName.toLowerCase()] = dataviews[dataviewClassName];
|
||||
return allDataviews;
|
||||
}, {});
|
||||
}
|
||||
|
||||
static getDataview (query, dataviewDefinition) {
|
||||
const { type, options, sql } = dataviewDefinition;
|
||||
var DataviewFactory = {
|
||||
dataviews: Object.keys(dataviews).reduce(function(allDataviews, dataviewClassName) {
|
||||
allDataviews[dataviewClassName.toLowerCase()] = dataviews[dataviewClassName];
|
||||
return allDataviews;
|
||||
}, {}),
|
||||
|
||||
getDataview: function(query, dataviewDefinition) {
|
||||
var type = dataviewDefinition.type;
|
||||
if (!this.dataviews[type]) {
|
||||
throw new Error('Invalid dataview type: "' + type + '"');
|
||||
}
|
||||
|
||||
return new this.dataviews[type](query, options, sql);
|
||||
return new this.dataviews[type](query, dataviewDefinition.options, dataviewDefinition.sql);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = DataviewFactory;
|
||||
|
||||
@@ -1,36 +1,18 @@
|
||||
const BaseDataview = require('./base');
|
||||
const debug = require('debug')('windshaft:dataview:formula');
|
||||
var _ = require('underscore');
|
||||
var BaseWidget = require('./base');
|
||||
var debug = require('debug')('windshaft:widget:formula');
|
||||
|
||||
const countInfinitiesQueryTpl = ctx => `
|
||||
SELECT count(1) FROM (${ctx.query}) __cdb_formula_infinities
|
||||
WHERE ${ctx.column} = 'infinity'::float OR ${ctx.column} = '-infinity'::float
|
||||
`;
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
const countNansQueryTpl = ctx => `
|
||||
SELECT count(1) FROM (${ctx.query}) __cdb_formula_nans
|
||||
WHERE ${ctx.column} = 'NaN'::float
|
||||
`;
|
||||
var formulaQueryTpl = dot.template([
|
||||
'SELECT',
|
||||
'{{=it._operation}}({{=it._column}}) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n'));
|
||||
|
||||
const filterOutSpecialNumericValuesTpl = ctx => `
|
||||
WHERE
|
||||
${ctx.column} != 'infinity'::float
|
||||
AND
|
||||
${ctx.column} != '-infinity'::float
|
||||
AND
|
||||
${ctx.column} != 'NaN'::float
|
||||
`;
|
||||
|
||||
const formulaQueryTpl = ctx => `
|
||||
SELECT
|
||||
${ctx.operation}(${ctx.column}) AS result,
|
||||
(SELECT count(1) FROM (${ctx.query}) _cdb_formula_nulls WHERE ${ctx.column} IS NULL) AS nulls_count
|
||||
${ctx.isFloatColumn ? `,(${countInfinitiesQueryTpl(ctx)}) AS infinities_count` : ''}
|
||||
${ctx.isFloatColumn ? `,(${countNansQueryTpl(ctx)}) AS nans_count` : ''}
|
||||
FROM (${ctx.query}) __cdb_formula
|
||||
${ctx.isFloatColumn && ctx.operation !== 'count' ? `${filterOutSpecialNumericValuesTpl(ctx)}` : ''}
|
||||
`;
|
||||
|
||||
const VALID_OPERATIONS = {
|
||||
var VALID_OPERATIONS = {
|
||||
count: true,
|
||||
avg: true,
|
||||
sum: true,
|
||||
@@ -38,7 +20,7 @@ const VALID_OPERATIONS = {
|
||||
max: true
|
||||
};
|
||||
|
||||
const TYPE = 'formula';
|
||||
var TYPE = 'formula';
|
||||
|
||||
/**
|
||||
{
|
||||
@@ -49,90 +31,74 @@ const TYPE = 'formula';
|
||||
}
|
||||
}
|
||||
*/
|
||||
module.exports = class Formula extends BaseDataview {
|
||||
constructor (query, options = {}, queries = {}) {
|
||||
super();
|
||||
|
||||
this._checkOptions(options);
|
||||
|
||||
this.query = query;
|
||||
this.queries = queries;
|
||||
this.column = options.column || '1';
|
||||
this.operation = options.operation;
|
||||
this._isFloatColumn = null;
|
||||
function Formula(query, options) {
|
||||
if (!_.isString(options.operation)) {
|
||||
throw new Error('Formula expects `operation` in widget options');
|
||||
}
|
||||
|
||||
_checkOptions (options) {
|
||||
if (typeof options.operation !== 'string') {
|
||||
throw new Error(`Formula expects 'operation' in dataview options`);
|
||||
}
|
||||
|
||||
if (!VALID_OPERATIONS[options.operation]) {
|
||||
throw new Error(`Formula does not support '${options.operation}' operation`);
|
||||
}
|
||||
|
||||
if (options.operation !== 'count' && typeof options.column !== 'string') {
|
||||
throw new Error(`Formula expects 'column' in dataview options`);
|
||||
}
|
||||
if (!VALID_OPERATIONS[options.operation]) {
|
||||
throw new Error("Formula does not support '" + options.operation + "' operation");
|
||||
}
|
||||
|
||||
|
||||
sql (psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
if (this._isFloatColumn === null) {
|
||||
this._isFloatColumn = false;
|
||||
this.getColumnType(psql, this.column, this.queries.no_filters, (err, type) => {
|
||||
if (!err && !!type) {
|
||||
this._isFloatColumn = type.float;
|
||||
}
|
||||
this.sql(psql, override, callback);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const formulaSql = formulaQueryTpl({
|
||||
isFloatColumn: this._isFloatColumn,
|
||||
query: this.query,
|
||||
operation: this.operation,
|
||||
column: this.column
|
||||
});
|
||||
|
||||
debug(formulaSql);
|
||||
|
||||
return callback(null, formulaSql);
|
||||
if (options.operation !== 'count' && !_.isString(options.column)) {
|
||||
throw new Error('Formula expects `column` in widget options');
|
||||
}
|
||||
|
||||
format (res) {
|
||||
const {
|
||||
result = 0,
|
||||
nulls_count = 0,
|
||||
nans_count,
|
||||
infinities_count
|
||||
} = res.rows[0] || {};
|
||||
BaseWidget.apply(this);
|
||||
|
||||
return {
|
||||
operation: this.operation,
|
||||
result,
|
||||
nulls: nulls_count,
|
||||
nans: nans_count,
|
||||
infinities: infinities_count
|
||||
};
|
||||
this.query = query;
|
||||
this.column = options.column || '1';
|
||||
this.operation = options.operation;
|
||||
}
|
||||
|
||||
Formula.prototype = new BaseWidget();
|
||||
Formula.prototype.constructor = Formula;
|
||||
|
||||
module.exports = Formula;
|
||||
|
||||
Formula.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
getType () {
|
||||
return TYPE;
|
||||
}
|
||||
var _query = this.query;
|
||||
var formulaSql = formulaQueryTpl({
|
||||
_query: _query,
|
||||
_operation: this.operation,
|
||||
_column: this.column
|
||||
});
|
||||
|
||||
toString () {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_operation: this.operation
|
||||
});
|
||||
}
|
||||
debug(formulaSql);
|
||||
|
||||
return callback(null, formulaSql);
|
||||
};
|
||||
|
||||
Formula.prototype.format = function(result) {
|
||||
var formattedResult = {
|
||||
operation: this.operation,
|
||||
result: 0,
|
||||
nulls: 0
|
||||
};
|
||||
|
||||
if (result.rows.length) {
|
||||
formattedResult.operation = this.operation;
|
||||
formattedResult.result = result.rows[0].result;
|
||||
formattedResult.nulls = result.rows[0].nulls_count;
|
||||
}
|
||||
|
||||
return formattedResult;
|
||||
};
|
||||
|
||||
Formula.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
|
||||
Formula.prototype.toString = function() {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_operation: this.operation
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,72 +1,308 @@
|
||||
const debug = require('debug')('windshaft:dataview:histogram');
|
||||
const NumericHistogram = require('./histograms/numeric-histogram');
|
||||
const DateHistogram = require('./histograms/date-histogram');
|
||||
var _ = require('underscore');
|
||||
var BaseWidget = require('./base');
|
||||
var debug = require('debug')('windshaft:dataview:histogram');
|
||||
|
||||
const DATE_HISTOGRAM = 'DateHistogram';
|
||||
const NUMERIC_HISTOGRAM = 'NumericHistogram';
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
module.exports = class Histogram {
|
||||
constructor (query, options, queries) {
|
||||
this.query = query;
|
||||
this.options = options || {};
|
||||
this.queries = queries;
|
||||
var columnTypeQueryTpl = dot.template(
|
||||
'SELECT pg_typeof({{=it.column}})::oid FROM ({{=it.query}}) _cdb_histogram_column_type limit 1'
|
||||
);
|
||||
var columnCastTpl = dot.template("date_part('epoch', {{=it.column}})");
|
||||
|
||||
this.histogramImplementation = this._getHistogramImplementation();
|
||||
var BIN_MIN_NUMBER = 6;
|
||||
var BIN_MAX_NUMBER = 48;
|
||||
|
||||
var basicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._column}}) AS max_val, min({{=it._column}}) AS min_val,',
|
||||
' avg({{=it._column}}) AS avg_val, count(1) AS total_rows',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
var overrideBasicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._end}}) AS max_val, min({{=it._start}}) AS min_val,',
|
||||
' avg({{=it._column}}) AS avg_val, count(1) AS total_rows',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var iqrQueryTpl = dot.template([
|
||||
'iqrange AS (',
|
||||
' SELECT max(quartile_max) - min(quartile_max) AS iqr',
|
||||
' FROM (',
|
||||
' SELECT quartile, max(_cdb_iqr_column) AS quartile_max from (',
|
||||
' SELECT {{=it._column}} AS _cdb_iqr_column, ntile(4) over (order by {{=it._column}}',
|
||||
' ) AS quartile',
|
||||
' FROM ({{=it._query}}) _cdb_rank) _cdb_quartiles',
|
||||
' WHERE quartile = 1 or quartile = 3',
|
||||
' GROUP BY quartile',
|
||||
' ) _cdb_iqr',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var binsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
' SELECT CASE WHEN total_rows = 0 OR iqr = 0',
|
||||
' THEN 1',
|
||||
' ELSE GREATEST(',
|
||||
' LEAST({{=it._minBins}}, CAST(total_rows AS INT)),',
|
||||
' LEAST(',
|
||||
' CAST(((max_val - min_val) / (2 * iqr * power(total_rows, 1/3))) AS INT),',
|
||||
' {{=it._maxBins}}',
|
||||
' )',
|
||||
' )',
|
||||
' END AS bins_number',
|
||||
' FROM basics, iqrange, ({{=it._query}}) _cdb_bins',
|
||||
' LIMIT 1',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var overrideBinsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
' SELECT {{=it._bins}} AS bins_number',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var nullsQueryTpl = dot.template([
|
||||
'nulls AS (',
|
||||
' SELECT',
|
||||
' count(*) AS nulls_count',
|
||||
' FROM ({{=it._query}}) _cdb_histogram_nulls',
|
||||
' WHERE {{=it._column}} IS NULL',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var histogramQueryTpl = dot.template([
|
||||
'SELECT',
|
||||
' (max_val - min_val) / cast(bins_number as float) AS bin_width,',
|
||||
' bins_number,',
|
||||
' nulls_count,',
|
||||
' avg_val,',
|
||||
' CASE WHEN min_val = max_val',
|
||||
' THEN 0',
|
||||
' ELSE GREATEST(1, LEAST(WIDTH_BUCKET({{=it._column}}, min_val, max_val, bins_number), bins_number)) - 1',
|
||||
' END AS bin,',
|
||||
' min({{=it._column}})::numeric AS min,',
|
||||
' max({{=it._column}})::numeric AS max,',
|
||||
' avg({{=it._column}})::numeric AS avg,',
|
||||
' count(*) AS freq',
|
||||
'FROM ({{=it._query}}) _cdb_histogram, basics, nulls, bins',
|
||||
'WHERE {{=it._column}} IS NOT NULL',
|
||||
'GROUP BY bin, bins_number, bin_width, nulls_count, avg_val',
|
||||
'ORDER BY bin'
|
||||
].join('\n'));
|
||||
|
||||
|
||||
var TYPE = 'histogram';
|
||||
|
||||
/**
|
||||
{
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'name',
|
||||
bins: 10 // OPTIONAL
|
||||
}
|
||||
}
|
||||
*/
|
||||
function Histogram(query, options, queries) {
|
||||
if (!_.isString(options.column)) {
|
||||
throw new Error('Histogram expects `column` in widget options');
|
||||
}
|
||||
|
||||
_getHistogramImplementation (override) {
|
||||
let implementation = null;
|
||||
this.query = query;
|
||||
this.queries = queries;
|
||||
this.column = options.column;
|
||||
this.bins = options.bins;
|
||||
|
||||
switch (this._getHistogramSubtype(override)) {
|
||||
case DATE_HISTOGRAM:
|
||||
debug('Delegating to DateHistogram with options: %j and overriding: %j', this.options, override);
|
||||
implementation = new DateHistogram(this.query, this.options, this.queries);
|
||||
break;
|
||||
case NUMERIC_HISTOGRAM:
|
||||
debug('Delegating to NumericHistogram with options: %j and overriding: %j', this.options, override);
|
||||
implementation = new NumericHistogram(this.query, this.options, this.queries);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported Histogram type');
|
||||
}
|
||||
this._columnType = null;
|
||||
}
|
||||
|
||||
return implementation;
|
||||
}
|
||||
Histogram.prototype = new BaseWidget();
|
||||
Histogram.prototype.constructor = Histogram;
|
||||
|
||||
_getHistogramSubtype (override) {
|
||||
if(this._isDateHistogram(override)) {
|
||||
return DATE_HISTOGRAM;
|
||||
}
|
||||
module.exports = Histogram;
|
||||
|
||||
return NUMERIC_HISTOGRAM;
|
||||
}
|
||||
|
||||
_isDateHistogram (override = {}) {
|
||||
return (this.options.hasOwnProperty('aggregation') || override.hasOwnProperty('aggregation'));
|
||||
}
|
||||
|
||||
getResult (psql, override, callback) {
|
||||
this.histogramImplementation = this._getHistogramImplementation(override);
|
||||
this.histogramImplementation.getResult(psql, override, callback);
|
||||
}
|
||||
|
||||
// In order to keep previous behaviour with overviews,
|
||||
// we have to expose the following methods to bypass
|
||||
// the concrete overview implementation
|
||||
|
||||
sql (psql, override, callback) {
|
||||
this.histogramImplementation.sql(psql, override, callback);
|
||||
}
|
||||
|
||||
format (result, override) {
|
||||
return this.histogramImplementation.format(result, override);
|
||||
}
|
||||
|
||||
getType () {
|
||||
return this.histogramImplementation.getType();
|
||||
}
|
||||
|
||||
toString () {
|
||||
return this.histogramImplementation.toString();
|
||||
}
|
||||
var DATE_OIDS = {
|
||||
1082: true,
|
||||
1114: true,
|
||||
1184: true
|
||||
};
|
||||
|
||||
Histogram.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
var _column = this.column;
|
||||
|
||||
var columnTypeQuery = columnTypeQueryTpl({
|
||||
column: _column, query: this.queries.no_filters
|
||||
});
|
||||
|
||||
if (this._columnType === null) {
|
||||
psql.query(columnTypeQuery, function(err, result) {
|
||||
// assume numeric, will fail later
|
||||
self._columnType = 'numeric';
|
||||
if (!err && !!result.rows[0]) {
|
||||
var pgType = result.rows[0].pg_typeof;
|
||||
if (DATE_OIDS.hasOwnProperty(pgType)) {
|
||||
self._columnType = 'date';
|
||||
}
|
||||
}
|
||||
self.sql(psql, override, callback);
|
||||
}, true); // use read-only transaction
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this._columnType === 'date') {
|
||||
_column = columnCastTpl({column: _column});
|
||||
}
|
||||
|
||||
var _query = this.query;
|
||||
|
||||
var basicsQuery, binsQuery;
|
||||
|
||||
if (override && _.has(override, 'start') && _.has(override, 'end') && _.has(override, 'bins')) {
|
||||
debug('overriding with %j', override);
|
||||
basicsQuery = overrideBasicsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column,
|
||||
_start: getBinStart(override),
|
||||
_end: getBinEnd(override)
|
||||
});
|
||||
|
||||
binsQuery = [
|
||||
overrideBinsQueryTpl({
|
||||
_bins: override.bins
|
||||
})
|
||||
].join(',\n');
|
||||
} else {
|
||||
basicsQuery = basicsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
});
|
||||
|
||||
if (override && _.has(override, 'bins')) {
|
||||
binsQuery = [
|
||||
overrideBinsQueryTpl({
|
||||
_bins: override.bins
|
||||
})
|
||||
].join(',\n');
|
||||
} else {
|
||||
binsQuery = [
|
||||
iqrQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
}),
|
||||
binsQueryTpl({
|
||||
_query: _query,
|
||||
_minBins: BIN_MIN_NUMBER,
|
||||
_maxBins: BIN_MAX_NUMBER
|
||||
})
|
||||
].join(',\n');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var histogramSql = [
|
||||
"WITH",
|
||||
[
|
||||
basicsQuery,
|
||||
binsQuery,
|
||||
nullsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
].join(',\n'),
|
||||
histogramQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
].join('\n');
|
||||
|
||||
debug(histogramSql);
|
||||
|
||||
return callback(null, histogramSql);
|
||||
};
|
||||
|
||||
Histogram.prototype.format = function(result, override) {
|
||||
override = override || {};
|
||||
var buckets = [];
|
||||
|
||||
var binsCount = getBinsCount(override);
|
||||
var width = getWidth(override);
|
||||
var binsStart = getBinStart(override);
|
||||
var nulls = 0;
|
||||
var avg;
|
||||
|
||||
if (result.rows.length) {
|
||||
var firstRow = result.rows[0];
|
||||
binsCount = firstRow.bins_number;
|
||||
width = firstRow.bin_width || width;
|
||||
avg = firstRow.avg_val;
|
||||
nulls = firstRow.nulls_count;
|
||||
binsStart = override.hasOwnProperty('start') ? getBinStart(override) : firstRow.min;
|
||||
|
||||
buckets = result.rows.map(function(row) {
|
||||
return _.omit(row, 'bins_number', 'bin_width', 'nulls_count', 'avg_val');
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
bin_width: width,
|
||||
bins_count: binsCount,
|
||||
bins_start: binsStart,
|
||||
nulls: nulls,
|
||||
avg: avg,
|
||||
bins: buckets
|
||||
};
|
||||
};
|
||||
|
||||
function getBinStart(override) {
|
||||
if (override.hasOwnProperty('start') && override.hasOwnProperty('end')) {
|
||||
return Math.min(override.start, override.end);
|
||||
}
|
||||
return override.start || 0;
|
||||
}
|
||||
|
||||
function getBinEnd(override) {
|
||||
if (override.hasOwnProperty('start') && override.hasOwnProperty('end')) {
|
||||
return Math.max(override.start, override.end);
|
||||
}
|
||||
return override.end || 0;
|
||||
}
|
||||
|
||||
function getBinsCount(override) {
|
||||
return override.bins || 0;
|
||||
}
|
||||
|
||||
function getWidth(override) {
|
||||
var width = 0;
|
||||
var binsCount = override.bins;
|
||||
|
||||
if (binsCount && Number.isFinite(override.start) && Number.isFinite(override.end)) {
|
||||
width = (override.end - override.start) / binsCount;
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
Histogram.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
|
||||
Histogram.prototype.toString = function() {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_column: this.column,
|
||||
_query: this.query
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
const BaseDataview = require('../base');
|
||||
|
||||
const TYPE = 'histogram';
|
||||
|
||||
module.exports = class BaseHistogram extends BaseDataview {
|
||||
constructor (query, options, queries) {
|
||||
super();
|
||||
|
||||
if (typeof options.column !== 'string') {
|
||||
throw new Error('Histogram expects `column` in widget options');
|
||||
}
|
||||
|
||||
this.query = query;
|
||||
this.queries = queries;
|
||||
this.column = options.column;
|
||||
this.bins = options.bins;
|
||||
|
||||
this._columnType = null;
|
||||
}
|
||||
|
||||
sql (psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
if (this._columnType === null) {
|
||||
this.getColumnType(psql, this.column, this.queries.no_filters, (err, type) => {
|
||||
// assume numeric, will fail later
|
||||
this._columnType = 'numeric';
|
||||
if (!err && !!type) {
|
||||
this._columnType = Object.keys(type).find(function (key) {
|
||||
return type[key];
|
||||
});
|
||||
}
|
||||
this.sql(psql, override, callback);
|
||||
}, true); // use read-only transaction
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._buildQuery(psql, override, callback);
|
||||
}
|
||||
|
||||
format (result, override) {
|
||||
const histogram = this._getSummary(result, override);
|
||||
histogram.bins = this._getBuckets(result);
|
||||
return histogram;
|
||||
}
|
||||
|
||||
getType () {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
toString () {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_column: this.column,
|
||||
_query: this.query
|
||||
});
|
||||
}
|
||||
|
||||
_hasOverridenRange (override) {
|
||||
return override && override.hasOwnProperty('start') && override.hasOwnProperty('end');
|
||||
}
|
||||
|
||||
_getBinStart (override = {}) {
|
||||
if (this._hasOverridenRange(override)) {
|
||||
return Math.min(override.start, override.end);
|
||||
}
|
||||
|
||||
return override.start || 0;
|
||||
}
|
||||
|
||||
_getBinEnd (override = {}) {
|
||||
if (this._hasOverridenRange(override)) {
|
||||
return Math.max(override.start, override.end);
|
||||
}
|
||||
|
||||
return override.end || 0;
|
||||
}
|
||||
|
||||
_getBinsCount (override = {}) {
|
||||
return override.bins || 0;
|
||||
}
|
||||
};
|
||||
@@ -1,302 +0,0 @@
|
||||
const BaseHistogram = require('./base-histogram');
|
||||
const debug = require('debug')('windshaft:dataview:date-histogram');
|
||||
|
||||
const dateIntervalQueryTpl = ctx => `
|
||||
WITH
|
||||
__cdb_dates AS (
|
||||
SELECT
|
||||
MAX(${ctx.column}::timestamp) AS __cdb_end,
|
||||
MIN(${ctx.column}::timestamp) AS __cdb_start
|
||||
FROM (${ctx.query}) __cdb_source
|
||||
),
|
||||
__cdb_interval_in_days AS (
|
||||
SELECT
|
||||
DATE_PART('day', __cdb_end - __cdb_start) AS __cdb_days
|
||||
FROM __cdb_dates
|
||||
),
|
||||
__cdb_interval_in_hours AS (
|
||||
SELECT
|
||||
__cdb_days * 24 + DATE_PART('hour', __cdb_end - __cdb_start) AS __cdb_hours
|
||||
FROM __cdb_interval_in_days, __cdb_dates
|
||||
),
|
||||
__cdb_interval_in_minutes AS (
|
||||
SELECT
|
||||
__cdb_hours * 60 + DATE_PART('minute', __cdb_end - __cdb_start) AS __cdb_minutes
|
||||
FROM __cdb_interval_in_hours, __cdb_dates
|
||||
),
|
||||
__cdb_interval_in_seconds AS (
|
||||
SELECT
|
||||
__cdb_minutes * 60 + DATE_PART('second', __cdb_end - __cdb_start) AS __cdb_seconds
|
||||
FROM __cdb_interval_in_minutes, __cdb_dates
|
||||
)
|
||||
SELECT
|
||||
ROUND(__cdb_days / 365) AS year,
|
||||
ROUND(__cdb_days / 90) AS quarter,
|
||||
ROUND(__cdb_days / 30) AS month,
|
||||
ROUND(__cdb_days / 7) AS week,
|
||||
__cdb_days AS day,
|
||||
__cdb_hours AS hour,
|
||||
__cdb_minutes AS minute,
|
||||
__cdb_seconds AS second
|
||||
FROM __cdb_interval_in_days, __cdb_interval_in_hours, __cdb_interval_in_minutes, __cdb_interval_in_seconds
|
||||
`;
|
||||
|
||||
const nullsQueryTpl = ctx => `
|
||||
__cdb_nulls AS (
|
||||
SELECT
|
||||
count(*) AS __cdb_nulls_count
|
||||
FROM (${ctx.query}) __cdb_histogram_nulls
|
||||
WHERE ${ctx.column} IS NULL
|
||||
)
|
||||
`;
|
||||
|
||||
const dateBasicsQueryTpl = ctx => `
|
||||
__cdb_basics AS (
|
||||
SELECT
|
||||
max(date_part('epoch', ${ctx.column})) AS __cdb_max_val,
|
||||
min(date_part('epoch', ${ctx.column})) AS __cdb_min_val,
|
||||
avg(date_part('epoch', ${ctx.column})) AS __cdb_avg_val,
|
||||
min(
|
||||
date_trunc(
|
||||
'${ctx.aggregation}', ${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}'
|
||||
)
|
||||
) AS __cdb_start_date,
|
||||
max(${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}') AS __cdb_end_date,
|
||||
count(1) AS __cdb_total_rows
|
||||
FROM (${ctx.query}) __cdb_basics_query
|
||||
)
|
||||
`;
|
||||
|
||||
const dateOverrideBasicsQueryTpl = ctx => `
|
||||
__cdb_basics AS (
|
||||
SELECT
|
||||
max(${ctx.end})::float AS __cdb_max_val,
|
||||
min(${ctx.start})::float AS __cdb_min_val,
|
||||
avg(date_part('epoch', ${ctx.column})) AS __cdb_avg_val,
|
||||
min(
|
||||
date_trunc(
|
||||
'${ctx.aggregation}',
|
||||
TO_TIMESTAMP(${ctx.start})::timestamp AT TIME ZONE '${ctx.offset}'
|
||||
)
|
||||
) AS __cdb_start_date,
|
||||
max(
|
||||
TO_TIMESTAMP(${ctx.end})::timestamp AT TIME ZONE '${ctx.offset}'
|
||||
) AS __cdb_end_date,
|
||||
count(1) AS __cdb_total_rows
|
||||
FROM (${ctx.query}) __cdb_basics_query
|
||||
)
|
||||
`;
|
||||
|
||||
const dateBinsQueryTpl = ctx => `
|
||||
__cdb_bins AS (
|
||||
SELECT
|
||||
__cdb_bins_array,
|
||||
ARRAY_LENGTH(__cdb_bins_array, 1) AS __cdb_bins_number
|
||||
FROM (
|
||||
SELECT
|
||||
ARRAY(
|
||||
SELECT GENERATE_SERIES(
|
||||
__cdb_start_date::timestamptz,
|
||||
__cdb_end_date::timestamptz,
|
||||
${ctx.aggregation === 'quarter' ? `'3 month'::interval` : `'1 ${ctx.aggregation}'::interval`}
|
||||
)
|
||||
) AS __cdb_bins_array
|
||||
FROM __cdb_basics
|
||||
) __cdb_bins_array_query
|
||||
)
|
||||
`;
|
||||
|
||||
const dateHistogramQueryTpl = ctx => `
|
||||
SELECT
|
||||
(__cdb_max_val - __cdb_min_val) / cast(__cdb_bins_number as float) AS bin_width,
|
||||
__cdb_bins_number AS bins_number,
|
||||
__cdb_nulls_count AS nulls_count,
|
||||
CASE WHEN __cdb_min_val = __cdb_max_val
|
||||
THEN 0
|
||||
ELSE GREATEST(
|
||||
1,
|
||||
LEAST(
|
||||
WIDTH_BUCKET(
|
||||
${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}',
|
||||
__cdb_bins_array
|
||||
),
|
||||
__cdb_bins_number
|
||||
)
|
||||
) - 1
|
||||
END AS bin,
|
||||
min(
|
||||
date_part(
|
||||
'epoch',
|
||||
date_trunc(
|
||||
'${ctx.aggregation}', ${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}'
|
||||
) AT TIME ZONE '${ctx.offset}'
|
||||
)
|
||||
)::numeric AS timestamp,
|
||||
date_part('epoch', __cdb_start_date)::numeric AS timestamp_start,
|
||||
min(date_part('epoch', ${ctx.column}))::numeric AS min,
|
||||
max(date_part('epoch', ${ctx.column}))::numeric AS max,
|
||||
avg(date_part('epoch', ${ctx.column}))::numeric AS avg,
|
||||
count(*) AS freq
|
||||
FROM (${ctx.query}) __cdb_histogram, __cdb_basics, __cdb_bins, __cdb_nulls
|
||||
WHERE date_part('epoch', ${ctx.column}) IS NOT NULL
|
||||
GROUP BY bin, bins_number, bin_width, nulls_count, timestamp_start
|
||||
ORDER BY bin
|
||||
`;
|
||||
|
||||
const MAX_INTERVAL_VALUE = 366;
|
||||
|
||||
const DATE_AGGREGATIONS = {
|
||||
'auto': true,
|
||||
'minute': true,
|
||||
'hour': true,
|
||||
'day': true,
|
||||
'week': true,
|
||||
'month': true,
|
||||
'quarter': true,
|
||||
'year': true
|
||||
};
|
||||
|
||||
/**
|
||||
date_histogram: {
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'date', // column data type: date
|
||||
aggregation: 'day' // MANDATORY
|
||||
offset: -7200 // OPTIONAL (UTC offset in seconds)
|
||||
}
|
||||
}
|
||||
*/
|
||||
module.exports = class DateHistogram extends BaseHistogram {
|
||||
constructor (query, options, queries) {
|
||||
super(query, options, queries);
|
||||
|
||||
this.aggregation = options.aggregation;
|
||||
this.offset = options.offset;
|
||||
}
|
||||
|
||||
_buildQueryTpl (ctx) {
|
||||
return `
|
||||
WITH
|
||||
${this._hasOverridenRange(ctx.override) ? dateOverrideBasicsQueryTpl(ctx) : dateBasicsQueryTpl(ctx)},
|
||||
${dateBinsQueryTpl(ctx)},
|
||||
${nullsQueryTpl(ctx)}
|
||||
${dateHistogramQueryTpl(ctx)}
|
||||
`;
|
||||
}
|
||||
|
||||
_buildQuery (psql, override, callback) {
|
||||
if (!this._isValidAggregation(override)) {
|
||||
return callback(new Error('Invalid aggregation value. Valid ones: ' +
|
||||
Object.keys(DATE_AGGREGATIONS).join(', ')
|
||||
));
|
||||
}
|
||||
|
||||
if (this._getAggregation(override) === 'auto') {
|
||||
this._getAutomaticAggregation(psql, function (err, aggregation) {
|
||||
if (err || aggregation === 'none') {
|
||||
this.aggregation = 'day';
|
||||
} else {
|
||||
this.aggregation = aggregation;
|
||||
}
|
||||
override.aggregation = this.aggregation;
|
||||
this._buildQuery(psql, override, callback);
|
||||
}.bind(this));
|
||||
return null;
|
||||
}
|
||||
|
||||
const histogramSql = this._buildQueryTpl({
|
||||
override: override,
|
||||
query: this.query,
|
||||
column: this.column,
|
||||
aggregation: this._getAggregation(override),
|
||||
start: this._getBinStart(override),
|
||||
end: this._getBinEnd(override),
|
||||
offset: this._parseOffset(override)
|
||||
});
|
||||
|
||||
debug(histogramSql);
|
||||
|
||||
return callback(null, histogramSql);
|
||||
}
|
||||
|
||||
_isValidAggregation (override) {
|
||||
return DATE_AGGREGATIONS.hasOwnProperty(this._getAggregation(override));
|
||||
}
|
||||
|
||||
_getAutomaticAggregation (psql, callback) {
|
||||
const dateIntervalQuery = dateIntervalQueryTpl({
|
||||
query: this.query,
|
||||
column: this.column
|
||||
});
|
||||
|
||||
psql.query(dateIntervalQuery, function (err, result) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const aggegations = result.rows[0];
|
||||
const aggregation = Object.keys(aggegations)
|
||||
.map(key => ({ name: key, value: aggegations[key] }))
|
||||
.reduce((closer, current) => {
|
||||
if (current.value > MAX_INTERVAL_VALUE) {
|
||||
return closer;
|
||||
}
|
||||
|
||||
const closerDiff = MAX_INTERVAL_VALUE - closer.value;
|
||||
const currentDiff = MAX_INTERVAL_VALUE - current.value;
|
||||
|
||||
if (Number.isFinite(current.value) && closerDiff > currentDiff) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return closer;
|
||||
}, { name: 'none', value: -1 });
|
||||
|
||||
callback(null, aggregation.name);
|
||||
});
|
||||
}
|
||||
|
||||
_getSummary (result, override) {
|
||||
const firstRow = result.rows[0] || {};
|
||||
|
||||
return {
|
||||
aggregation: this._getAggregation(override),
|
||||
offset: this._getOffset(override),
|
||||
timestamp_start: firstRow.timestamp_start,
|
||||
|
||||
bin_width: firstRow.bin_width,
|
||||
bins_count: firstRow.bins_number,
|
||||
bins_start: firstRow.timestamp,
|
||||
nulls: firstRow.nulls_count,
|
||||
infinities: firstRow.infinities_count,
|
||||
nans: firstRow.nans_count,
|
||||
avg: firstRow.avg_val
|
||||
};
|
||||
}
|
||||
|
||||
_getBuckets (result) {
|
||||
return result.rows.map(({ bin, min, max, avg, freq, timestamp }) => ({ bin, min, max, avg, freq, timestamp }));
|
||||
}
|
||||
|
||||
_getAggregation (override = {}) {
|
||||
return override.aggregation ? override.aggregation : this.aggregation;
|
||||
}
|
||||
|
||||
_getOffset (override = {}) {
|
||||
return Number.isFinite(override.offset) ? override.offset : (this.offset || 0);
|
||||
}
|
||||
|
||||
_parseOffset (override) {
|
||||
if (this._shouldIgnoreOffset(override)) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
const offsetInHours = Math.ceil(this._getOffset(override) / 3600);
|
||||
|
||||
return '' + offsetInHours;
|
||||
}
|
||||
|
||||
_shouldIgnoreOffset (override) {
|
||||
return (this._getAggregation(override) === 'hour' || this._getAggregation(override) === 'minute');
|
||||
}
|
||||
};
|
||||
@@ -1,234 +0,0 @@
|
||||
const BaseHistogram = require('./base-histogram');
|
||||
const debug = require('debug')('windshaft:dataview:numeric-histogram');
|
||||
|
||||
const columnCastTpl = ctx => `date_part('epoch', ${ctx.column})`;
|
||||
|
||||
const filterOutSpecialNumericValues = ctx => `
|
||||
${ctx.column} != 'infinity'::float
|
||||
AND
|
||||
${ctx.column} != '-infinity'::float
|
||||
AND
|
||||
${ctx.column} != 'NaN'::float
|
||||
`;
|
||||
|
||||
const filteredQueryTpl = ctx => `
|
||||
__cdb_filtered_source AS (
|
||||
SELECT *
|
||||
FROM (${ctx.query}) __cdb_filtered_source_query
|
||||
WHERE ${ctx.column} IS NOT NULL
|
||||
${ctx.isFloatColumn ? `AND ${filterOutSpecialNumericValues(ctx)}` : ''}
|
||||
)
|
||||
`;
|
||||
|
||||
const basicsQueryTpl = ctx => `
|
||||
__cdb_basics AS (
|
||||
SELECT
|
||||
max(${ctx.column}) AS __cdb_max_val, min(${ctx.column}) AS __cdb_min_val,
|
||||
avg(${ctx.column}) AS __cdb_avg_val, count(1) AS __cdb_total_rows
|
||||
FROM __cdb_filtered_source
|
||||
)
|
||||
`;
|
||||
|
||||
const overrideBasicsQueryTpl = ctx => `
|
||||
__cdb_basics AS (
|
||||
SELECT
|
||||
max(${ctx.end}) AS __cdb_max_val, min(${ctx.start}) AS __cdb_min_val,
|
||||
avg(${ctx.column}) AS __cdb_avg_val, count(1) AS __cdb_total_rows
|
||||
FROM __cdb_filtered_source
|
||||
)
|
||||
`;
|
||||
|
||||
const iqrQueryTpl = ctx => `
|
||||
__cdb_iqrange AS (
|
||||
SELECT max(quartile_max) - min(quartile_max) AS __cdb_iqr
|
||||
FROM (
|
||||
SELECT quartile, max(_cdb_iqr_column) AS quartile_max from (
|
||||
SELECT ${ctx.column} AS _cdb_iqr_column, ntile(4) over (order by ${ctx.column}
|
||||
) AS quartile
|
||||
FROM __cdb_filtered_source) _cdb_quartiles
|
||||
WHERE quartile = 1 or quartile = 3
|
||||
GROUP BY quartile
|
||||
) __cdb_iqr
|
||||
)
|
||||
`;
|
||||
|
||||
const binsQueryTpl = ctx => `
|
||||
__cdb_bins AS (
|
||||
SELECT
|
||||
CASE WHEN __cdb_total_rows = 0 OR __cdb_iqr = 0
|
||||
THEN 1
|
||||
ELSE GREATEST(
|
||||
LEAST(${ctx.minBins}, CAST(__cdb_total_rows AS INT)),
|
||||
LEAST(
|
||||
CAST(((__cdb_max_val - __cdb_min_val) / (2 * __cdb_iqr * power(__cdb_total_rows, 1/3))) AS INT),
|
||||
${ctx.maxBins}
|
||||
)
|
||||
)
|
||||
END AS __cdb_bins_number
|
||||
FROM __cdb_basics, __cdb_iqrange, __cdb_filtered_source
|
||||
LIMIT 1
|
||||
)
|
||||
`;
|
||||
|
||||
const overrideBinsQueryTpl = ctx => `
|
||||
__cdb_bins AS (
|
||||
SELECT ${ctx.override.bins} AS __cdb_bins_number
|
||||
)
|
||||
`;
|
||||
|
||||
const nullsQueryTpl = ctx => `
|
||||
__cdb_nulls AS (
|
||||
SELECT
|
||||
count(*) AS __cdb_nulls_count
|
||||
FROM (${ctx.query}) __cdb_histogram_nulls
|
||||
WHERE ${ctx.column} IS NULL
|
||||
)
|
||||
`;
|
||||
|
||||
const infinitiesQueryTpl = ctx => `
|
||||
__cdb_infinities AS (
|
||||
SELECT
|
||||
count(*) AS __cdb_infinities_count
|
||||
FROM (${ctx.query}) __cdb_infinities_query
|
||||
WHERE
|
||||
${ctx.column} = 'infinity'::float
|
||||
OR
|
||||
${ctx.column} = '-infinity'::float
|
||||
)
|
||||
`;
|
||||
|
||||
const nansQueryTpl = ctx => `
|
||||
__cdb_nans AS (
|
||||
SELECT
|
||||
count(*) AS __cdb_nans_count
|
||||
FROM (${ctx.query}) __cdb_nans_query
|
||||
WHERE ${ctx.column} = 'NaN'::float
|
||||
)
|
||||
`;
|
||||
|
||||
const specialNumericValuesColumnDefinitionTpl = () => `
|
||||
__cdb_infinities_count AS infinities_count,
|
||||
__cdb_nans_count AS nans_count
|
||||
`;
|
||||
|
||||
const specialNumericValuesCTETpl = () => `
|
||||
__cdb_infinities, __cdb_nans
|
||||
`;
|
||||
|
||||
const specialNumericValuesColumnTpl = () => `
|
||||
infinities_count, nans_count
|
||||
`;
|
||||
|
||||
const histogramQueryTpl = ctx => `
|
||||
SELECT
|
||||
(__cdb_max_val - __cdb_min_val) / cast(__cdb_bins_number as float) AS bin_width,
|
||||
__cdb_bins_number AS bins_number,
|
||||
__cdb_nulls_count AS nulls_count,
|
||||
${ctx.isFloatColumn ? `${specialNumericValuesColumnDefinitionTpl()},` : ''}
|
||||
__cdb_avg_val AS avg_val,
|
||||
CASE WHEN __cdb_min_val = __cdb_max_val
|
||||
THEN 0
|
||||
ELSE GREATEST(
|
||||
1,
|
||||
LEAST(
|
||||
WIDTH_BUCKET(${ctx.column}, __cdb_min_val, __cdb_max_val, __cdb_bins_number),
|
||||
__cdb_bins_number
|
||||
)
|
||||
) - 1
|
||||
END AS bin,
|
||||
min(${ctx.column})::numeric AS min,
|
||||
max(${ctx.column})::numeric AS max,
|
||||
avg(${ctx.column})::numeric AS avg,
|
||||
count(*) AS freq
|
||||
FROM __cdb_filtered_source, __cdb_basics, __cdb_nulls, __cdb_bins
|
||||
${ctx.isFloatColumn ? `, ${specialNumericValuesCTETpl()}` : ''}
|
||||
GROUP BY bin, bins_number, bin_width, nulls_count, avg_val
|
||||
${ctx.isFloatColumn ? `, ${specialNumericValuesColumnTpl()}` : ''}
|
||||
ORDER BY bin
|
||||
`;
|
||||
|
||||
const BIN_MIN_NUMBER = 6;
|
||||
const BIN_MAX_NUMBER = 48;
|
||||
|
||||
/**
|
||||
Numeric histogram:
|
||||
{
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'name', // column data type: numeric
|
||||
bins: 10 // OPTIONAL
|
||||
}
|
||||
}
|
||||
*/
|
||||
module.exports = class NumericHistogram extends BaseHistogram {
|
||||
constructor (query, options, queries) {
|
||||
super(query, options, queries);
|
||||
}
|
||||
|
||||
_buildQuery (psql, override, callback) {
|
||||
const histogramSql = this._buildQueryTpl({
|
||||
override: override,
|
||||
column: this._columnType === 'date' ? columnCastTpl({ column: this.column }) : this.column,
|
||||
isFloatColumn: this._columnType === 'float',
|
||||
query: this.query,
|
||||
start: this._getBinStart(override),
|
||||
end: this._getBinEnd(override),
|
||||
minBins: BIN_MIN_NUMBER,
|
||||
maxBins: BIN_MAX_NUMBER,
|
||||
});
|
||||
|
||||
debug(histogramSql);
|
||||
|
||||
return callback(null, histogramSql);
|
||||
}
|
||||
|
||||
_buildQueryTpl (ctx) {
|
||||
return `
|
||||
WITH
|
||||
${filteredQueryTpl(ctx)},
|
||||
${this._hasOverridenRange(ctx.override) ? overrideBasicsQueryTpl(ctx) : basicsQueryTpl(ctx)},
|
||||
${this._hasOverridenBins(ctx.override) ?
|
||||
overrideBinsQueryTpl(ctx) :
|
||||
`${iqrQueryTpl(ctx)}, ${binsQueryTpl(ctx)}`
|
||||
},
|
||||
${nullsQueryTpl(ctx)}
|
||||
${ctx.isFloatColumn ? `,${infinitiesQueryTpl(ctx)}, ${nansQueryTpl(ctx)}` : ''}
|
||||
${histogramQueryTpl(ctx)}
|
||||
`;
|
||||
}
|
||||
|
||||
_hasOverridenBins (override) {
|
||||
return override && override.hasOwnProperty('bins');
|
||||
}
|
||||
|
||||
_getSummary (result, override) {
|
||||
const firstRow = result.rows[0] || {};
|
||||
|
||||
return {
|
||||
bin_width: firstRow.bin_width,
|
||||
bins_count: firstRow.bins_number,
|
||||
bins_start: this._populateBinStart(firstRow, override),
|
||||
nulls: firstRow.nulls_count,
|
||||
infinities: firstRow.infinities_count,
|
||||
nans: firstRow.nans_count,
|
||||
avg: firstRow.avg_val,
|
||||
};
|
||||
}
|
||||
|
||||
_getBuckets (result) {
|
||||
return result.rows.map(({ bin, min, max, avg, freq }) => ({ bin, min, max, avg, freq }));
|
||||
}
|
||||
|
||||
_populateBinStart (firstRow, override = {}) {
|
||||
let binStart;
|
||||
|
||||
if (override.hasOwnProperty('start')) {
|
||||
binStart = this._getBinStart(override);
|
||||
} else {
|
||||
binStart = firstRow.min;
|
||||
}
|
||||
|
||||
return binStart;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
module.exports = {
|
||||
Aggregation: require('./aggregation'),
|
||||
Formula: require('./formula'),
|
||||
Histogram: require('./histogram')
|
||||
Histogram: require('./histogram'),
|
||||
List: require('./list')
|
||||
};
|
||||
|
||||
66
lib/cartodb/models/dataview/list.js
Normal file
66
lib/cartodb/models/dataview/list.js
Normal file
@@ -0,0 +1,66 @@
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var BaseWidget = require('./base');
|
||||
|
||||
var TYPE = 'list';
|
||||
|
||||
var listSqlTpl = dot.template('select {{=it._columns}} from ({{=it._query}}) as _cdb_list');
|
||||
|
||||
/**
|
||||
{
|
||||
type: 'list',
|
||||
options: {
|
||||
columns: ['name', 'description']
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function List(query, options) {
|
||||
options = options || {};
|
||||
|
||||
if (!Array.isArray(options.columns)) {
|
||||
throw new Error('List expects `columns` array in widget options');
|
||||
}
|
||||
|
||||
BaseWidget.apply(this);
|
||||
|
||||
this.query = query;
|
||||
this.columns = options.columns;
|
||||
}
|
||||
|
||||
List.prototype = new BaseWidget();
|
||||
List.prototype.constructor = List;
|
||||
|
||||
module.exports = List;
|
||||
|
||||
List.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
}
|
||||
|
||||
var listSql = listSqlTpl({
|
||||
_query: this.query,
|
||||
_columns: this.columns.join(', ')
|
||||
});
|
||||
|
||||
return callback(null, listSql);
|
||||
};
|
||||
|
||||
List.prototype.format = function(result) {
|
||||
return {
|
||||
rows: result.rows
|
||||
};
|
||||
};
|
||||
|
||||
List.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
|
||||
List.prototype.toString = function() {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_query: this.query,
|
||||
_columns: this.columns.join(', ')
|
||||
});
|
||||
};
|
||||
@@ -1,36 +1,14 @@
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../aggregation');
|
||||
var debug = require('debug')('windshaft:widget:aggregation:overview');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var filteredQueryTpl = dot.template([
|
||||
'filtered_source AS (',
|
||||
' SELECT *',
|
||||
' FROM ({{=it._query}}) _cdb_filtered_source',
|
||||
' {{?it._aggregationColumn && it._isFloatColumn}}WHERE',
|
||||
' {{=it._aggregationColumn}} != \'infinity\'::float',
|
||||
' AND',
|
||||
' {{=it._aggregationColumn}} != \'-infinity\'::float',
|
||||
' AND',
|
||||
' {{=it._aggregationColumn}} != \'NaN\'::float{{?}}',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
var summaryQueryTpl = dot.template([
|
||||
'summary AS (',
|
||||
' SELECT',
|
||||
' sum(_feature_count) AS count,',
|
||||
' sum(CASE WHEN {{=it._column}} IS NULL THEN 1 ELSE 0 END) AS nulls_count',
|
||||
' {{?it._isFloatColumn}},sum(',
|
||||
' CASE',
|
||||
' WHEN {{=it._aggregationColumn}} = \'infinity\'::float OR {{=it._aggregationColumn}} = \'-infinity\'::float',
|
||||
' THEN 1',
|
||||
' ELSE 0',
|
||||
' END',
|
||||
' ) AS infinities_count,',
|
||||
' sum(CASE WHEN {{=it._aggregationColumn}} = \'NaN\'::float THEN 1 ELSE 0 END) AS nans_count{{?}}',
|
||||
' FROM ({{=it._query}}) _cdb_aggregation_nulls',
|
||||
')'
|
||||
].join('\n'));
|
||||
@@ -39,7 +17,7 @@ var rankedCategoriesQueryTpl = dot.template([
|
||||
'categories AS(',
|
||||
' SELECT {{=it._column}} AS category, {{=it._aggregation}} AS value,',
|
||||
' row_number() OVER (ORDER BY {{=it._aggregation}} desc) as rank',
|
||||
' FROM filtered_source',
|
||||
' FROM ({{=it._query}}) _cdb_aggregation_all',
|
||||
' {{?it._aggregationColumn!==null}}WHERE {{=it._aggregationColumn}} IS NOT NULL{{?}}',
|
||||
' GROUP BY {{=it._column}}',
|
||||
' ORDER BY 2 DESC',
|
||||
@@ -58,46 +36,40 @@ var categoriesSummaryCountQueryTpl = dot.template([
|
||||
' SELECT count(1) AS categories_count',
|
||||
' FROM (',
|
||||
' SELECT {{=it._column}} AS category',
|
||||
' FROM filtered_source',
|
||||
' FROM ({{=it._query}}) _cdb_categories',
|
||||
' GROUP BY {{=it._column}}',
|
||||
' ) _cdb_categories_count',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var rankedAggregationQueryTpl = dot.template([
|
||||
'SELECT CAST(category AS text), value, false as agg, nulls_count, min_val, max_val,',
|
||||
' count, categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
|
||||
'SELECT CAST(category AS text), value, false as agg, nulls_count, min_val, max_val, count, categories_count',
|
||||
' FROM categories, summary, categories_summary_min_max, categories_summary_count',
|
||||
' WHERE rank < {{=it._limit}}',
|
||||
'UNION ALL',
|
||||
'SELECT \'Other\' category, sum(value), true as agg, nulls_count, min_val, max_val,',
|
||||
' count, categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
|
||||
'SELECT \'Other\' category, sum(value), true as agg, nulls_count, min_val, max_val, count, categories_count',
|
||||
' FROM categories, summary, categories_summary_min_max, categories_summary_count',
|
||||
' WHERE rank >= {{=it._limit}}',
|
||||
'GROUP BY nulls_count, min_val, max_val, count,',
|
||||
' categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}'
|
||||
'GROUP BY nulls_count, min_val, max_val, count, categories_count'
|
||||
].join('\n'));
|
||||
|
||||
var aggregationQueryTpl = dot.template([
|
||||
'SELECT CAST({{=it._column}} AS text) AS category, {{=it._aggregation}} AS value, false as agg,',
|
||||
' nulls_count, min_val, max_val, count, categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
|
||||
'FROM filtered_source, summary, categories_summary_min_max, categories_summary_count',
|
||||
'GROUP BY category, nulls_count, min_val, max_val, count,',
|
||||
' categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
|
||||
' nulls_count, min_val, max_val, count, categories_count',
|
||||
'FROM ({{=it._query}}) _cdb_aggregation_all, summary, categories_summary_min_max, categories_summary_count',
|
||||
'GROUP BY category, nulls_count, min_val, max_val, count, categories_count',
|
||||
'ORDER BY value DESC'
|
||||
].join('\n'));
|
||||
|
||||
var CATEGORIES_LIMIT = 6;
|
||||
|
||||
function Aggregation(query, options, queryRewriter, queryRewriteData, params, queries) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params, queries);
|
||||
function Aggregation(query, options, queryRewriter, queryRewriteData, params) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
|
||||
this.query = query;
|
||||
this.queries = queries;
|
||||
this.column = options.column;
|
||||
this.aggregation = options.aggregation;
|
||||
this.aggregationColumn = options.aggregationColumn;
|
||||
this._isFloatColumn = null;
|
||||
}
|
||||
|
||||
Aggregation.prototype = Object.create(BaseOverviewsDataview.prototype);
|
||||
@@ -106,49 +78,27 @@ Aggregation.prototype.constructor = Aggregation;
|
||||
module.exports = Aggregation;
|
||||
|
||||
Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var _query = this.rewrittenQuery(this.query);
|
||||
var _aggregationColumn = this.aggregation !== 'count' ? this.aggregationColumn : null;
|
||||
|
||||
if (this.aggregationColumn && this._isFloatColumn === null) {
|
||||
this._isFloatColumn = false;
|
||||
this.getColumnType(psql, this.aggregationColumn, this.queries.no_filters, function (err, type) {
|
||||
if (!err && !!type) {
|
||||
self._isFloatColumn = type.float;
|
||||
}
|
||||
self.sql(psql, override, callback);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
var aggregationSql;
|
||||
if (!!override.ownFilter) {
|
||||
aggregationSql = [
|
||||
"WITH",
|
||||
[
|
||||
filteredQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregationColumn: _aggregationColumn
|
||||
}),
|
||||
summaryQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregationColumn: _aggregationColumn
|
||||
_column: this.column
|
||||
}),
|
||||
rankedCategoriesQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql(),
|
||||
_aggregationColumn: _aggregationColumn
|
||||
_aggregationColumn: this.aggregation !== 'count' ? this.aggregationColumn : null
|
||||
}),
|
||||
categoriesSummaryMinMaxQueryTpl({
|
||||
_query: _query,
|
||||
@@ -160,7 +110,6 @@ Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
})
|
||||
].join(',\n'),
|
||||
aggregationQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql(),
|
||||
@@ -171,23 +120,15 @@ Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
aggregationSql = [
|
||||
"WITH",
|
||||
[
|
||||
filteredQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregationColumn: _aggregationColumn
|
||||
}),
|
||||
summaryQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregationColumn: _aggregationColumn
|
||||
_column: this.column
|
||||
}),
|
||||
rankedCategoriesQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql(),
|
||||
_aggregationColumn: _aggregationColumn
|
||||
_aggregationColumn: this.aggregation !== 'count' ? this.aggregationColumn : null
|
||||
}),
|
||||
categoriesSummaryMinMaxQueryTpl({
|
||||
_query: _query,
|
||||
@@ -199,7 +140,6 @@ Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
})
|
||||
].join(',\n'),
|
||||
rankedAggregationQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_limit: CATEGORIES_LIMIT
|
||||
@@ -207,8 +147,6 @@ Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
debug(aggregationSql);
|
||||
|
||||
return callback(null, aggregationSql);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
var _ = require('underscore');
|
||||
var BaseDataview = require('../base');
|
||||
|
||||
function BaseOverviewsDataview(query, queryOptions, BaseDataview, queryRewriter, queryRewriteData, options, queries) {
|
||||
function BaseOverviewsDataview(query, queryOptions, BaseDataview, queryRewriter, queryRewriteData, options) {
|
||||
this.BaseDataview = BaseDataview;
|
||||
this.query = query;
|
||||
this.queryOptions = queryOptions;
|
||||
this.queryRewriter = queryRewriter;
|
||||
this.queryRewriteData = queryRewriteData;
|
||||
this.options = options;
|
||||
this.queries = queries;
|
||||
this.baseDataview = new this.BaseDataview(this.query, this.queryOptions, this.queries);
|
||||
this.baseDataview = new this.BaseDataview(this.query, this.queryOptions);
|
||||
}
|
||||
|
||||
module.exports = BaseOverviewsDataview;
|
||||
|
||||
@@ -1,61 +1,34 @@
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../formula');
|
||||
var debug = require('debug')('windshaft:widget:formula:overview');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var formulaQueryTpls = {
|
||||
'count': dot.template([
|
||||
'SELECT',
|
||||
'sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'{{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_infinities',
|
||||
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nans',
|
||||
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
'sum': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'{{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_infinities',
|
||||
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count',
|
||||
',(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nans',
|
||||
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
|
||||
'FROM ({{=it._query}}) _cdb_formula',
|
||||
'{{?it._isFloatColumn}}WHERE',
|
||||
' {{=it._column}} != \'infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'-infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'NaN\'::float{{?}}'
|
||||
].join('\n')),
|
||||
'avg': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count)/sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'{{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_infinities',
|
||||
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count',
|
||||
',(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nans',
|
||||
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
|
||||
'FROM ({{=it._query}}) _cdb_formula',
|
||||
'{{?it._isFloatColumn}}WHERE',
|
||||
' {{=it._column}} != \'infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'-infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'NaN\'::float{{?}}'
|
||||
].join('\n')),
|
||||
'count': dot.template([
|
||||
'SELECT',
|
||||
'sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
'sum': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
'avg': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count)/sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
};
|
||||
|
||||
function Formula(query, options, queryRewriter, queryRewriteData, params, queries) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params, queries);
|
||||
function Formula(query, options, queryRewriter, queryRewriteData, params) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
this.column = options.column || '1';
|
||||
this.operation = options.operation;
|
||||
this._isFloatColumn = null;
|
||||
this.queries = queries;
|
||||
}
|
||||
|
||||
Formula.prototype = Object.create(BaseOverviewsDataview.prototype);
|
||||
@@ -63,38 +36,21 @@ Formula.prototype.constructor = Formula;
|
||||
|
||||
module.exports = Formula;
|
||||
|
||||
Formula.prototype.sql = function (psql, override, callback) {
|
||||
var self = this;
|
||||
Formula.prototype.sql = function(psql, override, callback) {
|
||||
var formulaQueryTpl = formulaQueryTpls[this.operation];
|
||||
|
||||
if (formulaQueryTpl) {
|
||||
if ( formulaQueryTpl ) {
|
||||
// supported formula for use with overviews
|
||||
if (this._isFloatColumn === null) {
|
||||
this._isFloatColumn = false;
|
||||
this.getColumnType(psql, this.column, this.queries.no_filters, function (err, type) {
|
||||
if (!err && !!type) {
|
||||
self._isFloatColumn = type.float;
|
||||
}
|
||||
self.sql(psql, override, callback);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
var formulaSql = formulaQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: this.rewrittenQuery(this.query),
|
||||
_query: this.rewrittenQuery(this.query),
|
||||
_operation: this.operation,
|
||||
_column: this.column
|
||||
_column: this.column
|
||||
});
|
||||
|
||||
callback = callback || override;
|
||||
|
||||
debug(formulaSql);
|
||||
|
||||
return callback(null, formulaSql);
|
||||
}
|
||||
|
||||
|
||||
// default behaviour
|
||||
return this.defaultSql(psql, override, callback);
|
||||
};
|
||||
|
||||
@@ -1,35 +1,23 @@
|
||||
var _ = require('underscore');
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../histogram');
|
||||
var debug = require('debug')('windshaft:dataview:histogram:overview');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var columnTypeQueryTpl = dot.template(
|
||||
'SELECT pg_typeof({{=it.column}})::oid FROM ({{=it.query}}) _cdb_histogram_column_type limit 1'
|
||||
);
|
||||
|
||||
var BIN_MIN_NUMBER = 6;
|
||||
var BIN_MAX_NUMBER = 48;
|
||||
|
||||
var filteredQueryTpl = dot.template([
|
||||
'filtered_source AS (',
|
||||
' SELECT *',
|
||||
' FROM ({{=it._query}}) _cdb_filtered_source',
|
||||
' WHERE',
|
||||
' {{=it._column}} IS NOT NULL',
|
||||
' {{?it._isFloatColumn}}AND',
|
||||
' {{=it._column}} != \'infinity\'::float',
|
||||
' AND',
|
||||
' {{=it._column}} != \'-infinity\'::float',
|
||||
' AND',
|
||||
' {{=it._column}} != \'NaN\'::float{{?}}',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
var basicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._column}}) AS max_val, min({{=it._column}}) AS min_val,',
|
||||
' sum({{=it._column}}*_feature_count)/sum(_feature_count) AS avg_val, sum(_feature_count) AS total_rows',
|
||||
' FROM filtered_source',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
@@ -38,7 +26,7 @@ var overrideBasicsQueryTpl = dot.template([
|
||||
' SELECT',
|
||||
' max({{=it._end}}) AS max_val, min({{=it._start}}) AS min_val,',
|
||||
' sum({{=it._column}}*_feature_count)/sum(_feature_count) AS avg_val, sum(_feature_count) AS total_rows',
|
||||
' FROM filtered_source',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
@@ -49,7 +37,7 @@ var iqrQueryTpl = dot.template([
|
||||
' SELECT quartile, max(_cdb_iqr_column) AS quartile_max from (',
|
||||
' SELECT {{=it._column}} AS _cdb_iqr_column, ntile(4) over (order by {{=it._column}}',
|
||||
' ) AS quartile',
|
||||
' FROM filtered_source) _cdb_quartiles',
|
||||
' FROM ({{=it._query}}) _cdb_rank) _cdb_quartiles',
|
||||
' WHERE quartile = 1 or quartile = 3',
|
||||
' GROUP BY quartile',
|
||||
' ) _cdb_iqr',
|
||||
@@ -68,7 +56,7 @@ var binsQueryTpl = dot.template([
|
||||
' )',
|
||||
' )',
|
||||
' END AS bins_number',
|
||||
' FROM basics, iqrange, filtered_source',
|
||||
' FROM basics, iqrange, ({{=it._query}}) _cdb_bins',
|
||||
' LIMIT 1',
|
||||
')'
|
||||
].join('\n'));
|
||||
@@ -88,34 +76,11 @@ var nullsQueryTpl = dot.template([
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var infinitiesQueryTpl = dot.template([
|
||||
'infinities AS (',
|
||||
' SELECT',
|
||||
' count(*) AS infinities_count',
|
||||
' FROM ({{=it._query}}) _cdb_histogram_infinities',
|
||||
' WHERE',
|
||||
' {{=it._column}} = \'infinity\'::float',
|
||||
' OR',
|
||||
' {{=it._column}} = \'-infinity\'::float',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var nansQueryTpl = dot.template([
|
||||
'nans AS (',
|
||||
' SELECT',
|
||||
' count(*) AS nans_count',
|
||||
' FROM ({{=it._query}}) _cdb_histogram_infinities',
|
||||
' WHERE {{=it._column}} = \'NaN\'::float',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var histogramQueryTpl = dot.template([
|
||||
'SELECT',
|
||||
' (max_val - min_val) / cast(bins_number as float) AS bin_width,',
|
||||
' bins_number,',
|
||||
' nulls_count,',
|
||||
' {{?it._isFloatColumn}}infinities_count,',
|
||||
' nans_count,{{?}}',
|
||||
' avg_val,',
|
||||
' CASE WHEN min_val = max_val',
|
||||
' THEN 0',
|
||||
@@ -125,14 +90,14 @@ var histogramQueryTpl = dot.template([
|
||||
' max({{=it._column}})::numeric AS max,',
|
||||
' sum({{=it._column}}*_feature_count)/sum(_feature_count)::numeric AS avg,',
|
||||
' sum(_feature_count) AS freq',
|
||||
'FROM filtered_source, basics, nulls, bins{{?it._isFloatColumn}},infinities, nans{{?}}',
|
||||
'FROM ({{=it._query}}) _cdb_histogram, basics, nulls, bins',
|
||||
'WHERE {{=it._column}} IS NOT NULL',
|
||||
'GROUP BY bin, bins_number, bin_width, nulls_count, avg_val',
|
||||
' {{?it._isFloatColumn}}, infinities_count, nans_count{{?}}',
|
||||
'ORDER BY bin'
|
||||
].join('\n'));
|
||||
|
||||
function Histogram(query, options, queryRewriter, queryRewriteData, params, queries) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params, queries);
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
|
||||
this.query = query;
|
||||
this.queries = queries;
|
||||
@@ -147,23 +112,36 @@ Histogram.prototype.constructor = Histogram;
|
||||
|
||||
module.exports = Histogram;
|
||||
|
||||
Histogram.prototype.sql = function(psql, override, callback) {
|
||||
var self = this;
|
||||
|
||||
var DATE_OIDS = {
|
||||
1082: true,
|
||||
1114: true,
|
||||
1184: true
|
||||
};
|
||||
|
||||
Histogram.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
var _column = this.column;
|
||||
|
||||
var columnTypeQuery = columnTypeQueryTpl({
|
||||
column: _column, query: this.rewrittenQuery(this.queries.no_filters)
|
||||
});
|
||||
|
||||
if (this._columnType === null) {
|
||||
this.getColumnType(psql, this.column, this.queries.no_filters, function (err, type) {
|
||||
psql.query(columnTypeQuery, function(err, result) {
|
||||
// assume numeric, will fail later
|
||||
self._columnType = 'numeric';
|
||||
if (!err && !!type) {
|
||||
self._columnType = Object.keys(type).find(function (key) {
|
||||
return type[key];
|
||||
});
|
||||
if (!err && !!result.rows[0]) {
|
||||
var pgType = result.rows[0].pg_typeof;
|
||||
if (DATE_OIDS.hasOwnProperty(pgType)) {
|
||||
self._columnType = 'date';
|
||||
}
|
||||
}
|
||||
self.sql(psql, override, callback);
|
||||
}, true); // use read-only transaction
|
||||
@@ -176,24 +154,11 @@ Histogram.prototype.sql = function(psql, override, callback) {
|
||||
return this.defaultSql(psql, override, callback);
|
||||
}
|
||||
|
||||
var histogramSql = this._buildQuery(override);
|
||||
|
||||
return callback(null, histogramSql);
|
||||
};
|
||||
|
||||
Histogram.prototype._buildQuery = function (override) {
|
||||
var filteredQuery, basicsQuery, binsQuery;
|
||||
var _column = this.column;
|
||||
var _query = this.rewrittenQuery(this.query);
|
||||
|
||||
filteredQuery = filteredQueryTpl({
|
||||
_isFloatColumn: this._columnType === 'float',
|
||||
_query: _query,
|
||||
_column: _column
|
||||
});
|
||||
var basicsQuery, binsQuery;
|
||||
|
||||
if (this._shouldOverride(override)) {
|
||||
debug('overriding with %j', override);
|
||||
if (override && _.has(override, 'start') && _.has(override, 'end') && _.has(override, 'bins')) {
|
||||
basicsQuery = overrideBasicsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column,
|
||||
@@ -212,7 +177,7 @@ Histogram.prototype._buildQuery = function (override) {
|
||||
_column: _column
|
||||
});
|
||||
|
||||
if (this._shouldOverrideBins(override)) {
|
||||
if (override && _.has(override, 'bins')) {
|
||||
binsQuery = [
|
||||
overrideBinsQueryTpl({
|
||||
_bins: override.bins
|
||||
@@ -233,50 +198,22 @@ Histogram.prototype._buildQuery = function (override) {
|
||||
}
|
||||
}
|
||||
|
||||
var cteSql = [
|
||||
filteredQuery,
|
||||
basicsQuery,
|
||||
binsQuery,
|
||||
nullsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
];
|
||||
|
||||
if (this._columnType === 'float') {
|
||||
cteSql.push(
|
||||
infinitiesQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
}),
|
||||
nansQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
var histogramSql = [
|
||||
"WITH",
|
||||
cteSql.join(',\n'),
|
||||
[
|
||||
basicsQuery,
|
||||
binsQuery,
|
||||
nullsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
].join(',\n'),
|
||||
histogramQueryTpl({
|
||||
_isFloatColumn: this._columnType === 'float',
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
].join('\n');
|
||||
|
||||
debug(histogramSql);
|
||||
|
||||
return histogramSql;
|
||||
return callback(null, histogramSql);
|
||||
};
|
||||
|
||||
Histogram.prototype._shouldOverride = function (override) {
|
||||
return override && _.has(override, 'start') && _.has(override, 'end') && _.has(override, 'bins');
|
||||
};
|
||||
|
||||
Histogram.prototype._shouldOverrideBins = function (override) {
|
||||
return override && _.has(override, 'bins');
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module.exports = {
|
||||
Aggregation: require('./aggregation'),
|
||||
Formula: require('./formula'),
|
||||
Histogram: require('./histogram')
|
||||
Histogram: require('./histogram'),
|
||||
List: require('./list')
|
||||
};
|
||||
|
||||
11
lib/cartodb/models/dataview/overviews/list.js
Normal file
11
lib/cartodb/models/dataview/overviews/list.js
Normal file
@@ -0,0 +1,11 @@
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../list');
|
||||
|
||||
function List(query, options, queryRewriter, queryRewriteData, params) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
}
|
||||
|
||||
List.prototype = Object.create(BaseOverviewsDataview.prototype);
|
||||
List.prototype.constructor = List;
|
||||
|
||||
module.exports = List;
|
||||
@@ -8,7 +8,7 @@ var filterQueryTpl = dot.template([
|
||||
].join('\n'));
|
||||
|
||||
var bboxFilterTpl = dot.template(
|
||||
'ST_Intersects({{=it._column}}, ST_Transform(ST_MakeEnvelope({{=it._bbox}}, 4326), {{=it._srid}}))'
|
||||
'{{=it._column}} && ST_Transform(ST_MakeEnvelope({{=it._bbox}}, 4326), {{=it._srid}})'
|
||||
);
|
||||
|
||||
var LATITUDE_MAX_VALUE = 85.0511287798066;
|
||||
@@ -66,8 +66,7 @@ function getBoundingBoxes(west, south, east, north) {
|
||||
bboxes.push([west, south, east, north]);
|
||||
} else {
|
||||
bboxes.push([west, south, 180, north]);
|
||||
// here we assume west,east have been adjusted => west >= -180 => east > 180
|
||||
bboxes.push([-180, south, east - 360, north]);
|
||||
bboxes.push([-180, south, east % 180, north]);
|
||||
}
|
||||
|
||||
return bboxes;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
function MapConfigBufferSizeAdapter() {
|
||||
this.formats = ['png', 'png32', 'mvt', 'grid.json'];
|
||||
}
|
||||
|
||||
module.exports = MapConfigBufferSizeAdapter;
|
||||
|
||||
MapConfigBufferSizeAdapter.prototype.getMapConfig = function (user, requestMapConfig, params, context, callback) {
|
||||
if (!context.templateParams || !context.templateParams.buffersize) {
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
this.formats.forEach(function (format) {
|
||||
if (Number.isFinite(context.templateParams.buffersize[format])) {
|
||||
if (requestMapConfig.buffersize === undefined) {
|
||||
requestMapConfig.buffersize = {};
|
||||
}
|
||||
|
||||
requestMapConfig.buffersize[format] = context.templateParams.buffersize[format];
|
||||
}
|
||||
});
|
||||
|
||||
setImmediate(function () {
|
||||
callback(null, requestMapConfig);
|
||||
});
|
||||
};
|
||||
@@ -43,6 +43,7 @@ MapConfigNamedLayersAdapter.prototype.getMapConfig = function (user, requestMapC
|
||||
|
||||
if (nestedNamedLayers.length > 0) {
|
||||
var nestedNamedMapsError = new Error('Nested named layers are not allowed');
|
||||
// nestedNamedMapsError.http_status = 400;
|
||||
return done(nestedNamedMapsError);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,13 @@ var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
var queue = require('queue-async');
|
||||
var PSQL = require('cartodb-psql');
|
||||
/**
|
||||
* cartodb-psql creates `global.Promise` as an empty constructor.
|
||||
* However, `turbo-carto` relies on a polyfil that fails to create the polyfil
|
||||
* as it finds `global.Promise` but it doesn't find `Promise.resolve`.
|
||||
*/
|
||||
global.Promise = global.Promise || function() {};
|
||||
global.Promise.resolve = global.Promise.resolve || function() {};
|
||||
var turboCarto = require('turbo-carto');
|
||||
|
||||
var SubstitutionTokens = require('../../../utils/substitution-tokens');
|
||||
|
||||
@@ -26,7 +26,7 @@ CreateLayergroupMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
var context = {};
|
||||
step(
|
||||
function prepareContextLimits() {
|
||||
self.userLimitsApi.getRenderLimits(self.user, self.params.api_key, this);
|
||||
self.userLimitsApi.getRenderLimits(self.user, this);
|
||||
},
|
||||
function handleRenderLimits(err, renderLimits) {
|
||||
assert.ifError(err);
|
||||
|
||||
@@ -27,7 +27,7 @@ MapStoreMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
var context = {};
|
||||
step(
|
||||
function prepareContextLimits() {
|
||||
self.userLimitsApi.getRenderLimits(self.user, self.params.api_key, this);
|
||||
self.userLimitsApi.getRenderLimits(self.user, this);
|
||||
},
|
||||
function handleRenderLimits(err, renderLimits) {
|
||||
assert.ifError(err);
|
||||
|
||||
@@ -90,7 +90,6 @@ NamedMapMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
},
|
||||
function instantiateTemplate(err, templateParams) {
|
||||
assert.ifError(err);
|
||||
context.templateParams = templateParams;
|
||||
return self.templateMaps.instance(self.template, templateParams);
|
||||
},
|
||||
function prepareAdapterMapConfig(err, requestMapConfig) {
|
||||
@@ -114,7 +113,7 @@ NamedMapMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
function prepareContextLimits(err, _mapConfig) {
|
||||
assert.ifError(err);
|
||||
mapConfig = _mapConfig;
|
||||
self.userLimitsApi.getRenderLimits(self.owner, self.params.api_key, this);
|
||||
self.userLimitsApi.getRenderLimits(self.owner, this);
|
||||
},
|
||||
function cacheAndReturnMapConfig(err, renderLimits) {
|
||||
self.err = err;
|
||||
|
||||
@@ -4,8 +4,6 @@ var RedisPool = require('redis-mpool');
|
||||
var cartodbRedis = require('cartodb-redis');
|
||||
var _ = require('underscore');
|
||||
|
||||
var lzmaMiddleware = require('./middleware/lzma');
|
||||
|
||||
var controller = require('./controllers');
|
||||
|
||||
var SurrogateKeysCache = require('./cache/surrogate_keys_cache');
|
||||
@@ -37,15 +35,12 @@ var timeoutErrorTile = require('fs').readFileSync(timeoutErrorTilePath, {encodin
|
||||
|
||||
var SqlWrapMapConfigAdapter = require('./models/mapconfig/adapter/sql-wrap-mapconfig-adapter');
|
||||
var MapConfigNamedLayersAdapter = require('./models/mapconfig/adapter/mapconfig-named-layers-adapter');
|
||||
var MapConfigBufferSizeAdapter = require('./models/mapconfig/adapter/mapconfig-buffer-size-adapter');
|
||||
var AnalysisMapConfigAdapter = require('./models/mapconfig/adapter/analysis-mapconfig-adapter');
|
||||
var MapConfigOverviewsAdapter = require('./models/mapconfig/adapter/mapconfig-overviews-adapter');
|
||||
var TurboCartoAdapter = require('./models/mapconfig/adapter/turbo-carto-adapter');
|
||||
var DataviewsWidgetsAdapter = require('./models/mapconfig/adapter/dataviews-widgets-adapter');
|
||||
var MapConfigAdapter = require('./models/mapconfig/adapter');
|
||||
|
||||
var StatsBackend = require('./backends/stats');
|
||||
|
||||
module.exports = function(serverOptions) {
|
||||
// Make stats client globally accessible
|
||||
global.statsClient = StatsClient.getInstance(serverOptions.statsd);
|
||||
@@ -120,27 +115,8 @@ module.exports = function(serverOptions) {
|
||||
var onTileErrorStrategy;
|
||||
if (global.environment.enabledFeatures.onTileErrorStrategy !== false) {
|
||||
onTileErrorStrategy = function onTileErrorStrategy$TimeoutTile(err, tile, headers, stats, format, callback) {
|
||||
|
||||
function isRenderTimeoutError (err) {
|
||||
return err.message === 'Render timed out';
|
||||
}
|
||||
|
||||
function isDatasourceTimeoutError (err) {
|
||||
return err.message && err.message.match(/canceling statement due to statement timeout/i);
|
||||
}
|
||||
|
||||
function isTimeoutError (err) {
|
||||
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
|
||||
}
|
||||
|
||||
function isRasterFormat (format) {
|
||||
return format === 'png' || format === 'jpg';
|
||||
}
|
||||
|
||||
if (isTimeoutError(err) && isRasterFormat(format)) {
|
||||
return callback(null, timeoutErrorTile, {
|
||||
'Content-Type': 'image/png',
|
||||
}, {});
|
||||
if (err && err.message === 'Render timed out' && format === 'png') {
|
||||
return callback(null, timeoutErrorTile, { 'Content-Type': 'image/png' }, {});
|
||||
} else {
|
||||
return callback(err, tile, headers, stats);
|
||||
}
|
||||
@@ -174,14 +150,11 @@ module.exports = function(serverOptions) {
|
||||
|
||||
var analysisBackend = new AnalysisBackend(metadataBackend, serverOptions.analysis);
|
||||
|
||||
var statsBackend = new StatsBackend();
|
||||
|
||||
var layergroupAffectedTablesCache = new LayergroupAffectedTablesCache();
|
||||
app.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
|
||||
var mapConfigAdapter = new MapConfigAdapter(
|
||||
new MapConfigNamedLayersAdapter(templateMaps, pgConnection),
|
||||
new MapConfigBufferSizeAdapter(),
|
||||
new SqlWrapMapConfigAdapter(),
|
||||
new DataviewsWidgetsAdapter(),
|
||||
new AnalysisMapConfigAdapter(analysisBackend),
|
||||
@@ -234,8 +207,7 @@ module.exports = function(serverOptions) {
|
||||
surrogateKeysCache,
|
||||
userLimitsApi,
|
||||
layergroupAffectedTablesCache,
|
||||
mapConfigAdapter,
|
||||
statsBackend
|
||||
mapConfigAdapter
|
||||
).register(app);
|
||||
|
||||
new controller.NamedMaps(
|
||||
@@ -331,25 +303,6 @@ function bootstrap(opts) {
|
||||
app.enable('jsonp callback');
|
||||
app.disable('x-powered-by');
|
||||
app.disable('etag');
|
||||
|
||||
// Fix: https://github.com/CartoDB/Windshaft-cartodb/issues/705
|
||||
// See: http://expressjs.com/en/4x/api.html#app.set
|
||||
app.set('json replacer', function (key, value) {
|
||||
if (value !== value) {
|
||||
return 'NaN';
|
||||
}
|
||||
|
||||
if (value === Infinity) {
|
||||
return 'Infinity';
|
||||
}
|
||||
|
||||
if (value === -Infinity) {
|
||||
return '-Infinity';
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.use(function bootstrap$prepareRequestResponse(req, res, next) {
|
||||
@@ -366,8 +319,6 @@ function bootstrap(opts) {
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(lzmaMiddleware);
|
||||
|
||||
// temporary measure until we upgrade to newer version expressjs so we can check err.status
|
||||
app.use(function(err, req, res, next) {
|
||||
if (err) {
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
function prepareQuery(sql) {
|
||||
var affectedTableRegexCache = {
|
||||
bbox: /!bbox!/g,
|
||||
scale_denominator: /!scale_denominator!/g,
|
||||
pixel_width: /!pixel_width!/g,
|
||||
pixel_height: /!pixel_height!/g
|
||||
};
|
||||
|
||||
return sql
|
||||
.replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)')
|
||||
.replace(affectedTableRegexCache.scale_denominator, '0')
|
||||
.replace(affectedTableRegexCache.pixel_width, '1')
|
||||
.replace(affectedTableRegexCache.pixel_height, '1');
|
||||
}
|
||||
|
||||
module.exports.extractTableNames = function extractTableNames(query) {
|
||||
return [
|
||||
'SELECT * FROM CDB_QueryTablesText($windshaft$',
|
||||
prepareQuery(query),
|
||||
'$windshaft$) as tablenames'
|
||||
].join('');
|
||||
};
|
||||
|
||||
module.exports.getQueryRowCount = function getQueryRowEstimation(query) {
|
||||
return 'select CDB_EstimateRowCount(\'' + query + '\') as rows';
|
||||
};
|
||||
37
package.json
37
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "4.0.0",
|
||||
"version": "3.6.7",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
@@ -16,20 +16,17 @@
|
||||
"contributors": [
|
||||
"Simon Tokumine <simon@vizzuality.com>",
|
||||
"Javi Santana <jsantana@vizzuality.com>",
|
||||
"Sandro Santilli <strk@vizzuality.com>",
|
||||
"Carlos Matallín <matallo@carto.com>",
|
||||
"Daniel Garcia Aubert <dgaubert@carto.com>",
|
||||
"Mario de Frutos <mario.defrutos@carto.com>"
|
||||
"Sandro Santilli <strk@vizzuality.com>"
|
||||
],
|
||||
"dependencies": {
|
||||
"body-parser": "^1.18.2",
|
||||
"camshaft": "0.59.2",
|
||||
"cartodb-psql": "0.10.2",
|
||||
"cartodb-query-tables": "0.3.0",
|
||||
"cartodb-redis": "0.14.0",
|
||||
"debug": "^3.1.0",
|
||||
"body-parser": "~1.14.0",
|
||||
"camshaft": "0.54.4",
|
||||
"cartodb-psql": "0.10.1",
|
||||
"cartodb-query-tables": "0.2.0",
|
||||
"cartodb-redis": "0.13.2",
|
||||
"debug": "~2.2.0",
|
||||
"dot": "~1.0.2",
|
||||
"express": "~4.16.0",
|
||||
"express": "~4.13.3",
|
||||
"fastly-purge": "~1.0.1",
|
||||
"log4js": "cartodb/log4js-node#cdb",
|
||||
"lru-cache": "2.6.5",
|
||||
@@ -37,31 +34,29 @@
|
||||
"node-statsd": "~0.0.7",
|
||||
"queue-async": "~1.0.7",
|
||||
"redis-mpool": "0.4.1",
|
||||
"request": "^2.83.0",
|
||||
"semver": "~5.3.0",
|
||||
"request": "~2.79.0",
|
||||
"step": "~0.0.6",
|
||||
"step-profiler": "~0.3.0",
|
||||
"turbo-carto": "0.20.1",
|
||||
"turbo-carto": "0.19.0",
|
||||
"underscore": "~1.6.0",
|
||||
"windshaft": "3.3.3",
|
||||
"windshaft": "3.1.2",
|
||||
"yargs": "~5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"istanbul": "~0.4.3",
|
||||
"jshint": "~2.9.4",
|
||||
"mocha": "~3.4.1",
|
||||
"moment": "~2.18.1",
|
||||
"jshint": "~2.6.0",
|
||||
"mocha": "~1.21.4",
|
||||
"nock": "~2.11.0",
|
||||
"redis": "~0.12.1",
|
||||
"semver": "~1.1.4",
|
||||
"strftime": "~0.8.2"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "jshint lib test",
|
||||
"preinstall": "make pre-install",
|
||||
"test": "make test-all"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9",
|
||||
"yarn": ">=0.27.5 <1.0.0"
|
||||
"yarn": "^0.21.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,34 +5,34 @@ var TestClient = require('../../support/test-client');
|
||||
var dot = require('dot');
|
||||
var debug = require('debug')('windshaft:cartodb:test');
|
||||
|
||||
describe('analysis-layers use cases', function () {
|
||||
describe('analysis-layers use cases', function() {
|
||||
|
||||
|
||||
var multitypeStyleTemplate = dot.template(
|
||||
`#points['mapnik::geometry_type'=1] {
|
||||
marker-fill-opacity: {{=it._opacity}};
|
||||
marker-line-color: #FFF;
|
||||
marker-line-width: 0.5;
|
||||
marker-line-opacity: {{=it._opacity}};
|
||||
marker-placement: point;
|
||||
marker-type: ellipse;
|
||||
marker-width: 8;
|
||||
marker-fill: {{=it._color}};
|
||||
marker-allow-overlap: true;
|
||||
}
|
||||
#lines['mapnik::geometry_type'=2] {
|
||||
line-color: {{=it._color}};
|
||||
line-width: 2;
|
||||
line-opacity: {{=it._opacity}};
|
||||
}
|
||||
#polygons['mapnik::geometry_type'=3] {
|
||||
polygon-fill: {{=it._color}};
|
||||
polygon-opacity: {{=it._opacity}};
|
||||
line-color: #FFF;
|
||||
line-width: 0.5;
|
||||
line-opacity: {{=it._opacity}};
|
||||
}`
|
||||
);
|
||||
var multitypeStyleTemplate = dot.template([
|
||||
"#points['mapnik::geometry_type'=1] {",
|
||||
" marker-fill-opacity: {{=it._opacity}};",
|
||||
" marker-line-color: #FFF;",
|
||||
" marker-line-width: 0.5;",
|
||||
" marker-line-opacity: {{=it._opacity}};",
|
||||
" marker-placement: point;",
|
||||
" marker-type: ellipse;",
|
||||
" marker-width: 8;",
|
||||
" marker-fill: {{=it._color}};",
|
||||
" marker-allow-overlap: true;",
|
||||
"}",
|
||||
"#lines['mapnik::geometry_type'=2] {",
|
||||
" line-color: {{=it._color}};",
|
||||
" line-width: 2;",
|
||||
" line-opacity: {{=it._opacity}};",
|
||||
"}",
|
||||
"#polygons['mapnik::geometry_type'=3] {",
|
||||
" polygon-fill: {{=it._color}};",
|
||||
" polygon-opacity: {{=it._opacity}};",
|
||||
" line-color: #FFF;",
|
||||
" line-width: 0.5;",
|
||||
" line-opacity: {{=it._opacity}};",
|
||||
"}"
|
||||
].join('\n'));
|
||||
|
||||
|
||||
function cartocss(color, opacity) {
|
||||
@@ -47,53 +47,18 @@ describe('analysis-layers use cases', function () {
|
||||
version: '1.5.0',
|
||||
layers: layers,
|
||||
dataviews: dataviews || {},
|
||||
analyses: analysis || []
|
||||
analysis: analysis || []
|
||||
};
|
||||
}
|
||||
|
||||
function analysisDef(analysis) {
|
||||
return JSON.stringify(analysis);
|
||||
}
|
||||
|
||||
var DEFAULT_MULTITYPE_STYLE = cartocss();
|
||||
|
||||
var TILE_ANALYSIS_TABLES = { z: 14, x: 8023, y: 6177 };
|
||||
|
||||
var pointInPolygonDef = {
|
||||
id: 'a1',
|
||||
type: 'point-in-polygon',
|
||||
params: {
|
||||
points_source: {
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from analysis_rent_listings'
|
||||
}
|
||||
},
|
||||
polygons_source: {
|
||||
type: 'buffer',
|
||||
params: {
|
||||
source: {
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from analysis_banks'
|
||||
}
|
||||
},
|
||||
radius: 250
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var bufferDef = {
|
||||
id: 'b1',
|
||||
type: 'buffer',
|
||||
params: {
|
||||
source: {
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from analysis_banks'
|
||||
}
|
||||
},
|
||||
radius: 250
|
||||
}
|
||||
};
|
||||
|
||||
var useCases = [
|
||||
{
|
||||
desc: '1 mapnik layer',
|
||||
@@ -103,7 +68,7 @@ describe('analysis-layers use cases', function () {
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select * from analysis_rent_listings',
|
||||
sql: "select * from analysis_rent_listings",
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
@@ -118,7 +83,7 @@ describe('analysis-layers use cases', function () {
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select * from analysis_banks',
|
||||
sql: "select * from analysis_banks",
|
||||
cartocss: cartocss('#2167AB'),
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
@@ -126,7 +91,7 @@ describe('analysis-layers use cases', function () {
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select * from analysis_rent_listings',
|
||||
sql: "select * from analysis_rent_listings",
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
@@ -140,27 +105,30 @@ describe('analysis-layers use cases', function () {
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select * from analysis_rent_listings',
|
||||
sql: "select * from analysis_rent_listings",
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
type: 'analysis',
|
||||
options: {
|
||||
source: {
|
||||
id: 'b1'
|
||||
},
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
def: analysisDef({
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 250
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('black', 0.5)
|
||||
}
|
||||
}
|
||||
],
|
||||
{},
|
||||
[
|
||||
bufferDef
|
||||
]
|
||||
)
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
@@ -169,115 +137,531 @@ describe('analysis-layers use cases', function () {
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select * from analysis_rent_listings',
|
||||
sql: "select * from analysis_rent_listings",
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
type: 'analysis',
|
||||
options: {
|
||||
source: {
|
||||
id: 'a1'
|
||||
},
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
def: analysisDef({
|
||||
"type": "point-in-polygon",
|
||||
"params": {
|
||||
"pointsSource": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_rent_listings"
|
||||
}
|
||||
},
|
||||
"polygonsSource": {
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 250
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('green', 1.0)
|
||||
}
|
||||
}
|
||||
],
|
||||
{},
|
||||
[
|
||||
pointInPolygonDef
|
||||
]
|
||||
)
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'point-in-polygon from buffer atm-machines and rent listings + rent listings',
|
||||
mapConfig: mapConfig([
|
||||
{
|
||||
type: 'analysis',
|
||||
options: {
|
||||
def: analysisDef({
|
||||
"type": "point-in-polygon",
|
||||
"params": {
|
||||
"pointsSource": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_rent_listings"
|
||||
}
|
||||
},
|
||||
"polygonsSource": {
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 250
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('green', 1.0)
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: "select * from analysis_rent_listings",
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings',
|
||||
mapConfig: mapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: "select * from analysis_rent_listings",
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'analysis',
|
||||
options: {
|
||||
def: analysisDef({
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 300
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('magenta', 0.5)
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'analysis',
|
||||
options: {
|
||||
def: analysisDef({
|
||||
"type": "point-in-polygon",
|
||||
"params": {
|
||||
"pointsSource": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_rent_listings"
|
||||
}
|
||||
},
|
||||
"polygonsSource": {
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 300
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('green', 1.0)
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
skip: true,
|
||||
desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings',
|
||||
mapConfig: mapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "a" },
|
||||
"cartocss": DEFAULT_MULTITYPE_STYLE,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "b1" },
|
||||
"cartocss": cartocss('green', 1.0),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "b2" },
|
||||
"cartocss": cartocss('magenta', 0.5),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
id: "b2",
|
||||
options: {
|
||||
def: analysisDef({
|
||||
"type": "count-in-polygon",
|
||||
"id": "a0",
|
||||
"params": {
|
||||
"columnName": 'count_airbnb',
|
||||
"pointsSource": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
query: "select * from analysis_rent_listings"
|
||||
},
|
||||
dataviews: {
|
||||
price_histogram: {
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'price'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"polygonsSource": {
|
||||
"id": "b1",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "b0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
query: "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 250
|
||||
},
|
||||
dataviews: {
|
||||
bank_category: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'bank'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
dataviews: {
|
||||
count_histogram: {
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'count_airbnb'
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('green', 1.0)
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
skip: true,
|
||||
desc: 'I. Distribution centers',
|
||||
mapConfig: mapConfig(
|
||||
// layers
|
||||
[
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
source: {
|
||||
id: 'a1'
|
||||
},
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
"source": { id: "b0" },
|
||||
"cartocss": [
|
||||
"#distribution_centers {",
|
||||
" marker-fill-opacity: 1.0;",
|
||||
" marker-line-color: #FFF;",
|
||||
" marker-line-width: 0.5;",
|
||||
" marker-line-opacity: 0.7;",
|
||||
" marker-placement: point;",
|
||||
" marker-type: ellipse;",
|
||||
" marker-width: 8;",
|
||||
" marker-fill: blue;",
|
||||
" marker-allow-overlap: true;",
|
||||
"}"
|
||||
].join('\n'),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select * from analysis_rent_listings',
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
"source": { id: "a0" },
|
||||
"cartocss": [
|
||||
"#shops {",
|
||||
" marker-fill-opacity: 1.0;",
|
||||
" marker-line-color: #FFF;",
|
||||
" marker-line-width: 0.5;",
|
||||
" marker-line-opacity: 0.7;",
|
||||
" marker-placement: point;",
|
||||
" marker-type: ellipse;",
|
||||
" marker-width: 8;",
|
||||
" marker-fill: red;",
|
||||
" marker-allow-overlap: true;",
|
||||
"}"
|
||||
].join('\n'),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "a1" },
|
||||
"cartocss": [
|
||||
"#routing {",
|
||||
" line-color: ramp([routing_time], colorbrewer(Reds));",
|
||||
" line-width: ramp([routing_time], 2, 8);",
|
||||
" line-opacity: 1.0;",
|
||||
"}"
|
||||
].join('\n'),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{},
|
||||
// dataviews
|
||||
{
|
||||
distribution_center_name_category: {
|
||||
source: { id: 'b0' },
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'name'
|
||||
}
|
||||
},
|
||||
time_histogram: {
|
||||
source: { id: 'a1' },
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'routing_time'
|
||||
}
|
||||
},
|
||||
distance_histogram: {
|
||||
source: { id: 'a1' },
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'routing_distance'
|
||||
}
|
||||
}
|
||||
},
|
||||
// analysis
|
||||
[
|
||||
pointInPolygonDef
|
||||
{
|
||||
id: 'a1',
|
||||
type: 'routing-n-to-n',
|
||||
params: {
|
||||
// distanceColumn: 'routing_distance',
|
||||
// timeColumn: 'routing_time',
|
||||
originSource: {
|
||||
id: 'b0',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from distribution_centers'
|
||||
}
|
||||
},
|
||||
destinationSource: {
|
||||
id: 'a0',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from shops'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings',
|
||||
skip: true,
|
||||
desc: 'II. Population analysis',
|
||||
mapConfig: mapConfig(
|
||||
// layers
|
||||
[
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "a2" },
|
||||
"cartocss": [
|
||||
"#count_in_polygon {",
|
||||
" polygon-opacity: 1.0",
|
||||
" line-color: #FFF;",
|
||||
" line-width: 0.5;",
|
||||
" line-opacity: 0.7",
|
||||
" polygon-fill: ramp([estimated_people], colorbrewer(Reds));",
|
||||
"}"
|
||||
].join('\n'),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "a0" },
|
||||
"cartocss": DEFAULT_MULTITYPE_STYLE,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
// dataviews
|
||||
{
|
||||
total_population_formula: {
|
||||
"source": { id: "a3" },
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: 'total_population',
|
||||
operation: 'sum'
|
||||
}
|
||||
},
|
||||
people_histogram: { // this injects a range filter at `a2` node output
|
||||
"source": { id: "a2" },
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'estimated_people'
|
||||
}
|
||||
},
|
||||
subway_line_category: { // this injects a category filter at `a0` node output
|
||||
"source": { id: "a0" },
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'subway_line'
|
||||
}
|
||||
}
|
||||
},
|
||||
// analysis
|
||||
[
|
||||
{
|
||||
id: 'a3',
|
||||
// this will union the polygons, produce just one polygon, and calculate the total population for it
|
||||
type: 'total-population',
|
||||
params: {
|
||||
columnName: 'total_population',
|
||||
source: {
|
||||
id: 'a2',
|
||||
type: 'estimated-population',
|
||||
params: {
|
||||
columnName: 'estimated_people',
|
||||
source: {
|
||||
id: 'a1',
|
||||
type: 'trade-area',
|
||||
params: {
|
||||
source: {
|
||||
"id": "a0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
query: "select * from subway_stops"
|
||||
}
|
||||
},
|
||||
kind: 'walk',
|
||||
time: 300
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
skip: true,
|
||||
desc: 'III. Point in polygon',
|
||||
mapConfig: mapConfig(
|
||||
// layers
|
||||
[
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select * from analysis_rent_listings',
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
source: {
|
||||
id: 'a1'
|
||||
},
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
source: {
|
||||
id: 'b1'
|
||||
},
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
"source": { id: "a1" },
|
||||
"cartocss": [
|
||||
"#count_in_polygon {",
|
||||
" polygon-opacity: 1.0",
|
||||
" line-color: #FFF;",
|
||||
" line-width: 0.5;",
|
||||
" line-opacity: 0.7",
|
||||
" polygon-fill: ramp([count_people], colorbrewer(Reds));",
|
||||
"}"
|
||||
].join('\n'),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{},
|
||||
// dataviews
|
||||
{
|
||||
age_histogram: {
|
||||
"source": { id: "a0" },
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'age'
|
||||
}
|
||||
},
|
||||
income_histogram: {
|
||||
"source": { id: "a0" },
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'income'
|
||||
}
|
||||
},
|
||||
gender_category: {
|
||||
"source": { id: "a0" },
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'gender'
|
||||
}
|
||||
}
|
||||
},
|
||||
// analysis
|
||||
[
|
||||
bufferDef,
|
||||
pointInPolygonDef
|
||||
{
|
||||
"id": "a1",
|
||||
"type": "count-in-polygon",
|
||||
"params": {
|
||||
"columnName": 'count_people',
|
||||
"pointsSource": {
|
||||
"id": 'a0',
|
||||
"type": "source",
|
||||
"params": {
|
||||
query: "select the_geom, age, gender, income from people"
|
||||
}
|
||||
},
|
||||
"polygonsSource": {
|
||||
"id": "b0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
query: "select * from postal_codes"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
useCases.forEach(function (useCase) {
|
||||
useCases.forEach(function(useCase, imageIdx) {
|
||||
if (!!useCase.skip) {
|
||||
return debug(JSON.stringify(useCase.mapConfig, null, 4));
|
||||
debug(JSON.stringify(useCase.mapConfig, null, 4));
|
||||
}
|
||||
it(`should implement use case: '${useCase.desc}'`, function (done) {
|
||||
it.skip('should implement use case: "' + useCase.desc + '"', function(done) {
|
||||
|
||||
var testClient = new TestClient(useCase.mapConfig, 1234);
|
||||
|
||||
var tile = useCase.tile || TILE_ANALYSIS_TABLES;
|
||||
|
||||
testClient.getTile(tile.z, tile.x, tile.y, function (err, res, image) {
|
||||
testClient.getTile(tile.z, tile.x, tile.y, function(err, res, image) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
//image.save('/tmp/tests/' + imageIdx + '---' + useCase.desc.replace(/\s/g, '-') + '.png');
|
||||
image.save('/tmp/tests/' + imageIdx + '---' + useCase.desc.replace(/\s/g, '-') + '.png');
|
||||
|
||||
assert.equal(image.width(), 256);
|
||||
|
||||
|
||||
@@ -1,441 +0,0 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var fs = require('fs');
|
||||
var assert = require('../support/assert');
|
||||
var TestClient = require('../support/test-client');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var IMAGE_TOLERANCE_PER_MIL = 5;
|
||||
|
||||
var CARTOCSS_LABELS = [
|
||||
'#layer {',
|
||||
' polygon-fill: #374C70;',
|
||||
' polygon-opacity: 0.9;',
|
||||
' line-width: 1;',
|
||||
' line-color: #FFF;',
|
||||
' line-opacity: 0.5;',
|
||||
'}',
|
||||
'#layer::labels {',
|
||||
' text-name: [name];',
|
||||
' text-face-name: \'DejaVu Sans Book\';',
|
||||
' text-size: 20;',
|
||||
' text-fill: #FFFFFF;',
|
||||
' text-label-position-tolerance: 0;',
|
||||
' text-halo-radius: 1;',
|
||||
' text-halo-fill: #6F808D;',
|
||||
' text-dy: -10;',
|
||||
' text-allow-overlap: true;',
|
||||
' text-placement: point;',
|
||||
' text-placement-type: dummy;',
|
||||
'}'
|
||||
].join('\n');
|
||||
|
||||
function createMapConfig (bufferSize, cartocss) {
|
||||
cartocss = cartocss || CARTOCSS_LABELS;
|
||||
|
||||
return {
|
||||
version: '1.6.0',
|
||||
buffersize: bufferSize,
|
||||
layers: [{
|
||||
type: "cartodb",
|
||||
options: {
|
||||
sql: [
|
||||
'select',
|
||||
' *',
|
||||
'from',
|
||||
' populated_places_simple_reduced',
|
||||
].join('\n'),
|
||||
cartocss: cartocss,
|
||||
cartocss_version: '2.3.0',
|
||||
interactivity: 'cartodb_id'
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
describe('buffer size per format', function () {
|
||||
var testCases = [
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 0',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-0.png',
|
||||
mapConfig: createMapConfig({ png: 0, 'grid.json': 0 }),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 128',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.png',
|
||||
mapConfig: createMapConfig({ png: 128, 'grid.json': 128 }),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get mvt tile using buffer-size 0',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'mvt',
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-0.mvt',
|
||||
mapConfig: createMapConfig({ mvt: 0 }),
|
||||
assert: function (tile, callback) {
|
||||
var tileJSON = tile.toJSON();
|
||||
var features = tileJSON[0].features;
|
||||
assert.equal(features.length, 1);
|
||||
callback();
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get mvt tile using buffer-size 128',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'mvt',
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.mvt',
|
||||
mapConfig: createMapConfig({ mvt: 128 }),
|
||||
assert: function (tile, callback) {
|
||||
var tileJSON = tile.toJSON();
|
||||
var features = tileJSON[0].features;
|
||||
assert.equal(features.length, 9);
|
||||
callback();
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get grid.json tile using buffer-size 0 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'grid.json',
|
||||
layers: [0],
|
||||
fixturePath: './test/fixtures/buffer-size/tile-grid.json.7.64.48-buffer-size-0.grid.json',
|
||||
mapConfig: createMapConfig({ 'grid.json': 0 }),
|
||||
assert: function (tile, callback) {
|
||||
assert.utfgridEqualsFile(tile, this.fixturePath, 2,callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get grid.json tile using buffer-size 128 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'grid.json',
|
||||
layers: [0],
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.grid.json',
|
||||
mapConfig: createMapConfig({ 'grid.json': 128 }),
|
||||
assert: function (tile, callback) {
|
||||
assert.utfgridEqualsFile(tile, this.fixturePath, 2, callback);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(function (test) {
|
||||
it(test.desc, function (done) {
|
||||
var testClient = new TestClient(test.mapConfig, 1234);
|
||||
var coords = test.coords;
|
||||
var options = {
|
||||
format: test.format,
|
||||
layers: test.layers
|
||||
};
|
||||
testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
// To generate images use:
|
||||
// tile.save(test.fixturePath);
|
||||
test.assert(tile, function (err) {
|
||||
assert.ifError(err);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createBufferSizeTemplate (name, buffersize, placeholders, cartocss) {
|
||||
cartocss = cartocss || CARTOCSS_LABELS;
|
||||
|
||||
return {
|
||||
"version": "0.0.1",
|
||||
"name": name,
|
||||
"placeholders": placeholders || {
|
||||
"buffersize": {
|
||||
"type": "number",
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"layergroup": createMapConfig(buffersize)
|
||||
};
|
||||
}
|
||||
|
||||
describe('buffer size per format for named maps', function () {
|
||||
var testCases = [
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 0 (default value in template)',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-0.png',
|
||||
template: createBufferSizeTemplate('named-default-buffer-size', {png: '<%= buffersize %>'}),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 128 (placehoder value)',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
placeholders: { buffersize: 128 },
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.png',
|
||||
template: createBufferSizeTemplate('named-custom-buffer-size', { png: '<%= buffersize %>'}),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 0 (default value in template by format)',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
placeholders: { buffersize_png: 0 },
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-0.png',
|
||||
template: createBufferSizeTemplate('named-default-buffer-size-by-format', {
|
||||
png: '<%= buffersize_png %>'
|
||||
}, {
|
||||
"buffersize_png": {
|
||||
"type": "number",
|
||||
"default": "0"
|
||||
}
|
||||
}),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 128 (placehoder value in template by format)',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
placeholders: { buffersize_png: 128 },
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.png',
|
||||
template: createBufferSizeTemplate('named-custom-buffer-size-by-format', {
|
||||
png: '<%= buffersize_png %>'
|
||||
}, {
|
||||
"buffersize_png": {
|
||||
"type": "number",
|
||||
"default": "0"
|
||||
}
|
||||
}),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get grid.json tile using buffer-size 0 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'grid.json',
|
||||
layers: [0],
|
||||
placeholders: { buffersize_gridjson: 0 },
|
||||
fixturePath: './test/fixtures/buffer-size/tile-grid.json.7.64.48-buffer-size-0.grid.json',
|
||||
template: createBufferSizeTemplate('named-default-buffer-size-by-format-gridjson', {
|
||||
'grid.json': '<%= buffersize_gridjson %>'
|
||||
}, {
|
||||
"buffersize_gridjson": {
|
||||
"type": "number",
|
||||
"default": "0"
|
||||
}
|
||||
}),
|
||||
assert: function (tile, callback) {
|
||||
assert.utfgridEqualsFile(tile, this.fixturePath, 2,callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get grid.json tile using buffer-size 128 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'grid.json',
|
||||
layers: [0],
|
||||
placeholders: { buffersize_gridjson: 128 },
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.grid.json',
|
||||
template: createBufferSizeTemplate('named-custom-buffer-size-by-format-gridjson', {
|
||||
'grid.json': '<%= buffersize_gridjson %>'
|
||||
}, {
|
||||
"buffersize_gridjson": {
|
||||
"type": "number",
|
||||
"default": "0"
|
||||
}
|
||||
}),
|
||||
assert: function (tile, callback) {
|
||||
assert.utfgridEqualsFile(tile, this.fixturePath, 2, callback);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(function (test) {
|
||||
it(test.desc, function (done) {
|
||||
var testClient = new TestClient(test.template, 1234);
|
||||
var coords = test.coords;
|
||||
var options = {
|
||||
format: test.format,
|
||||
placeholders: test.placeholders,
|
||||
layers: test.layers
|
||||
};
|
||||
testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
// To generate images use:
|
||||
//tile.save('./test/fixtures/buffer-size/tile-7.64.48-buffer-size-0-test.png');
|
||||
test.assert(tile, function (err) {
|
||||
assert.ifError(err);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('buffer size per format for named maps w/o placeholders', function () {
|
||||
var testCases = [
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 0 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
png: 0
|
||||
}
|
||||
},
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-0.png',
|
||||
template: createBufferSizeTemplate('named-no-buffer-size-png-0', {}, {}),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 128 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
png: 128
|
||||
}
|
||||
},
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.png',
|
||||
template: createBufferSizeTemplate('named-no-buffer-size-png-128', {}, {}),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get mvt tile using buffer-size 0 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'mvt',
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
mvt: 0
|
||||
}
|
||||
},
|
||||
fixturePath: './test/fixtures/buffer-size/tile-mvt-7.64.48-buffer-size-0.mvt',
|
||||
template: createBufferSizeTemplate('named-no-buffer-size-mvt', {}, {}),
|
||||
assert: function (tile, callback) {
|
||||
var tileJSON = tile.toJSON();
|
||||
var features = tileJSON[0].features;
|
||||
|
||||
var dataFixture = fs.readFileSync(this.fixturePath);
|
||||
var vtile = new mapnik.VectorTile(this.coords.z, this.coords.x, this.coords.y);
|
||||
vtile.setDataSync(dataFixture);
|
||||
var vtileJSON = vtile.toJSON();
|
||||
var vtileFeatures = vtileJSON[0].features;
|
||||
|
||||
assert.equal(features.length, vtileFeatures.length);
|
||||
callback();
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get mvt tile using buffer-size 128 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'mvt',
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
mvt: 128
|
||||
}
|
||||
},
|
||||
fixturePath: './test/fixtures/buffer-size/tile-mvt-7.64.48-buffer-size-128.mvt',
|
||||
template: createBufferSizeTemplate('named-no-buffer-size-mvt-128', {}, {}),
|
||||
assert: function (tile, callback) {
|
||||
var tileJSON = tile.toJSON();
|
||||
var features = tileJSON[0].features;
|
||||
|
||||
var dataFixture = fs.readFileSync(this.fixturePath);
|
||||
var vtile = new mapnik.VectorTile(this.coords.z, this.coords.x, this.coords.y);
|
||||
vtile.setDataSync(dataFixture);
|
||||
var vtileJSON = vtile.toJSON();
|
||||
var vtileFeatures = vtileJSON[0].features;
|
||||
|
||||
assert.equal(features.length, vtileFeatures.length);
|
||||
callback();
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get grid.json tile using buffer-size 0 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'grid.json',
|
||||
layers: [0],
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
'grid.json': 0
|
||||
}
|
||||
},
|
||||
fixturePath: './test/fixtures/buffer-size/tile-grid.json.7.64.48-buffer-size-0.grid.json',
|
||||
template: createBufferSizeTemplate('named-no-buffer-size-grid-json-0', {}, {}),
|
||||
assert: function (tile, callback) {
|
||||
assert.utfgridEqualsFile(tile, this.fixturePath, 2,callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get grid.json tile using buffer-size 128 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'grid.json',
|
||||
layers: [0],
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
'grid.json': 128
|
||||
}
|
||||
},
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.grid.json',
|
||||
template: createBufferSizeTemplate('named-no-buffer-size-grid-json-128', {}, {}),
|
||||
assert: function (tile, callback) {
|
||||
assert.utfgridEqualsFile(tile, this.fixturePath, 2, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 0' +
|
||||
' overriden by template params with no buffersize in mapconfig',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
png: 0
|
||||
}
|
||||
},
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-0.png',
|
||||
template: createBufferSizeTemplate('named-no-buffer-size-mapconfig-png-0', undefined, {}),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
testCases.forEach(function (test) {
|
||||
it(test.desc, function (done) {
|
||||
var testClient = new TestClient(test.template, 1234);
|
||||
var coords = test.coords;
|
||||
var options = {
|
||||
format: test.format,
|
||||
placeholders: test.placeholders,
|
||||
layers: test.layers
|
||||
};
|
||||
testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
// To generate images use:
|
||||
//tile.save(test.fixturePath);
|
||||
// require('fs').writeFileSync(test.fixturePath, JSON.stringify(tile));
|
||||
// require('fs').writeFileSync(test.fixturePath, tile.getDataSync());
|
||||
test.assert(tile, function (err) {
|
||||
assert.ifError(err);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -145,182 +145,4 @@ describe('aggregations happy cases', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var widgetSearchExpects = {
|
||||
'count': [ { category: 'other_a', value: 3 } ],
|
||||
'sum': [ { category: 'other_a', value: 6 } ],
|
||||
'avg': [ { category: 'other_a', value: 2 } ],
|
||||
'max': [ { category: 'other_a', value: 3 } ],
|
||||
'min': [ { category: 'other_a', value: 1 } ]
|
||||
};
|
||||
|
||||
Object.keys(operations_and_values).forEach(function (operation) {
|
||||
var description = 'should search OTHER category using "' + operation + '"';
|
||||
|
||||
it(description, function (done) {
|
||||
this.testClient = new TestClient(aggregationOperationMapConfig(operation, query_other, 'cat', 'val'));
|
||||
this.testClient.widgetSearch('cat', 'other_a', function (err, res, searchResult) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.ok(searchResult);
|
||||
assert.equal(searchResult.type, 'aggregation');
|
||||
|
||||
assert.equal(searchResult.categories.length, 1);
|
||||
assert.deepEqual(
|
||||
searchResult.categories,
|
||||
widgetSearchExpects[operation]
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregation-dataview: special float values', function() {
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
function createMapConfig(layers, dataviews, analysis) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: layers,
|
||||
dataviews: dataviews || {},
|
||||
analyses: analysis || []
|
||||
};
|
||||
}
|
||||
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"cartocss": "#points { marker-width: 10; marker-fill: red; }",
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
val_aggregation: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'cat',
|
||||
aggregation: 'avg',
|
||||
aggregationColumn: 'val'
|
||||
}
|
||||
},
|
||||
sum_aggregation_numeric: {
|
||||
source: {
|
||||
id: 'a1'
|
||||
},
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'cat',
|
||||
aggregation: 'sum',
|
||||
aggregationColumn: 'val'
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
"id": "a0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
'SELECT',
|
||||
' null::geometry the_geom_webmercator,',
|
||||
' CASE',
|
||||
' WHEN x % 4 = 0 THEN \'infinity\'::float',
|
||||
' WHEN x % 4 = 1 THEN \'-infinity\'::float',
|
||||
' WHEN x % 4 = 2 THEN \'NaN\'::float',
|
||||
' ELSE x',
|
||||
' END AS val,',
|
||||
' CASE',
|
||||
' WHEN x % 2 = 0 THEN \'category_1\'',
|
||||
' ELSE \'category_2\'',
|
||||
' END AS cat',
|
||||
'FROM generate_series(1, 1000) x'
|
||||
].join('\n')
|
||||
}
|
||||
}, {
|
||||
"id": "a1",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
'SELECT',
|
||||
' null::geometry the_geom_webmercator,',
|
||||
' CASE',
|
||||
' WHEN x % 3 = 0 THEN \'NaN\'::numeric',
|
||||
' WHEN x % 3 = 1 THEN x',
|
||||
' ELSE x',
|
||||
' END AS val,',
|
||||
' CASE',
|
||||
' WHEN x % 2 = 0 THEN \'category_1\'',
|
||||
' ELSE \'category_2\'',
|
||||
' END AS cat',
|
||||
'FROM generate_series(1, 1000) x'
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
// Source a0
|
||||
// -----------------------------------------------
|
||||
// the_geom_webmercator | val | cat
|
||||
// ----------------------+-----------+------------
|
||||
// | -Infinity | category_2
|
||||
// | NaN | category_1
|
||||
// | 3 | category_2
|
||||
// | Infinity | category_1
|
||||
// | -Infinity | category_2
|
||||
// | NaN | category_1
|
||||
// | 7 | category_2
|
||||
// | Infinity | category_1
|
||||
// | -Infinity | category_2
|
||||
// | NaN | category_1
|
||||
// | 11 | category_2
|
||||
// | " | "
|
||||
|
||||
var filters = [{ own_filter: 0 }, {}];
|
||||
filters.forEach(function (filter) {
|
||||
it('should handle special float values using filter: ' + JSON.stringify(filter), function(done) {
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('val_aggregation', { own_filter: 0 }, function(err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.ok(dataview.infinities === (250 + 250));
|
||||
assert.ok(dataview.nans === 250);
|
||||
assert.ok(dataview.categories.length === 1);
|
||||
dataview.categories.forEach(function (category) {
|
||||
assert.ok(category.category === 'category_2');
|
||||
assert.ok(category.value === 501);
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle special numeric values using filter: ' + JSON.stringify(filter), function(done) {
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('sum_aggregation_numeric', { own_filter: 0 }, function(err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.ok(dataview.nans === 333);
|
||||
assert.ok(dataview.categories.length === 2);
|
||||
dataview.categories.forEach(function (category) {
|
||||
assert.ok(category.value !== null);
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
require('../../support/test_helper');
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
function createMapConfig(layers, dataviews, analysis) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: layers,
|
||||
dataviews: dataviews || {},
|
||||
analyses: analysis || []
|
||||
};
|
||||
}
|
||||
|
||||
describe('formula-dataview: special float values', function() {
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"cartocss": "#points { marker-width: 10; marker-fill: red; }",
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
val_formula: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: 'val',
|
||||
operation: 'avg'
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
"id": "a0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
'SELECT',
|
||||
' null::geometry the_geom_webmercator,',
|
||||
' CASE',
|
||||
' WHEN x % 4 = 0 THEN \'infinity\'::float',
|
||||
' WHEN x % 4 = 1 THEN \'-infinity\'::float',
|
||||
' WHEN x % 4 = 2 THEN \'NaN\'::float',
|
||||
' ELSE x',
|
||||
' END AS val',
|
||||
'FROM generate_series(1, 1000) x'
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
it('should filter infinities out and count them in the summary', function(done) {
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('val_formula', {}, function(err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
assert.equal(dataview.result, 501);
|
||||
assert.ok(dataview.infinities === (250 + 250));
|
||||
assert.ok(dataview.nans === 250);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -124,13 +124,6 @@ describe('dataviews using tables with overviews', function() {
|
||||
params: {
|
||||
query: 'select * from test_table_overviews'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'data-source-special-float-values',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from test_special_float_values_table_overviews'
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
@@ -151,17 +144,6 @@ describe('dataviews using tables with overviews', function() {
|
||||
aggregationColumn: 'name',
|
||||
}
|
||||
},
|
||||
test_categories_special_values: {
|
||||
type: 'aggregation',
|
||||
source: {
|
||||
id: 'data-source-special-float-values'
|
||||
},
|
||||
options: {
|
||||
column: 'name',
|
||||
aggregation: 'sum',
|
||||
aggregationColumn: 'value',
|
||||
}
|
||||
},
|
||||
test_histogram: {
|
||||
type: 'histogram',
|
||||
source: {id: 'data-source'},
|
||||
@@ -178,16 +160,6 @@ describe('dataviews using tables with overviews', function() {
|
||||
bins: 2
|
||||
}
|
||||
},
|
||||
test_histogram_special_values: {
|
||||
type: 'histogram',
|
||||
source: {
|
||||
id: 'data-source-special-float-values'
|
||||
},
|
||||
options: {
|
||||
column: 'value',
|
||||
bins: 2
|
||||
}
|
||||
},
|
||||
test_avg: {
|
||||
type: 'formula',
|
||||
source: {id: 'data-source'},
|
||||
@@ -196,16 +168,6 @@ describe('dataviews using tables with overviews', function() {
|
||||
operation: 'avg'
|
||||
}
|
||||
},
|
||||
test_formula_sum_special_values: {
|
||||
type: 'formula',
|
||||
source: {
|
||||
id: 'data-source-special-float-values'
|
||||
},
|
||||
options: {
|
||||
column: 'value',
|
||||
operation: 'sum'
|
||||
}
|
||||
},
|
||||
test_count: {
|
||||
type: 'formula',
|
||||
source: {id: 'data-source'},
|
||||
@@ -240,17 +202,6 @@ describe('dataviews using tables with overviews', function() {
|
||||
cartocss_version: '2.3.0',
|
||||
source: { id: 'data-source' }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from test_special_float_values_table_overviews',
|
||||
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
|
||||
cartocss_version: '2.3.0',
|
||||
source: {
|
||||
id: 'data-source-special-float-values'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -261,14 +212,7 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation":"sum",
|
||||
"result":15,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"nulls":0,
|
||||
"type":"formula"
|
||||
});
|
||||
assert.deepEqual(formula_result, {"operation":"sum","result":15,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -280,14 +224,7 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation":"avg",
|
||||
"result":3,
|
||||
"nulls":0,
|
||||
"type":"formula",
|
||||
"infinities": 0,
|
||||
"nans": 0
|
||||
});
|
||||
assert.deepEqual(formula_result, {"operation":"avg","result":3,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -299,14 +236,7 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation":"count",
|
||||
"result":5,
|
||||
"nulls":0,
|
||||
"type":"formula",
|
||||
"infinities": 0,
|
||||
"nans": 0
|
||||
});
|
||||
assert.deepEqual(formula_result, {"operation":"count","result":5,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -318,14 +248,7 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation": "max",
|
||||
"result": 5,
|
||||
"nulls": 0,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"type": "formula"
|
||||
});
|
||||
assert.deepEqual(formula_result, {"operation":"max","result":5,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -337,14 +260,7 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation": "min",
|
||||
"result": 1,
|
||||
"nulls": 0,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"type": "formula"
|
||||
});
|
||||
assert.deepEqual(formula_result, {"operation":"min","result":1,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -359,14 +275,7 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation":"sum",
|
||||
"result":15,
|
||||
"nulls":0,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"type":"formula"
|
||||
});
|
||||
assert.deepEqual(formula_result, {"operation":"sum","result":15,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -463,14 +372,7 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation":"sum",
|
||||
"result":1,
|
||||
"nulls":0,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"type":"formula"
|
||||
});
|
||||
assert.deepEqual(formula_result, {"operation":"sum","result":1,"nulls":0,"type":"formula"});
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
@@ -481,14 +383,7 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation":"avg",
|
||||
"result":1,
|
||||
"nulls":0,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"type":"formula"
|
||||
});
|
||||
assert.deepEqual(formula_result, {"operation":"avg","result":1,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -500,14 +395,7 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation":"count",
|
||||
"result":1,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"nulls":0,
|
||||
"type":"formula"
|
||||
});
|
||||
assert.deepEqual(formula_result, {"operation":"count","result":1,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -519,14 +407,7 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation": "max",
|
||||
"result": 1,
|
||||
"nulls": 0,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"type": "formula"
|
||||
});
|
||||
assert.deepEqual(formula_result, {"operation":"max","result":1,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -538,14 +419,7 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation": "min",
|
||||
"result": 1,
|
||||
"nulls": 0,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"type": "formula"
|
||||
});
|
||||
assert.deepEqual(formula_result, {"operation":"min","result":1,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -563,14 +437,7 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation":"sum",
|
||||
"result":1,
|
||||
"nulls":0,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"type":"formula"
|
||||
});
|
||||
assert.deepEqual(formula_result, {"operation":"sum","result":1,"nulls":0,"type":"formula"});
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
@@ -578,69 +445,5 @@ describe('dataviews using tables with overviews', function() {
|
||||
|
||||
});
|
||||
|
||||
describe('aggregation special float values', function () {
|
||||
var params = {};
|
||||
|
||||
it("should expose an aggregation dataview filtering special float values out", function (done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_categories_special_values', params, function (err, dataview) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(dataview, {
|
||||
aggregation: 'sum',
|
||||
count: 5,
|
||||
nulls: 0,
|
||||
nans: 1,
|
||||
infinities: 1,
|
||||
min: 6,
|
||||
max: 6,
|
||||
categoriesCount: 1,
|
||||
categories: [ { category: 'Hawai', value: 6, agg: false } ],
|
||||
type: 'aggregation'
|
||||
});
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose a histogram dataview filtering special float values out', function (done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_histogram_special_values', params, function (err, dataview) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(dataview, {
|
||||
bin_width: 0,
|
||||
bins_count: 1,
|
||||
bins_start: 3,
|
||||
nulls: 0,
|
||||
infinities: 1,
|
||||
nans: 1,
|
||||
avg: 3,
|
||||
bins: [ { bin: 0, min: 3, max: 3, avg: 3, freq: 2 } ],
|
||||
type: 'histogram'
|
||||
});
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose a formula (sum) dataview filtering special float values out', function (done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_formula_sum_special_values', params, function (err, dataview) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(dataview, {
|
||||
operation: 'sum',
|
||||
result: 6,
|
||||
nulls: 0,
|
||||
nans: 1,
|
||||
infinities: 1,
|
||||
type: 'formula'
|
||||
});
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
311
test/acceptance/limits.js
Normal file
311
test/acceptance/limits.js
Normal file
@@ -0,0 +1,311 @@
|
||||
var testHelper = require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var _ = require('underscore');
|
||||
var redis = require('redis');
|
||||
|
||||
var CartodbWindshaft = require('../../lib/cartodb/server');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
|
||||
describe('render limits', function() {
|
||||
|
||||
var layergroupUrl = '/api/v1/map';
|
||||
|
||||
var redisClient = redis.createClient(global.environment.redis.port);
|
||||
|
||||
var server;
|
||||
var keysToDelete;
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
var user = 'localhost';
|
||||
|
||||
var pointSleepSql = "SELECT pg_sleep(0.5)," +
|
||||
" 'SRID=3857;POINT(0 0)'::geometry the_geom_webmercator, 1 cartodb_id";
|
||||
var pointCartoCss = '#layer { marker-fill:red; }';
|
||||
var polygonSleepSql = "SELECT pg_sleep(0.5)," +
|
||||
" ST_Buffer('SRID=3857;POINT(0 0)'::geometry, 100000000) the_geom_webmercator, 1 cartodb_id";
|
||||
var polygonCartoCss = '#layer { polygon-fill:red; }';
|
||||
|
||||
function singleLayergroupConfig(sql, cartocss) {
|
||||
return {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss: cartocss,
|
||||
cartocss_version: '2.0.1'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function createRequest(layergroup, userHost) {
|
||||
return {
|
||||
url: layergroupUrl,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: userHost,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
};
|
||||
}
|
||||
|
||||
function withRenderLimit(user, renderLimit, callback) {
|
||||
redisClient.SELECT(5, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var userLimitsKey = 'limits:tiler:' + user;
|
||||
redisClient.HSET(userLimitsKey, 'render', renderLimit, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
keysToDelete[userLimitsKey] = 5;
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
describe('with onTileErrorStrategy DISABLED', function() {
|
||||
var onTileErrorStrategyEnabled;
|
||||
before(function() {
|
||||
onTileErrorStrategyEnabled = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = false;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategyEnabled;
|
||||
});
|
||||
|
||||
it("layergroup creation fails if test tile is slow", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(polygonSleepSql, polygonCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed.errors, [ 'Render timed out' ]);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("layergroup creation does not fail if user limit is high enough even if test tile is slow", function(done) {
|
||||
withRenderLimit(user, 5000, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(polygonSleepSql, polygonCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("layergroup creation works if test tile is fast but tile request fails if they are slow", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(pointSleepSql, pointCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed.errors, ['Render timed out']);
|
||||
done();
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("tile request does not fail if user limit is high enough", function(done) {
|
||||
withRenderLimit(user, 5000, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(pointSleepSql, pointCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('with onTileErrorStrategy', function() {
|
||||
|
||||
it("layergroup creation works even if test tile is slow", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(polygonSleepSql, polygonCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("layergroup creation and tile requests works even if they are slow but returns fallback", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(pointSleepSql, pointCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
var referenceImagePath = './test/fixtures/render-timeout-fallback.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath, 25,
|
||||
function(imgErr/*, similarity*/) {
|
||||
done(imgErr);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1041,7 +1041,7 @@ describe(suiteName, function() {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/91
|
||||
// and https://github.com/CartoDB/Windshaft-cartodb/issues/38
|
||||
it("tiles for private tables can be fetched with api_key", function(done) {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
const assert = require('../support/assert');
|
||||
const TestClient = require('../support/test-client');
|
||||
|
||||
function createMapConfig (sql = TestClient.SQL.ONE_POINT) {
|
||||
return {
|
||||
version: '1.6.0',
|
||||
layers: [{
|
||||
type: "cartodb",
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss: TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version: '2.3.0',
|
||||
interactivity: 'cartodb_id'
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
describe('mvt', function () {
|
||||
const testCases = [
|
||||
{
|
||||
desc: 'should get empty mvt with code 204 (no content)',
|
||||
coords: { z: 0, x: 0, y: 0 },
|
||||
format: 'mvt',
|
||||
response: {
|
||||
status: 204,
|
||||
headers: {
|
||||
'Content-Type': undefined
|
||||
}
|
||||
},
|
||||
mapConfig: createMapConfig(TestClient.SQL.EMPTY)
|
||||
},
|
||||
{
|
||||
desc: 'should get mvt tile with code 200 (ok)',
|
||||
coords: { z: 0, x: 0, y: 0 },
|
||||
format: 'mvt',
|
||||
response: {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-protobuf'
|
||||
}
|
||||
},
|
||||
mapConfig: createMapConfig()
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(function (test) {
|
||||
it(test.desc, done => {
|
||||
const testClient = new TestClient(test.mapConfig, 1234);
|
||||
const { z, x, y } = test.coords;
|
||||
const { format, response } = test;
|
||||
|
||||
testClient.getTile(z, x, y, { format, response }, (err, res) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.equal(res.statusCode, test.response.status);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -192,36 +192,12 @@ describe('named maps static view', function() {
|
||||
}
|
||||
getStaticMap({ zoom: 3 }, function(err, img) {
|
||||
assert.ok(!err);
|
||||
img.save('/tmp/static.png');
|
||||
assert.imageIsSimilarToFile(img, previewFixture('override-zoom'), IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return override bbox', function (done) {
|
||||
var view = {
|
||||
bounds: {
|
||||
west: 0,
|
||||
south: 0,
|
||||
east: 45,
|
||||
north: 45
|
||||
},
|
||||
zoom: 4,
|
||||
center: {
|
||||
lng: 40,
|
||||
lat: 20
|
||||
}
|
||||
};
|
||||
templateMaps.addTemplate(username, createTemplate(view), function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
getStaticMap({ bbox: '0,45,90,45' }, function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('override-bbox'), IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow to select the layers to render', function (done) {
|
||||
var view = {
|
||||
bounds: {
|
||||
|
||||
@@ -5,7 +5,6 @@ var step = require('step');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
var testClient = require('./support/test_client');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
@@ -24,14 +23,6 @@ describe('multilayer error cases', function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
// var client = null;
|
||||
afterEach(function(done) {
|
||||
if (this.client) {
|
||||
return this.client.drain(done);
|
||||
}
|
||||
return done();
|
||||
});
|
||||
|
||||
it("post layergroup with wrong Content-Type", function(done) {
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
@@ -162,16 +153,24 @@ describe('multilayer error cases', function() {
|
||||
]
|
||||
};
|
||||
ServerOptions.afterLayergroupCreateCalls = 0;
|
||||
this.client = new TestClient(layergroup);
|
||||
this.client.getLayergroup({status: 400}, function(err, parsed) {
|
||||
assert.ok(!err, err);
|
||||
// See http://github.com/CartoDB/Windshaft/issues/159
|
||||
assert.equal(ServerOptions.afterLayergroupCreateCalls, 0);
|
||||
assert.ok(parsed);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
var error = parsed.errors[0];
|
||||
assert.ok(error.match(/column "missing" does not exist/m), error);
|
||||
done();
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) {
|
||||
try {
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
// See http://github.com/CartoDB/Windshaft/issues/159
|
||||
assert.equal(ServerOptions.afterLayergroupCreateCalls, 0);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
var error = parsed.errors[0];
|
||||
assert.ok(error.match(/column "missing" does not exist/m), error);
|
||||
// TODO: check which layer introduced the problem ?
|
||||
done();
|
||||
} catch (err) { done(err); }
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -16,13 +16,13 @@ describe('server_png8_format', function() {
|
||||
var serverOptionsPng32 = ServerOptions;
|
||||
serverOptionsPng32.grainstore = _.clone(ServerOptions.grainstore);
|
||||
serverOptionsPng32.grainstore.mapnik_tile_format = 'png32';
|
||||
var serverPng32 = cartodbServer(serverOptionsPng32);
|
||||
var serverPng32 = new cartodbServer(serverOptionsPng32);
|
||||
serverPng32.setMaxListeners(0);
|
||||
|
||||
var serverOptionsPng8 = ServerOptions;
|
||||
serverOptionsPng8.grainstore = _.clone(ServerOptions.grainstore);
|
||||
serverOptionsPng8.grainstore.mapnik_tile_format = 'png8:m=h';
|
||||
var serverPng8 = cartodbServer(serverOptionsPng8);
|
||||
var serverPng8 = new cartodbServer(serverOptionsPng8);
|
||||
serverPng8.setMaxListeners(0);
|
||||
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var TestClient = require('../support/test-client');
|
||||
|
||||
describe('special numeric values', function() {
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
var ATTRIBUTES_LAYER = 1;
|
||||
|
||||
function createMapConfig(sql, id, columns) {
|
||||
return {
|
||||
version: '1.6.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: "select 1 as id, 'SRID=4326;POINT(0 0)'::geometry as the_geom",
|
||||
cartocss: '#style { }',
|
||||
cartocss_version: '2.0.1'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: sql || "select 1 as i, 6 as n, 'SRID=4326;POINT(0 0)'::geometry as the_geom",
|
||||
attributes: {
|
||||
id: id || 'i',
|
||||
columns: columns || ['n']
|
||||
},
|
||||
cartocss: '#style { }',
|
||||
cartocss_version: '2.0.1'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
it('should retrieve special numeric values', function (done) {
|
||||
var featureId = 1;
|
||||
var sql = [
|
||||
'SELECT',
|
||||
' 1 as cartodb_id,',
|
||||
' null::geometry the_geom_webmercator,',
|
||||
' \'infinity\'::float as infinity,',
|
||||
' \'-infinity\'::float as _infinity,',
|
||||
' \'NaN\'::float as nan'
|
||||
].join('\n');
|
||||
var id = 'cartodb_id';
|
||||
var columns = ['infinity', '_infinity', 'nan'];
|
||||
|
||||
var mapConfig = createMapConfig(sql, id, columns);
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getFeatureAttributes(featureId, ATTRIBUTES_LAYER, {}, function (err, attributes) {
|
||||
assert.ifError(err);
|
||||
assert.equal(attributes.infinity, 'Infinity');
|
||||
assert.equal(attributes._infinity, '-Infinity');
|
||||
assert.equal(attributes.nan, 'NaN');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('Create mapnik layergroup', function() {
|
||||
before(function() {
|
||||
this.layerStatsConfig = global.environment.enabledFeatures.layerStats;
|
||||
global.environment.enabledFeatures.layerStats = true;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
global.environment.enabledFeatures.layerStats = this.layerStatsConfig;
|
||||
});
|
||||
|
||||
var cartocssVersion = '2.3.0';
|
||||
var cartocss = '#layer { line-width:16; }';
|
||||
|
||||
var mapnikLayer1 = {
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from test_table limit 1',
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss
|
||||
}
|
||||
};
|
||||
|
||||
var mapnikLayer2 = {
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from test_table_2 limit 2',
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss
|
||||
}
|
||||
};
|
||||
|
||||
var mapnikLayer3 = {
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from test_table_3 limit 3',
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss
|
||||
}
|
||||
};
|
||||
|
||||
var mapnikLayer4 = {
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: [
|
||||
'select t1.cartodb_id, t1.the_geom, t1.the_geom_webmercator, t2.address',
|
||||
' from test_table t1, test_table_2 t2',
|
||||
' where t1.cartodb_id = t2.cartodb_id;'
|
||||
].join(''),
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss
|
||||
}
|
||||
};
|
||||
|
||||
var httpLayer = {
|
||||
type: 'http',
|
||||
options: {
|
||||
urlTemplate: 'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png',
|
||||
subdomains: ['a','b','c']
|
||||
}
|
||||
};
|
||||
|
||||
var mapnikLayerGeomColumn = {
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select *, the_geom as my_geom from test_table_3 limit 2',
|
||||
geom_column: 'my_geom',
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss
|
||||
}
|
||||
};
|
||||
|
||||
function mapnikBasicLayerId(index) {
|
||||
return 'layer' + index;
|
||||
}
|
||||
function typeLayerId(type, index) {
|
||||
return type + '-' + mapnikBasicLayerId(index);
|
||||
}
|
||||
|
||||
it('with one mapnik layer should response with meta-stats for that layer', function(done) {
|
||||
var testClient = new TestClient({
|
||||
version: '1.4.0',
|
||||
layers: [
|
||||
mapnikLayer1
|
||||
]
|
||||
});
|
||||
|
||||
testClient.getLayergroup(function(err, layergroup) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layergroup.metadata.layers[0].id, mapnikBasicLayerId(0));
|
||||
assert.equal(layergroup.metadata.layers[0].meta.stats.estimatedFeatureCount, 1);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('with two mapnik layer should response with meta-stats for every layer', function(done) {
|
||||
var testClient = new TestClient({
|
||||
version: '1.4.0',
|
||||
layers: [
|
||||
mapnikLayer1,
|
||||
mapnikLayer2
|
||||
]
|
||||
});
|
||||
|
||||
testClient.getLayergroup(function(err, layergroup) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layergroup.metadata.layers[0].id, mapnikBasicLayerId(0));
|
||||
assert.equal(layergroup.metadata.layers[0].meta.stats.estimatedFeatureCount, 1);
|
||||
assert.equal(layergroup.metadata.layers[1].id, mapnikBasicLayerId(1));
|
||||
assert.equal(layergroup.metadata.layers[1].meta.stats.estimatedFeatureCount, 2);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('with three mapnik layer should response with meta-stats for every layer', function(done) {
|
||||
var testClient = new TestClient({
|
||||
version: '1.4.0',
|
||||
layers: [
|
||||
mapnikLayer1,
|
||||
mapnikLayer2,
|
||||
mapnikLayer3
|
||||
]
|
||||
});
|
||||
|
||||
testClient.getLayergroup(function(err, layergroup) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layergroup.metadata.layers[0].id, mapnikBasicLayerId(0));
|
||||
assert.equal(layergroup.metadata.layers[0].meta.stats.estimatedFeatureCount, 1);
|
||||
assert.equal(layergroup.metadata.layers[1].id, mapnikBasicLayerId(1));
|
||||
assert.equal(layergroup.metadata.layers[1].meta.stats.estimatedFeatureCount, 2);
|
||||
assert.equal(layergroup.metadata.layers[2].id, mapnikBasicLayerId(2));
|
||||
assert.equal(layergroup.metadata.layers[2].meta.stats.estimatedFeatureCount, 3);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('with one mapnik layer (sql with join) should response with meta-stats for that layer', function(done) {
|
||||
var testClient = new TestClient({
|
||||
version: '1.4.0',
|
||||
layers: [
|
||||
mapnikLayer4
|
||||
]
|
||||
});
|
||||
|
||||
testClient.getLayergroup(function(err, layergroup) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layergroup.metadata.layers[0].id, mapnikBasicLayerId(0));
|
||||
assert.equal(layergroup.metadata.layers[0].meta.stats.estimatedFeatureCount, 5);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('with two mapnik layer (sql with join) should response with meta-stats for every layer', function(done) {
|
||||
var testClient = new TestClient({
|
||||
version: '1.4.0',
|
||||
layers: [
|
||||
mapnikLayer4,
|
||||
mapnikLayer4
|
||||
]
|
||||
});
|
||||
|
||||
testClient.getLayergroup(function(err, layergroup) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layergroup.metadata.layers[0].id, mapnikBasicLayerId(0));
|
||||
assert.equal(layergroup.metadata.layers[0].meta.stats.estimatedFeatureCount, 5);
|
||||
assert.equal(layergroup.metadata.layers[1].id, mapnikBasicLayerId(1));
|
||||
assert.equal(layergroup.metadata.layers[1].meta.stats.estimatedFeatureCount, 5);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('with two mapnik layer (with & without join) should response with meta-stats for every layer', function(done) {
|
||||
var testClient = new TestClient({
|
||||
version: '1.4.0',
|
||||
layers: [
|
||||
mapnikLayer3,
|
||||
mapnikLayer4
|
||||
]
|
||||
});
|
||||
|
||||
testClient.getLayergroup(function(err, layergroup) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layergroup.metadata.layers[0].id, mapnikBasicLayerId(0));
|
||||
assert.equal(layergroup.metadata.layers[0].meta.stats.estimatedFeatureCount, 3);
|
||||
assert.ok(!layergroup.metadata.layers[0].meta.stats[1]);
|
||||
assert.equal(layergroup.metadata.layers[1].id, mapnikBasicLayerId(1));
|
||||
assert.equal(layergroup.metadata.layers[1].meta.stats.estimatedFeatureCount, 5);
|
||||
assert.ok(!layergroup.metadata.layers[2]);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('with mapnik and layer and httplayer should response with layer metadata with same order', function(done) {
|
||||
var testClient = new TestClient({
|
||||
version: '1.4.0',
|
||||
layers: [
|
||||
mapnikLayer1,
|
||||
httpLayer
|
||||
]
|
||||
});
|
||||
|
||||
testClient.getLayergroup(function(err, layergroup) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layergroup.metadata.layers[0].id, mapnikBasicLayerId(0));
|
||||
assert.equal(layergroup.metadata.layers[0].type, 'mapnik');
|
||||
assert.equal(layergroup.metadata.layers[0].meta.stats.estimatedFeatureCount, 1);
|
||||
assert.equal(layergroup.metadata.layers[1].id, typeLayerId('http', 0));
|
||||
assert.equal(layergroup.metadata.layers[1].type, 'http');
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('with httpLayer and mapnik layer should response with layer metadata with same order', function(done) {
|
||||
var testClient = new TestClient({
|
||||
version: '1.4.0',
|
||||
layers: [
|
||||
httpLayer,
|
||||
mapnikLayer1
|
||||
]
|
||||
});
|
||||
|
||||
testClient.getLayergroup(function (err, layergroup) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layergroup.metadata.layers[0].id, typeLayerId('http', 0));
|
||||
assert.equal(layergroup.metadata.layers[0].type, 'http');
|
||||
assert.ok(!layergroup.metadata.layers[0].meta.cartocss);
|
||||
assert.equal(layergroup.metadata.layers[1].meta.stats.estimatedFeatureCount, 1);
|
||||
assert.equal(layergroup.metadata.layers[1].id, mapnikBasicLayerId(0));
|
||||
assert.equal(layergroup.metadata.layers[1].type, 'mapnik');
|
||||
assert.equal(layergroup.metadata.layers[1].meta.cartocss, cartocss);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with different geom_column', function(done) {
|
||||
var testClient = new TestClient({
|
||||
version: '1.4.0',
|
||||
layers: [
|
||||
mapnikLayerGeomColumn
|
||||
]
|
||||
});
|
||||
|
||||
testClient.getLayergroup(function(err, layergroup) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layergroup.metadata.layers[0].id, mapnikBasicLayerId(0));
|
||||
// we don't care about stats here as is an aliased column
|
||||
assert.ok(layergroup.metadata.layers[0].meta.stats.hasOwnProperty('estimatedFeatureCount'));
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not include the stats part if the FF is disabled', function(done) {
|
||||
global.environment.enabledFeatures.layerStats = false;
|
||||
var testClient = new TestClient({
|
||||
version: '1.4.0',
|
||||
layers: [
|
||||
httpLayer,
|
||||
mapnikLayer1,
|
||||
httpLayer
|
||||
]
|
||||
});
|
||||
|
||||
testClient.getLayergroup(function(err, layergroup) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layergroup.metadata.layers[0].id, typeLayerId('http', 0));
|
||||
assert.equal(layergroup.metadata.layers[0].type, 'http');
|
||||
assert.equal(layergroup.metadata.layers[1].id, mapnikBasicLayerId(0));
|
||||
assert.equal(layergroup.metadata.layers[1].type, 'mapnik');
|
||||
assert.ok(!layergroup.metadata.layers[1].meta.hasOwnProperty('stats'));
|
||||
assert.equal(layergroup.metadata.layers[2].id, typeLayerId('http', 1));
|
||||
assert.equal(layergroup.metadata.layers[2].type, 'http');
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,225 +0,0 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('multilayer stats disabled', function() {
|
||||
|
||||
before(function () {
|
||||
this.layerMetadataConfig = global.environment.enabledFeatures.layerMetadata;
|
||||
this.layerStatsConfig = global.environment.enabledFeatures.layerStats;
|
||||
global.environment.enabledFeatures.layerMetadata = true;
|
||||
global.environment.enabledFeatures.layerStats = false;
|
||||
});
|
||||
|
||||
after(function () {
|
||||
global.environment.enabledFeatures.layerMetadata = this.layerMetadataConfig;
|
||||
global.environment.enabledFeatures.layerStats = this.layerStatsConfig;
|
||||
});
|
||||
|
||||
|
||||
function testLayerMetadataStats(testScenario) {
|
||||
|
||||
it(testScenario.desc, function(done) {
|
||||
var mapConfig = {
|
||||
version: '1.3.0',
|
||||
layers: testScenario.layers
|
||||
};
|
||||
|
||||
var testClient = new TestClient(mapConfig);
|
||||
|
||||
testClient.getLayergroup(function(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
layergroup.metadata.layers.forEach(function (layer) {
|
||||
if (layer.type !== 'torque' && layer.type !== 'mapnik') {
|
||||
assert.ok(!('stats' in layer.meta));
|
||||
} else if (layer.type !== 'torque') {
|
||||
assert.ok(!('stats' in layer.meta));
|
||||
assert.ok('cartocss' in layer.meta);
|
||||
} else {
|
||||
assert.ok('cartocss' in layer.meta);
|
||||
// check torque metadata at least match in number
|
||||
var torqueLayers = mapConfig.layers.filter(function(layer) { return layer.type === 'torque'; });
|
||||
if (torqueLayers.length) {
|
||||
assert.equal(Object.keys(layergroup.metadata.torque).length, torqueLayers.length);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var cartocssVersion = '2.3.0';
|
||||
var cartocss = '#layer { line-width:16; }';
|
||||
var sql = "select 1 as i, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom, " +
|
||||
"st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 3857) as the_geom_webmercator";
|
||||
var sqlWadus = "select 1 as wadus, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom, " +
|
||||
"st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 3857) as the_geom_webmercator";
|
||||
|
||||
var httpLayer = {
|
||||
type: 'http',
|
||||
options: {
|
||||
urlTemplate: 'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png',
|
||||
subdomains: ['a','b','c']
|
||||
}
|
||||
};
|
||||
|
||||
var torqueLayer = {
|
||||
type: 'torque',
|
||||
options: {
|
||||
sql: "select 1 id, '1970-01-02'::date d, 'POINT(0 0)'::geometry the_geom_webmercator",
|
||||
cartocss: [
|
||||
"Map {",
|
||||
"-torque-frame-count:2;",
|
||||
"-torque-resolution:3;",
|
||||
"-torque-time-attribute:d;",
|
||||
"-torque-aggregation-function:'count(id)';",
|
||||
"}"
|
||||
].join(' '),
|
||||
cartocss_version: '2.0.1'
|
||||
}
|
||||
};
|
||||
|
||||
var mapnikLayer = {
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss
|
||||
}
|
||||
};
|
||||
|
||||
var mapnikInteractivityLayer = {
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss,
|
||||
interactivity: 'i'
|
||||
}
|
||||
};
|
||||
|
||||
var cartodbLayer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss
|
||||
}
|
||||
};
|
||||
|
||||
var cartodbInteractivityLayer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss,
|
||||
interactivity: 'i'
|
||||
}
|
||||
};
|
||||
|
||||
var cartodbWadusInteractivityLayer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: sqlWadus,
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss,
|
||||
interactivity: 'wadus'
|
||||
}
|
||||
};
|
||||
|
||||
var noTypeLayer = {
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss
|
||||
}
|
||||
};
|
||||
|
||||
var noTypeInteractivityLayer = {
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss_version: cartocssVersion,
|
||||
cartocss: cartocss,
|
||||
interactivity: 'i'
|
||||
}
|
||||
};
|
||||
|
||||
var testScenarios = [
|
||||
{
|
||||
desc: 'one layer, no interactivity',
|
||||
layers: [cartodbLayer]
|
||||
},
|
||||
{
|
||||
desc: 'two layers, different interactivity columns',
|
||||
layers: [
|
||||
cartodbWadusInteractivityLayer,
|
||||
cartodbInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'torque + interactivity layers',
|
||||
layers: [
|
||||
torqueLayer,
|
||||
cartodbWadusInteractivityLayer,
|
||||
cartodbInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'interactivity + torque + interactivity',
|
||||
layers: [
|
||||
cartodbInteractivityLayer,
|
||||
torqueLayer,
|
||||
cartodbInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'http + interactivity + torque + no interactivity + torque + interactivity',
|
||||
layers: [
|
||||
httpLayer,
|
||||
cartodbInteractivityLayer,
|
||||
torqueLayer,
|
||||
cartodbLayer,
|
||||
torqueLayer,
|
||||
cartodbWadusInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'mapnik type – two layers, interactivity mix',
|
||||
layers: [
|
||||
mapnikLayer,
|
||||
mapnikInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'mapnik type – http + interactivity + torque + interactivity',
|
||||
layers: [
|
||||
httpLayer,
|
||||
mapnikInteractivityLayer,
|
||||
torqueLayer,
|
||||
cartodbInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'no type – two layers, interactivity mix',
|
||||
layers: [
|
||||
noTypeLayer,
|
||||
noTypeInteractivityLayer
|
||||
]
|
||||
},
|
||||
{
|
||||
desc: 'no type – http + interactivity + torque + interactivity',
|
||||
layers: [
|
||||
httpLayer,
|
||||
noTypeInteractivityLayer,
|
||||
torqueLayer,
|
||||
noTypeInteractivityLayer
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
testScenarios.forEach(testLayerMetadataStats);
|
||||
|
||||
});
|
||||
@@ -1,786 +0,0 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
const assert = require('../support/assert');
|
||||
const TestClient = require('../support/test-client');
|
||||
|
||||
const timeoutErrorTilePath = `${process.cwd()}/assets/render-timeout-fallback.png`;
|
||||
|
||||
const pointSleepSql = `
|
||||
SELECT
|
||||
pg_sleep(0.3),
|
||||
'SRID=3857;POINT(0 0)'::geometry the_geom_webmercator,
|
||||
1 cartodb_id,
|
||||
2 val
|
||||
`;
|
||||
|
||||
const validationPointSleepSql = `
|
||||
SELECT
|
||||
pg_sleep(0.3),
|
||||
ST_Transform('SRID=4326;POINT(-180 85.05112877)'::geometry, 3857) the_geom_webmercator,
|
||||
1 cartodb_id,
|
||||
2 val
|
||||
`;
|
||||
|
||||
const createMapConfig = ({
|
||||
version = '1.6.0',
|
||||
type = 'cartodb',
|
||||
sql = pointSleepSql,
|
||||
cartocss = TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version = '2.3.0',
|
||||
interactivity = 'cartodb_id',
|
||||
countBy = 'cartodb_id',
|
||||
attributes
|
||||
} = {}) => ({
|
||||
version,
|
||||
layers: [{
|
||||
type,
|
||||
options: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
cartocss,
|
||||
cartocss_version,
|
||||
attributes,
|
||||
interactivity
|
||||
}
|
||||
}],
|
||||
analyses: [
|
||||
{
|
||||
id: 'a0',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: sql
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
count: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: countBy,
|
||||
operation: 'count'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const DATASOURCE_TIMEOUT_ERROR = {
|
||||
errors: ['You are over platform\'s limits. Please contact us to know more details'],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'datasource',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details'
|
||||
}]
|
||||
};
|
||||
|
||||
describe('user database timeout limit', function () {
|
||||
describe('dataview', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation works but dataview request fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getDataview('count', params, (err, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('raster', function () {
|
||||
describe('while validating in layergroup creation', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({ sql: validationPointSleepSql });
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to statement timeout', function (done) {
|
||||
const expectedResponse = {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'datasource',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details',
|
||||
layer: { id: 'layer0', index: 0, type: 'mapnik' }
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetching raster tiles', function () {
|
||||
describe('with user\'s timeout of 200 ms', function () {
|
||||
describe('with onTileErrorStrategy ENABLED', function () {
|
||||
let onTileErrorStrategy;
|
||||
|
||||
beforeEach(function (done) {
|
||||
onTileErrorStrategy = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = true;
|
||||
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
const expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.layergroupid = res.layergroupid;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategy;
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('"png" fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
format: 'png',
|
||||
layers: [ 0 ]
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, tile) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.imageIsSimilarToFile(tile, timeoutErrorTilePath, 0.05, (err) => {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('"static png" fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
zoom: 0,
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
width: 256,
|
||||
height: 256,
|
||||
format: 'png'
|
||||
};
|
||||
|
||||
this.testClient.getStaticCenter(params, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.imageIsSimilarToFile(tile, timeoutErrorTilePath, 0.05, (err) => {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with onTileErrorStrategy DISABLED', function () {
|
||||
let onTileErrorStrategy;
|
||||
|
||||
beforeEach(function (done) {
|
||||
onTileErrorStrategy = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = false;
|
||||
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
const expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.layergroupid = res.layergroupid;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategy;
|
||||
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('"png" fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
format: 'png',
|
||||
layers: [ 0 ],
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('"static png" fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
zoom: 0,
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
width: 256,
|
||||
height: 256,
|
||||
format: 'png',
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getStaticCenter(params, (err, res, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('vector', function () {
|
||||
describe('while validating in layergroup creation', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({ sql: validationPointSleepSql });
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to statement timeout', function (done) {
|
||||
const expectedResponse = {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'datasource',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details',
|
||||
layer: { id: 'layer0', index: 0, type: 'mapnik' }
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetching vector tiles', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
const expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.layergroupid = res.layergroupid;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
describe('with user\'s timeout of 200 ms', function () {
|
||||
beforeEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, done);
|
||||
});
|
||||
|
||||
it('"mvt" fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
format: 'mvt',
|
||||
layers: [ 0 ],
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('interactivity', function () {
|
||||
describe('while validating in layergroup creation', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({ sql: validationPointSleepSql, interactivity: 'val' });
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to statement timeout', function (done) {
|
||||
const expectedResponse = {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'datasource',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details',
|
||||
layer: { id: 'layer0', index: 0, type: 'mapnik' }
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetching interactivity tiles', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({ interactivity: 'val' });
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
const expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.layergroupid = res.layergroupid;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
describe('with user\'s timeout of 200 ms', function () {
|
||||
beforeEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, done);
|
||||
});
|
||||
|
||||
it('"grid.json" fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
format: 'grid.json',
|
||||
layers: 'mapnik',
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('torque', function () {
|
||||
describe('while validating in layergroup creation', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({
|
||||
type: 'torque',
|
||||
cartocss: TestClient.CARTOCSS.TORQUE
|
||||
});
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('fails due to statement timeout', function (done) {
|
||||
const expectedResponse = {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'datasource',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details',
|
||||
layer: { id: 'torque-layer0', index: 0, type: 'torque' }
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetching torque tiles', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({
|
||||
type: 'torque',
|
||||
cartocss: TestClient.CARTOCSS.TORQUE
|
||||
});
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
const expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.layergroupid = res.layergroupid;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
describe('with user\'s timeout of 200 ms', function () {
|
||||
beforeEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, done);
|
||||
});
|
||||
|
||||
it('"torque.json" fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
format: 'torque.json',
|
||||
layers: [ 0 ],
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('".png" fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
format: 'torque.png',
|
||||
layers: [ 0 ],
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, attributes) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(attributes, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('attributes:', function () {
|
||||
describe('while validating in map instatiation', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({
|
||||
attributes: {
|
||||
id: 'cartodb_id',
|
||||
columns: [ 'val' ]
|
||||
}
|
||||
});
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation fails due to statement timeout', function (done) {
|
||||
const expectedResponse = {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'datasource',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details',
|
||||
layer: {
|
||||
id: 'layer0',
|
||||
index: 0,
|
||||
type: 'mapnik'
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetching by feature id', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({
|
||||
attributes: {
|
||||
id: 'cartodb_id',
|
||||
columns: [ 'val' ]
|
||||
}
|
||||
});
|
||||
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
|
||||
const expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.layergroupid = res.layergroupid;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
describe('with user\'s timeout of 200 ms', function () {
|
||||
beforeEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(200, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, done);
|
||||
});
|
||||
|
||||
it('fails due to statement timeout', function (done) {
|
||||
const params = {
|
||||
layergroupid: this.layergroupid,
|
||||
featureId: 1,
|
||||
layer: 0,
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getAttributes(params, (err, res, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,394 +0,0 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
const assert = require('../support/assert');
|
||||
const TestClient = require('../support/test-client');
|
||||
|
||||
const timeoutErrorTilePath = `${process.cwd()}/assets/render-timeout-fallback.png`;
|
||||
|
||||
const pointSleepSql = `
|
||||
SELECT
|
||||
pg_sleep(0.5),
|
||||
'SRID=3857;POINT(0 0)'::geometry the_geom_webmercator,
|
||||
1 cartodb_id,
|
||||
2 val
|
||||
`;
|
||||
|
||||
// during instatiation we validate tile 30/0/0, creating a point in that tile `pg_sleep` will throw a timeout
|
||||
const validationPointSleepSql = `
|
||||
SELECT
|
||||
pg_sleep(0.5),
|
||||
ST_Transform('SRID=4326;POINT(-180 85.05112877)'::geometry, 3857) the_geom_webmercator,
|
||||
1 cartodb_id,
|
||||
2 val
|
||||
`;
|
||||
|
||||
const createMapConfig = ({
|
||||
version = '1.6.0',
|
||||
type = 'cartodb',
|
||||
sql = pointSleepSql,
|
||||
cartocss = TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version = '2.3.0',
|
||||
interactivity = 'cartodb_id',
|
||||
countBy = 'cartodb_id'
|
||||
} = {}) => ({
|
||||
version,
|
||||
layers: [{
|
||||
type,
|
||||
options: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
cartocss,
|
||||
cartocss_version,
|
||||
interactivity
|
||||
}
|
||||
}],
|
||||
analyses: [
|
||||
{
|
||||
id: 'a0',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: sql
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
count: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: countBy,
|
||||
operation: 'count'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe('user render timeout limit', function () {
|
||||
describe('map instantiation => validation', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig({ sql: validationPointSleepSql });
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 50, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation fails due to statement timeout', function (done) {
|
||||
const expectedResponse = {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: ["You are over platform\'s limits. Please contact us to know more details"],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'render',
|
||||
message: "You are over platform\'s limits. Please contact us to know more details",
|
||||
layer: {
|
||||
id: "layer0",
|
||||
index: 0,
|
||||
type: "mapnik"
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('raster', function () {
|
||||
describe('with onTileErrorStrategy ENABLED', function () {
|
||||
let onTileErrorStrategy;
|
||||
|
||||
beforeEach(function (done) {
|
||||
onTileErrorStrategy = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = true;
|
||||
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 50, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategy;
|
||||
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation works but tile request fails due to render timeout', function (done) {
|
||||
this.testClient.getTile(0, 0, 0, {}, (err, res, tile) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.imageIsSimilarToFile(tile, timeoutErrorTilePath, 0.05, (err) => {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with onTileErrorStrategy DISABLED', function() {
|
||||
var onTileErrorStrategy;
|
||||
|
||||
beforeEach(function (done) {
|
||||
onTileErrorStrategy = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = false;
|
||||
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 50, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategy;
|
||||
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation works and render tile fails', function (done) {
|
||||
var params = {
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: ["You are over platform\'s limits. Please contact us to know more details"],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'render',
|
||||
message: "You are over platform\'s limits. Please contact us to know more details"
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('vector', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 50, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation works but vector tile request fails due to render timeout', function (done) {
|
||||
const params = {
|
||||
format: 'mvt',
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, tile) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(tile, {
|
||||
errors: ['You are over platform\'s limits. Please contact us to know more details'],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'render',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details'
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('interativity', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 50, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation works but "grid.json" tile request fails due to render timeout', function (done) {
|
||||
const params = {
|
||||
layers: 'mapnik',
|
||||
format: 'grid.json',
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, tile) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(tile, {
|
||||
errors: ['You are over platform\'s limits. Please contact us to know more details'],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'render',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details'
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('static images', function () {
|
||||
describe('with onTileErrorStrategy ENABLED', function () {
|
||||
let onTileErrorStrategy;
|
||||
|
||||
beforeEach(function (done) {
|
||||
onTileErrorStrategy = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = true;
|
||||
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 50, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategy;
|
||||
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation works but static image fails due to render timeout', function (done) {
|
||||
const params = {
|
||||
zoom: 0,
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
width: 256,
|
||||
height: 256,
|
||||
format: 'png'
|
||||
};
|
||||
|
||||
this.testClient.getStaticCenter(params, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.imageIsSimilarToFile(tile, timeoutErrorTilePath, 0.05, (err) => {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with onTileErrorStrategy DISABLED', function() {
|
||||
var onTileErrorStrategy;
|
||||
|
||||
beforeEach(function (done) {
|
||||
onTileErrorStrategy = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = false;
|
||||
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 50, done);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategy;
|
||||
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation works and render tile fails', function (done) {
|
||||
const params = {
|
||||
zoom: 0,
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
width: 256,
|
||||
height: 256,
|
||||
format: 'png',
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getStaticCenter(params, function (err, res, timeoutError) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: ["You are over platform\'s limits. Please contact us to know more details"],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'render',
|
||||
message: "You are over platform\'s limits. Please contact us to know more details"
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
51
test/acceptance/widgets/list.js
Normal file
51
test/acceptance/widgets/list.js
Normal file
@@ -0,0 +1,51 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('list widgets', function() {
|
||||
|
||||
it("should expose layer list", function(done) {
|
||||
|
||||
var listWidgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from test_table',
|
||||
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
|
||||
cartocss_version: '2.3.0',
|
||||
widgets: {
|
||||
names: {
|
||||
type: 'list',
|
||||
options: {
|
||||
columns: ['name']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var testClient = new TestClient(listWidgetMapConfig);
|
||||
|
||||
testClient.getWidget('names', function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var expectedList = [
|
||||
{name:"Hawai"},
|
||||
{name:"El Estocolmo"},
|
||||
{name:"El Rey del Tallarín"},
|
||||
{name:"El Lacón"},
|
||||
{name:"El Pico"}
|
||||
];
|
||||
assert.deepEqual(JSON.parse(res.body).rows, expectedList);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -322,25 +322,6 @@ describe('widgets', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
[adm0name].forEach(function(userQuery) {
|
||||
it('should search with sum aggregation: ' + userQuery, function(done) {
|
||||
this.testClient = new TestClient(aggregationSumMapConfig);
|
||||
this.testClient.widgetSearch('adm0name', userQuery, function (err, res, searchResult) {
|
||||
assert.ok(!err, err);
|
||||
assert.ok(searchResult);
|
||||
assert.equal(searchResult.type, 'aggregation');
|
||||
|
||||
assert.equal(searchResult.categories.length, 1);
|
||||
assert.deepEqual(
|
||||
searchResult.categories,
|
||||
[{ category:"Argentina", value:28015640 }]
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
106
test/acceptance/widgets/ported/list.js
Normal file
106
test/acceptance/widgets/ported/list.js
Normal file
@@ -0,0 +1,106 @@
|
||||
require('../../../support/test_helper');
|
||||
|
||||
var assert = require('../../../support/assert');
|
||||
var TestClient = require('../../../support/test-client');
|
||||
var _ = require('underscore');
|
||||
|
||||
describe('widgets', function() {
|
||||
|
||||
describe('lists', function() {
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
function listsMapConfig(columns) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from test_table',
|
||||
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }',
|
||||
cartocss_version: '2.0.1',
|
||||
widgets: {
|
||||
places: {
|
||||
type: 'list',
|
||||
options: {
|
||||
columns: columns || ['name', 'address']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
var EXPECTED_NAMES = ['Hawai', 'El Estocolmo', 'El Rey del Tallarín', 'El Lacón', 'El Pico'];
|
||||
|
||||
it('can be fetched from a valid list', function(done) {
|
||||
var columns = ['name', 'address'];
|
||||
this.testClient = new TestClient(listsMapConfig(columns));
|
||||
this.testClient.getWidget('places', function (err, res, list) {
|
||||
assert.ok(!err, err);
|
||||
assert.ok(list);
|
||||
assert.equal(list.type, 'list');
|
||||
assert.equal(list.rows.length, 5);
|
||||
|
||||
assert.ok(onlyHasFields(list, columns));
|
||||
|
||||
var names = list.rows.map(function (item) {
|
||||
return item.name;
|
||||
});
|
||||
assert.deepEqual(names, EXPECTED_NAMES);
|
||||
|
||||
var expectedAddresses = [
|
||||
'Calle de Pérez Galdós 9, Madrid, Spain',
|
||||
'Calle de la Palma 72, Madrid, Spain',
|
||||
'Plaza Conde de Toreno 2, Madrid, Spain',
|
||||
'Manuel Fernández y González 8, Madrid, Spain',
|
||||
'Calle Divino Pastor 12, Madrid, Spain'
|
||||
];
|
||||
var addresses = list.rows.map(function (item) {
|
||||
return item.address;
|
||||
});
|
||||
assert.deepEqual(addresses, expectedAddresses);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch just one column', function(done) {
|
||||
var columns = ['name'];
|
||||
this.testClient = new TestClient(listsMapConfig(columns));
|
||||
this.testClient.getWidget('places', function (err, res, list) {
|
||||
assert.ok(!err, err);
|
||||
assert.ok(list);
|
||||
assert.equal(list.type, 'list');
|
||||
assert.equal(list.rows.length, 5);
|
||||
|
||||
assert.ok(onlyHasFields(list, columns));
|
||||
|
||||
var names = list.rows.map(function (item) {
|
||||
return item.name;
|
||||
});
|
||||
assert.deepEqual(names, EXPECTED_NAMES);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
function onlyHasFields(list, expectedFields) {
|
||||
var fields = (!!list.rows[0]) ? Object.keys(list.rows[0]) : [];
|
||||
|
||||
return _.difference(fields, expectedFields).length === 0 &&
|
||||
_.difference(expectedFields, fields).length === 0;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -218,114 +218,6 @@ describe('widgets-regressions', function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not count the polygons outside the bounding box', function(done) {
|
||||
|
||||
// $ % $ = not intersecting left triangle
|
||||
// $$ **VVVVV** %% % = not intersecting right triangle
|
||||
// $$$ *VVVVV* %%% * = intersecting triangle
|
||||
// $$$$ ***** %%%% V = bounding box
|
||||
// $$$$$ *** %%%%%
|
||||
// $$$$$$ * %%%%%%
|
||||
// $$$$$$$ %%%%%%%
|
||||
// $$$$$$$$ %%%%%%%%
|
||||
|
||||
const notIntersectingLeftTriangle = {
|
||||
type: "Polygon",
|
||||
coordinates:[[
|
||||
[-161.015625,69.28725695167886],
|
||||
[-162.7734375,-7.710991655433217],
|
||||
[-40.78125,-8.059229627200192],
|
||||
[-161.015625,69.28725695167886]
|
||||
]]
|
||||
};
|
||||
|
||||
const notIntersectingRightTriangle = {
|
||||
type: "Polygon",
|
||||
coordinates: [[
|
||||
[-29.179687499999996,-7.01366792756663],
|
||||
[103.71093749999999,-6.664607562172573],
|
||||
[105.46875,69.16255790810501],
|
||||
[-29.179687499999996,-7.01366792756663]
|
||||
]]
|
||||
};
|
||||
|
||||
const intersectingTriangle = {
|
||||
type: "Polygon",
|
||||
coordinates:[[
|
||||
[-117.42187500000001,68.13885164925573],
|
||||
[-35.859375,20.96143961409684],
|
||||
[59.4140625,68.52823492039876],
|
||||
[-117.42187500000001,68.13885164925573]
|
||||
]]
|
||||
};
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
ST_TRANSFORM(ST_SETSRID(ST_GeomFromGeoJSON(
|
||||
'${JSON.stringify(notIntersectingLeftTriangle)}'
|
||||
), 4326), 3857) AS the_geom_webmercator, 1 AS cartodb_id, 'notIntersectingLeftTriangle' AS name
|
||||
UNION
|
||||
SELECT
|
||||
ST_TRANSFORM(ST_SETSRID(ST_GeomFromGeoJSON(
|
||||
'${JSON.stringify(notIntersectingRightTriangle)}'
|
||||
), 4326), 3857), 2, 'notIntersectingRightTriangle'
|
||||
UNION
|
||||
SELECT
|
||||
ST_TRANSFORM(ST_SETSRID(ST_GeomFromGeoJSON(
|
||||
'${JSON.stringify(intersectingTriangle)}'
|
||||
), 4326), 3857), 3, 'intersectingTriangle'
|
||||
`;
|
||||
|
||||
const mapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"cartocss": "#points { marker-width: 10; marker-fill: red; }",
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
val_formula: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: "name",
|
||||
aggregation: "count",
|
||||
}
|
||||
}
|
||||
},
|
||||
analyses: [
|
||||
{
|
||||
"id": "a0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": query
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
const params = {
|
||||
bbox: '-77.34374999999999,45.82879925192134,17.578125,55.97379820507658'
|
||||
};
|
||||
this.testClient.getDataview('val_formula', params, function(err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.categories.length, 1);
|
||||
assert.equal(dataview.categories[0].category, 'intersectingTriangle');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[-53839,4629161]},"properties":{"name":"Alicante","cartodb_id":1200}},{"type":"Feature","geometry":{"type":"Point","coordinates":[242835,5069332]},"properties":{"name":"Barcelona","cartodb_id":5330}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-5567,4861644]},"properties":{"name":"Castello","cartodb_id":1201}},{"type":"Feature","geometry":{"type":"Point","coordinates":[272735,5092314]},"properties":{"name":"Mataro","cartodb_id":615}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-125787,4576600]},"properties":{"name":"Murcia","cartodb_id":952}},{"type":"Feature","geometry":{"type":"Point","coordinates":[295469,4804267]},"properties":{"name":"Palma","cartodb_id":5500}},{"type":"Feature","geometry":{"type":"Point","coordinates":[139148,5030112]},"properties":{"name":"Tarragona","cartodb_id":616}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-44746,4791667]},"properties":{"name":"Valencia","cartodb_id":5942}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-99072,5108695]},"properties":{"name":"Zaragoza","cartodb_id":5932}}]}
|
||||
@@ -1 +0,0 @@
|
||||
{"grid":[" "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," !! ","!!! !!!!! ","!!!!!!! ! ","!!! !!!!! "," !! ! "," "," "," "," "," "," "," "," ### # "," ####### ###"," ####### ## ","$ ## #### ## ","$$ ","$$ ","$$ "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "],"keys":["","9","2","1"],"data":{"1":{"cartodb_id":5942},"2":{"cartodb_id":5500},"9":{"cartodb_id":1201}}}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -1 +0,0 @@
|
||||
{"grid":[" "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," !!! ! "," !!!!!!! !!!"," !!!!!!! !! "," !! !!!! !! "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "],"keys":["","1"],"data":{"1":{"cartodb_id":5500}}}
|
||||
@@ -1 +0,0 @@
|
||||
{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[295469,4804267]},"properties":{"name":"Palma","cartodb_id":5500}}]}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 94 KiB |
@@ -126,25 +126,22 @@ assert.response = function(server, req, res, callback) {
|
||||
// Assert response body
|
||||
if (res.body) {
|
||||
var eql = res.body instanceof RegExp ? res.body.test(response.body) : res.body === response.body;
|
||||
if (!eql) {
|
||||
return callback(response, new Error(colorize(
|
||||
'[red]{Invalid response body.}\n' +
|
||||
assert.ok(
|
||||
eql,
|
||||
colorize('[red]{Invalid response body.}\n' +
|
||||
' Expected: [green]{' + res.body + '}\n' +
|
||||
' Got: [red]{' + response.body + '}'))
|
||||
);
|
||||
}
|
||||
' Got: [red]{' + response.body + '}')
|
||||
);
|
||||
}
|
||||
|
||||
// Assert response status
|
||||
if (typeof status === 'number') {
|
||||
if (response.statusCode != status) {
|
||||
return callback(response, new Error(colorize(
|
||||
'[red]{Invalid response status code.}\n' +
|
||||
assert.equal(response.statusCode, status,
|
||||
colorize('[red]{Invalid response status code.}\n' +
|
||||
' Expected: [green]{' + status + '}\n' +
|
||||
' Got: [red]{' + response.statusCode + '}\n' +
|
||||
' Body: ' + response.body))
|
||||
);
|
||||
}
|
||||
' Body: ' + response.body)
|
||||
);
|
||||
}
|
||||
|
||||
// Assert response headers
|
||||
@@ -155,13 +152,11 @@ assert.response = function(server, req, res, callback) {
|
||||
actual = response.headers[name.toLowerCase()],
|
||||
expected = res.headers[name],
|
||||
headerEql = expected instanceof RegExp ? expected.test(actual) : expected === actual;
|
||||
if (!headerEql) {
|
||||
return callback(response, new Error(colorize(
|
||||
'Invalid response header [bold]{' + name + '}.\n' +
|
||||
assert.ok(headerEql,
|
||||
colorize('Invalid response header [bold]{' + name + '}.\n' +
|
||||
' Expected: [green]{' + expected + '}\n' +
|
||||
' Got: [red]{' + actual + '}'))
|
||||
);
|
||||
}
|
||||
' Got: [red]{' + actual + '}')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ if test x"$PREPARE_PGSQL" = xyes; then
|
||||
createdb -Ttemplate_postgis -EUTF8 "${TEST_DB}" || die "Could not create test database"
|
||||
|
||||
LOCAL_SQL_SCRIPTS='analysis_catalog windshaft.test gadm4 ported/populated_places_simple_reduced cdb_analysis_check cdb_invalidate_varnish'
|
||||
REMOTE_SQL_SCRIPTS='CDB_QueryStatements CDB_QueryTables CDB_CartodbfyTable CDB_TableMetadata CDB_ForeignTable CDB_UserTables CDB_ColumnNames CDB_ZoomFromScale CDB_OverviewsSupport CDB_Overviews CDB_QuantileBins CDB_JenksBins CDB_HeadsTailsBins CDB_EqualIntervalBins CDB_Hexagon CDB_XYZ CDB_EstimateRowCount'
|
||||
REMOTE_SQL_SCRIPTS='CDB_QueryStatements CDB_QueryTables CDB_CartodbfyTable CDB_TableMetadata CDB_ForeignTable CDB_UserTables CDB_ColumnNames CDB_ZoomFromScale CDB_OverviewsSupport CDB_Overviews CDB_QuantileBins CDB_JenksBins CDB_HeadsTailsBins CDB_EqualIntervalBins CDB_Hexagon CDB_XYZ'
|
||||
|
||||
CURL_ARGS=""
|
||||
for i in ${REMOTE_SQL_SCRIPTS}
|
||||
|
||||
@@ -339,78 +339,6 @@ INSERT INTO _vovw_2_test_table_overviews VALUES
|
||||
INSERT INTO _vovw_1_test_table_overviews VALUES
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.314252', 1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', 3.0, '0101000020E610000000000000000020C00000000000004440', '0101000020110F000076491621312319C122D4663F1DCC5241', 5);
|
||||
|
||||
-- table with overviews whit special float values
|
||||
|
||||
CREATE TABLE test_special_float_values_table_overviews (
|
||||
cartodb_id integer NOT NULL,
|
||||
name character varying,
|
||||
address character varying,
|
||||
value float8,
|
||||
the_geom geometry,
|
||||
the_geom_webmercator geometry,
|
||||
_feature_count integer,
|
||||
CONSTRAINT enforce_dims_the_geom CHECK ((st_ndims(the_geom) = 2)),
|
||||
CONSTRAINT enforce_dims_the_geom_webmercator CHECK ((st_ndims(the_geom_webmercator) = 2)),
|
||||
CONSTRAINT enforce_geotype_the_geom CHECK (((geometrytype(the_geom) = 'POINT'::text) OR (the_geom IS NULL))),
|
||||
CONSTRAINT enforce_geotype_the_geom_webmercator CHECK (((geometrytype(the_geom_webmercator) = 'POINT'::text) OR (the_geom_webmercator IS NULL))),
|
||||
CONSTRAINT enforce_srid_the_geom CHECK ((st_srid(the_geom) = 4326)),
|
||||
CONSTRAINT enforce_srid_the_geom_webmercator CHECK ((st_srid(the_geom_webmercator) = 3857))
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE test_special_float_values_table_overviews TO :TESTUSER;
|
||||
GRANT SELECT ON TABLE test_special_float_values_table_overviews TO :PUBLICUSER;
|
||||
|
||||
CREATE SEQUENCE test_special_float_values_table_overviews_cartodb_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE test_special_float_values_table_overviews_cartodb_id_seq OWNED BY test_special_float_values_table_overviews.cartodb_id;
|
||||
|
||||
SELECT pg_catalog.setval('test_special_float_values_table_overviews_cartodb_id_seq', 60, true);
|
||||
|
||||
ALTER TABLE test_special_float_values_table_overviews ALTER COLUMN cartodb_id SET DEFAULT nextval('test_special_float_values_table_overviews_cartodb_id_seq'::regclass);
|
||||
|
||||
INSERT INTO test_special_float_values_table_overviews VALUES
|
||||
(1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', 1.0, '0101000020E6100000A6B73F170D990DC064E8D84125364440', '0101000020110F000076491621312319C122D4663F1DCC5241', 1),
|
||||
(2, 'El Estocolmo', 'Calle de la Palma 72, Madrid, Spain', 2.0, '0101000020E6100000C90567F0F7AB0DC0AB07CC43A6364440', '0101000020110F0000C4356B29423319C15DD1092DADCC5241', 1),
|
||||
(3, 'El Rey del Tallarín', 'Plaza Conde de Toreno 2, Madrid, Spain', 'NaN'::float, '0101000020E610000021C8410933AD0DC0CB0EF10F5B364440', '0101000020110F000053E71AC64D3419C10F664E4659CC5241', 1),
|
||||
(4, 'El Lacón', 'Manuel Fernández y González 8, Madrid, Spain', 4.0, '0101000020E6100000BC5983F755990DC07D923B6C22354440', '0101000020110F00005DACDB056F2319C1EC41A980FCCA5241', 1),
|
||||
(5, 'El Pico', 'Calle Divino Pastor 12, Madrid, Spain', 'infinity'::float, '0101000020E61000003B6D8D08C6A10DC0371B2B31CF364440', '0101000020110F00005F716E91992A19C17DAAA4D6DACC5241', 1);
|
||||
|
||||
ALTER TABLE ONLY test_special_float_values_table_overviews ADD CONSTRAINT test_special_float_values_table_overviews_pkey PRIMARY KEY (cartodb_id);
|
||||
|
||||
CREATE INDEX test_special_float_values_table_overviews_the_geom_idx ON test_special_float_values_table_overviews USING gist (the_geom);
|
||||
CREATE INDEX test_special_float_values_table_overviews_the_geom_webmercator_idx ON test_special_float_values_table_overviews USING gist (the_geom_webmercator);
|
||||
|
||||
GRANT ALL ON TABLE test_special_float_values_table_overviews TO :TESTUSER;
|
||||
GRANT SELECT ON TABLE test_special_float_values_table_overviews TO :PUBLICUSER;
|
||||
|
||||
CREATE TABLE _vovw_1_test_special_float_values_table_overviews (
|
||||
cartodb_id integer NOT NULL,
|
||||
name character varying,
|
||||
address character varying,
|
||||
value float8,
|
||||
the_geom geometry,
|
||||
the_geom_webmercator geometry,
|
||||
_feature_count integer,
|
||||
CONSTRAINT enforce_dims_the_geom CHECK ((st_ndims(the_geom) = 2)),
|
||||
CONSTRAINT enforce_dims_the_geom_webmercator CHECK ((st_ndims(the_geom_webmercator) = 2)),
|
||||
CONSTRAINT enforce_geotype_the_geom CHECK (((geometrytype(the_geom) = 'POINT'::text) OR (the_geom IS NULL))),
|
||||
CONSTRAINT enforce_geotype_the_geom_webmercator CHECK (((geometrytype(the_geom_webmercator) = 'POINT'::text) OR (the_geom_webmercator IS NULL))),
|
||||
CONSTRAINT enforce_srid_the_geom CHECK ((st_srid(the_geom) = 4326)),
|
||||
CONSTRAINT enforce_srid_the_geom_webmercator CHECK ((st_srid(the_geom_webmercator) = 3857))
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE _vovw_1_test_special_float_values_table_overviews TO :TESTUSER;
|
||||
GRANT SELECT ON TABLE _vovw_1_test_special_float_values_table_overviews TO :PUBLICUSER;
|
||||
|
||||
INSERT INTO _vovw_1_test_special_float_values_table_overviews VALUES
|
||||
(1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', 3, '0101000020E610000000000000000020C00000000000004440', '0101000020110F000076491621312319C122D4663F1DCC5241', 2),
|
||||
(3, 'El Rey del Tallarín', 'Plaza Conde de Toreno 2, Madrid, Spain', 'NaN'::float, '0101000020E610000021C8410933AD0DC0CB0EF10F5B364440', '0101000020110F000053E71AC64D3419C10F664E4659CC5241', 1),
|
||||
(4, 'El Lacón', 'Manuel Fernández y González 8, Madrid, Spain', 'infinity'::float, '0101000020E6100000BC5983F755990DC07D923B6C22354440', '0101000020110F00005DACDB056F2319C1EC41A980FCCA5241', 2);
|
||||
|
||||
-- analysis tables -----------------------------------------------
|
||||
|
||||
@@ -721,5 +649,3 @@ CREATE OR REPLACE FUNCTION cdb_crankshaft.CDB_KMeans(query text, no_clusters int
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
GRANT ALL ON FUNCTION cdb_crankshaft.CDB_KMeans(text, integer, integer) TO :TESTUSER;
|
||||
|
||||
ANALYZE;
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
var qs = require('querystring');
|
||||
var step = require('step');
|
||||
var urlParser = require('url');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var _ = require('underscore');
|
||||
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
|
||||
var LayergroupToken = require('./layergroup-token');
|
||||
@@ -15,33 +14,16 @@ var helper = require('./test_helper');
|
||||
var CartodbWindshaft = require('../../lib/cartodb/server');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
serverOptions.analysis.batch.inlineExecution = true;
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
const MAPNIK_SUPPORTED_FORMATS = {
|
||||
'png': true,
|
||||
'png32': true,
|
||||
'grid.json': true,
|
||||
'geojson': true,
|
||||
'mvt': true
|
||||
}
|
||||
|
||||
function TestClient(config, apiKey) {
|
||||
this.mapConfig = isMapConfig(config) ? config : null;
|
||||
this.template = isTemplate(config) ? config : null;
|
||||
function TestClient(mapConfig, apiKey) {
|
||||
this.mapConfig = mapConfig;
|
||||
this.apiKey = apiKey;
|
||||
this.keysToDelete = {};
|
||||
this.server = new CartodbWindshaft(serverOptions);
|
||||
}
|
||||
|
||||
module.exports = TestClient;
|
||||
|
||||
function isMapConfig(config) {
|
||||
return config && config.layers;
|
||||
}
|
||||
|
||||
function isTemplate(config) {
|
||||
return config && config.layergroup;
|
||||
}
|
||||
|
||||
module.exports.RESPONSE = {
|
||||
ERROR: {
|
||||
status: 400,
|
||||
@@ -81,42 +63,9 @@ module.exports.CARTOCSS = {
|
||||
' line-width: 0.5;',
|
||||
' line-opacity: 1;',
|
||||
'}'
|
||||
].join('\n'),
|
||||
|
||||
TORQUE: [
|
||||
'Map {',
|
||||
' -torque-frame-count: 256;',
|
||||
' -torque-animation-duration: 30;',
|
||||
' -torque-time-attribute: "cartodb_id";',
|
||||
' -torque-aggregation-function: "count(1)";',
|
||||
' -torque-resolution: 4;',
|
||||
' -torque-data-aggregation: linear;',
|
||||
'}',
|
||||
'#layer {',
|
||||
' marker-width: 7;',
|
||||
' marker-fill: #FFB927;',
|
||||
' marker-fill-opacity: 0.9;',
|
||||
' marker-line-width: 1;',
|
||||
' marker-line-color: #FFF;',
|
||||
' marker-line-opacity: 1;',
|
||||
' comp-op: lighter;',
|
||||
'}',
|
||||
'#layer[frame-offset=1] {',
|
||||
' marker-width: 9;',
|
||||
' marker-fill-opacity: 0.45;',
|
||||
'}',
|
||||
'#layer[frame-offset=2] {',
|
||||
' marker-width: 11;',
|
||||
' marker-fill-opacity: 0.225;',
|
||||
'}'
|
||||
].join('\n')
|
||||
};
|
||||
|
||||
module.exports.SQL = {
|
||||
EMPTY: 'select 1 as cartodb_id, null::geometry as the_geom_webmercator',
|
||||
ONE_POINT: 'select 1 as cartodb_id, \'SRID=3857;POINT(0 0)\'::geometry the_geom_webmercator'
|
||||
}
|
||||
|
||||
TestClient.prototype.getWidget = function(widgetName, params, callback) {
|
||||
var self = this;
|
||||
|
||||
@@ -134,7 +83,7 @@ TestClient.prototype.getWidget = function(widgetName, params, callback) {
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(self.server,
|
||||
assert.response(server,
|
||||
{
|
||||
url: url,
|
||||
method: 'POST',
|
||||
@@ -193,7 +142,7 @@ TestClient.prototype.getWidget = function(widgetName, params, callback) {
|
||||
|
||||
url = '/api/v1/map/' + layergroupId + '/0/widget/' + widgetName + '?' + qs.stringify(urlParams);
|
||||
|
||||
assert.response(self.server,
|
||||
assert.response(server,
|
||||
{
|
||||
url: url,
|
||||
method: 'GET',
|
||||
@@ -245,7 +194,7 @@ TestClient.prototype.widgetSearch = function(widgetName, userQuery, params, call
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(self.server,
|
||||
assert.response(server,
|
||||
{
|
||||
url: url,
|
||||
method: 'POST',
|
||||
@@ -302,7 +251,7 @@ TestClient.prototype.widgetSearch = function(widgetName, userQuery, params, call
|
||||
}
|
||||
url = '/api/v1/map/' + layergroupId + '/0/widget/' + widgetName + '/search?' + qs.stringify(urlParams);
|
||||
|
||||
assert.response(self.server,
|
||||
assert.response(server,
|
||||
{
|
||||
url: url,
|
||||
method: 'GET',
|
||||
@@ -369,7 +318,7 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(self.server,
|
||||
assert.response(server,
|
||||
{
|
||||
url: url,
|
||||
method: 'POST',
|
||||
@@ -411,7 +360,7 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
|
||||
own_filter: params.hasOwnProperty('own_filter') ? params.own_filter : 1
|
||||
};
|
||||
|
||||
['bbox', 'bins', 'start', 'end', 'aggregation', 'offset'].forEach(function(extraParam) {
|
||||
['bbox', 'bins', 'start', 'end'].forEach(function(extraParam) {
|
||||
if (params.hasOwnProperty(extraParam)) {
|
||||
urlParams[extraParam] = params[extraParam];
|
||||
}
|
||||
@@ -422,7 +371,7 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
|
||||
}
|
||||
url = '/api/v1/map/' + layergroupId + '/dataview/' + dataviewName + '?' + qs.stringify(urlParams);
|
||||
|
||||
assert.response(self.server,
|
||||
assert.response(server,
|
||||
{
|
||||
url: url,
|
||||
method: 'GET',
|
||||
@@ -441,115 +390,9 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
|
||||
);
|
||||
},
|
||||
function finish(err, dataview) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (layergroupId) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
}
|
||||
|
||||
return callback(null, dataview);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
TestClient.prototype.getFeatureAttributes = function(featureId, layerId, params, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!callback) {
|
||||
callback = params;
|
||||
params = {};
|
||||
}
|
||||
|
||||
var extraParams = {};
|
||||
if (this.apiKey) {
|
||||
extraParams.api_key = this.apiKey;
|
||||
}
|
||||
if (params && params.filters) {
|
||||
extraParams.filters = JSON.stringify(params.filters);
|
||||
}
|
||||
|
||||
var url = '/api/v1/map';
|
||||
if (Object.keys(extraParams).length > 0) {
|
||||
url += '?' + qs.stringify(extraParams);
|
||||
}
|
||||
|
||||
var expectedResponse = params.response || {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
var layergroupId;
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: url,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(self.mapConfig)
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
|
||||
if (parsedBody.layergroupid) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
}
|
||||
|
||||
return next(null, parsedBody.layergroupid);
|
||||
}
|
||||
);
|
||||
},
|
||||
function getFeatureAttributes(err, layergroupId) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
|
||||
url = '/api/v1/map/' + layergroupId + '/' + layerId + '/attributes/' + featureId;
|
||||
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: url,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
},
|
||||
expectedResponse,
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
next(null, JSON.parse(res.body));
|
||||
}
|
||||
);
|
||||
},
|
||||
function finish(err, attributes) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, attributes);
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
return callback(err, dataview);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -563,77 +406,24 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
}
|
||||
|
||||
var url = '/api/v1/map';
|
||||
var urlNamed = url + '/named';
|
||||
|
||||
if (this.apiKey) {
|
||||
url += '?' + qs.stringify({api_key: this.apiKey});
|
||||
}
|
||||
|
||||
var layergroupId;
|
||||
|
||||
if (params.layergroupid) {
|
||||
layergroupId = params.layergroupid
|
||||
}
|
||||
|
||||
step(
|
||||
function createTemplate () {
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
|
||||
if (!self.template) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!self.apiKey) {
|
||||
return next(new Error('apiKey param is mandatory to create a new template'));
|
||||
}
|
||||
|
||||
params.placeholders = params.placeholders || {};
|
||||
|
||||
assert.response(self.server,
|
||||
assert.response(server,
|
||||
{
|
||||
url: urlNamed + '?' + qs.stringify({ api_key: self.apiKey }),
|
||||
url: url,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(self.template)
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
},
|
||||
function (res, err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
return next(null, JSON.parse(res.body).template_id);
|
||||
}
|
||||
);
|
||||
},
|
||||
function createLayergroup(err, templateId) {
|
||||
var next = this;
|
||||
|
||||
if (layergroupId) {
|
||||
return next(null, layergroupId);
|
||||
}
|
||||
|
||||
var data = templateId ? params.placeholders : self.mapConfig
|
||||
var path = templateId ?
|
||||
urlNamed + '/' + templateId + '?' + qs.stringify({api_key: self.apiKey}) :
|
||||
url;
|
||||
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: path,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(data)
|
||||
data: JSON.stringify(self.mapConfig)
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
@@ -666,10 +456,6 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
|
||||
var format = params.format || 'png';
|
||||
|
||||
if (layers === undefined && !MAPNIK_SUPPORTED_FORMATS[format]) {
|
||||
throw new Error(`Missing layer filter while fetching ${format} tile, review params argument`);
|
||||
}
|
||||
|
||||
url += [z,x,y].join('/');
|
||||
url += '.' + format;
|
||||
|
||||
@@ -685,76 +471,37 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
}
|
||||
};
|
||||
|
||||
var expectedResponse = Object.assign({}, {
|
||||
var expectedResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}, params.response);
|
||||
|
||||
};
|
||||
|
||||
var isPng = format.match(/png$/);
|
||||
|
||||
if (isPng) {
|
||||
request.encoding = 'binary';
|
||||
expectedResponse.headers['Content-Type'] = 'image/png';
|
||||
}
|
||||
|
||||
var isMvt = format.match(/mvt$/);
|
||||
|
||||
if (isMvt) {
|
||||
request.encoding = 'binary';
|
||||
if (expectedResponse.status === 200) {
|
||||
expectedResponse.headers['Content-Type'] = 'application/x-protobuf';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var isGeojson = format.match(/geojson$/);
|
||||
|
||||
if (isGeojson) {
|
||||
request.encoding = 'utf-8';
|
||||
expectedResponse.headers['Content-Type'] = 'application/json; charset=utf-8';
|
||||
}
|
||||
|
||||
var isGridJSON = format.match(/grid.json$/);
|
||||
|
||||
if (isGridJSON) {
|
||||
request.encoding = 'utf-8';
|
||||
expectedResponse.headers['Content-Type'] = 'application/json; charset=utf-8';
|
||||
}
|
||||
|
||||
if (params.contentType) {
|
||||
expectedResponse.headers['Content-Type'] = 'application/json; charset=utf-8';
|
||||
}
|
||||
|
||||
assert.response(self.server, request, expectedResponse, function(res, err) {
|
||||
assert.response(server, request, expectedResponse, function(res, err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var body;
|
||||
switch (res.headers['content-type']) {
|
||||
case 'image/png':
|
||||
body = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
break;
|
||||
case 'application/x-protobuf':
|
||||
body = new mapnik.VectorTile(z, x, y);
|
||||
body.setDataSync(new Buffer(res.body, 'binary'));
|
||||
break;
|
||||
case 'application/json; charset=utf-8':
|
||||
body = JSON.parse(res.body);
|
||||
break;
|
||||
default:
|
||||
body = res.body
|
||||
break;
|
||||
var obj;
|
||||
|
||||
if (isPng) {
|
||||
obj = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
} else {
|
||||
obj = JSON.parse(res.body);
|
||||
}
|
||||
|
||||
next(null, res, body);
|
||||
next(null, res, obj);
|
||||
});
|
||||
},
|
||||
function finish(err, res, image) {
|
||||
if (layergroupId) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
}
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
return callback(err, res, image);
|
||||
}
|
||||
);
|
||||
@@ -779,7 +526,7 @@ TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
|
||||
url += '?' + qs.stringify({api_key: this.apiKey});
|
||||
}
|
||||
|
||||
assert.response(self.server,
|
||||
assert.response(server,
|
||||
{
|
||||
url: url,
|
||||
method: 'POST',
|
||||
@@ -791,124 +538,18 @@ TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
|
||||
},
|
||||
expectedResponse,
|
||||
function(res, err) {
|
||||
// If there is a response, we are still interested in catching the created keys
|
||||
// to be able to delete them on the .drain() method.
|
||||
if (res) {
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
if (parsedBody.layergroupid) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
}
|
||||
}
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, parsedBody);
|
||||
}
|
||||
);
|
||||
};
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
|
||||
TestClient.prototype.getStaticCenter = function (params, callback) {
|
||||
var self = this;
|
||||
|
||||
let { layergroupid, z, lat, lng, width, height, format } = params
|
||||
|
||||
var url = `/api/v1/map/`;
|
||||
|
||||
if (this.apiKey) {
|
||||
url += '?' + qs.stringify({api_key: this.apiKey});
|
||||
}
|
||||
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
|
||||
if (layergroupid) {
|
||||
return next(null, layergroupid);
|
||||
}
|
||||
|
||||
var data = self.mapConfig
|
||||
var path = url;
|
||||
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: path,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(data)
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
return next(null, JSON.parse(res.body).layergroupid);
|
||||
}
|
||||
);
|
||||
},
|
||||
function getStaticResult(err, _layergroupid) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
|
||||
layergroupid = _layergroupid;
|
||||
|
||||
url = `/api/v1/map/static/center/${layergroupid}/${z}/${lat}/${lng}/${width}/${height}.${format}`
|
||||
|
||||
if (self.apiKey) {
|
||||
url += '?' + qs.stringify({api_key: self.apiKey});
|
||||
}
|
||||
|
||||
var request = {
|
||||
url: url,
|
||||
encoding: 'binary',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
};
|
||||
|
||||
var expectedResponse = Object.assign({}, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
}, params.response);
|
||||
|
||||
assert.response(self.server, request, expectedResponse, function(res, err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var body;
|
||||
switch (res.headers['content-type']) {
|
||||
case 'image/png':
|
||||
body = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
break;
|
||||
case 'application/json; charset=utf-8':
|
||||
body = JSON.parse(res.body);
|
||||
break;
|
||||
default:
|
||||
body = res.body
|
||||
break;
|
||||
}
|
||||
|
||||
next(null, res, body);
|
||||
});
|
||||
},
|
||||
function finish(err, res, image) {
|
||||
if (layergroupid) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupid).token] = 0;
|
||||
if (parsedBody.layergroupid) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
}
|
||||
return callback(err, res, image);
|
||||
|
||||
return callback(null, parsedBody);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -927,7 +568,7 @@ TestClient.prototype.getNodeStatus = function(nodeName, callback) {
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(self.server,
|
||||
assert.response(server,
|
||||
{
|
||||
url: url,
|
||||
method: 'POST',
|
||||
@@ -988,7 +629,7 @@ TestClient.prototype.getNodeStatus = function(nodeName, callback) {
|
||||
}
|
||||
};
|
||||
|
||||
assert.response(self.server, request, expectedResponse, function(res, err) {
|
||||
assert.response(server, request, expectedResponse, function(res, err) {
|
||||
assert.ifError(err);
|
||||
next(null, res, JSON.parse(res.body));
|
||||
});
|
||||
@@ -1001,119 +642,11 @@ TestClient.prototype.getNodeStatus = function(nodeName, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
TestClient.prototype.getAttributes = function(params, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!Number.isFinite(params.featureId)) {
|
||||
throw new Error('featureId param must be a number')
|
||||
}
|
||||
|
||||
if (!Number.isFinite(params.layer)) {
|
||||
throw new Error('layer param must be a number')
|
||||
}
|
||||
|
||||
var url = '/api/v1/map';
|
||||
|
||||
if (this.apiKey) {
|
||||
url += '?' + qs.stringify({ api_key: this.apiKey });
|
||||
}
|
||||
|
||||
var layergroupid;
|
||||
|
||||
if (params.layergroupid) {
|
||||
layergroupid = params.layergroupid
|
||||
}
|
||||
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
|
||||
if (layergroupid) {
|
||||
return next(null, layergroupid);
|
||||
}
|
||||
|
||||
assert.response(self.server,
|
||||
{
|
||||
url: url,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(self.mapConfig)
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
|
||||
return next(null, parsedBody.layergroupid);
|
||||
}
|
||||
);
|
||||
},
|
||||
function getAttributes(err, _layergroupid) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
|
||||
layergroupid = _layergroupid;
|
||||
|
||||
url = `/api/v1/map/${layergroupid}/${params.layer}/attributes/${params.featureId}`;
|
||||
|
||||
if (self.apiKey) {
|
||||
url += '?' + qs.stringify({api_key: self.apiKey});
|
||||
}
|
||||
|
||||
var request = {
|
||||
url: url,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
};
|
||||
|
||||
var expectedResponse = params.response || {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
assert.response(self.server, request, expectedResponse, function (res, err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var attributes = JSON.parse(res.body);
|
||||
|
||||
next(null, res, attributes);
|
||||
});
|
||||
},
|
||||
function finish(err, res, attributes) {
|
||||
if (layergroupid) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupid).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
}
|
||||
|
||||
return callback(err, res, attributes);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
TestClient.prototype.drain = function(callback) {
|
||||
helper.deleteRedisKeys(this.keysToDelete, callback);
|
||||
};
|
||||
|
||||
module.exports.getStaticMap = function getStaticMap(templateName, params, callback) {
|
||||
var self = this;
|
||||
|
||||
self.server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
if (!callback) {
|
||||
callback = params;
|
||||
params = null;
|
||||
@@ -1144,56 +677,9 @@ module.exports.getStaticMap = function getStaticMap(templateName, params, callba
|
||||
// this could be removed once named maps are invalidated, otherwise you hits the cache
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
assert.response(self.server, requestOptions, expectedResponse, function (res, err) {
|
||||
assert.response(server, requestOptions, expectedResponse, function (res, err) {
|
||||
helper.deleteRedisKeys({'user:localhost:mapviews:global': 5}, function() {
|
||||
return callback(err, mapnik.Image.fromBytes(new Buffer(res.body, 'binary')));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
TestClient.prototype.setUserRenderTimeoutLimit = function (user, userTimeoutLimit, callback) {
|
||||
const userTimeoutLimitsKey = `limits:timeout:${user}`;
|
||||
const params = [
|
||||
userTimeoutLimitsKey,
|
||||
'render', userTimeoutLimit,
|
||||
'render_public', userTimeoutLimit
|
||||
];
|
||||
|
||||
this.keysToDelete[userTimeoutLimitsKey] = 5;
|
||||
|
||||
helper.configureMetadata('hmset', params, callback);
|
||||
};
|
||||
|
||||
TestClient.prototype.setUserDatabaseTimeoutLimit = function (timeoutLimit, callback) {
|
||||
const dbname = _.template(global.environment.postgres_auth_user, { user_id: 1 }) + '_db';
|
||||
const dbuser = _.template(global.environment.postgres_auth_user, { user_id: 1 })
|
||||
const pass = _.template(global.environment.postgres_auth_pass, { user_id: 1 })
|
||||
const publicuser = global.environment.postgres.user;
|
||||
|
||||
// we need to guarantee all new connections have the new settings
|
||||
helper.cleanPGPoolConnections()
|
||||
|
||||
const psql = new PSQL({
|
||||
user: 'postgres',
|
||||
dbname: dbname,
|
||||
host: global.environment.postgres.host,
|
||||
port: global.environment.postgres.port
|
||||
});
|
||||
|
||||
step(
|
||||
function configureTimeouts () {
|
||||
const timeoutSQLs = [
|
||||
`ALTER ROLE "${publicuser}" SET STATEMENT_TIMEOUT TO ${timeoutLimit}`,
|
||||
`ALTER ROLE "${dbuser}" SET STATEMENT_TIMEOUT TO ${timeoutLimit}`,
|
||||
`ALTER DATABASE "${dbname}" SET STATEMENT_TIMEOUT TO ${timeoutLimit}`
|
||||
];
|
||||
|
||||
const group = this.group();
|
||||
|
||||
timeoutSQLs.forEach(sql => psql.query(sql, group()));
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ var lzmaWorker = new LZMA();
|
||||
var redis = require('redis');
|
||||
var nock = require('nock');
|
||||
var log4js = require('log4js');
|
||||
var pg = require('pg');
|
||||
|
||||
// set environment specific variables
|
||||
global.environment = require(__dirname + '/../../config/environments/test');
|
||||
@@ -128,11 +127,6 @@ afterEach(function(done) {
|
||||
});
|
||||
});
|
||||
|
||||
function cleanPGPoolConnections () {
|
||||
// TODO: this method will be replaced by psql.end
|
||||
pg.end();
|
||||
}
|
||||
|
||||
function deleteRedisKeys(keysToDelete, callback) {
|
||||
|
||||
if (Object.keys(keysToDelete).length === 0) {
|
||||
@@ -172,30 +166,12 @@ function rmdirRecursiveSync(dirname) {
|
||||
}
|
||||
}
|
||||
|
||||
function configureMetadata(action, params, callback) {
|
||||
redisClient.SELECT(5, function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
redisClient[action](params, function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
deleteRedisKeys: deleteRedisKeys,
|
||||
lzma_compress_to_base64: lzma_compress_to_base64,
|
||||
checkNoCache: checkNoCache,
|
||||
checkSurrogateKey: checkSurrogateKey,
|
||||
checkCache: checkCache,
|
||||
rmdirRecursiveSync: rmdirRecursiveSync,
|
||||
configureMetadata,
|
||||
cleanPGPoolConnections
|
||||
rmdirRecursiveSync: rmdirRecursiveSync
|
||||
};
|
||||
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
var assert = require('assert');
|
||||
var MapnikLayerStats = require('../../../../../lib/cartodb/backends/layer-stats/mapnik-layer-stats');
|
||||
var MapConfig = require('windshaft').model.MapConfig;
|
||||
|
||||
function getDbConnectionMock () {
|
||||
return {
|
||||
query: function(sql, callback) {
|
||||
return callback(null, {
|
||||
rows: [{rows: 1}]
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('mapnik-layer-stats', function() {
|
||||
|
||||
beforeEach(function () {
|
||||
this.dbConnectionMock = getDbConnectionMock();
|
||||
this.rendererCacheMock = {};
|
||||
this.params = {};
|
||||
});
|
||||
|
||||
var testMapConfigOneLayer = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var testMapConfigTwoLayers = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
var testMapConfigOneLayerTwoTables = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.3.0',
|
||||
affected_tables: ['test_table_1', 'test_table_2']
|
||||
}
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
var testMapConfigTwoLayerTwoTables = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.3.0',
|
||||
affected_tables: ['test_table_1', 'test_table_2']
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.3.0',
|
||||
affected_tables: ['test_table_3', 'test_table_4']
|
||||
}
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
it('should return 1 feature for one layer', function(done) {
|
||||
var mapConfig = MapConfig.create(testMapConfigOneLayer);
|
||||
var layer = mapConfig.getLayer(0);
|
||||
var testSubject = new MapnikLayerStats();
|
||||
testSubject.getStats(layer, this.dbConnectionMock, function (err, result) {
|
||||
assert.ifError(err);
|
||||
assert.equal(result.estimatedFeatureCount, 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 1 feature for two layers', function(done) {
|
||||
var self = this;
|
||||
var mapConfig = MapConfig.create(testMapConfigTwoLayers);
|
||||
var layer0 = mapConfig.getLayer(0);
|
||||
var layer1 = mapConfig.getLayer(1);
|
||||
var testSubject = new MapnikLayerStats();
|
||||
testSubject.getStats(layer0, self.dbConnectionMock, function (err, result) {
|
||||
assert.ifError(err);
|
||||
assert.equal(result.estimatedFeatureCount, 1);
|
||||
testSubject.getStats(layer1, self.dbConnectionMock, function (err, result) {
|
||||
assert.ifError(err);
|
||||
assert.equal(result.estimatedFeatureCount, 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 1 feature for one layers with two tables', function(done) {
|
||||
var mapConfig = MapConfig.create(testMapConfigOneLayerTwoTables);
|
||||
var layer = mapConfig.getLayer(0);
|
||||
var testSubject = new MapnikLayerStats();
|
||||
testSubject.getStats(layer, this.dbConnectionMock, function (err, result) {
|
||||
assert.ifError(err);
|
||||
assert.equal(result.estimatedFeatureCount, 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 1 feature for two layers and two tables', function(done) {
|
||||
var self = this;
|
||||
var mapConfig = MapConfig.create(testMapConfigTwoLayerTwoTables);
|
||||
var layer0 = mapConfig.getLayer(0);
|
||||
var layer1 = mapConfig.getLayer(1);
|
||||
var testSubject = new MapnikLayerStats();
|
||||
testSubject.getStats(layer0, self.dbConnectionMock, function (err, result) {
|
||||
assert.ifError(err);
|
||||
assert.equal(result.estimatedFeatureCount, 1);
|
||||
testSubject.getStats(layer1, self.dbConnectionMock, function (err, result) {
|
||||
assert.ifError(err);
|
||||
assert.equal(result.estimatedFeatureCount, 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
var assert = require('assert');
|
||||
var TorqueLayerStats = require('../../../../../lib/cartodb/backends/layer-stats/torque-layer-stats');
|
||||
var MapConfig = require('windshaft').model.MapConfig;
|
||||
|
||||
describe('torque-layer-stats', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
this.params = {};
|
||||
});
|
||||
|
||||
var testMapConfigOneLayer = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'torque',
|
||||
options: {
|
||||
sql: 'select * from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.3.0',
|
||||
}
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
it('should return torque stats for one layer', function(done) {
|
||||
var mapConfig = MapConfig.create(testMapConfigOneLayer);
|
||||
var layerId = 0;
|
||||
var layer = mapConfig.getLayer(layerId);
|
||||
var testSubject = new TorqueLayerStats();
|
||||
testSubject.getStats(layer, {}, function (err, result) {
|
||||
assert.ifError(err);
|
||||
assert.deepEqual({}, result);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
var PostgresDatasource = require('../../../../lib/cartodb/backends/turbo-carto-postgres-datasource');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var _ = require('underscore');
|
||||
var assert = require('assert');
|
||||
|
||||
describe('turbo-carto-postgres-datasource', function() {
|
||||
|
||||
beforeEach(function () {
|
||||
const dbname = _.template(global.environment.postgres_auth_user, { user_id: 1 }) + '_db';
|
||||
const psql = new PSQL({
|
||||
user: 'postgres',
|
||||
dbname: dbname,
|
||||
host: global.environment.postgres.host,
|
||||
port: global.environment.postgres.port
|
||||
});
|
||||
const sql = [
|
||||
'SELECT',
|
||||
' null::geometry the_geom_webmercator,',
|
||||
' CASE',
|
||||
' WHEN x % 4 = 0 THEN \'infinity\'::float',
|
||||
' WHEN x % 4 = 1 THEN \'-infinity\'::float',
|
||||
' WHEN x % 4 = 2 THEN \'NaN\'::float',
|
||||
' ELSE x',
|
||||
' END AS values',
|
||||
'FROM generate_series(1, 1000) x'
|
||||
].join('\n');
|
||||
this.datasource = new PostgresDatasource(psql, sql);
|
||||
});
|
||||
|
||||
it('should ignore NaNs and Infinities when computing ramps', function(done) {
|
||||
var column = 'values';
|
||||
var buckets = 4;
|
||||
var method = 'equal';
|
||||
this.datasource.getRamp(column, buckets, method, function(err, result) {
|
||||
var expected_result = {
|
||||
ramp: [ 252, 501, 750, 999 ],
|
||||
stats: { min_val: 3, max_val: 999, avg_val: 501 },
|
||||
strategy: undefined
|
||||
};
|
||||
assert.deepEqual(result, expected_result);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
var assert = require('assert');
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
var lzmaMiddleware = require('../../../lib/cartodb/middleware/lzma');
|
||||
|
||||
describe('lzma-middleware', function() {
|
||||
|
||||
it('it should extend params with decoded lzma', function(done) {
|
||||
var qo = {
|
||||
config: {
|
||||
version: '1.3.0'
|
||||
}
|
||||
};
|
||||
testHelper.lzma_compress_to_base64(JSON.stringify(qo), 1, function(err, data) {
|
||||
var req = {
|
||||
headers: {
|
||||
host:'localhost'
|
||||
},
|
||||
query: {
|
||||
api_key: 'test',
|
||||
lzma: data
|
||||
}
|
||||
};
|
||||
lzmaMiddleware(req, {}, function(err) {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
var query = req.query;
|
||||
assert.deepEqual(qo.config, query.config);
|
||||
assert.equal('test', query.api_key);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -143,6 +143,79 @@ describe('dataviews-widgets-adapter', function() {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"version": "1.4.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select * from test_table",
|
||||
"cartocss": "#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }",
|
||||
"cartocss_version": "2.3.0",
|
||||
"widgets": {
|
||||
"names": {
|
||||
"type": "list",
|
||||
"options": {
|
||||
"columns": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"version": "1.4.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "cdb-layer-source-0"
|
||||
},
|
||||
"cartocss": "#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }",
|
||||
"cartocss_version": "2.3.0",
|
||||
// keep them for now
|
||||
"widgets": {
|
||||
"names": {
|
||||
"type": "list",
|
||||
"options": {
|
||||
"columns": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"analyses": [
|
||||
{
|
||||
"id": "cdb-layer-source-0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from test_table"
|
||||
}
|
||||
}
|
||||
],
|
||||
"dataviews": {
|
||||
"names": {
|
||||
"source": {
|
||||
"id": "cdb-layer-source-0"
|
||||
},
|
||||
"type": "list",
|
||||
"options": {
|
||||
"columns": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"input": {
|
||||
"version": "1.4.0",
|
||||
|
||||
@@ -111,23 +111,6 @@ describe('Bounding box filter', function() {
|
||||
createRef([-180, -45, 0, 45])
|
||||
);
|
||||
});
|
||||
|
||||
it('generating multiple bbox', function() {
|
||||
var bbox = [90, -45, 190, 45];
|
||||
var bboxFilter = createFilter(bbox);
|
||||
|
||||
assert.equal(bboxFilter.bboxes.length, 2);
|
||||
|
||||
assert.deepEqual(
|
||||
bboxFilter.bboxes[0],
|
||||
createRef([90, -45, 180, 45])
|
||||
);
|
||||
assert.deepEqual(
|
||||
bboxFilter.bboxes[1],
|
||||
createRef([-180, -45, -170, 45])
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('out of bounds', function() {
|
||||
|
||||
@@ -6,13 +6,10 @@ var LayergroupController = require('../../../../lib/cartodb/controllers/layergro
|
||||
|
||||
describe('tile stats', function() {
|
||||
|
||||
beforeEach(function () {
|
||||
this.statsClient = global.statsClient;
|
||||
after(function() {
|
||||
global.statsClient = null;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
global.statsClient = this.statsClient;
|
||||
});
|
||||
|
||||
it('finalizeGetTileOrGrid does not call statsClient when format is not supported', function() {
|
||||
var expectedCalls = 2, // it will call increment once for the general error
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var assert = require('assert');
|
||||
var _ = require('underscore');
|
||||
require('../../support/test_helper');
|
||||
var test_helper = require('../../support/test_helper');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
var cartodbRedis = require('cartodb-redis');
|
||||
@@ -98,31 +98,34 @@ describe('req2params', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('it should remove invalid params', function(done) {
|
||||
var config = {
|
||||
version: '1.3.0'
|
||||
};
|
||||
var req = {
|
||||
headers: {
|
||||
host:'localhost'
|
||||
},
|
||||
query: {
|
||||
non_included: 'toberemoved',
|
||||
api_key: 'test',
|
||||
style: 'override',
|
||||
config: config
|
||||
it('it should extend params with decoded lzma', function(done) {
|
||||
var qo = {
|
||||
config: {
|
||||
version: '1.3.0'
|
||||
}
|
||||
};
|
||||
baseController.req2params(prepareRequest(req), function(err, req) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
var query = req.params;
|
||||
assert.deepEqual(config, query.config);
|
||||
assert.equal('test', query.api_key);
|
||||
assert.equal(undefined, query.non_included);
|
||||
assert.equal(undefined, query.style);
|
||||
done();
|
||||
test_helper.lzma_compress_to_base64(JSON.stringify(qo), 1, function(err, data) {
|
||||
var req = {
|
||||
headers: {
|
||||
host:'localhost'
|
||||
},
|
||||
query: {
|
||||
non_included: 'toberemoved',
|
||||
api_key: 'test',
|
||||
style: 'override',
|
||||
lzma: data
|
||||
}
|
||||
};
|
||||
baseController.req2params(prepareRequest(req), function(err, req) {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
var query = req.params;
|
||||
assert.deepEqual(qo.config, query.config);
|
||||
assert.equal('test', query.api_key);
|
||||
assert.equal(undefined, query.non_included);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user