Compare commits
282 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d949d1c27f | ||
|
|
aa1d411fb8 | ||
|
|
f297374449 | ||
|
|
060b93c314 | ||
|
|
3ceeaedf02 | ||
|
|
c6ba9e6102 | ||
|
|
bf40b240d3 | ||
|
|
5d4d2bddd6 | ||
|
|
95dfd87c96 | ||
|
|
eab9e8846e | ||
|
|
788bc302a0 | ||
|
|
1ba240d099 | ||
|
|
ee0405da1e | ||
|
|
5e9b326d03 | ||
|
|
1f30367e59 | ||
|
|
26a2f73c2a | ||
|
|
60005e2f7f | ||
|
|
1c7da2c4b3 | ||
|
|
3799dd2574 | ||
|
|
7efb2a2344 | ||
|
|
88777abc2c | ||
|
|
4d9a6f8fbe | ||
|
|
3d9c2e66c5 | ||
|
|
6bbe715aa6 | ||
|
|
ba002fdb2c | ||
|
|
10d1381e51 | ||
|
|
dfef7ff3c0 | ||
|
|
83d0ce4040 | ||
|
|
75f72c4d07 | ||
|
|
adb9e55fb2 | ||
|
|
5d3726de44 | ||
|
|
f186e4736b | ||
|
|
a00c2b1eef | ||
|
|
64d601179d | ||
|
|
cf2b73e473 | ||
|
|
70932c23df | ||
|
|
519d49bd10 | ||
|
|
bf814c4442 | ||
|
|
f136993c50 | ||
|
|
ba008ab518 | ||
|
|
e4ed6ee1cc | ||
|
|
fda7661dad | ||
|
|
79233471c6 | ||
|
|
a75beefe6e | ||
|
|
e43ccf4f12 | ||
|
|
cd8e320534 | ||
|
|
d7f4d39aa2 | ||
|
|
89333185a9 | ||
|
|
99b95cf839 | ||
|
|
9fbc56b82c | ||
|
|
9a1bc51fdb | ||
|
|
d42257127b | ||
|
|
5a730c6df1 | ||
|
|
418c8691d1 | ||
|
|
9885045b41 | ||
|
|
062e6f9594 | ||
|
|
d8428938ae | ||
|
|
ca5f280cb3 | ||
|
|
524d5a5597 | ||
|
|
a43779b050 | ||
|
|
ef3917fa6f | ||
|
|
031e1253ca | ||
|
|
8012d76b68 | ||
|
|
d726c9ad01 | ||
|
|
1ce8076699 | ||
|
|
54f32113f3 | ||
|
|
19bf079f2d | ||
|
|
b7ecde5c9d | ||
|
|
a2f804d79f | ||
|
|
efdfabf3e9 | ||
|
|
e9a4fc4b2c | ||
|
|
a1d536642e | ||
|
|
3c00266666 | ||
|
|
7f64d15944 | ||
|
|
8259271184 | ||
|
|
20366cedb4 | ||
|
|
a102d1d366 | ||
|
|
4b97b4fd26 | ||
|
|
b94debf10e | ||
|
|
60030784c1 | ||
|
|
cc9b190e5d | ||
|
|
4946ca688c | ||
|
|
d2828ecaff | ||
|
|
5a3dd6a914 | ||
|
|
bcd2fd8f88 | ||
|
|
94a5e66881 | ||
|
|
d55b78f76b | ||
|
|
42149f9ae7 | ||
|
|
1e08d946b1 | ||
|
|
f22216e6d2 | ||
|
|
d9cf830fb4 | ||
|
|
b762008c79 | ||
|
|
ca2c2b80d8 | ||
|
|
f946dfa65f | ||
|
|
418f5faa11 | ||
|
|
bba6db9dbf | ||
|
|
326cad2f2c | ||
|
|
34808d6147 | ||
|
|
79b04bbdfd | ||
|
|
45a663d5ae | ||
|
|
cace6169c0 | ||
|
|
bdce2f95f2 | ||
|
|
506e16fc87 | ||
|
|
c367743d76 | ||
|
|
fa7140e736 | ||
|
|
c63226cd26 | ||
|
|
777df6337b | ||
|
|
2dda0a80da | ||
|
|
e2bd97eea6 | ||
|
|
fb03cd3424 | ||
|
|
8a48b96c53 | ||
|
|
76b0c94835 | ||
|
|
6a36aa1f13 | ||
|
|
800870e783 | ||
|
|
6638ba91c3 | ||
|
|
47e4b9da0d | ||
|
|
81e0c3a098 | ||
|
|
2068861988 | ||
|
|
878f3bd627 | ||
|
|
170fcc1973 | ||
|
|
d0c88ce21d | ||
|
|
86d8f28661 | ||
|
|
e97147ddb4 | ||
|
|
a40bc4a527 | ||
|
|
77f64bee8c | ||
|
|
e81a16ce0d | ||
|
|
153a792fcb | ||
|
|
5c1b1e3214 | ||
|
|
0bca3d6f33 | ||
|
|
14e90a6c76 | ||
|
|
a57cd25bec | ||
|
|
a46f7b3099 | ||
|
|
cb7fb97a13 | ||
|
|
e4ae3e235d | ||
|
|
423620b6c5 | ||
|
|
877ed63090 | ||
|
|
8e9f61f9f1 | ||
|
|
81e54660bb | ||
|
|
4d6c501fa5 | ||
|
|
55d9c02f8d | ||
|
|
a0c24d132e | ||
|
|
6dd4914460 | ||
|
|
ee4e7b01a9 | ||
|
|
434de7786c | ||
|
|
07b4cb78b1 | ||
|
|
6b472c0b20 | ||
|
|
4b0a4dd675 | ||
|
|
97f8c361ed | ||
|
|
0c044636ef | ||
|
|
f95c310462 | ||
|
|
9d8ce6bc44 | ||
|
|
e4407ece84 | ||
|
|
507d105ab2 | ||
|
|
ba6cca46a1 | ||
|
|
753ada0e76 | ||
|
|
d311dccce8 | ||
|
|
b81cfe418a | ||
|
|
8ee4a2c049 | ||
|
|
a987f6ac05 | ||
|
|
b0e47ecc62 | ||
|
|
daa3fdca11 | ||
|
|
bcfc43a517 | ||
|
|
b83351a504 | ||
|
|
1edf684475 | ||
|
|
0bc68d7144 | ||
|
|
52d1cd47db | ||
|
|
98e8d745b1 | ||
|
|
e8740af6ef | ||
|
|
a00f468e62 | ||
|
|
27a52b66c6 | ||
|
|
96b9d498fd | ||
|
|
6d30903531 | ||
|
|
4a63fed943 | ||
|
|
6fe73862f3 | ||
|
|
1664975dd1 | ||
|
|
02ac25181e | ||
|
|
239aa12622 | ||
|
|
aa43eb8953 | ||
|
|
6d46a21005 | ||
|
|
fb7f79594d | ||
|
|
f390a10830 | ||
|
|
4193f96c03 | ||
|
|
afa1e2881f | ||
|
|
3fa6750f9a | ||
|
|
1c842c1592 | ||
|
|
3c88634d09 | ||
|
|
19bb11adc5 | ||
|
|
4405d61845 | ||
|
|
1bb716ef33 | ||
|
|
eb2825eea8 | ||
|
|
811f2bdae3 | ||
|
|
53bc14bc9e | ||
|
|
50ddfaa968 | ||
|
|
ae35acd21d | ||
|
|
d4d32bdfa3 | ||
|
|
3b7db0b08f | ||
|
|
43fec74372 | ||
|
|
c7f5f310f0 | ||
|
|
e26cfb2efb | ||
|
|
b32d056efe | ||
|
|
0b27d174ef | ||
|
|
869f2ac322 | ||
|
|
5bc1903677 | ||
|
|
faaebaa07d | ||
|
|
eceffda87f | ||
|
|
e93fe13b41 | ||
|
|
245d24ea29 | ||
|
|
605be77a04 | ||
|
|
acd0610517 | ||
|
|
e37682403c | ||
|
|
b2fcbdd8a3 | ||
|
|
2f68d658f0 | ||
|
|
85e7245a33 | ||
|
|
2db2546cca | ||
|
|
b3d7909849 | ||
|
|
b035b5d384 | ||
|
|
f52cc276be | ||
|
|
c637caf9c9 | ||
|
|
d405987a96 | ||
|
|
06efe410ef | ||
|
|
5bf4eba215 | ||
|
|
87c4848e19 | ||
|
|
3f075ca432 | ||
|
|
6725025e1a | ||
|
|
8d42909eab | ||
|
|
d947700646 | ||
|
|
cc68b84212 | ||
|
|
446449bbde | ||
|
|
b1f788fb57 | ||
|
|
f80e7112bc | ||
|
|
68f967e582 | ||
|
|
2edcbb4724 | ||
|
|
006dd86614 | ||
|
|
dab204ea71 | ||
|
|
1c6c3962db | ||
|
|
214d684fcc | ||
|
|
9118e2dc5e | ||
|
|
e7592ee570 | ||
|
|
81d99ca655 | ||
|
|
7b35701fa8 | ||
|
|
4f8b541010 | ||
|
|
55dd049812 | ||
|
|
66b41a6ae7 | ||
|
|
499e9de75d | ||
|
|
855f47e446 | ||
|
|
fc472e65b6 | ||
|
|
91e0e0fd18 | ||
|
|
077f19d506 | ||
|
|
ed51513b5e | ||
|
|
52630b8084 | ||
|
|
6f04214f5d | ||
|
|
f376a7cdd5 | ||
|
|
b7c6f5acdf | ||
|
|
0887e5d5f7 | ||
|
|
d01857923e | ||
|
|
deb29f2c77 | ||
|
|
d937ed31d5 | ||
|
|
73ae736603 | ||
|
|
1767b83d09 | ||
|
|
ba3af551e3 | ||
|
|
e0d4a9e596 | ||
|
|
555d3f558c | ||
|
|
386d6bfea8 | ||
|
|
479b8be639 | ||
|
|
a007fce913 | ||
|
|
100a2986b9 | ||
|
|
752bfe779e | ||
|
|
8cf878f723 | ||
|
|
605d7057c9 | ||
|
|
60e4defa66 | ||
|
|
e7b8d9b223 | ||
|
|
e041b5b8a9 | ||
|
|
4a2950796b | ||
|
|
9a8f72b8db | ||
|
|
667925c455 | ||
|
|
f24217a400 | ||
|
|
84fd01535c | ||
|
|
e362fca9eb | ||
|
|
00f81db57e | ||
|
|
0c9d60b573 | ||
|
|
ad227a5240 | ||
|
|
2b1f12e9d5 |
41
NEWS.md
41
NEWS.md
@@ -1,10 +1,49 @@
|
||||
# Changelog
|
||||
|
||||
## 4.7.0
|
||||
Released 2018-01-03
|
||||
|
||||
New features:
|
||||
- Return tilejson in metadata #837.
|
||||
|
||||
Bug fixes:
|
||||
- Allow to create vector map-config for layers that doesn't have points. Layers with lines or polygons won't be aggregated by default.
|
||||
|
||||
|
||||
## 4.6.0
|
||||
Released 2018-01-02
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [4.2.0](https://github.com/CartoDB/windshaft/releases/tag/4.2.0).
|
||||
- Validate aggregation input params.
|
||||
- Fix column names collisions in histograms [#828](https://github.com/CartoDB/Windshaft-cartodb/pull/828).
|
||||
- Add full-sample aggregation support for vector map-config.
|
||||
|
||||
|
||||
## 4.5.0
|
||||
Released 2017-12-19
|
||||
|
||||
Announcements:
|
||||
- Date histograms: Add second, decade, century and millenium aggregations
|
||||
- Date histograms: Switch the auto threshold from 366 buckets to 100.
|
||||
- Logging all errors.
|
||||
- Add support for aggregated visualizations.
|
||||
- Allow vector-only map-config creation.
|
||||
- Histograms: Now they accept a `no_filters` parameter.
|
||||
|
||||
|
||||
## 4.4.0
|
||||
Released 2017-12-12
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.60.0](https://github.com/CartoDB/camshaft/releases/tag/0.60.0).
|
||||
|
||||
|
||||
## 4.3.1
|
||||
Released 2017-12-12
|
||||
|
||||
Bug fix:
|
||||
- Fixed bug introduced in version 4.0.1 that brokes the static map generation using JPG as format
|
||||
- Fixed bug introduced in version 4.0.1 that brokes the static map generation using JPG as format #808
|
||||
|
||||
## 4.3.0
|
||||
Released 2017-12-11
|
||||
|
||||
@@ -55,7 +55,7 @@ var config = {
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: false
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler]) (:res[X-Tiler-Errors])'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
|
||||
@@ -55,7 +55,7 @@ var config = {
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: true
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler]) (:res[X-Tiler-Errors])'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
|
||||
@@ -55,7 +55,7 @@ var config = {
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: true
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type]'
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type] (:res[X-Tiler-Errors])'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
|
||||
@@ -54,7 +54,7 @@ var config = {
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: false
|
||||
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
|
||||
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler]) (:res[X-Tiler-Errors])'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Anonymous Maps
|
||||
|
||||
Anonymous Maps allows you to instantiate a map given SQL and CartoCSS. It also allows you to add interaction capabilities using [UTF Grid.](https://github.com/mapbox/utfgrid-spec)
|
||||
Anonymous Maps allows you to instantiate a map given SQL and CartoCSS. It also allows you to add interaction capabilities using [UTF Grid.](https://github.com/mapbox/utfgrid-spec).
|
||||
Alternatively, you can get the data for the map (geometry and attributes for each layer) using vector tiles (in which case CartoCSS is not required).
|
||||
|
||||
|
||||
## Instantiate
|
||||
@@ -72,19 +73,19 @@ curl 'https://{username}.carto.com/api/v1/map' -H 'Content-Type: application/jso
|
||||
|
||||
## Map Tile Rendering
|
||||
|
||||
Map tiles create the graphical representation of your map in a web browser. The performance rendering of map tiles is dependent on the type of geospatial data model (raster or vector) that you are using.
|
||||
Map tiles are used to create the graphic representation of your map in a web browser. Tiles can be requested either as pre-rendered *raster* tiles (images) or as *vector* map data to be rendered by the client (browser).
|
||||
|
||||
- **Raster**: Generates map tiles based on a grid of pixels to represent your data. Each cell is a fixed size and contains values for particular map features. On the server-side, each request queries a dataset to retrieve data for each map tile. The grid size of map tiles can often lead to graphic quality issues.
|
||||
- **Raster**: If a tile is requested as a raster image format, like PNG, the map will be rendered on the server, using the CartoCSS styles defined in the layers of the map. It is necessary that all the layers of a map define CartoCSS styles in order to obtain raster tiles. Raster tiles are made up of 256x256 pixels; to avoid graphic quality issues tiles should be used unscaled to represent the zoom level (Z) for which they are requested. In order to render tiles, data will be retrieved from the database (in vector format) on the server-side.
|
||||
|
||||
- **Vector**: Generates map tiles based on pre-defined coordinates to represent your data, similar to how basemap image tiles are rendered. On the client-side, map tiles represent real-world geometries of a map. Depending on the coordinates, vertices are used to connect the data and display points, lines, or polygons for the map tiles.
|
||||
- **Vector**: Tiles can also be requested as MVT (Mapbox Vector Tiles). In this case, only the geospatial vector data, without any styling, is returned. These tiles should be processed in the client-side to render the map. In this case layers do not need to define CartoCSS, as any rendering and styling will be performed on the client side. The vector data of a tile represents real-world geometries by defining the vertices of points, lines or polygons in a tile-specific coordinate system.
|
||||
|
||||
## Retrieve resources from the layergroup
|
||||
|
||||
When you have a layergroup, there are several resources for retrieving layergoup details such as, accessing Mapnik tiles, getting individual layers, accessing defined Attributes, and blending and layer selection.
|
||||
|
||||
### Mapnik tiles
|
||||
### Raster tiles
|
||||
|
||||
These raster tiles retrieve just the Mapnik layers. See [individual layers](#individual-layers) for details about how to retrieve other layers.
|
||||
These raster tiles are PNG images that represent only the Mapnik layers of a map. See [individual layers](#individual-layers) for details about how to retrieve other layers.
|
||||
|
||||
```bash
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png
|
||||
@@ -92,9 +93,9 @@ https://{username}.carto.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png
|
||||
|
||||
### Mapbox Vector Tiles (MVT)
|
||||
|
||||
[Mapbox Vector Tiles (MVT)](https://www.mapbox.com/vector-tiles/specification/) are map tiles that store geographic vector data on the client-side. Browser performance is fast since you can pan and zoom without having to query the server.
|
||||
[Mapbox Vector Tiles (MVT)](https://www.mapbox.com/vector-tiles/specification/) are map tiles that transfer geographic vector data to the client-side. Browser performance is fast since you can pan and zoom without having to query the server.
|
||||
|
||||
CARTO uses a Web Graphics Library (WebGL) to process MVT files. This is useful since WebGL's are compatible with most web browsers, include support for multiple client-side mapping engines, and do not require additional information from the server; which makes it more efficient for rendering map tiles. However, you can use any implementation tool for processing MVT files.
|
||||
CARTO uses Web Graphics Library (WebGL) to process MVT files on the browser. This is useful since WebGL is compatible with most web browsers, include support for multiple client-side mapping engines, and do not require additional information from the server; which makes it more efficient for rendering map tiles. However, you can use any implementation tool for processing MVT files.
|
||||
|
||||
The following examples describe how to fetch MVT tiles with a cURL request.
|
||||
|
||||
@@ -245,7 +246,7 @@ center: [30, 0]
|
||||
map.setStyle({
|
||||
"version": 7,
|
||||
"glyphs": "...",
|
||||
"constants": {...},
|
||||
"constants": {...},
|
||||
"sources": {
|
||||
"cartodb": {
|
||||
"type": "vector",
|
||||
|
||||
@@ -37,12 +37,19 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
|
||||
throw new Error("Dataview '" + dataviewName + "' does not exists");
|
||||
}
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
|
||||
var ownFilter = +params.own_filter;
|
||||
ownFilter = !!ownFilter;
|
||||
var noFilters = +params.no_filters;
|
||||
if (Number.isFinite(ownFilter) && Number.isFinite(noFilters)) {
|
||||
err = new Error();
|
||||
err.message = 'Both own_filter and no_filters cannot be sent in the same request';
|
||||
err.type = 'dataview';
|
||||
err.http_status = 400;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var query = (ownFilter) ? dataviewDefinition.sql.own_filter_on : dataviewDefinition.sql.own_filter_off;
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
|
||||
var query = getDataviewQuery(dataviewDefinition, ownFilter, noFilters);
|
||||
if (params.bbox) {
|
||||
var bboxFilter = new BBoxFilter({column: 'the_geom_webmercator', srid: 3857}, {bbox: params.bbox});
|
||||
query = bboxFilter.sql(query);
|
||||
@@ -55,7 +62,7 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
|
||||
);
|
||||
|
||||
var dataview = dataviewFactory.getDataview(query, dataviewDefinition);
|
||||
dataview.getResult(pg, getOverrideParams(params, ownFilter), this);
|
||||
dataview.getResult(pg, getOverrideParams(params, !!ownFilter), this);
|
||||
},
|
||||
function returnCallback(err, result) {
|
||||
return callback(err, result);
|
||||
@@ -63,6 +70,16 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
|
||||
);
|
||||
};
|
||||
|
||||
function getDataviewQuery(dataviewDefinition, ownFilter, noFilters) {
|
||||
if (noFilters) {
|
||||
return dataviewDefinition.sql.no_filters;
|
||||
} else if (ownFilter === 1) {
|
||||
return dataviewDefinition.sql.own_filter_on;
|
||||
} else {
|
||||
return dataviewDefinition.sql.own_filter_off;
|
||||
}
|
||||
}
|
||||
|
||||
function getQueryRewriteData(mapConfig, dataviewDefinition, params) {
|
||||
var sourceId = dataviewDefinition.source.id; // node.id
|
||||
var layer = _.find(mapConfig.obj().layers, function(l) {
|
||||
|
||||
@@ -106,6 +106,7 @@ LayergroupController.prototype.register = function(app) {
|
||||
var allowedDataviewQueryParams = [
|
||||
'filters', // json
|
||||
'own_filter', // 0, 1
|
||||
'no_filters', // 0, 1
|
||||
'bbox', // w,s,e,n
|
||||
'start', // number
|
||||
'end', // number
|
||||
|
||||
@@ -6,6 +6,7 @@ var ResourceLocator = require('../models/resource-locator');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
const allowQueryParams = require('../middleware/allow-query-params');
|
||||
|
||||
var MapConfig = windshaft.model.MapConfig;
|
||||
var Datasource = windshaft.model.Datasource;
|
||||
@@ -69,6 +70,7 @@ MapController.prototype.composeCreateMapMiddleware = function (useTemplate = fal
|
||||
return [
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(['aggregation']),
|
||||
this.prepareContext,
|
||||
this.initProfiler(isTemplateInstantiation),
|
||||
this.checkJsonContentType(),
|
||||
@@ -87,6 +89,8 @@ MapController.prototype.composeCreateMapMiddleware = function (useTemplate = fal
|
||||
this.setDataviewsAndWidgetsUrlsToLayergroupMetadata(),
|
||||
this.setAnalysesMetadataToLayergroup(includeQuery),
|
||||
this.setTurboCartoMetadataToLayergroup(),
|
||||
this.setAggregationMetadataToLayergroup(),
|
||||
this.setTilejsonMetadataToLayergroup(),
|
||||
this.setSurrogateKeyHeader(),
|
||||
this.sendResponse(),
|
||||
this.augmentError({ label, addContext })
|
||||
@@ -310,6 +314,74 @@ MapController.prototype.augmentLayergroupData = function () {
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
function getTilejson(tiles, grids) {
|
||||
const tilejson = {
|
||||
tilejson: '2.2.0',
|
||||
tiles: tiles.https || tiles.http
|
||||
};
|
||||
|
||||
if (grids) {
|
||||
tilejson.grids = grids.https || grids.http;
|
||||
}
|
||||
|
||||
return tilejson;
|
||||
}
|
||||
|
||||
MapController.prototype.setTilejsonMetadataToLayergroup = function () {
|
||||
return function augmentLayergroupTilejsonMiddleware (req, res, next) {
|
||||
const { layergroup, user, mapconfig } = res.locals;
|
||||
|
||||
const isVectorOnlyMapConfig = mapconfig.isVectorOnlyMapConfig();
|
||||
let hasMapnikLayers = false;
|
||||
layergroup.metadata.layers.forEach((layerMetadata, index) => {
|
||||
const layerId = mapconfig.getLayerId(index);
|
||||
const rasterResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.png`;
|
||||
if (mapconfig.layerType(index) === 'mapnik') {
|
||||
hasMapnikLayers = true;
|
||||
const vectorResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.mvt`;
|
||||
const layerTilejson = {
|
||||
vector: getTilejson(this.resourceLocator.getTileUrls(user, vectorResource))
|
||||
};
|
||||
if (!isVectorOnlyMapConfig) {
|
||||
let grids = null;
|
||||
const layer = mapconfig.getLayer(index);
|
||||
if (layer.options.interactivity) {
|
||||
const gridResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.grid.json`;
|
||||
grids = this.resourceLocator.getTileUrls(user, gridResource);
|
||||
}
|
||||
layerTilejson.raster = getTilejson(
|
||||
this.resourceLocator.getTileUrls(user, rasterResource),
|
||||
grids
|
||||
);
|
||||
}
|
||||
layerMetadata.tilejson = layerTilejson;
|
||||
} else {
|
||||
layerMetadata.tilejson = {
|
||||
raster: getTilejson(this.resourceLocator.getTileUrls(user, rasterResource))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const tilejson = {};
|
||||
|
||||
if (hasMapnikLayers) {
|
||||
tilejson.vector = getTilejson(
|
||||
this.resourceLocator.getTileUrls(user, `${layergroup.layergroupid}/{z}/{x}/{y}.mvt`)
|
||||
);
|
||||
if (!isVectorOnlyMapConfig) {
|
||||
tilejson.raster = getTilejson(
|
||||
this.resourceLocator.getTileUrls(user, `${layergroup.layergroupid}/{z}/{x}/{y}.png`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
layergroup.metadata.tilejson = tilejson;
|
||||
|
||||
next();
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.getAffectedTables = function () {
|
||||
return function getAffectedTablesMiddleware (req, res, next) {
|
||||
const { dbname, layergroup, user, mapconfig } = res.locals;
|
||||
@@ -540,13 +612,13 @@ MapController.prototype.setTurboCartoMetadataToLayergroup = function () {
|
||||
return function setTurboCartoMetadataToLayergroupMiddleware (req, res, next) {
|
||||
const { layergroup, mapconfig, context } = res.locals;
|
||||
|
||||
addContextMetadata(layergroup, mapconfig.obj(), context);
|
||||
addTurboCartoContextMetadata(layergroup, mapconfig.obj(), context);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
function addContextMetadata(layergroup, mapConfig, context) {
|
||||
function addTurboCartoContextMetadata(layergroup, mapConfig, context) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
if (context.turboCarto && Array.isArray(context.turboCarto.layers)) {
|
||||
@@ -557,6 +629,28 @@ function addContextMetadata(layergroup, mapConfig, context) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: see how evolve this function, it's a good candidate to be refactored
|
||||
MapController.prototype.setAggregationMetadataToLayergroup = function () {
|
||||
return function setAggregationMetadataToLayergroupMiddleware (req, res, next) {
|
||||
const { layergroup, mapconfig, context } = res.locals;
|
||||
|
||||
addAggregationContextMetadata(layergroup, mapconfig.obj(), context);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
function addAggregationContextMetadata(layergroup, mapConfig, context) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
if (context.aggregation && Array.isArray(context.aggregation.layers)) {
|
||||
layer.meta.aggregation = context.aggregation.layers[layerIndex];
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MapController.prototype.setSurrogateKeyHeader = function () {
|
||||
return function setSurrogateKeyHeaderMiddleware(req, res, next) {
|
||||
const { affectedTables, user, templateName } = res.locals;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var templateName = require('../backends/template_maps').templateName;
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
const { templateName } = require('../backends/template_maps');
|
||||
const cors = require('../middleware/cors');
|
||||
const userMiddleware = require('../middleware/user');
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
@@ -20,211 +16,189 @@ function NamedMapsAdminController(authApi, templateMaps) {
|
||||
module.exports = NamedMapsAdminController;
|
||||
|
||||
NamedMapsAdminController.prototype.register = function (app) {
|
||||
const { base_url_templated } = app;
|
||||
|
||||
app.post(
|
||||
app.base_url_templated + '/',
|
||||
`${base_url_templated}/`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.create.bind(this)
|
||||
this.checkContentType('POST', 'POST TEMPLATE'),
|
||||
this.authorizedByAPIKey('create', 'POST TEMPLATE'),
|
||||
this.create()
|
||||
);
|
||||
|
||||
app.put(
|
||||
app.base_url_templated + '/:template_id',
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.update.bind(this)
|
||||
this.checkContentType('PUT', 'PUT TEMPLATE'),
|
||||
this.authorizedByAPIKey('update', 'PUT TEMPLATE'),
|
||||
this.update()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_templated + '/:template_id',
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.retrieve.bind(this)
|
||||
this.authorizedByAPIKey('get', 'GET TEMPLATE'),
|
||||
this.retrieve()
|
||||
);
|
||||
|
||||
app.delete(
|
||||
app.base_url_templated + '/:template_id',
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.destroy.bind(this)
|
||||
this.authorizedByAPIKey('delete', 'DELETE TEMPLATE'),
|
||||
this.destroy()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_templated + '/',
|
||||
`${base_url_templated}/`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.list.bind(this)
|
||||
this.authorizedByAPIKey('list', 'GET TEMPLATE LIST'),
|
||||
this.list()
|
||||
);
|
||||
|
||||
app.options(
|
||||
app.base_url_templated + '/:template_id',
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors('Content-Type')
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.create = function(req, res, next) {
|
||||
var self = this;
|
||||
NamedMapsAdminController.prototype.authorizedByAPIKey = function (action, label) {
|
||||
return function authorizedByAPIKeyMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
this.authApi.authorizedByAPIKey(user, req, (err, authenticated) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function addTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
ifInvalidContentType(req, 'template POST data must be of type application/json');
|
||||
var cfg = req.body;
|
||||
self.templateMaps.addTemplate(cdbuser, cfg, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_id){
|
||||
assert.ifError(err);
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self, req, res, 'POST TEMPLATE', null, next)
|
||||
);
|
||||
if (!authenticated) {
|
||||
const error = new Error(`Only authenticated user can ${action} templated maps`);
|
||||
error.http_status = 403;
|
||||
error.label = label;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.update = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
var template;
|
||||
var tpl_id;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can update templated maps');
|
||||
ifInvalidContentType(req, 'template PUT data must be of type application/json');
|
||||
|
||||
template = req.body;
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.updTemplate(cdbuser, tpl_id, template, this);
|
||||
},
|
||||
function prepareResponse(err){
|
||||
assert.ifError(err);
|
||||
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self, req, res, 'PUT TEMPLATE', null, next)
|
||||
);
|
||||
NamedMapsAdminController.prototype.checkContentType = function (action, label) {
|
||||
return function checkContentTypeMiddleware (req, res, next) {
|
||||
if (!req.is('application/json')) {
|
||||
const error = new Error(`template ${action} data must be of type application/json`);
|
||||
error.label = label;
|
||||
return next(error);
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.retrieve = function(req, res, next) {
|
||||
var self = this;
|
||||
NamedMapsAdminController.prototype.create = function () {
|
||||
return function createTemplateMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const template = req.body;
|
||||
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
this.templateMaps.addTemplate(user, template, (err, templateId) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function getTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
res.status(200);
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_val) {
|
||||
assert.ifError(err);
|
||||
if ( ! tpl_val ) {
|
||||
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]({ template_id: templateId });
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.update = function () {
|
||||
return function updateTemplateMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const template = req.body;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
this.templateMaps.updTemplate(user, templateId, template, (err) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]({ template_id: templateId });
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.retrieve = function () {
|
||||
return function retrieveTemplateMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
|
||||
const { user } = res.locals;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
this.templateMaps.getTemplate(user, templateId, (err, template) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!template) {
|
||||
const error = new Error(`Cannot find template '${templateId}' of user '${user}'`);
|
||||
error.http_status = 404;
|
||||
return next(error);
|
||||
}
|
||||
// auth_id was added by ourselves,
|
||||
// so we remove it before returning to the user
|
||||
delete tpl_val.auth_id;
|
||||
return { template: tpl_val };
|
||||
},
|
||||
finishFn(self, req, res, 'GET TEMPLATE', null, next)
|
||||
);
|
||||
delete template.auth_id;
|
||||
|
||||
res.status(200);
|
||||
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]({ template });
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.destroy = function(req, res, next) {
|
||||
var self = this;
|
||||
NamedMapsAdminController.prototype.destroy = function () {
|
||||
return function destroyTemplateMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
const { user } = res.locals;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function deleteTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can delete template maps');
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.delTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err/*, tpl_val*/){
|
||||
assert.ifError(err);
|
||||
return '';
|
||||
},
|
||||
finishFn(self, req, res, 'DELETE TEMPLATE', 204, next)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.list = function(req, res, next) {
|
||||
var self = this;
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function listTemplates(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can list templated maps');
|
||||
|
||||
self.templateMaps.listTemplates(cdbuser, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_ids){
|
||||
assert.ifError(err);
|
||||
return { template_ids: tpl_ids };
|
||||
},
|
||||
finishFn(self, req, res, 'GET TEMPLATE LIST', null, next)
|
||||
);
|
||||
};
|
||||
|
||||
function finishFn(controller, req, res, description, status, next) {
|
||||
return function finish(err, body){
|
||||
if (err) {
|
||||
err.label = description;
|
||||
next(err);
|
||||
} else {
|
||||
res.status(status || 200);
|
||||
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(body);
|
||||
} else {
|
||||
res.json(body);
|
||||
this.templateMaps.delTemplate(user, templateId, (err/* , tpl_val */) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function ifUnauthenticated(authenticated, description) {
|
||||
if (!authenticated) {
|
||||
var err = new Error(description);
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
res.status(204);
|
||||
|
||||
function ifInvalidContentType(req, description) {
|
||||
if (!req.is('application/json')) {
|
||||
throw new Error(description);
|
||||
}
|
||||
}
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]('');
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.list = function () {
|
||||
return function listTemplatesMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
|
||||
const { user } = res.locals;
|
||||
|
||||
this.templateMaps.listTemplates(user, (err, templateIds) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]({ template_ids: templateIds });
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
@@ -18,7 +18,8 @@ module.exports = function errorMiddleware (/* options */) {
|
||||
if (err.message === 'Tile does not exist' && res.locals.format === 'mvt') {
|
||||
statusCode = 204;
|
||||
}
|
||||
|
||||
|
||||
setErrorHeader(allErrors, statusCode, res);
|
||||
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
|
||||
|
||||
// If a callback was requested, force status to 200
|
||||
@@ -160,3 +161,53 @@ function errorMessageWithContext(err) {
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
function setErrorHeader(errors, statusCode, res) {
|
||||
let errorsCopy = errors.slice(0);
|
||||
const mainError = errorsCopy.shift();
|
||||
|
||||
let errorsLog = {
|
||||
mainError: {
|
||||
statusCode: statusCode || 200,
|
||||
message: mainError.message,
|
||||
name: mainError.name,
|
||||
label: mainError.label,
|
||||
type: mainError.type,
|
||||
subtype: mainError.subtype
|
||||
}
|
||||
};
|
||||
|
||||
errorsLog.moreErrors = errorsCopy.map(error => {
|
||||
return {
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
label: error.label,
|
||||
type: error.type,
|
||||
subtype: error.subtype
|
||||
};
|
||||
});
|
||||
|
||||
res.set('X-Tiler-Errors', stringifyForLogs(errorsLog));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove problematic nested characters
|
||||
* from object for logs RegEx
|
||||
*
|
||||
* @param {Object} object
|
||||
*/
|
||||
function stringifyForLogs(object) {
|
||||
Object.keys(object).map(key => {
|
||||
if(typeof object[key] === 'string') {
|
||||
object[key] = object[key].replace(/[^a-zA-Z0-9]/g, ' ');
|
||||
} else if (typeof object[key] === 'object') {
|
||||
stringifyForLogs(object[key]);
|
||||
} else if (object[key] instanceof Array) {
|
||||
for (let element of object[key]) {
|
||||
stringifyForLogs(element);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return JSON.stringify(object);
|
||||
}
|
||||
|
||||
229
lib/cartodb/models/aggregation/aggregation-mapconfig.js
Normal file
229
lib/cartodb/models/aggregation/aggregation-mapconfig.js
Normal file
@@ -0,0 +1,229 @@
|
||||
const MapConfig = require('windshaft').model.MapConfig;
|
||||
const aggregationQuery = require('./aggregation-query');
|
||||
const aggregationValidator = require('./aggregation-validator');
|
||||
const {
|
||||
createPositiveNumberValidator,
|
||||
createIncludesValueValidator,
|
||||
createAggregationColumnsValidator
|
||||
} = aggregationValidator;
|
||||
|
||||
const SubstitutionTokens = require('../../utils/substitution-tokens');
|
||||
|
||||
const removeDuplicates = arr => [...new Set(arr)];
|
||||
|
||||
function prepareSql(sql) {
|
||||
return sql && SubstitutionTokens.replace(sql, {
|
||||
bbox: 'ST_MakeEnvelope(0,0,0,0)',
|
||||
scale_denominator: '0',
|
||||
pixel_width: '1',
|
||||
pixel_height: '1'
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = class AggregationMapConfig extends MapConfig {
|
||||
static get AGGREGATIONS () {
|
||||
return aggregationQuery.SUPPORTED_AGGREGATE_FUNCTIONS;
|
||||
}
|
||||
|
||||
static get PLACEMENTS () {
|
||||
return aggregationQuery.SUPPORTED_PLACEMENTS;
|
||||
}
|
||||
|
||||
static get THRESHOLD () {
|
||||
return 1e5; // 100K
|
||||
}
|
||||
|
||||
static get RESOLUTION () {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static get SUPPORTED_GEOMETRY_TYPES () {
|
||||
return [
|
||||
'ST_Point'
|
||||
];
|
||||
}
|
||||
|
||||
static supportsGeometryType(geometryType) {
|
||||
return AggregationMapConfig.SUPPORTED_GEOMETRY_TYPES.includes(geometryType);
|
||||
}
|
||||
|
||||
constructor (user, config, connection, datasource) {
|
||||
super(config, datasource);
|
||||
|
||||
const validate = aggregationValidator(this);
|
||||
const positiveNumberValidator = createPositiveNumberValidator(this);
|
||||
const includesValidPlacementsValidator = createIncludesValueValidator(this, AggregationMapConfig.PLACEMENTS);
|
||||
const aggregationColumnsValidator = createAggregationColumnsValidator(this, AggregationMapConfig.AGGREGATIONS);
|
||||
|
||||
validate('resolution', positiveNumberValidator);
|
||||
validate('placement', includesValidPlacementsValidator);
|
||||
validate('threshold', positiveNumberValidator);
|
||||
validate('columns', aggregationColumnsValidator);
|
||||
|
||||
this.user = user;
|
||||
this.pgConnection = connection;
|
||||
}
|
||||
|
||||
getAggregatedQuery (index) {
|
||||
const { sql_raw, sql } = this.getLayer(index).options;
|
||||
const {
|
||||
// The default aggregation has no placement, columns or dimensions;
|
||||
// this enables the special "full-sample" aggregation.
|
||||
resolution = AggregationMapConfig.RESOLUTION,
|
||||
threshold = AggregationMapConfig.THRESHOLD,
|
||||
placement,
|
||||
columns = {},
|
||||
dimensions = {}
|
||||
} = this.getAggregation(index);
|
||||
|
||||
return aggregationQuery({
|
||||
query: sql_raw || sql,
|
||||
resolution,
|
||||
threshold,
|
||||
placement,
|
||||
columns,
|
||||
dimensions,
|
||||
isDefaultAggregation: this._isDefaultLayerAggregation(index)
|
||||
});
|
||||
}
|
||||
|
||||
isAggregationMapConfig () {
|
||||
return this.isVectorOnlyMapConfig() || this.hasAnyLayerAggregation();
|
||||
}
|
||||
|
||||
isAggregationLayer (index) {
|
||||
return this.isVectorOnlyMapConfig() || this.hasLayerAggregation(index);
|
||||
}
|
||||
|
||||
hasAnyLayerAggregation () {
|
||||
const layers = this.getLayers();
|
||||
|
||||
for (let index = 0; index < layers.length; index++) {
|
||||
if (this.hasLayerAggregation(index)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
hasLayerAggregation (index) {
|
||||
const layer = this.getLayer(index);
|
||||
const { aggregation } = layer.options;
|
||||
|
||||
return aggregation !== undefined && (typeof aggregation === 'object' || typeof aggregation === 'boolean');
|
||||
}
|
||||
|
||||
getAggregation (index) {
|
||||
if (this.isVectorOnlyMapConfig() && !this.hasLayerAggregation(index)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { aggregation } = this.getLayer(index).options;
|
||||
|
||||
if (typeof aggregation === 'boolean') {
|
||||
return {};
|
||||
}
|
||||
|
||||
return aggregation;
|
||||
}
|
||||
|
||||
getLayerAggregationColumns (index, callback) {
|
||||
if (this._isDefaultLayerAggregation(index)) {
|
||||
const skipGeoms = true;
|
||||
return this.getLayerColumns(index, skipGeoms, (err, columns) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, columns);
|
||||
});
|
||||
}
|
||||
|
||||
const columns = this._getLayerAggregationRequiredColumns(index);
|
||||
|
||||
return callback(null, columns);
|
||||
}
|
||||
|
||||
_getLayerAggregationRequiredColumns (index) {
|
||||
const { columns, dimensions } = this.getAggregation(index);
|
||||
|
||||
let aggregatedColumns = [];
|
||||
if (columns) {
|
||||
aggregatedColumns = Object.keys(columns)
|
||||
.map(key => columns[key].aggregated_column)
|
||||
.filter(aggregatedColumn => typeof aggregatedColumn === 'string');
|
||||
}
|
||||
|
||||
let dimensionsColumns = [];
|
||||
if (dimensions) {
|
||||
dimensionsColumns = Object.keys(dimensions)
|
||||
.map(key => dimensions[key])
|
||||
.filter(dimension => typeof dimension === 'string');
|
||||
}
|
||||
|
||||
return removeDuplicates(aggregatedColumns.concat(dimensionsColumns));
|
||||
}
|
||||
|
||||
doesLayerReachThreshold(index, featureCount) {
|
||||
const threshold = this.getAggregation(index) && this.getAggregation(index).threshold ?
|
||||
this.getAggregation(index).threshold :
|
||||
AggregationMapConfig.THRESHOLD;
|
||||
|
||||
return featureCount >= threshold;
|
||||
}
|
||||
|
||||
getLayerColumns (index, skipGeoms, callback) {
|
||||
const geomColumns = ['the_geom', 'the_geom_webmercator'];
|
||||
const limitedQuery = ctx => `SELECT * FROM (${ctx.query}) __cdb_schema LIMIT 0`;
|
||||
const layer = this.getLayer(index);
|
||||
|
||||
this.pgConnection.getConnection(this.user, (err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const sql = limitedQuery({
|
||||
query: prepareSql(layer.options.sql)
|
||||
});
|
||||
|
||||
connection.query(sql, (err, result) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let columns = result.fields || [];
|
||||
|
||||
columns = columns.map(({ name }) => name);
|
||||
|
||||
if (skipGeoms) {
|
||||
columns = columns.filter((column) => !geomColumns.includes(column));
|
||||
}
|
||||
|
||||
return callback(err, columns);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_isDefaultLayerAggregation (index) {
|
||||
const aggregation = this.getAggregation(index);
|
||||
|
||||
return (this.isVectorOnlyMapConfig() && !this.hasLayerAggregation(index)) ||
|
||||
aggregation === true ||
|
||||
this._isDefaultAggregation(aggregation);
|
||||
}
|
||||
|
||||
_isDefaultAggregation (aggregation) {
|
||||
return aggregation.placement === undefined &&
|
||||
aggregation.columns === undefined &&
|
||||
this._isEmptyParameter(aggregation.dimensions);
|
||||
}
|
||||
|
||||
_isEmptyParameter(parameter) {
|
||||
return parameter === undefined || parameter === null || this._isEmptyObject(parameter);
|
||||
}
|
||||
|
||||
_isEmptyObject (parameter) {
|
||||
return typeof parameter === 'object' && Object.keys(parameter).length === 0;
|
||||
}
|
||||
};
|
||||
251
lib/cartodb/models/aggregation/aggregation-query.js
Normal file
251
lib/cartodb/models/aggregation/aggregation-query.js
Normal file
@@ -0,0 +1,251 @@
|
||||
const DEFAULT_PLACEMENT = 'point-sample';
|
||||
|
||||
/**
|
||||
* Returns a template function (function that accepts template parameters and returns a string)
|
||||
* to generate an aggregation query.
|
||||
* Valid options to define the query template are:
|
||||
* - placement
|
||||
* - columns
|
||||
* - dimensions*
|
||||
* The query template parameters taken by the result template function are:
|
||||
* - sourceQuery
|
||||
* - res
|
||||
* - columns
|
||||
* - dimensions
|
||||
*/
|
||||
const templateForOptions = (options) => {
|
||||
let templateFn = defaultAggregationQueryTemplate;
|
||||
if (!options.isDefaultAggregation) {
|
||||
templateFn = aggregationQueryTemplates[options.placement || DEFAULT_PLACEMENT];
|
||||
if (!templateFn) {
|
||||
throw new Error("Invalid Aggregation placement: '" + options.placement + "'");
|
||||
}
|
||||
}
|
||||
return templateFn;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates an aggregation query given the aggregation options:
|
||||
* - query
|
||||
* - resolution - defined as in torque:
|
||||
* aggregation cell is resolution*resolution pixels, where tiles are always 256x256 pixels
|
||||
* - columns
|
||||
* - placement
|
||||
* - dimensions
|
||||
*
|
||||
* The default aggregation (when no explicit placement, columns or dimensions are present) returns
|
||||
* a sample record (with all the original columns and _cdb_feature_count) for each aggregation group.
|
||||
* When placement, columns or dimensions are specified, columns are aggregated as requested
|
||||
* (by default only _cdb_feature_count) and with the_geom_webmercator as defined by placement.
|
||||
*/
|
||||
const queryForOptions = (options) => templateForOptions(options)({
|
||||
sourceQuery: options.query,
|
||||
res: 256/options.resolution,
|
||||
columns: options.columns,
|
||||
dimensions: options.dimensions
|
||||
});
|
||||
|
||||
module.exports = queryForOptions;
|
||||
|
||||
const SUPPORTED_AGGREGATE_FUNCTIONS = {
|
||||
'count': {
|
||||
sql: (column_name, params) => `count(${params.aggregated_column || '*'})`
|
||||
},
|
||||
'avg': {
|
||||
sql: (column_name, params) => `avg(${params.aggregated_column || column_name})`
|
||||
},
|
||||
'sum': {
|
||||
sql: (column_name, params) => `sum(${params.aggregated_column || column_name})`
|
||||
},
|
||||
'min': {
|
||||
sql: (column_name, params) => `min(${params.aggregated_column || column_name})`
|
||||
},
|
||||
'max': {
|
||||
sql: (column_name, params) => `max(${params.aggregated_column || column_name})`
|
||||
},
|
||||
'mode': {
|
||||
sql: (column_name, params) => `_cdb_mode(${params.aggregated_column || column_name})`
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.SUPPORTED_AGGREGATE_FUNCTIONS = Object.keys(SUPPORTED_AGGREGATE_FUNCTIONS);
|
||||
|
||||
const sep = (list) => {
|
||||
let expr = list.join(', ');
|
||||
return expr ? ', ' + expr : expr;
|
||||
};
|
||||
|
||||
const aggregateColumns = ctx => {
|
||||
return Object.assign({
|
||||
_cdb_feature_count: {
|
||||
aggregate_function: 'count'
|
||||
}
|
||||
}, ctx.columns || {});
|
||||
};
|
||||
|
||||
const aggregateColumnNames = (ctx, table) => {
|
||||
let columns = aggregateColumns(ctx);
|
||||
if (table) {
|
||||
return sep(Object.keys(columns).map(
|
||||
column_name => `${table}.${column_name}`
|
||||
));
|
||||
}
|
||||
return sep(Object.keys(columns));
|
||||
};
|
||||
|
||||
const aggregateColumnDefs = ctx => {
|
||||
let columns = aggregateColumns(ctx);
|
||||
return sep(Object.keys(columns).map(column_name => {
|
||||
const aggregate_function = columns[column_name].aggregate_function || 'count';
|
||||
const aggregate_definition = SUPPORTED_AGGREGATE_FUNCTIONS[aggregate_function];
|
||||
if (!aggregate_definition) {
|
||||
throw new Error("Invalid Aggregate function: '" + aggregate_function + "'");
|
||||
}
|
||||
const aggregate_expression = aggregate_definition.sql(column_name, columns[column_name]);
|
||||
return `${aggregate_expression} AS ${column_name}`;
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
const aggregateDimensions = ctx => ctx.dimensions || {};
|
||||
|
||||
const dimensionNames = (ctx, table) => {
|
||||
let dimensions = aggregateDimensions(ctx);
|
||||
if (table) {
|
||||
return sep(Object.keys(dimensions).map(
|
||||
dimension_name => `${table}.${dimension_name}`
|
||||
));
|
||||
}
|
||||
return sep(Object.keys(dimensions));
|
||||
};
|
||||
|
||||
const dimensionDefs = ctx => {
|
||||
let dimensions = aggregateDimensions(ctx);
|
||||
return sep(Object.keys(dimensions).map(dimension_name => {
|
||||
const expression = dimensions[dimension_name];
|
||||
return `${expression} AS ${dimension_name}`;
|
||||
}));
|
||||
};
|
||||
|
||||
// SQL expression to compute the aggregation resolution (grid cell size).
|
||||
// This is equivalent to `${256/ctx.res}*CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!))`
|
||||
// This is defined by the ctx.res parameter, which is the number of grid cells per tile linear dimension
|
||||
// (i.e. each tile is divided into ctx.res*ctx.res cells).
|
||||
const gridResolution = ctx => `(${256*0.00028/ctx.res}*!scale_denominator!)::double precision`;
|
||||
|
||||
// Notes:
|
||||
// * We need to filter spatially using !bbox! to make the queries efficient because
|
||||
// the filter added by Mapnik (wrapping the query)
|
||||
// is only applied after the aggregation.
|
||||
// * This queries are used for rendering and the_geom is omitted in the results for better performance
|
||||
|
||||
// The special default aggregation includes all the columns of a sample row per grid cell and
|
||||
// the count (_cdb_feature_count) of the aggregated rows.
|
||||
const defaultAggregationQueryTemplate = ctx => `
|
||||
WITH
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
),
|
||||
_cdb_clusters AS (
|
||||
SELECT
|
||||
MIN(cartodb_id) AS cartodb_id
|
||||
${dimensionDefs(ctx)}
|
||||
${aggregateColumnDefs(ctx)}
|
||||
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
|
||||
WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
|
||||
GROUP BY
|
||||
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
|
||||
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
|
||||
${dimensionNames(ctx)}
|
||||
) SELECT
|
||||
_cdb_query.*
|
||||
${aggregateColumnNames(ctx)}
|
||||
FROM
|
||||
_cdb_clusters INNER JOIN (${ctx.sourceQuery}) _cdb_query
|
||||
ON (_cdb_clusters.cartodb_id = _cdb_query.cartodb_id)
|
||||
`;
|
||||
|
||||
const aggregationQueryTemplates = {
|
||||
'centroid': ctx => `
|
||||
WITH
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
)
|
||||
SELECT
|
||||
row_number() over() AS cartodb_id,
|
||||
ST_SetSRID(
|
||||
ST_MakePoint(
|
||||
AVG(ST_X(_cdb_query.the_geom_webmercator)),
|
||||
AVG(ST_Y(_cdb_query.the_geom_webmercator))
|
||||
), 3857
|
||||
) AS the_geom_webmercator
|
||||
${dimensionDefs(ctx)}
|
||||
${aggregateColumnDefs(ctx)}
|
||||
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
|
||||
WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
|
||||
GROUP BY
|
||||
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
|
||||
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
|
||||
${dimensionNames(ctx)}
|
||||
`,
|
||||
|
||||
'point-grid': ctx => `
|
||||
WITH
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
),
|
||||
_cdb_clusters AS (
|
||||
SELECT
|
||||
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gx,
|
||||
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gy
|
||||
${dimensionDefs(ctx)}
|
||||
${aggregateColumnDefs(ctx)}
|
||||
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
|
||||
WHERE the_geom_webmercator && _cdb_params.bbox
|
||||
GROUP BY _cdb_gx, _cdb_gy ${dimensionNames(ctx)}
|
||||
)
|
||||
SELECT
|
||||
ST_SetSRID(ST_MakePoint((_cdb_gx+0.5)*res, (_cdb_gy+0.5)*res), 3857) AS the_geom_webmercator
|
||||
${dimensionNames(ctx)}
|
||||
${aggregateColumnNames(ctx)}
|
||||
FROM _cdb_clusters, _cdb_params
|
||||
`,
|
||||
|
||||
'point-sample': ctx => `
|
||||
WITH
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
),
|
||||
_cdb_clusters AS (
|
||||
SELECT
|
||||
MIN(cartodb_id) AS cartodb_id
|
||||
${dimensionDefs(ctx)}
|
||||
${aggregateColumnDefs(ctx)}
|
||||
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
|
||||
WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
|
||||
GROUP BY
|
||||
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
|
||||
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
|
||||
${dimensionNames(ctx)}
|
||||
)
|
||||
SELECT
|
||||
_cdb_clusters.cartodb_id,
|
||||
the_geom, the_geom_webmercator
|
||||
${dimensionNames(ctx, '_cdb_query')}
|
||||
${aggregateColumnNames(ctx, '_cdb_clusters')}
|
||||
FROM
|
||||
_cdb_clusters INNER JOIN (${ctx.sourceQuery}) _cdb_query
|
||||
ON (_cdb_clusters.cartodb_id = _cdb_query.cartodb_id)
|
||||
`
|
||||
|
||||
};
|
||||
|
||||
module.exports.SUPPORTED_PLACEMENTS = Object.keys(aggregationQueryTemplates);
|
||||
93
lib/cartodb/models/aggregation/aggregation-validator.js
Normal file
93
lib/cartodb/models/aggregation/aggregation-validator.js
Normal file
@@ -0,0 +1,93 @@
|
||||
module.exports = function aggregationValidator (mapconfig) {
|
||||
return function validateProperty (key, validator) {
|
||||
for (let index = 0; index < mapconfig.getLayers().length; index++) {
|
||||
const aggregation = mapconfig.getAggregation(index);
|
||||
|
||||
if (aggregation === undefined || aggregation[key] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
validator(aggregation[key], key, index);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.createIncludesValueValidator = function (mapconfig, validValues) {
|
||||
return function validateIncludesValue (value, key, index) {
|
||||
if (!validValues.includes(value)) {
|
||||
const message = `Invalid ${key}. Valid values: ${validValues.join(', ')}`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.createPositiveNumberValidator = function (mapconfig) {
|
||||
return function validatePositiveNumber (value, key, index) {
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
const message = `Invalid ${key}, should be a number greather than 0`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.createAggregationColumnsValidator = function (mapconfig, validAggregatedFunctions) {
|
||||
const validateAggregationColumnNames = createAggregationColumnNamesValidator(mapconfig);
|
||||
const validateAggregateFunction = createAggregateFunctionValidator(mapconfig, validAggregatedFunctions);
|
||||
const validateAggregatedColumn = createAggregatedColumnValidator(mapconfig);
|
||||
|
||||
return function validateAggregationColumns (value, key, index) {
|
||||
validateAggregationColumnNames(value, key, index);
|
||||
validateAggregateFunction(value, key, index);
|
||||
validateAggregatedColumn(value, key, index);
|
||||
};
|
||||
};
|
||||
|
||||
function createAggregationColumnNamesValidator(mapconfig) {
|
||||
return function validateAggregationColumnNames (value, key, index) {
|
||||
Object.keys(value).forEach((columnName) => {
|
||||
if (columnName.length <= 0) {
|
||||
const message = `Invalid column name, should be a non empty string`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createAggregateFunctionValidator (mapconfig, validAggregatedFunctions) {
|
||||
return function validateAggregateFunction (value, key, index) {
|
||||
Object.keys(value).forEach((columnName) => {
|
||||
const { aggregate_function } = value[columnName];
|
||||
|
||||
if (!validAggregatedFunctions.includes(aggregate_function)) {
|
||||
const message = `Unsupported aggregation function ${aggregate_function},` +
|
||||
` valid ones: ${validAggregatedFunctions.join(', ')}`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createAggregatedColumnValidator (mapconfig) {
|
||||
return function validateAggregatedColumn (value, key, index) {
|
||||
Object.keys(value).forEach((columnName) => {
|
||||
const { aggregated_column } = value[columnName];
|
||||
|
||||
if (typeof aggregated_column !== 'string' || aggregated_column <= 0) {
|
||||
const message = `Invalid aggregated column, should be a non empty string`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createLayerError(message, mapconfig, index) {
|
||||
const error = new Error(message);
|
||||
error.type = 'layer';
|
||||
error.layer = {
|
||||
id: mapconfig.getLayerId(index),
|
||||
index: index,
|
||||
type: mapconfig.layerType(index)
|
||||
};
|
||||
|
||||
return error;
|
||||
}
|
||||
@@ -56,7 +56,7 @@ __wd_buckets AS
|
||||
${ctx.query}
|
||||
) __source, __wd_tz
|
||||
${condition_str}
|
||||
GROUP BY timestamp, __wd_tz.name
|
||||
GROUP BY 1, __wd_tz.name
|
||||
),`;
|
||||
}
|
||||
|
||||
@@ -127,8 +127,11 @@ const dateIntervalQueryTpl = ctx => `
|
||||
FROM __cdb_interval_in_minutes, __cdb_dates
|
||||
)
|
||||
SELECT
|
||||
ROUND(__cdb_days / 365243) AS millennium,
|
||||
ROUND(__cdb_days / 36525) AS century,
|
||||
ROUND(__cdb_days / 3652) AS decade,
|
||||
ROUND(__cdb_days / 365) AS year,
|
||||
ROUND(__cdb_days / 90) AS quarter,
|
||||
ROUND(__cdb_days / 91) AS quarter,
|
||||
ROUND(__cdb_days / 30) AS month,
|
||||
ROUND(__cdb_days / 7) AS week,
|
||||
__cdb_days AS day,
|
||||
@@ -138,18 +141,22 @@ const dateIntervalQueryTpl = ctx => `
|
||||
FROM __cdb_interval_in_days, __cdb_interval_in_hours, __cdb_interval_in_minutes, __cdb_interval_in_seconds
|
||||
`;
|
||||
|
||||
|
||||
const MAX_INTERVAL_VALUE = 366;
|
||||
/** Constant to switch between aggregations in auto mode */
|
||||
const MAX_INTERVAL_VALUE = 100;
|
||||
|
||||
const DATE_AGGREGATIONS = {
|
||||
'auto': true,
|
||||
'second' : true,
|
||||
'minute': true,
|
||||
'hour': true,
|
||||
'day': true,
|
||||
'week': true,
|
||||
'month': true,
|
||||
'quarter': true,
|
||||
'year': true
|
||||
'year': true,
|
||||
'decade' : true,
|
||||
'century' : true,
|
||||
'millennium' : true
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -243,9 +250,9 @@ ORDER BY bin ASC;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const aggegations = result.rows[0];
|
||||
const aggregation = Object.keys(aggegations)
|
||||
.map(key => ({ name: key, value: aggegations[key] }))
|
||||
const aggregations = result.rows[0];
|
||||
const aggregation = Object.keys(aggregations)
|
||||
.map(key => ({ name: key, value: aggregations[key] }))
|
||||
.reduce((closer, current) => {
|
||||
if (current.value > MAX_INTERVAL_VALUE) {
|
||||
return closer;
|
||||
|
||||
@@ -28,7 +28,7 @@ const binsQueryTpl = ctx => `
|
||||
) AS quartile
|
||||
FROM __cdb_filtered_source) _cdb_quartiles
|
||||
WHERE quartile = 1 or quartile = 3
|
||||
GROUP BY quartile
|
||||
GROUP BY 1
|
||||
) __cdb_iqr
|
||||
),
|
||||
__cdb_bins AS (
|
||||
@@ -137,8 +137,8 @@ FROM
|
||||
(
|
||||
${ctx.query}
|
||||
) __cdb_filtered_source_query${extra_tables}
|
||||
GROUP BY bin${extra_groupby}
|
||||
ORDER BY bin;`;
|
||||
GROUP BY 10${extra_groupby}
|
||||
ORDER BY 10;`;
|
||||
}
|
||||
|
||||
_hasOverridenBins (override) {
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
const AggregationMapConfig = require('../../aggregation/aggregation-mapconfig');
|
||||
const queryUtils = require('../../../utils/query-utils');
|
||||
|
||||
const unsupportedGeometryTypeErrorMessage = ctx =>
|
||||
`Unsupported geometry type: ${ctx.geometryType}. ` +
|
||||
`Aggregation is available only for geometry type: ${AggregationMapConfig.SUPPORTED_GEOMETRY_TYPES}`;
|
||||
|
||||
const invalidAggregationParamValueErrorMessage = ctx =>
|
||||
`Invalid value for 'aggregation' query param: ${ctx.value}. Valid ones are 'true' or 'false'`;
|
||||
|
||||
module.exports = class AggregationMapConfigAdapter {
|
||||
constructor (pgConnection) {
|
||||
this.pgConnection = pgConnection;
|
||||
}
|
||||
|
||||
getMapConfig (user, requestMapConfig, params, context, callback) {
|
||||
if (!this._isValidAggregationQueryParam(params)) {
|
||||
return callback(new Error(invalidAggregationParamValueErrorMessage({ value: params.aggregation })));
|
||||
}
|
||||
|
||||
let mapConfig;
|
||||
try {
|
||||
mapConfig = new AggregationMapConfig(user, requestMapConfig, this.pgConnection);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
|
||||
if (!this._shouldAdapt(mapConfig, params)) {
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
this.pgConnection.getConnection(user, (err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
this._adaptLayers(connection, mapConfig, requestMapConfig, context, callback);
|
||||
});
|
||||
}
|
||||
|
||||
_isValidAggregationQueryParam (params) {
|
||||
const { aggregation } = params;
|
||||
return aggregation === undefined || aggregation === 'true' || aggregation === 'false';
|
||||
}
|
||||
|
||||
_shouldAdapt (mapConfig, params) {
|
||||
const { aggregation } = params;
|
||||
|
||||
if (aggregation === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (aggregation === 'true' || mapConfig.isAggregationMapConfig()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_adaptLayers (connection, mapConfig, requestMapConfig, context, callback) {
|
||||
const adaptLayerPromises = requestMapConfig.layers.map((layer, index) => {
|
||||
return this._adaptLayer(connection, mapConfig, layer, index);
|
||||
});
|
||||
|
||||
Promise.all(adaptLayerPromises)
|
||||
.then(results => {
|
||||
context.aggregation = {
|
||||
layers: []
|
||||
};
|
||||
|
||||
results.forEach(({ layer, index, adapted }) => {
|
||||
if (adapted) {
|
||||
requestMapConfig.layers[index] = layer;
|
||||
}
|
||||
const aggregatedFormats = this._getAggregationMetadata(mapConfig, layer, adapted);
|
||||
context.aggregation.layers.push(aggregatedFormats);
|
||||
});
|
||||
|
||||
callback(null, requestMapConfig);
|
||||
})
|
||||
.catch(err => callback(err));
|
||||
}
|
||||
|
||||
_adaptLayer (connection, mapConfig, layer, index) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._shouldAdaptLayer(connection, mapConfig, layer, index, (err, shouldAdapt) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (!shouldAdapt) {
|
||||
return resolve({ layer, index, adapted: shouldAdapt });
|
||||
}
|
||||
|
||||
const sqlQueryWrap = layer.options.sql_wrap;
|
||||
|
||||
let aggregationSql = mapConfig.getAggregatedQuery(index);
|
||||
|
||||
if (sqlQueryWrap) {
|
||||
aggregationSql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, aggregationSql);
|
||||
}
|
||||
|
||||
layer.options.sql = aggregationSql;
|
||||
|
||||
mapConfig.getLayerAggregationColumns(index, (err, columns) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
layer.options.columns = columns;
|
||||
|
||||
return resolve({ layer, index, adapted: shouldAdapt });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_shouldAdaptLayer (connection, mapConfig, layer, index, callback) {
|
||||
if (!mapConfig.isAggregationLayer(index)) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
const aggregationMetadata = queryUtils.getAggregationMetadata({
|
||||
query: layer.options.sql_raw ? layer.options.sql_raw : layer.options.sql
|
||||
});
|
||||
|
||||
connection.query(aggregationMetadata, (err, res) => {
|
||||
if (err) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
const result = res.rows[0] || {};
|
||||
|
||||
if (!mapConfig.isVectorOnlyMapConfig() && !AggregationMapConfig.supportsGeometryType(result.type)) {
|
||||
const message = unsupportedGeometryTypeErrorMessage({ geometryType: result.type });
|
||||
const error = new Error(message);
|
||||
error.type = 'layer';
|
||||
error.layer = {
|
||||
id: mapConfig.getLayerId(index),
|
||||
index: index,
|
||||
type: mapConfig.layerType(index)
|
||||
};
|
||||
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (mapConfig.isVectorOnlyMapConfig() && !AggregationMapConfig.supportsGeometryType(result.type)) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
if (!mapConfig.doesLayerReachThreshold(index, result.count)) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
callback(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
_getAggregationMetadata (mapConfig, layer, adapted) {
|
||||
if (!adapted) {
|
||||
return { png: false, mvt: false };
|
||||
}
|
||||
|
||||
if (mapConfig.isVectorOnlyMapConfig()) {
|
||||
return { png: false, mvt: true };
|
||||
}
|
||||
|
||||
return { png: true, mvt: true };
|
||||
}
|
||||
};
|
||||
@@ -58,6 +58,13 @@ AnalysisMapConfigAdapter.prototype.getMapConfig = function(user, requestMapConfi
|
||||
|
||||
requestMapConfig = appendFiltersToNodes(requestMapConfig, dataviewsFiltersBySourceId);
|
||||
|
||||
// Expected format for analyses filters
|
||||
// filters = {analyses: {
|
||||
// a1: [{min, max}, {accept, reject}],
|
||||
// b1: [{range, column, min, max}, {category, column, accept, reject}]
|
||||
// }}
|
||||
requestMapConfig = appendFiltersToNodes(requestMapConfig, filters.analyses);
|
||||
|
||||
function createAnalysis(analysisDefinition, done) {
|
||||
self.analysisBackend.create(analysisConfiguration, analysisDefinition, function (err, analysis) {
|
||||
if (err) {
|
||||
@@ -200,6 +207,7 @@ function dataviewQuery(node, dataviewName, ownFilter) {
|
||||
|
||||
function appendFiltersToNodes(requestMapConfig, dataviewsFiltersBySourceId) {
|
||||
var analyses = requestMapConfig.analyses || [];
|
||||
dataviewsFiltersBySourceId = dataviewsFiltersBySourceId || {};
|
||||
|
||||
requestMapConfig.analyses = analyses.map(function(analysisDefinition) {
|
||||
var analysisGraph = new camshaft.reference.AnalysisGraph(analysisDefinition);
|
||||
|
||||
@@ -21,20 +21,36 @@ function ResourceLocator(environment) {
|
||||
|
||||
module.exports = ResourceLocator;
|
||||
|
||||
ResourceLocator.prototype.getUrls = function(username, resource) {
|
||||
ResourceLocator.prototype.getTileUrls = function(username, resourcePath) {
|
||||
if (this.resourcesUrlTemplates) {
|
||||
return this.getUrlsFromTemplate(username, resource);
|
||||
}
|
||||
var cdnDomain = getCdnDomain(this.environment.serverMetadata, resource);
|
||||
if (cdnDomain) {
|
||||
const urls = this.getUrlsFromTemplate(username, new TileResource(resourcePath));
|
||||
return {
|
||||
http: 'http://' + cdnDomain.http + '/' + username + '/api/v1/map/' + resource,
|
||||
https: 'https://' + cdnDomain.https + '/' + username + '/api/v1/map/' + resource
|
||||
http: Array.isArray(urls.http) ? urls.http : [urls.http],
|
||||
https: Array.isArray(urls.https) ? urls.https : [urls.https]
|
||||
};
|
||||
}
|
||||
var cdnUrls = getCdnUrls(this.environment.serverMetadata, username, new TileResource(resourcePath));
|
||||
if (cdnUrls) {
|
||||
return cdnUrls;
|
||||
} else {
|
||||
var port = this.environment.port;
|
||||
return {
|
||||
http: 'http://' + username + '.' + 'localhost.lan:' + port + '/api/v1/map/' + resource
|
||||
http: [`http://${username}.localhost.lan:${port}/api/v1/map/${resourcePath}`]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
ResourceLocator.prototype.getUrls = function(username, resourcePath) {
|
||||
if (this.resourcesUrlTemplates) {
|
||||
return this.getUrlsFromTemplate(username, new Resource(resourcePath));
|
||||
}
|
||||
var cdnUrls = getCdnUrls(this.environment.serverMetadata, username, new Resource(resourcePath));
|
||||
if (cdnUrls) {
|
||||
return cdnUrls;
|
||||
} else {
|
||||
var port = this.environment.port;
|
||||
return {
|
||||
http: `http://${username}.localhost.lan:${port}/api/v1/map/${resourcePath}`
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -45,45 +61,125 @@ ResourceLocator.prototype.getUrlsFromTemplate = function(username, resource) {
|
||||
var cdnDomain = getCdnDomain(this.environment.serverMetadata, resource) || {};
|
||||
|
||||
if (this.resourcesUrlTemplates.http) {
|
||||
urls.http = this.resourcesUrlTemplates.http({
|
||||
cdn_url: cdnDomain.http,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource
|
||||
});
|
||||
if (Array.isArray(cdnDomain.http)) {
|
||||
urls.http = cdnDomain.http.map(d => this.resourcesUrlTemplates.http({
|
||||
cdn_url: d,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource.getPath()
|
||||
}));
|
||||
} else {
|
||||
urls.http = this.resourcesUrlTemplates.http({
|
||||
cdn_url: cdnDomain.http,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource.getPath()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this.resourcesUrlTemplates.https) {
|
||||
urls.https = this.resourcesUrlTemplates.https({
|
||||
cdn_url: cdnDomain.https,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource
|
||||
});
|
||||
if (Array.isArray(cdnDomain.https)) {
|
||||
urls.https = cdnDomain.https.map(d => this.resourcesUrlTemplates.https({
|
||||
cdn_url: d,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource.getPath()
|
||||
}));
|
||||
} else {
|
||||
urls.https = this.resourcesUrlTemplates.https({
|
||||
cdn_url: cdnDomain.https,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource.getPath()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return urls;
|
||||
};
|
||||
|
||||
class Resource {
|
||||
constructor (resourcePath) {
|
||||
this.resourcePath = resourcePath;
|
||||
}
|
||||
|
||||
getPath () {
|
||||
return this.resourcePath;
|
||||
}
|
||||
|
||||
getDomain (domain, subdomains) {
|
||||
return domain.replace('{s}', subdomain(subdomains, this.resourcePath));
|
||||
}
|
||||
|
||||
getUrl (baseUrl, username, subdomains) {
|
||||
let urls = getUrl(baseUrl, username, this.resourcePath);
|
||||
if (subdomains) {
|
||||
urls = urls.replace('{s}', subdomain(subdomains, this.resourcePath));
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
}
|
||||
|
||||
class TileResource extends Resource {
|
||||
constructor (resourcePath) {
|
||||
super(resourcePath);
|
||||
}
|
||||
|
||||
getDomain (domain, subdomains) {
|
||||
return subdomains.map(s => domain.replace('{s}', s));
|
||||
}
|
||||
|
||||
getUrl (baseUrl, username, subdomains) {
|
||||
if (!subdomains) {
|
||||
return [super.getUrl(baseUrl, username)];
|
||||
}
|
||||
return subdomains.map(subdomain => {
|
||||
return getUrl(baseUrl, username, this.resourcePath)
|
||||
.replace('{s}', subdomain);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getUrl(baseUrl, username, path) {
|
||||
return `${baseUrl}/${username}/api/v1/map/${path}`;
|
||||
}
|
||||
|
||||
function getCdnUrls(serverMetadata, username, resource) {
|
||||
if (serverMetadata && serverMetadata.cdn_url) {
|
||||
var cdnUrl = serverMetadata.cdn_url;
|
||||
var httpUrls = resource.getUrl(`http://${cdnUrl.http}`, username);
|
||||
var httpsUrls = resource.getUrl(`https://${cdnUrl.https}`, username);
|
||||
if (cdnUrl.templates) {
|
||||
var templates = cdnUrl.templates;
|
||||
httpUrls = resource.getUrl(templates.http.url, username, templates.http.subdomains);
|
||||
httpsUrls = resource.getUrl(templates.https.url, username, templates.https.subdomains);
|
||||
}
|
||||
return {
|
||||
http: httpUrls,
|
||||
https: httpsUrls,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCdnDomain(serverMetadata, resource) {
|
||||
if (serverMetadata && serverMetadata.cdn_url) {
|
||||
var cdnUrl = serverMetadata.cdn_url;
|
||||
var http = cdnUrl.http;
|
||||
var https = cdnUrl.https;
|
||||
var httpDomain = cdnUrl.http;
|
||||
var httpsDomain = cdnUrl.https;
|
||||
if (cdnUrl.templates) {
|
||||
var templates = cdnUrl.templates;
|
||||
var httpUrlTemplate = templates.http.url;
|
||||
var httpsUrlTemplate = templates.https.url;
|
||||
http = httpUrlTemplate
|
||||
.replace(/^(http[s]*:\/\/)/, '')
|
||||
.replace('{s}', subdomain(templates.http.subdomains, resource));
|
||||
https = httpsUrlTemplate
|
||||
.replace(/^(http[s]*:\/\/)/, '')
|
||||
.replace('{s}', subdomain(templates.https.subdomains, resource));
|
||||
httpDomain = httpUrlTemplate.replace(/^(http[s]*:\/\/)/, '');
|
||||
httpDomain = resource.getDomain(httpDomain, templates.http.subdomains);
|
||||
httpsDomain = httpsUrlTemplate.replace(/^(http[s]*:\/\/)/, '');
|
||||
httpsDomain = resource.getDomain(httpsDomain, templates.https.subdomains);
|
||||
}
|
||||
return {
|
||||
http: http,
|
||||
https: https,
|
||||
http: httpDomain,
|
||||
https: httpsDomain,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -41,6 +41,7 @@ var AnalysisMapConfigAdapter = require('./models/mapconfig/adapter/analysis-mapc
|
||||
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 AggregationMapConfigAdapter = require('./models/mapconfig/adapter/aggregation-mapconfig-adapter');
|
||||
var MapConfigAdapter = require('./models/mapconfig/adapter');
|
||||
|
||||
var StatsBackend = require('./backends/stats');
|
||||
@@ -190,6 +191,7 @@ module.exports = function(serverOptions) {
|
||||
new SqlWrapMapConfigAdapter(),
|
||||
new DataviewsWidgetsAdapter(),
|
||||
new AnalysisMapConfigAdapter(analysisBackend),
|
||||
new AggregationMapConfigAdapter(pgConnection),
|
||||
new MapConfigOverviewsAdapter(overviewsMetadataApi, filterStatsApi),
|
||||
new TurboCartoAdapter()
|
||||
);
|
||||
|
||||
@@ -21,9 +21,25 @@ module.exports.extractTableNames = function extractTableNames(query) {
|
||||
].join('');
|
||||
};
|
||||
|
||||
module.exports.getQueryRowCount = function getQueryRowEstimation(query) {
|
||||
return 'select CDB_EstimateRowCount($$' + query + '$$) as rows';
|
||||
};
|
||||
function getQueryRowEstimation(query) {
|
||||
return 'select CDB_EstimateRowCount($windshaft$' + query + '$windshaft$) as rows';
|
||||
}
|
||||
module.exports.getQueryRowCount = getQueryRowEstimation;
|
||||
|
||||
module.exports.getAggregationMetadata = ctx => `
|
||||
WITH
|
||||
rowEstimation AS (
|
||||
${getQueryRowEstimation(ctx.query)}
|
||||
),
|
||||
geometryType AS (
|
||||
SELECT ST_GeometryType(the_geom) as geom_type
|
||||
FROM (${ctx.query}) AS __cdb_query WHERE the_geom IS NOT NULL LIMIT 1
|
||||
)
|
||||
SELECT
|
||||
rows AS count,
|
||||
geom_type AS type
|
||||
FROM rowEstimation, geometryType;
|
||||
`;
|
||||
|
||||
/** Cast the column to epoch */
|
||||
module.exports.columnCastTpl = function columnCastTpl(ctx) {
|
||||
@@ -54,4 +70,4 @@ module.exports.countNaNs = function countNaNs(ctx) {
|
||||
return `${!ctx.isFloatColumn ? `0` :
|
||||
`sum(CASE WHEN (${ctx.column} = 'NaN'::float) THEN 1 ELSE 0 END)`
|
||||
}`;
|
||||
};
|
||||
};
|
||||
|
||||
12
package.json
12
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "4.3.1",
|
||||
"version": "4.7.0",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
@@ -19,11 +19,13 @@
|
||||
"Sandro Santilli <strk@vizzuality.com>",
|
||||
"Carlos Matallín <matallo@carto.com>",
|
||||
"Daniel Garcia Aubert <dgaubert@carto.com>",
|
||||
"Mario de Frutos <mario.defrutos@carto.com>"
|
||||
"Mario de Frutos <mario.defrutos@carto.com>",
|
||||
"Ivan Malagon <ivan@carto.com>",
|
||||
"Simon Martin <simon@carto.com>"
|
||||
],
|
||||
"dependencies": {
|
||||
"body-parser": "^1.18.2",
|
||||
"camshaft": "0.59.4",
|
||||
"camshaft": "0.60.0",
|
||||
"cartodb-psql": "0.10.2",
|
||||
"cartodb-query-tables": "0.3.0",
|
||||
"cartodb-redis": "0.14.0",
|
||||
@@ -44,7 +46,7 @@
|
||||
"step-profiler": "~0.3.0",
|
||||
"turbo-carto": "0.20.2",
|
||||
"underscore": "~1.6.0",
|
||||
"windshaft": "4.1.0",
|
||||
"windshaft": "4.2.0",
|
||||
"yargs": "~5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -63,7 +65,7 @@
|
||||
"update-internal-deps": "rm -rf node_modules && rm -f yarn.lock && yarn",
|
||||
"docker-install": "sudo apt install docker.io && sudo usermod -aG docker $(whoami)",
|
||||
"docker-pull": "docker pull cartoimages/windshaft-testing",
|
||||
"docker-test": "docker run -v `pwd`:/srv cartoimages/windshaft-testing bash docker-test.sh",
|
||||
"docker-test": "docker run -v `pwd`:/srv cartoimages/windshaft-testing bash docker-test.sh && docker ps --filter status=dead --filter status=exited -aq | xargs -r docker rm -v",
|
||||
"docker-bash": "docker run -it -v `pwd`:/srv cartoimages/windshaft-testing bash",
|
||||
"docker-publish": "docker push cartoimages/windshaft-carto-testing"
|
||||
},
|
||||
|
||||
1371
test/acceptance/aggregation.js
Normal file
1371
test/acceptance/aggregation.js
Normal file
File diff suppressed because it is too large
Load Diff
150
test/acceptance/analysis/analyses-filters-params.js
Normal file
150
test/acceptance/analysis/analyses-filters-params.js
Normal file
@@ -0,0 +1,150 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
const assert = require('../../support/assert');
|
||||
const TestClient = require('../../support/test-client');
|
||||
|
||||
describe('analysis-filters-params', () => {
|
||||
|
||||
const CARTOCSS = `#layer {
|
||||
marker-fill-opacity: 1;
|
||||
marker-line-color: white;
|
||||
marker-line-width: 0.5;
|
||||
marker-line-opacity: 1;
|
||||
marker-placement: point;
|
||||
marker-type: ellipse;
|
||||
marker-width: 8;
|
||||
marker-fill: red;
|
||||
marker-allow-overlap: true;
|
||||
}`;
|
||||
|
||||
const mapConfig = {
|
||||
version: '1.6.0',
|
||||
layers: [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "a1"
|
||||
},
|
||||
"cartocss": CARTOCSS,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
pop_max_histogram: {
|
||||
source: {
|
||||
id: 'a1'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
},
|
||||
pop_min_histogram: {
|
||||
source: {
|
||||
id: 'a1'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_min'
|
||||
}
|
||||
}
|
||||
},
|
||||
analyses: [
|
||||
{
|
||||
"id": "a1",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var params = {
|
||||
filters: {
|
||||
dataviews: {
|
||||
pop_max_histogram: {
|
||||
min: 2e6
|
||||
},
|
||||
pop_min_histogram: {
|
||||
max: 2e6
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
it('should get a filtered histogram dataview with all filters', function(done) {
|
||||
const testClient = new TestClient(mapConfig, 1234);
|
||||
const testParams = Object.assign({}, params, {
|
||||
own_filter: 1
|
||||
});
|
||||
|
||||
testClient.getDataview('pop_max_histogram', testParams, (err, dataview) => {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.bins_count, 6);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should get a filtered histogram dataview with all filters except my own filter', function(done) {
|
||||
const testClient = new TestClient(mapConfig, 1234);
|
||||
const testParams = Object.assign({}, params, {
|
||||
own_filter: 0
|
||||
});
|
||||
|
||||
testClient.getDataview('pop_max_histogram', testParams, (err, dataview) => {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.bins_count, 24);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should get a filtered histogram dataview without filters', function(done) {
|
||||
const testClient = new TestClient(mapConfig, 1234);
|
||||
const testParams = Object.assign({}, params, {
|
||||
no_filters: 1
|
||||
});
|
||||
|
||||
testClient.getDataview('pop_max_histogram', testParams, (err, dataview) => {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.bins_count, 48);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error if both no_filters and own_filter params are present', function (done) {
|
||||
const testClient = new TestClient(mapConfig, 1234);
|
||||
const expectedError = {
|
||||
errors: ['Both own_filter and no_filters cannot be sent in the same request'],
|
||||
errors_with_context: [{
|
||||
type: 'dataview',
|
||||
message: 'Both own_filter and no_filters cannot be sent in the same request'
|
||||
}]
|
||||
};
|
||||
const testParams = Object.assign({}, params, {
|
||||
no_filters: 1,
|
||||
own_filter: 0,
|
||||
response: {
|
||||
status: 400
|
||||
}
|
||||
});
|
||||
|
||||
testClient.getDataview('pop_max_histogram', testParams, (err, dataview) => {
|
||||
assert.deepEqual(dataview, expectedError);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
84
test/acceptance/analysis/analyses-filters.js
Normal file
84
test/acceptance/analysis/analyses-filters.js
Normal file
@@ -0,0 +1,84 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
const assert = require('../../support/assert');
|
||||
const TestClient = require('../../support/test-client');
|
||||
|
||||
describe('analysis-layers-dataviews', () => {
|
||||
|
||||
const CARTOCSS = `#layer {
|
||||
marker-fill-opacity: 1;
|
||||
marker-line-color: white;
|
||||
marker-line-width: 0.5;
|
||||
marker-line-opacity: 1;
|
||||
marker-placement: point;
|
||||
marker-type: ellipse;
|
||||
marker-width: 8;
|
||||
marker-fill: red;
|
||||
marker-allow-overlap: true;
|
||||
}`;
|
||||
|
||||
const mapConfig = {
|
||||
version: '1.6.0',
|
||||
layers: [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "a1"
|
||||
},
|
||||
"cartocss": CARTOCSS,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
pop_max_histogram: {
|
||||
source: {
|
||||
id: 'a1'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
analyses: [
|
||||
{
|
||||
"id": "a1",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
it('should get a filtered histogram dataview', function(done) {
|
||||
const testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
const params = {
|
||||
filters: {
|
||||
analyses: {
|
||||
'a1': [
|
||||
{
|
||||
type: 'range',
|
||||
column: 'pop_max',
|
||||
params: {
|
||||
min: 2e6
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
testClient.getDataview('pop_max_histogram', params, (err, dataview) => {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.bins_start, 2008000);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -109,7 +109,8 @@ describe('analysis-layers-dataviews', function() {
|
||||
min: 2e6
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
own_filter: 1
|
||||
};
|
||||
|
||||
testClient.getDataview('pop_max_histogram', params, function(err, dataview) {
|
||||
|
||||
@@ -174,7 +174,9 @@ describe('analysis-layers', function() {
|
||||
}
|
||||
};
|
||||
|
||||
testClient.getLayergroup(PERMISSION_DENIED_RESPONSE, function(err, layergroupResult) {
|
||||
|
||||
|
||||
testClient.getLayergroup({ response: PERMISSION_DENIED_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(
|
||||
layergroupResult.errors,
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('analysis-layers error cases', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
@@ -97,7 +97,7 @@ describe('analysis-layers error cases', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
@@ -144,7 +144,7 @@ describe('analysis-layers error cases', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
@@ -190,7 +190,7 @@ describe('analysis-layers error cases', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 11111);
|
||||
|
||||
testClient.getLayergroup(AUTH_ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: AUTH_ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
@@ -246,7 +246,7 @@ describe('analysis-layers error cases', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
@@ -298,7 +298,7 @@ describe('analysis-layers error cases', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
@@ -351,7 +351,7 @@ describe('analysis-layers error cases', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
@@ -415,7 +415,7 @@ describe('analysis-layers error cases', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('histogram-dataview', function() {
|
||||
it('should fail when invalid dataviews object is provided, string case', function(done) {
|
||||
var mapConfig = createMapConfig("wadus-string");
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, errObj) {
|
||||
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, errObj) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.deepEqual(errObj.errors, [ '"dataviews" must be a valid JSON object: "string" type found' ]);
|
||||
@@ -63,7 +63,7 @@ describe('histogram-dataview', function() {
|
||||
it('should fail when invalid dataviews object is provided, array case', function(done) {
|
||||
var mapConfig = createMapConfig([]);
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, errObj) {
|
||||
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, errObj) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.deepEqual(errObj.errors, [ '"dataviews" must be a valid JSON object: "array" type found' ]);
|
||||
|
||||
@@ -124,7 +124,7 @@ describe('histogram-dataview for date column type', function() {
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "datetime-histogram-source"
|
||||
"id": "datetime-histogram-source-week"
|
||||
},
|
||||
"cartocss": "#points { marker-width: 10; marker-fill: red; }",
|
||||
"cartocss_version": "2.3.0"
|
||||
@@ -134,7 +134,7 @@ describe('histogram-dataview for date column type', function() {
|
||||
{
|
||||
datetime_histogram: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source'
|
||||
id: 'datetime-histogram-source-week'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
@@ -154,9 +154,109 @@ describe('histogram-dataview for date column type', function() {
|
||||
offset: -14400 // EDT Eastern Daylight Time (GMT-4) in seconds
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic: {
|
||||
datetime_histogram_automatic_second: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source'
|
||||
id: 'datetime-histogram-source-second'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_minute: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-minute'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_hour: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-hour'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_day: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-day'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_week: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-week'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_month: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-month'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_quarter: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-quarter'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_year: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-year'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_decade: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-decade'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_century: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-century'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'd',
|
||||
aggregation: 'auto'
|
||||
}
|
||||
},
|
||||
datetime_histogram_automatic_millennium: {
|
||||
source: {
|
||||
id: 'datetime-histogram-source-millennium'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
@@ -197,13 +297,144 @@ describe('histogram-dataview for date column type', function() {
|
||||
},
|
||||
[
|
||||
{
|
||||
"id": "datetime-histogram-source",
|
||||
"id": "datetime-histogram-source-second",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 01:00:00'::timestamp, '2008-04-09 01:00:00'::timestamp, '1 day'::interval",
|
||||
"'2007-02-15 01:00:00'::timestamp, '2007-02-15 01:00:57'::timestamp,",
|
||||
"'0.9 second'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-minute",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 01:00:00'::timestamp, '2007-02-15 02:00:57'::timestamp,",
|
||||
"'75 second'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-hour",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 01:00:00'::timestamp, '2007-02-18 02:00:57'::timestamp,",
|
||||
"'47 minutes'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-day",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 01:00:00'::timestamp, '2007-04-18 02:00:57'::timestamp,",
|
||||
"'24 hours'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-week",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 01:00:00'::timestamp, '2008-04-09 01:00:00'::timestamp,",
|
||||
"'1 day'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-month",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 01:00:00'::timestamp, '2010-04-09 01:00:00'::timestamp,",
|
||||
"'30 day'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-quarter",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 01:00:00'::timestamp, '2020-04-09 01:00:00'::timestamp,",
|
||||
"'30 day'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-year",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'1990-02-15 01:00:00'::timestamp, '2018-04-09 01:00:00'::timestamp,",
|
||||
"'30 day'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-decade",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'1850-02-15 01:00:00'::timestamp, '2018-04-09 01:00:00'::timestamp,",
|
||||
"'30 day'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-century",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'0650-02-15 01:00:00'::timestamp, '1918-04-09 01:00:00'::timestamp,",
|
||||
"'6 years'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "datetime-histogram-source-millennium",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'0005-02-15 01:00:00'::timestamp, '12000-04-09 01:00:00'::timestamp,",
|
||||
"'72 years'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
@@ -429,6 +660,47 @@ describe('histogram-dataview for date column type', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should aggregate histogram using "second" aggregation ' + test.desc, function (done) {
|
||||
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
|
||||
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
|
||||
var params = {
|
||||
offset: OFFSET_UTC_IN_SECONDS,
|
||||
aggregation: 'second'
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_second', params, function (err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
|
||||
assert.equal(dataview.bins.length, 57);
|
||||
|
||||
var initialTimestamp = '2007-02-15T01:00:00Z';
|
||||
var binsStartInMilliseconds = dataview.bins_start * 1000;
|
||||
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.format();
|
||||
assert.equal(binsStartFormatted, initialTimestamp);
|
||||
|
||||
dataview.bins.forEach(function (bin, index) {
|
||||
var binTimestampExpected = moment.utc(initialTimestamp)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.add(index, 'second')
|
||||
.format();
|
||||
var binsTimestampInMilliseconds = bin.timestamp * 1000;
|
||||
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.format();
|
||||
|
||||
assert.equal(binTimestampFormatted, binTimestampExpected);
|
||||
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
|
||||
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should aggregate histogram using "quarter" aggregation ' + test.desc, function (done) {
|
||||
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
|
||||
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
|
||||
@@ -470,6 +742,132 @@ describe('histogram-dataview for date column type', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should aggregate histogram using "decade" aggregation ' + test.desc, function (done) {
|
||||
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
|
||||
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
|
||||
var params = {
|
||||
offset: OFFSET_UTC_IN_SECONDS,
|
||||
aggregation: 'decade'
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_decade', params, function (err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
|
||||
assert.equal(dataview.bins.length, 17);
|
||||
|
||||
var initialTimestamp = '1850-01-01T00:00:00Z';
|
||||
var binsStartInMilliseconds = dataview.bins_start * 1000;
|
||||
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.format();
|
||||
assert.equal(binsStartFormatted, initialTimestamp);
|
||||
|
||||
dataview.bins.forEach(function (bin, index) {
|
||||
var binTimestampExpected = moment.utc(initialTimestamp)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.add(index * 10, 'year')
|
||||
.format();
|
||||
var binsTimestampInMilliseconds = bin.timestamp * 1000;
|
||||
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.format();
|
||||
|
||||
assert.equal(binTimestampFormatted, binTimestampExpected);
|
||||
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
|
||||
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should aggregate histogram using "century" aggregation ' + test.desc, function (done) {
|
||||
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
|
||||
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
|
||||
var params = {
|
||||
offset: OFFSET_UTC_IN_SECONDS,
|
||||
aggregation: 'century'
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_century', params, function (err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
|
||||
assert.equal(dataview.bins.length, 14);
|
||||
|
||||
var initialTimestamp = '0601-01-01T00:00:00Z';
|
||||
var binsStartInMilliseconds = dataview.bins_start * 1000;
|
||||
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.format();
|
||||
assert.equal(binsStartFormatted, initialTimestamp);
|
||||
|
||||
dataview.bins.forEach(function (bin, index) {
|
||||
var binTimestampExpected = moment.utc(initialTimestamp)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.add(index * 100, 'year')
|
||||
.format();
|
||||
var binsTimestampInMilliseconds = bin.timestamp * 1000;
|
||||
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.format();
|
||||
|
||||
assert.equal(binTimestampFormatted, binTimestampExpected);
|
||||
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
|
||||
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should aggregate histogram using "millennium" aggregation ' + test.desc, function (done) {
|
||||
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
|
||||
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
|
||||
var params = {
|
||||
offset: OFFSET_UTC_IN_SECONDS,
|
||||
aggregation: 'millennium'
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_millennium', params, function (err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
|
||||
assert.equal(dataview.bins.length, 12);
|
||||
|
||||
var initialTimestamp = '0001-01-01T00:00:00Z';
|
||||
var binsStartInMilliseconds = dataview.bins_start * 1000;
|
||||
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.format();
|
||||
assert.equal(binsStartFormatted, initialTimestamp);
|
||||
|
||||
dataview.bins.forEach(function (bin, index) {
|
||||
var binTimestampExpected = moment.utc(initialTimestamp)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.add(index * 1000, 'year')
|
||||
.format();
|
||||
var binsTimestampInMilliseconds = bin.timestamp * 1000;
|
||||
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
|
||||
.utcOffset(OFFSET_UTC_IN_MINUTES)
|
||||
.format();
|
||||
|
||||
assert.equal(binTimestampFormatted, binTimestampExpected);
|
||||
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
|
||||
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
it('bins_count should be equal to bins length filtered by start and end ' + test.desc, function (done) {
|
||||
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
|
||||
var params = {
|
||||
@@ -533,19 +931,129 @@ describe('histogram-dataview for date column type', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram', function (done) {
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: second', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic', params, function (err, dataview) {
|
||||
this.testClient.getDataview('datetime_histogram_automatic_second', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'week');
|
||||
assert.equal(dataview.bins.length, 61);
|
||||
assert.equal(dataview.bins_count, 61);
|
||||
assert.equal(dataview.aggregation, 'second');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: minute', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_minute', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'minute');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: hour', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_hour', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'hour');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: day', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_day', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'day');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: week', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_week', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'week');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: month', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_month', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'month');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: quarter', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_quarter', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'quarter');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: year', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_year', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'year');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: decade', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_decade', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'decade');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: century', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_century', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'century');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram: millennium', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_automatic_millennium', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'millennium');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
it('should work with dates', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
@@ -560,19 +1068,6 @@ describe('histogram-dataview for date column type', function() {
|
||||
});
|
||||
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram with dates', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('date_histogram_automatic', params, function (err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.aggregation, 'week');
|
||||
assert.equal(dataview.bins.length, 61);
|
||||
assert.equal(dataview.bins_count, 61);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not apply offset for a histogram aggregated by minutes', function (done) {
|
||||
var self = this;
|
||||
var params = {
|
||||
@@ -858,13 +1353,15 @@ describe('histogram-dates: aggregation input value', function() {
|
||||
|
||||
assert.deepEqual(dataviewError, {
|
||||
errors: [
|
||||
'Invalid aggregation value. Valid ones: auto, minute, hour, day, week, month, quarter, year'
|
||||
'Invalid aggregation value. Valid ones: auto, second, minute, ' +
|
||||
'hour, day, week, month, quarter, year, decade, century, millennium'
|
||||
],
|
||||
errors_with_context: [{
|
||||
type: 'unknown',
|
||||
message: [
|
||||
'Invalid aggregation value. ',
|
||||
'Valid ones: auto, minute, hour, day, week, month, quarter, year'
|
||||
'Valid ones: auto, second, minute, hour, day, week, month, ' +
|
||||
'quarter, year, decade, century, millennium'
|
||||
].join('')
|
||||
}]
|
||||
});
|
||||
@@ -887,13 +1384,15 @@ describe('histogram-dates: aggregation input value', function() {
|
||||
|
||||
assert.deepEqual(dataviewError, {
|
||||
errors: [
|
||||
'Invalid aggregation value. Valid ones: auto, minute, hour, day, week, month, quarter, year'
|
||||
'Invalid aggregation value. Valid ones: auto, second, minute, ' +
|
||||
'hour, day, week, month, quarter, year, decade, century, millennium'
|
||||
],
|
||||
errors_with_context: [{
|
||||
type: 'unknown',
|
||||
message: [
|
||||
'Invalid aggregation value. ',
|
||||
'Valid ones: auto, minute, hour, day, week, month, quarter, year'
|
||||
'Valid ones: auto, second, minute, hour, day, week, month, ' +
|
||||
'quarter, year, decade, century, millennium'
|
||||
].join('')
|
||||
}]
|
||||
});
|
||||
@@ -966,7 +1465,7 @@ describe('histogram-dates: timestamp starts at epoch', function() {
|
||||
const { aggregation, timestamp_start } = dataview;
|
||||
|
||||
assert.equal(timestamp_start, 0);
|
||||
assert.equal(aggregation, 'month');
|
||||
assert.equal(aggregation, 'quarter');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -393,7 +393,8 @@ describe('dataviews using tables with overviews', function() {
|
||||
var params = {
|
||||
filters: {
|
||||
dataviews: { test_histogram: { min: 2 } }
|
||||
}
|
||||
},
|
||||
own_filter: 1
|
||||
};
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_histogram', params, function (err, histogram) {
|
||||
@@ -412,7 +413,8 @@ describe('dataviews using tables with overviews', function() {
|
||||
var params = {
|
||||
filters: {
|
||||
dataviews: { test_histogram: { max: -1 } }
|
||||
}
|
||||
},
|
||||
own_filter: 1
|
||||
};
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_histogram', params, function (err, histogram) {
|
||||
@@ -433,7 +435,8 @@ describe('dataviews using tables with overviews', function() {
|
||||
var params = {
|
||||
filters: {
|
||||
dataviews: { test_histogram_date: { max: -1 } }
|
||||
}
|
||||
},
|
||||
own_filter: 1
|
||||
};
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_histogram_date', params, function (err, histogram) {
|
||||
|
||||
42
test/acceptance/error-middleware.js
Normal file
42
test/acceptance/error-middleware.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const assert = require('../support/assert');
|
||||
const TestClient = require('../support/test-client');
|
||||
|
||||
describe('error middleware', function () {
|
||||
it('should returns a errors header', function (done) {
|
||||
const mapConfig = {
|
||||
version: '1.6.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {}
|
||||
}]
|
||||
};
|
||||
|
||||
const errorHeader = {
|
||||
mainError: {
|
||||
statusCode: 400,
|
||||
message: "Missing cartocss for layer 0 options",
|
||||
name: "Error",
|
||||
label: "ANONYMOUS LAYERGROUP",
|
||||
type: "layer",
|
||||
},
|
||||
moreErrors: []
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
const params = {
|
||||
response: {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'X-Tiler-Errors': JSON.stringify(errorHeader)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(params, (err) => {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
54
test/acceptance/layers-filters.js
Normal file
54
test/acceptance/layers-filters.js
Normal file
@@ -0,0 +1,54 @@
|
||||
require('../support/test_helper');
|
||||
var TestClient = require('../support/test-client');
|
||||
|
||||
describe('layers filters', function() {
|
||||
const type = 'mapnik';
|
||||
const sql = 'select * from populated_places_simple_reduced';
|
||||
const cartocss = `#points {
|
||||
marker-fill-opacity: 1.0;
|
||||
marker-line-color: #FFF;
|
||||
marker-line-width: 0.5;
|
||||
marker-line-opacity: 1.0;
|
||||
marker-placement: point;
|
||||
marker-type: ellipse;
|
||||
marker-width: 8;
|
||||
marker-fill: red;
|
||||
marker-allow-overlap: true;
|
||||
}`;
|
||||
const cartocss_version = '3.0.12';
|
||||
const options = {
|
||||
sql,
|
||||
cartocss,
|
||||
cartocss_version
|
||||
};
|
||||
|
||||
const mapConfig = {
|
||||
version: '1.6.0',
|
||||
layers: [
|
||||
{
|
||||
type,
|
||||
id: 'layerA',
|
||||
options
|
||||
},
|
||||
{
|
||||
type,
|
||||
id: 'layerB',
|
||||
options
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
}
|
||||
});
|
||||
|
||||
['layerA', 'layerB'].forEach(layer => {
|
||||
it(`should work for individual layer ids: ${layer}`, function (done) {
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
this.testClient.getTile(0, 0, 0, { layers: layer }, done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -151,7 +151,7 @@ describe('multilayer error cases', function() {
|
||||
};
|
||||
ServerOptions.afterLayergroupCreateCalls = 0;
|
||||
this.client = new TestClient(layergroup);
|
||||
this.client.getLayergroup({status: 400}, function(err, parsed) {
|
||||
this.client.getLayergroup({ response: { status: 400 } }, function(err, parsed) {
|
||||
assert.ok(!err, err);
|
||||
// See http://github.com/CartoDB/Windshaft/issues/159
|
||||
assert.equal(ServerOptions.afterLayergroupCreateCalls, 0);
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('regressions', function() {
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
|
||||
204
test/acceptance/tilejson.js
Normal file
204
test/acceptance/tilejson.js
Normal file
@@ -0,0 +1,204 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
const assert = require('../support/assert');
|
||||
const TestClient = require('../support/test-client');
|
||||
|
||||
describe('tilejson', function() {
|
||||
|
||||
function tilejsonValidation(tilejson, shouldHaveGrid = false) {
|
||||
assert.equal(tilejson.tilejson, '2.2.0');
|
||||
|
||||
assert.ok(Array.isArray(tilejson.tiles));
|
||||
assert.ok(tilejson.tiles.length > 0);
|
||||
|
||||
if (shouldHaveGrid) {
|
||||
assert.ok(Array.isArray(tilejson.grids));
|
||||
assert.ok(tilejson.grids.length > 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const sql = 'SELECT * FROM populated_places_simple_reduced';
|
||||
const cartocss = TestClient.CARTOCSS.POINTS;
|
||||
const cartocss_version = '3.0.12';
|
||||
|
||||
const RASTER_LAYER = {
|
||||
options: {
|
||||
sql, cartocss, cartocss_version
|
||||
}
|
||||
};
|
||||
const RASTER_INTERACTIVITY_LAYER = {
|
||||
options: {
|
||||
sql, cartocss, cartocss_version,
|
||||
interactivity: ['cartodb_id']
|
||||
}
|
||||
};
|
||||
const VECTOR_LAYER = {
|
||||
options: {
|
||||
sql
|
||||
}
|
||||
};
|
||||
const PLAIN_LAYER = {
|
||||
type: 'plain',
|
||||
options: {
|
||||
color: '#000000'
|
||||
}
|
||||
};
|
||||
|
||||
function mapConfig(layers) {
|
||||
return {
|
||||
version: '1.7.0',
|
||||
layers: Array.isArray(layers) ? layers : [layers]
|
||||
};
|
||||
}
|
||||
|
||||
describe('per layer', function() {
|
||||
it('should expose raster + vector tilejson for raster layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(RASTER_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 1);
|
||||
|
||||
const layer = metadata.layers[0];
|
||||
assert.deepEqual(Object.keys(layer.tilejson), ['vector', 'raster']);
|
||||
|
||||
Object.keys(layer.tilejson).forEach(k => {
|
||||
tilejsonValidation(layer.tilejson[k]);
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose just the vector tilejson vector only layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(VECTOR_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 1);
|
||||
|
||||
const layer = metadata.layers[0];
|
||||
assert.deepEqual(Object.keys(layer.tilejson), ['vector']);
|
||||
|
||||
Object.keys(layer.tilejson).forEach(k => {
|
||||
tilejsonValidation(layer.tilejson[k]);
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose just the raster tilejson plain layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(PLAIN_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 1);
|
||||
|
||||
const layer = metadata.layers[0];
|
||||
assert.deepEqual(Object.keys(layer.tilejson), ['raster']);
|
||||
|
||||
Object.keys(layer.tilejson).forEach(k => {
|
||||
tilejsonValidation(layer.tilejson[k]);
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose grids for the raster layer with interactivity', function(done) {
|
||||
var testClient = new TestClient(mapConfig(RASTER_INTERACTIVITY_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 1);
|
||||
|
||||
const layer = metadata.layers[0];
|
||||
assert.deepEqual(Object.keys(layer.tilejson), ['vector', 'raster']);
|
||||
|
||||
tilejsonValidation(layer.tilejson.vector);
|
||||
tilejsonValidation(layer.tilejson.raster, true);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with several layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig([RASTER_LAYER, RASTER_INTERACTIVITY_LAYER]));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 2);
|
||||
|
||||
assert.deepEqual(Object.keys(metadata.layers[0].tilejson), ['vector', 'raster']);
|
||||
tilejsonValidation(metadata.layers[0].tilejson.vector);
|
||||
tilejsonValidation(metadata.layers[0].tilejson.raster);
|
||||
|
||||
assert.deepEqual(Object.keys(metadata.layers[1].tilejson), ['vector', 'raster']);
|
||||
tilejsonValidation(metadata.layers[1].tilejson.vector);
|
||||
tilejsonValidation(metadata.layers[1].tilejson.raster, true);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('root tilejson', function() {
|
||||
|
||||
it('should expose just the `vector` tilejson when for vector only mapnik layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(VECTOR_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
const tilejson = metadata.tilejson;
|
||||
assert.deepEqual(Object.keys(tilejson), ['vector']);
|
||||
|
||||
Object.keys(tilejson).forEach(k => {
|
||||
tilejsonValidation(tilejson[k]);
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose just the `vector` and `raster` tilejson for mapnik layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(RASTER_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
const tilejson = metadata.tilejson;
|
||||
assert.deepEqual(Object.keys(tilejson), ['vector', 'raster']);
|
||||
|
||||
Object.keys(tilejson).forEach(k => {
|
||||
tilejsonValidation(tilejson[k]);
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -51,7 +51,7 @@ describe('turbo-carto error cases', function() {
|
||||
|
||||
it('should return invalid number of ramp error', function(done) {
|
||||
this.testClient = new TestClient(makeMapconfig('ramp([pop_max], 8, 96, 3, (8,24,96,128))'));
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
|
||||
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup.hasOwnProperty('errors'));
|
||||
@@ -65,7 +65,7 @@ describe('turbo-carto error cases', function() {
|
||||
|
||||
it('should return invalid column from datasource', function(done) {
|
||||
this.testClient = new TestClient(makeMapconfig(null, 'ramp([wadus_column], (red, green, blue))'));
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
|
||||
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup.hasOwnProperty('errors'));
|
||||
@@ -80,7 +80,7 @@ describe('turbo-carto error cases', function() {
|
||||
|
||||
it('should return invalid method from datasource', function(done) {
|
||||
this.testClient = new TestClient(makeMapconfig(null, 'ramp([wadus_column], (red, green, blue), wadusmethod)'));
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
|
||||
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup.hasOwnProperty('errors'));
|
||||
@@ -95,7 +95,7 @@ describe('turbo-carto error cases', function() {
|
||||
|
||||
it('should fail by falling back to normal carto parser', function(done) {
|
||||
this.testClient = new TestClient(makeMapconfig('ramp([price], (8,24,96), (8,24,96));//(red, green, blue))'));
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
|
||||
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup.hasOwnProperty('errors'));
|
||||
@@ -109,7 +109,7 @@ describe('turbo-carto error cases', function() {
|
||||
|
||||
it('turbo-carto: should return error invalid column from datasource with some context', function(done) {
|
||||
this.testClient = new TestClient(makeMapconfig(null, 'ramp([wadus_column], (red, green, blue))'));
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
|
||||
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup.hasOwnProperty('errors'));
|
||||
@@ -164,7 +164,7 @@ describe('turbo-carto error cases', function() {
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(multipleErrorsMapConfig);
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
|
||||
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup.hasOwnProperty('errors'));
|
||||
|
||||
@@ -79,7 +79,7 @@ describe('turbo-carto regressions', function() {
|
||||
].join('\n');
|
||||
|
||||
this.testClient = new TestClient(makeMapconfig('SELECT * FROM test_table_private_1', cartocss));
|
||||
this.testClient.getLayergroup(TestClient.RESPONSE.ERROR, function(err, layergroup) {
|
||||
this.testClient.getLayergroup({ response: TestClient.RESPONSE.ERROR }, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(!layergroup.hasOwnProperty('layergroupid'));
|
||||
|
||||
@@ -138,7 +138,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
@@ -177,7 +177,7 @@ describe('user database timeout limit', function () {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
@@ -259,7 +259,7 @@ describe('user database timeout limit', function () {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
@@ -360,7 +360,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
@@ -387,7 +387,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
@@ -426,16 +426,16 @@ describe('user database timeout limit', function () {
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, tile) => {
|
||||
assert.ifError(err);
|
||||
|
||||
|
||||
var tileJSON = tile.toJSON();
|
||||
assert.equal(Array.isArray(tileJSON), true);
|
||||
assert.equal(tileJSON.length, 2);
|
||||
assert.equal(tileJSON[0].name, 'errorTileSquareLayer');
|
||||
assert.equal(tileJSON[1].name, 'errorTileStripesLayer');
|
||||
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -467,7 +467,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
@@ -494,7 +494,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
@@ -571,7 +571,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
@@ -601,7 +601,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
@@ -702,7 +702,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
|
||||
assert.deepEqual(timeoutError, {
|
||||
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
|
||||
errors_with_context: [{
|
||||
@@ -740,7 +740,7 @@ describe('user database timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, res) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('user render timeout limit', function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
|
||||
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, {
|
||||
@@ -245,7 +245,7 @@ describe('user render timeout limit', function () {
|
||||
assert.equal(tileJSON.length, 2);
|
||||
assert.equal(tileJSON[0].name, 'errorTileSquareLayer');
|
||||
assert.equal(tileJSON[1].name, 'errorTileStripesLayer');
|
||||
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -399,4 +399,3 @@ describe('user render timeout limit', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ suites.forEach((suite) => {
|
||||
this.testClient.mapConfig.layers[0].options.cartocss = cartocss;
|
||||
this.testClient.mapConfig.layers[0].options.cartocss_version = cartocssVersion;
|
||||
|
||||
this.testClient.getLayergroup(response, (err, body) => {
|
||||
this.testClient.getLayergroup({ response }, (err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,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 CDB_EstimateRowCount CDB_RectangleGrid'
|
||||
|
||||
CURL_ARGS=""
|
||||
for i in ${REMOTE_SQL_SCRIPTS}
|
||||
@@ -99,7 +99,7 @@ if test x"$PREPARE_PGSQL" = xyes; then
|
||||
sed -e 's/PARALLEL \= [A-Z]*,/''/g' \
|
||||
-e 's/PARALLEL [A-Z]*/''/g' sql/$i.sql > $TMPFILE
|
||||
mv $TMPFILE sql/$i.sql
|
||||
fi
|
||||
fi
|
||||
cat sql/${i}.sql |
|
||||
sed -e 's/cartodb\./public./g' -e "s/''cartodb''/''public''/g" |
|
||||
sed "s/:PUBLICUSER/${PUBLICUSER}/" |
|
||||
|
||||
@@ -411,9 +411,13 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
var urlParams = {
|
||||
own_filter: params.hasOwnProperty('own_filter') ? params.own_filter : 1
|
||||
};
|
||||
var urlParams = {};
|
||||
if (params.hasOwnProperty('no_filters')) {
|
||||
urlParams.no_filters = params.no_filters;
|
||||
}
|
||||
if (params.hasOwnProperty('own_filter')) {
|
||||
urlParams.own_filter = params.own_filter;
|
||||
}
|
||||
|
||||
['bbox', 'bins', 'start', 'end', 'aggregation', 'offset', 'categories'].forEach(function(extraParam) {
|
||||
if (params.hasOwnProperty(extraParam)) {
|
||||
@@ -618,8 +622,19 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
}
|
||||
|
||||
var data = templateId ? params.placeholders : self.mapConfig;
|
||||
|
||||
const queryParams = {};
|
||||
|
||||
if (self.apiKey) {
|
||||
queryParams.api_key = self.apiKey;
|
||||
}
|
||||
|
||||
if (params.aggregation !== undefined) {
|
||||
queryParams.aggregation = params.aggregation;
|
||||
}
|
||||
|
||||
var path = templateId ?
|
||||
urlNamed + '/' + templateId + '?' + qs.stringify({api_key: self.apiKey}) :
|
||||
urlNamed + '/' + templateId + '?' + qs.stringify(queryParams) :
|
||||
url;
|
||||
|
||||
assert.response(self.server,
|
||||
@@ -647,7 +662,7 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
);
|
||||
},
|
||||
function getTileResult(err, layergroupId) {
|
||||
// jshint maxcomplexity:12
|
||||
// jshint maxcomplexity:13
|
||||
assert.ifError(err);
|
||||
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
@@ -671,8 +686,14 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
url += [z,x,y].join('/');
|
||||
url += '.' + format;
|
||||
|
||||
const queryParams = {};
|
||||
|
||||
if (self.apiKey) {
|
||||
url += '?' + qs.stringify({api_key: self.apiKey});
|
||||
queryParams.api_key = self.apiKey;
|
||||
}
|
||||
|
||||
if (Object.keys(queryParams).length) {
|
||||
url += '?' + qs.stringify(queryParams);
|
||||
}
|
||||
|
||||
var request = {
|
||||
@@ -754,12 +775,21 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
|
||||
TestClient.prototype.getLayergroup = function (params, callback) {
|
||||
// jshint maxcomplexity: 7
|
||||
var self = this;
|
||||
|
||||
if (!callback) {
|
||||
callback = expectedResponse;
|
||||
expectedResponse = {
|
||||
callback = params;
|
||||
params = null;
|
||||
}
|
||||
|
||||
if (!params) {
|
||||
params = {};
|
||||
}
|
||||
|
||||
if (!params.response) {
|
||||
params.response = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
@@ -769,8 +799,18 @@ TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
|
||||
|
||||
var url = '/api/v1/map';
|
||||
|
||||
if (this.apiKey) {
|
||||
url += '?' + qs.stringify({api_key: this.apiKey});
|
||||
const queryParams = {};
|
||||
|
||||
if (self.apiKey) {
|
||||
queryParams.api_key = self.apiKey;
|
||||
}
|
||||
|
||||
if (params.aggregation !== undefined) {
|
||||
queryParams.aggregation = params.aggregation;
|
||||
}
|
||||
|
||||
if (Object.keys(queryParams).length) {
|
||||
url += '?' + qs.stringify(queryParams);
|
||||
}
|
||||
|
||||
assert.response(self.server,
|
||||
@@ -783,7 +823,7 @@ TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
|
||||
},
|
||||
data: JSON.stringify(self.mapConfig)
|
||||
},
|
||||
expectedResponse,
|
||||
params.response,
|
||||
function(res, err) {
|
||||
var parsedBody;
|
||||
// If there is a response, we are still interested in catching the created keys
|
||||
|
||||
@@ -75,6 +75,22 @@ function checkSurrogateKey(res, expectedKey) {
|
||||
assert.deepEqual(keys, expectedKeys);
|
||||
}
|
||||
|
||||
var uncaughtExceptions = [];
|
||||
process.on('uncaughtException', function(err) {
|
||||
uncaughtExceptions.push(err);
|
||||
});
|
||||
beforeEach(function() {
|
||||
uncaughtExceptions = [];
|
||||
});
|
||||
//global afterEach to capture uncaught exceptions
|
||||
afterEach(function() {
|
||||
assert.equal(
|
||||
uncaughtExceptions.length,
|
||||
0,
|
||||
'uncaughtException:\n\n' + uncaughtExceptions.map(err => err.stack).join('\n\n'));
|
||||
});
|
||||
|
||||
|
||||
var redisClient;
|
||||
|
||||
beforeEach(function() {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
require('../../support/test_helper.js');
|
||||
|
||||
var assert = require('assert');
|
||||
var errorMiddleware = require('../../../lib/cartodb/middleware/error-middleware');
|
||||
|
||||
describe('error-middleware', function() {
|
||||
|
||||
it('different formats for postgis plugin error returns 400 as status code', function() {
|
||||
|
||||
var expectedStatusCode = 400;
|
||||
assert.equal(
|
||||
errorMiddleware.findStatusCode("Postgis Plugin: ERROR: column \"missing\" does not exist\n"),
|
||||
expectedStatusCode,
|
||||
"Error status code for single line does not match"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
errorMiddleware.findStatusCode("Postgis Plugin: PSQL error:\nERROR: column \"missing\" does not exist\n"),
|
||||
expectedStatusCode,
|
||||
"Error status code for multiline/PSQL does not match"
|
||||
);
|
||||
});
|
||||
});
|
||||
178
test/unit/cartodb/error-middleware.test.js
Normal file
178
test/unit/cartodb/error-middleware.test.js
Normal file
@@ -0,0 +1,178 @@
|
||||
require('../../support/test_helper.js');
|
||||
|
||||
var assert = require('assert');
|
||||
var errorMiddleware = require('../../../lib/cartodb/middleware/error-middleware');
|
||||
|
||||
describe('error-middleware', function() {
|
||||
|
||||
it('different formats for postgis plugin error returns 400 as status code', function() {
|
||||
|
||||
var expectedStatusCode = 400;
|
||||
assert.equal(
|
||||
errorMiddleware.findStatusCode("Postgis Plugin: ERROR: column \"missing\" does not exist\n"),
|
||||
expectedStatusCode,
|
||||
"Error status code for single line does not match"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
errorMiddleware.findStatusCode("Postgis Plugin: PSQL error:\nERROR: column \"missing\" does not exist\n"),
|
||||
expectedStatusCode,
|
||||
"Error status code for multiline/PSQL does not match"
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a header with errors', function (done) {
|
||||
const error = new Error('error test');
|
||||
error.label = 'test label';
|
||||
error.type = 'test type';
|
||||
error.subtype = 'test subtype';
|
||||
|
||||
const errors = [error, error];
|
||||
|
||||
const req = {};
|
||||
const res = {
|
||||
headers: {},
|
||||
set (key, value) {
|
||||
this.headers[key] = value;
|
||||
},
|
||||
statusCode: 0,
|
||||
status (status) {
|
||||
this.statusCode = status;
|
||||
},
|
||||
json () {},
|
||||
send () {}
|
||||
};
|
||||
|
||||
const errorHeader = {
|
||||
mainError: {
|
||||
statusCode: 400,
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
label: error.label,
|
||||
type: error.type,
|
||||
subtype: error.subtype,
|
||||
},
|
||||
moreErrors: [{
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
label: error.label,
|
||||
type: error.type,
|
||||
subtype: error.subtype
|
||||
}]
|
||||
};
|
||||
|
||||
const errorFn = errorMiddleware();
|
||||
errorFn(errors, req, res);
|
||||
|
||||
assert.deepEqual(res.headers, {
|
||||
'X-Tiler-Errors': JSON.stringify(errorHeader)
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('JSONP should return a header with error status code', function (done) {
|
||||
const error = new Error('error test');
|
||||
error.label = 'test label';
|
||||
error.type = 'test type';
|
||||
error.subtype = 'test subtype';
|
||||
|
||||
const errors = [error, error];
|
||||
|
||||
const req = {
|
||||
query: { callback: true }
|
||||
};
|
||||
const res = {
|
||||
headers: {},
|
||||
set (key, value) {
|
||||
this.headers[key] = value;
|
||||
},
|
||||
statusCode: 0,
|
||||
status (status) {
|
||||
this.statusCode = status;
|
||||
},
|
||||
jsonp () {},
|
||||
send () {}
|
||||
};
|
||||
|
||||
const errorHeader = {
|
||||
mainError: {
|
||||
statusCode: 400,
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
label: error.label,
|
||||
type: error.type,
|
||||
subtype: error.subtype,
|
||||
},
|
||||
moreErrors: [{
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
label: error.label,
|
||||
type: error.type,
|
||||
subtype: error.subtype
|
||||
}]
|
||||
};
|
||||
|
||||
const errorFn = errorMiddleware();
|
||||
errorFn(errors, req, res);
|
||||
|
||||
assert.deepEqual(res.headers, {
|
||||
'X-Tiler-Errors': JSON.stringify(errorHeader)
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should escape chars that broke logs regex', function (done) {
|
||||
const badString = 'error: ( ) = " \" \' * $ & |';
|
||||
const escapedString = 'error ';
|
||||
|
||||
const error = new Error(badString);
|
||||
error.label = badString;
|
||||
error.type = badString;
|
||||
error.subtype = badString;
|
||||
|
||||
const errors = [error, error];
|
||||
|
||||
const req = {};
|
||||
const res = {
|
||||
headers: {},
|
||||
set (key, value) {
|
||||
this.headers[key] = value;
|
||||
},
|
||||
statusCode: 0,
|
||||
status (status) {
|
||||
this.statusCode = status;
|
||||
},
|
||||
json () {},
|
||||
send () {}
|
||||
};
|
||||
|
||||
const errorHeader = {
|
||||
mainError: {
|
||||
statusCode: 400,
|
||||
message: escapedString,
|
||||
name: error.name,
|
||||
label: escapedString,
|
||||
type: escapedString,
|
||||
subtype: escapedString,
|
||||
},
|
||||
moreErrors: [{
|
||||
message: escapedString,
|
||||
name: error.name,
|
||||
label: escapedString,
|
||||
type: escapedString,
|
||||
subtype: escapedString
|
||||
}]
|
||||
};
|
||||
|
||||
const errorFn = errorMiddleware();
|
||||
errorFn(errors, req, res);
|
||||
|
||||
assert.deepEqual(res.headers, {
|
||||
'X-Tiler-Errors': JSON.stringify(errorHeader)
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -3,9 +3,10 @@ require('../../../support/test_helper');
|
||||
var assert = require('../../../support/assert');
|
||||
var ResourceLocator = require('../../../../lib/cartodb/models/resource-locator');
|
||||
|
||||
describe('ResourceLocator.getUrls', function() {
|
||||
describe('ResourceLocator', function() {
|
||||
var USERNAME = 'username';
|
||||
var RESOURCE = 'wadus';
|
||||
var TILE_RESOURCE = 'wadus/{z}/{x}/{y}.png';
|
||||
var HTTP_SUBDOMAINS = ['1', '2', '3', '4'];
|
||||
var HTTPS_SUBDOMAINS = ['a', 'b', 'c', 'd'];
|
||||
|
||||
@@ -15,118 +16,221 @@ describe('ResourceLocator.getUrls', function() {
|
||||
assert.ok(urls);
|
||||
});
|
||||
|
||||
var BASIC_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com'
|
||||
}
|
||||
}
|
||||
};
|
||||
it('should return default urls when basic http and https domains are provided', function() {
|
||||
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
describe('basic', function() {
|
||||
|
||||
assert.equal(urls.http, ['http://cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
|
||||
assert.equal(urls.https, ['https://cdn.ssl.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
|
||||
var BASIC_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('getUrls', function() {
|
||||
it('should return default urls when basic http and https domains are provided', function() {
|
||||
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.equal(urls.http, ['http://cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
|
||||
assert.equal(urls.https, ['https://cdn.ssl.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTileUrls', function() {
|
||||
it('should return default urls when basic http and https domains are provided', function() {
|
||||
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(
|
||||
urls.http,
|
||||
[`http://cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`]
|
||||
);
|
||||
assert.deepEqual(
|
||||
urls.https,
|
||||
[`https://cdn.ssl.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var RESOURCE_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com'
|
||||
}
|
||||
},
|
||||
resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan/api/v1/map',
|
||||
https: 'https://{{=it.user}}.ssl.localhost.lan/api/v1/map'
|
||||
}
|
||||
};
|
||||
it('resources_url_templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
describe('resource templates', function() {
|
||||
|
||||
assert.equal(urls.http, ['http://' + USERNAME + '.localhost.lan', 'api/v1/map', RESOURCE].join('/'));
|
||||
assert.equal(urls.https, ['https://' + USERNAME + '.ssl.localhost.lan', 'api/v1/map', RESOURCE].join('/'));
|
||||
var RESOURCE_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com'
|
||||
}
|
||||
},
|
||||
resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan/api/v1/map',
|
||||
https: 'https://{{=it.user}}.ssl.localhost.lan/api/v1/map'
|
||||
}
|
||||
};
|
||||
|
||||
describe('getUrls', function() {
|
||||
it('resources_url_templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + USERNAME + '.localhost.lan', 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
['https://' + USERNAME + '.ssl.localhost.lan', 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTileUrls', function() {
|
||||
it('resources_url_templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(
|
||||
urls.http,
|
||||
[`http://${USERNAME}.localhost.lan/api/v1/map/${TILE_RESOURCE}`]
|
||||
);
|
||||
assert.deepEqual(
|
||||
urls.https,
|
||||
[`https://${USERNAME}.ssl.localhost.lan/api/v1/map/${TILE_RESOURCE}`]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var CDN_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com',
|
||||
templates: {
|
||||
http: {
|
||||
url: "http://{s}.cdn.carto.com",
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
},
|
||||
https: {
|
||||
url: "https://cdn_{s}.ssl.cdn.carto.com",
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
describe('cdn templates', function() {
|
||||
|
||||
var CDN_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com',
|
||||
templates: {
|
||||
http: {
|
||||
url: "http://{s}.cdn.carto.com",
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
},
|
||||
https: {
|
||||
url: "https://cdn_{s}.ssl.cdn.carto.com",
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
it('cdn_url templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
};
|
||||
describe('getUrls', function() {
|
||||
it('cdn_url templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
|
||||
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
|
||||
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
|
||||
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + httpSubdomain + '.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTileUrls', function() {
|
||||
it('cdn_url templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(
|
||||
urls.http,
|
||||
HTTP_SUBDOMAINS
|
||||
.map(s => `http://${s}.cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
|
||||
);
|
||||
assert.deepEqual(
|
||||
urls.https,
|
||||
HTTPS_SUBDOMAINS
|
||||
.map(s => `https://cdn_${s}.ssl.cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + httpSubdomain + '.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
});
|
||||
|
||||
var CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com',
|
||||
templates: {
|
||||
http: {
|
||||
url: "http://{s}.cdn.carto.com",
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
},
|
||||
https: {
|
||||
url: "https://cdn_{s}.ssl.cdn.carto.com",
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
describe('cdn and resource templates', function() {
|
||||
|
||||
var CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com',
|
||||
templates: {
|
||||
http: {
|
||||
url: "http://{s}.cdn.carto.com",
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
},
|
||||
https: {
|
||||
url: "https://cdn_{s}.ssl.cdn.carto.com",
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
resources_url_templates: {
|
||||
http: 'http://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map',
|
||||
https: 'https://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map'
|
||||
}
|
||||
},
|
||||
resources_url_templates: {
|
||||
http: 'http://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map',
|
||||
https: 'https://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map'
|
||||
}
|
||||
};
|
||||
it('should mix cdn_url templates and resources_url_templates', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
};
|
||||
|
||||
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
|
||||
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
|
||||
describe('getUrls', function() {
|
||||
it('should mix cdn_url templates and resources_url_templates', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
|
||||
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + httpSubdomain + '.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
`https://cdn_${httpsSubdomain}.ssl.cdn.carto.com/u/${USERNAME}/api/v1/map/${RESOURCE}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTileUrls', function() {
|
||||
it('should mix cdn_url templates and resources_url_templates', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(
|
||||
urls.http,
|
||||
HTTP_SUBDOMAINS
|
||||
.map(s => `http://${s}.cdn.carto.com/u/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
|
||||
);
|
||||
assert.deepEqual(
|
||||
urls.https,
|
||||
HTTPS_SUBDOMAINS
|
||||
.map(s => `https://cdn_${s}.ssl.cdn.carto.com/u/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + httpSubdomain + '.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('tile stats', function() {
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
global.statsClient = this.statsClient;
|
||||
global.statsClient = this.statsClient;
|
||||
});
|
||||
|
||||
it('finalizeGetTileOrGrid does not call statsClient when format is not supported', function() {
|
||||
@@ -84,7 +84,7 @@ describe('tile stats', function() {
|
||||
});
|
||||
|
||||
function mockStatsClientGetInstance(instance) {
|
||||
global.statsClient = instance;
|
||||
global.statsClient = Object.assign(global.statsClient, instance);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
119
yarn.lock
119
yarn.lock
@@ -10,7 +10,11 @@
|
||||
mapnik "~3.5.0"
|
||||
sphericalmercator "1.0.x"
|
||||
|
||||
abbrev@1, abbrev@1.0.x:
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
|
||||
abbrev@1.0.x:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
|
||||
|
||||
@@ -29,13 +33,13 @@ ajv@^4.9.1:
|
||||
json-stable-stringify "^1.0.1"
|
||||
|
||||
ajv@^5.1.0:
|
||||
version "5.2.3"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.3.tgz#c06f598778c44c6b161abafe3466b81ad1814ed2"
|
||||
version "5.5.2"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
|
||||
dependencies:
|
||||
co "^4.6.0"
|
||||
fast-deep-equal "^1.0.0"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
json-schema-traverse "^0.3.0"
|
||||
json-stable-stringify "^1.0.1"
|
||||
|
||||
align-text@^0.1.1, align-text@^0.1.3:
|
||||
version "0.1.4"
|
||||
@@ -211,9 +215,9 @@ camelcase@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
|
||||
|
||||
camshaft@0.59.4:
|
||||
version "0.59.4"
|
||||
resolved "https://registry.yarnpkg.com/camshaft/-/camshaft-0.59.4.tgz#7d4d0b8585fa180b5e750206aa84940870ebabfc"
|
||||
camshaft@0.60.0:
|
||||
version "0.60.0"
|
||||
resolved "https://registry.yarnpkg.com/camshaft/-/camshaft-0.60.0.tgz#0433b5a576e08cabbc9bae1e1b22305274b8b7b6"
|
||||
dependencies:
|
||||
async "^1.5.2"
|
||||
bunyan "1.8.1"
|
||||
@@ -520,7 +524,11 @@ domutils@1.5:
|
||||
dom-serializer "0"
|
||||
domelementtype "1"
|
||||
|
||||
dot@^1.0.3, dot@~1.0.2:
|
||||
dot@^1.0.3:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/dot/-/dot-1.1.2.tgz#c7377019fc4e550798928b2b9afeb66abfa1f2f9"
|
||||
|
||||
dot@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dot/-/dot-1.0.3.tgz#f8750bfb6b03c7664eb0e6cb1eb4c66419af9427"
|
||||
|
||||
@@ -644,14 +652,22 @@ extend@~3.0.0, extend@~3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
|
||||
|
||||
extsprintf@1.3.0, extsprintf@^1.2.0:
|
||||
extsprintf@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
|
||||
|
||||
extsprintf@^1.2.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
|
||||
|
||||
fast-deep-equal@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
|
||||
|
||||
fast-json-stable-stringify@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
|
||||
|
||||
fast-levenshtein@~2.0.4:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
@@ -776,7 +792,7 @@ getpass@^0.1.1:
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
glob@7.1.1, glob@^7.1.1:
|
||||
glob@7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
|
||||
dependencies:
|
||||
@@ -807,7 +823,7 @@ glob@^6.0.1:
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^7.0.5:
|
||||
glob@^7.0.5, glob@^7.1.1:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
|
||||
dependencies:
|
||||
@@ -826,9 +842,9 @@ graceful-fs@^4.1.2:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
|
||||
|
||||
grainstore@1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/grainstore/-/grainstore-1.7.0.tgz#28d78895c82e6201f7d0ff63af1056f3c0fda0d3"
|
||||
grainstore@~1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/grainstore/-/grainstore-1.8.0.tgz#9398729df88f3aecb55ffbb415d541dcca4420af"
|
||||
dependencies:
|
||||
carto "0.16.3"
|
||||
debug "~3.1.0"
|
||||
@@ -845,8 +861,8 @@ growl@1.9.2:
|
||||
resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f"
|
||||
|
||||
handlebars@^4.0.1:
|
||||
version "4.0.10"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f"
|
||||
version "4.0.11"
|
||||
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc"
|
||||
dependencies:
|
||||
async "^1.4.0"
|
||||
optimist "^0.6.1"
|
||||
@@ -998,8 +1014,8 @@ is-arrayish@^0.2.1:
|
||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||
|
||||
is-buffer@^1.1.5:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||
|
||||
is-builtin-module@^1.0.0:
|
||||
version "1.0.0"
|
||||
@@ -1057,8 +1073,8 @@ istanbul@~0.4.3:
|
||||
wordwrap "^1.0.0"
|
||||
|
||||
js-base64@^2.1.9:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf"
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.0.tgz#9e566fee624751a1d720c966cd6226d29d4025aa"
|
||||
|
||||
js-string-escape@1.0.1:
|
||||
version "1.0.1"
|
||||
@@ -1325,7 +1341,7 @@ mime@~1.3.4:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@0.0.8, minimist@~0.0.1:
|
||||
minimist@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
|
||||
@@ -1333,6 +1349,10 @@ minimist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
|
||||
minimist@~0.0.1:
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
|
||||
|
||||
minimist@~0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.2.0.tgz#4dffe525dae2b864c66c2e23c6271d7afdecefce"
|
||||
@@ -1359,7 +1379,11 @@ mocha@~3.4.1:
|
||||
mkdirp "0.5.1"
|
||||
supports-color "3.1.2"
|
||||
|
||||
moment@^2.10.6, moment@~2.18.1:
|
||||
moment@^2.10.6:
|
||||
version "2.20.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd"
|
||||
|
||||
moment@~2.18.1:
|
||||
version "2.18.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
|
||||
|
||||
@@ -1379,11 +1403,7 @@ mv@~2:
|
||||
ncp "~2.0.0"
|
||||
rimraf "~2.4.0"
|
||||
|
||||
nan@^2.0.8, nan@^2.3.4, nan@~2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
|
||||
|
||||
nan@^2.4.0:
|
||||
nan@^2.0.8, nan@^2.3.4, nan@^2.4.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
|
||||
|
||||
@@ -1395,6 +1415,10 @@ nan@~2.6.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
|
||||
|
||||
nan@~2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
|
||||
|
||||
ncp@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
|
||||
@@ -1588,6 +1612,10 @@ pg-connection-string@0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7"
|
||||
|
||||
pg-int8@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
|
||||
|
||||
pg-pool@1.*:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-1.8.0.tgz#f7ec73824c37a03f076f51bfdf70e340147c4f37"
|
||||
@@ -1596,9 +1624,10 @@ pg-pool@1.*:
|
||||
object-assign "4.1.0"
|
||||
|
||||
pg-types@1.*:
|
||||
version "1.12.1"
|
||||
resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-1.12.1.tgz#d64087e3903b58ffaad279e7595c52208a14c3d2"
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-1.13.0.tgz#75f490b8a8abf75f1386ef5ec4455ecf6b345c63"
|
||||
dependencies:
|
||||
pg-int8 "1.0.1"
|
||||
postgres-array "~1.0.0"
|
||||
postgres-bytea "~1.0.0"
|
||||
postgres-date "~1.0.0"
|
||||
@@ -1920,18 +1949,14 @@ safe-json-stringify@~1:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz#81a098f447e4bbc3ff3312a243521bc060ef5911"
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@~5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
|
||||
|
||||
semver@4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7"
|
||||
|
||||
semver@^5.1.0, semver@^5.3.0:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
|
||||
|
||||
semver@~4.3.3:
|
||||
version "4.3.6"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
|
||||
@@ -1940,6 +1965,10 @@ semver@~5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a"
|
||||
|
||||
semver@~5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
|
||||
|
||||
send@0.16.1:
|
||||
version "0.16.1"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3"
|
||||
@@ -1998,8 +2027,8 @@ sntp@1.x.x:
|
||||
hoek "2.x.x"
|
||||
|
||||
sntp@2.x.x:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.0.2.tgz#5064110f0af85f7cfdb7d6b67a40028ce52b4b2b"
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8"
|
||||
dependencies:
|
||||
hoek "4.x.x"
|
||||
|
||||
@@ -2082,7 +2111,11 @@ sshpk@^1.7.0:
|
||||
jsbn "~0.1.0"
|
||||
tweetnacl "~0.14.0"
|
||||
|
||||
"statuses@>= 1.3.1 < 2", statuses@~1.3.1:
|
||||
"statuses@>= 1.3.1 < 2":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
|
||||
|
||||
statuses@~1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
|
||||
|
||||
@@ -2359,9 +2392,9 @@ window-size@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
|
||||
|
||||
windshaft@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/windshaft/-/windshaft-4.1.0.tgz#dc17c8369570c305171d1ab5ca130369bba04d58"
|
||||
windshaft@4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/windshaft/-/windshaft-4.2.0.tgz#6a0409832a0d3bccfa09a88a8ab8288686b6762d"
|
||||
dependencies:
|
||||
abaculus cartodb/abaculus#2.0.3-cdb1
|
||||
canvas cartodb/node-canvas#1.6.2-cdb2
|
||||
@@ -2369,7 +2402,7 @@ windshaft@^4.1.0:
|
||||
cartodb-psql "^0.10.1"
|
||||
debug "^3.1.0"
|
||||
dot "~1.0.2"
|
||||
grainstore "1.7.0"
|
||||
grainstore "~1.8.0"
|
||||
mapnik "3.5.14"
|
||||
queue-async "~1.0.7"
|
||||
redis-mpool "0.4.1"
|
||||
|
||||
Reference in New Issue
Block a user