Compare commits

..

120 Commits
4.5.0 ... 4.7.0

Author SHA1 Message Date
Raul Ochoa
d949d1c27f Release 4.7.0 2018-01-03 19:18:39 +00:00
Raul Ochoa
aa1d411fb8 Update news and bump version 2018-01-03 19:17:58 +00:00
Raul Ochoa
f297374449 Merge pull request #837 from CartoDB/tilejson
Return tilejson in metadata
2018-01-03 20:13:44 +01:00
Raul Ochoa
060b93c314 Rename middleware fn name 2018-01-03 18:44:10 +00:00
Raul Ochoa
3ceeaedf02 Fix test after breaking it with linting changes 2018-01-03 16:19:14 +00:00
Raul Ochoa
c6ba9e6102 Fix linting 2018-01-03 16:10:09 +00:00
Raul Ochoa
bf40b240d3 Return tilejson in metadata
It returns tilejson for each individual layer and also for all vector and raster layers.
2018-01-03 16:54:45 +01:00
Raul Ochoa
5d4d2bddd6 Implementation for getTilesUrls
This will be useful for generating the tilejson in the metadata
2018-01-03 16:05:19 +01:00
Raul Ochoa
95dfd87c96 Add test cases for getTilesUrls 2018-01-03 16:04:31 +01:00
Raul Ochoa
eab9e8846e Reorg suite to accommodate getTileURLs 2018-01-03 12:57:01 +00:00
Daniel
788bc302a0 Merge pull request #833 from CartoDB/middlewarify-named-map-admin-controller
Middlewarify named map admin controller
2018-01-03 13:20:14 +01:00
Daniel García Aubert
1ba240d099 Rename middleware function 2018-01-03 13:15:11 +01:00
Daniel
ee0405da1e Merge pull request #836 from CartoDB/disable-default-aggregation
Add test to check layer aggregation disabling
2018-01-03 13:09:31 +01:00
Daniel
5e9b326d03 Merge pull request #835 from CartoDB/skip-polygon-layer-vector-map-config
In vector-only map-config, only aggregate layers with points
2018-01-03 13:09:07 +01:00
Daniel García Aubert
1f30367e59 Add test to check layer aggregation disabling 2018-01-03 12:40:00 +01:00
Daniel García Aubert
26a2f73c2a Update NEWS 2018-01-03 12:30:46 +01:00
Daniel García Aubert
60005e2f7f Fix bad assertion 2018-01-03 12:24:07 +01:00
Daniel García Aubert
1c7da2c4b3 Going green: do not fail when map-config is vector-only and a layer doesn't have points 2018-01-03 12:00:25 +01:00
Daniel García Aubert
3799dd2574 Going red: fail when vector only map-config has a polygon layer 2018-01-03 11:14:20 +01:00
Raul Ochoa
7efb2a2344 Stubs next version 2018-01-02 15:40:54 +00:00
Raul Ochoa
88777abc2c Release 4.6.0 2018-01-02 15:40:10 +00:00
Raul Ochoa
4d9a6f8fbe update news 2018-01-02 15:39:37 +00:00
Daniel
3d9c2e66c5 Merge pull request #830 from CartoDB/pg-mvt-do-not-filter-columns
Aggregation: be able to return a complete row sample as default aggregation
2018-01-02 15:36:08 +01:00
Daniel García Aubert
6bbe715aa6 Update NEWS 2018-01-02 12:57:54 +01:00
Daniel García Aubert
ba002fdb2c Update windshaft to 4.20 2018-01-02 12:38:58 +01:00
Raul Ochoa
10d1381e51 Merge remote-tracking branch 'origin/master' into pg-mvt-do-not-filter-columns 2017-12-29 17:50:09 +00:00
Daniel García Aubert
dfef7ff3c0 Use spread assignment 2017-12-29 18:45:45 +01:00
Daniel García Aubert
83d0ce4040 Rename method 2017-12-29 18:25:08 +01:00
Daniel García Aubert
75f72c4d07 Return empty aggregation configuration when the map-config is vector-only and the layer has no aggregation 2017-12-29 17:52:28 +01:00
Daniel García Aubert
adb9e55fb2 Avoid snake_case notation 2017-12-29 16:30:42 +01:00
Daniel García Aubert
5d3726de44 Use original variable name 2017-12-29 16:24:38 +01:00
Daniel García Aubert
f186e4736b Use template string 2017-12-29 16:19:00 +01:00
Daniel García Aubert
a00c2b1eef Now main middlewares return a named function with the right context bound 2017-12-29 16:15:48 +01:00
Daniel García Aubert
64d601179d Use const instead of var 2017-12-29 15:22:17 +01:00
Daniel García Aubert
cf2b73e473 Move to up intermediate middlewares 2017-12-29 15:19:52 +01:00
Daniel García Aubert
70932c23df Remove step and assert dependencies 2017-12-29 15:17:29 +01:00
Daniel García Aubert
519d49bd10 Remove finish function and respond in the main middleware 2017-12-29 15:04:44 +01:00
Daniel García Aubert
bf814c4442 keep error label 2017-12-29 13:05:01 +01:00
Daniel García Aubert
f136993c50 Use checkContentType middleware 2017-12-29 12:44:56 +01:00
Daniel García Aubert
ba008ab518 Remove unused function 2017-12-29 12:36:56 +01:00
Daniel García Aubert
e4ed6ee1cc Use authorizedByAPIKey middleware 2017-12-29 12:34:50 +01:00
Daniel García Aubert
fda7661dad Create authorizedByAPIKey middleware 2017-12-29 12:23:52 +01:00
Raul Ochoa
79233471c6 Merge pull request #832 from CartoDB/layers-filters
Support individual layer id filters
2017-12-28 19:48:18 +01:00
Raul Ochoa
a75beefe6e Upgrades windshaft to 4.1.1 2017-12-28 18:34:53 +00:00
Raul Ochoa
e43ccf4f12 Going red: individual layer id filters fail
Depends on https://github.com/CartoDB/Windshaft/pull/584.
2017-12-28 18:19:52 +00:00
Raul Ochoa
cd8e320534 Merge pull request #831 from CartoDB/fail-on-uncaught-exceptions
Make tests to fail if they got an uncaught exception
2017-12-28 19:15:29 +01:00
Raul Ochoa
d7f4d39aa2 Don't not override the full client but only the provided methods 2017-12-28 18:09:41 +00:00
Raul Ochoa
89333185a9 Make tests to fail if they got an uncaught exception 2017-12-28 16:37:17 +00:00
Daniel García Aubert
99b95cf839 Move check of default-aggregation to mapconfig model 2017-12-28 13:50:59 +01:00
Daniel García Aubert
9fbc56b82c Remove FIXME notes to test against PostGIS vector renderer 2017-12-28 13:13:32 +01:00
Daniel García Aubert
9a1bc51fdb Use aggregation-mapconfig's method to discover columns to be exposed used to aggregate 2017-12-28 13:12:41 +01:00
Daniel García Aubert
d42257127b Add method to discover required columns or all of them if it's a default aggregation 2017-12-28 13:11:32 +01:00
Daniel García Aubert
5a730c6df1 Remove exited containers after running test against docker 2017-12-28 13:10:11 +01:00
Daniel García Aubert
418c8691d1 Support default full-sample aggregation for postgis vector renderer 2017-12-27 20:08:43 +01:00
Daniel García Aubert
9885045b41 Do not default to null 2017-12-27 12:48:06 +01:00
Daniel García Aubert
062e6f9594 Merge branch 'full-sample' of github.com:CartoDB/Windshaft-cartodb into pg-mvt-do-not-filter-columns 2017-12-27 12:45:43 +01:00
Daniel
d8428938ae Merge pull request #829 from CartoDB/vector-tiles-doc
Update vector tiles documentation
2017-12-26 11:42:06 +01:00
Simon Martín
ca5f280cb3 updating NEWS.md 2017-12-26 11:03:12 +01:00
Simon Martín
524d5a5597 Merge pull request #828 from Algunenano/i827_timestamp
Timeseries: Avoid collisions with columns named 'timestamp'
2017-12-26 09:55:46 +01:00
Daniel García Aubert
a43779b050 Get columns from layer query a set them into layer opptions 2017-12-22 18:19:57 +01:00
Javier Goizueta
ef3917fa6f Update vector tiles documentation
This update reflects the fact that CartoCSS is now optional for vector tiles.
2017-12-22 17:18:42 +01:00
Raul Marin
031e1253ca Numeric histograms: Avoid conflicts with 'bin' named columns 2017-12-22 17:12:57 +01:00
Raul Marin
8012d76b68 Timeseries: Avoid collisions with columns named 'timestamp' 2017-12-22 15:58:40 +01:00
Javier Goizueta
d726c9ad01 Fix point-sample aggregation
it failed in  the case of aggregate columns with the name of base columns
2017-12-22 15:48:30 +01:00
Javier Goizueta
1ce8076699 Change default aggregation placement to point-sample
For consistency with the default aggregation.
2017-12-22 15:46:29 +01:00
Javier Goizueta
54f32113f3 Add some aggregation tests 2017-12-22 15:45:34 +01:00
Javier Goizueta
19bf079f2d Exclude test from PostGIS 2.4 2017-12-22 15:45:14 +01:00
Daniel García Aubert
b7ecde5c9d Add function get columns for layer's query 2017-12-22 13:43:30 +01:00
Daniel García Aubert
a2f804d79f Use full-sample aggregation mode 2017-12-22 12:15:37 +01:00
Daniel García Aubert
efdfabf3e9 Remove method 2017-12-22 12:14:34 +01:00
Javier Goizueta
e9a4fc4b2c Use full-sample aggregation only as default
Sampling is performed only when placement, columns or dimensions are specified;
otherwise the regular centroid/grid-point/grid-center is used without sampling.
2017-12-22 11:31:33 +01:00
Daniel García Aubert
a1d536642e Merge branch 'full-sample' into pg-mvt-do-not-filter-columns 2017-12-21 20:01:07 +01:00
Daniel García Aubert
3c00266666 Add support for aggregated columns in mvt format 2017-12-21 20:00:17 +01:00
Daniel García Aubert
7f64d15944 Merge branch 'master' into full-sample 2017-12-20 15:58:58 +01:00
Daniel
8259271184 Merge pull request #826 from CartoDB/fix-typo-dimensions
Fix typo
2017-12-20 15:02:32 +01:00
Daniel García Aubert
20366cedb4 Skip test for PostGis 2.4 2017-12-20 14:53:34 +01:00
Daniel García Aubert
a102d1d366 jshint, I hate you 2017-12-20 14:41:29 +01:00
Daniel García Aubert
4b97b4fd26 Fix typo 2017-12-20 14:35:47 +01:00
Daniel
b94debf10e Merge pull request #825 from CartoDB/export-supported-placements
Export supported placements and create static methods to expose them
2017-12-20 13:27:23 +01:00
Daniel García Aubert
60030784c1 Export supported placements and create static methods to expose them in a fancy way 2017-12-20 12:42:29 +01:00
Daniel García Aubert
cc9b190e5d Minor style formats 2017-12-19 16:17:37 +01:00
Daniel García Aubert
4946ca688c Add test to check full-sample query 2017-12-19 16:17:13 +01:00
Daniel García Aubert
d2828ecaff Update test 2017-12-19 13:07:57 +01:00
Daniel García Aubert
5a3dd6a914 Use supported placemets of aggregation-query 2017-12-19 13:00:18 +01:00
Daniel García Aubert
bcd2fd8f88 Export supported placements 2017-12-19 12:59:33 +01:00
Daniel García Aubert
94a5e66881 Merge branch 'master' into full-sample 2017-12-19 12:47:36 +01:00
Daniel García Aubert
d55b78f76b Update next release version in NEWS 2017-12-19 12:46:29 +01:00
Daniel García Aubert
42149f9ae7 Update NEWS 2017-12-19 12:45:30 +01:00
Daniel
1e08d946b1 Merge pull request #822 from CartoDB/aggregation-validation
Validate aggregation input params
2017-12-19 12:42:28 +01:00
Daniel García Aubert
f22216e6d2 Catch error threw from constructor and follow node callback pattern 2017-12-19 12:23:54 +01:00
Raul Marin
d9cf830fb4 Stub for next release 2017-12-19 12:23:27 +01:00
Daniel García Aubert
326cad2f2c Typo 2017-12-19 10:54:20 +01:00
Daniel García Aubert
34808d6147 Improve naming 2017-12-19 10:50:53 +01:00
Daniel García Aubert
79b04bbdfd Rename param 2017-12-19 10:47:53 +01:00
Daniel García Aubert
45a663d5ae Split columns validator 2017-12-19 10:43:34 +01:00
Daniel García Aubert
cace6169c0 Add function to create layer errors 2017-12-19 10:25:41 +01:00
Daniel García Aubert
bdce2f95f2 Add validations for columns 2017-12-18 20:42:26 +01:00
Javier Goizueta
506e16fc87 Experimental full-sample aggregation 2017-12-18 20:18:37 +01:00
Daniel García Aubert
c367743d76 Export SUPPORTED_AGGREGATE_FUNCTIONS 2017-12-18 20:06:16 +01:00
Daniel García Aubert
fa7140e736 Rename argument 2017-12-18 19:52:50 +01:00
Daniel García Aubert
c63226cd26 Improve function naming 2017-12-18 19:51:55 +01:00
Daniel García Aubert
777df6337b Style typo 2017-12-18 19:47:11 +01:00
Daniel García Aubert
2dda0a80da Improve error context 2017-12-18 19:35:12 +01:00
Daniel García Aubert
e2bd97eea6 Move validation to the constructor 2017-12-18 19:19:02 +01:00
Daniel García Aubert
fb03cd3424 Move aggregation validation to its own module 2017-12-18 19:17:43 +01:00
Daniel García Aubert
8a48b96c53 Rename file 2017-12-18 19:06:01 +01:00
Daniel García Aubert
76b0c94835 Rename file 2017-12-18 19:05:49 +01:00
Daniel García Aubert
6a36aa1f13 Order checks to validate if a layer should be adapted 2017-12-18 18:56:53 +01:00
Daniel García Aubert
800870e783 Remove local variable 2017-12-18 18:55:32 +01:00
Daniel García Aubert
6638ba91c3 Refactor supported geometry types 2017-12-18 18:53:44 +01:00
Daniel García Aubert
47e4b9da0d Encapsulate threshold layer validation in aggregation-mapconfig 2017-12-18 18:43:14 +01:00
Daniel García Aubert
81e0c3a098 Add RESOLUTION default getter 2017-12-18 18:26:08 +01:00
Daniel García Aubert
2068861988 Add PLACEMENT default getter 2017-12-18 18:24:09 +01:00
Daniel García Aubert
878f3bd627 Move .sql() to aggregation-mapconfig 2017-12-18 18:17:01 +01:00
Daniel García Aubert
170fcc1973 Move static methods 2017-12-18 17:42:12 +01:00
Daniel García Aubert
d0c88ce21d Improve naming 2017-12-18 17:26:41 +01:00
Daniel García Aubert
e81a16ce0d Improve validation by applying refactor 2017-12-18 14:31:53 +01:00
Daniel García Aubert
153a792fcb Improve validation by applying refactor 2017-12-18 14:25:44 +01:00
Daniel García Aubert
5c1b1e3214 Improve validation by applying refactor 2017-12-18 14:21:03 +01:00
Daniel García Aubert
0bca3d6f33 Validate placement, threshold and resolution 2017-12-18 13:42:27 +01:00
21 changed files with 2017 additions and 460 deletions

