Compare commits
304 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65308ea2eb | ||
|
|
8d16bf566d | ||
|
|
f2fa650661 | ||
|
|
aa57cdefb3 | ||
|
|
7c5b7641d8 | ||
|
|
eb4a49ec92 | ||
|
|
88f02458db | ||
|
|
1b405e42c2 | ||
|
|
bb5bfd10ee | ||
|
|
088a8b81a6 | ||
|
|
243e982bd6 | ||
|
|
dfe01c836c | ||
|
|
fcbf5ffcc5 | ||
|
|
90c9ad18e0 | ||
|
|
16a36a9d7a | ||
|
|
893b886a1e | ||
|
|
a60b335151 | ||
|
|
9ee0e2c3d0 | ||
|
|
565cfb7fbe | ||
|
|
0c8a31fad9 | ||
|
|
169b95809a | ||
|
|
23c0cb757d | ||
|
|
e18e86f565 | ||
|
|
496778c276 | ||
|
|
0ba4975360 | ||
|
|
e3d95fa654 | ||
|
|
cded5afdcb | ||
|
|
1b6de9961a | ||
|
|
a20e789302 | ||
|
|
f41af41bd4 | ||
|
|
c9e0f330c0 | ||
|
|
f9428682f9 | ||
|
|
330f8f3cb5 | ||
|
|
8270699b8e | ||
|
|
51c19c0b2e | ||
|
|
de376eef86 | ||
|
|
0a106cd038 | ||
|
|
1a78b8a75a | ||
|
|
e131df601c | ||
|
|
967d9b76e6 | ||
|
|
bee04e2553 | ||
|
|
37111f396d | ||
|
|
4df46fe5ea | ||
|
|
b1b2054f0a | ||
|
|
c1f2b96bfc | ||
|
|
804c6645fa | ||
|
|
5d6ccc07fd | ||
|
|
a585ba5480 | ||
|
|
448dcc7d82 | ||
|
|
0aaafa2068 | ||
|
|
1aa981d556 | ||
|
|
ccce598b04 | ||
|
|
667b2a9cb1 | ||
|
|
298882f410 | ||
|
|
6aaa5f99e2 | ||
|
|
22e3016cd3 | ||
|
|
d5c552a03a | ||
|
|
a5347c27e3 | ||
|
|
520e84e46b | ||
|
|
27521964c7 | ||
|
|
bdf4827300 | ||
|
|
172b3ece71 | ||
|
|
71146dbfaf | ||
|
|
38ca5db51b | ||
|
|
590233e3ee | ||
|
|
6f59c61c8b | ||
|
|
aff5fcda63 | ||
|
|
56d33b7f5b | ||
|
|
749b205944 | ||
|
|
ad0c035e2d | ||
|
|
d15ccd271e | ||
|
|
2aee357006 | ||
|
|
fc9dce0cca | ||
|
|
9149f72f42 | ||
|
|
743bb0723b | ||
|
|
0bf36fa058 | ||
|
|
970310bf7f | ||
|
|
4fc90db495 | ||
|
|
50ecdb5fee | ||
|
|
1ea4fc50c9 | ||
|
|
cda9a09b8e | ||
|
|
216c877f4b | ||
|
|
33fbff5011 | ||
|
|
c48e89826d | ||
|
|
52542e4a88 | ||
|
|
693a2e7bee | ||
|
|
f9ba3c41d3 | ||
|
|
ac153232d0 | ||
|
|
46289f27df | ||
|
|
05ccf20634 | ||
|
|
6acb873d95 | ||
|
|
65e8609fec | ||
|
|
677f6caab8 | ||
|
|
cb167313d2 | ||
|
|
2854d0252c | ||
|
|
717332d941 | ||
|
|
4607e4a12d | ||
|
|
3e7106002d | ||
|
|
08b91f935d | ||
|
|
1d08734721 | ||
|
|
b11b872b75 | ||
|
|
93bd2c9e50 | ||
|
|
658763da8c | ||
|
|
d2b5eaa8c3 | ||
|
|
eb5bf52bd9 | ||
|
|
c8000e5cf8 | ||
|
|
46c76d6a4c | ||
|
|
e6bec5ccb0 | ||
|
|
125587522f | ||
|
|
aeb9585708 | ||
|
|
8ed5df0072 | ||
|
|
6bbaeaa286 | ||
|
|
3d15551cb5 | ||
|
|
e0ffeb0adc | ||
|
|
e06f8fe25e | ||
|
|
da2228088e | ||
|
|
cdc39c8cae | ||
|
|
99fa66c026 | ||
|
|
d85a5d83b7 | ||
|
|
bb02494e02 | ||
|
|
39eb0f7bec | ||
|
|
5f7d5f6ec8 | ||
|
|
a4b2044e10 | ||
|
|
d1093686a3 | ||
|
|
12822c4341 | ||
|
|
fab87e2168 | ||
|
|
34e219353c | ||
|
|
3cf4a8f70b | ||
|
|
48172d4dc1 | ||
|
|
467bee4c91 | ||
|
|
3f2ef63976 | ||
|
|
235f5e4566 | ||
|
|
3f49743cd0 | ||
|
|
fb3afaa6ab | ||
|
|
b6c405bf68 | ||
|
|
da87a95dd9 | ||
|
|
cd7c604d10 | ||
|
|
b7227e0581 | ||
|
|
c564f5467a | ||
|
|
b04cc9c228 | ||
|
|
c0df0d12c6 | ||
|
|
5645cd16b3 | ||
|
|
eb6da1398e | ||
|
|
35c5cd34c2 | ||
|
|
6c51667ffb | ||
|
|
1396ca9fe3 | ||
|
|
56bb083239 | ||
|
|
ddcb812218 | ||
|
|
66c3d58b92 | ||
|
|
9326217c18 | ||
|
|
1207764c18 | ||
|
|
73a633ae7d | ||
|
|
3068ff1ea4 | ||
|
|
9ad6d0cbcc | ||
|
|
86389382fa | ||
|
|
c2bf7b075c | ||
|
|
294a222669 | ||
|
|
515146bf28 | ||
|
|
a1c08f9bf7 | ||
|
|
f8ff41be01 | ||
|
|
67ab12e8e7 | ||
|
|
d959ef5007 | ||
|
|
8cc4fe5b56 | ||
|
|
22a34d763c | ||
|
|
d30f710534 | ||
|
|
02304dc450 | ||
|
|
893fac31a7 | ||
|
|
abef8918c0 | ||
|
|
8380d291d0 | ||
|
|
251e636ad2 | ||
|
|
286059b8a3 | ||
|
|
b18bf967fd | ||
|
|
a81e98995a | ||
|
|
a797e13eb3 | ||
|
|
5e073f39bd | ||
|
|
8a88b29665 | ||
|
|
d77739dfa4 | ||
|
|
484e0fda2f | ||
|
|
3827901535 | ||
|
|
82648df21c | ||
|
|
1766cd0ad4 | ||
|
|
6af83d7630 | ||
|
|
28501f6b9d | ||
|
|
e3405ea2fc | ||
|
|
5c0f597cbb | ||
|
|
7289394f6a | ||
|
|
1ba1c488fa | ||
|
|
10f9f61e1e | ||
|
|
0e20958220 | ||
|
|
28cb05e45b | ||
|
|
c004e105ef | ||
|
|
f456237aa7 | ||
|
|
7f66189164 | ||
|
|
cac16f8b66 | ||
|
|
a706fd81ba | ||
|
|
43885f130b | ||
|
|
58be2b8fc5 | ||
|
|
78671aa499 | ||
|
|
d29da0bcc3 | ||
|
|
e9d0b3b77d | ||
|
|
4e6253b717 | ||
|
|
44eb323764 | ||
|
|
d4015085c7 | ||
|
|
b9c511ee60 | ||
|
|
64fe070ab2 | ||
|
|
5d750f3b98 | ||
|
|
664892bba9 | ||
|
|
38c50e0bec | ||
|
|
6c0e6210d6 | ||
|
|
f350206990 | ||
|
|
c8d2c9ea37 | ||
|
|
cab2d6d5d4 | ||
|
|
242e63716f | ||
|
|
c70b8cb5bf | ||
|
|
06138a82a8 | ||
|
|
678fbb1c8f | ||
|
|
2f310a15bd | ||
|
|
bf637ccd5b | ||
|
|
f387f2ee6f | ||
|
|
34d9e5a4eb | ||
|
|
54b7ee85c2 | ||
|
|
9083fc2e20 | ||
|
|
72a9a3e097 | ||
|
|
102228c55b | ||
|
|
148e6e6ae5 | ||
|
|
226653207a | ||
|
|
b93c09959c | ||
|
|
5abe25c316 | ||
|
|
1f03a6b181 | ||
|
|
16e8202782 | ||
|
|
ec8fcc7302 | ||
|
|
1c3f2b93e3 | ||
|
|
21720267cf | ||
|
|
3ce10690d6 | ||
|
|
6bfc5d8891 | ||
|
|
430e1513d8 | ||
|
|
c894414192 | ||
|
|
aa62529041 | ||
|
|
55f593eae6 | ||
|
|
f9d87bc40f | ||
|
|
783eb0eec7 | ||
|
|
c22a35489d | ||
|
|
482feabce2 | ||
|
|
0a753400e0 | ||
|
|
a21648ab4a | ||
|
|
b4d03c074a | ||
|
|
5463248578 | ||
|
|
e5cae8b8e3 | ||
|
|
ad1abb28af | ||
|
|
f824fc5243 | ||
|
|
4a2cc6a5f8 | ||
|
|
ca612dd02a | ||
|
|
fedcb0d0f9 | ||
|
|
1739cee11d | ||
|
|
178b9e8563 | ||
|
|
ac474cb253 | ||
|
|
79510185da | ||
|
|
84cd93b1b0 | ||
|
|
134cc9ac0c | ||
|
|
615229fc31 | ||
|
|
4600005a86 | ||
|
|
383c8305cc | ||
|
|
b94dfe066d | ||
|
|
de267917f4 | ||
|
|
3f6afb4530 | ||
|
|
540fda1e6c | ||
|
|
e0e67df91c | ||
|
|
4899c7ffef | ||
|
|
f0920aedef | ||
|
|
b236112069 | ||
|
|
0e8fb68794 | ||
|
|
f7b9287c93 | ||
|
|
85d4c81e58 | ||
|
|
ff19a8a2fe | ||
|
|
3bab081438 | ||
|
|
6dc9cc0b23 | ||
|
|
8fd35849c7 | ||
|
|
c09899913f | ||
|
|
0bdeee64a7 | ||
|
|
ee8619c470 | ||
|
|
9d81321d78 | ||
|
|
b0486f9bae | ||
|
|
2eb1c0f3e0 | ||
|
|
22b7828725 | ||
|
|
78404b1308 | ||
|
|
9bd862ffaf | ||
|
|
8139cdf8b2 | ||
|
|
a8898a8022 | ||
|
|
df5ec0f4d9 | ||
|
|
51ba3db4ac | ||
|
|
d31e52a625 | ||
|
|
3a8b99a14e | ||
|
|
fac1ab4a1c | ||
|
|
a9b0acc317 | ||
|
|
5cb2e5d3c5 | ||
|
|
e2ed0058d8 | ||
|
|
2f499a148a | ||
|
|
49204650c6 | ||
|
|
234576ab5f | ||
|
|
02cd6a43ad | ||
|
|
429f070372 | ||
|
|
3b9c561cee | ||
|
|
daeae5d95c | ||
|
|
e9d925334c |
@@ -1,4 +1,3 @@
|
||||
test/results/
|
||||
test/monkey/
|
||||
test/benchmark.js
|
||||
test/support/
|
||||
|
||||
31
.travis.yml
31
.travis.yml
@@ -1,27 +1,14 @@
|
||||
sudo: required
|
||||
dist: trusty
|
||||
addons:
|
||||
postgresql: "9.5"
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- postgresql-9.5-postgis-2.3
|
||||
- postgresql-plpython-9.5
|
||||
- pkg-config
|
||||
- libcairo2-dev
|
||||
- libjpeg8-dev
|
||||
- libgif-dev
|
||||
- libpango1.0-dev
|
||||
- g++-4.9
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
before_install:
|
||||
- createdb template_postgis
|
||||
- createuser publicuser
|
||||
- psql -c "CREATE EXTENSION postgis" template_postgis
|
||||
- docker pull cartoimages/windshaft-carto-testing
|
||||
|
||||
env:
|
||||
- NPROCS=1 JOBS=1 PGUSER=postgres CXX=g++-4.9
|
||||
script:
|
||||
- docker run -e POSTGIS_VERSION=2.4 -v `pwd`:/srv cartoimages/windshaft-carto-testing
|
||||
|
||||
language: generic
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
|
||||
2
Makefile
2
Makefile
@@ -16,7 +16,7 @@ config.status--test:
|
||||
./configure --environment=test
|
||||
|
||||
config/environments/test.js: config.status--test
|
||||
./config.status--test
|
||||
./config.status--test
|
||||
|
||||
TEST_SUITE := $(shell find test/{acceptance,integration,unit} -name "*.js")
|
||||
TEST_SUITE_UNIT := $(shell find test/unit -name "*.js")
|
||||
|
||||
53
NEWS.md
53
NEWS.md
@@ -1,5 +1,58 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
## 4.3.0
|
||||
Released 2017-12-11
|
||||
|
||||
Announcements:
|
||||
- Optimize Formula queries.
|
||||
- Optimize Formula queries in overviews.
|
||||
- Optimize Numeric Histogram queries.
|
||||
- Optimize Date Histogram queries.
|
||||
- Date Histograms: Now returns the same value for max/min/avg/timestamp per bin.
|
||||
- Date Histograms: Now it should return the same no matter the DB/Client time zone.
|
||||
|
||||
## 4.2.0
|
||||
Released 2017-12-04
|
||||
|
||||
Announcements:
|
||||
- Allow to request MVT tiles without CartoCSS
|
||||
- Upgrades windshaft to [4.1.0](https://github.com/CartoDB/windshaft/releases/tag/4.1.0).
|
||||
|
||||
|
||||
## 4.1.1
|
||||
Released 2017-11-29
|
||||
|
||||
Announcements:
|
||||
- Upgrades turbo-carto to [0.20.2](https://github.com/CartoDB/turbo-carto/releases/tag/0.20.2).
|
||||
|
||||
|
||||
## 4.1.0
|
||||
Released 2017-mm-dd
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [4.0.1](https://github.com/CartoDB/windshaft/releases/tag/4.0.1).
|
||||
- Add `categories` query param to define the number of categories to be ranked for aggregation dataviews.
|
||||
|
||||
|
||||
## 4.0.1
|
||||
Released 2017-10-18
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.59.4](https://github.com/CartoDB/camshaft/releases/tag/0.59.4).
|
||||
- Upgrades windshaft to [4.0.0](https://github.com/CartoDB/windshaft/releases/tag/4.0.0).
|
||||
- Split and move `req2params` method to multiple middlewares.
|
||||
- Use express error handler middleware to respond in case of something went wrong.
|
||||
- Use `res.locals` object to share info between middlewares and leave `req.params` as an object containing properties mapped to the named route params.
|
||||
- Move `LZMA` decompression to its own middleware.
|
||||
- Implement stats middleware removing some duplicated code while sending response.
|
||||
|
||||
|
||||
## 4.0.0
|
||||
Released 2017-10-04
|
||||
|
||||
|
||||
BIN
assets/render-timeout-fallback.mvt
Normal file
BIN
assets/render-timeout-fallback.mvt
Normal file
Binary file not shown.
@@ -107,6 +107,20 @@ var config = {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mvt: {
|
||||
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
|
||||
//PostGIS 2.4 is required for this to work
|
||||
//If disabled it will use Mapnik MVT generation
|
||||
usePostGIS: false,
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
},
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik backend
|
||||
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
|
||||
|
||||
@@ -101,6 +101,20 @@ var config = {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mvt: {
|
||||
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
|
||||
//PostGIS 2.4 is required for this to work
|
||||
//If disabled it will use Mapnik MVT generation
|
||||
usePostGIS: false,
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
},
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik backend
|
||||
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
|
||||
|
||||
@@ -101,6 +101,20 @@ var config = {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mvt: {
|
||||
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
|
||||
//PostGIS 2.4 is required for this to work
|
||||
//If disabled it will use Mapnik MVT generation
|
||||
usePostGIS: false,
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
},
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik backend
|
||||
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
|
||||
|
||||
@@ -100,6 +100,20 @@ var config = {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mvt: {
|
||||
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
|
||||
//PostGIS 2.4 is required for this to work
|
||||
//If disabled it will use Mapnik MVT generation
|
||||
usePostGIS: false,
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
},
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik backend
|
||||
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
|
||||
|
||||
11
docker-test.sh
Normal file
11
docker-test.sh
Normal file
@@ -0,0 +1,11 @@
|
||||
export NPROCS=1 && export JOBS=1 && export CXX=g++-4.9 && export PGUSER=postgres
|
||||
|
||||
npm install -g yarn@0.27.5
|
||||
yarn
|
||||
|
||||
/etc/init.d/postgresql start
|
||||
|
||||
createdb template_postgis && createuser publicuser
|
||||
psql -c "CREATE EXTENSION postgis" template_postgis
|
||||
|
||||
POSTGIS_VERSION=2.4 npm test
|
||||
@@ -17,3 +17,4 @@ You can create two types of maps with the Maps API:
|
||||
* [Anonymous Maps](anonymous_maps.md)
|
||||
* [Named Maps](named_maps.md)
|
||||
* [Static Maps API](static_maps_api.md)
|
||||
* [MapConfig File Format]([local file in the docs repo](https://github.com/CartoDB/docs/blob/master/_app/_mapsapi/06-mapconfig.md))
|
||||
|
||||
@@ -70,21 +70,203 @@ curl 'https://{username}.carto.com/api/v1/map' -H 'Content-Type: application/jso
|
||||
}
|
||||
```
|
||||
|
||||
### Retrieve resources from the layergroup
|
||||
## 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.
|
||||
|
||||
- **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.
|
||||
|
||||
- **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.
|
||||
|
||||
## 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
|
||||
### Mapnik tiles
|
||||
|
||||
These tiles will get just the Mapnik layers. To get individual layers, see the following section.
|
||||
These raster tiles retrieve just the Mapnik layers. 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
|
||||
```
|
||||
|
||||
#### Individual layers
|
||||
### Mapbox Vector Tiles (MVT)
|
||||
|
||||
The MapConfig specification holds the layers definition in a 0-based index. Layers can be requested individually in different formats depending on the layer type.
|
||||
[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.
|
||||
|
||||
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.
|
||||
|
||||
The following examples describe how to fetch MVT tiles with a cURL request.
|
||||
|
||||
#### MVT and Windshaft
|
||||
|
||||
CARTO uses Windshaft as the map tiler library to render multilayer maps with the Maps API. You can use Windshaft to request MVT using the same layer type that is used for requesting raster tiles (Mapnik layer). Simply change the file format `.mvt` in the URL.
|
||||
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/HASH/:layer/{z}/{x}/{y}.mvt
|
||||
```
|
||||
|
||||
The following example instantiates an anonymous map with layer options:
|
||||
|
||||
```bash
|
||||
{
|
||||
user_name: 'mycartodbuser',
|
||||
sublayers: [{
|
||||
sql: "SELECT * FROM table_name";
|
||||
cartocss: '#layer { marker-fill: #F0F0F0; }'
|
||||
}],
|
||||
maps_api_template: 'https://{user}.cartodb.com' // Optional
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: If no layer type is specified, Mapnik tiles are used by default. To access MVT tiles, specify `https://{username}.cartodb.com/api/v1/map/HASH/{z}/{x}/{y}.mvt` as the `maps_api_template` variable.
|
||||
|
||||
**Tip:** If you are using [Named Maps](https://carto.com/docs/carto-engine/maps-api/named-maps/) to instantiate a layer, indicate the MVT file format and layer in the response:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/named/:templateId/:layer/{z}/{x}/{y}.mvt
|
||||
```
|
||||
|
||||
For all layers in a Named Map, you must indicate Mapnik as the layer filter:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/named/:templateId/mapnik/{z}/{x}/{y}.mvt
|
||||
```
|
||||
|
||||
#### Layergroup Filter for MVT Tiles
|
||||
|
||||
To filter layers using Windshaft, use the following request where layers are numbered:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/HASH/0,1,2/{z}/{x}/{y}.mvt
|
||||
```
|
||||
|
||||
To request all layers, remove the layergroup filter parameter:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/HASH/{z}/{x}/{y}.mvt
|
||||
```
|
||||
|
||||
To filter a specific layer:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/HASH/2/{z}/{x}/{y}.mvt
|
||||
```
|
||||
|
||||
#### Example 1: MVT Tiles with Windshaft, CARTO.js, and MapboxGL
|
||||
|
||||
1) Import the required libraries:
|
||||
|
||||
```bash
|
||||
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.9.0/mapbox-gl.js'></script>
|
||||
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.9.0/mapbox-gl.css' rel='stylesheet' />
|
||||
<script src="http://libs.cartocdn.com/cartodb.js/v3/3.15/cartodb.core.js"></script>
|
||||
```
|
||||
|
||||
2) Configure Map Client:
|
||||
|
||||
```bash
|
||||
mapboxgl.accessToken = '{yourMapboxToken}';
|
||||
```
|
||||
|
||||
3) Create Map Object (Mapbox):
|
||||
|
||||
```bash
|
||||
var map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
zoom: 1,
|
||||
minZoom: 0,
|
||||
maxZoom: 18,
|
||||
center: [30, 0]
|
||||
});
|
||||
```
|
||||
|
||||
4) Define Layer Options (CARTO):
|
||||
|
||||
```bash
|
||||
var layerOptions = {
|
||||
user_name: "{username}",
|
||||
sublayers: [{
|
||||
sql: "SELECT * FROM {table_name}",
|
||||
cartocss: "...",
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
5) Request Tiles (from CARTO) and Set to Map Object (Mapbox):
|
||||
|
||||
**Note:** By default, [CARTO core functions](https://carto.com/docs/carto-engine/carto-js/core-api/) retrieve URLs for fully rendered tiles. You must replace the default format (.png) with the MVT format (.mvt).
|
||||
|
||||
|
||||
```bash
|
||||
cartodb.Tiles.getTiles(layerOptions, function(result, err) {
|
||||
var tiles = result.tiles.map(function(tileUrl) {
|
||||
return tileUrl
|
||||
.replace('{s}', 'a')
|
||||
.replace(/\.png/, '.mvt');
|
||||
});
|
||||
map.setStyle(simpleStyle(tiles));
|
||||
});
|
||||
```
|
||||
|
||||
#### Example 2: MVT Libraries with Windshaft and MapboxGL
|
||||
|
||||
When you are not including CARTO.js to implement MVT tiles, you must use the `map.setStyle` parameter to specify vector map rendering.
|
||||
|
||||
1) Import the required libraries:
|
||||
|
||||
```bash
|
||||
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.9.0/mapbox-gl.js'></script>
|
||||
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.9.0/mapbox-gl.css' rel='stylesheet'/>
|
||||
```
|
||||
|
||||
2) Configure Map Client:
|
||||
|
||||
```bash
|
||||
mapboxgl.accessToken = '{yourMapboxToken}';
|
||||
```
|
||||
|
||||
3) Create Map Object (Mapbox):
|
||||
|
||||
```bash
|
||||
var map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
zoom: 1,
|
||||
minZoom: 0,
|
||||
maxZoom: 18,
|
||||
center: [30, 0]
|
||||
});
|
||||
```
|
||||
|
||||
4) Set the Style
|
||||
|
||||
```bash
|
||||
map.setStyle({
|
||||
"version": 7,
|
||||
"glyphs": "...",
|
||||
"constants": {...},
|
||||
"sources": {
|
||||
"cartodb": {
|
||||
"type": "vector",
|
||||
"tiles": [ "http://{username}.cartodb.com/api/v1/map/named/templateId/mapnik/{z}/{x}/{y}.mvt"
|
||||
],
|
||||
"maxzoom": 18
|
||||
}
|
||||
},
|
||||
"layers": [{...}]
|
||||
});
|
||||
```
|
||||
|
||||
**Tip:** If you are using MapboxGL, see the following resource for additional information.
|
||||
|
||||
- [MapboxGL API Reference](https://www.mapbox.com/mapbox-gl-js/api/)
|
||||
- [MapboxGL Style Specifications](https://www.mapbox.com/mapbox-gl-js/style-spec/)
|
||||
- [Example of MapboxGL Implementation](https://www.mapbox.com/mapbox-gl-js/examples/)
|
||||
|
||||
### Individual layers
|
||||
|
||||
The MapConfig specification holds the layers definition in a 0-based index. Layers can be requested individually, in different formats, depending on the layer type.
|
||||
|
||||
Individual layers can be accessed using that 0-based index. For UTF grid tiles:
|
||||
|
||||
@@ -100,19 +282,19 @@ If the MapConfig had a Torque layer at index 1 it could be possible to request i
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/1/{z}/{x}/{y}.torque.json
|
||||
```
|
||||
|
||||
#### Attributes defined in `attributes` section
|
||||
### Attributes defined in `attributes` section
|
||||
|
||||
```bash
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/{layer}/attributes/{feature_id}
|
||||
```
|
||||
|
||||
Which returns JSON with the attributes defined, like:
|
||||
Which returns JSON with the attributes defined, such as:
|
||||
|
||||
```javascript
|
||||
{ "c": 1, "d": 2 }
|
||||
```
|
||||
|
||||
#### Blending and layer selection
|
||||
### Blending and layer selection
|
||||
|
||||
```bash
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/{layer_filter}/{z}/{x}/{y}.png
|
||||
@@ -141,10 +323,7 @@ https://{username}.carto.com/api/v1/map/{layergroupid}/0,3,4/{z}/{x}/{y}.png
|
||||
Some notes about filtering:
|
||||
|
||||
- Invalid index values or out of bounds indexes will end in `Invalid layer filtering` errors.
|
||||
- Ordering is not considered. So right now filtering layers 0,3,4 is the very same thing as filtering 3,4,0. As this
|
||||
may change in the future **it is recommended** to always select the layers in ascending order so you will get a
|
||||
consistent behavior in the future.
|
||||
|
||||
- Ordering is not considered. So right now filtering layers 0,3,4 is the very same thing as filtering 3,4,0. As this may change in the future, **it is recommended** to always select the layers in ascending order so that you will always get consistent behavior.
|
||||
|
||||
## Create JSONP
|
||||
|
||||
@@ -185,7 +364,6 @@ callback({
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
## Remove
|
||||
|
||||
Anonymous Maps cannot be removed by an API call. They will expire after about five minutes, or sometimes longer. If an Anonymous Map expires and tiles are requested from it, an error will be raised. This could happen if a user leaves a map open and after time, returns to the map and attempts to interact with it in a way that requires new tiles (e.g. zoom). The client will need to go through the steps of creating the map again to fix the problem.
|
||||
|
||||
@@ -22,6 +22,6 @@ Errors are reported using standard HTTP codes and extended information encoded i
|
||||
|
||||
If you use JSONP, the 200 HTTP code is always returned so the JavaScript client can receive errors from the JSON object.
|
||||
|
||||
## CORS support
|
||||
## CORS Support
|
||||
|
||||
All the endpoints, which might be accessed using a web browser, add CORS headers and allow OPTIONS method.
|
||||
|
||||
@@ -152,7 +152,8 @@ It is important to note that generated images are cached from the live data refe
|
||||
* Timeout limits for generating static maps are the same across CARTO Builder and CARTO Engine. It is important to ensure timely processing of queries.
|
||||
* If you are publishing your map as a static image with the API, you must manually add [attributions](https://carto.com/attribution) for your static map image. For example, add the following attribution code:
|
||||
|
||||
{% highlight javascript %}attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="https://carto.com/attributions">CARTO</a>
|
||||
{% highlight javascript %}
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="https://carto.com/attributions">CARTO</a>
|
||||
{% endhighlight %}
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -19,22 +19,22 @@ function AuthApi(pgConnection, metadataBackend, mapStore, templateMaps) {
|
||||
|
||||
module.exports = AuthApi;
|
||||
|
||||
// Check if a request is authorized by a signer
|
||||
// Check if the user is authorized by a signer
|
||||
//
|
||||
// @param req express request object
|
||||
// @param res express response object
|
||||
// @param callback function(err, signed_by) signed_by will be
|
||||
// null if the request is not signed by anyone
|
||||
// or will be a string cartodb username otherwise.
|
||||
//
|
||||
AuthApi.prototype.authorizedBySigner = function(req, callback) {
|
||||
if ( ! req.params.token || ! req.params.signer ) {
|
||||
AuthApi.prototype.authorizedBySigner = function(res, callback) {
|
||||
if ( ! res.locals.token || ! res.locals.signer ) {
|
||||
return callback(null, false); // no signer requested
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
var layergroup_id = req.params.token;
|
||||
var auth_token = req.params.auth_token;
|
||||
var layergroup_id = res.locals.token;
|
||||
var auth_token = res.locals.auth_token;
|
||||
|
||||
this.mapStore.load(layergroup_id, function(err, mapConfig) {
|
||||
if (err) {
|
||||
@@ -84,11 +84,12 @@ AuthApi.prototype.authorizedByAPIKey = function(user, req, callback) {
|
||||
* Check access authorization
|
||||
*
|
||||
* @param req - standard req object. Importantly contains table and host information
|
||||
* @param res - standard res object. Contains the auth parameters in locals
|
||||
* @param callback function(err, allowed) is access allowed not?
|
||||
*/
|
||||
AuthApi.prototype.authorize = function(req, callback) {
|
||||
AuthApi.prototype.authorize = function(req, res, callback) {
|
||||
var self = this;
|
||||
var user = req.context.user;
|
||||
var user = res.locals.user;
|
||||
|
||||
step(
|
||||
function () {
|
||||
@@ -101,11 +102,11 @@ AuthApi.prototype.authorize = function(req, callback) {
|
||||
// if not authorized by api_key, continue
|
||||
if (!authorized) {
|
||||
// not authorized by api_key, check if authorized by signer
|
||||
return self.authorizedBySigner(req, this);
|
||||
return self.authorizedBySigner(res, this);
|
||||
}
|
||||
|
||||
// authorized by api key, login as the given username and stop
|
||||
self.pgConnection.setDBAuth(user, req.params, function(err) {
|
||||
self.pgConnection.setDBAuth(user, res.locals, function(err) {
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
},
|
||||
@@ -120,7 +121,7 @@ AuthApi.prototype.authorize = function(req, callback) {
|
||||
// if no signer name was given, let dbparams and
|
||||
// PostgreSQL do the rest.
|
||||
//
|
||||
if ( ! req.params.signer ) {
|
||||
if ( ! res.locals.signer ) {
|
||||
return callback(null, true); // authorized so far
|
||||
}
|
||||
|
||||
@@ -128,7 +129,7 @@ AuthApi.prototype.authorize = function(req, callback) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
self.pgConnection.setDBAuth(user, req.params, function(err) {
|
||||
self.pgConnection.setDBAuth(user, res.locals, function(err) {
|
||||
req.profiler.done('setDBAuth');
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
|
||||
@@ -94,7 +94,7 @@ function getQueryRewriteData(mapConfig, dataviewDefinition, params) {
|
||||
}
|
||||
|
||||
function getOverrideParams(params, ownFilter) {
|
||||
var overrideParams = _.reduce(_.pick(params, 'start', 'end', 'bins', 'offset'),
|
||||
var overrideParams = _.reduce(_.pick(params, 'start', 'end', 'bins', 'offset', 'categories'),
|
||||
function castNumbers(overrides, val, k) {
|
||||
if (!Number.isFinite(+val)) {
|
||||
throw new Error('Invalid number format for parameter \'' + k + '\'');
|
||||
@@ -113,9 +113,7 @@ function getOverrideParams(params, ownFilter) {
|
||||
return overrideParams;
|
||||
}
|
||||
|
||||
DataviewBackend.prototype.search = function (mapConfigProvider, user, params, callback) {
|
||||
var dataviewName = params.dataviewName;
|
||||
|
||||
DataviewBackend.prototype.search = function (mapConfigProvider, user, dataviewName, params, callback) {
|
||||
step(
|
||||
function getMapConfig() {
|
||||
mapConfigProvider.getMapConfig(this);
|
||||
|
||||
@@ -1,152 +1,150 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
var PSQL = require('cartodb-psql');
|
||||
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
function AnalysesController(authApi, pgConnection) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
function AnalysesController(prepareContext) {
|
||||
this.prepareContext = prepareContext;
|
||||
}
|
||||
|
||||
util.inherits(AnalysesController, BaseController);
|
||||
|
||||
module.exports = AnalysesController;
|
||||
|
||||
AnalysesController.prototype.register = function(app) {
|
||||
app.get(app.base_url_mapconfig + '/analyses/catalog', cors(), userMiddleware, this.catalog.bind(this));
|
||||
};
|
||||
|
||||
AnalysesController.prototype.sendResponse = function(req, res, resource) {
|
||||
res.set('Cache-Control', 'public,max-age=10,must-revalidate');
|
||||
this.send(req, res, resource, 200);
|
||||
};
|
||||
|
||||
AnalysesController.prototype.catalog = function(req, res) {
|
||||
var self = this;
|
||||
var username = req.context.user;
|
||||
|
||||
step(
|
||||
function reqParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function catalogQuery(err) {
|
||||
assert.ifError(err);
|
||||
var pg = new PSQL(dbParamsFromReqParams(req.params));
|
||||
getMetadata(username, pg, this);
|
||||
},
|
||||
function prepareResponse(err, results) {
|
||||
assert.ifError(err);
|
||||
|
||||
var analysisIdToTable = results.tables.reduce(function(analysisIdToTable, table) {
|
||||
var analysisId = table.relname.split('_')[2];
|
||||
if (analysisId && analysisId.length === 40) {
|
||||
analysisIdToTable[analysisId] = table;
|
||||
}
|
||||
return analysisIdToTable;
|
||||
}, {});
|
||||
|
||||
var catalogWithTables = results.catalog.map(function(analysis) {
|
||||
if (analysisIdToTable.hasOwnProperty(analysis.node_id)) {
|
||||
analysis.table = analysisIdToTable[analysis.node_id];
|
||||
}
|
||||
return analysis;
|
||||
});
|
||||
|
||||
return catalogWithTables.sort(function(analysisA, analysisB) {
|
||||
if (!!analysisA.table && !!analysisB.table) {
|
||||
return analysisB.table.size - analysisA.table.size;
|
||||
}
|
||||
|
||||
if (!!analysisA.table) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!!analysisB.table) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
});
|
||||
},
|
||||
function sendResponse(err, catalogWithTables) {
|
||||
if (err) {
|
||||
if (err.message.match(/permission\sdenied/)) {
|
||||
err = new Error('Unauthorized');
|
||||
err.http_status = 401;
|
||||
}
|
||||
self.sendError(req, res, err);
|
||||
} else {
|
||||
self.sendResponse(req, res, { catalog: catalogWithTables });
|
||||
}
|
||||
}
|
||||
AnalysesController.prototype.register = function (app) {
|
||||
app.get(
|
||||
`${app.base_url_mapconfig}/analyses/catalog`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.prepareContext,
|
||||
this.createPGClient(),
|
||||
this.getDataFromQuery({ queryTemplate: catalogQueryTpl, key: 'catalog' }),
|
||||
this.getDataFromQuery({ queryTemplate: tablesQueryTpl, key: 'tables' }),
|
||||
this.prepareResponse(),
|
||||
this.setCacheControlHeader(),
|
||||
this.sendResponse(),
|
||||
this.unathorizedError()
|
||||
);
|
||||
};
|
||||
|
||||
var catalogQueryTpl = dot.template(
|
||||
'SELECT analysis_def->>\'type\' as type, * FROM cartodb.cdb_analysis_catalog WHERE username = \'{{=it._username}}\''
|
||||
);
|
||||
|
||||
var tablesQueryTpl = dot.template([
|
||||
"WITH analysis_tables AS (",
|
||||
" SELECT",
|
||||
" n.nspname AS nspname,",
|
||||
" c.relname AS relname,",
|
||||
" pg_total_relation_size(",
|
||||
" format('%s.%s', pg_catalog.quote_ident(n.nspname), pg_catalog.quote_ident(c.relname))",
|
||||
" ) AS size,",
|
||||
" format('%s.%s', pg_catalog.quote_ident(nspname), pg_catalog.quote_ident(relname)) AS fully_qualified_name",
|
||||
" FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n",
|
||||
" WHERE c.relnamespace = n.oid",
|
||||
" AND pg_catalog.quote_ident(c.relname) ~ '^analysis_[a-z0-9]{10}_[a-z0-9]{40}$'",
|
||||
" AND n.nspname IN ('{{=it._username}}', 'public')",
|
||||
")",
|
||||
"SELECT *, pg_size_pretty(size) as size_pretty",
|
||||
"FROM analysis_tables",
|
||||
"ORDER BY size DESC"
|
||||
].join('\n'));
|
||||
|
||||
|
||||
function getMetadata(username, pg, callback) {
|
||||
var results = {
|
||||
catalog: [],
|
||||
tables: []
|
||||
AnalysesController.prototype.createPGClient = function () {
|
||||
return function createPGClientMiddleware (req, res, next) {
|
||||
res.locals.pg = new PSQL(dbParamsFromReqParams(res.locals));
|
||||
next();
|
||||
};
|
||||
step(
|
||||
function getCatalog() {
|
||||
pg.query(catalogQueryTpl({_username: username}), this, true); // use read-only transaction
|
||||
},
|
||||
function handleCatalog(err, resultSet) {
|
||||
assert.ifError(err);
|
||||
resultSet = resultSet || {};
|
||||
results.catalog = resultSet.rows || [];
|
||||
this();
|
||||
},
|
||||
function getTables(err) {
|
||||
assert.ifError(err);
|
||||
pg.query(tablesQueryTpl({_username: username}), this, true); // use read-only transaction
|
||||
},
|
||||
function handleTables(err, resultSet) {
|
||||
assert.ifError(err);
|
||||
resultSet = resultSet || {};
|
||||
results.tables = resultSet.rows || [];
|
||||
this();
|
||||
},
|
||||
function finish(err) {
|
||||
};
|
||||
|
||||
AnalysesController.prototype.getDataFromQuery = function ({ queryTemplate, key }) {
|
||||
const readOnlyTransactionOn = true;
|
||||
|
||||
return function getCatalogMiddleware(req, res, next) {
|
||||
const { pg, user } = res.locals;
|
||||
const sql = queryTemplate({ _username: user });
|
||||
|
||||
pg.query(sql, (err, resultSet = {}) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return callback(null, results);
|
||||
}
|
||||
);
|
||||
}
|
||||
res.locals[key] = resultSet.rows || [];
|
||||
|
||||
next();
|
||||
}, readOnlyTransactionOn);
|
||||
};
|
||||
};
|
||||
|
||||
AnalysesController.prototype.prepareResponse = function () {
|
||||
return function prepareResponseMiddleware (req, res, next) {
|
||||
const { catalog, tables } = res.locals;
|
||||
|
||||
const analysisIdToTable = tables.reduce((analysisIdToTable, table) => {
|
||||
const analysisId = table.relname.split('_')[2];
|
||||
|
||||
if (analysisId && analysisId.length === 40) {
|
||||
analysisIdToTable[analysisId] = table;
|
||||
}
|
||||
|
||||
return analysisIdToTable;
|
||||
}, {});
|
||||
|
||||
const analysisCatalog = catalog.map(analysis => {
|
||||
if (analysisIdToTable.hasOwnProperty(analysis.node_id)) {
|
||||
analysis.table = analysisIdToTable[analysis.node_id];
|
||||
}
|
||||
|
||||
return analysis;
|
||||
})
|
||||
.sort((analysisA, analysisB) => {
|
||||
if (!!analysisA.table && !!analysisB.table) {
|
||||
return analysisB.table.size - analysisA.table.size;
|
||||
}
|
||||
|
||||
if (!!analysisA.table) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!!analysisB.table) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
});
|
||||
|
||||
res.body = { catalog: analysisCatalog };
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
AnalysesController.prototype.setCacheControlHeader = function () {
|
||||
return function setCacheControlHeaderMiddleware (req, res, next) {
|
||||
res.set('Cache-Control', 'public,max-age=10,must-revalidate');
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
AnalysesController.prototype.sendResponse = function() {
|
||||
return function sendResponseMiddleware (req, res) {
|
||||
res.status(200);
|
||||
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(res.body);
|
||||
} else {
|
||||
res.json(res.body);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
AnalysesController.prototype.unathorizedError = function () {
|
||||
return function unathorizedErrorMiddleware(err, req, res, next) {
|
||||
if (err.message.match(/permission\sdenied/)) {
|
||||
err = new Error('Unauthorized');
|
||||
err.http_status = 401;
|
||||
}
|
||||
|
||||
next(err);
|
||||
};
|
||||
};
|
||||
|
||||
const catalogQueryTpl = ctx => `
|
||||
SELECT analysis_def->>'type' as type, * FROM cdb_analysis_catalog WHERE username = '${ctx._username}'
|
||||
`;
|
||||
|
||||
var tablesQueryTpl = ctx => `
|
||||
WITH analysis_tables AS (
|
||||
SELECT
|
||||
n.nspname AS nspname,
|
||||
c.relname AS relname,
|
||||
pg_total_relation_size(
|
||||
format('%s.%s', pg_catalog.quote_ident(n.nspname), pg_catalog.quote_ident(c.relname))
|
||||
) AS size,
|
||||
format('%s.%s', pg_catalog.quote_ident(nspname), pg_catalog.quote_ident(relname)) AS fully_qualified_name
|
||||
FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n
|
||||
WHERE c.relnamespace = n.oid
|
||||
AND pg_catalog.quote_ident(c.relname) ~ '^analysis_[a-z0-9]{10}_[a-z0-9]{40}$'
|
||||
AND n.nspname IN ('${ctx._username}', 'public')
|
||||
)
|
||||
SELECT *, pg_size_pretty(size) as size_pretty
|
||||
FROM analysis_tables
|
||||
ORDER BY size DESC
|
||||
`;
|
||||
|
||||
function dbParamsFromReqParams(params) {
|
||||
var dbParams = {};
|
||||
|
||||
@@ -1,301 +0,0 @@
|
||||
var assert = require('assert');
|
||||
|
||||
var _ = require('underscore');
|
||||
var step = require('step');
|
||||
var debug = require('debug')('windshaft:cartodb');
|
||||
|
||||
// Whitelist query parameters and attach format
|
||||
var REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'config',
|
||||
'map_key',
|
||||
'api_key',
|
||||
'auth_token',
|
||||
'callback',
|
||||
'zoom',
|
||||
'lon',
|
||||
'lat',
|
||||
// analysis
|
||||
'filters' // json
|
||||
];
|
||||
|
||||
function BaseController(authApi, pgConnection) {
|
||||
this.authApi = authApi;
|
||||
this.pgConnection = pgConnection;
|
||||
}
|
||||
|
||||
module.exports = BaseController;
|
||||
|
||||
// jshint maxcomplexity:8
|
||||
/**
|
||||
* Whitelist input and get database name & default geometry type from
|
||||
* subdomain/user metadata held in CartoDB Redis
|
||||
* @param req - standard express request obj. Should have host & table
|
||||
* @param callback
|
||||
*/
|
||||
BaseController.prototype.req2params = function(req, callback){
|
||||
var self = this;
|
||||
|
||||
var allowedQueryParams = REQUEST_QUERY_PARAMS_WHITELIST;
|
||||
if (Array.isArray(req.context.allowedQueryParams)) {
|
||||
allowedQueryParams = allowedQueryParams.concat(req.context.allowedQueryParams);
|
||||
}
|
||||
req.query = _.pick(req.query, allowedQueryParams);
|
||||
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
|
||||
|
||||
var user = req.context.user;
|
||||
|
||||
if ( req.params.token ) {
|
||||
// Token might match the following patterns:
|
||||
// - {user}@{tpl_id}@{token}:{cache_buster}
|
||||
var tksplit = req.params.token.split(':');
|
||||
req.params.token = tksplit[0];
|
||||
if ( tksplit.length > 1 ) {
|
||||
req.params.cache_buster= tksplit[1];
|
||||
}
|
||||
tksplit = req.params.token.split('@');
|
||||
if ( tksplit.length > 1 ) {
|
||||
req.params.signer = tksplit.shift();
|
||||
if ( ! req.params.signer ) {
|
||||
req.params.signer = user;
|
||||
}
|
||||
else if ( req.params.signer !== user ) {
|
||||
var err = new Error(
|
||||
'Cannot use map signature of user "' + req.params.signer + '" on db of user "' + user + '"'
|
||||
);
|
||||
err.http_status = 403;
|
||||
req.profiler.done('req2params');
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
if ( tksplit.length > 1 ) {
|
||||
/*var template_hash = */tksplit.shift(); // unused
|
||||
}
|
||||
req.params.token = tksplit.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// bring all query values onto req.params object
|
||||
_.extend(req.params, req.query);
|
||||
|
||||
req.profiler.done('req2params.setup');
|
||||
|
||||
step(
|
||||
function getPrivacy(){
|
||||
self.authApi.authorize(req, this);
|
||||
},
|
||||
function validateAuthorization(err, authorized) {
|
||||
req.profiler.done('authorize');
|
||||
assert.ifError(err);
|
||||
if(!authorized) {
|
||||
err = new Error("Sorry, you are unauthorized (permission denied)");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function getDatabase(err){
|
||||
assert.ifError(err);
|
||||
self.pgConnection.setDBConn(user, req.params, this);
|
||||
},
|
||||
function finishSetup(err) {
|
||||
if ( err ) {
|
||||
req.profiler.done('req2params');
|
||||
return callback(err, req);
|
||||
}
|
||||
|
||||
// Add default database connection parameters
|
||||
// if none given
|
||||
_.defaults(req.params, {
|
||||
dbuser: global.environment.postgres.user,
|
||||
dbpassword: global.environment.postgres.password,
|
||||
dbhost: global.environment.postgres.host,
|
||||
dbport: global.environment.postgres.port
|
||||
});
|
||||
|
||||
req.profiler.done('req2params');
|
||||
callback(null, req);
|
||||
}
|
||||
);
|
||||
};
|
||||
// jshint maxcomplexity:6
|
||||
|
||||
// jshint maxcomplexity:9
|
||||
BaseController.prototype.send = function(req, res, body, status, headers) {
|
||||
if (req.params.dbhost) {
|
||||
res.set('X-Served-By-DB-Host', req.params.dbhost);
|
||||
}
|
||||
|
||||
res.set('X-Tiler-Profiler', req.profiler.toJSONString());
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
res.status(status);
|
||||
|
||||
if (!Buffer.isBuffer(body) && typeof body === 'object') {
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(body);
|
||||
} else {
|
||||
res.json(body);
|
||||
}
|
||||
} else {
|
||||
res.send(body);
|
||||
}
|
||||
|
||||
try {
|
||||
// May throw due to dns, see
|
||||
// See http://github.com/CartoDB/Windshaft/issues/166
|
||||
req.profiler.sendStats();
|
||||
} catch (err) {
|
||||
debug("error sending profiling stats: " + err);
|
||||
}
|
||||
};
|
||||
// jshint maxcomplexity:6
|
||||
|
||||
BaseController.prototype.sendError = function(req, res, err, label) {
|
||||
var allErrors = Array.isArray(err) ? err : [err];
|
||||
|
||||
allErrors = populateTimeoutErrors(allErrors);
|
||||
|
||||
label = label || 'UNKNOWN';
|
||||
err = allErrors[0] || new Error(label);
|
||||
allErrors[0] = err;
|
||||
|
||||
var statusCode = findStatusCode(err);
|
||||
|
||||
if (err.message === 'Tile does not exist' && req.params.format === 'mvt') {
|
||||
statusCode = 204;
|
||||
}
|
||||
|
||||
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
|
||||
|
||||
// If a callback was requested, force status to 200
|
||||
if (req.query && req.query.callback) {
|
||||
statusCode = 200;
|
||||
}
|
||||
|
||||
var errorResponseBody = {
|
||||
errors: allErrors.map(errorMessage),
|
||||
errors_with_context: allErrors.map(errorMessageWithContext)
|
||||
};
|
||||
|
||||
this.send(req, res, errorResponseBody, statusCode);
|
||||
};
|
||||
|
||||
function stripConnectionInfo(message) {
|
||||
// Strip connection info, if any
|
||||
return message
|
||||
// See https://github.com/CartoDB/Windshaft/issues/173
|
||||
.replace(/Connection string: '[^']*'\n\s/im, '')
|
||||
// See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644
|
||||
.replace(/is the server.*encountered/im, 'encountered');
|
||||
}
|
||||
|
||||
var ERROR_INFO_TO_EXPOSE = {
|
||||
message: true,
|
||||
layer: true,
|
||||
type: true,
|
||||
analysis: true,
|
||||
subtype: true
|
||||
};
|
||||
|
||||
function shouldBeExposed (prop) {
|
||||
return !!ERROR_INFO_TO_EXPOSE[prop];
|
||||
}
|
||||
|
||||
function errorMessage(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
|
||||
return stripConnectionInfo(message);
|
||||
}
|
||||
|
||||
function errorMessageWithContext(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
|
||||
var error = {
|
||||
type: err.type || 'unknown',
|
||||
message: stripConnectionInfo(message),
|
||||
};
|
||||
|
||||
for (var prop in err) {
|
||||
// type & message are properties from Error's prototype and will be skipped
|
||||
if (err.hasOwnProperty(prop) && shouldBeExposed(prop)) {
|
||||
error[prop] = err[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
module.exports.errorMessage = errorMessage;
|
||||
|
||||
function findStatusCode(err) {
|
||||
var statusCode;
|
||||
if ( err.http_status ) {
|
||||
statusCode = err.http_status;
|
||||
} else {
|
||||
statusCode = statusFromErrorMessage('' + err);
|
||||
}
|
||||
return statusCode;
|
||||
}
|
||||
module.exports.findStatusCode = findStatusCode;
|
||||
|
||||
function statusFromErrorMessage(errMsg) {
|
||||
// Find an appropriate statusCode based on message
|
||||
// jshint maxcomplexity:7
|
||||
var statusCode = 400;
|
||||
if ( -1 !== errMsg.indexOf('permission denied') ) {
|
||||
statusCode = 403;
|
||||
}
|
||||
else if ( -1 !== errMsg.indexOf('authentication failed') ) {
|
||||
statusCode = 403;
|
||||
}
|
||||
else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) {
|
||||
statusCode = 400;
|
||||
}
|
||||
else if ( -1 !== errMsg.indexOf('does not exist') ) {
|
||||
if ( -1 !== errMsg.indexOf(' role ') ) {
|
||||
statusCode = 403; // role 'xxx' does not exist
|
||||
} else if ( errMsg.match(/function .* does not exist/) ) {
|
||||
statusCode = 400; // invalid SQL (SQL function does not exist)
|
||||
} else {
|
||||
statusCode = 404;
|
||||
}
|
||||
}
|
||||
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
function isRenderTimeoutError (err) {
|
||||
return err.message === 'Render timed out';
|
||||
}
|
||||
|
||||
function isDatasourceTimeoutError (err) {
|
||||
return err.message && err.message.match(/canceling statement due to statement timeout/i);
|
||||
}
|
||||
|
||||
function isTimeoutError (err) {
|
||||
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
|
||||
}
|
||||
|
||||
function populateTimeoutErrors (errors) {
|
||||
return errors.map(function (error) {
|
||||
if (isRenderTimeoutError(error)) {
|
||||
error.subtype = 'render';
|
||||
}
|
||||
|
||||
if (isDatasourceTimeoutError(error)) {
|
||||
error.subtype = 'datasource';
|
||||
}
|
||||
|
||||
if (isTimeoutError(error)) {
|
||||
error.message = 'You are over platform\'s limits. Please contact us to know more details';
|
||||
error.type = 'limit';
|
||||
error.http_status = 429;
|
||||
}
|
||||
|
||||
return error;
|
||||
});
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
var allowQueryParams = require('../middleware/allow-query-params');
|
||||
var vectorError = require('../middleware/vector-error');
|
||||
|
||||
var DataviewBackend = require('../backends/dataview');
|
||||
var AnalysisStatusBackend = require('../backends/analysis-status');
|
||||
@@ -28,10 +26,8 @@ var QueryTables = require('cartodb-query-tables');
|
||||
* @param {AnalysisBackend} analysisBackend
|
||||
* @constructor
|
||||
*/
|
||||
function LayergroupController(authApi, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
|
||||
function LayergroupController(prepareContext, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, analysisBackend) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
this.pgConnection = pgConnection;
|
||||
this.mapStore = mapStore;
|
||||
this.tileBackend = tileBackend;
|
||||
@@ -43,39 +39,66 @@ function LayergroupController(authApi, pgConnection, mapStore, tileBackend, prev
|
||||
|
||||
this.dataviewBackend = new DataviewBackend(analysisBackend);
|
||||
this.analysisStatusBackend = new AnalysisStatusBackend();
|
||||
}
|
||||
|
||||
util.inherits(LayergroupController, BaseController);
|
||||
this.prepareContext = prepareContext;
|
||||
}
|
||||
|
||||
module.exports = LayergroupController;
|
||||
|
||||
|
||||
LayergroupController.prototype.register = function(app) {
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:z/:x/:y@:scale_factor?x.:format', cors(), userMiddleware,
|
||||
this.tile.bind(this));
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:z/:x/:y@:scale_factor?x.:format',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.prepareContext,
|
||||
this.tile.bind(this),
|
||||
vectorError()
|
||||
);
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:z/:x/:y.:format', cors(), userMiddleware,
|
||||
this.tile.bind(this));
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:z/:x/:y.:format',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.prepareContext,
|
||||
this.tile.bind(this),
|
||||
vectorError()
|
||||
);
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/:z/:x/:y.(:format)', cors(), userMiddleware,
|
||||
this.layer.bind(this));
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:layer/:z/:x/:y.(:format)',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
validateLayerRouteMiddleware,
|
||||
this.prepareContext,
|
||||
this.layer.bind(this),
|
||||
vectorError()
|
||||
);
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/attributes/:fid', cors(), userMiddleware,
|
||||
this.attributes.bind(this));
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:layer/attributes/:fid',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.prepareContext,
|
||||
this.attributes.bind(this)
|
||||
);
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/static/center/:token/:z/:lat/:lng/:width/:height.:format',
|
||||
cors(), userMiddleware, allowQueryParams(['layer']),
|
||||
this.center.bind(this));
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/static/center/:token/:z/:lat/:lng/:width/:height.:format',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(['layer']),
|
||||
this.prepareContext,
|
||||
this.center.bind(this)
|
||||
);
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format',
|
||||
cors(), userMiddleware, allowQueryParams(['layer']),
|
||||
this.bbox.bind(this));
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(['layer']),
|
||||
this.prepareContext,
|
||||
this.bbox.bind(this)
|
||||
);
|
||||
|
||||
// Undocumented/non-supported API endpoint methods.
|
||||
// Use at your own peril.
|
||||
@@ -90,7 +113,8 @@ LayergroupController.prototype.register = function(app) {
|
||||
'bins', // number
|
||||
'aggregation', //string
|
||||
'offset', // number
|
||||
'q' // widgets search
|
||||
'q', // widgets search
|
||||
'categories', // number
|
||||
];
|
||||
|
||||
app.get(
|
||||
@@ -98,6 +122,7 @@ LayergroupController.prototype.register = function(app) {
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.prepareContext,
|
||||
this.dataview.bind(this)
|
||||
);
|
||||
|
||||
@@ -106,6 +131,7 @@ LayergroupController.prototype.register = function(app) {
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.prepareContext,
|
||||
this.dataview.bind(this)
|
||||
);
|
||||
|
||||
@@ -114,6 +140,7 @@ LayergroupController.prototype.register = function(app) {
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.prepareContext,
|
||||
this.dataviewSearch.bind(this)
|
||||
);
|
||||
|
||||
@@ -122,30 +149,32 @@ LayergroupController.prototype.register = function(app) {
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.prepareContext,
|
||||
this.dataviewSearch.bind(this)
|
||||
);
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/analysis/node/:nodeId', cors(), userMiddleware,
|
||||
this.analysisNodeStatus.bind(this));
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/analysis/node/:nodeId',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.prepareContext,
|
||||
this.analysisNodeStatus.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.analysisNodeStatus = function(req, res) {
|
||||
LayergroupController.prototype.analysisNodeStatus = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function retrieveNodeStatus(err) {
|
||||
assert.ifError(err);
|
||||
self.analysisStatusBackend.getNodeStatus(req.params, this);
|
||||
function retrieveNodeStatus() {
|
||||
self.analysisStatusBackend.getNodeStatus(res.locals, this);
|
||||
},
|
||||
function finish(err, nodeStatus, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET NODE STATUS');
|
||||
err.label = 'GET NODE STATUS';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, nodeStatus, 200, {
|
||||
'Cache-Control': 'public,max-age=5',
|
||||
@@ -156,54 +185,50 @@ LayergroupController.prototype.analysisNodeStatus = function(req, res) {
|
||||
);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.dataview = function(req, res) {
|
||||
LayergroupController.prototype.dataview = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function retrieveDataview(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
function retrieveDataview() {
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
||||
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
|
||||
);
|
||||
self.dataviewBackend.getDataview(
|
||||
mapConfigProvider,
|
||||
res.locals.user,
|
||||
res.locals,
|
||||
this
|
||||
);
|
||||
self.dataviewBackend.getDataview(mapConfigProvider, req.context.user, req.params, this);
|
||||
},
|
||||
function finish(err, dataview, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET DATAVIEW');
|
||||
err.label = 'GET DATAVIEW';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, dataview, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
LayergroupController.prototype.dataviewSearch = function(req, res) {
|
||||
LayergroupController.prototype.dataviewSearch = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function searchDataview(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
function searchDataview() {
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
||||
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
|
||||
);
|
||||
self.dataviewBackend.search(mapConfigProvider, req.context.user, req.params, this);
|
||||
self.dataviewBackend.search(mapConfigProvider, res.locals.user, req.params.dataviewName, res.locals, this);
|
||||
},
|
||||
function finish(err, searchResult, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET DATAVIEW SEARCH');
|
||||
err.label = 'GET DATAVIEW SEARCH';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, searchResult, 200);
|
||||
}
|
||||
@@ -212,28 +237,24 @@ LayergroupController.prototype.dataviewSearch = function(req, res) {
|
||||
|
||||
};
|
||||
|
||||
LayergroupController.prototype.attributes = function(req, res) {
|
||||
LayergroupController.prototype.attributes = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
req.profiler.start('windshaft.maplayer_attribute');
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function retrieveFeatureAttributes(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
function retrieveFeatureAttributes() {
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
||||
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
|
||||
);
|
||||
self.attributesBackend.getFeatureAttributes(mapConfigProvider, req.params, false, this);
|
||||
self.attributesBackend.getFeatureAttributes(mapConfigProvider, res.locals, false, this);
|
||||
},
|
||||
function finish(err, tile, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET ATTRIBUTES');
|
||||
err.label = 'GET ATTRIBUTES';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, tile, 200);
|
||||
}
|
||||
@@ -243,44 +264,41 @@ LayergroupController.prototype.attributes = function(req, res) {
|
||||
};
|
||||
|
||||
// Gets a tile for a given token and set of tile ZXY coords. (OSM style)
|
||||
LayergroupController.prototype.tile = function(req, res) {
|
||||
LayergroupController.prototype.tile = function(req, res, next) {
|
||||
req.profiler.start('windshaft.map_tile');
|
||||
this.tileOrLayer(req, res);
|
||||
this.tileOrLayer(req, res, next);
|
||||
};
|
||||
|
||||
// Gets a tile for a given token, layer set of tile ZXY coords. (OSM style)
|
||||
LayergroupController.prototype.layer = function(req, res, next) {
|
||||
if (req.params.token === 'static') {
|
||||
return next();
|
||||
}
|
||||
req.profiler.start('windshaft.maplayer_tile');
|
||||
this.tileOrLayer(req, res);
|
||||
this.tileOrLayer(req, res, next);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.tileOrLayer = function (req, res) {
|
||||
LayergroupController.prototype.tileOrLayer = function (req, res, next) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function mapController$prepareParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function mapController$getTileOrGrid(err) {
|
||||
assert.ifError(err);
|
||||
function mapController$getTileOrGrid() {
|
||||
self.tileBackend.getTile(
|
||||
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
|
||||
new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals),
|
||||
req.params, this
|
||||
);
|
||||
},
|
||||
function mapController$finalize(err, tile, headers, stats) {
|
||||
req.profiler.add(stats);
|
||||
self.finalizeGetTileOrGrid(err, req, res, tile, headers);
|
||||
self.finalizeGetTileOrGrid(err, req, res, tile, headers, next);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function getStatusCode(tile, format){
|
||||
return tile.length===0 && format==='mvt'? 204:200;
|
||||
}
|
||||
|
||||
// This function is meant for being called as the very last
|
||||
// step by all endpoints serving tiles or grids
|
||||
LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers) {
|
||||
LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers, next) {
|
||||
var supportedFormats = {
|
||||
grid_json: true,
|
||||
json_torque: true,
|
||||
@@ -309,52 +327,52 @@ LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, t
|
||||
}
|
||||
err.message = errMsg;
|
||||
|
||||
this.sendError(req, res, err, 'TILE RENDER');
|
||||
err.label = 'TILE RENDER';
|
||||
next(err);
|
||||
|
||||
global.statsClient.increment('windshaft.tiles.error');
|
||||
global.statsClient.increment('windshaft.tiles.' + formatStat + '.error');
|
||||
} else {
|
||||
this.sendResponse(req, res, tile, 200, headers);
|
||||
this.sendResponse(req, res, tile, getStatusCode(tile, formatStat), headers);
|
||||
global.statsClient.increment('windshaft.tiles.success');
|
||||
global.statsClient.increment('windshaft.tiles.' + formatStat + '.success');
|
||||
}
|
||||
};
|
||||
|
||||
LayergroupController.prototype.bbox = function(req, res) {
|
||||
LayergroupController.prototype.bbox = function(req, res, next) {
|
||||
this.staticMap(req, res, +req.params.width, +req.params.height, {
|
||||
west: +req.params.west,
|
||||
north: +req.params.north,
|
||||
east: +req.params.east,
|
||||
south: +req.params.south
|
||||
});
|
||||
}, null, next);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.center = function(req, res) {
|
||||
LayergroupController.prototype.center = function(req, res, next) {
|
||||
this.staticMap(req, res, +req.params.width, +req.params.height, +req.params.z, {
|
||||
lng: +req.params.lng,
|
||||
lat: +req.params.lat
|
||||
});
|
||||
}, next);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center) {
|
||||
LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center, next) {
|
||||
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
req.params.layer = 'all';
|
||||
req.params.format = 'png';
|
||||
// We force always the tile to be generated using PNG because
|
||||
// is the only format we support by now
|
||||
res.locals.format = 'png';
|
||||
res.locals.layer = res.locals.layer || 'all';
|
||||
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function reqParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function getImage(err) {
|
||||
assert.ifError(err);
|
||||
function getImage() {
|
||||
if (center) {
|
||||
self.previewBackend.getImage(
|
||||
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
|
||||
new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals),
|
||||
format, width, height, zoom, center, this);
|
||||
} else {
|
||||
self.previewBackend.getImage(
|
||||
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
|
||||
new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals),
|
||||
format, width, height, zoom /* bounds */, this);
|
||||
}
|
||||
},
|
||||
@@ -363,7 +381,8 @@ LayergroupController.prototype.staticMap = function(req, res, width, height, zoo
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'STATIC_MAP');
|
||||
err.label = 'STATIC_MAP';
|
||||
next(err);
|
||||
} else {
|
||||
res.set('Content-Type', headers['Content-Type'] || 'image/' + format);
|
||||
self.sendResponse(req, res, image, 200);
|
||||
@@ -381,18 +400,18 @@ LayergroupController.prototype.sendResponse = function(req, res, body, status, h
|
||||
|
||||
// Set Last-Modified header
|
||||
var lastUpdated;
|
||||
if (req.params.cache_buster) {
|
||||
if (res.locals.cache_buster) {
|
||||
// Assuming cache_buster is a timestamp
|
||||
lastUpdated = new Date(parseInt(req.params.cache_buster));
|
||||
lastUpdated = new Date(parseInt(res.locals.cache_buster));
|
||||
} else {
|
||||
lastUpdated = new Date();
|
||||
}
|
||||
res.set('Last-Modified', lastUpdated.toUTCString());
|
||||
|
||||
var dbName = req.params.dbname;
|
||||
var dbName = res.locals.dbname;
|
||||
step(
|
||||
function getAffectedTables() {
|
||||
self.getAffectedTables(req.context.user, dbName, req.params.token, this);
|
||||
self.getAffectedTables(res.locals.user, dbName, res.locals.token, this);
|
||||
},
|
||||
function sendResponse(err, affectedTables) {
|
||||
req.profiler.done('affectedTables');
|
||||
@@ -403,10 +422,24 @@ LayergroupController.prototype.sendResponse = function(req, res, body, status, h
|
||||
res.set('X-Cache-Channel', affectedTables.getCacheChannel());
|
||||
self.surrogateKeysCache.tag(res, affectedTables);
|
||||
}
|
||||
self.send(req, res, body, status, headers);
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
res.status(status);
|
||||
|
||||
if (!Buffer.isBuffer(body) && typeof body === 'object') {
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(body);
|
||||
} else {
|
||||
res.json(body);
|
||||
}
|
||||
} else {
|
||||
res.send(body);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
LayergroupController.prototype.getAffectedTables = function(user, dbName, layergroupId, callback) {
|
||||
@@ -468,3 +501,12 @@ LayergroupController.prototype.getAffectedTables = function(user, dbName, layerg
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function validateLayerRouteMiddleware(req, res, next) {
|
||||
if (req.params.token === 'static') {
|
||||
return next('route');
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
var _ = require('underscore');
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
var windshaft = require('windshaft');
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
|
||||
var ResourceLocator = require('../models/resource-locator');
|
||||
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
@@ -20,7 +15,6 @@ var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
var NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider');
|
||||
var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/create-layergroup-provider');
|
||||
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
* @param {PgConnection} pgConnection
|
||||
@@ -34,12 +28,9 @@ var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/cr
|
||||
* @param {StatsBackend} statsBackend
|
||||
* @constructor
|
||||
*/
|
||||
function MapController(authApi, pgConnection, templateMaps, mapBackend, metadataBackend,
|
||||
function MapController(prepareContext, pgConnection, templateMaps, mapBackend, metadataBackend,
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, mapConfigAdapter,
|
||||
statsBackend) {
|
||||
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
this.pgConnection = pgConnection;
|
||||
this.templateMaps = templateMaps;
|
||||
this.mapBackend = mapBackend;
|
||||
@@ -52,334 +43,345 @@ function MapController(authApi, pgConnection, templateMaps, mapBackend, metadata
|
||||
this.resourceLocator = new ResourceLocator(global.environment);
|
||||
|
||||
this.statsBackend = statsBackend;
|
||||
this.prepareContext = prepareContext;
|
||||
}
|
||||
|
||||
util.inherits(MapController, BaseController);
|
||||
|
||||
module.exports = MapController;
|
||||
|
||||
|
||||
MapController.prototype.register = function(app) {
|
||||
app.get(app.base_url_mapconfig, cors(), userMiddleware, this.createGet.bind(this));
|
||||
app.post(app.base_url_mapconfig, cors(), userMiddleware, this.createPost.bind(this));
|
||||
app.get(app.base_url_templated + '/:template_id/jsonp', cors(), userMiddleware, this.jsonp.bind(this));
|
||||
app.post(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.instantiate.bind(this));
|
||||
const { base_url_mapconfig, base_url_templated } = app;
|
||||
const useTemplate = true;
|
||||
|
||||
app.get(base_url_mapconfig, this.composeCreateMapMiddleware());
|
||||
app.post(base_url_mapconfig, this.composeCreateMapMiddleware());
|
||||
app.get(`${base_url_templated}/:template_id/jsonp`, this.composeCreateMapMiddleware(useTemplate));
|
||||
app.post(`${base_url_templated}/:template_id`, this.composeCreateMapMiddleware(useTemplate));
|
||||
app.options(app.base_url_mapconfig, cors('Content-Type'));
|
||||
};
|
||||
|
||||
MapController.prototype.createGet = function(req, res){
|
||||
req.profiler.start('windshaft.createmap_get');
|
||||
MapController.prototype.composeCreateMapMiddleware = function (useTemplate = false) {
|
||||
const isTemplateInstantiation = useTemplate;
|
||||
const useTemplateHash = useTemplate;
|
||||
const includeQuery = !useTemplate;
|
||||
const label = useTemplate ? 'NAMED MAP LAYERGROUP' : 'ANONYMOUS LAYERGROUP';
|
||||
const addContext = !useTemplate;
|
||||
|
||||
this.create(req, res, function createGet$prepareConfig(err, req) {
|
||||
assert.ifError(err);
|
||||
if ( ! req.params.config ) {
|
||||
throw new Error('layergroup GET needs a "config" parameter');
|
||||
}
|
||||
return JSON.parse(req.params.config);
|
||||
});
|
||||
return [
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.prepareContext,
|
||||
this.initProfiler(isTemplateInstantiation),
|
||||
this.checkJsonContentType(),
|
||||
useTemplate ? this.checkInstantiteLayergroup() : this.checkCreateLayergroup(),
|
||||
useTemplate ? this.getTemplate() : this.prepareAdapterMapConfig(),
|
||||
useTemplate ? this.instantiateLayergroup() : this.createLayergroup(),
|
||||
this.incrementMapViewCount(),
|
||||
this.augmentLayergroupData(),
|
||||
this.getAffectedTables(),
|
||||
this.setCacheChannel(),
|
||||
this.setLastModified(),
|
||||
this.setLastUpdatedTimeToLayergroup(),
|
||||
this.setCacheControl(),
|
||||
this.setLayerStats(),
|
||||
this.setLayergroupIdHeader(useTemplateHash),
|
||||
this.setDataviewsAndWidgetsUrlsToLayergroupMetadata(),
|
||||
this.setAnalysesMetadataToLayergroup(includeQuery),
|
||||
this.setTurboCartoMetadataToLayergroup(),
|
||||
this.setSurrogateKeyHeader(),
|
||||
this.sendResponse(),
|
||||
this.augmentError({ label, addContext })
|
||||
];
|
||||
};
|
||||
|
||||
MapController.prototype.createPost = function(req, res) {
|
||||
req.profiler.start('windshaft.createmap_post');
|
||||
MapController.prototype.initProfiler = function (isTemplateInstantiation) {
|
||||
const operation = isTemplateInstantiation ? 'instance_template' : 'createmap';
|
||||
|
||||
this.create(req, res, function createPost$prepareConfig(err, req) {
|
||||
assert.ifError(err);
|
||||
if (!req.is('application/json')) {
|
||||
throw new Error('layergroup POST data must be of type application/json');
|
||||
}
|
||||
return req.body;
|
||||
});
|
||||
return function initProfilerMiddleware (req, res, next) {
|
||||
req.profiler.start(`windshaft-cartodb.${operation}_${req.method.toLowerCase()}`);
|
||||
req.profiler.done(`${operation}.initProfilerMiddleware`);
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.instantiate = function(req, res) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_post');
|
||||
|
||||
this.instantiateTemplate(req, res, function prepareTemplateParams(callback) {
|
||||
if (!req.is('application/json')) {
|
||||
return callback(new Error('Template POST data must be of type application/json'));
|
||||
MapController.prototype.checkJsonContentType = function () {
|
||||
return function checkJsonContentTypeMiddleware(req, res, next) {
|
||||
if (req.method === 'POST' && !req.is('application/json')) {
|
||||
return next(new Error('POST data must be of type application/json'));
|
||||
}
|
||||
return callback(null, req.body);
|
||||
});
|
||||
|
||||
req.profiler.done('checkJsonContentTypeMiddleware');
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.jsonp = function(req, res) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_get');
|
||||
MapController.prototype.checkInstantiteLayergroup = function () {
|
||||
return function checkInstantiteLayergroupMiddleware(req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
const { callback, config } = req.query;
|
||||
|
||||
this.instantiateTemplate(req, res, function prepareJsonTemplateParams(callback) {
|
||||
var err = null;
|
||||
if ( req.query.callback === undefined || req.query.callback.length === 0) {
|
||||
err = new Error('callback parameter should be present and be a function name');
|
||||
}
|
||||
if (callback === undefined || callback.length === 0) {
|
||||
return next(new Error('callback parameter should be present and be a function name'));
|
||||
}
|
||||
|
||||
var templateParams = {};
|
||||
if (req.query.config) {
|
||||
try {
|
||||
templateParams = JSON.parse(req.query.config);
|
||||
} catch(e) {
|
||||
err = new Error('Invalid config parameter, should be a valid JSON');
|
||||
if (config) {
|
||||
try {
|
||||
req.body = JSON.parse(config);
|
||||
} catch(e) {
|
||||
return next(new Error('Invalid config parameter, should be a valid JSON'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return callback(err, templateParams);
|
||||
});
|
||||
req.profiler.done('checkInstantiteLayergroup');
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.create = function(req, res, prepareConfigFn) {
|
||||
var self = this;
|
||||
MapController.prototype.checkCreateLayergroup = function () {
|
||||
return function checkCreateLayergroupMiddleware (req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
const { config } = res.locals;
|
||||
|
||||
var mapConfig;
|
||||
if (!config) {
|
||||
return next(new Error('layergroup GET needs a "config" parameter'));
|
||||
}
|
||||
|
||||
var context = {};
|
||||
try {
|
||||
req.body = JSON.parse(config);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
|
||||
step(
|
||||
function setupParams(){
|
||||
self.req2params(req, this);
|
||||
},
|
||||
prepareConfigFn,
|
||||
function prepareAdapterMapConfig(err, requestMapConfig) {
|
||||
assert.ifError(err);
|
||||
context.analysisConfiguration = {
|
||||
user: req.context.user,
|
||||
req.profiler.done('checkCreateLayergroup');
|
||||
return next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.getTemplate = function () {
|
||||
return function getTemplateMiddleware (req, res, next) {
|
||||
const templateParams = req.body;
|
||||
const { user } = res.locals;
|
||||
|
||||
const mapconfigProvider = new NamedMapMapConfigProvider(
|
||||
this.templateMaps,
|
||||
this.pgConnection,
|
||||
this.metadataBackend,
|
||||
this.userLimitsApi,
|
||||
this.mapConfigAdapter,
|
||||
user,
|
||||
req.params.template_id,
|
||||
templateParams,
|
||||
res.locals.auth_token,
|
||||
res.locals
|
||||
);
|
||||
|
||||
mapconfigProvider.getMapConfig((err, mapconfig, rendererParams) => {
|
||||
req.profiler.done('named.getMapConfig');
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.mapconfig = mapconfig;
|
||||
res.locals.rendererParams = rendererParams;
|
||||
res.locals.mapconfigProvider = mapconfigProvider;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.prepareAdapterMapConfig = function () {
|
||||
return function prepareAdapterMapConfigMiddleware(req, res, next) {
|
||||
const requestMapConfig = req.body;
|
||||
const { user, dbhost, dbport, dbname, dbuser, dbpassword, api_key } = res.locals;
|
||||
|
||||
const context = {
|
||||
analysisConfiguration: {
|
||||
user,
|
||||
db: {
|
||||
host: req.params.dbhost,
|
||||
port: req.params.dbport,
|
||||
dbname: req.params.dbname,
|
||||
user: req.params.dbuser,
|
||||
pass: req.params.dbpassword
|
||||
host: dbhost,
|
||||
port: dbport,
|
||||
dbname: dbname,
|
||||
user: dbuser,
|
||||
pass: dbpassword
|
||||
},
|
||||
batch: {
|
||||
username: req.context.user,
|
||||
apiKey: req.params.api_key
|
||||
username: user,
|
||||
apiKey: api_key
|
||||
}
|
||||
};
|
||||
self.mapConfigAdapter.getMapConfig(req.context.user, requestMapConfig, req.params, context, this);
|
||||
},
|
||||
function createLayergroup(err, requestMapConfig) {
|
||||
assert.ifError(err);
|
||||
var datasource = context.datasource || Datasource.EmptyDatasource();
|
||||
mapConfig = new MapConfig(requestMapConfig, datasource);
|
||||
self.mapBackend.createLayergroup(
|
||||
mapConfig, req.params,
|
||||
new CreateLayergroupMapConfigProvider(mapConfig, req.context.user, self.userLimitsApi, req.params),
|
||||
this
|
||||
);
|
||||
},
|
||||
function afterLayergroupCreate(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup, context.analysesResults, this);
|
||||
},
|
||||
function finish(err, layergroup) {
|
||||
}
|
||||
};
|
||||
|
||||
this.mapConfigAdapter.getMapConfig(user, requestMapConfig, res.locals, context, (err, requestMapConfig) => {
|
||||
req.profiler.done('anonymous.getMapConfig');
|
||||
if (err) {
|
||||
if (Number.isFinite(err.layerIndex)) {
|
||||
var error = new Error(err.message);
|
||||
error.http_status = err.http_status;
|
||||
|
||||
if (!err.http_status && err.message.indexOf('column "the_geom_webmercator" does not exist') >= 0) {
|
||||
error.http_status = 400;
|
||||
}
|
||||
|
||||
error.type = 'layer';
|
||||
error.subtype = err.message.indexOf('Postgis Plugin') >= 0 ? 'query' : undefined;
|
||||
error.layer = {
|
||||
id: mapConfig.getLayerId(err.layerIndex),
|
||||
index: err.layerIndex,
|
||||
type: mapConfig.layerType(err.layerIndex)
|
||||
};
|
||||
|
||||
err = error;
|
||||
}
|
||||
self.sendError(req, res, err, 'ANONYMOUS LAYERGROUP');
|
||||
} else {
|
||||
var analysesResults = context.analysesResults || [];
|
||||
self.addDataviewsAndWidgetsUrls(req.context.user, layergroup, mapConfig.obj());
|
||||
self.addAnalysesMetadata(req.context.user, layergroup, analysesResults, true);
|
||||
addContextMetadata(layergroup, mapConfig.obj(), context);
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
self.send(req, res, layergroup, 200);
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function addContextMetadata(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)) {
|
||||
layer.meta.cartocss_meta = context.turboCarto.layers[layerIndex];
|
||||
}
|
||||
return layer;
|
||||
req.body = requestMapConfig;
|
||||
res.locals.context = context;
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
|
||||
var mapConfigProvider;
|
||||
var mapConfig;
|
||||
step(
|
||||
function setupParams(){
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function getTemplateParams() {
|
||||
prepareParamsFn(this);
|
||||
},
|
||||
function getTemplate(err, templateParams) {
|
||||
assert.ifError(err);
|
||||
mapConfigProvider = new NamedMapMapConfigProvider(
|
||||
self.templateMaps,
|
||||
self.pgConnection,
|
||||
self.metadataBackend,
|
||||
self.userLimitsApi,
|
||||
self.mapConfigAdapter,
|
||||
cdbuser,
|
||||
req.params.template_id,
|
||||
templateParams,
|
||||
req.query.auth_token,
|
||||
req.params
|
||||
);
|
||||
mapConfigProvider.getMapConfig(this);
|
||||
},
|
||||
function createLayergroup(err, mapConfig_, rendererParams) {
|
||||
assert.ifError(err);
|
||||
mapConfig = mapConfig_;
|
||||
self.mapBackend.createLayergroup(
|
||||
mapConfig, rendererParams,
|
||||
new CreateLayergroupMapConfigProvider(mapConfig, cdbuser, self.userLimitsApi, rendererParams),
|
||||
this
|
||||
);
|
||||
},
|
||||
function afterLayergroupCreate(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup,
|
||||
mapConfigProvider.analysesResults,
|
||||
this);
|
||||
},
|
||||
function finishTemplateInstantiation(err, layergroup) {
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'NAMED MAP LAYERGROUP');
|
||||
} else {
|
||||
var templateHash = self.templateMaps.fingerPrint(mapConfigProvider.template).substring(0, 8);
|
||||
layergroup.layergroupid = cdbuser + '@' + templateHash + '@' + layergroup.layergroupid;
|
||||
|
||||
var _mapConfig = mapConfig.obj();
|
||||
self.addDataviewsAndWidgetsUrls(cdbuser, layergroup, _mapConfig);
|
||||
self.addAnalysesMetadata(cdbuser, layergroup, mapConfigProvider.analysesResults);
|
||||
addContextMetadata(layergroup, _mapConfig, mapConfigProvider.context);
|
||||
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, mapConfigProvider.getTemplateName()));
|
||||
|
||||
self.send(req, res, layergroup, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.afterLayergroupCreate =
|
||||
function(req, res, mapconfig, layergroup, analysesResults, callback) {
|
||||
var self = this;
|
||||
MapController.prototype.createLayergroup = function () {
|
||||
return function createLayergroupMiddleware (req, res, next) {
|
||||
const requestMapConfig = req.body;
|
||||
const { context, user } = res.locals;
|
||||
const datasource = context.datasource || Datasource.EmptyDatasource();
|
||||
const mapconfig = new MapConfig(requestMapConfig, datasource);
|
||||
const mapconfigProvider =
|
||||
new CreateLayergroupMapConfigProvider(mapconfig, user, this.userLimitsApi, res.locals);
|
||||
|
||||
var username = req.context.user;
|
||||
res.locals.mapconfig = mapconfig;
|
||||
res.locals.analysesResults = context.analysesResults;
|
||||
|
||||
var tasksleft = 2; // redis key and affectedTables
|
||||
var errors = [];
|
||||
|
||||
var done = function(err) {
|
||||
if ( err ) {
|
||||
errors.push('' + err);
|
||||
}
|
||||
if ( ! --tasksleft ) {
|
||||
err = errors.length ? new Error(errors.join('\n')) : null;
|
||||
callback(err, layergroup);
|
||||
}
|
||||
};
|
||||
|
||||
// include in layergroup response the variables in serverMedata
|
||||
// those variables are useful to send to the client information
|
||||
// about how to reach this server or information about it
|
||||
_.extend(layergroup, global.environment.serverMetadata);
|
||||
|
||||
// Don't wait for the mapview count increment to
|
||||
// take place before proceeding. Error will be logged
|
||||
// asynchronously
|
||||
this.metadataBackend.incMapviewCount(username, mapconfig.obj().stat_tag, function(err) {
|
||||
req.profiler.done('incMapviewCount');
|
||||
if ( err ) {
|
||||
global.logger.log("ERROR: failed to increment mapview count for user '" + username + "': " + err);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
var sql = [];
|
||||
mapconfig.getLayers().forEach(function(layer) {
|
||||
sql.push(layer.options.sql);
|
||||
if (layer.options.affected_tables) {
|
||||
layer.options.affected_tables.map(function(table) {
|
||||
sql.push('SELECT * FROM ' + table + ' LIMIT 0');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var dbName = req.params.dbname;
|
||||
var layergroupId = layergroup.layergroupid;
|
||||
var dbConnection;
|
||||
|
||||
step(
|
||||
function getPgConnection() {
|
||||
self.pgConnection.getConnection(username, this);
|
||||
},
|
||||
function getAffectedTablesAndLastUpdatedTime(err, connection) {
|
||||
assert.ifError(err);
|
||||
dbConnection = connection;
|
||||
QueryTables.getAffectedTablesFromQuery(dbConnection, sql.join(';'), this);
|
||||
},
|
||||
function handleAffectedTablesAndLastUpdatedTime(err, result) {
|
||||
req.profiler.done('queryTablesAndLastUpdated');
|
||||
assert.ifError(err);
|
||||
// feed affected tables cache so it can be reused from, for instance, layergroup controller
|
||||
self.layergroupAffectedTables.set(dbName, layergroupId, result);
|
||||
|
||||
var lastUpdateTime = result.getLastUpdatedAt();
|
||||
lastUpdateTime = getLastUpdatedTime(analysesResults, lastUpdateTime) || lastUpdateTime;
|
||||
|
||||
// last update for layergroup cache buster
|
||||
layergroup.layergroupid = layergroup.layergroupid + ':' + lastUpdateTime;
|
||||
layergroup.last_updated = new Date(lastUpdateTime).toISOString();
|
||||
|
||||
if (req.method === 'GET') {
|
||||
var ttl = global.environment.varnish.layergroupTtl || 86400;
|
||||
res.set('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
|
||||
res.set('Last-Modified', (new Date()).toUTCString());
|
||||
res.set('X-Cache-Channel', result.getCacheChannel());
|
||||
if (result.tables && result.tables.length > 0) {
|
||||
self.surrogateKeysCache.tag(res, result);
|
||||
}
|
||||
this.mapBackend.createLayergroup(mapconfig, res.locals, mapconfigProvider, (err, layergroup) => {
|
||||
req.profiler.done('createLayergroup');
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
function fetchLayersStats(err) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
self.statsBackend.getStats(mapconfig, dbConnection, function(err, layersStats) {
|
||||
res.locals.layergroup = layergroup;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.instantiateLayergroup = function () {
|
||||
return function instantiateLayergroupMiddleware (req, res, next) {
|
||||
const { user, mapconfig, rendererParams } = res.locals;
|
||||
const mapconfigProvider =
|
||||
new CreateLayergroupMapConfigProvider(mapconfig, user, this.userLimitsApi, rendererParams);
|
||||
|
||||
this.mapBackend.createLayergroup(mapconfig, rendererParams, mapconfigProvider, (err, layergroup) => {
|
||||
req.profiler.done('createLayergroup');
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.layergroup = layergroup;
|
||||
|
||||
const { mapconfigProvider } = res.locals;
|
||||
|
||||
res.locals.analysesResults = mapconfigProvider.analysesResults;
|
||||
res.locals.template = mapconfigProvider.template;
|
||||
res.locals.templateName = mapconfigProvider.getTemplateName();
|
||||
res.locals.context = mapconfigProvider.context;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.incrementMapViewCount = function () {
|
||||
return function incrementMapViewCountMiddleware(req, res, next) {
|
||||
const { mapconfig, user } = res.locals;
|
||||
|
||||
// Error won't blow up, just be logged.
|
||||
this.metadataBackend.incMapviewCount(user, mapconfig.obj().stat_tag, (err) => {
|
||||
req.profiler.done('incMapviewCount');
|
||||
|
||||
if (err) {
|
||||
global.logger.log(`ERROR: failed to increment mapview count for user '${user}': ${err.message}`);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.augmentLayergroupData = function () {
|
||||
return function augmentLayergroupDataMiddleware (req, res, next) {
|
||||
const { layergroup } = res.locals;
|
||||
|
||||
// include in layergroup response the variables in serverMedata
|
||||
// those variables are useful to send to the client information
|
||||
// about how to reach this server or information about it
|
||||
_.extend(layergroup, global.environment.serverMetadata);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.getAffectedTables = function () {
|
||||
return function getAffectedTablesMiddleware (req, res, next) {
|
||||
const { dbname, layergroup, user, mapconfig } = res.locals;
|
||||
|
||||
this.pgConnection.getConnection(user, (err, connection) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
const sql = [];
|
||||
mapconfig.getLayers().forEach(function(layer) {
|
||||
sql.push(layer.options.sql);
|
||||
if (layer.options.affected_tables) {
|
||||
layer.options.affected_tables.map(function(table) {
|
||||
sql.push('SELECT * FROM ' + table + ' LIMIT 0');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
QueryTables.getAffectedTablesFromQuery(connection, sql.join(';'), (err, affectedTables) => {
|
||||
req.profiler.done('getAffectedTablesFromQuery');
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (layersStats.length > 0) {
|
||||
layergroup.metadata.layers.forEach(function (layer, index) {
|
||||
layer.meta.stats = layersStats[index];
|
||||
});
|
||||
}
|
||||
return next();
|
||||
|
||||
// feed affected tables cache so it can be reused from, for instance, layergroup controller
|
||||
this.layergroupAffectedTables.set(dbname, layergroup.layergroupId, affectedTables);
|
||||
|
||||
res.locals.affectedTables = affectedTables;
|
||||
|
||||
next();
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.setCacheChannel = function () {
|
||||
return function setCacheChannelMiddleware (req, res, next) {
|
||||
const { affectedTables } = res.locals;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
res.set('X-Cache-Channel', affectedTables.getCacheChannel());
|
||||
}
|
||||
);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.setLastModified = function () {
|
||||
return function setLastModifiedMiddleware (req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
res.set('Last-Modified', (new Date()).toUTCString());
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.setLastUpdatedTimeToLayergroup = function () {
|
||||
return function setLastUpdatedTimeToLayergroupMiddleware (req, res, next) {
|
||||
const { affectedTables, layergroup, analysesResults } = res.locals;
|
||||
|
||||
var lastUpdateTime = affectedTables.getLastUpdatedAt();
|
||||
|
||||
lastUpdateTime = getLastUpdatedTime(analysesResults, lastUpdateTime) || lastUpdateTime;
|
||||
|
||||
// last update for layergroup cache buster
|
||||
layergroup.layergroupid = layergroup.layergroupid + ':' + lastUpdateTime;
|
||||
layergroup.last_updated = new Date(lastUpdateTime).toISOString();
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
function getLastUpdatedTime(analysesResults, lastUpdateTime) {
|
||||
@@ -395,34 +397,66 @@ function getLastUpdatedTime(analysesResults, lastUpdateTime) {
|
||||
}, lastUpdateTime);
|
||||
}
|
||||
|
||||
MapController.prototype.addAnalysesMetadata = function(username, layergroup, analysesResults, includeQuery) {
|
||||
includeQuery = includeQuery || false;
|
||||
analysesResults = analysesResults || [];
|
||||
layergroup.metadata.analyses = [];
|
||||
MapController.prototype.setCacheControl = function () {
|
||||
return function setCacheControlMiddleware (req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
var ttl = global.environment.varnish.layergroupTtl || 86400;
|
||||
res.set('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
|
||||
}
|
||||
|
||||
analysesResults.forEach(function(analysis) {
|
||||
var nodes = analysis.getNodes();
|
||||
layergroup.metadata.analyses.push({
|
||||
nodes: nodes.reduce(function(nodesIdMap, node) {
|
||||
if (node.params.id) {
|
||||
var nodeResource = layergroup.layergroupid + '/analysis/node/' + node.id();
|
||||
var nodeRepr = {
|
||||
status: node.getStatus(),
|
||||
url: this.resourceLocator.getUrls(username, nodeResource)
|
||||
};
|
||||
if (includeQuery) {
|
||||
nodeRepr.query = node.getQuery();
|
||||
}
|
||||
if (node.getStatus() === 'failed') {
|
||||
nodeRepr.error_message = node.getErrorMessage();
|
||||
}
|
||||
nodesIdMap[node.params.id] = nodeRepr;
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.setLayerStats = function () {
|
||||
return function setLayerStatsMiddleware(req, res, next) {
|
||||
const { user, mapconfig, layergroup } = res.locals;
|
||||
|
||||
this.pgConnection.getConnection(user, (err, connection) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
this.statsBackend.getStats(mapconfig, connection, function(err, layersStats) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return nodesIdMap;
|
||||
}.bind(this), {})
|
||||
if (layersStats.length > 0) {
|
||||
layergroup.metadata.layers.forEach(function (layer, index) {
|
||||
layer.meta.stats = layersStats[index];
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
}.bind(this));
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.setLayergroupIdHeader = function (useTemplateHash) {
|
||||
return function setLayergroupIdHeaderMiddleware (req, res, next) {
|
||||
const { layergroup, user, template } = res.locals;
|
||||
|
||||
if (useTemplateHash) {
|
||||
var templateHash = this.templateMaps.fingerPrint(template).substring(0, 8);
|
||||
layergroup.layergroupid = `${user}@${templateHash}@${layergroup.layergroupid}`;
|
||||
}
|
||||
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
|
||||
next();
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.setDataviewsAndWidgetsUrlsToLayergroupMetadata = function () {
|
||||
return function setDataviewsAndWidgetsUrlsToLayergroupMetadataMiddleware (req, res, next) {
|
||||
const { layergroup, user, mapconfig } = res.locals;
|
||||
|
||||
this.addDataviewsAndWidgetsUrls(user, layergroup, mapconfig.obj());
|
||||
|
||||
next();
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
// TODO this should take into account several URL patterns
|
||||
@@ -461,3 +495,131 @@ MapController.prototype.addWidgetsUrl = function(username, layergroup, mapConfig
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
MapController.prototype.setAnalysesMetadataToLayergroup = function (includeQuery) {
|
||||
return function setAnalysesMetadataToLayergroupMiddleware (req, res, next) {
|
||||
const { layergroup, user, analysesResults = [] } = res.locals;
|
||||
|
||||
this.addAnalysesMetadata(user, layergroup, analysesResults, includeQuery);
|
||||
|
||||
next();
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.addAnalysesMetadata = function(username, layergroup, analysesResults, includeQuery) {
|
||||
includeQuery = includeQuery || false;
|
||||
analysesResults = analysesResults || [];
|
||||
layergroup.metadata.analyses = [];
|
||||
|
||||
analysesResults.forEach(function(analysis) {
|
||||
var nodes = analysis.getNodes();
|
||||
layergroup.metadata.analyses.push({
|
||||
nodes: nodes.reduce(function(nodesIdMap, node) {
|
||||
if (node.params.id) {
|
||||
var nodeResource = layergroup.layergroupid + '/analysis/node/' + node.id();
|
||||
var nodeRepr = {
|
||||
status: node.getStatus(),
|
||||
url: this.resourceLocator.getUrls(username, nodeResource)
|
||||
};
|
||||
if (includeQuery) {
|
||||
nodeRepr.query = node.getQuery();
|
||||
}
|
||||
if (node.getStatus() === 'failed') {
|
||||
nodeRepr.error_message = node.getErrorMessage();
|
||||
}
|
||||
nodesIdMap[node.params.id] = nodeRepr;
|
||||
}
|
||||
|
||||
return nodesIdMap;
|
||||
}.bind(this), {})
|
||||
});
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
MapController.prototype.setTurboCartoMetadataToLayergroup = function () {
|
||||
return function setTurboCartoMetadataToLayergroupMiddleware (req, res, next) {
|
||||
const { layergroup, mapconfig, context } = res.locals;
|
||||
|
||||
addContextMetadata(layergroup, mapconfig.obj(), context);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
function addContextMetadata(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)) {
|
||||
layer.meta.cartocss_meta = context.turboCarto.layers[layerIndex];
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MapController.prototype.setSurrogateKeyHeader = function () {
|
||||
return function setSurrogateKeyHeaderMiddleware(req, res, next) {
|
||||
const { affectedTables, user, templateName } = res.locals;
|
||||
|
||||
if (req.method === 'GET' && affectedTables.tables && affectedTables.tables.length > 0) {
|
||||
this.surrogateKeysCache.tag(res, affectedTables);
|
||||
}
|
||||
|
||||
if (templateName) {
|
||||
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, templateName));
|
||||
}
|
||||
|
||||
next();
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.sendResponse = function () {
|
||||
return function sendResponseMiddleware (req, res) {
|
||||
req.profiler.done('res');
|
||||
const { layergroup } = res.locals;
|
||||
|
||||
res.status(200);
|
||||
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(layergroup);
|
||||
} else {
|
||||
res.json(layergroup);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.augmentError = function (options) {
|
||||
const { addContext = false, label = 'MAPS CONTROLLER' } = options;
|
||||
|
||||
return function augmentErrorMiddleware (err, req, res, next) {
|
||||
req.profiler.done('error');
|
||||
const { mapconfig } = res.locals;
|
||||
|
||||
if (addContext) {
|
||||
err = Number.isFinite(err.layerIndex) ? populateError(err, mapconfig) : err;
|
||||
}
|
||||
|
||||
err.label = label;
|
||||
|
||||
next(err);
|
||||
};
|
||||
};
|
||||
|
||||
function populateError(err, mapConfig) {
|
||||
var error = new Error(err.message);
|
||||
error.http_status = err.http_status;
|
||||
|
||||
if (!err.http_status && err.message.indexOf('column "the_geom_webmercator" does not exist') >= 0) {
|
||||
error.http_status = 400;
|
||||
}
|
||||
|
||||
error.type = 'layer';
|
||||
error.subtype = err.message.indexOf('Postgis Plugin') >= 0 ? 'query' : undefined;
|
||||
error.layer = {
|
||||
id: mapConfig.getLayerId(err.layerIndex),
|
||||
index: err.layerIndex,
|
||||
type: mapConfig.layerType(err.layerIndex)
|
||||
};
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -3,42 +3,46 @@ var assert = require('assert');
|
||||
var _ = require('underscore');
|
||||
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
var allowQueryParams = require('../middleware/allow-query-params');
|
||||
var vectorError = require('../middleware/vector-error');
|
||||
|
||||
function NamedMapsController(authApi, pgConnection, namedMapProviderCache, tileBackend, previewBackend,
|
||||
function NamedMapsController(prepareContext, namedMapProviderCache, tileBackend, previewBackend,
|
||||
surrogateKeysCache, tablesExtentApi, metadataBackend) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
this.namedMapProviderCache = namedMapProviderCache;
|
||||
this.tileBackend = tileBackend;
|
||||
this.previewBackend = previewBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.tablesExtentApi = tablesExtentApi;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.prepareContext = prepareContext;
|
||||
}
|
||||
|
||||
util.inherits(NamedMapsController, BaseController);
|
||||
|
||||
module.exports = NamedMapsController;
|
||||
|
||||
NamedMapsController.prototype.register = function(app) {
|
||||
app.get(app.base_url_templated +
|
||||
'/:template_id/:layer/:z/:x/:y.(:format)', cors(), userMiddleware,
|
||||
this.tile.bind(this));
|
||||
app.get(
|
||||
app.base_url_templated + '/:template_id/:layer/:z/:x/:y.(:format)',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.prepareContext,
|
||||
this.tile.bind(this),
|
||||
vectorError()
|
||||
);
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/static/named/:template_id/:width/:height.:format', cors(), userMiddleware,
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
|
||||
this.staticMap.bind(this));
|
||||
this.prepareContext,
|
||||
this.staticMap.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.sendResponse = function(req, res, resource, headers, namedMapProvider) {
|
||||
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(req.context.user, namedMapProvider.getTemplateName()));
|
||||
NamedMapsController.prototype.sendResponse = function(req, res, body, headers, namedMapProvider) {
|
||||
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(res.locals.user, namedMapProvider.getTemplateName()));
|
||||
res.set('Content-Type', headers['content-type'] || headers['Content-Type'] || 'image/png');
|
||||
res.set('Cache-Control', 'public,max-age=7200,must-revalidate');
|
||||
|
||||
@@ -70,29 +74,26 @@ NamedMapsController.prototype.sendResponse = function(req, res, resource, header
|
||||
self.surrogateKeysCache.tag(res, result);
|
||||
}
|
||||
}
|
||||
self.send(req, res, resource, 200);
|
||||
res.status(200);
|
||||
res.send(body);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.tile = function(req, res) {
|
||||
NamedMapsController.prototype.tile = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbUser = req.context.user;
|
||||
var cdbUser = res.locals.user;
|
||||
|
||||
var namedMapProvider;
|
||||
step(
|
||||
function reqParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function getNamedMapProvider(err) {
|
||||
assert.ifError(err);
|
||||
function getNamedMapProvider() {
|
||||
self.namedMapProviderCache.get(
|
||||
cdbUser,
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
req.params,
|
||||
res.locals,
|
||||
this
|
||||
);
|
||||
},
|
||||
@@ -103,8 +104,10 @@ NamedMapsController.prototype.tile = function(req, res) {
|
||||
},
|
||||
function handleImage(err, tile, headers, stats) {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'NAMED_MAP_TILE');
|
||||
err.label = 'NAMED_MAP_TILE';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, tile, headers, namedMapProvider);
|
||||
}
|
||||
@@ -112,28 +115,26 @@ NamedMapsController.prototype.tile = function(req, res) {
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.staticMap = function(req, res) {
|
||||
NamedMapsController.prototype.staticMap = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbUser = req.context.user;
|
||||
var cdbUser = res.locals.user;
|
||||
|
||||
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
req.params.format = 'png';
|
||||
req.params.layer = 'all';
|
||||
// We force always the tile to be generated using PNG because
|
||||
// is the only format we support by now
|
||||
res.locals.format = 'png';
|
||||
res.locals.layer = res.locals.layer || 'all';
|
||||
|
||||
var namedMapProvider;
|
||||
step(
|
||||
function reqParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function getNamedMapProvider(err) {
|
||||
assert.ifError(err);
|
||||
function getNamedMapProvider() {
|
||||
self.namedMapProviderCache.get(
|
||||
cdbUser,
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
req.params,
|
||||
res.locals,
|
||||
this
|
||||
);
|
||||
},
|
||||
@@ -142,11 +143,11 @@ NamedMapsController.prototype.staticMap = function(req, res) {
|
||||
|
||||
namedMapProvider = _namedMapProvider;
|
||||
|
||||
self.prepareLayerFilterFromPreviewLayers(cdbUser, req, namedMapProvider, this);
|
||||
self.prepareLayerFilterFromPreviewLayers(cdbUser, req, res.locals, namedMapProvider, this);
|
||||
},
|
||||
function prepareImageOptions(err) {
|
||||
assert.ifError(err);
|
||||
self.getStaticImageOptions(cdbUser, req.params, namedMapProvider, this);
|
||||
self.getStaticImageOptions(cdbUser, res.locals, namedMapProvider, this);
|
||||
},
|
||||
function getImage(err, imageOpts) {
|
||||
assert.ifError(err);
|
||||
@@ -180,7 +181,8 @@ NamedMapsController.prototype.staticMap = function(req, res) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'STATIC_VIZ_MAP');
|
||||
err.label = 'STATIC_VIZ_MAP';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, image, headers, namedMapProvider);
|
||||
}
|
||||
@@ -188,7 +190,13 @@ NamedMapsController.prototype.staticMap = function(req, res) {
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (user, req, namedMapProvider, callback) {
|
||||
NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (
|
||||
user,
|
||||
req,
|
||||
params,
|
||||
namedMapProvider,
|
||||
callback
|
||||
) {
|
||||
var self = this;
|
||||
namedMapProvider.getTemplate(function (err, template) {
|
||||
if (err) {
|
||||
@@ -213,7 +221,7 @@ NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (us
|
||||
}
|
||||
|
||||
// overwrites 'all' default filter
|
||||
req.params.layer = layerVisibilityFilter.join(',');
|
||||
params.layer = layerVisibilityFilter.join(',');
|
||||
|
||||
// recreates the provider
|
||||
self.namedMapProviderCache.get(
|
||||
@@ -221,7 +229,7 @@ NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (us
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
req.params,
|
||||
params,
|
||||
callback
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,9 +2,6 @@ var step = require('step');
|
||||
var assert = require('assert');
|
||||
var templateName = require('../backends/template_maps').templateName;
|
||||
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
@@ -15,30 +12,59 @@ var userMiddleware = require('../middleware/user');
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @constructor
|
||||
*/
|
||||
function NamedMapsAdminController(authApi, pgConnection, templateMaps) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
function NamedMapsAdminController(authApi, templateMaps) {
|
||||
this.authApi = authApi;
|
||||
this.templateMaps = templateMaps;
|
||||
}
|
||||
|
||||
util.inherits(NamedMapsAdminController, BaseController);
|
||||
|
||||
module.exports = NamedMapsAdminController;
|
||||
|
||||
NamedMapsAdminController.prototype.register = function(app) {
|
||||
app.post(app.base_url_templated, cors(), userMiddleware, this.create.bind(this));
|
||||
app.put(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.update.bind(this));
|
||||
app.get(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.retrieve.bind(this));
|
||||
app.delete(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.destroy.bind(this));
|
||||
app.get(app.base_url_templated, cors(), userMiddleware, this.list.bind(this));
|
||||
app.options(app.base_url_templated + '/:template_id', cors('Content-Type'));
|
||||
NamedMapsAdminController.prototype.register = function (app) {
|
||||
app.post(
|
||||
app.base_url_templated + '/',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.create.bind(this)
|
||||
);
|
||||
|
||||
app.put(
|
||||
app.base_url_templated + '/:template_id',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.update.bind(this)
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_templated + '/:template_id',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.retrieve.bind(this)
|
||||
);
|
||||
|
||||
app.delete(
|
||||
app.base_url_templated + '/:template_id',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.destroy.bind(this)
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_templated + '/',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.list.bind(this)
|
||||
);
|
||||
|
||||
app.options(
|
||||
app.base_url_templated + '/:template_id',
|
||||
cors('Content-Type')
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.create = function(req, res) {
|
||||
NamedMapsAdminController.prototype.create = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var cdbuser = res.locals.user;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
@@ -55,14 +81,14 @@ NamedMapsAdminController.prototype.create = function(req, res) {
|
||||
assert.ifError(err);
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self, req, res, 'POST TEMPLATE')
|
||||
finishFn(self, req, res, 'POST TEMPLATE', null, next)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.update = function(req, res) {
|
||||
NamedMapsAdminController.prototype.update = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var cdbuser = res.locals.user;
|
||||
var template;
|
||||
var tpl_id;
|
||||
|
||||
@@ -84,16 +110,16 @@ NamedMapsAdminController.prototype.update = function(req, res) {
|
||||
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self, req, res, 'PUT TEMPLATE')
|
||||
finishFn(self, req, res, 'PUT TEMPLATE', null, next)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.retrieve = function(req, res) {
|
||||
NamedMapsAdminController.prototype.retrieve = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var cdbuser = res.locals.user;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
@@ -118,16 +144,16 @@ NamedMapsAdminController.prototype.retrieve = function(req, res) {
|
||||
delete tpl_val.auth_id;
|
||||
return { template: tpl_val };
|
||||
},
|
||||
finishFn(self, req, res, 'GET TEMPLATE')
|
||||
finishFn(self, req, res, 'GET TEMPLATE', null, next)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.destroy = function(req, res) {
|
||||
NamedMapsAdminController.prototype.destroy = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var cdbuser = res.locals.user;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
@@ -144,15 +170,15 @@ NamedMapsAdminController.prototype.destroy = function(req, res) {
|
||||
assert.ifError(err);
|
||||
return '';
|
||||
},
|
||||
finishFn(self, req, res, 'DELETE TEMPLATE', 204)
|
||||
finishFn(self, req, res, 'DELETE TEMPLATE', 204, next)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.list = function(req, res) {
|
||||
NamedMapsAdminController.prototype.list = function(req, res, next) {
|
||||
var self = this;
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var cdbuser = res.locals.user;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
@@ -168,16 +194,23 @@ NamedMapsAdminController.prototype.list = function(req, res) {
|
||||
assert.ifError(err);
|
||||
return { template_ids: tpl_ids };
|
||||
},
|
||||
finishFn(self, req, res, 'GET TEMPLATE LIST')
|
||||
finishFn(self, req, res, 'GET TEMPLATE LIST', null, next)
|
||||
);
|
||||
};
|
||||
|
||||
function finishFn(controller, req, res, description, status) {
|
||||
return function finish(err, response){
|
||||
function finishFn(controller, req, res, description, status, next) {
|
||||
return function finish(err, body){
|
||||
if (err) {
|
||||
controller.sendError(req, res, err, description);
|
||||
err.label = description;
|
||||
next(err);
|
||||
} else {
|
||||
controller.send(req, res, response, status || 200);
|
||||
res.status(status || 200);
|
||||
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(body);
|
||||
} else {
|
||||
res.json(body);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ module.exports = function allowQueryParams(params) {
|
||||
throw new Error('allowQueryParams must receive an Array of params');
|
||||
}
|
||||
return function allowQueryParamsMiddleware(req, res, next) {
|
||||
req.context.allowedQueryParams = params;
|
||||
res.locals.allowedQueryParams = params;
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
20
lib/cartodb/middleware/context/authorize.js
Normal file
20
lib/cartodb/middleware/context/authorize.js
Normal file
@@ -0,0 +1,20 @@
|
||||
module.exports = function authorizeMiddleware (authApi) {
|
||||
return function (req, res, next) {
|
||||
req.profiler.done('req2params.setup');
|
||||
|
||||
authApi.authorize(req, res, (err, authorized) => {
|
||||
req.profiler.done('authorize');
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if(!authorized) {
|
||||
err = new Error("Sorry, you are unauthorized (permission denied)");
|
||||
err.http_status = 403;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
};
|
||||
};
|
||||
32
lib/cartodb/middleware/context/clean-up-query-params.js
Normal file
32
lib/cartodb/middleware/context/clean-up-query-params.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const _ = require('underscore');
|
||||
|
||||
// Whitelist query parameters and attach format
|
||||
const REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'config',
|
||||
'map_key',
|
||||
'api_key',
|
||||
'auth_token',
|
||||
'callback',
|
||||
'zoom',
|
||||
'lon',
|
||||
'lat',
|
||||
// analysis
|
||||
'filters' // json
|
||||
];
|
||||
|
||||
module.exports = function cleanUpQueryParamsMiddleware () {
|
||||
return function cleanUpQueryParams (req, res, next) {
|
||||
var allowedQueryParams = REQUEST_QUERY_PARAMS_WHITELIST;
|
||||
|
||||
if (Array.isArray(res.locals.allowedQueryParams)) {
|
||||
allowedQueryParams = allowedQueryParams.concat(res.locals.allowedQueryParams);
|
||||
}
|
||||
|
||||
req.query = _.pick(req.query, allowedQueryParams);
|
||||
|
||||
// bring all query values onto res.locals object
|
||||
_.extend(res.locals, req.query);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
31
lib/cartodb/middleware/context/db-conn-setup.js
Normal file
31
lib/cartodb/middleware/context/db-conn-setup.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const _ = require('underscore');
|
||||
|
||||
module.exports = function dbConnSetupMiddleware(pgConnection) {
|
||||
return function dbConnSetup(req, res, next) {
|
||||
const user = res.locals.user;
|
||||
pgConnection.setDBConn(user, res.locals, (err) => {
|
||||
if (err) {
|
||||
if (err.message && -1 !== err.message.indexOf('name not found')) {
|
||||
err.http_status = 404;
|
||||
}
|
||||
req.profiler.done('req2params');
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// Add default database connection parameters
|
||||
// if none given
|
||||
_.defaults(res.locals, {
|
||||
dbuser: global.environment.postgres.user,
|
||||
dbpassword: global.environment.postgres.password,
|
||||
dbhost: global.environment.postgres.host,
|
||||
dbport: global.environment.postgres.port
|
||||
});
|
||||
|
||||
res.set('X-Served-By-DB-Host', res.locals.dbhost);
|
||||
|
||||
req.profiler.done('req2params');
|
||||
|
||||
next(null);
|
||||
});
|
||||
};
|
||||
};
|
||||
15
lib/cartodb/middleware/context/index.js
Normal file
15
lib/cartodb/middleware/context/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const locals = require('./locals');
|
||||
const cleanUpQueryParams = require('./clean-up-query-params');
|
||||
const layergroupToken = require('./layergroup-token');
|
||||
const authorize = require('./authorize');
|
||||
const dbConnSetup = require('./db-conn-setup');
|
||||
|
||||
module.exports = function prepareContextMiddleware(authApi, pgConnection) {
|
||||
return [
|
||||
locals,
|
||||
cleanUpQueryParams(),
|
||||
layergroupToken,
|
||||
authorize(authApi),
|
||||
dbConnSetup(pgConnection)
|
||||
];
|
||||
};
|
||||
32
lib/cartodb/middleware/context/layergroup-token.js
Normal file
32
lib/cartodb/middleware/context/layergroup-token.js
Normal file
@@ -0,0 +1,32 @@
|
||||
var LayergroupToken = require('../../models/layergroup-token');
|
||||
|
||||
module.exports = function layergroupTokenMiddleware(req, res, next) {
|
||||
if (!res.locals.token) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var user = res.locals.user;
|
||||
|
||||
var layergroupToken = LayergroupToken.parse(res.locals.token);
|
||||
res.locals.token = layergroupToken.token;
|
||||
res.locals.cache_buster = layergroupToken.cacheBuster;
|
||||
|
||||
if (layergroupToken.signer) {
|
||||
res.locals.signer = layergroupToken.signer;
|
||||
if (!res.locals.signer) {
|
||||
res.locals.signer = user;
|
||||
} else if (res.locals.signer !== user) {
|
||||
var err = new Error(`Cannot use map signature of user "${res.locals.signer}" on db of user "${user}"`);
|
||||
err.type = 'auth';
|
||||
err.http_status = 403;
|
||||
if (req.query && req.query.callback) {
|
||||
err.http_status = 200;
|
||||
}
|
||||
|
||||
req.profiler.done('req2params');
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
6
lib/cartodb/middleware/context/locals.js
Normal file
6
lib/cartodb/middleware/context/locals.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = function localsMiddleware(req, res, next) {
|
||||
// save req.params in res.locals
|
||||
res.locals = Object.assign(req.params || {}, res.locals);
|
||||
|
||||
next();
|
||||
};
|
||||
162
lib/cartodb/middleware/error-middleware.js
Normal file
162
lib/cartodb/middleware/error-middleware.js
Normal file
@@ -0,0 +1,162 @@
|
||||
const _ = require('underscore');
|
||||
const debug = require('debug')('windshaft:cartodb:error-middleware');
|
||||
|
||||
module.exports = function errorMiddleware (/* options */) {
|
||||
return function error (err, req, res, next) {
|
||||
// jshint unused:false
|
||||
// jshint maxcomplexity:9
|
||||
var allErrors = Array.isArray(err) ? err : [err];
|
||||
|
||||
allErrors = populateTimeoutErrors(allErrors);
|
||||
|
||||
const label = err.label || 'UNKNOWN';
|
||||
err = allErrors[0] || new Error(label);
|
||||
allErrors[0] = err;
|
||||
|
||||
var statusCode = findStatusCode(err);
|
||||
|
||||
if (err.message === 'Tile does not exist' && res.locals.format === 'mvt') {
|
||||
statusCode = 204;
|
||||
}
|
||||
|
||||
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
|
||||
|
||||
// If a callback was requested, force status to 200
|
||||
if (req.query && req.query.callback) {
|
||||
statusCode = 200;
|
||||
}
|
||||
|
||||
var errorResponseBody = {
|
||||
errors: allErrors.map(errorMessage),
|
||||
errors_with_context: allErrors.map(errorMessageWithContext)
|
||||
};
|
||||
|
||||
res.status(statusCode);
|
||||
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(errorResponseBody);
|
||||
} else {
|
||||
res.json(errorResponseBody);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function isRenderTimeoutError (err) {
|
||||
return err.message === 'Render timed out';
|
||||
}
|
||||
|
||||
function isDatasourceTimeoutError (err) {
|
||||
return err.message && err.message.match(/canceling statement due to statement timeout/i);
|
||||
}
|
||||
|
||||
function isTimeoutError (err) {
|
||||
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
|
||||
}
|
||||
|
||||
function populateTimeoutErrors (errors) {
|
||||
return errors.map(function (error) {
|
||||
if (isRenderTimeoutError(error)) {
|
||||
error.subtype = 'render';
|
||||
}
|
||||
|
||||
if (isDatasourceTimeoutError(error)) {
|
||||
error.subtype = 'datasource';
|
||||
}
|
||||
|
||||
if (isTimeoutError(error)) {
|
||||
error.message = 'You are over platform\'s limits. Please contact us to know more details';
|
||||
error.type = 'limit';
|
||||
error.http_status = 429;
|
||||
}
|
||||
|
||||
return error;
|
||||
});
|
||||
}
|
||||
|
||||
function findStatusCode(err) {
|
||||
var statusCode;
|
||||
if ( err.http_status ) {
|
||||
statusCode = err.http_status;
|
||||
} else {
|
||||
statusCode = statusFromErrorMessage('' + err);
|
||||
}
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
module.exports.findStatusCode = findStatusCode;
|
||||
|
||||
function statusFromErrorMessage(errMsg) {
|
||||
// Find an appropriate statusCode based on message
|
||||
// jshint maxcomplexity:7
|
||||
var statusCode = 400;
|
||||
if ( -1 !== errMsg.indexOf('permission denied') ) {
|
||||
statusCode = 403;
|
||||
}
|
||||
else if ( -1 !== errMsg.indexOf('authentication failed') ) {
|
||||
statusCode = 403;
|
||||
}
|
||||
else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) {
|
||||
statusCode = 400;
|
||||
}
|
||||
else if ( -1 !== errMsg.indexOf('does not exist') ) {
|
||||
if ( -1 !== errMsg.indexOf(' role ') ) {
|
||||
statusCode = 403; // role 'xxx' does not exist
|
||||
} else if ( errMsg.match(/function .* does not exist/) ) {
|
||||
statusCode = 400; // invalid SQL (SQL function does not exist)
|
||||
} else {
|
||||
statusCode = 404;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
function errorMessage(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
|
||||
return stripConnectionInfo(message);
|
||||
}
|
||||
|
||||
module.exports.errorMessage = errorMessage;
|
||||
|
||||
function stripConnectionInfo(message) {
|
||||
// Strip connection info, if any
|
||||
return message
|
||||
// See https://github.com/CartoDB/Windshaft/issues/173
|
||||
.replace(/Connection string: '[^']*'\n\s/im, '')
|
||||
// See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644
|
||||
.replace(/is the server.*encountered/im, 'encountered');
|
||||
}
|
||||
|
||||
var ERROR_INFO_TO_EXPOSE = {
|
||||
message: true,
|
||||
layer: true,
|
||||
type: true,
|
||||
analysis: true,
|
||||
subtype: true
|
||||
};
|
||||
|
||||
function shouldBeExposed (prop) {
|
||||
return !!ERROR_INFO_TO_EXPOSE[prop];
|
||||
}
|
||||
|
||||
function errorMessageWithContext(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
|
||||
var error = {
|
||||
type: err.type || 'unknown',
|
||||
message: stripConnectionInfo(message),
|
||||
};
|
||||
|
||||
for (var prop in err) {
|
||||
// type & message are properties from Error's prototype and will be skipped
|
||||
if (err.hasOwnProperty(prop) && shouldBeExposed(prop)) {
|
||||
error[prop] = err[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
var LZMA = require('lzma').LZMA;
|
||||
const LZMA = require('lzma').LZMA;
|
||||
|
||||
var lzmaWorker = new LZMA();
|
||||
const lzmaWorker = new LZMA();
|
||||
|
||||
module.exports = function lzmaMiddleware(req, res, next) {
|
||||
if (!req.query.hasOwnProperty('lzma')) {
|
||||
|
||||
27
lib/cartodb/middleware/stats.js
Normal file
27
lib/cartodb/middleware/stats.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const Profiler = require('../stats/profiler_proxy');
|
||||
const debug = require('debug')('windshaft:cartodb:stats');
|
||||
const onHeaders = require('on-headers');
|
||||
|
||||
module.exports = function statsMiddleware(options) {
|
||||
const { enabled = true, statsClient } = options;
|
||||
|
||||
return function stats(req, res, next) {
|
||||
req.profiler = new Profiler({
|
||||
statsd_client: statsClient,
|
||||
profile: enabled
|
||||
});
|
||||
|
||||
onHeaders(res, () => res.set('X-Tiler-Profiler', req.profiler.toJSONString()));
|
||||
|
||||
res.on('finish', () => {
|
||||
try {
|
||||
// May throw due to dns, see: http://github.com/CartoDB/Windshaft/issues/166
|
||||
req.profiler.sendStats();
|
||||
} catch (err) {
|
||||
debug("error sending profiling stats: " + err);
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
@@ -2,6 +2,7 @@ var CdbRequest = require('../models/cdb_request');
|
||||
var cdbRequest = new CdbRequest();
|
||||
|
||||
module.exports = function userMiddleware(req, res, next) {
|
||||
req.context.user = cdbRequest.userByReq(req);
|
||||
res.locals.user = cdbRequest.userByReq(req);
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
30
lib/cartodb/middleware/vector-error.js
Normal file
30
lib/cartodb/middleware/vector-error.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const timeoutErrorVectorTile = fs.readFileSync(__dirname + '/../../../assets/render-timeout-fallback.mvt');
|
||||
|
||||
module.exports = function vectorError() {
|
||||
return function vectorErrorMiddleware(err, req, res, next) {
|
||||
if(req.params.format === 'mvt') {
|
||||
|
||||
if (isTimeoutError(err)) {
|
||||
res.set('Content-Type', 'application/x-protobuf');
|
||||
return res.status(429).send(timeoutErrorVectorTile);
|
||||
}
|
||||
}
|
||||
|
||||
next(err);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
function isRenderTimeoutError (err) {
|
||||
return err.message === 'Render timed out';
|
||||
}
|
||||
|
||||
function isDatasourceTimeoutError (err) {
|
||||
return err.message && err.message.match(/canceling statement due to statement timeout/i);
|
||||
}
|
||||
|
||||
function isTimeoutError (err) {
|
||||
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
|
||||
}
|
||||
@@ -245,6 +245,10 @@ module.exports = class Aggregation extends BaseDataview {
|
||||
return null;
|
||||
}
|
||||
|
||||
const limit = Number.isFinite(override.categories) && override.categories > 0 ?
|
||||
override.categories :
|
||||
CATEGORIES_LIMIT;
|
||||
|
||||
const aggregationSql = aggregationDataviewQueryTpl({
|
||||
override: override,
|
||||
query: this.query,
|
||||
@@ -256,7 +260,7 @@ module.exports = class Aggregation extends BaseDataview {
|
||||
aggregationColumn: this.aggregationColumn || 1
|
||||
}),
|
||||
isFloatColumn: this._isFloatColumn,
|
||||
limit: CATEGORIES_LIMIT
|
||||
limit
|
||||
});
|
||||
|
||||
debug(aggregationSql);
|
||||
|
||||
@@ -1,34 +1,14 @@
|
||||
const BaseDataview = require('./base');
|
||||
const debug = require('debug')('windshaft:dataview:formula');
|
||||
const utils = require('../../utils/query-utils');
|
||||
|
||||
const countInfinitiesQueryTpl = ctx => `
|
||||
SELECT count(1) FROM (${ctx.query}) __cdb_formula_infinities
|
||||
WHERE ${ctx.column} = 'infinity'::float OR ${ctx.column} = '-infinity'::float
|
||||
`;
|
||||
|
||||
const countNansQueryTpl = ctx => `
|
||||
SELECT count(1) FROM (${ctx.query}) __cdb_formula_nans
|
||||
WHERE ${ctx.column} = 'NaN'::float
|
||||
`;
|
||||
|
||||
const filterOutSpecialNumericValuesTpl = ctx => `
|
||||
WHERE
|
||||
${ctx.column} != 'infinity'::float
|
||||
AND
|
||||
${ctx.column} != '-infinity'::float
|
||||
AND
|
||||
${ctx.column} != 'NaN'::float
|
||||
`;
|
||||
|
||||
const formulaQueryTpl = ctx => `
|
||||
SELECT
|
||||
${ctx.operation}(${ctx.column}) AS result,
|
||||
(SELECT count(1) FROM (${ctx.query}) _cdb_formula_nulls WHERE ${ctx.column} IS NULL) AS nulls_count
|
||||
${ctx.isFloatColumn ? `,(${countInfinitiesQueryTpl(ctx)}) AS infinities_count` : ''}
|
||||
${ctx.isFloatColumn ? `,(${countNansQueryTpl(ctx)}) AS nans_count` : ''}
|
||||
FROM (${ctx.query}) __cdb_formula
|
||||
${ctx.isFloatColumn && ctx.operation !== 'count' ? `${filterOutSpecialNumericValuesTpl(ctx)}` : ''}
|
||||
`;
|
||||
const formulaQueryTpl = ctx =>
|
||||
`SELECT
|
||||
${ctx.operation}(${utils.handleFloatColumn(ctx)}) AS result,
|
||||
${utils.countNULLs(ctx)} AS nulls_count
|
||||
${ctx.isFloatColumn ? `,${utils.countInfinites(ctx)} AS infinities_count,` : ``}
|
||||
${ctx.isFloatColumn ? `${utils.countNaNs(ctx)} AS nans_count` : ``}
|
||||
FROM (${ctx.query}) __cdb_formula`;
|
||||
|
||||
const VALID_OPERATIONS = {
|
||||
count: true,
|
||||
|
||||
@@ -1,5 +1,102 @@
|
||||
const BaseHistogram = require('./base-histogram');
|
||||
const debug = require('debug')('windshaft:dataview:date-histogram');
|
||||
const utils = require('../../../utils/query-utils');
|
||||
|
||||
/**
|
||||
* Gets the name of a timezone with the same offset as the required
|
||||
* using the pg_timezone_names table. We do this because it's simpler to pass
|
||||
* the name than to pass the offset itself as PostgreSQL uses different
|
||||
* sign convention. For example: TIME ZONE 'CET' is equal to TIME ZONE 'UTC-1',
|
||||
* not 'UTC+1' which would be expected.
|
||||
* Gives priority to Etc/GMT±N timezones but still support odd offsets like 8.5
|
||||
* hours for Asia/Pyongyang.
|
||||
* It also makes it easier to, in the future, support the input of expected timezone
|
||||
* instead of the offset; that is using 'Europe/Madrid' instead of
|
||||
* '+3600' or '+7200'. The daylight saving status can be handled by postgres.
|
||||
*/
|
||||
const offsetNameQueryTpl = ctx => `
|
||||
WITH __wd_tz AS
|
||||
(
|
||||
SELECT name
|
||||
FROM pg_timezone_names
|
||||
WHERE utc_offset = interval '${ctx.offset} hours'
|
||||
ORDER BY CASE WHEN name LIKE 'Etc/GMT%' THEN 0 ELSE 1 END
|
||||
LIMIT 1
|
||||
),`;
|
||||
|
||||
/**
|
||||
* Function to get the subquery that places each row in its bin depending on
|
||||
* the aggregation. Since the data stored is in epoch we need to adapt it to
|
||||
* our timezone so when calling date_trunc it falls into the correct bin
|
||||
*/
|
||||
function dataBucketsQuery(ctx) {
|
||||
var condition_str = '';
|
||||
|
||||
if (ctx.start !== 0) {
|
||||
condition_str = `WHERE ${ctx.column} >= to_timestamp(${ctx.start})`;
|
||||
}
|
||||
if (ctx.end !== 0) {
|
||||
if (condition_str === '') {
|
||||
condition_str = `WHERE ${ctx.column} <= to_timestamp(${ctx.end})`;
|
||||
}
|
||||
else {
|
||||
condition_str += ` and ${ctx.column} <= to_timestamp(${ctx.end})`;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
__wd_buckets AS
|
||||
(
|
||||
SELECT
|
||||
date_trunc('${ctx.aggregation}', timezone(__wd_tz.name, ${ctx.column}::timestamptz)) as timestamp,
|
||||
count(*) as freq,
|
||||
${utils.countNULLs(ctx)} as nulls_count
|
||||
FROM
|
||||
(
|
||||
${ctx.query}
|
||||
) __source, __wd_tz
|
||||
${condition_str}
|
||||
GROUP BY timestamp, __wd_tz.name
|
||||
),`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that generates an array with all the possible bins between the
|
||||
* start and end date. If not provided we use the min and max generated from
|
||||
* the dataBucketsQuery
|
||||
*/
|
||||
function allBucketsArrayQuery(ctx) {
|
||||
var extra_from = ``;
|
||||
var series_start = ``;
|
||||
var series_end = ``;
|
||||
|
||||
if (ctx.start === 0) {
|
||||
extra_from = `, __wd_buckets GROUP BY __wd_tz.name`;
|
||||
series_start = `min(__wd_buckets.timestamp)`;
|
||||
} else {
|
||||
series_start = `date_trunc('${ctx.aggregation}', timezone(__wd_tz.name, to_timestamp(${ctx.start})))`;
|
||||
}
|
||||
|
||||
if (ctx.end === 0) {
|
||||
extra_from = `, __wd_buckets GROUP BY __wd_tz.name`;
|
||||
series_end = `max(__wd_buckets.timestamp)`;
|
||||
} else {
|
||||
series_end = `date_trunc('${ctx.aggregation}', timezone(__wd_tz.name, to_timestamp(${ctx.end})))`;
|
||||
}
|
||||
|
||||
return `
|
||||
__wd_all_buckets AS
|
||||
(
|
||||
SELECT ARRAY(
|
||||
SELECT
|
||||
generate_series(
|
||||
${series_start},
|
||||
${series_end},
|
||||
interval '${ctx.interval}') as bin_start
|
||||
FROM __wd_tz${extra_from}
|
||||
) as bins
|
||||
)`;
|
||||
}
|
||||
|
||||
const dateIntervalQueryTpl = ctx => `
|
||||
WITH
|
||||
@@ -41,107 +138,6 @@ const dateIntervalQueryTpl = ctx => `
|
||||
FROM __cdb_interval_in_days, __cdb_interval_in_hours, __cdb_interval_in_minutes, __cdb_interval_in_seconds
|
||||
`;
|
||||
|
||||
const nullsQueryTpl = ctx => `
|
||||
__cdb_nulls AS (
|
||||
SELECT
|
||||
count(*) AS __cdb_nulls_count
|
||||
FROM (${ctx.query}) __cdb_histogram_nulls
|
||||
WHERE ${ctx.column} IS NULL
|
||||
)
|
||||
`;
|
||||
|
||||
const dateBasicsQueryTpl = ctx => `
|
||||
__cdb_basics AS (
|
||||
SELECT
|
||||
max(date_part('epoch', ${ctx.column})) AS __cdb_max_val,
|
||||
min(date_part('epoch', ${ctx.column})) AS __cdb_min_val,
|
||||
avg(date_part('epoch', ${ctx.column})) AS __cdb_avg_val,
|
||||
min(
|
||||
date_trunc(
|
||||
'${ctx.aggregation}', ${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}'
|
||||
)
|
||||
) AS __cdb_start_date,
|
||||
max(${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}') AS __cdb_end_date,
|
||||
count(1) AS __cdb_total_rows
|
||||
FROM (${ctx.query}) __cdb_basics_query
|
||||
)
|
||||
`;
|
||||
|
||||
const dateOverrideBasicsQueryTpl = ctx => `
|
||||
__cdb_basics AS (
|
||||
SELECT
|
||||
max(${ctx.end})::float AS __cdb_max_val,
|
||||
min(${ctx.start})::float AS __cdb_min_val,
|
||||
avg(date_part('epoch', ${ctx.column})) AS __cdb_avg_val,
|
||||
min(
|
||||
date_trunc(
|
||||
'${ctx.aggregation}',
|
||||
TO_TIMESTAMP(${ctx.start})::timestamp AT TIME ZONE '${ctx.offset}'
|
||||
)
|
||||
) AS __cdb_start_date,
|
||||
max(
|
||||
TO_TIMESTAMP(${ctx.end})::timestamp AT TIME ZONE '${ctx.offset}'
|
||||
) AS __cdb_end_date,
|
||||
count(1) AS __cdb_total_rows
|
||||
FROM (${ctx.query}) __cdb_basics_query
|
||||
)
|
||||
`;
|
||||
|
||||
const dateBinsQueryTpl = ctx => `
|
||||
__cdb_bins AS (
|
||||
SELECT
|
||||
__cdb_bins_array,
|
||||
ARRAY_LENGTH(__cdb_bins_array, 1) AS __cdb_bins_number
|
||||
FROM (
|
||||
SELECT
|
||||
ARRAY(
|
||||
SELECT GENERATE_SERIES(
|
||||
__cdb_start_date::timestamptz,
|
||||
__cdb_end_date::timestamptz,
|
||||
${ctx.aggregation === 'quarter' ? `'3 month'::interval` : `'1 ${ctx.aggregation}'::interval`}
|
||||
)
|
||||
) AS __cdb_bins_array
|
||||
FROM __cdb_basics
|
||||
) __cdb_bins_array_query
|
||||
)
|
||||
`;
|
||||
|
||||
const dateHistogramQueryTpl = ctx => `
|
||||
SELECT
|
||||
(__cdb_max_val - __cdb_min_val) / cast(__cdb_bins_number as float) AS bin_width,
|
||||
__cdb_bins_number AS bins_number,
|
||||
__cdb_nulls_count AS nulls_count,
|
||||
CASE WHEN __cdb_min_val = __cdb_max_val
|
||||
THEN 0
|
||||
ELSE GREATEST(
|
||||
1,
|
||||
LEAST(
|
||||
WIDTH_BUCKET(
|
||||
${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}',
|
||||
__cdb_bins_array
|
||||
),
|
||||
__cdb_bins_number
|
||||
)
|
||||
) - 1
|
||||
END AS bin,
|
||||
min(
|
||||
date_part(
|
||||
'epoch',
|
||||
date_trunc(
|
||||
'${ctx.aggregation}', ${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}'
|
||||
) AT TIME ZONE '${ctx.offset}'
|
||||
)
|
||||
)::numeric AS timestamp,
|
||||
date_part('epoch', __cdb_start_date)::numeric AS timestamp_start,
|
||||
min(date_part('epoch', ${ctx.column}))::numeric AS min,
|
||||
max(date_part('epoch', ${ctx.column}))::numeric AS max,
|
||||
avg(date_part('epoch', ${ctx.column}))::numeric AS avg,
|
||||
count(*) AS freq
|
||||
FROM (${ctx.query}) __cdb_histogram, __cdb_basics, __cdb_bins, __cdb_nulls
|
||||
WHERE date_part('epoch', ${ctx.column}) IS NOT NULL
|
||||
GROUP BY bin, bins_number, bin_width, nulls_count, timestamp_start
|
||||
ORDER BY bin
|
||||
`;
|
||||
|
||||
const MAX_INTERVAL_VALUE = 366;
|
||||
|
||||
@@ -176,12 +172,21 @@ module.exports = class DateHistogram extends BaseHistogram {
|
||||
|
||||
_buildQueryTpl (ctx) {
|
||||
return `
|
||||
WITH
|
||||
${this._hasOverridenRange(ctx.override) ? dateOverrideBasicsQueryTpl(ctx) : dateBasicsQueryTpl(ctx)},
|
||||
${dateBinsQueryTpl(ctx)},
|
||||
${nullsQueryTpl(ctx)}
|
||||
${dateHistogramQueryTpl(ctx)}
|
||||
`;
|
||||
${offsetNameQueryTpl(ctx)}
|
||||
${dataBucketsQuery(ctx)}
|
||||
${allBucketsArrayQuery(ctx)}
|
||||
SELECT
|
||||
array_position(__wd_all_buckets.bins, __wd_buckets.timestamp) - 1 as bin,
|
||||
date_part('epoch', timezone(__wd_tz.name, __wd_buckets.timestamp)) AS timestamp,
|
||||
__wd_buckets.freq as freq,
|
||||
date_part('epoch', timezone(__wd_tz.name, (__wd_all_buckets.bins)[1])) as timestamp_start,
|
||||
array_length(__wd_all_buckets.bins, 1) as bins_number,
|
||||
date_part('epoch', interval '${ctx.interval}') as bin_width,
|
||||
__wd_buckets.nulls_count as nulls_count
|
||||
FROM __wd_buckets, __wd_all_buckets, __wd_tz
|
||||
GROUP BY __wd_tz.name, __wd_all_buckets.bins, __wd_buckets.timestamp, __wd_buckets.nulls_count, __wd_buckets.freq
|
||||
ORDER BY bin ASC;
|
||||
`;
|
||||
}
|
||||
|
||||
_buildQuery (psql, override, callback) {
|
||||
@@ -204,6 +209,9 @@ module.exports = class DateHistogram extends BaseHistogram {
|
||||
return null;
|
||||
}
|
||||
|
||||
var interval = this._getAggregation(override) === 'quarter' ?
|
||||
'3 months' : '1 ' + this._getAggregation(override);
|
||||
|
||||
const histogramSql = this._buildQueryTpl({
|
||||
override: override,
|
||||
query: this.query,
|
||||
@@ -211,7 +219,8 @@ module.exports = class DateHistogram extends BaseHistogram {
|
||||
aggregation: this._getAggregation(override),
|
||||
start: this._getBinStart(override),
|
||||
end: this._getBinEnd(override),
|
||||
offset: this._parseOffset(override)
|
||||
offset: this._parseOffset(override),
|
||||
interval: interval
|
||||
});
|
||||
|
||||
debug(histogramSql);
|
||||
@@ -264,8 +273,8 @@ module.exports = class DateHistogram extends BaseHistogram {
|
||||
offset: this._getOffset(override),
|
||||
timestamp_start: firstRow.timestamp_start,
|
||||
|
||||
bin_width: firstRow.bin_width,
|
||||
bins_count: firstRow.bins_number,
|
||||
bin_width: firstRow.bin_width || 0,
|
||||
bins_count: firstRow.bins_number || 0,
|
||||
bins_start: firstRow.timestamp,
|
||||
nulls: firstRow.nulls_count,
|
||||
infinities: firstRow.infinities_count,
|
||||
@@ -275,6 +284,10 @@ module.exports = class DateHistogram extends BaseHistogram {
|
||||
}
|
||||
|
||||
_getBuckets (result) {
|
||||
result.rows.forEach(function(row) {
|
||||
row.min = row.max = row.avg = row.timestamp;
|
||||
});
|
||||
|
||||
return result.rows.map(({ bin, min, max, avg, freq, timestamp }) => ({ bin, min, max, avg, freq, timestamp }));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,25 @@
|
||||
const BaseHistogram = require('./base-histogram');
|
||||
const debug = require('debug')('windshaft:dataview:numeric-histogram');
|
||||
const utils = require('../../../utils/query-utils');
|
||||
|
||||
const columnCastTpl = ctx => `date_part('epoch', ${ctx.column})`;
|
||||
|
||||
const filterOutSpecialNumericValues = ctx => `
|
||||
${ctx.column} != 'infinity'::float
|
||||
AND
|
||||
${ctx.column} != '-infinity'::float
|
||||
AND
|
||||
${ctx.column} != 'NaN'::float
|
||||
`;
|
||||
|
||||
const filteredQueryTpl = ctx => `
|
||||
/** Query to get min and max values from the query */
|
||||
const irqQueryTpl = ctx => `
|
||||
__cdb_filtered_source AS (
|
||||
SELECT *
|
||||
FROM (${ctx.query}) __cdb_filtered_source_query
|
||||
WHERE ${ctx.column} IS NOT NULL
|
||||
${ctx.isFloatColumn ? `AND ${filterOutSpecialNumericValues(ctx)}` : ''}
|
||||
)
|
||||
`;
|
||||
|
||||
const basicsQueryTpl = ctx => `
|
||||
WHERE ${utils.handleFloatColumn(ctx)} IS NOT NULL
|
||||
),
|
||||
__cdb_basics AS (
|
||||
SELECT
|
||||
max(${ctx.column}) AS __cdb_max_val, min(${ctx.column}) AS __cdb_min_val,
|
||||
avg(${ctx.column}) AS __cdb_avg_val, count(1) AS __cdb_total_rows
|
||||
max(${ctx.column}) AS __cdb_max_val,
|
||||
min(${ctx.column}) AS __cdb_min_val,
|
||||
count(1) AS __cdb_total_rows
|
||||
FROM __cdb_filtered_source
|
||||
)
|
||||
`;
|
||||
|
||||
const overrideBasicsQueryTpl = ctx => `
|
||||
__cdb_basics AS (
|
||||
SELECT
|
||||
max(${ctx.end}) AS __cdb_max_val, min(${ctx.start}) AS __cdb_min_val,
|
||||
avg(${ctx.column}) AS __cdb_avg_val, count(1) AS __cdb_total_rows
|
||||
FROM __cdb_filtered_source
|
||||
)
|
||||
`;
|
||||
|
||||
const iqrQueryTpl = ctx => `
|
||||
/* Query to calculate the number of bins (needs irqQueryTpl before it*/
|
||||
const binsQueryTpl = ctx => `
|
||||
__cdb_iqrange AS (
|
||||
SELECT max(quartile_max) - min(quartile_max) AS __cdb_iqr
|
||||
FROM (
|
||||
@@ -49,10 +30,7 @@ const iqrQueryTpl = ctx => `
|
||||
WHERE quartile = 1 or quartile = 3
|
||||
GROUP BY quartile
|
||||
) __cdb_iqr
|
||||
)
|
||||
`;
|
||||
|
||||
const binsQueryTpl = ctx => `
|
||||
),
|
||||
__cdb_bins AS (
|
||||
SELECT
|
||||
CASE WHEN __cdb_total_rows = 0 OR __cdb_iqr = 0
|
||||
@@ -70,83 +48,6 @@ const binsQueryTpl = ctx => `
|
||||
)
|
||||
`;
|
||||
|
||||
const overrideBinsQueryTpl = ctx => `
|
||||
__cdb_bins AS (
|
||||
SELECT ${ctx.override.bins} AS __cdb_bins_number
|
||||
)
|
||||
`;
|
||||
|
||||
const nullsQueryTpl = ctx => `
|
||||
__cdb_nulls AS (
|
||||
SELECT
|
||||
count(*) AS __cdb_nulls_count
|
||||
FROM (${ctx.query}) __cdb_histogram_nulls
|
||||
WHERE ${ctx.column} IS NULL
|
||||
)
|
||||
`;
|
||||
|
||||
const infinitiesQueryTpl = ctx => `
|
||||
__cdb_infinities AS (
|
||||
SELECT
|
||||
count(*) AS __cdb_infinities_count
|
||||
FROM (${ctx.query}) __cdb_infinities_query
|
||||
WHERE
|
||||
${ctx.column} = 'infinity'::float
|
||||
OR
|
||||
${ctx.column} = '-infinity'::float
|
||||
)
|
||||
`;
|
||||
|
||||
const nansQueryTpl = ctx => `
|
||||
__cdb_nans AS (
|
||||
SELECT
|
||||
count(*) AS __cdb_nans_count
|
||||
FROM (${ctx.query}) __cdb_nans_query
|
||||
WHERE ${ctx.column} = 'NaN'::float
|
||||
)
|
||||
`;
|
||||
|
||||
const specialNumericValuesColumnDefinitionTpl = () => `
|
||||
__cdb_infinities_count AS infinities_count,
|
||||
__cdb_nans_count AS nans_count
|
||||
`;
|
||||
|
||||
const specialNumericValuesCTETpl = () => `
|
||||
__cdb_infinities, __cdb_nans
|
||||
`;
|
||||
|
||||
const specialNumericValuesColumnTpl = () => `
|
||||
infinities_count, nans_count
|
||||
`;
|
||||
|
||||
const histogramQueryTpl = ctx => `
|
||||
SELECT
|
||||
(__cdb_max_val - __cdb_min_val) / cast(__cdb_bins_number as float) AS bin_width,
|
||||
__cdb_bins_number AS bins_number,
|
||||
__cdb_nulls_count AS nulls_count,
|
||||
${ctx.isFloatColumn ? `${specialNumericValuesColumnDefinitionTpl()},` : ''}
|
||||
__cdb_avg_val AS avg_val,
|
||||
CASE WHEN __cdb_min_val = __cdb_max_val
|
||||
THEN 0
|
||||
ELSE GREATEST(
|
||||
1,
|
||||
LEAST(
|
||||
WIDTH_BUCKET(${ctx.column}, __cdb_min_val, __cdb_max_val, __cdb_bins_number),
|
||||
__cdb_bins_number
|
||||
)
|
||||
) - 1
|
||||
END AS bin,
|
||||
min(${ctx.column})::numeric AS min,
|
||||
max(${ctx.column})::numeric AS max,
|
||||
avg(${ctx.column})::numeric AS avg,
|
||||
count(*) AS freq
|
||||
FROM __cdb_filtered_source, __cdb_basics, __cdb_nulls, __cdb_bins
|
||||
${ctx.isFloatColumn ? `, ${specialNumericValuesCTETpl()}` : ''}
|
||||
GROUP BY bin, bins_number, bin_width, nulls_count, avg_val
|
||||
${ctx.isFloatColumn ? `, ${specialNumericValuesColumnTpl()}` : ''}
|
||||
ORDER BY bin
|
||||
`;
|
||||
|
||||
const BIN_MIN_NUMBER = 6;
|
||||
const BIN_MAX_NUMBER = 48;
|
||||
|
||||
@@ -167,14 +68,14 @@ module.exports = class NumericHistogram extends BaseHistogram {
|
||||
|
||||
_buildQuery (psql, override, callback) {
|
||||
const histogramSql = this._buildQueryTpl({
|
||||
override: override,
|
||||
column: this._columnType === 'date' ? columnCastTpl({ column: this.column }) : this.column,
|
||||
column: this._columnType === 'date' ? utils.columnCastTpl({ column: this.column }) : this.column,
|
||||
isFloatColumn: this._columnType === 'float',
|
||||
query: this.query,
|
||||
start: this._getBinStart(override),
|
||||
end: this._getBinEnd(override),
|
||||
bins: this._getBinsCount(override),
|
||||
minBins: BIN_MIN_NUMBER,
|
||||
maxBins: BIN_MAX_NUMBER,
|
||||
maxBins: BIN_MAX_NUMBER
|
||||
});
|
||||
|
||||
debug(histogramSql);
|
||||
@@ -182,19 +83,62 @@ module.exports = class NumericHistogram extends BaseHistogram {
|
||||
return callback(null, histogramSql);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ctx: Object with the following values
|
||||
* ctx.column -- Column for the histogram
|
||||
* ctx.isFloatColumn - Whether the column is float or not
|
||||
* ctx.query -- Subquery to extract data
|
||||
* ctx.start -- Start value for the bins. [>= end to force calculation]
|
||||
* ctx.end -- End value for the bins.
|
||||
* ctx.bins -- Numbers of bins to generate [<0 to force calculation]
|
||||
* ctx.minBins - If !full min bins to calculate [Optional]
|
||||
* ctx.maxBins - If !full max bins to calculate [Optional]
|
||||
*/
|
||||
_buildQueryTpl (ctx) {
|
||||
var extra_tables = ``;
|
||||
var extra_queries = ``;
|
||||
var extra_groupby = ``;
|
||||
|
||||
if (ctx.start >= ctx.end) {
|
||||
ctx.end = `__cdb_basics.__cdb_max_val`;
|
||||
ctx.start = `__cdb_basics.__cdb_min_val`;
|
||||
extra_groupby = `, __cdb_basics.__cdb_max_val, __cdb_basics.__cdb_min_val`;
|
||||
extra_tables = `, __cdb_basics`;
|
||||
extra_queries = `WITH ${irqQueryTpl(ctx)}`;
|
||||
}
|
||||
|
||||
if (ctx.bins <= 0) {
|
||||
ctx.bins = `__cdb_bins.__cdb_bins_number`;
|
||||
extra_groupby += `, __cdb_bins.__cdb_bins_number`;
|
||||
extra_tables += `, __cdb_bins`;
|
||||
extra_queries = `WITH ${irqQueryTpl(ctx)}, ${binsQueryTpl(ctx)}`;
|
||||
}
|
||||
|
||||
return `
|
||||
WITH
|
||||
${filteredQueryTpl(ctx)},
|
||||
${this._hasOverridenRange(ctx.override) ? overrideBasicsQueryTpl(ctx) : basicsQueryTpl(ctx)},
|
||||
${this._hasOverridenBins(ctx.override) ?
|
||||
overrideBinsQueryTpl(ctx) :
|
||||
`${iqrQueryTpl(ctx)}, ${binsQueryTpl(ctx)}`
|
||||
},
|
||||
${nullsQueryTpl(ctx)}
|
||||
${ctx.isFloatColumn ? `,${infinitiesQueryTpl(ctx)}, ${nansQueryTpl(ctx)}` : ''}
|
||||
${histogramQueryTpl(ctx)}
|
||||
`;
|
||||
${extra_queries}
|
||||
SELECT
|
||||
(${ctx.end} - ${ctx.start}) / ${ctx.bins}::float AS bin_width,
|
||||
${ctx.bins} as bins_number,
|
||||
${utils.countNULLs(ctx)} AS nulls_count,
|
||||
${utils.countInfinites(ctx)} AS infinities_count,
|
||||
${utils.countNaNs(ctx)} AS nans_count,
|
||||
min(${utils.handleFloatColumn(ctx)}) AS min,
|
||||
max(${utils.handleFloatColumn(ctx)}) AS max,
|
||||
avg(${utils.handleFloatColumn(ctx)}) AS avg,
|
||||
sum(CASE WHEN (${utils.handleFloatColumn(ctx)} is not NULL) THEN 1 ELSE 0 END) as freq,
|
||||
CASE WHEN ${ctx.start} = ${ctx.end}
|
||||
THEN 0
|
||||
ELSE GREATEST(1, LEAST(
|
||||
${ctx.bins},
|
||||
WIDTH_BUCKET(${utils.handleFloatColumn(ctx)}, ${ctx.start}, ${ctx.end}, ${ctx.bins}))) - 1
|
||||
END AS bin
|
||||
FROM
|
||||
(
|
||||
${ctx.query}
|
||||
) __cdb_filtered_source_query${extra_tables}
|
||||
GROUP BY bin${extra_groupby}
|
||||
ORDER BY bin;`;
|
||||
}
|
||||
|
||||
_hasOverridenBins (override) {
|
||||
@@ -204,14 +148,31 @@ module.exports = class NumericHistogram extends BaseHistogram {
|
||||
_getSummary (result, override) {
|
||||
const firstRow = result.rows[0] || {};
|
||||
|
||||
var total_nulls = 0;
|
||||
var total_infinities = 0;
|
||||
var total_nans = 0;
|
||||
var total_avg = 0;
|
||||
var total_count = 0;
|
||||
|
||||
result.rows.forEach(function(row) {
|
||||
total_nulls += row.nulls_count;
|
||||
total_infinities += row.infinities_count;
|
||||
total_nans += row.nans_count;
|
||||
total_avg += row.avg * row.freq;
|
||||
total_count += row.freq;
|
||||
});
|
||||
if (total_count !== 0) {
|
||||
total_avg /= total_count;
|
||||
}
|
||||
|
||||
return {
|
||||
bin_width: firstRow.bin_width,
|
||||
bins_count: firstRow.bins_number,
|
||||
bins_start: this._populateBinStart(firstRow, override),
|
||||
nulls: firstRow.nulls_count,
|
||||
infinities: firstRow.infinities_count,
|
||||
nans: firstRow.nans_count,
|
||||
avg: firstRow.avg_val,
|
||||
nulls: total_nulls,
|
||||
infinities: total_infinities,
|
||||
nans: total_nans,
|
||||
avg: total_avg
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,55 +1,38 @@
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../formula');
|
||||
var debug = require('debug')('windshaft:widget:formula:overview');
|
||||
const utils = require('../../../utils/query-utils');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var formulaQueryTpls = {
|
||||
'count': dot.template([
|
||||
'SELECT',
|
||||
'sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'{{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_infinities',
|
||||
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nans',
|
||||
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
'sum': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'{{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_infinities',
|
||||
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count',
|
||||
',(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nans',
|
||||
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
|
||||
'FROM ({{=it._query}}) _cdb_formula',
|
||||
'{{?it._isFloatColumn}}WHERE',
|
||||
' {{=it._column}} != \'infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'-infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'NaN\'::float{{?}}'
|
||||
].join('\n')),
|
||||
'avg': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count)/sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'{{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_infinities',
|
||||
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count',
|
||||
',(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nans',
|
||||
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
|
||||
'FROM ({{=it._query}}) _cdb_formula',
|
||||
'{{?it._isFloatColumn}}WHERE',
|
||||
' {{=it._column}} != \'infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'-infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'NaN\'::float{{?}}'
|
||||
].join('\n')),
|
||||
const VALID_OPERATIONS = {
|
||||
count: true,
|
||||
sum: true,
|
||||
avg: true
|
||||
};
|
||||
|
||||
/** Formulae to calculate the end result using _feature_count from the overview table*/
|
||||
function dataviewResult(ctx) {
|
||||
switch (ctx.operation) {
|
||||
case 'count':
|
||||
return `sum(_feature_count)`;
|
||||
case 'sum':
|
||||
return `sum(${utils.handleFloatColumn(ctx)}*_feature_count)`;
|
||||
case 'avg':
|
||||
return `sum(${utils.handleFloatColumn(ctx)}*_feature_count)/sum(_feature_count) `;
|
||||
}
|
||||
return `${ctx.operation}(${utils.handleFloatColumn(ctx)})`;
|
||||
}
|
||||
|
||||
const formulaQueryTpl = ctx =>
|
||||
`SELECT
|
||||
${dataviewResult(ctx)} AS result,
|
||||
${utils.countNULLs(ctx)} AS nulls_count
|
||||
${ctx.isFloatColumn ? `,${utils.countInfinites(ctx)} AS infinities_count,` : ``}
|
||||
${ctx.isFloatColumn ? `${utils.countNaNs(ctx)} AS nans_count` : ``}
|
||||
FROM (${ctx.query}) __cdb_formula`;
|
||||
|
||||
function Formula(query, options, queryRewriter, queryRewriteData, params, queries) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params, queries);
|
||||
this.column = options.column || '1';
|
||||
@@ -65,36 +48,31 @@ module.exports = Formula;
|
||||
|
||||
Formula.prototype.sql = function (psql, override, callback) {
|
||||
var self = this;
|
||||
var formulaQueryTpl = formulaQueryTpls[this.operation];
|
||||
|
||||
if (formulaQueryTpl) {
|
||||
// supported formula for use with overviews
|
||||
if (this._isFloatColumn === null) {
|
||||
this._isFloatColumn = false;
|
||||
this.getColumnType(psql, this.column, this.queries.no_filters, function (err, type) {
|
||||
if (!err && !!type) {
|
||||
self._isFloatColumn = type.float;
|
||||
}
|
||||
self.sql(psql, override, callback);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
var formulaSql = formulaQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: this.rewrittenQuery(this.query),
|
||||
_operation: this.operation,
|
||||
_column: this.column
|
||||
});
|
||||
|
||||
callback = callback || override;
|
||||
|
||||
debug(formulaSql);
|
||||
|
||||
return callback(null, formulaSql);
|
||||
if (!VALID_OPERATIONS[this.operation]) {
|
||||
return this.defaultSql(psql, override, callback);
|
||||
}
|
||||
|
||||
if (this._isFloatColumn === null) {
|
||||
this._isFloatColumn = false;
|
||||
this.getColumnType(psql, this.column, this.queries.no_filters, function (err, type) {
|
||||
if (!err && !!type) {
|
||||
self._isFloatColumn = type.float;
|
||||
}
|
||||
self.sql(psql, override, callback);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// default behaviour
|
||||
return this.defaultSql(psql, override, callback);
|
||||
var formulaSql = formulaQueryTpl({
|
||||
isFloatColumn: this._isFloatColumn,
|
||||
query: this.rewrittenQuery(this.query),
|
||||
operation: this.operation,
|
||||
column: this.column
|
||||
});
|
||||
|
||||
callback = callback || override;
|
||||
|
||||
debug(formulaSql);
|
||||
|
||||
return callback(null, formulaSql);
|
||||
};
|
||||
|
||||
@@ -4,8 +4,6 @@ var RedisPool = require('redis-mpool');
|
||||
var cartodbRedis = require('cartodb-redis');
|
||||
var _ = require('underscore');
|
||||
|
||||
var lzmaMiddleware = require('./middleware/lzma');
|
||||
|
||||
var controller = require('./controllers');
|
||||
|
||||
var SurrogateKeysCache = require('./cache/surrogate_keys_cache');
|
||||
@@ -14,7 +12,8 @@ var VarnishHttpCacheBackend = require('./cache/backend/varnish_http');
|
||||
var FastlyCacheBackend = require('./cache/backend/fastly');
|
||||
|
||||
var StatsClient = require('./stats/client');
|
||||
var Profiler = require('./stats/profiler_proxy');
|
||||
const stats = require('./middleware/stats');
|
||||
|
||||
var RendererStatsReporter = require('./stats/reporter/renderer');
|
||||
|
||||
var windshaft = require('windshaft');
|
||||
@@ -46,6 +45,11 @@ var MapConfigAdapter = require('./models/mapconfig/adapter');
|
||||
|
||||
var StatsBackend = require('./backends/stats');
|
||||
|
||||
const lzmaMiddleware = require('./middleware/lzma');
|
||||
const errorMiddleware = require('./middleware/error-middleware');
|
||||
|
||||
const prepareContextMiddleware = require('./middleware/context');
|
||||
|
||||
module.exports = function(serverOptions) {
|
||||
// Make stats client globally accessible
|
||||
global.statsClient = StatsClient.getInstance(serverOptions.statsd);
|
||||
@@ -154,7 +158,8 @@ module.exports = function(serverOptions) {
|
||||
grainstore: serverOptions.grainstore,
|
||||
mapnik: serverOptions.renderer.mapnik
|
||||
},
|
||||
http: serverOptions.renderer.http
|
||||
http: serverOptions.renderer.http,
|
||||
mvt: serverOptions.renderer.mvt
|
||||
});
|
||||
|
||||
// initialize render cache
|
||||
@@ -208,12 +213,16 @@ module.exports = function(serverOptions) {
|
||||
|
||||
var versions = getAndValidateVersions(serverOptions);
|
||||
|
||||
const prepareContext = typeof serverOptions.req2params === 'function' ?
|
||||
serverOptions.req2params :
|
||||
prepareContextMiddleware(authApi, pgConnection);
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
new controller.Layergroup(
|
||||
authApi,
|
||||
prepareContext,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
tileBackend,
|
||||
@@ -226,7 +235,7 @@ module.exports = function(serverOptions) {
|
||||
).register(app);
|
||||
|
||||
new controller.Map(
|
||||
authApi,
|
||||
prepareContext,
|
||||
pgConnection,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
@@ -239,8 +248,7 @@ module.exports = function(serverOptions) {
|
||||
).register(app);
|
||||
|
||||
new controller.NamedMaps(
|
||||
authApi,
|
||||
pgConnection,
|
||||
prepareContext,
|
||||
namedMapProviderCache,
|
||||
tileBackend,
|
||||
previewBackend,
|
||||
@@ -249,9 +257,9 @@ module.exports = function(serverOptions) {
|
||||
metadataBackend
|
||||
).register(app);
|
||||
|
||||
new controller.NamedMapsAdmin(authApi, pgConnection, templateMaps).register(app);
|
||||
new controller.NamedMapsAdmin(authApi, templateMaps).register(app);
|
||||
|
||||
new controller.Analyses(authApi, pgConnection).register(app);
|
||||
new controller.Analyses(prepareContext).register(app);
|
||||
|
||||
new controller.ServerInfo(versions).register(app);
|
||||
|
||||
@@ -259,6 +267,8 @@ module.exports = function(serverOptions) {
|
||||
* END Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
app.use(errorMiddleware());
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
@@ -353,12 +363,6 @@ function bootstrap(opts) {
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.use(function bootstrap$prepareRequestResponse(req, res, next) {
|
||||
req.context = req.context || {};
|
||||
req.profiler = new Profiler({
|
||||
statsd_client: global.statsClient,
|
||||
profile: opts.useProfiler
|
||||
});
|
||||
|
||||
if (global.environment && global.environment.api_hostname) {
|
||||
res.set('X-Served-By-Host', global.environment.api_hostname);
|
||||
}
|
||||
@@ -366,6 +370,11 @@ function bootstrap(opts) {
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(stats({
|
||||
enabled: opts.useProfiler,
|
||||
statsClient: global.statsClient
|
||||
}));
|
||||
|
||||
app.use(lzmaMiddleware);
|
||||
|
||||
// temporary measure until we upgrade to newer version expressjs so we can check err.status
|
||||
|
||||
@@ -81,6 +81,7 @@ module.exports = {
|
||||
statsInterval: rendererConfig.statsInterval
|
||||
},
|
||||
renderer: {
|
||||
mvt: rendererConfig.mvt,
|
||||
mapnik: _.defaults(rendererConfig.mapnik, {
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
|
||||
@@ -22,5 +22,36 @@ module.exports.extractTableNames = function extractTableNames(query) {
|
||||
};
|
||||
|
||||
module.exports.getQueryRowCount = function getQueryRowEstimation(query) {
|
||||
return 'select CDB_EstimateRowCount(\'' + query + '\') as rows';
|
||||
return 'select CDB_EstimateRowCount($$' + query + '$$) as rows';
|
||||
};
|
||||
|
||||
/** Cast the column to epoch */
|
||||
module.exports.columnCastTpl = function columnCastTpl(ctx) {
|
||||
return `date_part('epoch', ${ctx.column})`;
|
||||
};
|
||||
|
||||
/** If the column type is float, ignore any non numeric result (infinity / NaN) */
|
||||
module.exports.handleFloatColumn = function handleFloatColumn(ctx) {
|
||||
return `${!ctx.isFloatColumn ? `${ctx.column}` :
|
||||
`nullif(nullif(nullif(${ctx.column}, 'infinity'::float), '-infinity'::float), 'NaN'::float)`
|
||||
}`;
|
||||
};
|
||||
|
||||
/** Count NULL appearances */
|
||||
module.exports.countNULLs= function countNULLs(ctx) {
|
||||
return `sum(CASE WHEN (${ctx.column} IS NULL) THEN 1 ELSE 0 END)`;
|
||||
};
|
||||
|
||||
/** Count only infinity (positive and negative) appearances */
|
||||
module.exports.countInfinites = function countInfinites(ctx) {
|
||||
return `${!ctx.isFloatColumn ? `0` :
|
||||
`sum(CASE WHEN (${ctx.column} = 'infinity'::float OR ${ctx.column} = '-infinity'::float) THEN 1 ELSE 0 END)`
|
||||
}`;
|
||||
};
|
||||
|
||||
/** Count only NaNs appearances*/
|
||||
module.exports.countNaNs = function countNaNs(ctx) {
|
||||
return `${!ctx.isFloatColumn ? `0` :
|
||||
`sum(CASE WHEN (${ctx.column} = 'NaN'::float) THEN 1 ELSE 0 END)`
|
||||
}`;
|
||||
};
|
||||
17
package.json
17
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "4.0.0",
|
||||
"version": "4.3.1",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
@@ -23,7 +23,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"body-parser": "^1.18.2",
|
||||
"camshaft": "0.59.2",
|
||||
"camshaft": "0.59.4",
|
||||
"cartodb-psql": "0.10.2",
|
||||
"cartodb-query-tables": "0.3.0",
|
||||
"cartodb-redis": "0.14.0",
|
||||
@@ -35,15 +35,16 @@
|
||||
"lru-cache": "2.6.5",
|
||||
"lzma": "~2.3.2",
|
||||
"node-statsd": "~0.0.7",
|
||||
"on-headers": "^1.0.1",
|
||||
"queue-async": "~1.0.7",
|
||||
"redis-mpool": "0.4.1",
|
||||
"request": "^2.83.0",
|
||||
"semver": "~5.3.0",
|
||||
"step": "~0.0.6",
|
||||
"step-profiler": "~0.3.0",
|
||||
"turbo-carto": "0.20.1",
|
||||
"turbo-carto": "0.20.2",
|
||||
"underscore": "~1.6.0",
|
||||
"windshaft": "3.3.3",
|
||||
"windshaft": "4.1.0",
|
||||
"yargs": "~5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -58,7 +59,13 @@
|
||||
"scripts": {
|
||||
"lint": "jshint lib test",
|
||||
"preinstall": "make pre-install",
|
||||
"test": "make test-all"
|
||||
"test": "make test-all",
|
||||
"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-bash": "docker run -it -v `pwd`:/srv cartoimages/windshaft-testing bash",
|
||||
"docker-publish": "docker push cartoimages/windshaft-carto-testing"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9",
|
||||
|
||||
@@ -137,7 +137,7 @@ if test x"$OPT_COVERAGE" = xyes; then
|
||||
./node_modules/.bin/istanbul cover node_modules/.bin/_mocha -- -u tdd -t 5000 ${TESTS}
|
||||
else
|
||||
echo "Running tests"
|
||||
mocha -u tdd -t 5000 ${TESTS}
|
||||
./node_modules/.bin/_mocha -c -u tdd -t 5000 ${TESTS}
|
||||
fi
|
||||
ret=$?
|
||||
|
||||
|
||||
37
scripts/mvt-timeout-error.py
Executable file
37
scripts/mvt-timeout-error.py
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import mapbox_vector_tile
|
||||
|
||||
lines_list = []
|
||||
|
||||
# main diagonal line
|
||||
lines_list.append({ "geometry":"LINESTRING (0 0, 4096 4096)"})
|
||||
|
||||
# diagonal lines
|
||||
for i in range(4096/32, 4096, 4096/32):
|
||||
start = i
|
||||
end = 4096 - i
|
||||
|
||||
lines_list.append({ "geometry":"LINESTRING (0 " + str(start) + ", " + str(end) + " 4096)" })
|
||||
lines_list.append({ "geometry":"LINESTRING (" + str(start) + " 0, 4096 " + str(end) + ")" })
|
||||
|
||||
# box lines
|
||||
lines_list.append({ "geometry":"LINESTRING (0 0, 0 4096)"})
|
||||
lines_list.append({ "geometry":"LINESTRING (0 4096, 4096 4096)"})
|
||||
lines_list.append({ "geometry":"LINESTRING (4096 4096, 4096 0)"})
|
||||
lines_list.append({ "geometry":"LINESTRING (4096 0, 0 0)"})
|
||||
|
||||
|
||||
tile = mapbox_vector_tile.encode([
|
||||
{
|
||||
"name": "errorTileSquareLayer",
|
||||
"features": [{ "geometry":"POLYGON ((0 0, 0 4096, 4096 4096, 4096 0, 0 0))" }]
|
||||
},
|
||||
{
|
||||
"name": "errorTileStripesLayer",
|
||||
"features": lines_list
|
||||
}
|
||||
])
|
||||
|
||||
with open('./assets/render-timeout-fallback.mvt', 'w+') as f:
|
||||
f.write(tile)
|
||||
114
test/acceptance/analysis/analyses-controller.js
Normal file
114
test/acceptance/analysis/analyses-controller.js
Normal file
@@ -0,0 +1,114 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
|
||||
describe('analyses controller', function () {
|
||||
const mapConfig = {
|
||||
version: '1.5.0',
|
||||
layers:
|
||||
[{
|
||||
type: 'cartodb',
|
||||
options:
|
||||
{
|
||||
source: { id: 'a1' },
|
||||
cartocss: TestClient.CARTOCSS.POLYGONS,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}],
|
||||
dataviews: {},
|
||||
analyses:
|
||||
[{
|
||||
id: 'a1',
|
||||
type: 'buffer',
|
||||
params: {
|
||||
source: {
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from analysis_banks limit 1'
|
||||
}
|
||||
},
|
||||
radius: 250
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
});
|
||||
|
||||
it('should get an array of analyses from catalog', function (done) {
|
||||
this.testClient.getAnalysesCatalog({}, (err, result) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.ok(Array.isArray(result.catalog));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support jsonp responses', function (done) {
|
||||
this.testClient.getAnalysesCatalog({ jsonp: 'jsonp_test' }, (err, result) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.ok(result);
|
||||
|
||||
let didRunJsonCallback = false;
|
||||
// jshint ignore:start
|
||||
function jsonp_test(body) {
|
||||
assert.ok(Array.isArray(body.catalog));
|
||||
didRunJsonCallback = true;
|
||||
}
|
||||
|
||||
eval(result);
|
||||
// jshint ignore:end
|
||||
|
||||
assert.ok(didRunJsonCallback);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond "unauthorized" when missing api_key', function (done) {
|
||||
const apiKey = this.testClient.apiKey;
|
||||
this.testClient.apiKey = null;
|
||||
|
||||
this.testClient.getAnalysesCatalog({ status: 401 }, (err, result) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.deepEqual(result.errors[0], 'Unauthorized');
|
||||
this.testClient.apiKey = apiKey;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get an array of analyses from catalog', function (done) {
|
||||
this.testClient.getTile(0, 0, 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.testClient.getAnalysesCatalog({}, (err, result) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.ok(Array.isArray(result.catalog));
|
||||
assert.ok(result.catalog.length >= 2); // buffer & source at least
|
||||
|
||||
result.catalog
|
||||
.filter(analysis => analysis.node_id === '0a215e1f3405381cf0ea6b3b0deb6fdcfdc2fcaa')
|
||||
.forEach(analysis => assert.equal(analysis.type, 'buffer'));
|
||||
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,81 +0,0 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('analysis-layers-dataviews-geojson', function() {
|
||||
|
||||
function createMapConfig(layers, dataviews, analysis) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: layers,
|
||||
dataviews: dataviews || {},
|
||||
analyses: analysis || []
|
||||
};
|
||||
}
|
||||
|
||||
var 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;",
|
||||
"}"
|
||||
].join('\n');
|
||||
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745"
|
||||
},
|
||||
"cartocss": CARTOCSS,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
pop_max_histogram: {
|
||||
source: {
|
||||
id: '2570e105-7b37-40d2-bdf4-1af889598745'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
it('should get pop_max column from dataview', function(done) {
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getTile(0, 0, 0, {format: 'geojson', layers: 0}, function(err, res, geojson) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(Array.isArray(geojson.features));
|
||||
assert.ok(geojson.features.length > 0);
|
||||
var feature = geojson.features[0];
|
||||
assert.ok(feature.properties.hasOwnProperty('pop_max'), 'Missing pop_max property');
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -7,7 +7,7 @@ var serverOptions = require('../../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('named-maps analysis', function() {
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ require('../support/test_helper');
|
||||
var fs = require('fs');
|
||||
var assert = require('../support/assert');
|
||||
var TestClient = require('../support/test-client');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var IMAGE_TOLERANCE_PER_MIL = 5;
|
||||
|
||||
@@ -124,24 +125,37 @@ describe('buffer size per format', function () {
|
||||
}
|
||||
];
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
return this.testClient.drain(done);
|
||||
}
|
||||
return done();
|
||||
});
|
||||
|
||||
const originalUsePostGIS = serverOptions.renderer.mvt.usePostGIS;
|
||||
testCases.forEach(function (test) {
|
||||
it(test.desc, function (done) {
|
||||
var testClient = new TestClient(test.mapConfig, 1234);
|
||||
var coords = test.coords;
|
||||
var options = {
|
||||
format: test.format,
|
||||
layers: test.layers
|
||||
};
|
||||
testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
// To generate images use:
|
||||
// tile.save(test.fixturePath);
|
||||
test.assert(tile, function (err) {
|
||||
var testFn = (usePostGIS) => {
|
||||
it(test.desc, function (done) {
|
||||
serverOptions.renderer.mvt.usePostGIS = usePostGIS;
|
||||
this.testClient = new TestClient(test.mapConfig, 1234);
|
||||
serverOptions.renderer.mvt.usePostGIS = originalUsePostGIS;
|
||||
var coords = test.coords;
|
||||
var options = {
|
||||
format: test.format,
|
||||
layers: test.layers
|
||||
};
|
||||
this.testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
testClient.drain(done);
|
||||
// To generate images use:
|
||||
// tile.save(test.fixturePath);
|
||||
test.assert(tile, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
if (process.env.POSTGIS_VERSION === '2.4' && test.format === 'mvt'){
|
||||
testFn(true);
|
||||
}
|
||||
testFn(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -260,23 +274,27 @@ describe('buffer size per format for named maps', function () {
|
||||
}
|
||||
];
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
return this.testClient.drain(done);
|
||||
}
|
||||
return done();
|
||||
});
|
||||
|
||||
testCases.forEach(function (test) {
|
||||
it(test.desc, function (done) {
|
||||
var testClient = new TestClient(test.template, 1234);
|
||||
this.testClient = new TestClient(test.template, 1234);
|
||||
var coords = test.coords;
|
||||
var options = {
|
||||
format: test.format,
|
||||
placeholders: test.placeholders,
|
||||
layers: test.layers
|
||||
};
|
||||
testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
|
||||
this.testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
// To generate images use:
|
||||
//tile.save('./test/fixtures/buffer-size/tile-7.64.48-buffer-size-0-test.png');
|
||||
test.assert(tile, function (err) {
|
||||
assert.ifError(err);
|
||||
testClient.drain(done);
|
||||
});
|
||||
test.assert(tile, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -416,26 +434,40 @@ describe('buffer size per format for named maps w/o placeholders', function () {
|
||||
|
||||
];
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
return this.testClient.drain(done);
|
||||
}
|
||||
return done();
|
||||
});
|
||||
|
||||
const originalUsePostGIS = serverOptions.renderer.mvt.usePostGIS;
|
||||
testCases.forEach(function (test) {
|
||||
it(test.desc, function (done) {
|
||||
var testClient = new TestClient(test.template, 1234);
|
||||
var coords = test.coords;
|
||||
var options = {
|
||||
format: test.format,
|
||||
placeholders: test.placeholders,
|
||||
layers: test.layers
|
||||
};
|
||||
testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
// To generate images use:
|
||||
//tile.save(test.fixturePath);
|
||||
// require('fs').writeFileSync(test.fixturePath, JSON.stringify(tile));
|
||||
// require('fs').writeFileSync(test.fixturePath, tile.getDataSync());
|
||||
test.assert(tile, function (err) {
|
||||
assert.ifError(err);
|
||||
testClient.drain(done);
|
||||
var testFn = (usePostGIS) => {
|
||||
it(test.desc + `(${usePostGIS? 'PostGIS':'mapnik'})`, function (done) {
|
||||
serverOptions.renderer.mvt.usePostGIS = usePostGIS;
|
||||
test.template.name += '_1';
|
||||
this.testClient = new TestClient(test.template, 1234);
|
||||
serverOptions.renderer.mvt.usePostGIS = originalUsePostGIS;
|
||||
var coords = test.coords;
|
||||
var options = {
|
||||
format: test.format,
|
||||
placeholders: test.placeholders,
|
||||
layers: test.layers
|
||||
};
|
||||
this.testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
// To generate images use:
|
||||
//tile.save(test.fixturePath);
|
||||
// require('fs').writeFileSync(test.fixturePath, JSON.stringify(tile));
|
||||
// require('fs').writeFileSync(test.fixturePath, tile.getDataSync());
|
||||
test.assert(tile, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
if (process.env.POSTGIS_VERSION === '2.4' && test.format === 'mvt'){
|
||||
testFn(true);
|
||||
}
|
||||
testFn(false);
|
||||
});
|
||||
});
|
||||
|
||||
2
test/acceptance/cache/cache_headers.js
vendored
2
test/acceptance/cache/cache_headers.js
vendored
@@ -8,7 +8,7 @@ var serverOptions = require('../../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('get requests with cache headers', function() {
|
||||
|
||||
|
||||
@@ -324,3 +324,104 @@ describe('aggregation-dataview: special float values', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregation dataview tuned by categories query param', function () {
|
||||
const mapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: "cartodb",
|
||||
options: {
|
||||
source: {
|
||||
"id": "a0"
|
||||
},
|
||||
cartocss: "#points { marker-width: 10; marker-fill: red; }",
|
||||
cartocss_version: "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
categories: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'cat',
|
||||
aggregation: 'sum',
|
||||
aggregationColumn: 'val'
|
||||
}
|
||||
}
|
||||
},
|
||||
analyses: [
|
||||
{
|
||||
id: "a0",
|
||||
type: "source",
|
||||
params: {
|
||||
query: `
|
||||
SELECT
|
||||
null::geometry the_geom_webmercator,
|
||||
CASE
|
||||
WHEN x % 4 = 0 THEN 1
|
||||
WHEN x % 4 = 1 THEN 2
|
||||
WHEN x % 4 = 2 THEN 3
|
||||
ELSE 4
|
||||
END AS val,
|
||||
CASE
|
||||
WHEN x % 4 = 0 THEN 'category_1'
|
||||
WHEN x % 4 = 1 THEN 'category_2'
|
||||
WHEN x % 4 = 2 THEN 'category_3'
|
||||
ELSE 'category_4'
|
||||
END AS cat
|
||||
FROM generate_series(1, 1000) x
|
||||
`
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
var scenarios = [
|
||||
{
|
||||
params: { own_filter: 0, categories: -1 },
|
||||
categoriesExpected: 4
|
||||
},
|
||||
{
|
||||
params: { own_filter: 0, categories: 0 },
|
||||
categoriesExpected: 4
|
||||
},
|
||||
{
|
||||
params: { own_filter: 0, categories: 1 },
|
||||
categoriesExpected: 1
|
||||
},
|
||||
{
|
||||
params: { own_filter: 0, categories: 2 },
|
||||
categoriesExpected: 2
|
||||
},
|
||||
{
|
||||
params: { own_filter: 0, categories: 4 },
|
||||
categoriesExpected: 4
|
||||
},
|
||||
{
|
||||
params: { own_filter: 0, categories: 5 },
|
||||
categoriesExpected: 4
|
||||
}
|
||||
];
|
||||
|
||||
scenarios.forEach(function (scenario) {
|
||||
it(`should handle cartegories to customize aggregations: ${JSON.stringify(scenario.params)}`, function (done) {
|
||||
this.testClient.getDataview('categories', scenario.params, (err, dataview) => {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.categories.length, scenario.categoriesExpected);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -186,7 +186,7 @@ describe('histogram-dataview for date column type', function() {
|
||||
},
|
||||
minute_histogram: {
|
||||
source: {
|
||||
id: 'minute-histogram-source'
|
||||
id: 'minute-histogram-source-tz'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
@@ -214,8 +214,8 @@ describe('histogram-dataview for date column type', function() {
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 01:00:00'::timestamptz, '2008-04-09 01:00:00'::timestamptz, '1 day'::interval",
|
||||
"from generate_series('2007-02-15 01:00:00+00'::timestamptz,",
|
||||
"'2008-04-09 01:00:00+00'::timestamptz, '1 day'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
@@ -233,13 +233,13 @@ describe('histogram-dataview for date column type', function() {
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "minute-histogram-source",
|
||||
"id": "minute-histogram-source-tz",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
"select null::geometry the_geom_webmercator, date AS d",
|
||||
"from generate_series(",
|
||||
"'2007-02-15 23:50:00'::timestamp, '2007-02-16 00:10:00'::timestamp, '1 minute'::interval",
|
||||
"from generate_series('2007-02-15 23:50:00+00'::timestamptz,",
|
||||
"'2007-02-16 00:10:00+00'::timestamptz, '1 minute'::interval",
|
||||
") date"
|
||||
].join(' ')
|
||||
}
|
||||
@@ -256,6 +256,7 @@ describe('histogram-dataview for date column type', function() {
|
||||
}];
|
||||
|
||||
dateHistogramsUseCases.forEach(function (test) {
|
||||
|
||||
it('should create a date histogram aggregated in months (EDT) ' + test.desc, function (done) {
|
||||
var OFFSET_EDT_IN_MINUTES = -4 * 60; // EDT Eastern Daylight Time (GMT-4) in minutes
|
||||
|
||||
@@ -323,7 +324,7 @@ describe('histogram-dataview for date column type', function() {
|
||||
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, 6);
|
||||
assert.equal(dataview.bins_count, 6);
|
||||
dataview.bins.forEach(function (bin) {
|
||||
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
|
||||
});
|
||||
@@ -335,7 +336,7 @@ describe('histogram-dataview for date column type', function() {
|
||||
it('should cast overridden start and end to float to avoid out of range errors ' + test.desc, function (done) {
|
||||
var params = {
|
||||
start: -2145916800,
|
||||
end: 1009843199
|
||||
end: 1193792400
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
@@ -348,27 +349,6 @@ describe('histogram-dataview for date column type', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return same histogram ' + test.desc, function (done) {
|
||||
var params = {
|
||||
start: 1171501200, // 2007-02-15 01:00:00 = min(date_colum)
|
||||
end: 1207702800 // 2008-04-09 01:00:00 = max(date_colum)
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview(test.dataviewId, {}, function (err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview(test.dataviewId, params, function (err, filteredDataview) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.deepEqual(dataview, filteredDataview);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should aggregate histogram overriding default offset to CEST ' + test.desc, function (done) {
|
||||
var OFFSET_CEST_IN_SECONDS = 2 * 3600; // Central European Summer Time (Daylight Saving Time)
|
||||
var OFFSET_CEST_IN_MINUTES = 2 * 60; // Central European Summer Time (Daylight Saving Time)
|
||||
@@ -533,6 +513,26 @@ describe('histogram-dataview for date column type', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return same histogram ', function (done) {
|
||||
var params = {
|
||||
start: 1171501200, // 2007-02-15 01:00:00 = min(date_colum)
|
||||
end: 1207702800 // 2008-04-09 01:00:00 = max(date_colum)
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_tz', {}, function (err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('datetime_histogram_tz', params, function (err, filteredDataview) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.deepEqual(dataview, filteredDataview);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best aggregation (automatic mode) to build the histogram', function (done) {
|
||||
var params = {};
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
@@ -640,7 +640,7 @@ describe('histogram-dataview for date column type', function() {
|
||||
|
||||
var dataviewWithDailyAggFixture = {
|
||||
aggregation: 'day',
|
||||
bin_width: 600,
|
||||
bin_width: 86400,
|
||||
bins_count: 2,
|
||||
bins_start: 1171497600,
|
||||
timestamp_start: 1171497600,
|
||||
@@ -650,17 +650,17 @@ describe('histogram-dataview for date column type', function() {
|
||||
[{
|
||||
bin: 0,
|
||||
timestamp: 1171497600,
|
||||
min: 1171583400,
|
||||
max: 1171583940,
|
||||
avg: 1171583670,
|
||||
min: 1171497600,
|
||||
max: 1171497600,
|
||||
avg: 1171497600,
|
||||
freq: 10
|
||||
},
|
||||
{
|
||||
bin: 1,
|
||||
timestamp: 1171584000,
|
||||
min: 1171584000,
|
||||
max: 1171584600,
|
||||
avg: 1171584300,
|
||||
max: 1171584000,
|
||||
avg: 1171584000,
|
||||
freq: 11
|
||||
}],
|
||||
type: 'histogram'
|
||||
@@ -687,19 +687,19 @@ describe('histogram-dataview for date column type', function() {
|
||||
|
||||
var dataviewWithDailyAggAndOffsetFixture = {
|
||||
aggregation: 'day',
|
||||
bin_width: 1200,
|
||||
bin_width: 86400,
|
||||
bins_count: 1,
|
||||
bins_start: 1171501200,
|
||||
timestamp_start: 1171497600,
|
||||
timestamp_start: 1171501200,
|
||||
nulls: 0,
|
||||
offset: -3600,
|
||||
bins:
|
||||
[{
|
||||
bin: 0,
|
||||
timestamp: 1171501200,
|
||||
min: 1171583400,
|
||||
max: 1171584600,
|
||||
avg: 1171584000,
|
||||
min: 1171501200,
|
||||
max: 1171501200,
|
||||
avg: 1171501200,
|
||||
freq: 21
|
||||
}],
|
||||
type: 'histogram'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var assert = require('../support/assert');
|
||||
var step = require('step');
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
|
||||
var testHelper = require(__dirname + '/../support/test_helper');
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var TestClient = require('../support/test-client');
|
||||
|
||||
|
||||
describe('use only needed columns', function() {
|
||||
|
||||
function getFeatureByCartodbId(features, cartodbId) {
|
||||
for (var i = 0, len = features.length; i < len; i++) {
|
||||
if (features[i].properties.cartodb_id === cartodbId) {
|
||||
return features[i];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
var options = { format: 'geojson', layer: 0 };
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('with aggregation widget, interactivity and cartocss columns', function(done) {
|
||||
var widgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: '#layer0 { marker-fill: red; marker-width: 10; [name="Madrid"] { marker-fill: green; } }',
|
||||
cartocss_version: '2.0.1',
|
||||
widgets: {
|
||||
adm0name: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'adm0name',
|
||||
aggregation: 'sum',
|
||||
aggregationColumn: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
interactivity: "cartodb_id,pop_min"
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(widgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, geojsonTile) {
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
name: 'Mardin',
|
||||
adm0name: 'Turkey',
|
||||
pop_max: 71373,
|
||||
pop_min: 57586
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not duplicate columns', function(done) {
|
||||
var widgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: ['#layer0 {',
|
||||
'marker-fill: red;',
|
||||
'marker-width: 10;',
|
||||
'[name="Madrid"] { marker-fill: green; } ',
|
||||
'[pop_max>100000] { marker-fill: black; } ',
|
||||
'}'].join('\n'),
|
||||
cartocss_version: '2.3.0',
|
||||
widgets: {
|
||||
adm0name: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'adm0name',
|
||||
aggregation: 'sum',
|
||||
aggregationColumn: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
interactivity: "cartodb_id,pop_max"
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(widgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, geojsonTile) {
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
name: 'Mardin',
|
||||
adm0name: 'Turkey',
|
||||
pop_max: 71373
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('with formula widget, no interactivity and no cartocss columns', function(done) {
|
||||
var formulaWidgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced where pop_max > 0 and pop_max < 600000',
|
||||
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'cartodb_id',
|
||||
widgets: {
|
||||
pop_max_f: {
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: 'pop_max',
|
||||
operation: 'count'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(formulaWidgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, geojsonTile) {
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
pop_max: 71373
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('with cartocss with multiple expressions', function(done) {
|
||||
var formulaWidgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced where pop_max > 0 and pop_max < 600000',
|
||||
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }' +
|
||||
'#layer0 { marker-width: 14; [name="Madrid"] { marker-width: 20; } }' +
|
||||
'#layer0[pop_max>1000] { marker-width: 14; [name="Madrid"] { marker-width: 20; } }' +
|
||||
'#layer0[adm0name=~".*Turkey*"] { marker-width: 14; [name="Madrid"] { marker-width: 20; } }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'cartodb_id'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(formulaWidgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, geojsonTile) {
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
pop_max:71373,
|
||||
name:"Mardin",
|
||||
adm0name:"Turkey"
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with mapnik substitution tokens', function(done) {
|
||||
var cartocss = [
|
||||
"#layer {",
|
||||
" line-width: 2;",
|
||||
" line-color: #3B3B58;",
|
||||
" line-opacity: 1;",
|
||||
" polygon-opacity: 0.7;",
|
||||
" polygon-fill: ramp([points_count], (#E5F5F9,#99D8C9,#2CA25F))",
|
||||
"}"
|
||||
].join('\n');
|
||||
|
||||
var sql = [
|
||||
'WITH hgrid AS (',
|
||||
' SELECT CDB_HexagonGrid(',
|
||||
' ST_Expand(!bbox!, greatest(!pixel_width!,!pixel_height!) * 100),',
|
||||
' greatest(!pixel_width!,!pixel_height!) * 100',
|
||||
' ) as cell',
|
||||
')',
|
||||
'SELECT',
|
||||
' hgrid.cell as the_geom_webmercator,',
|
||||
' count(1) as points_count,',
|
||||
' count(1)/power(100 * CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!)), 2) as points_density,',
|
||||
' 1 as cartodb_id',
|
||||
'FROM hgrid, (SELECT * FROM populated_places_simple_reduced) i',
|
||||
'where ST_Intersects(i.the_geom_webmercator, hgrid.cell)',
|
||||
'GROUP BY hgrid.cell'
|
||||
].join('\n');
|
||||
|
||||
var mapConfig = {
|
||||
"version": "1.4.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": 'mapnik',
|
||||
"options": {
|
||||
"cartocss_version": '2.3.0',
|
||||
"sql": sql,
|
||||
"cartocss": cartocss
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
this.testClient.getTile(0, 0, 0, { format: 'geojson', layer: 0 }, function(err, res, geojson) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(geojson);
|
||||
assert.equal(geojson.features.length, 5);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip empty and null columns for geojson tiles', function(done) {
|
||||
|
||||
var mapConfig = {
|
||||
"analyses": [
|
||||
{
|
||||
"id": "a0",
|
||||
"params": {
|
||||
"query": "SELECT * FROM test_table"
|
||||
},
|
||||
"type": "source"
|
||||
}
|
||||
],
|
||||
"dataviews": {
|
||||
"4e7b0e07-6d21-4b83-9adb-6d7e17eea6ca": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"column": "cartodb_id",
|
||||
"operation": "avg"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "formula"
|
||||
},
|
||||
"74f590f8-625c-4e95-922f-34ad3e9919c0": {
|
||||
"options": {
|
||||
"aggregation": "sum",
|
||||
"aggregationColumn": "cartodb_id",
|
||||
"column": "name"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "aggregation"
|
||||
},
|
||||
"98a75757-3006-400a-b028-fb613a6c0b69": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"column": "cartodb_id",
|
||||
"operation": "sum"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "formula"
|
||||
},
|
||||
"ebbc97b2-87d2-4895-9e1f-2f012df3679d": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"bins": "12",
|
||||
"column": "cartodb_id"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "histogram"
|
||||
},
|
||||
"ebc0653f-3581-469c-8b31-c969e440a865": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"column": "cartodb_id",
|
||||
"operation": "avg"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "formula"
|
||||
}
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"options": {
|
||||
"subdomains": "abcd",
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png"
|
||||
},
|
||||
"type": "http"
|
||||
},
|
||||
{
|
||||
"options": {
|
||||
"attributes": {
|
||||
"columns": [
|
||||
"name",
|
||||
"address"
|
||||
],
|
||||
"id": "cartodb_id"
|
||||
},
|
||||
"cartocss": "#layer { marker-width: 10; marker-fill: red; }",
|
||||
"cartocss_version": "2.3.0",
|
||||
"interactivity": "cartodb_id",
|
||||
"layer_name": "wadus",
|
||||
"source": {
|
||||
"id": "a0"
|
||||
}
|
||||
},
|
||||
"type": "cartodb"
|
||||
},
|
||||
{
|
||||
"options": {
|
||||
"subdomains": "abcd",
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png"
|
||||
},
|
||||
"type": "http"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
this.testClient.getTile(0, 0, 0, { format: 'geojson', layer: 0 }, function(err, res, geojson) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(geojson);
|
||||
assert.equal(geojson.features.length, 5);
|
||||
|
||||
assert.deepEqual(Object.keys(geojson.features[0].properties), ['cartodb_id', 'name']);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -9,7 +9,7 @@ var mapnik = require('windshaft').mapnik;
|
||||
var semver = require('semver');
|
||||
|
||||
var helper = require(__dirname + '/../support/test_helper');
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
var windshaft_fixtures = __dirname + '/../../node_modules/windshaft/test/fixtures';
|
||||
|
||||
@@ -1163,8 +1163,12 @@ describe(suiteName, function() {
|
||||
);
|
||||
});
|
||||
|
||||
// WARN: MapConfig with mapnik layer and no cartocss it's valid since
|
||||
// vector & raster aggregation project, now we can request MVT format w/o defining styles
|
||||
// for the layer.
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/133
|
||||
it("MapConfig with mapnik layer and no cartocss", function(done) {
|
||||
it.skip("MapConfig with mapnik layer and no cartocss", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
|
||||
@@ -4,7 +4,7 @@ var assert = require('../support/assert');
|
||||
|
||||
var _ = require('underscore');
|
||||
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
var PgQueryRunner = require('../../lib/cartodb/backends/pg_query_runner');
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
const assert = require('../support/assert');
|
||||
const TestClient = require('../support/test-client');
|
||||
var assert = require('../support/assert');
|
||||
var TestClient = require('../support/test-client');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
|
||||
function createMapConfig (sql = TestClient.SQL.ONE_POINT) {
|
||||
function createMapConfig(sql = TestClient.SQL.ONE_POINT) {
|
||||
return {
|
||||
version: '1.6.0',
|
||||
layers: [{
|
||||
@@ -18,7 +19,100 @@ function createMapConfig (sql = TestClient.SQL.ONE_POINT) {
|
||||
};
|
||||
}
|
||||
|
||||
describe('mvt', function () {
|
||||
describe('mvt (mapnik)', mvt(false));
|
||||
if (process.env.POSTGIS_VERSION === '2.4') {
|
||||
describe('mvt (postgis)', mvt(true));
|
||||
}
|
||||
|
||||
function mvt(usePostGIS) {
|
||||
return function () {
|
||||
const originalUsePostGIS = serverOptions.renderer.mvt.usePostGIS;
|
||||
before(function () {
|
||||
serverOptions.renderer.mvt.usePostGIS = usePostGIS;
|
||||
});
|
||||
after(function (){
|
||||
serverOptions.renderer.mvt.usePostGIS = originalUsePostGIS;
|
||||
});
|
||||
|
||||
describe('analysis-layers-dataviews-mvt', function () {
|
||||
|
||||
function createMapConfig(layers, dataviews, analysis) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: layers,
|
||||
dataviews: dataviews || {},
|
||||
analyses: analysis || []
|
||||
};
|
||||
}
|
||||
|
||||
var 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;",
|
||||
"}"
|
||||
].join('\n');
|
||||
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745"
|
||||
},
|
||||
"cartocss": CARTOCSS,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
pop_max_histogram: {
|
||||
source: {
|
||||
id: '2570e105-7b37-40d2-bdf4-1af889598745'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
it('should get pop_max column from dataview', function (done) {
|
||||
var testClient = new TestClient(mapConfig);
|
||||
|
||||
testClient.getTile(0, 0, 0, { format: 'mvt', layers: 0 }, function (err, res, MVT) {
|
||||
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(Array.isArray(geojsonTile.features));
|
||||
assert.ok(geojsonTile.features.length > 0);
|
||||
var feature = geojsonTile.features[0];
|
||||
assert.ok(feature.properties.hasOwnProperty('pop_max'), 'Missing pop_max property');
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
desc: 'should get empty mvt with code 204 (no content)',
|
||||
@@ -48,16 +142,366 @@ describe('mvt', function () {
|
||||
|
||||
testCases.forEach(function (test) {
|
||||
it(test.desc, done => {
|
||||
const testClient = new TestClient(test.mapConfig, 1234);
|
||||
var testClient = new TestClient(test.mapConfig);
|
||||
const { z, x, y } = test.coords;
|
||||
const { format, response } = test;
|
||||
|
||||
testClient.getTile(z, x, y, { format, response }, (err, res) => {
|
||||
testClient.getTile(z, x, y, { format, response }, err => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.equal(res.statusCode, test.response.status);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
if (usePostGIS){
|
||||
describe('use only needed columns', onlyNeededColumns);
|
||||
}else{
|
||||
describe.skip('use only needed columns', onlyNeededColumns);
|
||||
}
|
||||
|
||||
function onlyNeededColumns() {
|
||||
function getFeatureByCartodbId(features, cartodbId) {
|
||||
for (var i = 0, len = features.length; i < len; i++) {
|
||||
if (features[i].properties.cartodb_id === cartodbId) {
|
||||
return features[i];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
var options = { format: 'mvt', layer: 0 };
|
||||
|
||||
afterEach(function (done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('with aggregation widget, interactivity and cartocss columns', function (done) {
|
||||
var widgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss:
|
||||
'#layer0 { marker-fill: red; marker-width: 10; [name="Madrid"] { marker-fill: green; } }',
|
||||
cartocss_version: '2.0.1',
|
||||
widgets: {
|
||||
adm0name: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'adm0name',
|
||||
aggregation: 'sum',
|
||||
aggregationColumn: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
interactivity: "cartodb_id,pop_min"
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(widgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
|
||||
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
name: 'Mardin',
|
||||
adm0name: 'Turkey',
|
||||
pop_max: 71373,
|
||||
pop_min: 57586
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not duplicate columns', function (done) {
|
||||
var widgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: ['#layer0 {',
|
||||
'marker-fill: red;',
|
||||
'marker-width: 10;',
|
||||
'[name="Madrid"] { marker-fill: green; } ',
|
||||
'[pop_max>100000] { marker-fill: black; } ',
|
||||
'}'].join('\n'),
|
||||
cartocss_version: '2.3.0',
|
||||
widgets: {
|
||||
adm0name: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'adm0name',
|
||||
aggregation: 'sum',
|
||||
aggregationColumn: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
interactivity: "cartodb_id,pop_max"
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(widgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
|
||||
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
name: 'Mardin',
|
||||
adm0name: 'Turkey',
|
||||
pop_max: 71373
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('with formula widget, no interactivity and no cartocss columns', function (done) {
|
||||
var formulaWidgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced where pop_max > 0 and pop_max < 600000',
|
||||
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'cartodb_id',
|
||||
widgets: {
|
||||
pop_max_f: {
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: 'pop_max',
|
||||
operation: 'count'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
};
|
||||
this.testClient = new TestClient(formulaWidgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
|
||||
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
pop_max: 71373
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('with cartocss with multiple expressions', function (done) {
|
||||
var formulaWidgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced where pop_max > 0 and pop_max < 600000',
|
||||
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }' +
|
||||
'#layer0 { marker-width: 14; [name="Madrid"] { marker-width: 20; } }' +
|
||||
'#layer0[pop_max>1000] { marker-width: 14; [name="Madrid"] { marker-width: 20; } }' +
|
||||
'#layer0[adm0name=~".*Turkey*"] { marker-width: 14; [name="Madrid"] { marker-width: 20; } }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'cartodb_id'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(formulaWidgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
|
||||
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
pop_max: 71373,
|
||||
name: "Mardin",
|
||||
adm0name: "Turkey"
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
var skipOnPostGIS = usePostGIS ? it.skip: it;
|
||||
skipOnPostGIS('should work with mapnik substitution tokens', function (done) {
|
||||
var cartocss = [
|
||||
"#layer {",
|
||||
" line-width: 2;",
|
||||
" line-color: #3B3B58;",
|
||||
" line-opacity: 1;",
|
||||
" polygon-opacity: 0.7;",
|
||||
" polygon-fill: ramp([points_count], (#E5F5F9,#99D8C9,#2CA25F))",
|
||||
"}"
|
||||
].join('\n');
|
||||
|
||||
var sql = [
|
||||
'WITH hgrid AS (',
|
||||
' SELECT CDB_HexagonGrid(',
|
||||
' ST_Expand(!bbox!, greatest(!pixel_width!,!pixel_height!) * 100),',
|
||||
' greatest(!pixel_width!,!pixel_height!) * 100',
|
||||
' ) as cell',
|
||||
')',
|
||||
'SELECT',
|
||||
' hgrid.cell as the_geom_webmercator,',
|
||||
' count(1) as points_count,',
|
||||
' count(1)/power(100 * CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!)), 2) as points_density,',
|
||||
' 1 as cartodb_id',
|
||||
'FROM hgrid, (SELECT * FROM populated_places_simple_reduced) i',
|
||||
'where ST_Intersects(i.the_geom_webmercator, hgrid.cell)',
|
||||
'GROUP BY hgrid.cell'
|
||||
].join('\n');
|
||||
|
||||
var mapConfig = {
|
||||
"version": "1.4.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": 'mapnik',
|
||||
"options": {
|
||||
"cartocss_version": '2.3.0',
|
||||
"sql": sql,
|
||||
"cartocss": cartocss
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
|
||||
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(geojsonTile);
|
||||
assert.equal(geojsonTile.features.length, 5);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip empty and null columns for geojson tiles', function (done) {
|
||||
|
||||
var mapConfig = {
|
||||
"analyses": [
|
||||
{
|
||||
"id": "a0",
|
||||
"params": {
|
||||
"query": "SELECT * FROM test_table"
|
||||
},
|
||||
"type": "source"
|
||||
}
|
||||
],
|
||||
"dataviews": {
|
||||
"4e7b0e07-6d21-4b83-9adb-6d7e17eea6ca": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"column": "cartodb_id",
|
||||
"operation": "avg"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "formula"
|
||||
},
|
||||
"74f590f8-625c-4e95-922f-34ad3e9919c0": {
|
||||
"options": {
|
||||
"aggregation": "sum",
|
||||
"aggregationColumn": "cartodb_id",
|
||||
"column": "name"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "aggregation"
|
||||
},
|
||||
"98a75757-3006-400a-b028-fb613a6c0b69": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"column": "cartodb_id",
|
||||
"operation": "sum"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "formula"
|
||||
},
|
||||
"ebbc97b2-87d2-4895-9e1f-2f012df3679d": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"bins": "12",
|
||||
"column": "cartodb_id"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "histogram"
|
||||
},
|
||||
"ebc0653f-3581-469c-8b31-c969e440a865": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"column": "cartodb_id",
|
||||
"operation": "avg"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "formula"
|
||||
}
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"options": {
|
||||
"subdomains": "abcd",
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png"
|
||||
},
|
||||
"type": "http"
|
||||
},
|
||||
{
|
||||
"options": {
|
||||
"attributes": {
|
||||
"columns": [
|
||||
"name",
|
||||
"address"
|
||||
],
|
||||
"id": "cartodb_id"
|
||||
},
|
||||
"cartocss": "#layer { marker-width: 10; marker-fill: red; }",
|
||||
"cartocss_version": "2.3.0",
|
||||
"interactivity": "cartodb_id",
|
||||
"layer_name": "wadus",
|
||||
"source": {
|
||||
"id": "a0"
|
||||
}
|
||||
},
|
||||
"type": "cartodb"
|
||||
},
|
||||
{
|
||||
"options": {
|
||||
"subdomains": "abcd",
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png"
|
||||
},
|
||||
"type": "http"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
|
||||
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(geojsonTile);
|
||||
assert.equal(geojsonTile.features.length, 5);
|
||||
|
||||
assert.deepEqual(Object.keys(geojsonTile.features[0].properties), ['cartodb_id', 'name']);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
|
||||
|
||||
@@ -19,7 +19,8 @@ describe('named maps static view', function() {
|
||||
var username = 'localhost';
|
||||
var templateName = 'template_with_view';
|
||||
|
||||
var IMAGE_TOLERANCE = 20;
|
||||
var PNG_IMAGE_TOLERANCE = 20;
|
||||
var JPG_IMAGE_TOLERANCE = 100;
|
||||
|
||||
function createTemplate(view, layers) {
|
||||
return {
|
||||
@@ -92,8 +93,8 @@ describe('named maps static view', function() {
|
||||
});
|
||||
}
|
||||
|
||||
function previewFixture(version) {
|
||||
return './test/fixtures/previews/populated_places_simple_reduced-' + version + '.png';
|
||||
function previewFixture(version, format='png') {
|
||||
return './test/fixtures/previews/populated_places_simple_reduced-' + version + '.' + format;
|
||||
}
|
||||
|
||||
it('should return an image estimating its bounds based on dataset', function (done) {
|
||||
@@ -103,7 +104,7 @@ describe('named maps static view', function() {
|
||||
}
|
||||
getStaticMap(function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('estimated'), IMAGE_TOLERANCE, done);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('estimated'), PNG_IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -122,7 +123,7 @@ describe('named maps static view', function() {
|
||||
}
|
||||
getStaticMap(function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), IMAGE_TOLERANCE, done);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), PNG_IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -142,7 +143,7 @@ describe('named maps static view', function() {
|
||||
}
|
||||
getStaticMap(function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('bounds'), IMAGE_TOLERANCE, done);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('bounds'), PNG_IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -167,7 +168,7 @@ describe('named maps static view', function() {
|
||||
}
|
||||
getStaticMap(function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), IMAGE_TOLERANCE, done);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), PNG_IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -192,7 +193,7 @@ describe('named maps static view', function() {
|
||||
}
|
||||
getStaticMap({ zoom: 3 }, function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('override-zoom'), IMAGE_TOLERANCE, done);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('override-zoom'), PNG_IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -217,7 +218,7 @@ describe('named maps static view', function() {
|
||||
}
|
||||
getStaticMap({ bbox: '0,45,90,45' }, function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('override-bbox'), IMAGE_TOLERANCE, done);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('override-bbox'), PNG_IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -256,7 +257,27 @@ describe('named maps static view', function() {
|
||||
}
|
||||
getStaticMap({ layer: 0 }, function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('bounds'), IMAGE_TOLERANCE, done);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('bounds'), PNG_IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return jpg static map', function (done) {
|
||||
var view = {
|
||||
zoom: 4,
|
||||
center: {
|
||||
lng: 40,
|
||||
lat: 20
|
||||
}
|
||||
};
|
||||
templateMaps.addTemplate(username, createTemplate(view), function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
getStaticMap({ format: 'jpeg' }, function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('zoom-center', 'jpeg'),
|
||||
JPG_IMAGE_TOLERANCE, done, 'jpeg');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ var assert = require('../support/assert');
|
||||
var cartodbServer = require('../../lib/cartodb/server');
|
||||
var ServerOptions = require('./ported/support/ported_server_options');
|
||||
var testClient = require('./ported/support/test_client');
|
||||
var BaseController = require('../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('overviews_queries', function() {
|
||||
|
||||
@@ -13,15 +12,7 @@ describe('overviews_queries', function() {
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 2;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
|
||||
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,13 +4,10 @@ var assert = require('../../support/assert');
|
||||
var step = require('step');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('attributes', function() {
|
||||
|
||||
var server = cartodbServer(PortedServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
@@ -49,16 +46,6 @@ describe('attributes', function() {
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
it("can only be fetched from layer having an attributes spec", function(done) {
|
||||
|
||||
var expected_token;
|
||||
|
||||
@@ -3,21 +3,7 @@ require('../../support/test_helper');
|
||||
var assert = require('../../support/assert');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('blend png renderer', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var IMAGE_TOLERANCE_PER_MIL = 20;
|
||||
|
||||
function plainTorqueMapConfig(plainColor) {
|
||||
|
||||
@@ -5,20 +5,13 @@ var testClient = require('./support/test_client');
|
||||
var fs = require('fs');
|
||||
var http = require('http');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('blend layer filtering', function() {
|
||||
|
||||
var IMG_TOLERANCE_PER_MIL = 20;
|
||||
|
||||
var httpRendererResourcesServer;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function(done) {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
|
||||
// Start a server to test external resources
|
||||
httpRendererResourcesServer = http.createServer( function(request, response) {
|
||||
var filename = __dirname + '/../../fixtures/http/light_nolabels-1-0-0.png';
|
||||
@@ -32,7 +25,6 @@ describe('blend layer filtering', function() {
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
httpRendererResourcesServer.close(done);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,19 +5,13 @@ var testClient = require('./support/test_client');
|
||||
var fs = require('fs');
|
||||
var http = require('http');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('blend http fallback', function() {
|
||||
|
||||
var IMG_TOLERANCE_PER_MIL = 20;
|
||||
|
||||
var httpRendererResourcesServer;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function(done) {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
// Start a server to test external resources
|
||||
httpRendererResourcesServer = http.createServer( function(request, response) {
|
||||
if (request.url.match(/^\/error404\//)) {
|
||||
@@ -39,7 +33,6 @@ describe('blend http fallback', function() {
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
httpRendererResourcesServer.close(done);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,21 +6,7 @@ var serverOptions = require('./support/ported_server_options');
|
||||
var fs = require('fs');
|
||||
var http = require('http');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe.skip('blend http client timeout', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var mapConfig = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var fs = require('fs');
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var http = require('http');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var nock = require('nock');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('external resources', function() {
|
||||
|
||||
var res_serv; // resources server
|
||||
@@ -19,12 +15,8 @@ describe('external resources', function() {
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 25;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function(done) {
|
||||
nock.enableNetConnect('127.0.0.1');
|
||||
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
// Start a server to test external resources
|
||||
res_serv = http.createServer( function(request, response) {
|
||||
++res_serv_status.numrequests;
|
||||
@@ -44,8 +36,6 @@ describe('external resources', function() {
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
|
||||
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
|
||||
|
||||
// Close the resources server
|
||||
|
||||
@@ -6,19 +6,14 @@ var assert = require('../../support/assert');
|
||||
var testClient = require('./support/test_client');
|
||||
var serverOptions = require('./support/ported_server_options');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe.skip('render limits', function() {
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 25;
|
||||
|
||||
var limitsConfig;
|
||||
var onTileErrorStrategy;
|
||||
var req2paramsFn;
|
||||
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
limitsConfig = serverOptions.renderer.mapnik.limits;
|
||||
serverOptions.renderer.mapnik.limits = {
|
||||
render: 50,
|
||||
@@ -31,7 +26,6 @@ describe.skip('render limits', function() {
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
serverOptions.renderer.mapnik.limits = limitsConfig;
|
||||
serverOptions.renderer.onTileErrorStrategy = onTileErrorStrategy;
|
||||
});
|
||||
|
||||
@@ -7,8 +7,7 @@ var step = require('step');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('multilayer', function() {
|
||||
|
||||
@@ -24,16 +23,6 @@ describe('multilayer', function() {
|
||||
assert.equal(res.headers['access-control-allow-origin'], '*');
|
||||
}
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
// See https://github.com/Vizzuality/Windshaft/issues/70
|
||||
it("post layergroup with encoding in content-type", function(done) {
|
||||
var layergroup = {
|
||||
|
||||
@@ -7,23 +7,11 @@ var ServerOptions = require('./support/ported_server_options');
|
||||
var testClient = require('./support/test_client');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('multilayer error cases', function() {
|
||||
|
||||
var server = cartodbServer(ServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
// var client = null;
|
||||
afterEach(function(done) {
|
||||
if (this.client) {
|
||||
@@ -40,7 +28,7 @@ describe('multilayer error cases', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody.errors, ["layergroup POST data must be of type application/json"]);
|
||||
assert.deepEqual(parsedBody.errors, ["POST data must be of type application/json"]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,25 +5,13 @@ var _ = require('underscore');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var getLayerTypeFn = require('windshaft').model.MapConfig.prototype.getType;
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('multilayer interactivity and layers order', function() {
|
||||
|
||||
var server = cartodbServer(PortedServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
function layerType(layer) {
|
||||
return layer.type || 'undefined';
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ var assert = require('../../support/assert');
|
||||
var step = require('step');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('raster', function() {
|
||||
|
||||
@@ -20,18 +18,7 @@ describe('raster', function() {
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 2;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
it("can render raster for valid mapconfig", function(done) {
|
||||
|
||||
var mapconfig = {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('regressions', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ var mapnik = require('windshaft').mapnik;
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('retina support', function() {
|
||||
|
||||
@@ -15,15 +14,6 @@ describe('retina support', function() {
|
||||
var server = cartodbServer(ServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var keysToDelete;
|
||||
beforeEach(function(done) {
|
||||
|
||||
@@ -5,22 +5,12 @@ var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('server', function() {
|
||||
|
||||
var server = cartodbServer(ServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
|
||||
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
|
||||
});
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('server_gettile', function() {
|
||||
|
||||
var server = cartodbServer(ServerOptions);
|
||||
@@ -16,16 +14,7 @@ describe('server_gettile', function() {
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 25;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
|
||||
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,13 +6,11 @@ var fs = require('fs');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 85;
|
||||
|
||||
describe('server_png8_format', function() {
|
||||
|
||||
var serverOptionsPng32 = ServerOptions;
|
||||
serverOptionsPng32.grainstore = _.clone(ServerOptions.grainstore);
|
||||
serverOptionsPng32.grainstore.mapnik_tile_format = 'png32';
|
||||
@@ -25,13 +23,9 @@ describe('server_png8_format', function() {
|
||||
var serverPng8 = cartodbServer(serverOptionsPng8);
|
||||
serverPng8.setMaxListeners(0);
|
||||
|
||||
|
||||
var layergroupId;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function(done) {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
var testPngFilesDir = __dirname + '/../../results/png';
|
||||
fs.readdirSync(testPngFilesDir)
|
||||
.filter(function(fileName) {
|
||||
@@ -45,10 +39,6 @@ describe('server_png8_format', function() {
|
||||
done();
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var keysToDelete;
|
||||
beforeEach(function() {
|
||||
keysToDelete = {
|
||||
|
||||
@@ -5,9 +5,6 @@ var testClient = require('./support/test_client');
|
||||
var http = require('http');
|
||||
var fs = require('fs');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('static_maps', function() {
|
||||
|
||||
var validUrlTemplate = 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png';
|
||||
@@ -15,11 +12,7 @@ describe('static_maps', function() {
|
||||
|
||||
var httpRendererResourcesServer;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function(done) {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
|
||||
// Start a server to test external resources
|
||||
httpRendererResourcesServer = http.createServer( function(request, response) {
|
||||
var filename = __dirname + '/../../fixtures/http/basemap.png';
|
||||
@@ -33,7 +26,6 @@ describe('static_maps', function() {
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
httpRendererResourcesServer.close(done);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
var _ = require('underscore');
|
||||
var serverOptions = require('../../../../lib/cartodb/server_options');
|
||||
var LayergroupToken = require('../../../support/layergroup-token');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var LayergroupToken = require('../../../../lib/cartodb/models/layergroup-token');
|
||||
var OverviewsQueryRewriter = require('../../../../lib/cartodb/utils/overviews_query_rewriter');
|
||||
var overviewsQueryRewriter = new OverviewsQueryRewriter({
|
||||
zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)'
|
||||
@@ -48,7 +48,7 @@ module.exports = _.extend({}, serverOptions, {
|
||||
log_format: null, // do not log anything
|
||||
afterLayergroupCreateCalls: 0,
|
||||
useProfiler: true,
|
||||
req2params: function(req, callback){
|
||||
req2params: function(req, res, callback){
|
||||
|
||||
if ( req.query.testUnexpectedError ) {
|
||||
return callback('test unexpected error');
|
||||
@@ -56,13 +56,14 @@ module.exports = _.extend({}, serverOptions, {
|
||||
|
||||
// this is in case you want to test sql parameters eg ...png?sql=select * from my_table limit 10
|
||||
req.params = _.extend({}, req.params);
|
||||
|
||||
if (req.params.token) {
|
||||
req.params.token = LayergroupToken.parse(req.params.token).token;
|
||||
}
|
||||
|
||||
_.extend(req.params, req.query);
|
||||
req.params.user = 'localhost';
|
||||
req.context = {user: 'localhost'};
|
||||
res.locals.user = 'localhost';
|
||||
|
||||
req.params.dbhost = global.environment.postgres.host;
|
||||
req.params.dbport = req.params.dbport || global.environment.postgres.port;
|
||||
@@ -73,6 +74,9 @@ module.exports = _.extend({}, serverOptions, {
|
||||
}
|
||||
req.params.dbname = 'test_windshaft_cartodb_user_1_db';
|
||||
|
||||
// add all params to res.locals
|
||||
res.locals = _.extend({}, req.params);
|
||||
|
||||
|
||||
// increment number of calls counter
|
||||
global.req2params_calls = global.req2params_calls ? global.req2params_calls + 1 : 1;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
var testHelper = require('../../../support/test_helper');
|
||||
var LayergroupToken = require('../../../support/layergroup-token');
|
||||
var LayergroupToken = require('../../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
var step = require('step');
|
||||
var assert = require('../../../support/assert');
|
||||
|
||||
@@ -6,24 +6,13 @@ var step = require('step');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('torque', function() {
|
||||
|
||||
var server = cartodbServer(ServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var keysToDelete;
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
|
||||
@@ -4,21 +4,9 @@ var assert = require('../../support/assert');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('torque boundary points', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var layergroupIdToDelete = null;
|
||||
|
||||
beforeEach(function() {
|
||||
|
||||
@@ -3,21 +3,7 @@ require('../../support/test_helper');
|
||||
var assert = require('../../support/assert');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('torque png renderer', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var IMAGE_TOLERANCE_PER_MIL = 20;
|
||||
|
||||
var torquePngPointsMapConfig = {
|
||||
|
||||
@@ -3,21 +3,7 @@ require('../../support/test_helper');
|
||||
var assert = require('../../support/assert');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('torque tiles at 0,0 point', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
/*
|
||||
Tiles are represented as in:
|
||||
|
||||
|
||||
@@ -3,21 +3,7 @@ require('../../support/test_helper');
|
||||
var assert = require('../../support/assert');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('wrap x coordinate', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
describe('renders correct tile', function() {
|
||||
var IMG_TOLERANCE_PER_MIL = 20;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('template_api', function() {
|
||||
server.layergroupAffectedTablesCache.cache.reset();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var assert = require('../../support/assert');
|
||||
var step = require('step');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
var testHelper = require('../../support/test_helper');
|
||||
var CartodbWindshaft = require('../../../lib/cartodb/server');
|
||||
var serverOptions = require('../../../lib/cartodb/server_options');
|
||||
|
||||
@@ -419,18 +419,23 @@ describe('user database timeout limit', function () {
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
'Content-Type': 'application/x-protobuf'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, timeoutError) => {
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, tile) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(timeoutError, DATASOURCE_TIMEOUT_ERROR);
|
||||
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ require('../support/test_helper');
|
||||
|
||||
const assert = require('../support/assert');
|
||||
const TestClient = require('../support/test-client');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
|
||||
const timeoutErrorTilePath = `${process.cwd()}/assets/render-timeout-fallback.png`;
|
||||
|
||||
@@ -200,49 +201,56 @@ describe('user render timeout limit', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('vector', function () {
|
||||
beforeEach(function (done) {
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 50, done);
|
||||
});
|
||||
if (process.env.POSTGIS_VERSION === '2.4') {
|
||||
describe('vector (PostGIS)', vector(true));
|
||||
}
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.setUserRenderTimeoutLimit('localhost', 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
this.testClient.drain(done);
|
||||
describe('vector (mapnik)', vector(false));
|
||||
function vector(usePostGIS) {
|
||||
const originalUsePostGIS = serverOptions.renderer.mvt.usePostGIS;
|
||||
return function () {
|
||||
beforeEach(function (done) {
|
||||
serverOptions.renderer.mvt.usePostGIS = usePostGIS;
|
||||
const mapconfig = createMapConfig();
|
||||
this.testClient = new TestClient(mapconfig, 1234);
|
||||
this.testClient.setUserDatabaseTimeoutLimit(50, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation works but vector tile request fails due to render timeout', function (done) {
|
||||
const params = {
|
||||
format: 'mvt',
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
afterEach(function (done) {
|
||||
serverOptions.renderer.mvt.usePostGIS = originalUsePostGIS;
|
||||
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, params, (err, res, tile) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.deepEqual(tile, {
|
||||
errors: ['You are over platform\'s limits. Please contact us to know more details'],
|
||||
errors_with_context: [{
|
||||
type: 'limit',
|
||||
subtype: 'render',
|
||||
message: 'You are over platform\'s limits. Please contact us to know more details'
|
||||
}]
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('layergroup creation works but vector tile request fails due to render timeout', function (done) {
|
||||
const params = {
|
||||
format: 'mvt',
|
||||
response: {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-protobuf'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
describe('interativity', function () {
|
||||
beforeEach(function (done) {
|
||||
|
||||
221
test/acceptance/vector-layergroup.js
Normal file
221
test/acceptance/vector-layergroup.js
Normal file
@@ -0,0 +1,221 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
const assert = require('../support/assert');
|
||||
const TestClient = require('../support/test-client');
|
||||
const serverOptions = require('../../lib/cartodb/server_options');
|
||||
|
||||
|
||||
const POINTS_SQL_1 = `
|
||||
select
|
||||
st_setsrid(st_makepoint(x*10, x*10), 4326) as the_geom,
|
||||
st_transform(st_setsrid(st_makepoint(x*10, x*10), 4326), 3857) as the_geom_webmercator,
|
||||
x as value
|
||||
from generate_series(-3, 3) x
|
||||
`;
|
||||
|
||||
const POINTS_SQL_2 = `
|
||||
select
|
||||
st_setsrid(st_makepoint(x*10, x*10*(-1)), 4326) as the_geom,
|
||||
st_transform(st_setsrid(st_makepoint(x*10, x*10*(-1)), 4326), 3857) as the_geom_webmercator,
|
||||
x as value
|
||||
from generate_series(-3, 3) x
|
||||
`;
|
||||
|
||||
function createVectorLayergroup () {
|
||||
return {
|
||||
version: '1.6.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_2
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
const INCOMPATIBLE_LAYERS_ERROR = {
|
||||
"errors": [
|
||||
"The `mapnik` or `cartodb` layers must be consistent:" +
|
||||
" `cartocss` option is either present or voided in all layers. Mixing is not allowed."
|
||||
],
|
||||
"errors_with_context":[
|
||||
{
|
||||
"type":"mapconfig",
|
||||
"message": "The `mapnik` or `cartodb` layers must be consistent:" +
|
||||
" `cartocss` option is either present or voided in all layers. Mixing is not allowed."
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const INVALID_FORMAT_ERROR = {
|
||||
"errors": [
|
||||
"Unsupported format: 'cartocss' option is missing for png"
|
||||
],
|
||||
"errors_with_context":[
|
||||
{
|
||||
"type": "tile",
|
||||
"message": "Unsupported format: 'cartocss' option is missing for png"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const suites = [{
|
||||
desc: 'mvt (mapnik)',
|
||||
usePostGIS: false
|
||||
}];
|
||||
|
||||
if (process.env.POSTGIS_VERSION === '2.4') {
|
||||
suites.push({
|
||||
desc: 'mvt (postgis)',
|
||||
usePostGIS: true
|
||||
});
|
||||
}
|
||||
|
||||
suites.forEach((suite) => {
|
||||
const { desc, usePostGIS } = suite;
|
||||
|
||||
describe(desc, function () {
|
||||
const originalUsePostGIS = serverOptions.renderer.mvt.usePostGIS;
|
||||
|
||||
before(function () {
|
||||
serverOptions.renderer.mvt.usePostGIS = usePostGIS;
|
||||
});
|
||||
|
||||
after(function (){
|
||||
serverOptions.renderer.mvt.usePostGIS = originalUsePostGIS;
|
||||
});
|
||||
|
||||
describe('vector-layergroup', function () {
|
||||
beforeEach(function () {
|
||||
this.mapConfig = createVectorLayergroup();
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
it('should get vector tiles from layergroup with layers w/o cartocss', function (done) {
|
||||
this.testClient.getTile(0, 0, 0, { format: 'mvt' }, (err, res, tile) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(tile.tileSize, 4096);
|
||||
assert.equal(tile.z, 0);
|
||||
assert.equal(tile.x, 0);
|
||||
assert.equal(tile.y, 0);
|
||||
|
||||
const layer0 = JSON.parse(tile.toGeoJSONSync(0));
|
||||
|
||||
assert.equal(layer0.name, 'layer0');
|
||||
assert.equal(layer0.features[0].type, 'Feature');
|
||||
assert.equal(layer0.features[0].geometry.type, 'Point');
|
||||
|
||||
const layer1 = JSON.parse(tile.toGeoJSONSync(1));
|
||||
|
||||
assert.equal(layer1.name, 'layer1');
|
||||
assert.equal(layer1.features[0].type, 'Feature');
|
||||
assert.equal(layer1.features[0].geometry.type, 'Point');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get vector tiles from specific layer (layer0)', function (done) {
|
||||
this.testClient.getTile(0, 0, 0, { format: 'mvt', layers: 0 }, (err, res, tile) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(tile.tileSize, 4096);
|
||||
assert.equal(tile.z, 0);
|
||||
assert.equal(tile.x, 0);
|
||||
assert.equal(tile.y, 0);
|
||||
|
||||
const layer = JSON.parse(tile.toGeoJSONSync(0));
|
||||
|
||||
assert.equal(layer.name, 'layer0');
|
||||
assert.equal(layer.features[0].type, 'Feature');
|
||||
assert.equal(layer.features[0].geometry.type, 'Point');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get vector tiles from specific layer (layer1)', function (done) {
|
||||
this.testClient.getTile(0, 0, 0, { format: 'mvt', layers: 1 }, (err, res, tile) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(tile.tileSize, 4096);
|
||||
assert.equal(tile.z, 0);
|
||||
assert.equal(tile.x, 0);
|
||||
assert.equal(tile.y, 0);
|
||||
|
||||
const layer = JSON.parse(tile.toGeoJSONSync(0));
|
||||
|
||||
assert.equal(layer.name, 'layer1');
|
||||
assert.equal(layer.features[0].type, 'Feature');
|
||||
assert.equal(layer.features[0].geometry.type, 'Point');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when the format requested is not mvt', function (done) {
|
||||
const options = {
|
||||
format: 'png',
|
||||
response: {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, options, (err, res, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.deepEqual(body, INVALID_FORMAT_ERROR);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when the map-config mix layers with and without cartocss', function (done) {
|
||||
const response = {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
const cartocss = `#layer0 { marker-fill: red; marker-width: 10; }`;
|
||||
const cartocssVersion = '2.3.0';
|
||||
|
||||
this.testClient.mapConfig.layers[0].options.cartocss = cartocss;
|
||||
this.testClient.mapConfig.layers[0].options.cartocss_version = cartocssVersion;
|
||||
|
||||
this.testClient.getLayergroup(response, (err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.deepEqual(body, INCOMPATIBLE_LAYERS_ERROR);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@ var CartodbWindshaft = require('../../../lib/cartodb/server');
|
||||
var serverOptions = require('../../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('named-maps widgets', function() {
|
||||
|
||||
|
||||
BIN
test/fixtures/previews/populated_places_simple_reduced-zoom-center.jpeg
vendored
Normal file
BIN
test/fixtures/previews/populated_places_simple_reduced-zoom-center.jpeg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
3
test/results/jpeg/.gitignore
vendored
Normal file
3
test/results/jpeg/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.jpeg
|
||||
*.jpg
|
||||
*.js
|
||||
@@ -37,7 +37,7 @@ assert.imageBuffersAreSimilar = function(bufferA, bufferB, tolerance, callback)
|
||||
imagesAreSimilar(testImage, referenceImage, tolerance, callback);
|
||||
};
|
||||
|
||||
assert.imageIsSimilarToFile = function(testImage, referenceImageRelativeFilePath, tolerance, callback) {
|
||||
assert.imageIsSimilarToFile = function(testImage, referenceImageRelativeFilePath, tolerance, callback, format='png') {
|
||||
callback = callback || function(err) { assert.ifError(err); };
|
||||
|
||||
var referenceImageFilePath = path.resolve(referenceImageRelativeFilePath);
|
||||
@@ -46,19 +46,23 @@ assert.imageIsSimilarToFile = function(testImage, referenceImageRelativeFilePath
|
||||
|
||||
imagesAreSimilar(testImage, referenceImage, tolerance, function(err) {
|
||||
if (err) {
|
||||
var testImageFilePath = randomImagePath();
|
||||
testImage.save(testImageFilePath);
|
||||
var testImageFilePath = randomImagePath(format);
|
||||
testImage.save(testImageFilePath, format);
|
||||
}
|
||||
callback(err);
|
||||
});
|
||||
}, format);
|
||||
};
|
||||
|
||||
function imagesAreSimilar(testImage, referenceImage, tolerance, callback) {
|
||||
function imagesAreSimilar(testImage, referenceImage, tolerance, callback, format='png') {
|
||||
if (testImage.width() !== referenceImage.width() || testImage.height() !== referenceImage.height()) {
|
||||
return callback(new Error('Images are not the same size'));
|
||||
}
|
||||
|
||||
var pixelsDifference = referenceImage.compare(testImage);
|
||||
var options = {};
|
||||
if (format === 'jpeg') {
|
||||
options.alpha = false;
|
||||
}
|
||||
var pixelsDifference = referenceImage.compare(testImage, options);
|
||||
var similarity = pixelsDifference / (referenceImage.width() * referenceImage.height());
|
||||
var tolerancePerMil = (tolerance / 1000);
|
||||
|
||||
@@ -73,11 +77,10 @@ function imagesAreSimilar(testImage, referenceImage, tolerance, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
function randomImagePath() {
|
||||
return path.resolve('test/results/png/image-test-' + Date.now() + '.png');
|
||||
function randomImagePath(format='png') {
|
||||
return path.resolve('test/results/' + format + '/image-test-' + Date.now() + '.' + format);
|
||||
}
|
||||
|
||||
// jshint maxcomplexity:9
|
||||
assert.response = function(server, req, res, callback) {
|
||||
if (!callback) {
|
||||
callback = res;
|
||||
@@ -106,7 +109,6 @@ assert.response = function(server, req, res, callback) {
|
||||
|
||||
// jshint maxcomplexity:9
|
||||
function onServerListening() {
|
||||
var status = res.status || res.statusCode;
|
||||
var requestParams = {
|
||||
url: 'http://' + host + ':' + port + req.url,
|
||||
method: req.method || 'GET',
|
||||
@@ -122,61 +124,74 @@ assert.response = function(server, req, res, callback) {
|
||||
request(requestParams, function assert$response$requestHandler(error, response, body) {
|
||||
listener.close(function() {
|
||||
response.body = response.body || body;
|
||||
|
||||
// Assert response body
|
||||
if (res.body) {
|
||||
var eql = res.body instanceof RegExp ? res.body.test(response.body) : res.body === response.body;
|
||||
if (!eql) {
|
||||
return callback(response, new Error(colorize(
|
||||
'[red]{Invalid response body.}\n' +
|
||||
' Expected: [green]{' + res.body + '}\n' +
|
||||
' Got: [red]{' + response.body + '}'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Assert response status
|
||||
if (typeof status === 'number') {
|
||||
if (response.statusCode != status) {
|
||||
return callback(response, new Error(colorize(
|
||||
'[red]{Invalid response status code.}\n' +
|
||||
' Expected: [green]{' + status + '}\n' +
|
||||
' Got: [red]{' + response.statusCode + '}\n' +
|
||||
' Body: ' + response.body))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Assert response headers
|
||||
if (res.headers) {
|
||||
var keys = Object.keys(res.headers);
|
||||
for (var i = 0, len = keys.length; i < len; ++i) {
|
||||
var name = keys[i],
|
||||
actual = response.headers[name.toLowerCase()],
|
||||
expected = res.headers[name],
|
||||
headerEql = expected instanceof RegExp ? expected.test(actual) : expected === actual;
|
||||
if (!headerEql) {
|
||||
return callback(response, new Error(colorize(
|
||||
'Invalid response header [bold]{' + name + '}.\n' +
|
||||
' Expected: [green]{' + expected + '}\n' +
|
||||
' Got: [red]{' + actual + '}'))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Callback
|
||||
callback(response);
|
||||
var err = validateResponse(response, res);
|
||||
return callback(response, err);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
// jshint maxcomplexity:6
|
||||
|
||||
function validateResponseBody(response, expected) {
|
||||
if (expected.body) {
|
||||
var eql = expected.body instanceof RegExp ?
|
||||
expected.body.test(response.body) :
|
||||
expected.body === response.body;
|
||||
if (!eql) {
|
||||
return new Error(colorize(
|
||||
'[red]{Invalid response body.}\n' +
|
||||
' Expected: [green]{' + expected.body + '}\n' +
|
||||
' Got: [red]{' + response.body + '}')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateResponseStatus(response, expected) {
|
||||
var status = expected.status || expected.statusCode;
|
||||
// Assert response status
|
||||
if (typeof status === 'number') {
|
||||
if (response.statusCode !== status) {
|
||||
return new Error(colorize(
|
||||
'[red]{Invalid response status code.}\n' +
|
||||
' Expected: [green]{' + status + '}\n' +
|
||||
' Got: [red]{' + response.statusCode + '}\n' +
|
||||
' Body: ' + response.body)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateResponseHeaders(response, expected) {
|
||||
// Assert response headers
|
||||
if (expected.headers) {
|
||||
var keys = Object.keys(expected.headers);
|
||||
for (var i = 0, len = keys.length; i < len; ++i) {
|
||||
var name = keys[i],
|
||||
actual = response.headers[name.toLowerCase()],
|
||||
expectedHeader = expected.headers[name],
|
||||
headerEql = expectedHeader instanceof RegExp ? expectedHeader.test(actual) : expectedHeader === actual;
|
||||
if (!headerEql) {
|
||||
return new Error(colorize(
|
||||
'Invalid response header [bold]{' + name + '}.\n' +
|
||||
' Expected: [green]{' + expectedHeader + '}\n' +
|
||||
' Got: [red]{' + actual + '}')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateResponse(response, expected) {
|
||||
// Assert response body
|
||||
return validateResponseBody(response, expected) ||
|
||||
validateResponseStatus(response, expected) ||
|
||||
validateResponseHeaders(response, expected);
|
||||
}
|
||||
|
||||
// @param tolerance number of tolerated grid cell differences
|
||||
// jshint maxcomplexity:9
|
||||
assert.utfgridEqualsFile = function(buffer, file_b, tolerance, callback) {
|
||||
// jshint maxcomplexity:9
|
||||
fs.writeFileSync('/tmp/grid.json', buffer, 'binary'); // <-- to debug/update
|
||||
var expected_json = JSON.parse(fs.readFileSync(file_b, 'utf8'));
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
var _ = require('underscore');
|
||||
|
||||
|
||||
require(__dirname + '/test_helper');
|
||||
|
||||
module.exports = function(opts) {
|
||||
|
||||
var config = {
|
||||
redis_pool: {
|
||||
max: 10,
|
||||
idleTimeoutMillis: 1,
|
||||
reapIntervalMillis: 1,
|
||||
port: global.environment.redis.port
|
||||
}
|
||||
}
|
||||
|
||||
_.extend(config, opts || {});
|
||||
|
||||
return config;
|
||||
}();
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
PREPARE_REDIS=yes
|
||||
PREPARE_PGSQL=yes
|
||||
DOWNLOAD_SQL_FILES=yes
|
||||
PG_PARALLEL=$(pg_config --version | (awk '{$2*=1000; if ($2 >= 9600) print 1; else print 0;}' 2> /dev/null || echo 0))
|
||||
|
||||
while [ -n "$1" ]; do
|
||||
if test "$1" = "--skip-pg"; then
|
||||
@@ -92,6 +93,13 @@ if test x"$PREPARE_PGSQL" = xyes; then
|
||||
ALL_SQL_SCRIPTS="${REMOTE_SQL_SCRIPTS} ${LOCAL_SQL_SCRIPTS}"
|
||||
for i in ${ALL_SQL_SCRIPTS}
|
||||
do
|
||||
# Strip PARALLEL labels for PostgreSQL releases before 9.6
|
||||
if [ $PG_PARALLEL -eq 0 ]; then
|
||||
TMPFILE=$(mktemp /tmp/$(basename $0).XXXXXXXX)
|
||||
sed -e 's/PARALLEL \= [A-Z]*,/''/g' \
|
||||
-e 's/PARALLEL [A-Z]*/''/g' sql/$i.sql > $TMPFILE
|
||||
mv $TMPFILE sql/$i.sql
|
||||
fi
|
||||
cat sql/${i}.sql |
|
||||
sed -e 's/cartodb\./public./g' -e "s/''cartodb''/''public''/g" |
|
||||
sed "s/:PUBLICUSER/${PUBLICUSER}/" |
|
||||
|
||||
@@ -7,7 +7,7 @@ var PSQL = require('cartodb-psql');
|
||||
var _ = require('underscore');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
|
||||
var LayergroupToken = require('./layergroup-token');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
var assert = require('./assert');
|
||||
var helper = require('./test_helper');
|
||||
@@ -20,9 +20,8 @@ const MAPNIK_SUPPORTED_FORMATS = {
|
||||
'png': true,
|
||||
'png32': true,
|
||||
'grid.json': true,
|
||||
'geojson': true,
|
||||
'mvt': true
|
||||
}
|
||||
};
|
||||
|
||||
function TestClient(config, apiKey) {
|
||||
this.mapConfig = isMapConfig(config) ? config : null;
|
||||
@@ -115,6 +114,15 @@ module.exports.CARTOCSS = {
|
||||
module.exports.SQL = {
|
||||
EMPTY: 'select 1 as cartodb_id, null::geometry as the_geom_webmercator',
|
||||
ONE_POINT: 'select 1 as cartodb_id, \'SRID=3857;POINT(0 0)\'::geometry the_geom_webmercator'
|
||||
};
|
||||
|
||||
function resErr2errRes(callback) {
|
||||
return (res, err) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(err, res);
|
||||
};
|
||||
}
|
||||
|
||||
TestClient.prototype.getWidget = function(widgetName, params, callback) {
|
||||
@@ -130,7 +138,6 @@ TestClient.prototype.getWidget = function(widgetName, params, callback) {
|
||||
url += '?' + qs.stringify({ filters: JSON.stringify(params.filters) });
|
||||
}
|
||||
|
||||
var layergroupId;
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
@@ -176,11 +183,12 @@ TestClient.prototype.getWidget = function(widgetName, params, callback) {
|
||||
}
|
||||
);
|
||||
},
|
||||
function getWidgetResult(err, _layergroupId) {
|
||||
function getWidgetResult(err, layergroupId) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
layergroupId = _layergroupId;
|
||||
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
|
||||
@@ -217,8 +225,6 @@ TestClient.prototype.getWidget = function(widgetName, params, callback) {
|
||||
);
|
||||
},
|
||||
function finish(err, res) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
var widget;
|
||||
if (!err && res.body) {
|
||||
widget = JSON.parse(res.body);
|
||||
@@ -241,7 +247,6 @@ TestClient.prototype.widgetSearch = function(widgetName, userQuery, params, call
|
||||
url += '?' + qs.stringify({ filters: JSON.stringify(params.filters) });
|
||||
}
|
||||
|
||||
var layergroupId;
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
@@ -287,11 +292,12 @@ TestClient.prototype.widgetSearch = function(widgetName, userQuery, params, call
|
||||
}
|
||||
);
|
||||
},
|
||||
function getWidgetSearchResult(err, _layergroupId) {
|
||||
function getWidgetSearchResult(err, layergroupId) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
layergroupId = _layergroupId;
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
var urlParams = {
|
||||
q: userQuery,
|
||||
@@ -326,8 +332,6 @@ TestClient.prototype.widgetSearch = function(widgetName, userQuery, params, call
|
||||
);
|
||||
},
|
||||
function finish(err, res) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
var searchResult;
|
||||
if (!err && res.body) {
|
||||
searchResult = JSON.parse(res.body);
|
||||
@@ -365,7 +369,6 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
|
||||
}
|
||||
};
|
||||
|
||||
var layergroupId;
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
@@ -401,17 +404,18 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
|
||||
}
|
||||
);
|
||||
},
|
||||
function getDataviewResult(err, _layergroupId) {
|
||||
function getDataviewResult(err, layergroupId) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
layergroupId = _layergroupId;
|
||||
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
|
||||
};
|
||||
|
||||
['bbox', 'bins', 'start', 'end', 'aggregation', 'offset'].forEach(function(extraParam) {
|
||||
['bbox', 'bins', 'start', 'end', 'aggregation', 'offset', 'categories'].forEach(function(extraParam) {
|
||||
if (params.hasOwnProperty(extraParam)) {
|
||||
urlParams[extraParam] = params[extraParam];
|
||||
}
|
||||
@@ -444,12 +448,6 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (layergroupId) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
}
|
||||
|
||||
return callback(null, dataview);
|
||||
}
|
||||
);
|
||||
@@ -483,7 +481,6 @@ TestClient.prototype.getFeatureAttributes = function(featureId, layerId, params,
|
||||
}
|
||||
};
|
||||
|
||||
var layergroupId;
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
@@ -572,7 +569,7 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
var layergroupId;
|
||||
|
||||
if (params.layergroupid) {
|
||||
layergroupId = params.layergroupid
|
||||
layergroupId = params.layergroupid;
|
||||
}
|
||||
|
||||
step(
|
||||
@@ -587,7 +584,7 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
return next(new Error('apiKey param is mandatory to create a new template'));
|
||||
}
|
||||
|
||||
params.placeholders = params.placeholders || {};
|
||||
params.placeholders = params.placeholders || {};
|
||||
|
||||
assert.response(self.server,
|
||||
{
|
||||
@@ -620,7 +617,7 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
return next(null, layergroupId);
|
||||
}
|
||||
|
||||
var data = templateId ? params.placeholders : self.mapConfig
|
||||
var data = templateId ? params.placeholders : self.mapConfig;
|
||||
var path = templateId ?
|
||||
urlNamed + '/' + templateId + '?' + qs.stringify({api_key: self.apiKey}) :
|
||||
url;
|
||||
@@ -649,11 +646,12 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
}
|
||||
);
|
||||
},
|
||||
function getTileResult(err, _layergroupId) {
|
||||
function getTileResult(err, layergroupId) {
|
||||
// jshint maxcomplexity:12
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
layergroupId = _layergroupId;
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
url = '/api/v1/map/' + layergroupId + '/';
|
||||
|
||||
@@ -727,35 +725,31 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
|
||||
expectedResponse.headers['Content-Type'] = 'application/json; charset=utf-8';
|
||||
}
|
||||
|
||||
assert.response(self.server, request, expectedResponse, function(res, err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var body;
|
||||
switch (res.headers['content-type']) {
|
||||
case 'image/png':
|
||||
body = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
break;
|
||||
case 'application/x-protobuf':
|
||||
body = new mapnik.VectorTile(z, x, y);
|
||||
body.setDataSync(new Buffer(res.body, 'binary'));
|
||||
break;
|
||||
case 'application/json; charset=utf-8':
|
||||
body = JSON.parse(res.body);
|
||||
break;
|
||||
default:
|
||||
body = res.body
|
||||
break;
|
||||
}
|
||||
|
||||
next(null, res, body);
|
||||
});
|
||||
assert.response(self.server, request, expectedResponse, resErr2errRes(this));
|
||||
},
|
||||
function finish(err, res, image) {
|
||||
if (layergroupId) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
function finish(err, res) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(err, res, image);
|
||||
|
||||
var body;
|
||||
switch (res.headers['content-type']) {
|
||||
case 'image/png':
|
||||
body = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
break;
|
||||
case 'application/x-protobuf':
|
||||
body = new mapnik.VectorTile(z, x, y);
|
||||
body.setDataSync(new Buffer(res.body, 'binary'));
|
||||
break;
|
||||
case 'application/json; charset=utf-8':
|
||||
body = JSON.parse(res.body);
|
||||
break;
|
||||
default:
|
||||
body = res.body;
|
||||
break;
|
||||
}
|
||||
|
||||
return callback(err, res, body);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -791,10 +785,11 @@ TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
|
||||
},
|
||||
expectedResponse,
|
||||
function(res, err) {
|
||||
var parsedBody;
|
||||
// If there is a response, we are still interested in catching the created keys
|
||||
// to be able to delete them on the .drain() method.
|
||||
if (res) {
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
parsedBody = JSON.parse(res.body);
|
||||
if (parsedBody.layergroupid) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
@@ -812,7 +807,7 @@ TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
|
||||
TestClient.prototype.getStaticCenter = function (params, callback) {
|
||||
var self = this;
|
||||
|
||||
let { layergroupid, z, lat, lng, width, height, format } = params
|
||||
let { layergroupid, z, lat, lng, width, height, format } = params;
|
||||
|
||||
var url = `/api/v1/map/`;
|
||||
|
||||
@@ -828,7 +823,7 @@ TestClient.prototype.getStaticCenter = function (params, callback) {
|
||||
return next(null, layergroupid);
|
||||
}
|
||||
|
||||
var data = self.mapConfig
|
||||
var data = self.mapConfig;
|
||||
var path = url;
|
||||
|
||||
assert.response(self.server,
|
||||
@@ -855,14 +850,13 @@ TestClient.prototype.getStaticCenter = function (params, callback) {
|
||||
}
|
||||
);
|
||||
},
|
||||
function getStaticResult(err, _layergroupid) {
|
||||
function getStaticResult(err, layergroupId) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
layergroupid = _layergroupid;
|
||||
|
||||
url = `/api/v1/map/static/center/${layergroupid}/${z}/${lat}/${lng}/${width}/${height}.${format}`
|
||||
url = `/api/v1/map/static/center/${layergroupId}/${z}/${lat}/${lng}/${width}/${height}.${format}`;
|
||||
|
||||
if (self.apiKey) {
|
||||
url += '?' + qs.stringify({api_key: self.apiKey});
|
||||
@@ -884,31 +878,27 @@ TestClient.prototype.getStaticCenter = function (params, callback) {
|
||||
}
|
||||
}, params.response);
|
||||
|
||||
assert.response(self.server, request, expectedResponse, function(res, err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var body;
|
||||
switch (res.headers['content-type']) {
|
||||
case 'image/png':
|
||||
body = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
break;
|
||||
case 'application/json; charset=utf-8':
|
||||
body = JSON.parse(res.body);
|
||||
break;
|
||||
default:
|
||||
body = res.body
|
||||
break;
|
||||
}
|
||||
|
||||
next(null, res, body);
|
||||
});
|
||||
assert.response(self.server, request, expectedResponse, resErr2errRes(this));
|
||||
},
|
||||
function finish(err, res, image) {
|
||||
if (layergroupid) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupid).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
function(err, res) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(err, res, image);
|
||||
|
||||
var body;
|
||||
switch (res.headers['content-type']) {
|
||||
case 'image/png':
|
||||
body = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
break;
|
||||
case 'application/json; charset=utf-8':
|
||||
body = JSON.parse(res.body);
|
||||
break;
|
||||
default:
|
||||
body = res.body;
|
||||
break;
|
||||
}
|
||||
|
||||
return callback(err, res, body);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -922,7 +912,6 @@ TestClient.prototype.getNodeStatus = function(nodeName, callback) {
|
||||
url += '?' + qs.stringify({api_key: this.apiKey});
|
||||
}
|
||||
|
||||
var layergroupId;
|
||||
var nodes = {};
|
||||
step(
|
||||
function createLayergroup() {
|
||||
@@ -961,11 +950,11 @@ TestClient.prototype.getNodeStatus = function(nodeName, callback) {
|
||||
}
|
||||
);
|
||||
},
|
||||
function getNodeStatusResult(err, _layergroupId) {
|
||||
function getNodeStatusResult(err, layergroupId) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
layergroupId = _layergroupId;
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
url = urlParser.parse(nodes[nodeName]).path;
|
||||
|
||||
@@ -988,15 +977,13 @@ TestClient.prototype.getNodeStatus = function(nodeName, callback) {
|
||||
}
|
||||
};
|
||||
|
||||
assert.response(self.server, request, expectedResponse, function(res, err) {
|
||||
assert.ifError(err);
|
||||
next(null, res, JSON.parse(res.body));
|
||||
});
|
||||
assert.response(self.server, request, expectedResponse, resErr2errRes(this));
|
||||
},
|
||||
function finish(err, res, image) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
return callback(err, res, image);
|
||||
function finish(err, res) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, res, JSON.parse(res.body));
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1005,11 +992,11 @@ TestClient.prototype.getAttributes = function(params, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!Number.isFinite(params.featureId)) {
|
||||
throw new Error('featureId param must be a number')
|
||||
throw new Error('featureId param must be a number');
|
||||
}
|
||||
|
||||
if (!Number.isFinite(params.layer)) {
|
||||
throw new Error('layer param must be a number')
|
||||
throw new Error('layer param must be a number');
|
||||
}
|
||||
|
||||
var url = '/api/v1/map';
|
||||
@@ -1021,7 +1008,7 @@ TestClient.prototype.getAttributes = function(params, callback) {
|
||||
var layergroupid;
|
||||
|
||||
if (params.layergroupid) {
|
||||
layergroupid = params.layergroupid
|
||||
layergroupid = params.layergroupid;
|
||||
}
|
||||
|
||||
step(
|
||||
@@ -1058,14 +1045,13 @@ TestClient.prototype.getAttributes = function(params, callback) {
|
||||
}
|
||||
);
|
||||
},
|
||||
function getAttributes(err, _layergroupid) {
|
||||
function getAttributes(err, layergroupId) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
layergroupid = _layergroupid;
|
||||
|
||||
url = `/api/v1/map/${layergroupid}/${params.layer}/attributes/${params.featureId}`;
|
||||
url = `/api/v1/map/${layergroupId}/${params.layer}/attributes/${params.featureId}`;
|
||||
|
||||
if (self.apiKey) {
|
||||
url += '?' + qs.stringify({api_key: self.apiKey});
|
||||
@@ -1086,21 +1072,14 @@ TestClient.prototype.getAttributes = function(params, callback) {
|
||||
}
|
||||
};
|
||||
|
||||
assert.response(self.server, request, expectedResponse, function (res, err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var attributes = JSON.parse(res.body);
|
||||
|
||||
next(null, res, attributes);
|
||||
});
|
||||
assert.response(self.server, request, expectedResponse, resErr2errRes(this));
|
||||
},
|
||||
function finish(err, res, attributes) {
|
||||
if (layergroupid) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupid).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
function finish(err, res) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(err, res, attributes);
|
||||
var attributes = JSON.parse(res.body);
|
||||
return callback(null, res, attributes);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1144,7 +1123,7 @@ module.exports.getStaticMap = function getStaticMap(templateName, params, callba
|
||||
// this could be removed once named maps are invalidated, otherwise you hits the cache
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
assert.response(self.server, requestOptions, expectedResponse, function (res, err) {
|
||||
assert.response(server, requestOptions, expectedResponse, function (res, err) {
|
||||
helper.deleteRedisKeys({'user:localhost:mapviews:global': 5}, function() {
|
||||
return callback(err, mapnik.Image.fromBytes(new Buffer(res.body, 'binary')));
|
||||
});
|
||||
@@ -1166,12 +1145,11 @@ TestClient.prototype.setUserRenderTimeoutLimit = function (user, userTimeoutLimi
|
||||
|
||||
TestClient.prototype.setUserDatabaseTimeoutLimit = function (timeoutLimit, callback) {
|
||||
const dbname = _.template(global.environment.postgres_auth_user, { user_id: 1 }) + '_db';
|
||||
const dbuser = _.template(global.environment.postgres_auth_user, { user_id: 1 })
|
||||
const pass = _.template(global.environment.postgres_auth_pass, { user_id: 1 })
|
||||
const dbuser = _.template(global.environment.postgres_auth_user, { user_id: 1 });
|
||||
const publicuser = global.environment.postgres.user;
|
||||
|
||||
// we need to guarantee all new connections have the new settings
|
||||
helper.cleanPGPoolConnections()
|
||||
helper.cleanPGPoolConnections();
|
||||
|
||||
const psql = new PSQL({
|
||||
user: 'postgres',
|
||||
@@ -1196,4 +1174,42 @@ TestClient.prototype.setUserDatabaseTimeoutLimit = function (timeoutLimit, callb
|
||||
);
|
||||
};
|
||||
|
||||
TestClient.prototype.getAnalysesCatalog = function (params, callback) {
|
||||
var url = '/api/v1/map/analyses/catalog';
|
||||
|
||||
if (this.apiKey) {
|
||||
url += '?' + qs.stringify({api_key: this.apiKey});
|
||||
}
|
||||
|
||||
if (params.jsonp) {
|
||||
url += '&' + qs.stringify({callback: params.jsonp});
|
||||
}
|
||||
|
||||
assert.response(this.server,
|
||||
{
|
||||
url: url,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
},
|
||||
{
|
||||
status: params.status || 200,
|
||||
headers: {
|
||||
'Content-Type': params.jsonp ?
|
||||
'text/javascript; charset=utf-8' :
|
||||
'application/json; charset=utf-8'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var parsedBody = params.jsonp ? res.body : JSON.parse(res.body);
|
||||
|
||||
return callback(null, parsedBody);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
require('../../support/test_helper.js');
|
||||
|
||||
var assert = require('assert');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var errorMiddleware = require('../../../lib/cartodb/middleware/error-middleware');
|
||||
|
||||
describe('BaseController', function() {
|
||||
describe('error-middleware', function() {
|
||||
|
||||
it('different formats for postgis plugin error returns 400 as status code', function() {
|
||||
|
||||
var expectedStatusCode = 400;
|
||||
assert.equal(
|
||||
BaseController.findStatusCode("Postgis Plugin: ERROR: column \"missing\" does not exist\n"),
|
||||
errorMiddleware.findStatusCode("Postgis Plugin: ERROR: column \"missing\" does not exist\n"),
|
||||
expectedStatusCode,
|
||||
"Error status code for single line does not match"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
BaseController.findStatusCode("Postgis Plugin: PSQL error:\nERROR: column \"missing\" does not exist\n"),
|
||||
errorMiddleware.findStatusCode("Postgis Plugin: PSQL error:\nERROR: column \"missing\" does not exist\n"),
|
||||
expectedStatusCode,
|
||||
"Error status code for multiline/PSQL does not match"
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user