20
NEWS.md
View File

@@ -1,5 +1,25 @@
# 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

View File

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

View File

@@ -90,6 +90,7 @@ MapController.prototype.composeCreateMapMiddleware = function (useTemplate = fal
this.setAnalysesMetadataToLayergroup(includeQuery),
this.setTurboCartoMetadataToLayergroup(),
this.setAggregationMetadataToLayergroup(),
this.setTilejsonMetadataToLayergroup(),
this.setSurrogateKeyHeader(),
this.sendResponse(),
this.augmentError({ label, addContext })
@@ -313,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;

View File

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

View File

@@ -1,34 +0,0 @@
const MapConfig = require('windshaft').model.MapConfig;
module.exports = class AggregationMapConfig extends MapConfig {
constructor (config, datasource) {
super(config, datasource);
}
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');
}
};

View 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;
}
};

View File

@@ -1,8 +1,12 @@
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
@@ -10,9 +14,12 @@
* - dimensions
*/
const templateForOptions = (options) => {
let templateFn = aggregationQueryTemplates[options.placement];
if (!templateFn) {
throw new Error("Invalid Aggregation placement: '" + options.placement + "'");
let templateFn = defaultAggregationQueryTemplate;
if (!options.isDefaultAggregation) {
templateFn = aggregationQueryTemplates[options.placement || DEFAULT_PLACEMENT];
if (!templateFn) {
throw new Error("Invalid Aggregation placement: '" + options.placement + "'");
}
}
return templateFn;
};
@@ -25,6 +32,11 @@ const templateForOptions = (options) => {
* - 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,
@@ -56,6 +68,8 @@ const SUPPORTED_AGGREGATE_FUNCTIONS = {
}
};
module.exports.SUPPORTED_AGGREGATE_FUNCTIONS = Object.keys(SUPPORTED_AGGREGATE_FUNCTIONS);
const sep = (list) => {
let expr = list.join(', ');
return expr ? ', ' + expr : expr;
@@ -69,8 +83,13 @@ const aggregateColumns = ctx => {
}, ctx.columns || {});
};
const aggregateColumnNames = ctx => {
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));
};
@@ -90,8 +109,14 @@ const aggregateColumnDefs = ctx => {
const aggregateDimensions = ctx => ctx.dimensions || {};
const dimensionNames = ctx => {
return sep(Object.keys(aggregateDimensions(ctx)));
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 => {
@@ -114,9 +139,38 @@ const gridResolution = ctx => `(${256*0.00028/ctx.res}*!scale_denominator!)::dou
// 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 (
WITH
_cdb_params AS (
SELECT
${gridResolution(ctx)} AS res,
!bbox! AS bbox
@@ -140,34 +194,37 @@ const aggregationQueryTemplates = {
`,
'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 (
WITH
_cdb_params AS (
SELECT
${gridResolution(ctx)} AS res,
!bbox! AS bbox
), _cdb_clusters AS (
),
_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)}
@@ -178,13 +235,17 @@ const aggregationQueryTemplates = {
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
)
SELECT
_cdb_clusters.cartodb_id,
the_geom, the_geom_webmercator
${dimensionNames(ctx)}
${aggregateColumnNames(ctx)}
${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);

View 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;
}

View File

@@ -1,26 +0,0 @@
const aggregationQuery = require('./aggregation-query');
module.exports = class Aggregation {
static get THRESHOLD() {
return 1e5; // 100K
}
constructor (mapconfig, query, {
resolution = 1,
threshold = Aggregation.THRESHOLD,
placement = 'centroid',
columns = {},
dimensions = {}
} = {}) {
this.mapconfig = mapconfig;
this.query = query;
this.resolution = resolution;
this.threshold = threshold;
this.placement = placement;
this.columns = columns;
this.dimensions = dimensions;
}
sql () {
return aggregationQuery(this);
}
};

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
const Aggregation = require('../../aggregation/aggregation');
const AggregationMapConfig = require('../../aggregation/aggregation-map-config');
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: ST_Point`;
`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'`;
@@ -14,11 +14,17 @@ module.exports = class AggregationMapConfigAdapter {
}
getMapConfig (user, requestMapConfig, params, context, callback) {
if (!this._isValidAggregationParam(params)) {
if (!this._isValidAggregationQueryParam(params)) {
return callback(new Error(invalidAggregationParamValueErrorMessage({ value: params.aggregation })));
}
const mapConfig = new AggregationMapConfig(requestMapConfig);
let mapConfig;
try {
mapConfig = new AggregationMapConfig(user, requestMapConfig, this.pgConnection);
} catch (err) {
return callback(err);
}
if (!this._shouldAdapt(mapConfig, params)) {
return callback(null, requestMapConfig);
@@ -33,7 +39,7 @@ module.exports = class AggregationMapConfigAdapter {
});
}
_isValidAggregationParam (params) {
_isValidAggregationQueryParam (params) {
const { aggregation } = params;
return aggregation === undefined || aggregation === 'true' || aggregation === 'false';
}
@@ -83,30 +89,36 @@ module.exports = class AggregationMapConfigAdapter {
return reject(err);
}
if (shouldAdapt) {
const sql = layer.options.sql_raw ? layer.options.sql_raw : layer.options.sql;
const aggregation = new Aggregation(mapConfig, sql, layer.options.aggregation);
const sqlQueryWrap = layer.options.sql_wrap;
let aggregationSql = aggregation.sql();
if (sqlQueryWrap) {
aggregationSql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, aggregationSql);
}
layer.options.sql = aggregationSql;
if (!shouldAdapt) {
return resolve({ layer, index, adapted: 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) {
let shouldAdapt = false;
if (!mapConfig.isAggregationLayer(index)) {
return callback(null, shouldAdapt);
return callback(null, false);
}
const aggregationMetadata = queryUtils.getAggregationMetadata({
@@ -115,29 +127,33 @@ module.exports = class AggregationMapConfigAdapter {
connection.query(aggregationMetadata, (err, res) => {
if (err) {
return callback(null, shouldAdapt);
return callback(null, false);
}
const result = res.rows[0] || {};
const estimatedFeatureCount = result.count;
const threshold = layer.options.aggregation && layer.options.aggregation.threshold ?
layer.options.aggregation.threshold :
Aggregation.THRESHOLD;
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)
};
if (estimatedFeatureCount < threshold) {
return callback(null, shouldAdapt);
return callback(error);
}
const geometryType = result.type;
if (geometryType !== 'ST_Point') {
return callback(new Error(unsupportedGeometryTypeErrorMessage({ geometryType })));
if (mapConfig.isVectorOnlyMapConfig() && !AggregationMapConfig.supportsGeometryType(result.type)) {
return callback(null, false);
}
shouldAdapt = true;
if (!mapConfig.doesLayerReachThreshold(index, result.count)) {
return callback(null, false);
}
callback(null, shouldAdapt);
callback(null, true);
});
}

View File

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

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "windshaft-cartodb",
"version": "4.5.0",
"version": "4.7.0",
"description": "A map tile server for CartoDB",
"keywords": [
"cartodb"
@@ -46,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": {
@@ -65,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"
},

View File

@@ -289,7 +289,8 @@ describe('aggregation', function () {
options: {
sql: POINTS_SQL_2,
aggregation: {
threshold: 1
threshold: 1,
placement: 'centroid'
},
cartocss: '#layer { marker-width: [value]; }',
cartocss_version: '2.3.0'
@@ -309,6 +310,45 @@ describe('aggregation', function () {
});
});
it('should provide all columns in the default aggregation ',
function (done) {
const response = {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
this.mapConfig = createVectorMapConfig([
{
type: 'cartodb',
options: {
sql: POINTS_SQL_2,
aggregation: {
threshold: 1
},
cartocss: '#layer { marker-width: [value]; }',
cartocss_version: '2.3.0'
}
}
]);
this.testClient = new TestClient(this.mapConfig);
this.testClient.getLayergroup({ response }, (err, body) => {
if (err) {
return done(err);
}
assert.equal(typeof body.metadata, 'object');
assert.ok(Array.isArray(body.metadata.layers));
body.metadata.layers.forEach(layer => assert.ok(layer.meta.aggregation.mvt));
body.metadata.layers.forEach(layer => assert.ok(layer.meta.aggregation.png));
done();
});
});
it('should skip aggregation to create a layergroup with aggregation defined already', function (done) {
const mapConfig = createVectorMapConfig([
{
@@ -345,6 +385,44 @@ describe('aggregation', function () {
});
});
it('skip default aggregation by setting `aggregation: false` for just one layer', function (done) {
const mapConfig = createVectorMapConfig([
{
type: 'cartodb',
options: {
sql: POINTS_SQL_1,
aggregation: {
threshold: 1
}
}
},
{
type: 'cartodb',
options: {
sql: POINTS_SQL_2,
aggregation: false
}
}
]);
this.testClient = new TestClient(mapConfig);
this.testClient.getLayergroup((err, body) => {
if (err) {
return done(err);
}
assert.equal(typeof body.metadata, 'object');
assert.ok(Array.isArray(body.metadata.layers));
assert.equal(body.metadata.layers[0].meta.aggregation.mvt, true);
assert.equal(body.metadata.layers[1].meta.aggregation.mvt, false);
done();
});
});
it('when the aggregation param is not valid should respond with error', function (done) {
const mapConfig = createVectorMapConfig([
{
@@ -431,6 +509,8 @@ describe('aggregation', function () {
type: 'cartodb',
options: {
sql: POLYGONS_SQL_1,
cartocss: '#layer { marker-width: [value]; }',
cartocss_version: '2.3.0',
aggregation: {
threshold: 1
}
@@ -456,9 +536,14 @@ describe('aggregation', function () {
' Aggregation is available only for geometry type: ST_Point'
],
errors_with_context:[{
type: 'unknown',
type: 'layer',
message: 'Unsupported geometry type: ST_Polygon.' +
' Aggregation is available only for geometry type: ST_Point'
' Aggregation is available only for geometry type: ST_Point',
layer: {
id: 'layer0',
index: 0,
type: 'mapnik'
}
}]
});
@@ -524,6 +609,198 @@ describe('aggregation', function () {
});
});
it('when dimensions is provided should return a tile returning the column used as dimensions',
function (done) {
this.mapConfig = createVectorMapConfig([
{
type: 'cartodb',
options: {
sql: POINTS_SQL_1,
aggregation: {
threshold: 1,
dimensions: {
value: "value"
}
}
}
}
]);
this.testClient = new TestClient(this.mapConfig);
const options = {
format: 'mvt'
};
this.testClient.getTile(0, 0, 0, options, (err, res, tile) => {
if (err) {
return done(err);
}
const tileJSON = tile.toJSON();
tileJSON[0].features.forEach(feature => assert.equal(typeof feature.properties.value, 'number'));
done();
});
});
['centroid', 'point-sample', 'point-grid'].forEach(placement => {
it(`dimensions should work for ${placement} placement`, function(done) {
this.mapConfig = createVectorMapConfig([
{
type: 'cartodb',
options: {
sql: POINTS_SQL_1,
aggregation: {
placement: placement ,
threshold: 1,
dimensions: {
value: "value"
}
}
}
}
]);
this.testClient = new TestClient(this.mapConfig);
const options = {
format: 'mvt'
};
this.testClient.getTile(0, 0, 0, options, (err, res, tile) => {
if (err) {
return done(err);
}
const tileJSON = tile.toJSON();
tileJSON[0].features.forEach(
feature => assert.equal(typeof feature.properties.value, 'number')
);
done();
});
});
});
it(`dimensions should trigger non-default aggregation`, function(done) {
this.mapConfig = createVectorMapConfig([
{
type: 'cartodb',
options: {
sql: POINTS_SQL_2,
aggregation: {
threshold: 1,
dimensions: {
value: "value"
}
}
}
}
]);
this.testClient = new TestClient(this.mapConfig);
const options = {
format: 'mvt'
};
this.testClient.getTile(0, 0, 0, options, (err, res, tile) => {
if (err) {
return done(err);
}
const tileJSON = tile.toJSON();
tileJSON[0].features.forEach(
feature => assert.equal(typeof feature.properties.value, 'number')
);
tileJSON[0].features.forEach(
feature => assert.equal(typeof feature.properties.sqrt_value, 'undefined')
);
done();
});
});
it(`aggregation columns should trigger non-default aggregation`, function(done) {
this.mapConfig = createVectorMapConfig([
{
type: 'cartodb',
options: {
sql: POINTS_SQL_2,
aggregation: {
threshold: 1,
columns: {
value: {
aggregate_function: 'sum',
aggregated_column: 'value'
}
}
}
}
}
]);
this.testClient = new TestClient(this.mapConfig);
const options = {
format: 'mvt'
};
this.testClient.getTile(0, 0, 0, options, (err, res, tile) => {
if (err) {
return done(err);
}
const tileJSON = tile.toJSON();
tileJSON[0].features.forEach(
feature => assert.equal(typeof feature.properties.value, 'number')
);
tileJSON[0].features.forEach(
feature => assert.equal(typeof feature.properties.sqrt_value, 'undefined')
);
done();
});
});
['centroid', 'point-sample', 'point-grid'].forEach(placement => {
it(`aggregations with base column names should work for ${placement} placement`, function(done) {
this.mapConfig = createVectorMapConfig([
{
type: 'cartodb',
options: {
sql: POINTS_SQL_1,
aggregation: {
placement: placement ,
threshold: 1,
columns: {
value: {
aggregate_function: 'sum',
aggregated_column: 'value'
}
}
}
}
}
]);
this.testClient = new TestClient(this.mapConfig);
const options = {
format: 'mvt'
};
this.testClient.getTile(0, 0, 0, options, (err, res, tile) => {
if (err) {
return done(err);
}
const tileJSON = tile.toJSON();
tileJSON[0].features.forEach(
feature => assert.equal(typeof feature.properties.value, 'number')
);
done();
});
});
});
it('should work when the sql has single quotes', function (done) {
this.mapConfig = createVectorMapConfig([
{
@@ -531,6 +808,7 @@ describe('aggregation', function () {
options: {
sql: `
SELECT
cartodb_id,
the_geom_webmercator,
the_geom,
value,
@@ -686,6 +964,408 @@ describe('aggregation', function () {
done();
});
});
it('aggregates with full-sample placement by default', function (done) {
this.mapConfig = createVectorMapConfig([
{
type: 'cartodb',
options: {
sql: POINTS_SQL_1,
resolution: 256,
aggregation: {
threshold: 1
}
}
}
]);
this.testClient = new TestClient(this.mapConfig);
this.testClient.getTile(0, 0, 0, { format: 'mvt' }, function (err, res, mvt) {
if (err) {
return done(err);
}
const geojsonTile = JSON.parse(mvt.toGeoJSONSync(0));
assert.ok(Array.isArray(geojsonTile.features));
assert.ok(geojsonTile.features.length > 0);
const feature = geojsonTile.features[0];
assert.ok(feature.properties.hasOwnProperty('value'), 'Missing value property');
done();
});
});
it('should fail with bad resolution', function (done) {
this.mapConfig = createVectorMapConfig([
{
id: 'wadus',
type: 'cartodb',
options: {
sql: POINTS_SQL_1,
aggregation: {
resolution: 'wadus',
}
}
}
]);
this.testClient = new TestClient(this.mapConfig);
const options = {
response: {
status: 400
}
};
this.testClient.getLayergroup(options, (err, body) => {
if (err) {
return done(err);
}
assert.deepEqual(body, {
errors: [ 'Invalid resolution, should be a number greather than 0' ],
errors_with_context:[{
type: 'layer',
message: 'Invalid resolution, should be a number greather than 0',
layer: {
"id": "wadus",
"index": 0,
"type": "mapnik"
}
}]
});
done();
});
});
it('should fail with bad placement', function (done) {
this.mapConfig = createVectorMapConfig([
{
type: 'cartodb',
options: {
sql: POINTS_SQL_1,
aggregation: {
placement: 'wadus',
}
}
}
]);
this.testClient = new TestClient(this.mapConfig);
const options = {
response: {
status: 400
}
};
this.testClient.getLayergroup(options, (err, body) => {
if (err) {
return done(err);
}
assert.deepEqual(body, {
errors: [ 'Invalid placement. Valid values: centroid, point-grid, point-sample'],
errors_with_context:[{
type: 'layer',
message: 'Invalid placement. Valid values: centroid, point-grid, point-sample',
layer: {
id: "layer0",
index: 0,
type: "mapnik",
}
}]
});
done();
});
});
it('should fail with bad threshold', function (done) {
this.mapConfig = createVectorMapConfig([
{
type: 'cartodb',
options: {
sql: POINTS_SQL_1,
aggregation: {
threshold: 'wadus',
}
}
}
]);
this.testClient = new TestClient(this.mapConfig);
const options = {
response: {
status: 400
}
};
this.testClient.getLayergroup(options, (err, body) => {
if (err) {
return done(err);
}
assert.deepEqual(body, {
errors: [ 'Invalid threshold, should be a number greather than 0' ],
errors_with_context:[{
type: 'layer',
message: 'Invalid threshold, should be a number greather than 0',
layer: {
"id": "layer0",
"index": 0,
"type": "mapnik"
}
}]
});
done();
});
});
it('should fail with bad column name', function (done) {
this.mapConfig = createVectorMapConfig([
{
type: 'cartodb',
options: {
sql: POINTS_SQL_1,
aggregation: {
columns : {
'': {
aggregate_function: 'count',
aggregated_column: 'value',
}
},
}
}
}
]);
this.testClient = new TestClient(this.mapConfig);
const options = {
response: {
status: 400
}
};
this.testClient.getLayergroup(options, (err, body) => {
if (err) {
return done(err);
}
assert.deepEqual(body, {
errors: [ 'Invalid column name, should be a non empty string' ],
errors_with_context:[{
type: 'layer',
message: 'Invalid column name, should be a non empty string',
layer: {
"id": "layer0",
"index": 0,
"type": "mapnik"
}
}]
});
done();
});
});
it('should fail with bad aggregated function', function (done) {
this.mapConfig = createVectorMapConfig([
{
type: 'cartodb',
options: {
sql: POINTS_SQL_1,
aggregation: {
columns : {
'wadus_function': {
aggregate_function: 'wadus',
aggregated_column: 'value',
}
},
}
}
}
]);
this.testClient = new TestClient(this.mapConfig);
const options = {
response: {
status: 400
}
};
this.testClient.getLayergroup(options, (err, body) => {
if (err) {
return done(err);
}
assert.deepEqual(body, {
errors: [ 'Unsupported aggregation function wadus, ' +
'valid ones: count, avg, sum, min, max, mode' ],
errors_with_context:[{
type: 'layer',
message: 'Unsupported aggregation function wadus, ' +
'valid ones: count, avg, sum, min, max, mode',
layer: {
"id": "layer0",
"index": 0,
"type": "mapnik"
}
}]
});
done();
});
});
it('should fail with bad aggregated columns', function (done) {
this.mapConfig = createVectorMapConfig([
{
type: 'cartodb',
options: {
sql: POINTS_SQL_1,
aggregation: {
columns : {
'total_wadus': {
aggregate_function: 'sum',
aggregated_column: '',
}
},
}
}
}
]);
this.testClient = new TestClient(this.mapConfig);
const options = {
response: {
status: 400
}
};
this.testClient.getLayergroup(options, (err, body) => {
if (err) {
return done(err);
}
assert.deepEqual(body, {
errors: [ 'Invalid aggregated column, should be a non empty string' ],
errors_with_context:[{
type: 'layer',
message: 'Invalid aggregated column, should be a non empty string',
layer: {
"id": "layer0",
"index": 0,
"type": "mapnik"
}
}]
});
done();
});
});
it('should skip aggregation w/o failing when is Vector Only MapConfig and layer has polygons',
function (done) {
this.mapConfig = createVectorMapConfig([
{
type: 'cartodb',
options: {
sql: POLYGONS_SQL_1
}
}
]);
this.testClient = new TestClient(this.mapConfig);
this.testClient.getLayergroup((err, body) => {
if (err) {
return done(err);
}
assert.equal(typeof body.metadata, 'object');
assert.ok(Array.isArray(body.metadata.layers));
body.metadata.layers.forEach(layer => assert.ok(!layer.meta.aggregation.mvt));
body.metadata.layers.forEach(layer => assert.ok(!layer.meta.aggregation.png));
const options = {
format: 'mvt'
};
this.testClient.getTile(0, 0, 0, options, (err, res, tile) => {
if (err) {
return done(err);
}
const tileJSON = tile.toJSON();
assert.equal(tileJSON[0].features.length, 7);
done();
});
});
});
it('should skip aggregation for polygons (w/o failing) and aggregate when the layer has points',
function (done) {
this.mapConfig = createVectorMapConfig([
{
type: 'cartodb',
options: {
sql: POLYGONS_SQL_1
}
},
{
type: 'cartodb',
options: {
sql: POINTS_SQL_1,
aggregation: {
threshold: 1
}
}
}
]);
this.testClient = new TestClient(this.mapConfig);
this.testClient.getLayergroup((err, body) => {
if (err) {
return done(err);
}
assert.equal(typeof body.metadata, 'object');
assert.ok(Array.isArray(body.metadata.layers));
assert.equal(body.metadata.layers[0].meta.aggregation.mvt, false);
assert.equal(body.metadata.layers[1].meta.aggregation.mvt, true);
const options = {
format: 'mvt'
};
this.testClient.getTile(0, 0, 0, options, (err, res, tile) => {
if (err) {
return done(err);
}
const tileJSON = tile.toJSON();
assert.equal(tileJSON[0].features.length, 7);
done();
});
});
});
});
});
});

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

204
test/acceptance/tilejson.js Normal file
View 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);
});
});
});
});

View File

@@ -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() {

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
# yarn lockfile v1
abaculus@cartodb/abaculus#2.0.3-cdb1:
"abaculus@github:cartodb/abaculus#2.0.3-cdb1":
version "2.0.3-cdb1"
resolved "https://codeload.github.com/cartodb/abaculus/tar.gz/f5f34e1c80cdd8d49edd1d6fe3b2220ab2e23aaf"
dependencies:
@@ -33,8 +33,8 @@ ajv@^4.9.1:
json-stable-stringify "^1.0.1"
ajv@^5.1.0:
version "5.5.1"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.1.tgz#b38bb8876d9e86bee994956a04e721e88b248eb2"
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"
@@ -226,7 +226,7 @@ camshaft@0.60.0:
dot "^1.0.3"
request "^2.69.0"
canvas@cartodb/node-canvas#1.6.2-cdb2:
"canvas@github:cartodb/node-canvas#1.6.2-cdb2":
version "1.6.2-cdb2"
resolved "https://codeload.github.com/cartodb/node-canvas/tar.gz/8acf04557005c633f9e68524488a2657c04f3766"
dependencies:
@@ -244,15 +244,15 @@ carto@0.16.3:
semver "^5.1.0"
yargs "^4.2.0"
carto@CartoDB/carto#0.15.1-cdb1:
"carto@github:cartodb/carto#0.15.1-cdb1":
version "0.15.1-cdb1"
resolved "https://codeload.github.com/CartoDB/carto/tar.gz/8050ec843f1f32a6469e5d1cf49602773015d398"
resolved "https://codeload.github.com/cartodb/carto/tar.gz/8050ec843f1f32a6469e5d1cf49602773015d398"
dependencies:
mapnik-reference "~6.0.2"
optimist "~0.6.0"
underscore "~1.6.0"
carto@cartodb/carto#0.15.1-cdb3:
"carto@github:cartodb/carto#0.15.1-cdb3":
version "0.15.1-cdb3"
resolved "https://codeload.github.com/cartodb/carto/tar.gz/945f5efb74fd1af1f5e1f69f409f9567f94fb5a7"
dependencies:
@@ -842,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"
@@ -1380,8 +1380,8 @@ mocha@~3.4.1:
supports-color "3.1.2"
moment@^2.10.6:
version "2.19.4"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.4.tgz#17e5e2c6ead8819c8ecfad83a0acccb312e94682"
version "2.20.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd"
moment@~2.18.1:
version "2.18.1"
@@ -2223,7 +2223,7 @@ through@2:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
tilelive-bridge@cartodb/tilelive-bridge#2.3.1-cdb4:
"tilelive-bridge@github:cartodb/tilelive-bridge#2.3.1-cdb4":
version "2.3.1-cdb4"
resolved "https://codeload.github.com/cartodb/tilelive-bridge/tar.gz/faa2b638da2d119b78281575d40255cb523f6ca6"
dependencies:
@@ -2231,7 +2231,7 @@ tilelive-bridge@cartodb/tilelive-bridge#2.3.1-cdb4:
mapnik-pool "~0.1.3"
sphericalmercator "1.0.x"
tilelive-mapnik@cartodb/tilelive-mapnik#0.6.18-cdb3:
"tilelive-mapnik@github:cartodb/tilelive-mapnik#0.6.18-cdb3":
version "0.6.18-cdb3"
resolved "https://codeload.github.com/cartodb/tilelive-mapnik/tar.gz/23bd1c31dd57d0b76c86b9f1eaf62462b3c17d01"
dependencies:
@@ -2392,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
@@ -2402,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"