Compare commits
453 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
838a3464c1 | ||
|
|
0906ae3c93 | ||
|
|
fefff3b788 | ||
|
|
d5e985fde5 | ||
|
|
ed27f980c2 | ||
|
|
b6afad1787 | ||
|
|
7807ea5f8c | ||
|
|
a830eb4ea0 | ||
|
|
a4ec3ad6da | ||
|
|
b7c10c95d3 | ||
|
|
05ef43c342 | ||
|
|
c235754df2 | ||
|
|
70dab149ba | ||
|
|
ed4b44a78a | ||
|
|
54f113ab5f | ||
|
|
60bf81d950 | ||
|
|
bdfd58f468 | ||
|
|
de1aaf3808 | ||
|
|
769aee1107 | ||
|
|
3663e6d12a | ||
|
|
a6d9984453 | ||
|
|
4e8cf136c8 | ||
|
|
f49d7478d7 | ||
|
|
b4a1c9d648 | ||
|
|
692246ec44 | ||
|
|
48a7d28aa6 | ||
|
|
04146f897d | ||
|
|
a34658c97f | ||
|
|
cbfeb0158e | ||
|
|
8d37e00869 | ||
|
|
584d6ae9cf | ||
|
|
421e611356 | ||
|
|
f078713d28 | ||
|
|
a8d31d52cf | ||
|
|
d9213b2fe2 | ||
|
|
091efe52fc | ||
|
|
a5c508733a | ||
|
|
ce944d9a7d | ||
|
|
8321b5adba | ||
|
|
667c972308 | ||
|
|
3dbe05be3a | ||
|
|
2d4ce19250 | ||
|
|
3b3e0c0acd | ||
|
|
c3ddb933bb | ||
|
|
6aae60ece7 | ||
|
|
6b3dc8ece0 | ||
|
|
7dd231a8c9 | ||
|
|
35a3219012 | ||
|
|
7598e6ab4b | ||
|
|
bbedc5f41b | ||
|
|
e1a2c45b19 | ||
|
|
8fa801e032 | ||
|
|
75870dc6c1 | ||
|
|
42900b5d0e | ||
|
|
c1423d77ff | ||
|
|
0e43c54214 | ||
|
|
2cc4161239 | ||
|
|
fc8f3fdf27 | ||
|
|
24b76208ac | ||
|
|
0de272b195 | ||
|
|
7faf40004c | ||
|
|
88ae2d473a | ||
|
|
337b47685c | ||
|
|
ed3f9be655 | ||
|
|
248c6d5f22 | ||
|
|
dd4aa09d21 | ||
|
|
132f2226ca | ||
|
|
2eb6e95fed | ||
|
|
db8130be4f | ||
|
|
379b649e95 | ||
|
|
e5619492ef | ||
|
|
cc76ccc626 | ||
|
|
3d6512dd11 | ||
|
|
b19d97e01f | ||
|
|
25931a618b | ||
|
|
ffab576399 | ||
|
|
e7067ab9cf | ||
|
|
7cfcf6d579 | ||
|
|
a4b586055a | ||
|
|
5ad1e1b645 | ||
|
|
01ed513a79 | ||
|
|
504f68b8aa | ||
|
|
cbb08f5642 | ||
|
|
a4b5d681ce | ||
|
|
02f93f3a14 | ||
|
|
ad2f4573f8 | ||
|
|
06604cd738 | ||
|
|
089be35b5d | ||
|
|
bbcb335d60 | ||
|
|
6ef2e0bb5f | ||
|
|
d8202d881d | ||
|
|
aae814a156 | ||
|
|
49bcc5368d | ||
|
|
555e04f9e7 | ||
|
|
3f6f2e4e23 | ||
|
|
abffc4b067 | ||
|
|
363cb0b679 | ||
|
|
d26910ba9c | ||
|
|
74b2f305ea | ||
|
|
6c2f893651 | ||
|
|
faaf121eb6 | ||
|
|
83ab65163d | ||
|
|
9dcd5ff332 | ||
|
|
c6635f63c1 | ||
|
|
56213219e4 | ||
|
|
7c2dc20dbe | ||
|
|
c8e8317ea4 | ||
|
|
8509796743 | ||
|
|
90aaed0f2c | ||
|
|
48be15b742 | ||
|
|
a95b3f2f99 | ||
|
|
b2cc7ab84f | ||
|
|
eb3414f07f | ||
|
|
292dad130d | ||
|
|
ec41cddb19 | ||
|
|
5871f8290d | ||
|
|
33089be2cd | ||
|
|
d351c8d14c | ||
|
|
82446e5ffa | ||
|
|
b786164e8a | ||
|
|
f9cbb3aac8 | ||
|
|
a66c19c6c7 | ||
|
|
94d1667d70 | ||
|
|
3399db1cff | ||
|
|
874ea99d19 | ||
|
|
7022fb87b4 | ||
|
|
7c1e2a6af0 | ||
|
|
2f011c3266 | ||
|
|
4762aa0897 | ||
|
|
f30f83331f | ||
|
|
3695e1e3e5 | ||
|
|
585b5929aa | ||
|
|
0185cdf785 | ||
|
|
8d22ca66ba | ||
|
|
b0eacb2a79 | ||
|
|
9b40370794 | ||
|
|
95f3d58383 | ||
|
|
0f0cde1093 | ||
|
|
e679366dac | ||
|
|
ca56df5cfe | ||
|
|
d8a4209768 | ||
|
|
40712a2e62 | ||
|
|
acb9ce33b1 | ||
|
|
5e43a7145a | ||
|
|
39bd6694f2 | ||
|
|
5de8c4f9c3 | ||
|
|
31a554d94f | ||
|
|
9bc9fc46ff | ||
|
|
4274e06795 | ||
|
|
a2bf235553 | ||
|
|
1b18b2b188 | ||
|
|
9c27447b17 | ||
|
|
f03d98cd0d | ||
|
|
6331bebb30 | ||
|
|
fdd4c4aaa0 | ||
|
|
4dd404771e | ||
|
|
bf267e9c95 | ||
|
|
843f70cdba | ||
|
|
42e0e07c14 | ||
|
|
dfdd2b9043 | ||
|
|
8656fcd8d1 | ||
|
|
f2f6b9d49c | ||
|
|
82f1e6753b | ||
|
|
7ed717607a | ||
|
|
0ec9491d21 | ||
|
|
416970c819 | ||
|
|
ccc28f3617 | ||
|
|
5bac36b30f | ||
|
|
ef3ffddec7 | ||
|
|
e6ba467d98 | ||
|
|
314508bcd8 | ||
|
|
da18506e41 | ||
|
|
5eaee0b71e | ||
|
|
bd93e7dc7e | ||
|
|
2c762813ba | ||
|
|
136c6fa70b | ||
|
|
67b2343571 | ||
|
|
3caa1d9c4a | ||
|
|
b0c924ca03 | ||
|
|
f6f59023b4 | ||
|
|
9dc4e7c955 | ||
|
|
faa44e54ae | ||
|
|
bfb743b851 | ||
|
|
dad2e92dd3 | ||
|
|
59c312ea40 | ||
|
|
48c5a458f3 | ||
|
|
c0830862c8 | ||
|
|
42deb7abbe | ||
|
|
62deda6470 | ||
|
|
3e0981978a | ||
|
|
35e5170907 | ||
|
|
8eba5dcc01 | ||
|
|
5c2248d419 | ||
|
|
335d91b42d | ||
|
|
102b11b1b5 | ||
|
|
26df09b13f | ||
|
|
254991c56c | ||
|
|
a492ab0143 | ||
|
|
b0d63b2ec0 | ||
|
|
85b0c63eb0 | ||
|
|
98a92f51e6 | ||
|
|
ae50dbd47c | ||
|
|
a97e628520 | ||
|
|
b48dcc1418 | ||
|
|
8867cdbc02 | ||
|
|
f03ee4b836 | ||
|
|
90418b204e | ||
|
|
918674e01a | ||
|
|
7b44b7d559 | ||
|
|
612b11cbe8 | ||
|
|
b34f05690c | ||
|
|
9b01a05727 | ||
|
|
d0024409df | ||
|
|
91856372f0 | ||
|
|
2937b6a804 | ||
|
|
b76a8249fa | ||
|
|
db09476137 | ||
|
|
08a5e57180 | ||
|
|
7464d827fe | ||
|
|
ae0ec159e1 | ||
|
|
2a1c08da65 | ||
|
|
faab174a79 | ||
|
|
521b441da5 | ||
|
|
59ca00b33b | ||
|
|
6564ed69d8 | ||
|
|
fadd9032c6 | ||
|
|
80918f5b9b | ||
|
|
e061b3e631 | ||
|
|
06ec3f80b9 | ||
|
|
e6011287f4 | ||
|
|
a0f560ca1a | ||
|
|
7c7d606aa7 | ||
|
|
46587e3cf1 | ||
|
|
603ef4044c | ||
|
|
2e3abfb2cd | ||
|
|
47ccb7ded8 | ||
|
|
98907a886c | ||
|
|
7e14247ea9 | ||
|
|
e103427750 | ||
|
|
95f55b00b3 | ||
|
|
e519984790 | ||
|
|
fa3223777f | ||
|
|
eeb4966294 | ||
|
|
5823859b2a | ||
|
|
7b21bd26d0 | ||
|
|
4ac224688c | ||
|
|
4742e7f64f | ||
|
|
a66f127828 | ||
|
|
e84d88b7a3 | ||
|
|
cda2616a8a | ||
|
|
11aa4d12bd | ||
|
|
18dbeea003 | ||
|
|
3e916c6054 | ||
|
|
fc420c2c0f | ||
|
|
7b9d653c46 | ||
|
|
140441b777 | ||
|
|
5db0e9c8d8 | ||
|
|
63d1c19263 | ||
|
|
018cd25593 | ||
|
|
963737d3fb | ||
|
|
c059f44bf1 | ||
|
|
890f0d1ef6 | ||
|
|
5fca005a3f | ||
|
|
86d4f8e219 | ||
|
|
6b0ab45e63 | ||
|
|
0b475ab5e2 | ||
|
|
97972ac73f | ||
|
|
c3b38b2f60 | ||
|
|
b4e06ec1ac | ||
|
|
d0a8bd428f | ||
|
|
251fe96509 | ||
|
|
15bf74f770 | ||
|
|
32986e3ebd | ||
|
|
f106f27df4 | ||
|
|
3539b658fb | ||
|
|
abf33a1c68 | ||
|
|
fc6790ea1e | ||
|
|
ac6f0e1c67 | ||
|
|
041cd40ec2 | ||
|
|
8721f56269 | ||
|
|
1d3045c799 | ||
|
|
52a1ed869c | ||
|
|
04f60baec5 | ||
|
|
a8de436424 | ||
|
|
ee7917676b | ||
|
|
c7780e9f42 | ||
|
|
3edd7b8b01 | ||
|
|
455202cd1a | ||
|
|
8bdb82c7be | ||
|
|
fa503ee66a | ||
|
|
e1a2ee2381 | ||
|
|
b82d26527a | ||
|
|
1c50dd6b48 | ||
|
|
b0e9df1400 | ||
|
|
6ebf51ce45 | ||
|
|
d9a34f3384 | ||
|
|
8136a1e136 | ||
|
|
41f3606572 | ||
|
|
ea0542dcb1 | ||
|
|
a4dbc1bac2 | ||
|
|
065f56e161 | ||
|
|
6b5d6648de | ||
|
|
95538707c9 | ||
|
|
4c76a921b1 | ||
|
|
85c1c987af | ||
|
|
bde86323fd | ||
|
|
880e3f388d | ||
|
|
c1535b1a12 | ||
|
|
232ff1ba33 | ||
|
|
1b63dcd4e5 | ||
|
|
b32a0a6547 | ||
|
|
d634be0c30 | ||
|
|
f9fe3ace37 | ||
|
|
6cd8131888 | ||
|
|
0ea76f7d15 | ||
|
|
51e5b5c255 | ||
|
|
cedcc094e6 | ||
|
|
bbe8d4e820 | ||
|
|
5aa98c4ab2 | ||
|
|
d6a9103779 | ||
|
|
2e7784ddf2 | ||
|
|
086be461b2 | ||
|
|
a7157532f1 | ||
|
|
55fd660d69 | ||
|
|
80604b739a | ||
|
|
d88fbbaa87 | ||
|
|
7db0744f67 | ||
|
|
d1fcd797a3 | ||
|
|
150c6ee4be | ||
|
|
d0df8b1533 | ||
|
|
43e1de31fa | ||
|
|
33ed9ab47d | ||
|
|
749a08336a | ||
|
|
467097b3cc | ||
|
|
487aca52d0 | ||
|
|
072956addd | ||
|
|
781d2d3a28 | ||
|
|
2a767cdb83 | ||
|
|
e3cf69ac1a | ||
|
|
27b5420358 | ||
|
|
7641542e67 | ||
|
|
debb174af4 | ||
|
|
2bd4c9e814 | ||
|
|
0dc7872256 | ||
|
|
1e56ba1de9 | ||
|
|
6f4e338dcb | ||
|
|
941ebf7d80 | ||
|
|
c38bf6ade8 | ||
|
|
44c4db93da | ||
|
|
f644b3a226 | ||
|
|
7c9b4b7283 | ||
|
|
8c839e214d | ||
|
|
99421b613c | ||
|
|
bc7a556297 | ||
|
|
2c703e5c16 | ||
|
|
220f1d6a73 | ||
|
|
e68ba95fed | ||
|
|
83d00a8aca | ||
|
|
767dde0b1e | ||
|
|
da32d96607 | ||
|
|
76da828168 | ||
|
|
479e8970a1 | ||
|
|
068c242148 | ||
|
|
e4c409f9a5 | ||
|
|
00ffd75781 | ||
|
|
cf5e797f90 | ||
|
|
128ab53c55 | ||
|
|
ce4050e3e3 | ||
|
|
b82767c60d | ||
|
|
0fdab08600 | ||
|
|
4ba2632a92 | ||
|
|
d9e66c5964 | ||
|
|
72bebf1960 | ||
|
|
3fa2869665 | ||
|
|
e57c4c824b | ||
|
|
8e68e5395d | ||
|
|
0236935212 | ||
|
|
86e20b4b26 | ||
|
|
86d58fea7b | ||
|
|
9934d69736 | ||
|
|
ae48a01e26 | ||
|
|
4d11403be2 | ||
|
|
bcd14e4f77 | ||
|
|
60d2cc0a4f | ||
|
|
5e53920aae | ||
|
|
9c556964e5 | ||
|
|
d292a922f6 | ||
|
|
c016175a23 | ||
|
|
1b85951e06 | ||
|
|
a4e98163fb | ||
|
|
99324b15ef | ||
|
|
e34410fd2c | ||
|
|
cef7545c17 | ||
|
|
de8ed27207 | ||
|
|
0cfb204c04 | ||
|
|
fc82ca7490 | ||
|
|
183c8291bc | ||
|
|
d908ffdbca | ||
|
|
00a4f481f6 | ||
|
|
e0bd042bde | ||
|
|
f881efdc11 | ||
|
|
bda5022811 | ||
|
|
d5b5ef584d | ||
|
|
2cda43dc8d | ||
|
|
f7f513a61a | ||
|
|
940c982b68 | ||
|
|
d949d1c27f | ||
|
|
aa1d411fb8 | ||
|
|
f297374449 | ||
|
|
060b93c314 | ||
|
|
3ceeaedf02 | ||
|
|
c6ba9e6102 | ||
|
|
bf40b240d3 | ||
|
|
5d4d2bddd6 | ||
|
|
95dfd87c96 | ||
|
|
eab9e8846e | ||
|
|
788bc302a0 | ||
|
|
1ba240d099 | ||
|
|
ee0405da1e | ||
|
|
5e9b326d03 | ||
|
|
1f30367e59 | ||
|
|
26a2f73c2a | ||
|
|
60005e2f7f | ||
|
|
1c7da2c4b3 | ||
|
|
3799dd2574 | ||
|
|
7efb2a2344 | ||
|
|
49c97e2cf2 | ||
|
|
41e65a9633 | ||
|
|
feae766e62 | ||
|
|
e3bdeec8ca | ||
|
|
80c4207c74 | ||
|
|
80e4306fbc | ||
|
|
543d257a20 | ||
|
|
8a023e3d2f | ||
|
|
f13b45862d | ||
|
|
731fe4c00f | ||
|
|
500cbb959f | ||
|
|
108a319143 | ||
|
|
ef5ea5b4cb | ||
|
|
dfef7ff3c0 | ||
|
|
adb9e55fb2 | ||
|
|
5d3726de44 | ||
|
|
f186e4736b | ||
|
|
a00c2b1eef | ||
|
|
64d601179d | ||
|
|
cf2b73e473 | ||
|
|
70932c23df | ||
|
|
519d49bd10 | ||
|
|
bf814c4442 | ||
|
|
f136993c50 | ||
|
|
ba008ab518 | ||
|
|
e4ed6ee1cc | ||
|
|
fda7661dad |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ redis.pid
|
||||
*.log
|
||||
coverage/
|
||||
.DS_Store
|
||||
libredis_cell.so
|
||||
|
||||
@@ -5,10 +5,10 @@ services:
|
||||
- docker
|
||||
|
||||
before_install:
|
||||
- docker pull cartoimages/windshaft-carto-testing
|
||||
- docker pull cartoimages/windshaft-testing
|
||||
|
||||
script:
|
||||
- docker run -e POSTGIS_VERSION=2.4 -v `pwd`:/srv cartoimages/windshaft-carto-testing
|
||||
- docker run -e POSTGIS_VERSION=2.4 -v `pwd`:/srv cartoimages/windshaft-testing bash docker-test.sh
|
||||
|
||||
language: generic
|
||||
|
||||
|
||||
94
NEWS.md
94
NEWS.md
@@ -1,5 +1,98 @@
|
||||
# Changelog
|
||||
|
||||
## 6.0.0
|
||||
Released 2018-03-19
|
||||
Backward incompatible changes:
|
||||
- Needs Redis v4
|
||||
|
||||
New features:
|
||||
- Upgrades camshaft to 0.61.8
|
||||
- Upgrades cartodb-redis to 1.0.0
|
||||
- Rate limit feature (disabled by default)
|
||||
- Fixes for tests with PG11
|
||||
|
||||
## 5.4.0
|
||||
Released 2018-03-15
|
||||
- Upgrades Windshaft to 4.5.7 ([Mapnik top metrics](https://github.com/CartoDB/Windshaft/pull/597), [AttributesBackend allows multiple features if all the attributes are the same](https://github.com/CartoDB/Windshaft/pull/602))
|
||||
- Implemented middleware to authorize users via new Api Key system
|
||||
- Keep the old authorization system as fallback
|
||||
- Aggregation widget: Remove NULL categories in 'count' aggregations too
|
||||
- Update request to 2.85.0
|
||||
- Update camshaft to 0.61.4 (Fixes for AOI and Merge analyses)
|
||||
- Update windshaft to 4.6.0, which in turn updates @carto/mapnik to 3.6.2-carto.4 and related dependencies. It brings in a cache for rasterized symbols. See https://github.com/CartoDB/node-mapnik/blob/v3.6.2-carto/CHANGELOG.carto.md#362-carto4
|
||||
- PostGIS: Variables in postgis SQL queries must now additionally be wrapped in `!` (refs [#29](https://github.com/CartoDB/mapnik/issues/29), [mapnik/#3618](https://github.com/mapnik/mapnik/pull/3618)):
|
||||
```sql
|
||||
-- Before
|
||||
SELECT ... WHERE trait = @variable
|
||||
|
||||
-- Now
|
||||
SELECT ... WHERE trait = !@variable!
|
||||
```
|
||||
|
||||
## 5.3.1
|
||||
Released 2018-02-13
|
||||
- Improve the speed of the aggregation dataview #865
|
||||
|
||||
## 5.3.0
|
||||
Released 2018-02-12
|
||||
- Upgrades redis-mpool to 0.5.0
|
||||
- Upgrades windshaft to 4.5.2
|
||||
- Upgrades cartodb-redis to 0.15.0
|
||||
- Adds metrics option to the Mapnik renderer
|
||||
- Upgrades camshadft to 0.61.2
|
||||
|
||||
## 5.2.1
|
||||
Released 2018-02-01
|
||||
|
||||
Bug Fixes:
|
||||
- Allow use of aggregation with attributes #861
|
||||
|
||||
## 5.2.0
|
||||
Released 2018-02-01
|
||||
|
||||
Announcements:
|
||||
- Upgrade windshaft to [4.3.3](https://github.com/CartoDB/windshaft/releases/tag/4.3.2) adding support for cache-features' in Mapnik/CartoDB layers.
|
||||
|
||||
## 5.1.0
|
||||
Released 2018-01-30
|
||||
New features:
|
||||
- Now mapnik has support for fine-grained metrics.
|
||||
- Variables can be passed for later substitution in postgis datasource.
|
||||
|
||||
Announcements:
|
||||
- Upgrade windshaft to [4.3.1](https://github.com/CartoDB/windshaft/releases/tag/4.3.1). Underneath it upgrades mapnik and all the related dependencies.
|
||||
|
||||
## 5.0.1
|
||||
Released 2018-01-29
|
||||
|
||||
Bug Fixes:
|
||||
- Allow aggregation for queries with no the_geom (only the_geom_webmercator) #856
|
||||
|
||||
## 5.0.0
|
||||
Released 2018-01-29
|
||||
|
||||
Backward incompatible changes:
|
||||
- Aggregation dataview returns categories with the same type as the database type. For example, if we are aggretating by a numeric field, the resulting JSON will contain a number instead of a stringified number.
|
||||
|
||||
## 4.8.0
|
||||
Released 2018-01-04
|
||||
|
||||
New features:
|
||||
- Return url template in metadata #838.
|
||||
|
||||
Bux fixes:
|
||||
- Tests: Order torque objects before comparison
|
||||
|
||||
## 4.7.0
|
||||
Released 2018-01-03
|
||||
|
||||
New features:
|
||||
- Return tilejson in metadata #837.
|
||||
|
||||
Bug fixes:
|
||||
- Allow to create vector map-config for layers that doesn't have points. Layers with lines or polygons won't be aggregated by default.
|
||||
|
||||
|
||||
## 4.6.0
|
||||
Released 2018-01-02
|
||||
|
||||
@@ -9,7 +102,6 @@ Announcements:
|
||||
- Fix column names collisions in histograms [#828](https://github.com/CartoDB/Windshaft-cartodb/pull/828).
|
||||
- Add full-sample aggregation support for vector map-config.
|
||||
|
||||
|
||||
## 4.5.0
|
||||
Released 2017-12-19
|
||||
|
||||
|
||||
4
app.js
4
app.js
@@ -4,6 +4,7 @@ var path = require('path');
|
||||
var fs = require('fs');
|
||||
var _ = require('underscore');
|
||||
var semver = require('semver');
|
||||
const setICUEnvVariable = require('./lib/cartodb/utils/icu_data_env_setter');
|
||||
|
||||
// jshint undef:false
|
||||
var log = console.log.bind(console);
|
||||
@@ -16,6 +17,9 @@ if (!semver.satisfies(nodejsVersion, '>=6.9.0')) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// This function should be called before the require('yargs').
|
||||
setICUEnvVariable();
|
||||
|
||||
var argv = require('yargs')
|
||||
.usage('Usage: $0 <environment> [options]')
|
||||
.help('h')
|
||||
|
||||
@@ -204,8 +204,13 @@ var config = {
|
||||
// which cost is no more expensive than snapping and results are
|
||||
// much closer to the original geometry
|
||||
removeRepeatedPoints: false // this requires postgis >=2.2
|
||||
}
|
||||
},
|
||||
// If enabled Mapnik will reuse the features retrieved from the database
|
||||
// instead of requesting them once per style inside a layer
|
||||
'cache-features': true,
|
||||
|
||||
// Require metrics to the renderer
|
||||
metrics: false
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
@@ -338,7 +343,28 @@ var config = {
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: true
|
||||
layerStats: true,
|
||||
// whether it should rate limit endpoints (global configuration)
|
||||
rateLimitsEnabled: false,
|
||||
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
|
||||
rateLimitsByEndpoint: {
|
||||
anonymous: false,
|
||||
static: false,
|
||||
static_named: false,
|
||||
dataview: false,
|
||||
dataview_search: false,
|
||||
analysis: false,
|
||||
analysis_catalog: false,
|
||||
tile: false,
|
||||
attributes: false,
|
||||
named_list: false,
|
||||
named_create: false,
|
||||
named_get: false,
|
||||
named: false,
|
||||
named_update: false,
|
||||
named_delete: false,
|
||||
named_tiles: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -198,7 +198,14 @@ var config = {
|
||||
// which cost is no more expensive than snapping and results are
|
||||
// much closer to the original geometry
|
||||
removeRepeatedPoints: false // this requires postgis >=2.2
|
||||
}
|
||||
},
|
||||
|
||||
// If enabled Mapnik will reuse the features retrieved from the database
|
||||
// instead of requesting them once per style inside a layer
|
||||
'cache-features': true,
|
||||
|
||||
// Require metrics to the renderer
|
||||
metrics: false
|
||||
|
||||
},
|
||||
http: {
|
||||
@@ -338,7 +345,28 @@ var config = {
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: false
|
||||
layerStats: false,
|
||||
// whether it should rate limit endpoints (global configuration)
|
||||
rateLimitsEnabled: false,
|
||||
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
|
||||
rateLimitsByEndpoint: {
|
||||
anonymous: false,
|
||||
static: false,
|
||||
static_named: false,
|
||||
dataview: false,
|
||||
dataview_search: false,
|
||||
analysis: false,
|
||||
analysis_catalog: false,
|
||||
tile: false,
|
||||
attributes: false,
|
||||
named_list: false,
|
||||
named_create: false,
|
||||
named_get: false,
|
||||
named: false,
|
||||
named_update: false,
|
||||
named_delete: false,
|
||||
named_tiles: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -198,7 +198,14 @@ var config = {
|
||||
// which cost is no more expensive than snapping and results are
|
||||
// much closer to the original geometry
|
||||
removeRepeatedPoints: false // this requires postgis >=2.2
|
||||
}
|
||||
},
|
||||
|
||||
// If enabled Mapnik will reuse the features retrieved from the database
|
||||
// instead of requesting them once per style inside a layer
|
||||
'cache-features': true,
|
||||
|
||||
// Require metrics to the renderer
|
||||
metrics: false
|
||||
|
||||
},
|
||||
http: {
|
||||
@@ -338,7 +345,28 @@ var config = {
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: true
|
||||
layerStats: true,
|
||||
// whether it should rate limit endpoints (global configuration)
|
||||
rateLimitsEnabled: false,
|
||||
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
|
||||
rateLimitsByEndpoint: {
|
||||
anonymous: false,
|
||||
static: false,
|
||||
static_named: false,
|
||||
dataview: false,
|
||||
dataview_search: false,
|
||||
analysis: false,
|
||||
analysis_catalog: false,
|
||||
tile: false,
|
||||
attributes: false,
|
||||
named_list: false,
|
||||
named_create: false,
|
||||
named_get: false,
|
||||
named: false,
|
||||
named_update: false,
|
||||
named_delete: false,
|
||||
named_tiles: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -197,7 +197,14 @@ var config = {
|
||||
// which cost is no more expensive than snapping and results are
|
||||
// much closer to the original geometry
|
||||
removeRepeatedPoints: false // this requires postgis >=2.2
|
||||
}
|
||||
},
|
||||
|
||||
// If enabled Mapnik will reuse the features retrieved from the database
|
||||
// instead of requesting them once per style inside a layer
|
||||
'cache-features': true,
|
||||
|
||||
// Require metrics to the renderer
|
||||
metrics: false
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
@@ -332,7 +339,28 @@ var config = {
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: true
|
||||
layerStats: true,
|
||||
// whether it should rate limit endpoints (global configuration)
|
||||
rateLimitsEnabled: false,
|
||||
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
|
||||
rateLimitsByEndpoint: {
|
||||
anonymous: false,
|
||||
static: false,
|
||||
static_named: false,
|
||||
dataview: false,
|
||||
dataview_search: false,
|
||||
analysis: false,
|
||||
analysis_catalog: false,
|
||||
tile: false,
|
||||
attributes: false,
|
||||
named_list: false,
|
||||
named_create: false,
|
||||
named_get: false,
|
||||
named: false,
|
||||
named_update: false,
|
||||
named_delete: false,
|
||||
named_tiles: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
62
docs/MapConfig-Aggregation-extension.md
Normal file
62
docs/MapConfig-Aggregation-extension.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# 1. Purpose
|
||||
|
||||
This specification describes an extension for
|
||||
[MapConfig 1.7.0](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.7.0.md) version.
|
||||
|
||||
|
||||
# 2. Changes over specification
|
||||
|
||||
This extension introduces a new layer options for aggregated data tile generation.
|
||||
|
||||
## 2.1 Aggregation options
|
||||
|
||||
The layer options attribute is extended with a new optional `aggregation` attribute.
|
||||
The value of this attribute can be `false` to explicitly disable aggregation for the layer.
|
||||
|
||||
```javascript
|
||||
{
|
||||
aggregation: {
|
||||
|
||||
// OPTIONAL
|
||||
// string, defines the placement of aggregated geometries. Can be one of:
|
||||
// * "point-sample", the default places geometries at a sample point (one of the aggregated geometries)
|
||||
// * "point-grid" places geometries at the center of the aggregation grid cells
|
||||
// * "centroid" places geometriea at the average position of the aggregated points
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/aggregation.md#placement for more details
|
||||
placement: "point-sample",
|
||||
|
||||
// OPTIONAL
|
||||
// object, defines the columns of the aggregated datasets. Each property corresponds to a columns name and
|
||||
// should contain an object with two properties: "aggregate_function" (one of "sum", "max", "min", "avg", "mode" or "count"),
|
||||
// and "aggregated_column" (the name of a column of the original layer query or "*")
|
||||
// A column defined as `"_cdb_features_count": {"aggregate_function": "count", aggregated_column: "*"}`
|
||||
// is always generated in addition to the defined columns.
|
||||
// The column names `cartodb_id`, `the_geom`, `the_geom_webmercator` and `_cdb_feature_count` cannot be used
|
||||
// for aggregated columns, as they correspond to columns always present in the result.
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/aggregation.md#columns for more details
|
||||
columns: {
|
||||
"aggregated_column_1": {
|
||||
"aggregate_function": "sum",
|
||||
"aggregated_column": "original_column_1"
|
||||
}
|
||||
},
|
||||
|
||||
// OPTIONAL
|
||||
// Number, defines the cell-size of the spatial aggregation grid as a pixel resolution power of two (1/4, 1/2,... 2, 4, 16)
|
||||
// to scale from 256x256 pixels; the default is 1 corresponding to 256x256 cells per tile.
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/aggregation.md#resolution for more details
|
||||
resolution: 1,
|
||||
|
||||
// OPTIONAL
|
||||
// Number, the minimum number of (estimated) rows in the dataset (query results) for aggregation to be applied.
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/aggregation.md#threshold for more details
|
||||
threshold: 500000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# History
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Initial version
|
||||
187
docs/aggregation.md
Normal file
187
docs/aggregation.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Tile Aggregation
|
||||
|
||||
To be able to represent a large amount of data (say, hundred of thousands to millions of points) in a tile. This can be useful both for raster tiles (where the aggregation reduces the number of features to be rendered) and vector tiles (the tile contais less features).
|
||||
|
||||
Aggregation is available only for point geometries. During aggregation the points are grouped using a grid; all the points laying in the same cell of the grid are summarized in a single aggregated result point.
|
||||
- The position of the aggregated point is controlled by the `placement` parameter.
|
||||
- The aggregated rows always contain at least a column, named `_cdb_feature_count`, which contains the number of the original points that the aggregated point represents.
|
||||
|
||||
### Special default aggregation
|
||||
|
||||
When no placement or columns are specified a special default aggregation is performed.
|
||||
|
||||
This special mode performs only spatial aggregation (using a grid defined by the requested tile and the resolution, parameter, as all the other cases), and returns a _random_ record from each group (grid cell) with all its columns and an additional `_cdb_features_count` with the number of features in the group.
|
||||
|
||||
Regarding the randomness of the sample: currently we use the row with the minimum `cartodb_id` value in each group.
|
||||
|
||||
The rationale behind having this special aggregation with all the original columns is to provide a mostly transparent way to handle large datasets without having to provide special map configurations for those cases (i.e. preserving the logic used to produce the maps with smaller datasets). [Overviews have been used so far with this intent](https://carto.com/docs/tips-and-tricks/back-end-data-performance/), but they are inflexible.
|
||||
|
||||
### User defined aggregations
|
||||
|
||||
When either a explicit placement or columns are requested we no longer use the special, query; we use one determined by the placement (which will default to "centroid"), and it will have as columns only the aggregated columns specified, in addition to `_cdb_features_count`, which is always present.
|
||||
|
||||
We might decide in the future to allow sampling column values for any of the different placement modes.
|
||||
|
||||
### Behaviour for raster and vector tiles
|
||||
|
||||
The vector tiles from a vector-only map will be aggregated by default.
|
||||
However, Raster tiles (or vector tiles from a map which defines CartoCSS styles) will be aggregated only upon request.
|
||||
|
||||
Aggregation that would otherwise occur can be disabled by passing an `aggregation=false` parameter to the map instantiation HTTP call.
|
||||
|
||||
To control how aggregation is performed, an aggregation option can be added to the layer:
|
||||
|
||||
```json
|
||||
{
|
||||
"layers": [
|
||||
{
|
||||
"options": {
|
||||
"sql": "SELECT * FROM data",
|
||||
"aggregation": {
|
||||
"placement": "centroid",
|
||||
"columns": {
|
||||
"value": {
|
||||
"aggregate_function": "sum",
|
||||
"aggregated_column": "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Even if aggregation is explicitly requested it may not be activated, e.g., if the geometries are not points
|
||||
or the whole dataset is too small. The map instantiation response contains metadata that informs if any particular
|
||||
layer will be aggregated when tiles are requested, both for vector (mvt) and raster (png) tiles.
|
||||
|
||||
```json
|
||||
{
|
||||
"layergroupid": "7b97b6e76590fef889b63edd2efb1c79:1513608333045",
|
||||
"metadata": {
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"id": "layer0",
|
||||
"meta": {
|
||||
"stats": {
|
||||
"estimatedFeatureCount": 6232136
|
||||
},
|
||||
"aggregation": {
|
||||
"png": true,
|
||||
"mvt": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Aggregation parameters
|
||||
|
||||
The aggregation parameters for a layer are defined inside an `aggregation` option of the layer:
|
||||
|
||||
```json
|
||||
{
|
||||
"layers": [
|
||||
{
|
||||
"options": {
|
||||
"sql": "SELECT * FROM data",
|
||||
"aggregation": {"...": "..."}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `placement`
|
||||
|
||||
Determines the kind of aggregated geometry generated:
|
||||
|
||||
#### `point-sample`
|
||||
|
||||
This is the default placement. It will place the aggregated point at a random sample of the grouped points,
|
||||
like the default aggregation does. No other attribute is sampled, though, the point will contain the aggregated attributes determined by the `columns` parameter.
|
||||
|
||||
#### `point-grid`
|
||||
|
||||
Generates points at the center of the aggregation grid cells (squares).
|
||||
|
||||
#### `centroid`
|
||||
|
||||
Generates points with the averaged coordinated of the grouped points (i.e. the points inside each grid cell).
|
||||
|
||||
### `columns`
|
||||
|
||||
The aggregated attributes defined by `columns` are computed by a applying an _aggregate function_ to all the points in each group.
|
||||
Valid aggregate functions are `sum`, `avg` (average), `min` (minimum), `max` (maximum) and `mode` (the most frequent value in the group).
|
||||
The values to be aggregated are defined by the _aggregated column_ of the source data. The column keys define the name of the resulting column in the aggregated dataset.
|
||||
|
||||
For example here we define three aggregate attributes named `total`, `max_price` and `price` which are all computed with the same column, `price`,
|
||||
of the original dataset applying three different aggregate functions.
|
||||
|
||||
```json
|
||||
{
|
||||
"columns": {
|
||||
"total": { "aggregate_function": "sum", "aggregated_column": "price" },
|
||||
"max_price": { "aggregate_function": "max", "aggregated_column": "price" },
|
||||
"price": { "aggregate_function": "avg", "aggregated_column": "price" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Note that you can use the original column names as names of the result, but all the result column names must be unique. In particular, the names `cartodb_id`, `the_geom`, `the_geom_webmercator` and `_cdb_feature_count` cannot be used for aggregated columns, as they correspond to columns always present in the result.
|
||||
|
||||
### `resolution`
|
||||
|
||||
Defines the cell-size of the spatial aggregation grid. This is equivalent to the [CartoCSS `-torque-resolution`](https://carto.com/docs/carto-engine/cartocss/properties-for-torque/#-torque-resolution-float) property of Torque maps.
|
||||
|
||||
The aggregation cells are `resolution`×`resolution` pixels in size, where pixels here are defined to be 1/256 of the (linear) size of a tile.
|
||||
The default value is 1, so that aggregation coincides with raster pixels. A value of 2 would make each cell to be 4 (2×2) pixels, and a value of
|
||||
0.5 would yield 4 cells per pixel. In teneral values less than 1 produce sub-pixel precision.
|
||||
|
||||
> Note that is independent of the number of pixels for raster tile or the coordinate resolution (mvt_extent) of vector tiles.
|
||||
|
||||
|
||||
### `threshold`
|
||||
|
||||
This is the minimum number of (estimated) rows in the dataset (query results) for aggregation to be applied. If the number of rows estimate is less than the threshold aggregation will be disabled for the layer; the instantiation response will reflect that and tiles will be generated without aggregation.
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.7.0",
|
||||
"extent": [-20037508.5, -20037508.5, 20037508.5, 20037508.5],
|
||||
"srid": 3857,
|
||||
"maxzoom": 18,
|
||||
"minzoom": 3,
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select * from table",
|
||||
"cartocss": "#table { marker-width: [total]; marker-fill: ramp(value, (red, green, blue), jenks); }",
|
||||
"cartocss_version": "2.3.0",
|
||||
"aggregation": {
|
||||
"placement": "centroid",
|
||||
"columns": {
|
||||
"value": {
|
||||
"aggregate_function": "avg",
|
||||
"aggregated_column": "value"
|
||||
},
|
||||
"total": {
|
||||
"aggregate_function": "sum",
|
||||
"aggregated_column": "value"
|
||||
}
|
||||
},
|
||||
"resolution": 2,
|
||||
"threshold": 500000
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -42,6 +42,13 @@ updated_at | The ISO date of the last time the data involved in the query was up
|
||||
metadata | Includes information about the layers.
|
||||
cdn_url | URLs to fetch the data using the best CDN for your zone.
|
||||
|
||||
**Improved response metadata**
|
||||
|
||||
Originally, you needed to concantenate the `layergroupid` with the correct domain and the path for the tiles.
|
||||
Now, for convenience, the layergroup includes the final URLs in two formats:
|
||||
1. Leaflet's urlTemplate alike: useful when working with raster tiles or with libraries with an API similar to Leaflet's one.
|
||||
1. [TileJSON spec](https://github.com/mapbox/tilejson-spec): useful when working with Mapbox GL or any other library that supports TileJSON.
|
||||
|
||||
### Example
|
||||
|
||||
#### Call
|
||||
@@ -62,11 +69,30 @@ curl 'https://{username}.carto.com/api/v1/map' -H 'Content-Type: application/jso
|
||||
"type": "mapnik",
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
],
|
||||
"tilejson": {
|
||||
"raster": {
|
||||
"tilejson": "2.2.0",
|
||||
"tiles": [
|
||||
"http://a.cdb.com/c01a54877c62831bb51720263f91fb33/{z}/{x}/{y}.png",
|
||||
"http://b.cdb.com/c01a54877c62831bb51720263f91fb33/{z}/{x}/{y}.png"
|
||||
]
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raster": {
|
||||
"urlTemplate": "http://{s}.cdb.com/c01a54877c62831bb51720263f91fb33/{z}/{x}/{y}.png",
|
||||
"subdomains": ["a", "b"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
"https": "https://cdb.com",
|
||||
"templates": {
|
||||
"http": { "subdomains": ["a","b"], "url": "http://{s}.cdb.com" },
|
||||
"https": { "subdomains": ["a","b"], "url": "https://{s}.example.com" },
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
var _ = require('underscore'); // AUTH_FALLBACK
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -47,39 +46,113 @@ AuthApi.prototype.authorizedBySigner = function(res, callback) {
|
||||
});
|
||||
};
|
||||
|
||||
function isValidApiKey(apikey) {
|
||||
return apikey.type &&
|
||||
apikey.user &&
|
||||
apikey.databasePassword &&
|
||||
apikey.databaseRole;
|
||||
}
|
||||
|
||||
// Check if a request is authorized by api_key
|
||||
//
|
||||
// @param user
|
||||
// @param req express request object
|
||||
// @param res express response object
|
||||
// @param callback function(err, authorized)
|
||||
// NOTE: authorized is expected to be 0 or 1 (integer)
|
||||
//
|
||||
AuthApi.prototype.authorizedByAPIKey = function(user, req, callback) {
|
||||
var givenKey = req.query.api_key || req.query.map_key;
|
||||
if ( ! givenKey && req.body ) {
|
||||
// check also in request body
|
||||
givenKey = req.body.api_key || req.body.map_key;
|
||||
}
|
||||
if ( ! givenKey ) {
|
||||
return callback(null, 0); // no api key, no authorization...
|
||||
AuthApi.prototype.authorizedByAPIKey = function(user, res, callback) {
|
||||
const apikeyToken = res.locals.api_key;
|
||||
const basicAuthUsername = res.locals.basicAuthUsername;
|
||||
|
||||
if ( ! apikeyToken ) {
|
||||
return callback(null, false); // no api key, no authorization...
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.metadataBackend.getApikey(user, apikeyToken, (err, apikey) => {
|
||||
if (err) {
|
||||
if (isNameNotFoundError(err)) {
|
||||
err.http_status = 404;
|
||||
}
|
||||
|
||||
step(
|
||||
function () {
|
||||
self.metadataBackend.getUserMapKey(user, this);
|
||||
},
|
||||
function checkApiKey(err, val){
|
||||
assert.ifError(err);
|
||||
return val && givenKey === val;
|
||||
},
|
||||
function finish(err, authorized) {
|
||||
callback(err, authorized);
|
||||
return callback(err);
|
||||
}
|
||||
);
|
||||
|
||||
//Remove this block when Auth fallback is not used anymore
|
||||
// AUTH_FALLBACK
|
||||
apikey.databaseRole = composeUserDatabase(apikey);
|
||||
apikey.databasePassword = composeDatabasePassword(apikey);
|
||||
|
||||
if ( !isValidApiKey(apikey)) {
|
||||
const error = new Error('Unauthorized');
|
||||
error.type = 'auth';
|
||||
error.subtype = 'api-key-not-found';
|
||||
error.http_status = 401;
|
||||
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (!usernameMatches(basicAuthUsername, res.locals.user)) {
|
||||
const error = new Error('Forbidden');
|
||||
error.type = 'auth';
|
||||
error.subtype = 'api-key-username-mismatch';
|
||||
error.http_status = 403;
|
||||
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (!apikey.grantsMaps) {
|
||||
const error = new Error('Forbidden');
|
||||
error.type = 'auth';
|
||||
error.subtype = 'api-key-does-not-grant-access';
|
||||
error.http_status = 403;
|
||||
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
return callback(null, true);
|
||||
});
|
||||
};
|
||||
|
||||
//Remove this block when Auth fallback is not used anymore
|
||||
// AUTH_FALLBACK
|
||||
function composeUserDatabase (apikey) {
|
||||
if (shouldComposeUserDatabase(apikey)) {
|
||||
return _.template(global.environment.postgres_auth_user, apikey);
|
||||
}
|
||||
|
||||
return apikey.databaseRole;
|
||||
}
|
||||
|
||||
//Remove this block when Auth fallback is not used anymore
|
||||
// AUTH_FALLBACK
|
||||
function composeDatabasePassword (apikey) {
|
||||
if (shouldComposeDatabasePassword(apikey)) {
|
||||
return global.environment.postgres.password;
|
||||
}
|
||||
|
||||
return apikey.databasePassword;
|
||||
}
|
||||
|
||||
//Remove this block when Auth fallback is not used anymore
|
||||
// AUTH_FALLBACK
|
||||
function shouldComposeDatabasePassword (apikey) {
|
||||
return !apikey.databasePassword && global.environment.postgres.password;
|
||||
}
|
||||
|
||||
//Remove this block when Auth fallback is not used anymore
|
||||
// AUTH_FALLBACK
|
||||
function shouldComposeUserDatabase(apikey) {
|
||||
return !apikey.databaseRole && apikey.user_id && global.environment.postgres_auth_user;
|
||||
}
|
||||
|
||||
function isNameNotFoundError (err) {
|
||||
return err.message && -1 !== err.message.indexOf('name not found');
|
||||
}
|
||||
|
||||
function usernameMatches (basicAuthUsername, requestUsername) {
|
||||
return !(basicAuthUsername && (basicAuthUsername !== requestUsername));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access authorization
|
||||
*
|
||||
@@ -88,51 +161,57 @@ AuthApi.prototype.authorizedByAPIKey = function(user, req, callback) {
|
||||
* @param callback function(err, allowed) is access allowed not?
|
||||
*/
|
||||
AuthApi.prototype.authorize = function(req, res, callback) {
|
||||
var self = this;
|
||||
var user = res.locals.user;
|
||||
|
||||
step(
|
||||
function () {
|
||||
self.authorizedByAPIKey(user, req, this);
|
||||
},
|
||||
function checkApiKey(err, authorized){
|
||||
req.profiler.done('authorizedByAPIKey');
|
||||
assert.ifError(err);
|
||||
this.authorizedByAPIKey(user, res, (err, isAuthorizedByApikey) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// if not authorized by api_key, continue
|
||||
if (!authorized) {
|
||||
// not authorized by api_key, check if authorized by signer
|
||||
return self.authorizedBySigner(res, this);
|
||||
}
|
||||
if (isAuthorizedByApikey) {
|
||||
return this.pgConnection.setDBAuth(user, res.locals, 'regular', function (err) {
|
||||
req.profiler.done('setDBAuth');
|
||||
|
||||
// authorized by api key, login as the given username and stop
|
||||
self.pgConnection.setDBAuth(user, res.locals, function(err) {
|
||||
callback(err, true); // authorized (or error)
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, true);
|
||||
});
|
||||
},
|
||||
function checkSignAuthorized(err, authorized) {
|
||||
}
|
||||
|
||||
this.authorizedBySigner(res, (err, isAuthorizedBySigner) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if ( ! authorized ) {
|
||||
// request not authorized by signer.
|
||||
if (isAuthorizedBySigner) {
|
||||
return this.pgConnection.setDBAuth(user, res.locals, 'master', function (err) {
|
||||
req.profiler.done('setDBAuth');
|
||||
|
||||
// if no signer name was given, let dbparams and
|
||||
// PostgreSQL do the rest.
|
||||
//
|
||||
if ( ! res.locals.signer ) {
|
||||
return callback(null, true); // authorized so far
|
||||
}
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// if signer name was given, return no authorization
|
||||
return callback(null, false);
|
||||
callback(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
self.pgConnection.setDBAuth(user, res.locals, function(err) {
|
||||
req.profiler.done('setDBAuth');
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
}
|
||||
);
|
||||
// if no signer name was given, use default api key
|
||||
if (!res.locals.signer) {
|
||||
return this.pgConnection.setDBAuth(user, res.locals, 'default', function (err) {
|
||||
req.profiler.done('setDBAuth');
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
// if signer name was given, return no authorization
|
||||
return callback(null, false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -11,6 +11,8 @@ function UserLimitsApi(metadataBackend, options) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.options = options || {};
|
||||
this.options.limits = this.options.limits || {};
|
||||
|
||||
this.preprareRateLimit();
|
||||
}
|
||||
|
||||
module.exports = UserLimitsApi;
|
||||
@@ -77,3 +79,13 @@ UserLimitsApi.prototype.getTimeoutRenderLimit = function (username, apiKey, call
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
UserLimitsApi.prototype.preprareRateLimit = function () {
|
||||
if (this.options.limits.rateLimitsEnabled) {
|
||||
this.metadataBackend.loadRateLimitsScript();
|
||||
}
|
||||
};
|
||||
|
||||
UserLimitsApi.prototype.getRateLimit = function (user, endpointGroup, callback) {
|
||||
this.metadataBackend.getRateLimit(user, 'maps', endpointGroup, callback);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var _ = require('underscore');
|
||||
const debug = require('debug')('cachechan');
|
||||
|
||||
function PgConnection(metadataBackend) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
@@ -20,45 +19,85 @@ module.exports = PgConnection;
|
||||
//
|
||||
// @param callback function(err)
|
||||
//
|
||||
PgConnection.prototype.setDBAuth = function(username, params, callback) {
|
||||
var self = this;
|
||||
|
||||
var user_params = {};
|
||||
var auth_user = global.environment.postgres_auth_user;
|
||||
var auth_pass = global.environment.postgres_auth_pass;
|
||||
step(
|
||||
function getId() {
|
||||
self.metadataBackend.getUserId(username, this);
|
||||
},
|
||||
function(err, user_id) {
|
||||
assert.ifError(err);
|
||||
user_params.user_id = user_id;
|
||||
var dbuser = _.template(auth_user, user_params);
|
||||
_.extend(params, {dbuser:dbuser});
|
||||
|
||||
// skip looking up user_password if postgres_auth_pass
|
||||
// doesn't contain the "user_password" label
|
||||
if (!auth_pass || ! auth_pass.match(/\buser_password\b/) ) {
|
||||
return null;
|
||||
PgConnection.prototype.setDBAuth = function(username, params, apikeyType, callback) {
|
||||
if (apikeyType === 'master') {
|
||||
this.metadataBackend.getMasterApikey(username, (err, apikey) => {
|
||||
if (err) {
|
||||
if (isNameNotFoundError(err)) {
|
||||
err.http_status = 404;
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.metadataBackend.getUserDBPass(username, this);
|
||||
},
|
||||
function(err, user_password) {
|
||||
assert.ifError(err);
|
||||
user_params.user_password = user_password;
|
||||
if ( auth_pass ) {
|
||||
var dbpass = _.template(auth_pass, user_params);
|
||||
_.extend(params, {dbpassword:dbpass});
|
||||
params.dbuser = apikey.databaseRole;
|
||||
params.dbpassword = apikey.databasePassword;
|
||||
|
||||
//Remove this block when Auth fallback is not used anymore
|
||||
// AUTH_FALLBACK
|
||||
if (!params.dbuser && apikey.user_id && global.environment.postgres_auth_user) {
|
||||
params.dbuser = _.template(global.environment.postgres_auth_user, apikey);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
|
||||
return callback();
|
||||
});
|
||||
} else if (apikeyType === 'regular') { //Actually it can be any type of api key
|
||||
this.metadataBackend.getApikey(username, params.api_key, (err, apikey) => {
|
||||
if (err) {
|
||||
if (isNameNotFoundError(err)) {
|
||||
err.http_status = 404;
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
params.dbuser = apikey.databaseRole;
|
||||
params.dbpassword = apikey.databasePassword;
|
||||
|
||||
//Remove this block when Auth fallback is not used anymore
|
||||
// AUTH_FALLBACK
|
||||
// master apikey has been recreated from user's metadata
|
||||
if (!params.dbuser && apikey.user_id && apikey.type === 'master' && global.environment.postgres_auth_user) {
|
||||
params.dbuser = _.template(global.environment.postgres_auth_user, apikey);
|
||||
}
|
||||
|
||||
//Remove this block when Auth fallback is not used anymore
|
||||
// AUTH_FALLBACK
|
||||
// default apikey has been recreated from user's metadata
|
||||
if (!params.dbpassword && global.environment.postgres.password) {
|
||||
params.dbpassword = global.environment.postgres.password;
|
||||
}
|
||||
|
||||
return callback();
|
||||
});
|
||||
} else if (apikeyType === 'default') {
|
||||
this.metadataBackend.getApikey(username, 'default_public', (err, apikey) => {
|
||||
if (err) {
|
||||
if (isNameNotFoundError(err)) {
|
||||
err.http_status = 404;
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
params.dbuser = apikey.databaseRole;
|
||||
params.dbpassword = apikey.databasePassword;
|
||||
|
||||
//Remove this block when Auth fallback is not used anymore
|
||||
// AUTH_FALLBACK
|
||||
if (!params.dbpassword && global.environment.postgres.password) {
|
||||
params.dbpassword = global.environment.postgres.password;
|
||||
}
|
||||
|
||||
return callback();
|
||||
});
|
||||
} else {
|
||||
return callback(new Error(`Invalid Apikey type: ${apikeyType}, valid ones: master, regular, default`));
|
||||
}
|
||||
};
|
||||
|
||||
function isNameNotFoundError (err) {
|
||||
return err.message && -1 !== err.message.indexOf('name not found');
|
||||
}
|
||||
|
||||
|
||||
// Set db connection parameters to those for the given username
|
||||
//
|
||||
// @param dbowner cartodb username of database owner,
|
||||
@@ -71,36 +110,30 @@ PgConnection.prototype.setDBAuth = function(username, params, callback) {
|
||||
// @param callback function(err)
|
||||
//
|
||||
PgConnection.prototype.setDBConn = function(dbowner, params, callback) {
|
||||
var self = this;
|
||||
// Add default database connection parameters
|
||||
// if none given
|
||||
_.defaults(params, {
|
||||
dbuser: global.environment.postgres.user,
|
||||
dbpassword: global.environment.postgres.password,
|
||||
// dbuser: global.environment.postgres.user,
|
||||
// dbpassword: global.environment.postgres.password,
|
||||
dbhost: global.environment.postgres.host,
|
||||
dbport: global.environment.postgres.port
|
||||
});
|
||||
step(
|
||||
function getConnectionParams() {
|
||||
self.metadataBackend.getUserDBConnectionParams(dbowner, this);
|
||||
},
|
||||
function extendParams(err, dbParams){
|
||||
assert.ifError(err);
|
||||
// we don't want null values or overwrite a non public user
|
||||
if (params.dbuser !== 'publicuser' || !dbParams.dbuser) {
|
||||
delete dbParams.dbuser;
|
||||
}
|
||||
if ( dbParams ) {
|
||||
_.extend(params, dbParams);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
this.metadataBackend.getUserDBConnectionParams(dbowner, (err, dbParams) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// we don’t want null values or overwrite a non public user
|
||||
if (params.dbuser !== 'publicuser' || !dbParams.dbuser) {
|
||||
delete dbParams.dbuser;
|
||||
}
|
||||
|
||||
if (dbParams) {
|
||||
_.extend(params, dbParams);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a `cartodb-psql` object for a given username.
|
||||
@@ -109,28 +142,37 @@ PgConnection.prototype.setDBConn = function(dbowner, params, callback) {
|
||||
*/
|
||||
|
||||
PgConnection.prototype.getConnection = function(username, callback) {
|
||||
var self = this;
|
||||
debug("getConn1");
|
||||
|
||||
var params = {};
|
||||
|
||||
require('debug')('cachechan')("getConn1");
|
||||
step(
|
||||
function setAuth() {
|
||||
self.setDBAuth(username, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
assert.ifError(err);
|
||||
self.setDBConn(username, params, this);
|
||||
},
|
||||
function openConnection(err) {
|
||||
assert.ifError(err);
|
||||
return callback(err, new PSQL({
|
||||
user: params.dbuser,
|
||||
pass: params.dbpass,
|
||||
host: params.dbhost,
|
||||
port: params.dbport,
|
||||
dbname: params.dbname
|
||||
}));
|
||||
this.getDatabaseParams(username, (err, databaseParams) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
);
|
||||
return callback(err, new PSQL({
|
||||
user: databaseParams.dbuser,
|
||||
pass: databaseParams.dbpass,
|
||||
host: databaseParams.dbhost,
|
||||
port: databaseParams.dbport,
|
||||
dbname: databaseParams.dbname
|
||||
}));
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
PgConnection.prototype.getDatabaseParams = function(username, callback) {
|
||||
const databaseParams = {};
|
||||
|
||||
this.setDBAuth(username, databaseParams, 'master', err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
this.setDBConn(username, databaseParams, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, databaseParams);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
var assert = require('assert');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var step = require('step');
|
||||
|
||||
function PgQueryRunner(pgConnection) {
|
||||
this.pgConnection = pgConnection;
|
||||
@@ -16,31 +14,23 @@ module.exports = PgQueryRunner;
|
||||
* @param {Function} callback function({Error}, {Array}) second argument is guaranteed to be an array
|
||||
*/
|
||||
PgQueryRunner.prototype.run = function(username, query, callback) {
|
||||
var self = this;
|
||||
|
||||
var params = {};
|
||||
|
||||
step(
|
||||
function setAuth() {
|
||||
self.pgConnection.setDBAuth(username, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
assert.ifError(err);
|
||||
self.pgConnection.setDBConn(username, params, this);
|
||||
},
|
||||
function executeQuery(err) {
|
||||
assert.ifError(err);
|
||||
var psql = new PSQL({
|
||||
user: params.dbuser,
|
||||
pass: params.dbpass,
|
||||
host: params.dbhost,
|
||||
port: params.dbport,
|
||||
dbname: params.dbname
|
||||
});
|
||||
psql.query(query, function(err, resultSet) {
|
||||
resultSet = resultSet || {};
|
||||
return callback(err, resultSet.rows || []);
|
||||
});
|
||||
this.pgConnection.getDatabaseParams(username, (err, databaseParams) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
);
|
||||
|
||||
const psql = new PSQL({
|
||||
user: databaseParams.dbuser,
|
||||
pass: databaseParams.dbpass,
|
||||
host: databaseParams.dbhost,
|
||||
port: databaseParams.dbport,
|
||||
dbname: databaseParams.dbname
|
||||
});
|
||||
|
||||
psql.query(query, function (err, resultSet) {
|
||||
resultSet = resultSet || {};
|
||||
return callback(err, resultSet.rows || []);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
var PSQL = require('cartodb-psql');
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
const rateLimit = require('../middleware/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
|
||||
function AnalysesController(prepareContext) {
|
||||
function AnalysesController(prepareContext, userLimitsApi) {
|
||||
this.prepareContext = prepareContext;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
}
|
||||
|
||||
module.exports = AnalysesController;
|
||||
@@ -12,26 +15,27 @@ AnalysesController.prototype.register = function (app) {
|
||||
app.get(
|
||||
`${app.base_url_mapconfig}/analyses/catalog`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.ANALYSIS_CATALOG),
|
||||
this.prepareContext,
|
||||
this.createPGClient(),
|
||||
this.getDataFromQuery({ queryTemplate: catalogQueryTpl, key: 'catalog' }),
|
||||
this.getDataFromQuery({ queryTemplate: tablesQueryTpl, key: 'tables' }),
|
||||
this.prepareResponse(),
|
||||
this.setCacheControlHeader(),
|
||||
this.sendResponse(),
|
||||
this.unathorizedError()
|
||||
createPGClient(),
|
||||
getDataFromQuery({ queryTemplate: catalogQueryTpl, key: 'catalog' }),
|
||||
getDataFromQuery({ queryTemplate: tablesQueryTpl, key: 'tables' }),
|
||||
prepareResponse(),
|
||||
setCacheControlHeader(),
|
||||
sendResponse(),
|
||||
unauthorizedError()
|
||||
);
|
||||
};
|
||||
|
||||
AnalysesController.prototype.createPGClient = function () {
|
||||
function createPGClient () {
|
||||
return function createPGClientMiddleware (req, res, next) {
|
||||
res.locals.pg = new PSQL(dbParamsFromReqParams(res.locals));
|
||||
next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
AnalysesController.prototype.getDataFromQuery = function ({ queryTemplate, key }) {
|
||||
function getDataFromQuery({ queryTemplate, key }) {
|
||||
const readOnlyTransactionOn = true;
|
||||
|
||||
return function getCatalogMiddleware(req, res, next) {
|
||||
@@ -48,9 +52,9 @@ AnalysesController.prototype.getDataFromQuery = function ({ queryTemplate, key }
|
||||
next();
|
||||
}, readOnlyTransactionOn);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
AnalysesController.prototype.prepareResponse = function () {
|
||||
function prepareResponse () {
|
||||
return function prepareResponseMiddleware (req, res, next) {
|
||||
const { catalog, tables } = res.locals;
|
||||
|
||||
@@ -91,16 +95,16 @@ AnalysesController.prototype.prepareResponse = function () {
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
AnalysesController.prototype.setCacheControlHeader = function () {
|
||||
function setCacheControlHeader () {
|
||||
return function setCacheControlHeaderMiddleware (req, res, next) {
|
||||
res.set('Cache-Control', 'public,max-age=10,must-revalidate');
|
||||
next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
AnalysesController.prototype.sendResponse = function() {
|
||||
function sendResponse () {
|
||||
return function sendResponseMiddleware (req, res) {
|
||||
res.status(200);
|
||||
|
||||
@@ -110,9 +114,9 @@ AnalysesController.prototype.sendResponse = function() {
|
||||
res.json(res.body);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
AnalysesController.prototype.unathorizedError = function () {
|
||||
function unauthorizedError () {
|
||||
return function unathorizedErrorMiddleware(err, req, res, next) {
|
||||
if (err.message.match(/permission\sdenied/)) {
|
||||
err = new Error('Unauthorized');
|
||||
@@ -121,7 +125,7 @@ AnalysesController.prototype.unathorizedError = function () {
|
||||
|
||||
next(err);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const catalogQueryTpl = ctx => `
|
||||
SELECT analysis_def->>'type' as type, * FROM cdb_analysis_catalog WHERE username = '${ctx._username}'
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
|
||||
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');
|
||||
|
||||
var MapStoreMapConfigProvider = require('../models/mapconfig/provider/map-store-provider');
|
||||
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
const cors = require('../middleware/cors');
|
||||
const userMiddleware = require('../middleware/user');
|
||||
const allowQueryParams = require('../middleware/allow-query-params');
|
||||
const vectorError = require('../middleware/vector-error');
|
||||
const rateLimit = require('../middleware/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const DataviewBackend = require('../backends/dataview');
|
||||
const AnalysisStatusBackend = require('../backends/analysis-status');
|
||||
const MapStoreMapConfigProvider = require('../models/mapconfig/provider/map-store-provider');
|
||||
const QueryTables = require('cartodb-query-tables');
|
||||
const SUPPORTED_FORMATS = {
|
||||
grid_json: true,
|
||||
json_torque: true,
|
||||
torque_json: true,
|
||||
png: true,
|
||||
png32: true,
|
||||
mvt: true
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
* @param {prepareContext} prepareContext
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param {MapStore} mapStore
|
||||
* @param {TileBackend} tileBackend
|
||||
@@ -26,8 +30,18 @@ var QueryTables = require('cartodb-query-tables');
|
||||
* @param {AnalysisBackend} analysisBackend
|
||||
* @constructor
|
||||
*/
|
||||
function LayergroupController(prepareContext, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, analysisBackend) {
|
||||
function LayergroupController(
|
||||
prepareContext,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
tileBackend,
|
||||
previewBackend,
|
||||
attributesBackend,
|
||||
surrogateKeysCache,
|
||||
userLimitsApi,
|
||||
layergroupAffectedTables,
|
||||
analysisBackend
|
||||
) {
|
||||
this.pgConnection = pgConnection;
|
||||
this.mapStore = mapStore;
|
||||
this.tileBackend = tileBackend;
|
||||
@@ -46,64 +60,125 @@ function LayergroupController(prepareContext, pgConnection, mapStore, tileBacken
|
||||
module.exports = LayergroupController;
|
||||
|
||||
LayergroupController.prototype.register = function(app) {
|
||||
const { base_url_mapconfig: basePath } = app;
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:z/:x/:y@:scale_factor?x.:format',
|
||||
`${basePath}/:token/:z/:x/:y@:scale_factor?x.:format`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.TILE),
|
||||
this.prepareContext,
|
||||
this.tile.bind(this),
|
||||
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi),
|
||||
getTile(this.tileBackend, 'map_tile'),
|
||||
setCacheControlHeader(),
|
||||
setLastModifiedHeader(),
|
||||
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore),
|
||||
setCacheChannelHeader(),
|
||||
setSurrogateKeyHeader(this.surrogateKeysCache),
|
||||
incrementSuccessMetrics(global.statsClient),
|
||||
sendResponse(),
|
||||
incrementErrorMetrics(global.statsClient),
|
||||
tileError(),
|
||||
vectorError()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:z/:x/:y.:format',
|
||||
`${basePath}/:token/:z/:x/:y.:format`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.TILE),
|
||||
this.prepareContext,
|
||||
this.tile.bind(this),
|
||||
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi),
|
||||
getTile(this.tileBackend, 'map_tile'),
|
||||
setCacheControlHeader(),
|
||||
setLastModifiedHeader(),
|
||||
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore),
|
||||
setCacheChannelHeader(),
|
||||
setSurrogateKeyHeader(this.surrogateKeysCache),
|
||||
incrementSuccessMetrics(global.statsClient),
|
||||
sendResponse(),
|
||||
incrementErrorMetrics(global.statsClient),
|
||||
tileError(),
|
||||
vectorError()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:layer/:z/:x/:y.(:format)',
|
||||
`${basePath}/:token/:layer/:z/:x/:y.(:format)`,
|
||||
distinguishLayergroupFromStaticRoute(),
|
||||
cors(),
|
||||
userMiddleware,
|
||||
validateLayerRouteMiddleware,
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.TILE),
|
||||
this.prepareContext,
|
||||
this.layer.bind(this),
|
||||
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi),
|
||||
getTile(this.tileBackend, 'maplayer_tile'),
|
||||
setCacheControlHeader(),
|
||||
setLastModifiedHeader(),
|
||||
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore),
|
||||
setCacheChannelHeader(),
|
||||
setSurrogateKeyHeader(this.surrogateKeysCache),
|
||||
incrementSuccessMetrics(global.statsClient),
|
||||
sendResponse(),
|
||||
incrementErrorMetrics(global.statsClient),
|
||||
tileError(),
|
||||
vectorError()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:layer/attributes/:fid',
|
||||
`${basePath}/:token/:layer/attributes/:fid`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.ATTRIBUTES),
|
||||
this.prepareContext,
|
||||
this.attributes.bind(this)
|
||||
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi),
|
||||
getFeatureAttributes(this.attributesBackend),
|
||||
setCacheControlHeader(),
|
||||
setLastModifiedHeader(),
|
||||
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore),
|
||||
setCacheChannelHeader(),
|
||||
setSurrogateKeyHeader(this.surrogateKeysCache),
|
||||
sendResponse()
|
||||
);
|
||||
|
||||
const forcedFormat = 'png';
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/static/center/:token/:z/:lat/:lng/:width/:height.:format',
|
||||
`${basePath}/static/center/:token/:z/:lat/:lng/:width/:height.:format`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.STATIC),
|
||||
allowQueryParams(['layer']),
|
||||
this.prepareContext,
|
||||
this.center.bind(this)
|
||||
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi, forcedFormat),
|
||||
getPreviewImageByCenter(this.previewBackend),
|
||||
setCacheControlHeader(),
|
||||
setLastModifiedHeader(),
|
||||
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore),
|
||||
setCacheChannelHeader(),
|
||||
setSurrogateKeyHeader(this.surrogateKeysCache),
|
||||
sendResponse()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format',
|
||||
`${basePath}/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.STATIC),
|
||||
allowQueryParams(['layer']),
|
||||
this.prepareContext,
|
||||
this.bbox.bind(this)
|
||||
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi, forcedFormat),
|
||||
getPreviewImageByBoundingBox(this.previewBackend),
|
||||
setCacheControlHeader(),
|
||||
setLastModifiedHeader(),
|
||||
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore),
|
||||
setCacheChannelHeader(),
|
||||
setSurrogateKeyHeader(this.surrogateKeysCache),
|
||||
sendResponse()
|
||||
);
|
||||
|
||||
// Undocumented/non-supported API endpoint methods.
|
||||
// Use at your own peril.
|
||||
|
||||
var allowedDataviewQueryParams = [
|
||||
const allowedDataviewQueryParams = [
|
||||
'filters', // json
|
||||
'own_filter', // 0, 1
|
||||
'no_filters', // 0, 1
|
||||
@@ -119,395 +194,470 @@ LayergroupController.prototype.register = function(app) {
|
||||
];
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/dataview/:dataviewName',
|
||||
`${basePath}/:token/dataview/:dataviewName`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW),
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.prepareContext,
|
||||
this.dataview.bind(this)
|
||||
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi),
|
||||
getDataview(this.dataviewBackend),
|
||||
setCacheControlHeader(),
|
||||
setLastModifiedHeader(),
|
||||
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore),
|
||||
setCacheChannelHeader(),
|
||||
setSurrogateKeyHeader(this.surrogateKeysCache),
|
||||
sendResponse()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:layer/widget/:dataviewName',
|
||||
`${basePath}/:token/:layer/widget/:dataviewName`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW),
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.prepareContext,
|
||||
this.dataview.bind(this)
|
||||
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi),
|
||||
getDataview(this.dataviewBackend),
|
||||
setCacheControlHeader(),
|
||||
setLastModifiedHeader(),
|
||||
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore),
|
||||
setCacheChannelHeader(),
|
||||
setSurrogateKeyHeader(this.surrogateKeysCache),
|
||||
sendResponse()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/dataview/:dataviewName/search',
|
||||
`${basePath}/:token/dataview/:dataviewName/search`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW_SEARCH),
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.prepareContext,
|
||||
this.dataviewSearch.bind(this)
|
||||
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi),
|
||||
dataviewSearch(this.dataviewBackend),
|
||||
setCacheControlHeader(),
|
||||
setLastModifiedHeader(),
|
||||
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore),
|
||||
setCacheChannelHeader(),
|
||||
setSurrogateKeyHeader(this.surrogateKeysCache),
|
||||
sendResponse()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:layer/widget/:dataviewName/search',
|
||||
`${basePath}/:token/:layer/widget/:dataviewName/search`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW_SEARCH),
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.prepareContext,
|
||||
this.dataviewSearch.bind(this)
|
||||
createMapStoreMapConfigProvider(this.mapStore, this.userLimitsApi),
|
||||
dataviewSearch(this.dataviewBackend),
|
||||
setCacheControlHeader(),
|
||||
setLastModifiedHeader(),
|
||||
getAffectedTables(this.layergroupAffectedTables, this.pgConnection, this.mapStore),
|
||||
setCacheChannelHeader(),
|
||||
setSurrogateKeyHeader(this.surrogateKeysCache),
|
||||
sendResponse()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/analysis/node/:nodeId',
|
||||
`${basePath}/:token/analysis/node/:nodeId`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.ANALYSIS),
|
||||
this.prepareContext,
|
||||
this.analysisNodeStatus.bind(this)
|
||||
analysisNodeStatus(this.analysisStatusBackend),
|
||||
sendResponse()
|
||||
);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.analysisNodeStatus = function(req, res, next) {
|
||||
var self = this;
|
||||
function distinguishLayergroupFromStaticRoute () {
|
||||
return function distinguishLayergroupFromStaticRouteMiddleware(req, res, next) {
|
||||
if (req.params.token === 'static') {
|
||||
return next('route');
|
||||
}
|
||||
|
||||
step(
|
||||
function retrieveNodeStatus() {
|
||||
self.analysisStatusBackend.getNodeStatus(res.locals, this);
|
||||
},
|
||||
function finish(err, nodeStatus, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function analysisNodeStatus (analysisStatusBackend) {
|
||||
return function analysisNodeStatusMiddleware(req, res, next) {
|
||||
analysisStatusBackend.getNodeStatus(res.locals, (err, nodeStatus, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET NODE STATUS';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, nodeStatus, 200, {
|
||||
'Cache-Control': 'public,max-age=5',
|
||||
'Last-Modified': new Date().toUTCString()
|
||||
});
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.set({
|
||||
'Cache-Control': 'public,max-age=5',
|
||||
'Last-Modified': new Date().toUTCString()
|
||||
});
|
||||
|
||||
res.body = nodeStatus;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getRequestParams(locals) {
|
||||
const params = Object.assign({}, locals);
|
||||
|
||||
delete params.mapConfigProvider;
|
||||
delete params.allowedQueryParams;
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function createMapStoreMapConfigProvider (mapStore, userLimitsApi, forcedFormat = null) {
|
||||
return function createMapStoreMapConfigProviderMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
|
||||
const params = getRequestParams(res.locals);
|
||||
|
||||
if (forcedFormat) {
|
||||
params.format = forcedFormat;
|
||||
params.layer = params.layer || 'all';
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.dataview = function(req, res, next) {
|
||||
var self = this;
|
||||
res.locals.mapConfigProvider = new MapStoreMapConfigProvider(mapStore, user, userLimitsApi, params);
|
||||
|
||||
step(
|
||||
function retrieveDataview() {
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
|
||||
);
|
||||
self.dataviewBackend.getDataview(
|
||||
mapConfigProvider,
|
||||
res.locals.user,
|
||||
res.locals,
|
||||
this
|
||||
);
|
||||
},
|
||||
function finish(err, dataview, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function getDataview (dataviewBackend) {
|
||||
return function getDataviewMiddleware (req, res, next) {
|
||||
const { user, mapConfigProvider } = res.locals;
|
||||
const params = getRequestParams(res.locals);
|
||||
|
||||
dataviewBackend.getDataview(mapConfigProvider, user, params, (err, dataview, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET DATAVIEW';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, dataview, 200);
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.dataviewSearch = function(req, res, next) {
|
||||
var self = this;
|
||||
res.body = dataview;
|
||||
|
||||
step(
|
||||
function searchDataview() {
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
|
||||
);
|
||||
self.dataviewBackend.search(mapConfigProvider, res.locals.user, req.params.dataviewName, res.locals, this);
|
||||
},
|
||||
function finish(err, searchResult, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function dataviewSearch (dataviewBackend) {
|
||||
return function dataviewSearchMiddleware (req, res, next) {
|
||||
const { user, dataviewName, mapConfigProvider } = res.locals;
|
||||
const params = getRequestParams(res.locals);
|
||||
|
||||
dataviewBackend.search(mapConfigProvider, user, dataviewName, params, (err, searchResult, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET DATAVIEW SEARCH';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, searchResult, 200);
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
res.body = searchResult;
|
||||
|
||||
LayergroupController.prototype.attributes = function(req, res, next) {
|
||||
var self = this;
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
req.profiler.start('windshaft.maplayer_attribute');
|
||||
function getFeatureAttributes (attributesBackend) {
|
||||
return function getFeatureAttributesMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft.maplayer_attribute');
|
||||
|
||||
step(
|
||||
function retrieveFeatureAttributes() {
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
|
||||
);
|
||||
self.attributesBackend.getFeatureAttributes(mapConfigProvider, res.locals, false, this);
|
||||
},
|
||||
function finish(err, tile, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
const { mapConfigProvider } = res.locals;
|
||||
const params = getRequestParams(res.locals);
|
||||
|
||||
attributesBackend.getFeatureAttributes(mapConfigProvider, params, false, (err, tile, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET ATTRIBUTES';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, tile, 200);
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
res.body = tile;
|
||||
|
||||
// Gets a tile for a given token and set of tile ZXY coords. (OSM style)
|
||||
LayergroupController.prototype.tile = function(req, res, next) {
|
||||
req.profiler.start('windshaft.map_tile');
|
||||
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) {
|
||||
req.profiler.start('windshaft.maplayer_tile');
|
||||
this.tileOrLayer(req, res, next);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.tileOrLayer = function (req, res, next) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function mapController$getTileOrGrid() {
|
||||
self.tileBackend.getTile(
|
||||
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, next);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function getStatusCode(tile, format){
|
||||
return tile.length===0 && format==='mvt'? 204:200;
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// 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, next) {
|
||||
var supportedFormats = {
|
||||
grid_json: true,
|
||||
json_torque: true,
|
||||
torque_json: true,
|
||||
png: true,
|
||||
png32: true,
|
||||
mvt: true
|
||||
};
|
||||
function getStatusCode(tile, format){
|
||||
return tile.length === 0 && format === 'mvt'? 204 : 200;
|
||||
}
|
||||
|
||||
var formatStat = 'invalid';
|
||||
if (req.params.format) {
|
||||
var format = req.params.format.replace('.', '_');
|
||||
if (supportedFormats[format]) {
|
||||
formatStat = format;
|
||||
}
|
||||
}
|
||||
function parseFormat (format = '') {
|
||||
const prettyFormat = format.replace('.', '_');
|
||||
return SUPPORTED_FORMATS[prettyFormat] ? prettyFormat : 'invalid';
|
||||
}
|
||||
|
||||
if (err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var errMsg = err.message ? ( '' + err.message ) : ( '' + err );
|
||||
function getTile (tileBackend, profileLabel = 'tile') {
|
||||
return function getTileMiddleware (req, res, next) {
|
||||
req.profiler.start(`windshaft.${profileLabel}`);
|
||||
|
||||
// Rewrite mapnik parsing errors to start with layer number
|
||||
var matches = errMsg.match("(.*) in style 'layer([0-9]+)'");
|
||||
if (matches) {
|
||||
errMsg = 'style'+matches[2]+': ' + matches[1];
|
||||
}
|
||||
err.message = errMsg;
|
||||
const { mapConfigProvider } = res.locals;
|
||||
const params = getRequestParams(res.locals);
|
||||
|
||||
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, getStatusCode(tile, formatStat), headers);
|
||||
global.statsClient.increment('windshaft.tiles.success');
|
||||
global.statsClient.increment('windshaft.tiles.' + formatStat + '.success');
|
||||
}
|
||||
};
|
||||
|
||||
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, 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, next) {
|
||||
var format = req.params.format === 'jpg' ? 'jpeg' : '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 getImage() {
|
||||
if (center) {
|
||||
self.previewBackend.getImage(
|
||||
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, res.locals.user, self.userLimitsApi, res.locals),
|
||||
format, width, height, zoom /* bounds */, this);
|
||||
}
|
||||
},
|
||||
function handleImage(err, image, headers, stats) {
|
||||
req.profiler.done('render-' + format);
|
||||
req.profiler.add(stats || {});
|
||||
tileBackend.getTile(mapConfigProvider, params, (err, tile, headers, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'STATIC_MAP';
|
||||
next(err);
|
||||
} else {
|
||||
res.set('Content-Type', headers['Content-Type'] || 'image/' + format);
|
||||
self.sendResponse(req, res, image, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.sendResponse = function(req, res, body, status, headers) {
|
||||
var self = this;
|
||||
|
||||
req.profiler.done('res');
|
||||
|
||||
res.set('Cache-Control', 'public,max-age=31536000');
|
||||
|
||||
// Set Last-Modified header
|
||||
var lastUpdated;
|
||||
if (res.locals.cache_buster) {
|
||||
// Assuming cache_buster is a timestamp
|
||||
lastUpdated = new Date(parseInt(res.locals.cache_buster));
|
||||
} else {
|
||||
lastUpdated = new Date();
|
||||
}
|
||||
res.set('Last-Modified', lastUpdated.toUTCString());
|
||||
|
||||
var dbName = res.locals.dbname;
|
||||
step(
|
||||
function getAffectedTables() {
|
||||
self.getAffectedTables(res.locals.user, dbName, res.locals.token, this);
|
||||
},
|
||||
function sendResponse(err, affectedTables) {
|
||||
req.profiler.done('affectedTables');
|
||||
if (err) {
|
||||
global.logger.warn('ERROR generating cache channel: ' + err);
|
||||
}
|
||||
if (!!affectedTables) {
|
||||
res.set('X-Cache-Channel', affectedTables.getCacheChannel());
|
||||
self.surrogateKeysCache.tag(res, affectedTables);
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
res.status(status);
|
||||
const formatStat = parseFormat(req.params.format);
|
||||
|
||||
if (!Buffer.isBuffer(body) && typeof body === 'object') {
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(body);
|
||||
} else {
|
||||
res.json(body);
|
||||
}
|
||||
} else {
|
||||
res.send(body);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
res.statusCode = getStatusCode(tile, formatStat);
|
||||
res.body = tile;
|
||||
|
||||
LayergroupController.prototype.getAffectedTables = function(user, dbName, layergroupId, callback) {
|
||||
|
||||
if (this.layergroupAffectedTables.hasAffectedTables(dbName, layergroupId)) {
|
||||
return callback(null, this.layergroupAffectedTables.get(dbName, layergroupId));
|
||||
}
|
||||
|
||||
var self = this;
|
||||
step(
|
||||
function extractSQL() {
|
||||
step(
|
||||
function loadFromStore() {
|
||||
self.mapStore.load(layergroupId, this);
|
||||
},
|
||||
function getSQL(err, mapConfig) {
|
||||
assert.ifError(err);
|
||||
|
||||
var queries = [];
|
||||
mapConfig.getLayers().forEach(function(layer) {
|
||||
queries.push(layer.options.sql);
|
||||
if (layer.options.affected_tables) {
|
||||
layer.options.affected_tables.map(function(table) {
|
||||
queries.push('SELECT * FROM ' + table + ' LIMIT 0');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return queries.length ? queries.join(';') : null;
|
||||
},
|
||||
this
|
||||
);
|
||||
},
|
||||
function findAffectedTables(err, sql) {
|
||||
assert.ifError(err);
|
||||
|
||||
if ( ! sql ) {
|
||||
throw new Error("this request doesn't need an X-Cache-Channel generated");
|
||||
}
|
||||
|
||||
step(
|
||||
function getConnection() {
|
||||
self.pgConnection.getConnection(user, this);
|
||||
},
|
||||
function getAffectedTables(err, connection) {
|
||||
assert.ifError(err);
|
||||
|
||||
QueryTables.getAffectedTablesFromQuery(connection, sql, this);
|
||||
},
|
||||
this
|
||||
);
|
||||
},
|
||||
function buildCacheChannel(err, tables) {
|
||||
assert.ifError(err);
|
||||
self.layergroupAffectedTables.set(dbName, layergroupId, tables);
|
||||
|
||||
return tables;
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function validateLayerRouteMiddleware(req, res, next) {
|
||||
if (req.params.token === 'static') {
|
||||
return next('route');
|
||||
}
|
||||
|
||||
next();
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getPreviewImageByCenter (previewBackend) {
|
||||
return function getPreviewImageByCenterMiddleware (req, res, next) {
|
||||
const width = +req.params.width;
|
||||
const height = +req.params.height;
|
||||
const zoom = +req.params.z;
|
||||
const center = {
|
||||
lng: +req.params.lng,
|
||||
lat: +req.params.lat
|
||||
};
|
||||
|
||||
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
const { mapConfigProvider: provider } = res.locals;
|
||||
|
||||
previewBackend.getImage(provider, format, width, height, zoom, center, (err, image, headers, stats = {}) => {
|
||||
req.profiler.done(`render-${format}`);
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'STATIC_MAP';
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
res.set('Content-Type', headers['Content-Type'] || `image/${format}`);
|
||||
|
||||
res.body = image;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getPreviewImageByBoundingBox (previewBackend) {
|
||||
return function getPreviewImageByBoundingBoxMiddleware (req, res, next) {
|
||||
const width = +req.params.width;
|
||||
const height = +req.params.height;
|
||||
const bounds = {
|
||||
west: +req.params.west,
|
||||
north: +req.params.north,
|
||||
east: +req.params.east,
|
||||
south: +req.params.south
|
||||
};
|
||||
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
const { mapConfigProvider: provider } = res.locals;
|
||||
|
||||
previewBackend.getImage(provider, format, width, height, bounds, (err, image, headers, stats = {}) => {
|
||||
req.profiler.done(`render-${format}`);
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'STATIC_MAP';
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
res.set('Content-Type', headers['Content-Type'] || `image/${format}`);
|
||||
|
||||
res.body = image;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setLastModifiedHeader () {
|
||||
return function setLastModifiedHeaderMiddleware (req, res, next) {
|
||||
let { cache_buster: cacheBuster } = res.locals;
|
||||
|
||||
cacheBuster = parseInt(cacheBuster, 10);
|
||||
|
||||
const lastUpdated = res.locals.cache_buster ? new Date(cacheBuster) : new Date();
|
||||
|
||||
res.set('Last-Modified', lastUpdated.toUTCString());
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function setCacheControlHeader () {
|
||||
return function setCacheControlHeaderMiddleware (req, res, next) {
|
||||
res.set('Cache-Control', 'public,max-age=31536000');
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function getAffectedTables (layergroupAffectedTables, pgConnection, mapStore) {
|
||||
return function getAffectedTablesMiddleware (req, res, next) {
|
||||
const { user, dbname, token } = res.locals;
|
||||
|
||||
if (layergroupAffectedTables.hasAffectedTables(dbname, token)) {
|
||||
res.locals.affectedTables = layergroupAffectedTables.get(dbname, token);
|
||||
return next();
|
||||
}
|
||||
|
||||
mapStore.load(token, (err, mapconfig) => {
|
||||
if (err) {
|
||||
global.logger.warn('ERROR generating cache channel:', err);
|
||||
return next();
|
||||
}
|
||||
|
||||
const queries = [];
|
||||
mapconfig.getLayers().forEach(function(layer) {
|
||||
queries.push(layer.options.sql);
|
||||
if (layer.options.affected_tables) {
|
||||
layer.options.affected_tables.map(function(table) {
|
||||
queries.push(`SELECT * FROM ${table} LIMIT 0`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const sql = queries.length ? queries.join(';') : null;
|
||||
|
||||
if (!sql) {
|
||||
global.logger.warn('ERROR generating cache channel:' +
|
||||
' this request doesn\'t need an X-Cache-Channel generated');
|
||||
return next();
|
||||
}
|
||||
|
||||
pgConnection.getConnection(user, (err, connection) => {
|
||||
if (err) {
|
||||
global.logger.warn('ERROR generating cache channel:', err);
|
||||
return next();
|
||||
}
|
||||
|
||||
QueryTables.getAffectedTablesFromQuery(connection, sql, (err, affectedTables) => {
|
||||
req.profiler.done('getAffectedTablesFromQuery');
|
||||
if (err) {
|
||||
global.logger.warn('ERROR generating cache channel: ', err);
|
||||
return next();
|
||||
}
|
||||
|
||||
// feed affected tables cache so it can be reused from, for instance, map controller
|
||||
layergroupAffectedTables.set(dbname, token, affectedTables);
|
||||
|
||||
res.locals.affectedTables = affectedTables;
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setCacheChannelHeader () {
|
||||
return function setCacheChannelHeaderMiddleware (req, res, next) {
|
||||
const { affectedTables } = res.locals;
|
||||
|
||||
if (affectedTables) {
|
||||
res.set('X-Cache-Channel', affectedTables.getCacheChannel());
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function setSurrogateKeyHeader (surrogateKeysCache) {
|
||||
return function setSurrogateKeyHeaderMiddleware (req, res, next) {
|
||||
const { affectedTables } = res.locals;
|
||||
|
||||
if (affectedTables) {
|
||||
surrogateKeysCache.tag(res, affectedTables);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function incrementSuccessMetrics (statsClient) {
|
||||
return function incrementSuccessMetricsMiddleware (req, res, next) {
|
||||
const formatStat = parseFormat(req.params.format);
|
||||
|
||||
statsClient.increment('windshaft.tiles.success');
|
||||
statsClient.increment(`windshaft.tiles.${formatStat}.success`);
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function sendResponse () {
|
||||
return function sendResponseMiddleware (req, res) {
|
||||
req.profiler.done('res');
|
||||
|
||||
res.status(res.statusCode || 200);
|
||||
|
||||
if (!Buffer.isBuffer(res.body) && typeof res.body === 'object') {
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(res.body);
|
||||
} else {
|
||||
res.json(res.body);
|
||||
}
|
||||
} else {
|
||||
res.send(res.body);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function incrementErrorMetrics (statsClient) {
|
||||
return function incrementErrorMetricsMiddleware (err, req, res, next) {
|
||||
const formatStat = parseFormat(req.params.format);
|
||||
|
||||
statsClient.increment('windshaft.tiles.error');
|
||||
statsClient.increment(`windshaft.tiles.${formatStat}.error`);
|
||||
|
||||
next(err);
|
||||
};
|
||||
}
|
||||
|
||||
function tileError () {
|
||||
return function tileErrorMiddleware (err, req, res, next) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
let errMsg = err.message ? ( '' + err.message ) : ( '' + err );
|
||||
|
||||
// Rewrite mapnik parsing errors to start with layer number
|
||||
const matches = errMsg.match("(.*) in style 'layer([0-9]+)'");
|
||||
|
||||
if (matches) {
|
||||
errMsg = `style${matches[2]}: ${matches[1]}`;
|
||||
}
|
||||
|
||||
err.message = errMsg;
|
||||
err.label = 'TILE RENDER';
|
||||
|
||||
next(err);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
var _ = require('underscore');
|
||||
var windshaft = require('windshaft');
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
|
||||
var ResourceLocator = require('../models/resource-locator');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
const _ = require('underscore');
|
||||
const windshaft = require('windshaft');
|
||||
const MapConfig = windshaft.model.MapConfig;
|
||||
const Datasource = windshaft.model.Datasource;
|
||||
const QueryTables = require('cartodb-query-tables');
|
||||
const ResourceLocator = require('../models/resource-locator');
|
||||
const cors = require('../middleware/cors');
|
||||
const userMiddleware = require('../middleware/user');
|
||||
const allowQueryParams = require('../middleware/allow-query-params');
|
||||
|
||||
var MapConfig = windshaft.model.MapConfig;
|
||||
var Datasource = windshaft.model.Datasource;
|
||||
|
||||
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');
|
||||
const NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
const NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider');
|
||||
const CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/create-layergroup-provider');
|
||||
const LayergroupMetadata = require('../utils/layergroup-metadata');
|
||||
const rateLimit = require('../middleware/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
@@ -41,7 +39,8 @@ function MapController(prepareContext, pgConnection, templateMaps, mapBackend, m
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
|
||||
this.mapConfigAdapter = mapConfigAdapter;
|
||||
this.resourceLocator = new ResourceLocator(global.environment);
|
||||
const resourceLocator = new ResourceLocator(global.environment);
|
||||
this.layergroupMetadata = new LayergroupMetadata(resourceLocator);
|
||||
|
||||
this.statsBackend = statsBackend;
|
||||
this.prepareContext = prepareContext;
|
||||
@@ -53,14 +52,29 @@ MapController.prototype.register = function(app) {
|
||||
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'));
|
||||
app.get(
|
||||
base_url_mapconfig,
|
||||
this.composeCreateMapMiddleware(RATE_LIMIT_ENDPOINTS_GROUPS.ANONYMOUS)
|
||||
);
|
||||
app.post(
|
||||
base_url_mapconfig,
|
||||
this.composeCreateMapMiddleware(RATE_LIMIT_ENDPOINTS_GROUPS.ANONYMOUS)
|
||||
);
|
||||
app.get(
|
||||
`${base_url_templated}/:template_id/jsonp`,
|
||||
this.composeCreateMapMiddleware(RATE_LIMIT_ENDPOINTS_GROUPS.NAMED, useTemplate)
|
||||
);
|
||||
app.post(
|
||||
`${base_url_templated}/:template_id`,
|
||||
this.composeCreateMapMiddleware(RATE_LIMIT_ENDPOINTS_GROUPS.NAMED, useTemplate)
|
||||
);
|
||||
app.options(
|
||||
app.base_url_mapconfig,
|
||||
cors('Content-Type')
|
||||
);
|
||||
};
|
||||
|
||||
MapController.prototype.composeCreateMapMiddleware = function (useTemplate = false) {
|
||||
MapController.prototype.composeCreateMapMiddleware = function (endpointGroup, useTemplate = false) {
|
||||
const isTemplateInstantiation = useTemplate;
|
||||
const useTemplateHash = useTemplate;
|
||||
const includeQuery = !useTemplate;
|
||||
@@ -69,34 +83,56 @@ MapController.prototype.composeCreateMapMiddleware = function (useTemplate = fal
|
||||
|
||||
return [
|
||||
cors(),
|
||||
userMiddleware,
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, endpointGroup),
|
||||
allowQueryParams(['aggregation']),
|
||||
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.setAggregationMetadataToLayergroup(),
|
||||
this.setSurrogateKeyHeader(),
|
||||
this.sendResponse(),
|
||||
this.augmentError({ label, addContext })
|
||||
initProfiler(isTemplateInstantiation),
|
||||
checkJsonContentType(),
|
||||
this.getCreateMapMiddlewares(useTemplate),
|
||||
incrementMapViewCount(this.metadataBackend),
|
||||
augmentLayergroupData(),
|
||||
getAffectedTables(this.pgConnection, this.layergroupAffectedTables),
|
||||
setCacheChannel(),
|
||||
setLastModified(),
|
||||
setLastUpdatedTimeToLayergroup(),
|
||||
setCacheControl(),
|
||||
setLayerStats(this.pgConnection, this.statsBackend),
|
||||
setLayergroupIdHeader(this.templateMaps ,useTemplateHash),
|
||||
setDataviewsAndWidgetsUrlsToLayergroupMetadata(this.layergroupMetadata),
|
||||
setAnalysesMetadataToLayergroup(this.layergroupMetadata, includeQuery),
|
||||
setTurboCartoMetadataToLayergroup(this.layergroupMetadata),
|
||||
setAggregationMetadataToLayergroup(this.layergroupMetadata),
|
||||
setTilejsonMetadataToLayergroup(this.layergroupMetadata),
|
||||
setSurrogateKeyHeader(this.surrogateKeysCache),
|
||||
sendResponse(),
|
||||
augmentError({ label, addContext })
|
||||
];
|
||||
};
|
||||
|
||||
MapController.prototype.initProfiler = function (isTemplateInstantiation) {
|
||||
MapController.prototype.getCreateMapMiddlewares = function (useTemplate) {
|
||||
if (useTemplate) {
|
||||
return [
|
||||
checkInstantiteLayergroup(),
|
||||
getTemplate(
|
||||
this.templateMaps,
|
||||
this.pgConnection,
|
||||
this.metadataBackend,
|
||||
this.userLimitsApi,
|
||||
this.mapConfigAdapter
|
||||
),
|
||||
instantiateLayergroup(this.mapBackend, this.userLimitsApi)
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
checkCreateLayergroup(),
|
||||
prepareAdapterMapConfig(this.mapConfigAdapter),
|
||||
createLayergroup (this.mapBackend, this.userLimitsApi)
|
||||
];
|
||||
};
|
||||
|
||||
function initProfiler (isTemplateInstantiation) {
|
||||
const operation = isTemplateInstantiation ? 'instance_template' : 'createmap';
|
||||
|
||||
return function initProfilerMiddleware (req, res, next) {
|
||||
@@ -104,9 +140,9 @@ MapController.prototype.initProfiler = function (isTemplateInstantiation) {
|
||||
req.profiler.done(`${operation}.initProfilerMiddleware`);
|
||||
next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.checkJsonContentType = function () {
|
||||
function checkJsonContentType () {
|
||||
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'));
|
||||
@@ -116,9 +152,9 @@ MapController.prototype.checkJsonContentType = function () {
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.checkInstantiteLayergroup = function () {
|
||||
function checkInstantiteLayergroup () {
|
||||
return function checkInstantiteLayergroupMiddleware(req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
const { callback, config } = req.query;
|
||||
@@ -140,9 +176,9 @@ MapController.prototype.checkInstantiteLayergroup = function () {
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.checkCreateLayergroup = function () {
|
||||
function checkCreateLayergroup () {
|
||||
return function checkCreateLayergroupMiddleware (req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
const { config } = res.locals;
|
||||
@@ -161,19 +197,19 @@ MapController.prototype.checkCreateLayergroup = function () {
|
||||
req.profiler.done('checkCreateLayergroup');
|
||||
return next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.getTemplate = function () {
|
||||
function getTemplate (templateMaps, pgConnection, metadataBackend, userLimitsApi, mapConfigAdapter) {
|
||||
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,
|
||||
templateMaps,
|
||||
pgConnection,
|
||||
metadataBackend,
|
||||
userLimitsApi,
|
||||
mapConfigAdapter,
|
||||
user,
|
||||
req.params.template_id,
|
||||
templateParams,
|
||||
@@ -193,10 +229,10 @@ MapController.prototype.getTemplate = function () {
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.prepareAdapterMapConfig = function () {
|
||||
function prepareAdapterMapConfig (mapConfigAdapter) {
|
||||
return function prepareAdapterMapConfigMiddleware(req, res, next) {
|
||||
const requestMapConfig = req.body;
|
||||
const { user, dbhost, dbport, dbname, dbuser, dbpassword, api_key } = res.locals;
|
||||
@@ -218,7 +254,7 @@ MapController.prototype.prepareAdapterMapConfig = function () {
|
||||
}
|
||||
};
|
||||
|
||||
this.mapConfigAdapter.getMapConfig(user, requestMapConfig, res.locals, context, (err, requestMapConfig) => {
|
||||
mapConfigAdapter.getMapConfig(user, requestMapConfig, res.locals, context, (err, requestMapConfig) => {
|
||||
req.profiler.done('anonymous.getMapConfig');
|
||||
if (err) {
|
||||
return next(err);
|
||||
@@ -229,22 +265,22 @@ MapController.prototype.prepareAdapterMapConfig = function () {
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.createLayergroup = function () {
|
||||
function createLayergroup (mapBackend, userLimitsApi) {
|
||||
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);
|
||||
new CreateLayergroupMapConfigProvider(mapconfig, user, userLimitsApi, res.locals);
|
||||
|
||||
res.locals.mapconfig = mapconfig;
|
||||
res.locals.analysesResults = context.analysesResults;
|
||||
|
||||
this.mapBackend.createLayergroup(mapconfig, res.locals, mapconfigProvider, (err, layergroup) => {
|
||||
mapBackend.createLayergroup(mapconfig, res.locals, mapconfigProvider, (err, layergroup) => {
|
||||
req.profiler.done('createLayergroup');
|
||||
if (err) {
|
||||
return next(err);
|
||||
@@ -254,16 +290,16 @@ MapController.prototype.createLayergroup = function () {
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.instantiateLayergroup = function () {
|
||||
function instantiateLayergroup (mapBackend, userLimitsApi) {
|
||||
return function instantiateLayergroupMiddleware (req, res, next) {
|
||||
const { user, mapconfig, rendererParams } = res.locals;
|
||||
const mapconfigProvider =
|
||||
new CreateLayergroupMapConfigProvider(mapconfig, user, this.userLimitsApi, rendererParams);
|
||||
new CreateLayergroupMapConfigProvider(mapconfig, user, userLimitsApi, rendererParams);
|
||||
|
||||
this.mapBackend.createLayergroup(mapconfig, rendererParams, mapconfigProvider, (err, layergroup) => {
|
||||
mapBackend.createLayergroup(mapconfig, rendererParams, mapconfigProvider, (err, layergroup) => {
|
||||
req.profiler.done('createLayergroup');
|
||||
if (err) {
|
||||
return next(err);
|
||||
@@ -280,15 +316,15 @@ MapController.prototype.instantiateLayergroup = function () {
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.incrementMapViewCount = function () {
|
||||
function incrementMapViewCount (metadataBackend) {
|
||||
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) => {
|
||||
metadataBackend.incMapviewCount(user, mapconfig.obj().stat_tag, (err) => {
|
||||
req.profiler.done('incMapviewCount');
|
||||
|
||||
if (err) {
|
||||
@@ -297,10 +333,10 @@ MapController.prototype.incrementMapViewCount = function () {
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.augmentLayergroupData = function () {
|
||||
function augmentLayergroupData () {
|
||||
return function augmentLayergroupDataMiddleware (req, res, next) {
|
||||
const { layergroup } = res.locals;
|
||||
|
||||
@@ -311,13 +347,13 @@ MapController.prototype.augmentLayergroupData = function () {
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.getAffectedTables = function () {
|
||||
function getAffectedTables (pgConnection, layergroupAffectedTables) {
|
||||
return function getAffectedTablesMiddleware (req, res, next) {
|
||||
const { dbname, layergroup, user, mapconfig } = res.locals;
|
||||
|
||||
this.pgConnection.getConnection(user, (err, connection) => {
|
||||
pgConnection.getConnection(user, (err, connection) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
@@ -339,17 +375,17 @@ MapController.prototype.getAffectedTables = function () {
|
||||
}
|
||||
|
||||
// feed affected tables cache so it can be reused from, for instance, layergroup controller
|
||||
this.layergroupAffectedTables.set(dbname, layergroup.layergroupId, affectedTables);
|
||||
layergroupAffectedTables.set(dbname, layergroup.layergroupId, affectedTables);
|
||||
|
||||
res.locals.affectedTables = affectedTables;
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.setCacheChannel = function () {
|
||||
function setCacheChannel () {
|
||||
return function setCacheChannelMiddleware (req, res, next) {
|
||||
const { affectedTables } = res.locals;
|
||||
|
||||
@@ -359,9 +395,9 @@ MapController.prototype.setCacheChannel = function () {
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.setLastModified = function () {
|
||||
function setLastModified () {
|
||||
return function setLastModifiedMiddleware (req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
res.set('Last-Modified', (new Date()).toUTCString());
|
||||
@@ -369,9 +405,9 @@ MapController.prototype.setLastModified = function () {
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.setLastUpdatedTimeToLayergroup = function () {
|
||||
function setLastUpdatedTimeToLayergroup () {
|
||||
return function setLastUpdatedTimeToLayergroupMiddleware (req, res, next) {
|
||||
const { affectedTables, layergroup, analysesResults } = res.locals;
|
||||
|
||||
@@ -385,7 +421,7 @@ MapController.prototype.setLastUpdatedTimeToLayergroup = function () {
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function getLastUpdatedTime(analysesResults, lastUpdateTime) {
|
||||
if (!Array.isArray(analysesResults)) {
|
||||
@@ -400,7 +436,7 @@ function getLastUpdatedTime(analysesResults, lastUpdateTime) {
|
||||
}, lastUpdateTime);
|
||||
}
|
||||
|
||||
MapController.prototype.setCacheControl = function () {
|
||||
function setCacheControl () {
|
||||
return function setCacheControlMiddleware (req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
var ttl = global.environment.varnish.layergroupTtl || 86400;
|
||||
@@ -409,18 +445,18 @@ MapController.prototype.setCacheControl = function () {
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.setLayerStats = function () {
|
||||
function setLayerStats (pgConnection, statsBackend) {
|
||||
return function setLayerStatsMiddleware(req, res, next) {
|
||||
const { user, mapconfig, layergroup } = res.locals;
|
||||
|
||||
this.pgConnection.getConnection(user, (err, connection) => {
|
||||
pgConnection.getConnection(user, (err, connection) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
this.statsBackend.getStats(mapconfig, connection, function(err, layersStats) {
|
||||
statsBackend.getStats(mapconfig, connection, function(err, layersStats) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
@@ -434,171 +470,91 @@ MapController.prototype.setLayerStats = function () {
|
||||
next();
|
||||
});
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.setLayergroupIdHeader = function (useTemplateHash) {
|
||||
function setLayergroupIdHeader (templateMaps, useTemplateHash) {
|
||||
return function setLayergroupIdHeaderMiddleware (req, res, next) {
|
||||
const { layergroup, user, template } = res.locals;
|
||||
|
||||
if (useTemplateHash) {
|
||||
var templateHash = this.templateMaps.fingerPrint(template).substring(0, 8);
|
||||
var templateHash = 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 () {
|
||||
function setDataviewsAndWidgetsUrlsToLayergroupMetadata (layergroupMetadata) {
|
||||
return function setDataviewsAndWidgetsUrlsToLayergroupMetadataMiddleware (req, res, next) {
|
||||
const { layergroup, user, mapconfig } = res.locals;
|
||||
|
||||
this.addDataviewsAndWidgetsUrls(user, layergroup, mapconfig.obj());
|
||||
layergroupMetadata.addDataviewsAndWidgetsUrls(user, layergroup, mapconfig.obj());
|
||||
|
||||
next();
|
||||
}.bind(this);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// TODO this should take into account several URL patterns
|
||||
MapController.prototype.addDataviewsAndWidgetsUrls = function(username, layergroup, mapConfig) {
|
||||
this.addDataviewsUrls(username, layergroup, mapConfig);
|
||||
this.addWidgetsUrl(username, layergroup, mapConfig);
|
||||
};
|
||||
|
||||
MapController.prototype.addDataviewsUrls = function(username, layergroup, mapConfig) {
|
||||
layergroup.metadata.dataviews = layergroup.metadata.dataviews || {};
|
||||
var dataviews = mapConfig.dataviews || {};
|
||||
|
||||
Object.keys(dataviews).forEach(function(dataviewName) {
|
||||
var resource = layergroup.layergroupid + '/dataview/' + dataviewName;
|
||||
layergroup.metadata.dataviews[dataviewName] = {
|
||||
url: this.resourceLocator.getUrls(username, resource)
|
||||
};
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
MapController.prototype.addWidgetsUrl = function(username, layergroup, mapConfig) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
var mapConfigLayer = mapConfig.layers[layerIndex];
|
||||
if (mapConfigLayer.options && mapConfigLayer.options.widgets) {
|
||||
layer.widgets = layer.widgets || {};
|
||||
Object.keys(mapConfigLayer.options.widgets).forEach(function(widgetName) {
|
||||
var resource = layergroup.layergroupid + '/' + layerIndex + '/widget/' + widgetName;
|
||||
layer.widgets[widgetName] = {
|
||||
type: mapConfigLayer.options.widgets[widgetName].type,
|
||||
url: this.resourceLocator.getUrls(username, resource)
|
||||
};
|
||||
}.bind(this));
|
||||
}
|
||||
return layer;
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
MapController.prototype.setAnalysesMetadataToLayergroup = function (includeQuery) {
|
||||
function setAnalysesMetadataToLayergroup (layergroupMetadata, includeQuery) {
|
||||
return function setAnalysesMetadataToLayergroupMiddleware (req, res, next) {
|
||||
const { layergroup, user, analysesResults = [] } = res.locals;
|
||||
|
||||
this.addAnalysesMetadata(user, layergroup, analysesResults, includeQuery);
|
||||
layergroupMetadata.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 () {
|
||||
function setTurboCartoMetadataToLayergroup (layergroupMetadata) {
|
||||
return function setTurboCartoMetadataToLayergroupMiddleware (req, res, next) {
|
||||
const { layergroup, mapconfig, context } = res.locals;
|
||||
|
||||
addTurboCartoContextMetadata(layergroup, mapconfig.obj(), context);
|
||||
layergroupMetadata.addTurboCartoContextMetadata(layergroup, mapconfig.obj(), context);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
function addTurboCartoContextMetadata(layergroup, mapConfig, context) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
if (context.turboCarto && Array.isArray(context.turboCarto.layers)) {
|
||||
layer.meta.cartocss_meta = context.turboCarto.layers[layerIndex];
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: see how evolve this function, it's a good candidate to be refactored
|
||||
MapController.prototype.setAggregationMetadataToLayergroup = function () {
|
||||
function setAggregationMetadataToLayergroup (layergroupMetadata) {
|
||||
return function setAggregationMetadataToLayergroupMiddleware (req, res, next) {
|
||||
const { layergroup, mapconfig, context } = res.locals;
|
||||
|
||||
addAggregationContextMetadata(layergroup, mapconfig.obj(), context);
|
||||
layergroupMetadata.addAggregationContextMetadata(layergroup, mapconfig.obj(), context);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
function addAggregationContextMetadata(layergroup, mapConfig, context) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
if (context.aggregation && Array.isArray(context.aggregation.layers)) {
|
||||
layer.meta.aggregation = context.aggregation.layers[layerIndex];
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MapController.prototype.setSurrogateKeyHeader = function () {
|
||||
function setTilejsonMetadataToLayergroup (layergroupMetadata) {
|
||||
return function augmentLayergroupTilejsonMiddleware (req, res, next) {
|
||||
const { layergroup, user, mapconfig } = res.locals;
|
||||
|
||||
layergroupMetadata.addTileJsonMetadata(layergroup, user, mapconfig);
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function setSurrogateKeyHeader (surrogateKeysCache) {
|
||||
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);
|
||||
surrogateKeysCache.tag(res, affectedTables);
|
||||
}
|
||||
|
||||
if (templateName) {
|
||||
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, templateName));
|
||||
surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, templateName));
|
||||
}
|
||||
|
||||
next();
|
||||
}.bind(this);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.sendResponse = function () {
|
||||
function sendResponse () {
|
||||
return function sendResponseMiddleware (req, res) {
|
||||
req.profiler.done('res');
|
||||
const { layergroup } = res.locals;
|
||||
@@ -611,9 +567,9 @@ MapController.prototype.sendResponse = function () {
|
||||
res.json(layergroup);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MapController.prototype.augmentError = function (options) {
|
||||
function augmentError (options) {
|
||||
const { addContext = false, label = 'MAPS CONTROLLER' } = options;
|
||||
|
||||
return function augmentErrorMiddleware (err, req, res, next) {
|
||||
@@ -628,7 +584,7 @@ MapController.prototype.augmentError = function (options) {
|
||||
|
||||
next(err);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function populateError(err, mapConfig) {
|
||||
var error = new Error(err.message);
|
||||
|
||||
@@ -1,241 +1,12 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var _ = require('underscore');
|
||||
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
const NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
const cors = require('../middleware/cors');
|
||||
const userMiddleware = require('../middleware/user');
|
||||
const allowQueryParams = require('../middleware/allow-query-params');
|
||||
const vectorError = require('../middleware/vector-error');
|
||||
const rateLimit = require('../middleware/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
|
||||
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(prepareContext, namedMapProviderCache, tileBackend, previewBackend,
|
||||
surrogateKeysCache, tablesExtentApi, metadataBackend) {
|
||||
this.namedMapProviderCache = namedMapProviderCache;
|
||||
this.tileBackend = tileBackend;
|
||||
this.previewBackend = previewBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.tablesExtentApi = tablesExtentApi;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.prepareContext = prepareContext;
|
||||
}
|
||||
|
||||
module.exports = NamedMapsController;
|
||||
|
||||
NamedMapsController.prototype.register = function(app) {
|
||||
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,
|
||||
allowQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
|
||||
this.prepareContext,
|
||||
this.staticMap.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
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');
|
||||
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function getAffectedTablesAndLastUpdatedTime() {
|
||||
namedMapProvider.getAffectedTablesAndLastUpdatedTime(this);
|
||||
},
|
||||
function sendResponse(err, result) {
|
||||
req.profiler.done('affectedTables');
|
||||
if (err) {
|
||||
global.logger.log('ERROR generating cache channel: ' + err);
|
||||
}
|
||||
if (!result || !!result.tables) {
|
||||
// we increase cache control as we can invalidate it
|
||||
res.set('Cache-Control', 'public,max-age=31536000');
|
||||
|
||||
var lastModifiedDate;
|
||||
if (Number.isFinite(result.lastUpdatedTime)) {
|
||||
lastModifiedDate = new Date(result.getLastUpdatedAt());
|
||||
} else {
|
||||
lastModifiedDate = new Date();
|
||||
}
|
||||
res.set('Last-Modified', lastModifiedDate.toUTCString());
|
||||
|
||||
res.set('X-Cache-Channel', result.getCacheChannel());
|
||||
if (result.tables.length > 0) {
|
||||
self.surrogateKeysCache.tag(res, result);
|
||||
}
|
||||
}
|
||||
res.status(200);
|
||||
res.send(body);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.tile = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbUser = res.locals.user;
|
||||
|
||||
var namedMapProvider;
|
||||
step(
|
||||
function getNamedMapProvider() {
|
||||
self.namedMapProviderCache.get(
|
||||
cdbUser,
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
res.locals,
|
||||
this
|
||||
);
|
||||
},
|
||||
function getTile(err, _namedMapProvider) {
|
||||
assert.ifError(err);
|
||||
namedMapProvider = _namedMapProvider;
|
||||
self.tileBackend.getTile(namedMapProvider, req.params, this);
|
||||
},
|
||||
function handleImage(err, tile, headers, stats) {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'NAMED_MAP_TILE';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, tile, headers, namedMapProvider);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.staticMap = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbUser = res.locals.user;
|
||||
|
||||
var format = req.params.format === 'jpg' ? 'jpeg' : '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 namedMapProvider;
|
||||
step(
|
||||
function getNamedMapProvider() {
|
||||
self.namedMapProviderCache.get(
|
||||
cdbUser,
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
res.locals,
|
||||
this
|
||||
);
|
||||
},
|
||||
function prepareLayerVisibility(err, _namedMapProvider) {
|
||||
assert.ifError(err);
|
||||
|
||||
namedMapProvider = _namedMapProvider;
|
||||
|
||||
self.prepareLayerFilterFromPreviewLayers(cdbUser, req, res.locals, namedMapProvider, this);
|
||||
},
|
||||
function prepareImageOptions(err) {
|
||||
assert.ifError(err);
|
||||
self.getStaticImageOptions(cdbUser, res.locals, namedMapProvider, this);
|
||||
},
|
||||
function getImage(err, imageOpts) {
|
||||
assert.ifError(err);
|
||||
|
||||
var width = +req.params.width;
|
||||
var height = +req.params.height;
|
||||
|
||||
if (!_.isUndefined(imageOpts.zoom) && imageOpts.center) {
|
||||
self.previewBackend.getImage(
|
||||
namedMapProvider, format, width, height, imageOpts.zoom, imageOpts.center, this);
|
||||
} else {
|
||||
self.previewBackend.getImage(
|
||||
namedMapProvider, format, width, height, imageOpts.bounds, this);
|
||||
}
|
||||
},
|
||||
function incrementMapViews(err, image, headers, stats) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
namedMapProvider.getMapConfig(function(mapConfigErr, mapConfig) {
|
||||
self.metadataBackend.incMapviewCount(cdbUser, mapConfig.obj().stat_tag, function(sErr) {
|
||||
if (err) {
|
||||
global.logger.log("ERROR: failed to increment mapview count for user '%s': %s", cdbUser, sErr);
|
||||
}
|
||||
next(err, image, headers, stats);
|
||||
});
|
||||
});
|
||||
},
|
||||
function handleImage(err, image, headers, stats) {
|
||||
req.profiler.done('render-' + format);
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
err.label = 'STATIC_VIZ_MAP';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, image, headers, namedMapProvider);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (
|
||||
user,
|
||||
req,
|
||||
params,
|
||||
namedMapProvider,
|
||||
callback
|
||||
) {
|
||||
var self = this;
|
||||
namedMapProvider.getTemplate(function (err, template) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!template || !template.view || !template.view.preview_layers) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
var previewLayers = template.view.preview_layers;
|
||||
var layerVisibilityFilter = [];
|
||||
|
||||
template.layergroup.layers.forEach(function (layer, index) {
|
||||
if (previewLayers[''+index] !== false && previewLayers[layer.id] !== false) {
|
||||
layerVisibilityFilter.push(''+index);
|
||||
}
|
||||
});
|
||||
|
||||
if (!layerVisibilityFilter.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
// overwrites 'all' default filter
|
||||
params.layer = layerVisibilityFilter.join(',');
|
||||
|
||||
// recreates the provider
|
||||
self.namedMapProviderCache.get(
|
||||
user,
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
params,
|
||||
callback
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
var DEFAULT_ZOOM_CENTER = {
|
||||
const DEFAULT_ZOOM_CENTER = {
|
||||
zoom: 1,
|
||||
center: {
|
||||
lng: 0,
|
||||
@@ -247,88 +18,393 @@ function numMapper(n) {
|
||||
return +n;
|
||||
}
|
||||
|
||||
NamedMapsController.prototype.getStaticImageOptions = function(cdbUser, params, namedMapProvider, callback) {
|
||||
var self = this;
|
||||
function getRequestParams(locals) {
|
||||
const params = Object.assign({}, locals);
|
||||
|
||||
if ([params.zoom, params.lon, params.lat].map(numMapper).every(Number.isFinite)) {
|
||||
return callback(null, {
|
||||
zoom: params.zoom,
|
||||
center: {
|
||||
lng: params.lon,
|
||||
lat: params.lat
|
||||
}
|
||||
});
|
||||
}
|
||||
delete params.template;
|
||||
delete params.affectedTablesAndLastUpdate;
|
||||
delete params.namedMapProvider;
|
||||
delete params.allowedQueryParams;
|
||||
|
||||
if (params.bbox) {
|
||||
var bbox = params.bbox.split(',').map(numMapper);
|
||||
if (bbox.length === 4 && bbox.every(Number.isFinite)) {
|
||||
return callback(null, {
|
||||
bounds: {
|
||||
west: bbox[0],
|
||||
south: bbox[1],
|
||||
east: bbox[2],
|
||||
north: bbox[3]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
step(
|
||||
function getTemplate() {
|
||||
namedMapProvider.getTemplate(this);
|
||||
},
|
||||
function handleTemplateView(err, template) {
|
||||
assert.ifError(err);
|
||||
function NamedMapsController(
|
||||
prepareContext,
|
||||
namedMapProviderCache,
|
||||
tileBackend,
|
||||
previewBackend,
|
||||
surrogateKeysCache,
|
||||
tablesExtentApi,
|
||||
metadataBackend,
|
||||
userLimitsApi
|
||||
) {
|
||||
this.namedMapProviderCache = namedMapProviderCache;
|
||||
this.tileBackend = tileBackend;
|
||||
this.previewBackend = previewBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.tablesExtentApi = tablesExtentApi;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.prepareContext = prepareContext;
|
||||
}
|
||||
|
||||
if (template.view) {
|
||||
var zoomCenter = templateZoomCenter(template.view);
|
||||
if (zoomCenter) {
|
||||
if (Number.isFinite(+params.zoom)) {
|
||||
zoomCenter.zoom = +params.zoom;
|
||||
}
|
||||
return zoomCenter;
|
||||
}
|
||||
module.exports = NamedMapsController;
|
||||
|
||||
var bounds = templateBounds(template.view);
|
||||
if (bounds) {
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
NamedMapsController.prototype.register = function(app) {
|
||||
const { base_url_mapconfig, base_url_templated } = app;
|
||||
|
||||
return false;
|
||||
},
|
||||
function estimateBoundsIfNoImageOpts(err, imageOpts) {
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
app.get(
|
||||
`${base_url_templated}/:template_id/:layer/:z/:x/:y.(:format)`,
|
||||
cors(),
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_TILES),
|
||||
this.prepareContext,
|
||||
getNamedMapProvider({
|
||||
namedMapProviderCache: this.namedMapProviderCache,
|
||||
label: 'NAMED_MAP_TILE'
|
||||
}),
|
||||
getAffectedTables(),
|
||||
getTile({
|
||||
tileBackend: this.tileBackend,
|
||||
label: 'NAMED_MAP_TILE'
|
||||
}),
|
||||
setSurrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
setCacheChannelHeader(),
|
||||
setLastModifiedHeader(),
|
||||
setCacheControlHeader(),
|
||||
setContentTypeHeader(),
|
||||
sendResponse(),
|
||||
vectorError()
|
||||
);
|
||||
|
||||
var next = this;
|
||||
namedMapProvider.getAffectedTablesAndLastUpdatedTime(function(err, affectedTablesAndLastUpdate) {
|
||||
if (err) {
|
||||
return next(null);
|
||||
}
|
||||
|
||||
var affectedTables = affectedTablesAndLastUpdate.tables || [];
|
||||
|
||||
if (affectedTables.length === 0) {
|
||||
return next(null);
|
||||
}
|
||||
|
||||
self.tablesExtentApi.getBounds(cdbUser, affectedTables, function(err, result) {
|
||||
return next(null, result);
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
function returnCallback(err, imageOpts) {
|
||||
return callback(err, imageOpts || DEFAULT_ZOOM_CENTER);
|
||||
}
|
||||
app.get(
|
||||
`${base_url_mapconfig}/static/named/:template_id/:width/:height.:format`,
|
||||
cors(),
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.STATIC_NAMED),
|
||||
allowQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
|
||||
this.prepareContext,
|
||||
getNamedMapProvider({
|
||||
namedMapProviderCache: this.namedMapProviderCache,
|
||||
label: 'STATIC_VIZ_MAP', forcedFormat: 'png'
|
||||
}),
|
||||
getAffectedTables(),
|
||||
getTemplate({ label: 'STATIC_VIZ_MAP' }),
|
||||
prepareLayerFilterFromPreviewLayers({
|
||||
namedMapProviderCache: this.namedMapProviderCache,
|
||||
label: 'STATIC_VIZ_MAP'
|
||||
}),
|
||||
getStaticImageOptions({ tablesExtentApi: this.tablesExtentApi }),
|
||||
getImage({ previewBackend: this.previewBackend, label: 'STATIC_VIZ_MAP' }),
|
||||
incrementMapViews({ metadataBackend: this.metadataBackend }),
|
||||
setSurrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
setCacheChannelHeader(),
|
||||
setLastModifiedHeader(),
|
||||
setCacheControlHeader(),
|
||||
setContentTypeHeader(),
|
||||
sendResponse()
|
||||
);
|
||||
};
|
||||
|
||||
function getNamedMapProvider ({ namedMapProviderCache, label, forcedFormat = null }) {
|
||||
return function getNamedMapProviderMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const { config, auth_token } = req.query;
|
||||
const { template_id } = req.params;
|
||||
|
||||
if (forcedFormat) {
|
||||
res.locals.format = forcedFormat;
|
||||
res.locals.layer = res.locals.layer || 'all';
|
||||
}
|
||||
|
||||
const params = getRequestParams(res.locals);
|
||||
|
||||
namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, namedMapProvider) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.namedMapProvider = namedMapProvider;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getAffectedTables () {
|
||||
return function getAffectedTables (req, res, next) {
|
||||
const { namedMapProvider } = res.locals;
|
||||
|
||||
namedMapProvider.getAffectedTablesAndLastUpdatedTime((err, affectedTablesAndLastUpdate) => {
|
||||
req.profiler.done('affectedTables');
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.affectedTablesAndLastUpdate = affectedTablesAndLastUpdate;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getTemplate ({ label }) {
|
||||
return function getTemplateMiddleware (req, res, next) {
|
||||
const { namedMapProvider } = res.locals;
|
||||
|
||||
namedMapProvider.getTemplate((err, template) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.template = template;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function prepareLayerFilterFromPreviewLayers ({ namedMapProviderCache, label }) {
|
||||
return function prepareLayerFilterFromPreviewLayersMiddleware (req, res, next) {
|
||||
const { user, template } = res.locals;
|
||||
const { template_id } = req.params;
|
||||
const { config, auth_token } = req.query;
|
||||
|
||||
if (!template || !template.view || !template.view.preview_layers) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var previewLayers = template.view.preview_layers;
|
||||
var layerVisibilityFilter = [];
|
||||
|
||||
template.layergroup.layers.forEach((layer, index) => {
|
||||
if (previewLayers[''+index] !== false && previewLayers[layer.id] !== false) {
|
||||
layerVisibilityFilter.push(''+index);
|
||||
}
|
||||
});
|
||||
|
||||
if (!layerVisibilityFilter.length) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const params = getRequestParams(res.locals);
|
||||
|
||||
// overwrites 'all' default filter
|
||||
params.layer = layerVisibilityFilter.join(',');
|
||||
|
||||
// recreates the provider
|
||||
namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, provider) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.namedMapProvider = provider;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getTile ({ tileBackend, label }) {
|
||||
return function getTileMiddleware (req, res, next) {
|
||||
const { namedMapProvider } = res.locals;
|
||||
|
||||
tileBackend.getTile(namedMapProvider, req.params, (err, tile, headers, stats) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.body = tile;
|
||||
res.locals.headers = headers;
|
||||
res.locals.stats = stats;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getStaticImageOptions ({ tablesExtentApi }) {
|
||||
return function getStaticImageOptionsMiddleware(req, res, next) {
|
||||
const { user, namedMapProvider, template } = res.locals;
|
||||
|
||||
const imageOpts = getImageOptions(res.locals, template);
|
||||
|
||||
if (imageOpts) {
|
||||
res.locals.imageOpts = imageOpts;
|
||||
return next();
|
||||
}
|
||||
|
||||
res.locals.imageOpts = DEFAULT_ZOOM_CENTER;
|
||||
|
||||
namedMapProvider.getAffectedTablesAndLastUpdatedTime((err, affectedTablesAndLastUpdate) => {
|
||||
if (err) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var affectedTables = affectedTablesAndLastUpdate.tables || [];
|
||||
|
||||
if (affectedTables.length === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
tablesExtentApi.getBounds(user, affectedTables, (err, bounds) => {
|
||||
if (err) {
|
||||
return next();
|
||||
}
|
||||
|
||||
res.locals.imageOpts = bounds;
|
||||
|
||||
return next();
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getImageOptions (params, template) {
|
||||
const { zoom, lon, lat, bbox } = params;
|
||||
|
||||
let imageOpts = getImageOptionsFromCoordinates(zoom, lon, lat);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
|
||||
imageOpts = getImageOptionsFromBoundingBox(bbox);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
|
||||
imageOpts = getImageOptionsFromTemplate(template, zoom);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
}
|
||||
|
||||
function getImageOptionsFromCoordinates (zoom, lon, lat) {
|
||||
if ([zoom, lon, lat].map(numMapper).every(Number.isFinite)) {
|
||||
return {
|
||||
zoom: zoom,
|
||||
center: {
|
||||
lng: lon,
|
||||
lat: lat
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getImageOptionsFromTemplate (template, zoom) {
|
||||
if (template.view) {
|
||||
var zoomCenter = templateZoomCenter(template.view);
|
||||
if (zoomCenter) {
|
||||
if (Number.isFinite(+zoom)) {
|
||||
zoomCenter.zoom = +zoom;
|
||||
}
|
||||
|
||||
return zoomCenter;
|
||||
}
|
||||
|
||||
var bounds = templateBounds(template.view);
|
||||
if (bounds) {
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getImageOptionsFromBoundingBox (bbox = '') {
|
||||
var _bbox = bbox.split(',').map(numMapper);
|
||||
|
||||
if (_bbox.length === 4 && _bbox.every(Number.isFinite)) {
|
||||
return {
|
||||
bounds: {
|
||||
west: _bbox[0],
|
||||
south: _bbox[1],
|
||||
east: _bbox[2],
|
||||
north: _bbox[3]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getImage({ previewBackend, label }) {
|
||||
return function getImageMiddleware (req, res, next) {
|
||||
const { imageOpts, namedMapProvider } = res.locals;
|
||||
const { zoom, center, bounds } = imageOpts;
|
||||
|
||||
let { width, height } = req.params;
|
||||
|
||||
width = +width;
|
||||
height = +height;
|
||||
|
||||
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
|
||||
if (zoom !== undefined && center) {
|
||||
return previewBackend.getImage(namedMapProvider, format, width, height, zoom, center,
|
||||
(err, image, headers, stats) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.body = image;
|
||||
res.locals.headers = headers;
|
||||
res.locals.stats = stats;
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
previewBackend.getImage(namedMapProvider, format, width, height, bounds, (err, image, headers, stats) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.body = image;
|
||||
res.locals.headers = headers;
|
||||
res.locals.stats = stats;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function incrementMapViewsError (ctx) {
|
||||
return `ERROR: failed to increment mapview count for user '${ctx.user}': ${ctx.err}`;
|
||||
}
|
||||
|
||||
function incrementMapViews ({ metadataBackend }) {
|
||||
return function incrementMapViewsMiddleware(req, res, next) {
|
||||
const { user, namedMapProvider } = res.locals;
|
||||
|
||||
namedMapProvider.getMapConfig((err, mapConfig) => {
|
||||
if (err) {
|
||||
global.logger.log(incrementMapViewsError({ user, err }));
|
||||
return next();
|
||||
}
|
||||
|
||||
const statTag = mapConfig.obj().stat_tag;
|
||||
|
||||
metadataBackend.incMapviewCount(user, statTag, (err) => {
|
||||
if (err) {
|
||||
global.logger.log(incrementMapViewsError({ user, err }));
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function templateZoomCenter(view) {
|
||||
if (!_.isUndefined(view.zoom) && view.center) {
|
||||
if (view.zoom !== undefined && view.center) {
|
||||
return {
|
||||
zoom: view.zoom,
|
||||
center: view.center
|
||||
@@ -339,9 +415,8 @@ function templateZoomCenter(view) {
|
||||
|
||||
function templateBounds(view) {
|
||||
if (view.bounds) {
|
||||
var hasAllBounds = _.every(['west', 'south', 'east', 'north'], function(prop) {
|
||||
return Number.isFinite(view.bounds[prop]);
|
||||
});
|
||||
var hasAllBounds = ['west', 'south', 'east', 'north'].every(prop => Number.isFinite(view.bounds[prop]));
|
||||
|
||||
if (hasAllBounds) {
|
||||
return {
|
||||
bounds: {
|
||||
@@ -357,3 +432,86 @@ function templateBounds(view) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function setSurrogateKeyHeader ({ surrogateKeysCache }) {
|
||||
return function setSurrogateKeyHeaderMiddleware(req, res, next) {
|
||||
const { user, namedMapProvider, affectedTablesAndLastUpdate } = res.locals;
|
||||
|
||||
surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, namedMapProvider.getTemplateName()));
|
||||
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
|
||||
if (affectedTablesAndLastUpdate.tables.length > 0) {
|
||||
surrogateKeysCache.tag(res, affectedTablesAndLastUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function setCacheChannelHeader () {
|
||||
return function setCacheChannelHeaderMiddleware (req, res, next) {
|
||||
const { affectedTablesAndLastUpdate } = res.locals;
|
||||
|
||||
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
|
||||
res.set('X-Cache-Channel', affectedTablesAndLastUpdate.getCacheChannel());
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function setLastModifiedHeader () {
|
||||
return function setLastModifiedHeaderMiddleware(req, res, next) {
|
||||
const { affectedTablesAndLastUpdate } = res.locals;
|
||||
|
||||
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
|
||||
var lastModifiedDate;
|
||||
if (Number.isFinite(affectedTablesAndLastUpdate.lastUpdatedTime)) {
|
||||
lastModifiedDate = new Date(affectedTablesAndLastUpdate.getLastUpdatedAt());
|
||||
} else {
|
||||
lastModifiedDate = new Date();
|
||||
}
|
||||
|
||||
res.set('Last-Modified', lastModifiedDate.toUTCString());
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function setCacheControlHeader () {
|
||||
return function setCacheControlHeaderMiddleware(req, res, next) {
|
||||
const { affectedTablesAndLastUpdate } = res.locals;
|
||||
|
||||
res.set('Cache-Control', 'public,max-age=7200,must-revalidate');
|
||||
|
||||
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
|
||||
// we increase cache control as we can invalidate it
|
||||
res.set('Cache-Control', 'public,max-age=31536000');
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function setContentTypeHeader () {
|
||||
return function setContentTypeHeaderMiddleware(req, res, next) {
|
||||
const { headers = {} } = res.locals;
|
||||
|
||||
res.set('Content-Type', headers['content-type'] || headers['Content-Type'] || 'image/png');
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function sendResponse () {
|
||||
return function sendResponseMiddleware (req, res) {
|
||||
const { body, stats = {}, format } = res.locals;
|
||||
|
||||
req.profiler.done('render-' + format);
|
||||
req.profiler.add(stats);
|
||||
|
||||
res.status(200);
|
||||
res.send(body);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var templateName = require('../backends/template_maps').templateName;
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
const { templateName } = require('../backends/template_maps');
|
||||
const cors = require('../middleware/cors');
|
||||
const userMiddleware = require('../middleware/user');
|
||||
const rateLimit = require('../middleware/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const localsMiddleware = require('../middleware/context/locals');
|
||||
const credentialsMiddleware = require('../middleware/context/credentials');
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
@@ -12,219 +12,224 @@ var userMiddleware = require('../middleware/user');
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @constructor
|
||||
*/
|
||||
function NamedMapsAdminController(authApi, templateMaps) {
|
||||
function NamedMapsAdminController(authApi, templateMaps, userLimitsApi) {
|
||||
this.authApi = authApi;
|
||||
this.templateMaps = templateMaps;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
}
|
||||
|
||||
module.exports = NamedMapsAdminController;
|
||||
|
||||
NamedMapsAdminController.prototype.register = function (app) {
|
||||
const { base_url_templated } = app;
|
||||
|
||||
app.post(
|
||||
app.base_url_templated + '/',
|
||||
`${base_url_templated}/`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.create.bind(this)
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_CREATE),
|
||||
localsMiddleware(),
|
||||
credentialsMiddleware(),
|
||||
checkContentType({ action: 'POST', label: 'POST TEMPLATE' }),
|
||||
authorizedByAPIKey({ authApi: this.authApi, action: 'create', label: 'POST TEMPLATE' }),
|
||||
createTemplate({ templateMaps: this.templateMaps }),
|
||||
sendResponse()
|
||||
);
|
||||
|
||||
app.put(
|
||||
app.base_url_templated + '/:template_id',
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.update.bind(this)
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_UPDATE),
|
||||
localsMiddleware(),
|
||||
credentialsMiddleware(),
|
||||
checkContentType({ action: 'PUT', label: 'PUT TEMPLATE' }),
|
||||
authorizedByAPIKey({ authApi: this.authApi, action: 'update', label: 'PUT TEMPLATE' }),
|
||||
updateTemplate({ templateMaps: this.templateMaps }),
|
||||
sendResponse()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_templated + '/:template_id',
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.retrieve.bind(this)
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_GET),
|
||||
localsMiddleware(),
|
||||
credentialsMiddleware(),
|
||||
authorizedByAPIKey({ authApi: this.authApi, action: 'get', label: 'GET TEMPLATE' }),
|
||||
retrieveTemplate({ templateMaps: this.templateMaps }),
|
||||
sendResponse()
|
||||
);
|
||||
|
||||
app.delete(
|
||||
app.base_url_templated + '/:template_id',
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.destroy.bind(this)
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_DELETE),
|
||||
localsMiddleware(),
|
||||
credentialsMiddleware(),
|
||||
authorizedByAPIKey({ authApi: this.authApi, action: 'delete', label: 'DELETE TEMPLATE' }),
|
||||
destroyTemplate({ templateMaps: this.templateMaps }),
|
||||
sendResponse()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_templated + '/',
|
||||
`${base_url_templated}/`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.list.bind(this)
|
||||
userMiddleware(),
|
||||
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_LIST),
|
||||
localsMiddleware(),
|
||||
credentialsMiddleware(),
|
||||
authorizedByAPIKey({ authApi: this.authApi, action: 'list', label: 'GET TEMPLATE LIST' }),
|
||||
listTemplates({ templateMaps: this.templateMaps }),
|
||||
sendResponse()
|
||||
);
|
||||
|
||||
app.options(
|
||||
app.base_url_templated + '/:template_id',
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors('Content-Type')
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.create = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function addTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
ifInvalidContentType(req, 'template POST data must be of type application/json');
|
||||
var cfg = req.body;
|
||||
self.templateMaps.addTemplate(cdbuser, cfg, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_id){
|
||||
assert.ifError(err);
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self, req, res, 'POST TEMPLATE', null, next)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.update = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
var template;
|
||||
var tpl_id;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can update templated maps');
|
||||
ifInvalidContentType(req, 'template PUT data must be of type application/json');
|
||||
|
||||
template = req.body;
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.updTemplate(cdbuser, tpl_id, template, this);
|
||||
},
|
||||
function prepareResponse(err){
|
||||
assert.ifError(err);
|
||||
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self, req, res, 'PUT TEMPLATE', null, next)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.retrieve = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function getTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_val) {
|
||||
assert.ifError(err);
|
||||
if ( ! tpl_val ) {
|
||||
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
// auth_id was added by ourselves,
|
||||
// so we remove it before returning to the user
|
||||
delete tpl_val.auth_id;
|
||||
return { template: tpl_val };
|
||||
},
|
||||
finishFn(self, req, res, 'GET TEMPLATE', null, next)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.destroy = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function deleteTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can delete template maps');
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.delTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err/*, tpl_val*/){
|
||||
assert.ifError(err);
|
||||
return '';
|
||||
},
|
||||
finishFn(self, req, res, 'DELETE TEMPLATE', 204, next)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.list = function(req, res, next) {
|
||||
var self = this;
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function listTemplates(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can list templated maps');
|
||||
|
||||
self.templateMaps.listTemplates(cdbuser, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_ids){
|
||||
assert.ifError(err);
|
||||
return { template_ids: tpl_ids };
|
||||
},
|
||||
finishFn(self, req, res, 'GET TEMPLATE LIST', null, next)
|
||||
);
|
||||
};
|
||||
|
||||
function finishFn(controller, req, res, description, status, next) {
|
||||
return function finish(err, body){
|
||||
if (err) {
|
||||
err.label = description;
|
||||
next(err);
|
||||
} else {
|
||||
res.status(status || 200);
|
||||
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(body);
|
||||
} else {
|
||||
res.json(body);
|
||||
}
|
||||
function checkContentType ({ action, label }) {
|
||||
return function checkContentTypeMiddleware (req, res, next) {
|
||||
if (!req.is('application/json')) {
|
||||
const error = new Error(`template ${action} data must be of type application/json`);
|
||||
error.label = label;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function ifUnauthenticated(authenticated, description) {
|
||||
if (!authenticated) {
|
||||
var err = new Error(description);
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
function authorizedByAPIKey ({ authApi, action, label }) {
|
||||
return function authorizedByAPIKeyMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
|
||||
authApi.authorizedByAPIKey(user, res, (err, authenticated) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!authenticated) {
|
||||
const error = new Error(`Only authenticated user can ${action} templated maps`);
|
||||
error.http_status = 403;
|
||||
error.label = label;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function ifInvalidContentType(req, description) {
|
||||
if (!req.is('application/json')) {
|
||||
throw new Error(description);
|
||||
}
|
||||
function createTemplate ({ templateMaps }) {
|
||||
return function createTemplateMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const template = req.body;
|
||||
|
||||
templateMaps.addTemplate(user, template, (err, templateId) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.body = { template_id: templateId };
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function updateTemplate ({ templateMaps }) {
|
||||
return function updateTemplateMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const template = req.body;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
templateMaps.updTemplate(user, templateId, template, (err) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.body = { template_id: templateId };
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function retrieveTemplate ({ templateMaps }) {
|
||||
return function retrieveTemplateMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
|
||||
const { user } = res.locals;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
templateMaps.getTemplate(user, templateId, (err, template) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!template) {
|
||||
const error = new Error(`Cannot find template '${templateId}' of user '${user}'`);
|
||||
error.http_status = 404;
|
||||
return next(error);
|
||||
}
|
||||
// auth_id was added by ourselves,
|
||||
// so we remove it before returning to the user
|
||||
delete template.auth_id;
|
||||
|
||||
res.body = { template };
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function destroyTemplate ({ templateMaps }) {
|
||||
return function destroyTemplateMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
|
||||
const { user } = res.locals;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
templateMaps.delTemplate(user, templateId, (err/* , tpl_val */) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 204;
|
||||
res.body = '';
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function listTemplates ({ templateMaps }) {
|
||||
return function listTemplatesMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
|
||||
const { user } = res.locals;
|
||||
|
||||
templateMaps.listTemplates(user, (err, templateIds) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.body = { template_ids: templateIds };
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function sendResponse () {
|
||||
return function sendResponseMiddleware (req, res) {
|
||||
res.status(res.statusCode || 200);
|
||||
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method](res.body);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
module.exports = function allowQueryParams(params) {
|
||||
module.exports = function allowQueryParams (params) {
|
||||
if (!Array.isArray(params)) {
|
||||
throw new Error('allowQueryParams must receive an Array of params');
|
||||
}
|
||||
return function allowQueryParamsMiddleware(req, res, next) {
|
||||
|
||||
return function allowQueryParamsMiddleware (req, res, next) {
|
||||
res.locals.allowedQueryParams = params;
|
||||
next();
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
module.exports = function authorizeMiddleware (authApi) {
|
||||
return function (req, res, next) {
|
||||
req.profiler.done('req2params.setup');
|
||||
|
||||
module.exports = function authorize (authApi) {
|
||||
return function authorizeMiddleware (req, res, next) {
|
||||
authApi.authorize(req, res, (err, authorized) => {
|
||||
req.profiler.done('authorize');
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
85
lib/cartodb/middleware/context/credentials.js
Normal file
85
lib/cartodb/middleware/context/credentials.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const basicAuth = require('basic-auth');
|
||||
|
||||
module.exports = function credentials () {
|
||||
return function credentialsMiddleware(req, res, next) {
|
||||
const apikeyCredentials = getApikeyCredentialsFromRequest(req);
|
||||
|
||||
res.locals.api_key = apikeyCredentials.token;
|
||||
res.locals.basicAuthUsername = apikeyCredentials.username;
|
||||
res.set('vary', 'Authorization'); //Honor Authorization header when caching.
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
|
||||
function getApikeyCredentialsFromRequest(req) {
|
||||
let apikeyCredentials = {
|
||||
token: null,
|
||||
username: null,
|
||||
};
|
||||
|
||||
for (let getter of apikeyGetters) {
|
||||
apikeyCredentials = getter(req);
|
||||
if (apikeyTokenFound(apikeyCredentials)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return apikeyCredentials;
|
||||
}
|
||||
|
||||
const apikeyGetters = [
|
||||
getApikeyTokenFromHeaderAuthorization,
|
||||
getApikeyTokenFromRequestQueryString,
|
||||
getApikeyTokenFromRequestBody,
|
||||
];
|
||||
|
||||
function getApikeyTokenFromHeaderAuthorization(req) {
|
||||
const credentials = basicAuth(req);
|
||||
|
||||
if (credentials) {
|
||||
return {
|
||||
username: credentials.username,
|
||||
token: credentials.pass
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
username: null,
|
||||
token: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getApikeyTokenFromRequestQueryString(req) {
|
||||
let token = null;
|
||||
|
||||
if (req.query && req.query.api_key) {
|
||||
token = req.query.api_key;
|
||||
} else if (req.query && req.query.map_key) {
|
||||
token = req.query.map_key;
|
||||
}
|
||||
|
||||
return {
|
||||
username: null,
|
||||
token: token,
|
||||
};
|
||||
}
|
||||
|
||||
function getApikeyTokenFromRequestBody(req) {
|
||||
let token = null;
|
||||
|
||||
if (req.body && req.body.api_key) {
|
||||
token = req.body.api_key;
|
||||
} else if (req.body && req.body.map_key) {
|
||||
token = req.body.map_key;
|
||||
}
|
||||
|
||||
return {
|
||||
username: null,
|
||||
token: token,
|
||||
};
|
||||
}
|
||||
|
||||
function apikeyTokenFound(apikey) {
|
||||
return !!apikey && !!apikey.token;
|
||||
}
|
||||
@@ -1,31 +1,30 @@
|
||||
const _ = require('underscore');
|
||||
|
||||
module.exports = function dbConnSetupMiddleware(pgConnection) {
|
||||
return function dbConnSetup(req, res, next) {
|
||||
const user = res.locals.user;
|
||||
module.exports = function dbConnSetup (pgConnection) {
|
||||
return function dbConnSetupMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
|
||||
pgConnection.setDBConn(user, res.locals, (err) => {
|
||||
req.profiler.done('dbConnSetup');
|
||||
|
||||
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);
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
const locals = require('./locals');
|
||||
const cleanUpQueryParams = require('./clean-up-query-params');
|
||||
const layergroupToken = require('./layergroup-token');
|
||||
const credentials = require('./credentials');
|
||||
const authorize = require('./authorize');
|
||||
const dbConnSetup = require('./db-conn-setup');
|
||||
|
||||
module.exports = function prepareContextMiddleware(authApi, pgConnection) {
|
||||
return [
|
||||
locals,
|
||||
locals(),
|
||||
cleanUpQueryParams(),
|
||||
layergroupToken,
|
||||
layergroupToken(),
|
||||
credentials(),
|
||||
authorize(authApi),
|
||||
dbConnSetup(pgConnection)
|
||||
];
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
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();
|
||||
const LayergroupToken = require('../../models/layergroup-token');
|
||||
const authErrorMessageTemplate = function (signer, user) {
|
||||
return `Cannot use map signature of user "${signer}" on db of user "${user}"`;
|
||||
};
|
||||
|
||||
module.exports = function layergroupToken () {
|
||||
return function layergroupTokenMiddleware (req, res, next) {
|
||||
if (!res.locals.token) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const user = res.locals.user;
|
||||
|
||||
const 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 !== user) {
|
||||
const err = new Error(authErrorMessageTemplate(res.locals.signer, user));
|
||||
err.type = 'auth';
|
||||
err.http_status = (req.query && req.query.callback) ? 200: 403;
|
||||
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
module.exports = function localsMiddleware(req, res, next) {
|
||||
// save req.params in res.locals
|
||||
res.locals = Object.assign(req.params || {}, res.locals);
|
||||
module.exports = function locals () {
|
||||
return function localsMiddleware (req, res, next) {
|
||||
res.locals = Object.assign(req.params || {}, res.locals);
|
||||
|
||||
next();
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
module.exports = function cors(extraHeaders) {
|
||||
return function(req, res, next) {
|
||||
var baseHeaders = "X-Requested-With, X-Prototype-Version, X-CSRF-Token";
|
||||
module.exports = function cors (extraHeaders) {
|
||||
return function corsMiddleware (req, res, next) {
|
||||
let baseHeaders = "X-Requested-With, X-Prototype-Version, X-CSRF-Token";
|
||||
|
||||
if(extraHeaders) {
|
||||
baseHeaders += ", " + extraHeaders;
|
||||
}
|
||||
|
||||
res.set("Access-Control-Allow-Origin", "*");
|
||||
res.set("Access-Control-Allow-Headers", baseHeaders);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
const LZMA = require('lzma').LZMA;
|
||||
|
||||
const lzmaWorker = new LZMA();
|
||||
module.exports = function lzma () {
|
||||
const lzmaWorker = new LZMA();
|
||||
|
||||
module.exports = function lzmaMiddleware(req, res, next) {
|
||||
if (!req.query.hasOwnProperty('lzma')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Decode (from base64)
|
||||
var lzma = new Buffer(req.query.lzma, 'base64')
|
||||
.toString('binary')
|
||||
.split('')
|
||||
.map(function(c) {
|
||||
return c.charCodeAt(0) - 128;
|
||||
});
|
||||
|
||||
// Decompress
|
||||
lzmaWorker.decompress(lzma, function(result) {
|
||||
try {
|
||||
delete req.query.lzma;
|
||||
Object.assign(req.query, JSON.parse(result));
|
||||
next();
|
||||
} catch (err) {
|
||||
next(new Error('Error parsing lzma as JSON: ' + err));
|
||||
return function lzmaMiddleware (req, res, next) {
|
||||
if (!req.query.hasOwnProperty('lzma')) {
|
||||
return next();
|
||||
}
|
||||
});
|
||||
|
||||
// Decode (from base64)
|
||||
var lzma = new Buffer(req.query.lzma, 'base64')
|
||||
.toString('binary')
|
||||
.split('')
|
||||
.map(function(c) {
|
||||
return c.charCodeAt(0) - 128;
|
||||
});
|
||||
|
||||
// Decompress
|
||||
lzmaWorker.decompress(lzma, function(result) {
|
||||
try {
|
||||
delete req.query.lzma;
|
||||
Object.assign(req.query, JSON.parse(result));
|
||||
|
||||
req.profiler.done('lzma');
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
next(new Error('Error parsing lzma as JSON: ' + err));
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
67
lib/cartodb/middleware/rate-limit.js
Normal file
67
lib/cartodb/middleware/rate-limit.js
Normal file
@@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
|
||||
const RATE_LIMIT_ENDPOINTS_GROUPS = {
|
||||
ANONYMOUS: 'anonymous',
|
||||
STATIC: 'static',
|
||||
STATIC_NAMED: 'static_named',
|
||||
DATAVIEW: 'dataview',
|
||||
DATAVIEW_SEARCH: 'dataview_search',
|
||||
ANALYSIS: 'analysis',
|
||||
ANALYSIS_CATALOG: 'analysis_catalog',
|
||||
TILE: 'tile',
|
||||
ATTRIBUTES: 'attributes',
|
||||
NAMED_LIST: 'named_list',
|
||||
NAMED_CREATE: 'named_create',
|
||||
NAMED_GET: 'named_get',
|
||||
NAMED: 'named',
|
||||
NAMED_UPDATE: 'named_update',
|
||||
NAMED_DELETE: 'named_delete',
|
||||
NAMED_TILES: 'named_tiles'
|
||||
};
|
||||
|
||||
function rateLimit(userLimitsApi, endpointGroup = null) {
|
||||
if (!isRateLimitEnabled(endpointGroup)) {
|
||||
return function rateLimitDisabledMiddleware(req, res, next) { next(); };
|
||||
}
|
||||
|
||||
return function rateLimitMiddleware(req, res, next) {
|
||||
userLimitsApi.getRateLimit(res.locals.user, endpointGroup, function (err, userRateLimit) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!userRateLimit) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const [isBlocked, limit, remaining, retry, reset] = userRateLimit;
|
||||
|
||||
res.set({
|
||||
'Carto-Rate-Limit-Limit': limit,
|
||||
'Carto-Rate-Limit-Remaining': remaining,
|
||||
'Retry-After': retry,
|
||||
'Carto-Rate-Limit-Reset': reset
|
||||
});
|
||||
|
||||
if (isBlocked) {
|
||||
const rateLimitError = new Error('You are over the limits.');
|
||||
rateLimitError.http_status = 429;
|
||||
rateLimitError.type = 'limit';
|
||||
rateLimitError.subtype = 'rate-limit';
|
||||
return next(rateLimitError);
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function isRateLimitEnabled(endpointGroup) {
|
||||
return global.environment.enabledFeatures.rateLimitsEnabled &&
|
||||
endpointGroup &&
|
||||
global.environment.enabledFeatures.rateLimitsByEndpoint[endpointGroup];
|
||||
}
|
||||
|
||||
module.exports = rateLimit;
|
||||
module.exports.RATE_LIMIT_ENDPOINTS_GROUPS = RATE_LIMIT_ENDPOINTS_GROUPS;
|
||||
@@ -2,10 +2,10 @@ const Profiler = require('../stats/profiler_proxy');
|
||||
const debug = require('debug')('windshaft:cartodb:stats');
|
||||
const onHeaders = require('on-headers');
|
||||
|
||||
module.exports = function statsMiddleware(options) {
|
||||
module.exports = function stats (options) {
|
||||
const { enabled = true, statsClient } = options;
|
||||
|
||||
return function stats(req, res, next) {
|
||||
return function statsMiddleware (req, res, next) {
|
||||
req.profiler = new Profiler({
|
||||
statsd_client: statsClient,
|
||||
profile: enabled
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
var CdbRequest = require('../models/cdb_request');
|
||||
var cdbRequest = new CdbRequest();
|
||||
const CdbRequest = require('../models/cdb_request');
|
||||
|
||||
module.exports = function userMiddleware(req, res, next) {
|
||||
res.locals.user = cdbRequest.userByReq(req);
|
||||
module.exports = function user () {
|
||||
const cdbRequest = new CdbRequest();
|
||||
|
||||
next();
|
||||
return function userMiddleware(req, res, next) {
|
||||
res.locals.user = cdbRequest.userByReq(req);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const timeoutErrorVectorTile = fs.readFileSync(__dirname + '/../../../assets/render-timeout-fallback.mvt');
|
||||
|
||||
module.exports = function vectorError() {
|
||||
|
||||
@@ -47,6 +47,10 @@ module.exports = class AggregationMapConfig extends MapConfig {
|
||||
return AggregationMapConfig.SUPPORTED_GEOMETRY_TYPES.includes(geometryType);
|
||||
}
|
||||
|
||||
static getAggregationGeometryColumn() {
|
||||
return aggregationQuery.GEOMETRY_COLUMN;
|
||||
}
|
||||
|
||||
constructor (user, config, connection, datasource) {
|
||||
super(config, datasource);
|
||||
|
||||
|
||||
@@ -131,7 +131,12 @@ const dimensionDefs = ctx => {
|
||||
// This is equivalent to `${256/ctx.res}*CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!))`
|
||||
// This is defined by the ctx.res parameter, which is the number of grid cells per tile linear dimension
|
||||
// (i.e. each tile is divided into ctx.res*ctx.res cells).
|
||||
const gridResolution = ctx => `(${256*0.00028/ctx.res}*!scale_denominator!)::double precision`;
|
||||
// We limit the the minimum resolution to avoid division by zero problems. The limit used is
|
||||
// the pixel size of zoom level 30 (i.e. 1/2*(30+8) of the full earth web-mercator extent), which is about 0.15 mm.
|
||||
const gridResolution = ctx => {
|
||||
const minimumResolution = 2*Math.PI*6378137/Math.pow(2,38);
|
||||
return `GREATEST(${256*0.00028/ctx.res}*!scale_denominator!, ${minimumResolution})::double precision`;
|
||||
};
|
||||
|
||||
// Notes:
|
||||
// * We need to filter spatially using !bbox! to make the queries efficient because
|
||||
@@ -211,6 +216,7 @@ const aggregationQueryTemplates = {
|
||||
GROUP BY _cdb_gx, _cdb_gy ${dimensionNames(ctx)}
|
||||
)
|
||||
SELECT
|
||||
row_number() over() AS cartodb_id,
|
||||
ST_SetSRID(ST_MakePoint((_cdb_gx+0.5)*res, (_cdb_gy+0.5)*res), 3857) AS the_geom_webmercator
|
||||
${dimensionNames(ctx)}
|
||||
${aggregateColumnNames(ctx)}
|
||||
@@ -249,3 +255,4 @@ const aggregationQueryTemplates = {
|
||||
};
|
||||
|
||||
module.exports.SUPPORTED_PLACEMENTS = Object.keys(aggregationQueryTemplates);
|
||||
module.exports.GEOMETRY_COLUMN = 'the_geom_webmercator';
|
||||
|
||||
@@ -2,19 +2,17 @@ const BaseDataview = require('./base');
|
||||
const debug = require('debug')('windshaft:dataview:aggregation');
|
||||
|
||||
const filteredQueryTpl = ctx => `
|
||||
filtered_source AS (
|
||||
SELECT *
|
||||
FROM (${ctx.query}) _cdb_filtered_source
|
||||
${ctx.aggregationColumn && ctx.isFloatColumn ? `
|
||||
WHERE
|
||||
${ctx.aggregationColumn} != 'infinity'::float
|
||||
AND
|
||||
${ctx.aggregationColumn} != '-infinity'::float
|
||||
AND
|
||||
${ctx.aggregationColumn} != 'NaN'::float` :
|
||||
''
|
||||
}
|
||||
)
|
||||
SELECT *
|
||||
FROM (${ctx.query}) _cdb_filtered_source
|
||||
${ctx.aggregationColumn && ctx.isFloatColumn ? `
|
||||
WHERE
|
||||
${ctx.aggregationColumn} != 'infinity'::float
|
||||
AND
|
||||
${ctx.aggregationColumn} != '-infinity'::float
|
||||
AND
|
||||
${ctx.aggregationColumn} != 'NaN'::float` :
|
||||
''
|
||||
}
|
||||
`;
|
||||
|
||||
const summaryQueryTpl = ctx => `
|
||||
@@ -43,8 +41,8 @@ const rankedCategoriesQueryTpl = ctx => `
|
||||
${ctx.column} AS category,
|
||||
${ctx.aggregationFn} AS value,
|
||||
row_number() OVER (ORDER BY ${ctx.aggregationFn} desc) as rank
|
||||
FROM filtered_source
|
||||
${ctx.aggregationColumn !== null ? `WHERE ${ctx.aggregationColumn} IS NOT NULL` : ''}
|
||||
FROM (${filteredQueryTpl(ctx)}) filtered_source
|
||||
WHERE ${ctx.aggregation === "count" ? `${ctx.column}` : `${ctx.aggregationColumn}`} IS NOT NULL
|
||||
GROUP BY ${ctx.column}
|
||||
ORDER BY 2 DESC
|
||||
)
|
||||
@@ -74,7 +72,7 @@ const specialNumericValuesColumns = () => `, nans_count, infinities_count`;
|
||||
|
||||
const rankedAggregationQueryTpl = ctx => `
|
||||
SELECT
|
||||
CAST(category AS text),
|
||||
category,
|
||||
value,
|
||||
false as agg,
|
||||
nulls_count,
|
||||
@@ -87,7 +85,7 @@ const rankedAggregationQueryTpl = ctx => `
|
||||
WHERE rank < ${ctx.limit}
|
||||
UNION ALL
|
||||
SELECT
|
||||
'Other' category,
|
||||
null category,
|
||||
${ctx.aggregation !== 'count' ? ctx.aggregation : 'sum'}(value) as value,
|
||||
true as agg,
|
||||
nulls_count,
|
||||
@@ -109,7 +107,7 @@ const rankedAggregationQueryTpl = ctx => `
|
||||
|
||||
const aggregationQueryTpl = ctx => `
|
||||
SELECT
|
||||
CAST(${ctx.column} AS text) AS category,
|
||||
${ctx.column} AS category,
|
||||
${ctx.aggregationFn} AS value,
|
||||
false as agg,
|
||||
nulls_count,
|
||||
@@ -120,7 +118,7 @@ const aggregationQueryTpl = ctx => `
|
||||
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
||||
FROM (${ctx.query}) _cdb_aggregation_all, summary, categories_summary_min_max, categories_summary_count
|
||||
GROUP BY
|
||||
category,
|
||||
${ctx.column},
|
||||
nulls_count,
|
||||
min_val,
|
||||
max_val,
|
||||
@@ -134,7 +132,6 @@ const aggregationFnQueryTpl = ctx => `${ctx.aggregation}(${ctx.aggregationColumn
|
||||
|
||||
const aggregationDataviewQueryTpl = ctx => `
|
||||
WITH
|
||||
${filteredQueryTpl(ctx)},
|
||||
${summaryQueryTpl(ctx)},
|
||||
${rankedCategoriesQueryTpl(ctx)},
|
||||
${categoriesSummaryMinMaxQueryTpl(ctx)},
|
||||
@@ -292,7 +289,13 @@ module.exports = class Aggregation extends BaseDataview {
|
||||
min: min_val,
|
||||
max: max_val,
|
||||
categoriesCount: categories_count,
|
||||
categories: result.rows.map(({ category, value, agg }) => ({ category, value, agg }))
|
||||
categories: result.rows.map(({ category, value, agg }) => {
|
||||
return {
|
||||
category: agg ? 'Other' : category,
|
||||
value,
|
||||
agg
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -122,7 +122,8 @@ module.exports = class AggregationMapConfigAdapter {
|
||||
}
|
||||
|
||||
const aggregationMetadata = queryUtils.getAggregationMetadata({
|
||||
query: layer.options.sql_raw ? layer.options.sql_raw : layer.options.sql
|
||||
query: layer.options.sql_raw ? layer.options.sql_raw : layer.options.sql,
|
||||
geometryColumn: AggregationMapConfig.getAggregationGeometryColumn()
|
||||
});
|
||||
|
||||
connection.query(aggregationMetadata, (err, res) => {
|
||||
@@ -132,7 +133,7 @@ module.exports = class AggregationMapConfigAdapter {
|
||||
|
||||
const result = res.rows[0] || {};
|
||||
|
||||
if (!AggregationMapConfig.supportsGeometryType(result.type)) {
|
||||
if (!mapConfig.isVectorOnlyMapConfig() && !AggregationMapConfig.supportsGeometryType(result.type)) {
|
||||
const message = unsupportedGeometryTypeErrorMessage({ geometryType: result.type });
|
||||
const error = new Error(message);
|
||||
error.type = 'layer';
|
||||
@@ -145,6 +146,10 @@ module.exports = class AggregationMapConfigAdapter {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (mapConfig.isVectorOnlyMapConfig() && !AggregationMapConfig.supportsGeometryType(result.type)) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
if (!mapConfig.doesLayerReachThreshold(index, result.count)) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
@@ -108,8 +108,7 @@ MapConfigNamedLayersAdapter.prototype.getMapConfig = function (user, requestMapC
|
||||
var dbAuth = {};
|
||||
|
||||
if (_.some(layers, isNamedTypeLayer)) {
|
||||
// Lazy load dbAuth
|
||||
this.pgConnection.setDBAuth(user, dbAuth, function(err) {
|
||||
this.pgConnection.setDBAuth(user, dbAuth, 'master', function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
@@ -232,19 +232,19 @@ function configHash(config) {
|
||||
module.exports.configHash = configHash;
|
||||
|
||||
NamedMapMapConfigProvider.prototype.setDBParams = function(cdbuser, params, callback) {
|
||||
var self = this;
|
||||
step(
|
||||
function setAuth() {
|
||||
self.pgConnection.setDBAuth(cdbuser, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
assert.ifError(err);
|
||||
self.pgConnection.setDBConn(cdbuser, params, this);
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
this.pgConnection.getDatabaseParams(cdbuser, (err, databaseParams) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
);
|
||||
|
||||
params.dbuser = databaseParams.dbuser;
|
||||
params.dbpass = databaseParams.dbpass;
|
||||
params.dbhost = databaseParams.dbhost;
|
||||
params.dbport = databaseParams.dbport;
|
||||
params.dbname = databaseParams.dbname;
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
NamedMapMapConfigProvider.prototype.getTemplateName = function() {
|
||||
|
||||
@@ -21,69 +21,208 @@ function ResourceLocator(environment) {
|
||||
|
||||
module.exports = ResourceLocator;
|
||||
|
||||
ResourceLocator.prototype.getUrls = function(username, resource) {
|
||||
ResourceLocator.prototype.getTileUrls = function(username, resourcePath) {
|
||||
if (this.resourcesUrlTemplates) {
|
||||
return this.getUrlsFromTemplate(username, resource);
|
||||
}
|
||||
var cdnDomain = getCdnDomain(this.environment.serverMetadata, resource);
|
||||
if (cdnDomain) {
|
||||
const urls = this.getUrlsFromTemplate(username, new TileResource(resourcePath));
|
||||
return {
|
||||
http: 'http://' + cdnDomain.http + '/' + username + '/api/v1/map/' + resource,
|
||||
https: 'https://' + cdnDomain.https + '/' + username + '/api/v1/map/' + resource
|
||||
http: Array.isArray(urls.http) ? urls.http : [urls.http],
|
||||
https: Array.isArray(urls.https) ? urls.https : [urls.https]
|
||||
};
|
||||
}
|
||||
var cdnUrls = getCdnUrls(this.environment.serverMetadata, username, new TileResource(resourcePath));
|
||||
if (cdnUrls) {
|
||||
return cdnUrls;
|
||||
} else {
|
||||
var port = this.environment.port;
|
||||
return {
|
||||
http: 'http://' + username + '.' + 'localhost.lan:' + port + '/api/v1/map/' + resource
|
||||
http: [`http://${username}.localhost.lan:${port}/api/v1/map/${resourcePath}`]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
ResourceLocator.prototype.getTemplateUrls = function(username, resourcePath) {
|
||||
if (this.resourcesUrlTemplates) {
|
||||
return this.getUrlsFromTemplate(username, new TemplateResource(resourcePath), true);
|
||||
}
|
||||
var cdnUrls = getCdnUrls(this.environment.serverMetadata, username, new TemplateResource(resourcePath));
|
||||
if (cdnUrls) {
|
||||
return cdnUrls;
|
||||
} else {
|
||||
var port = this.environment.port;
|
||||
return {
|
||||
http: {
|
||||
urlTemplate: `http://${username}.localhost.lan:${port}/api/v1/map/${resourcePath}`,
|
||||
subdomains: []
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
ResourceLocator.prototype.getUrlsFromTemplate = function(username, resource) {
|
||||
var urls = {};
|
||||
var cdnDomain = getCdnDomain(this.environment.serverMetadata, resource) || {};
|
||||
ResourceLocator.prototype.getUrls = function(username, resourcePath) {
|
||||
if (this.resourcesUrlTemplates) {
|
||||
return this.getUrlsFromTemplate(username, new Resource(resourcePath));
|
||||
}
|
||||
var cdnUrls = getCdnUrls(this.environment.serverMetadata, username, new Resource(resourcePath));
|
||||
if (cdnUrls) {
|
||||
return cdnUrls;
|
||||
} else {
|
||||
var port = this.environment.port;
|
||||
return {
|
||||
http: `http://${username}.localhost.lan:${port}/api/v1/map/${resourcePath}`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if (this.resourcesUrlTemplates.http) {
|
||||
urls.http = this.resourcesUrlTemplates.http({
|
||||
cdn_url: cdnDomain.http,
|
||||
function urlForTemplate(tpl, username, cdnDomain, resource, templated) {
|
||||
cdnDomain = cdnDomain || {};
|
||||
if (templated) {
|
||||
return {
|
||||
urlTemplate: tpl({
|
||||
cdn_url: (cdnDomain.hasOwnProperty('urlTemplate') ? cdnDomain.urlTemplate : cdnDomain),
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource.getPath()
|
||||
}),
|
||||
subdomains: cdnDomain.subdomains || []
|
||||
};
|
||||
}
|
||||
if (Array.isArray(cdnDomain)) {
|
||||
return cdnDomain.map(d => tpl({
|
||||
cdn_url: d,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource
|
||||
resource: resource.getPath()
|
||||
}));
|
||||
} else {
|
||||
return tpl({
|
||||
cdn_url: cdnDomain,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource.getPath()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ResourceLocator.prototype.getUrlsFromTemplate = function(username, resource, templated) {
|
||||
var urls = {};
|
||||
var cdnDomain = getCdnDomain(this.environment.serverMetadata, resource) || {};
|
||||
if (this.resourcesUrlTemplates.http) {
|
||||
urls.http = urlForTemplate(this.resourcesUrlTemplates.http, username, cdnDomain.http, resource, templated);
|
||||
}
|
||||
if (this.resourcesUrlTemplates.https) {
|
||||
urls.https = this.resourcesUrlTemplates.https({
|
||||
cdn_url: cdnDomain.https,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource
|
||||
});
|
||||
urls.https = urlForTemplate(this.resourcesUrlTemplates.https, username, cdnDomain.https, resource, templated);
|
||||
}
|
||||
|
||||
return urls;
|
||||
};
|
||||
|
||||
class Resource {
|
||||
constructor (resourcePath) {
|
||||
this.resourcePath = resourcePath;
|
||||
}
|
||||
|
||||
getPath () {
|
||||
return this.resourcePath;
|
||||
}
|
||||
|
||||
getDomain (domain, subdomains) {
|
||||
if (!subdomains) {
|
||||
return domain;
|
||||
}
|
||||
return domain.replace('{s}', subdomain(subdomains, this.resourcePath));
|
||||
}
|
||||
|
||||
getUrl (baseUrl, username, subdomains) {
|
||||
let urls = getUrl(baseUrl, username, this.resourcePath);
|
||||
if (subdomains) {
|
||||
urls = urls.replace('{s}', subdomain(subdomains, this.resourcePath));
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
}
|
||||
|
||||
class TileResource extends Resource {
|
||||
constructor (resourcePath) {
|
||||
super(resourcePath);
|
||||
}
|
||||
|
||||
getDomain (domain, subdomains) {
|
||||
if (!subdomains) {
|
||||
return domain;
|
||||
}
|
||||
return subdomains.map(s => domain.replace('{s}', s));
|
||||
}
|
||||
|
||||
getUrl (baseUrl, username, subdomains) {
|
||||
if (!subdomains) {
|
||||
return [super.getUrl(baseUrl, username)];
|
||||
}
|
||||
return subdomains.map(subdomain => {
|
||||
return getUrl(baseUrl, username, this.resourcePath)
|
||||
.replace('{s}', subdomain);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class TemplateResource extends Resource {
|
||||
constructor (resourcePath) {
|
||||
super(resourcePath);
|
||||
}
|
||||
|
||||
getDomain (domain, subdomains) {
|
||||
return {
|
||||
urlTemplate: domain,
|
||||
subdomains: subdomains || []
|
||||
};
|
||||
}
|
||||
|
||||
getUrl (baseUrl, username, subdomains) {
|
||||
return {
|
||||
urlTemplate: getUrl(baseUrl, username, this.resourcePath),
|
||||
subdomains: subdomains || []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getUrl(baseUrl, username, path) {
|
||||
return `${baseUrl}/${username}/api/v1/map/${path}`;
|
||||
}
|
||||
|
||||
function getCdnUrls(serverMetadata, username, resource) {
|
||||
if (serverMetadata && serverMetadata.cdn_url) {
|
||||
var cdnUrl = serverMetadata.cdn_url;
|
||||
var httpUrls = resource.getUrl(`http://${cdnUrl.http}`, username);
|
||||
var httpsUrls = resource.getUrl(`https://${cdnUrl.https}`, username);
|
||||
if (cdnUrl.templates) {
|
||||
var templates = cdnUrl.templates;
|
||||
httpUrls = resource.getUrl(templates.http.url, username, templates.http.subdomains);
|
||||
httpsUrls = resource.getUrl(templates.https.url, username, templates.https.subdomains);
|
||||
}
|
||||
return {
|
||||
http: httpUrls,
|
||||
https: httpsUrls,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCdnDomain(serverMetadata, resource) {
|
||||
if (serverMetadata && serverMetadata.cdn_url) {
|
||||
var cdnUrl = serverMetadata.cdn_url;
|
||||
var http = cdnUrl.http;
|
||||
var https = cdnUrl.https;
|
||||
var httpDomain = resource.getDomain(cdnUrl.http);
|
||||
var httpsDomain = resource.getDomain(cdnUrl.https);
|
||||
if (cdnUrl.templates) {
|
||||
var templates = cdnUrl.templates;
|
||||
var httpUrlTemplate = templates.http.url;
|
||||
var httpsUrlTemplate = templates.https.url;
|
||||
http = httpUrlTemplate
|
||||
.replace(/^(http[s]*:\/\/)/, '')
|
||||
.replace('{s}', subdomain(templates.http.subdomains, resource));
|
||||
https = httpsUrlTemplate
|
||||
.replace(/^(http[s]*:\/\/)/, '')
|
||||
.replace('{s}', subdomain(templates.https.subdomains, resource));
|
||||
httpDomain = httpUrlTemplate.replace(/^(http[s]*:\/\/)/, '');
|
||||
httpDomain = resource.getDomain(httpDomain, templates.http.subdomains);
|
||||
httpsDomain = httpsUrlTemplate.replace(/^(http[s]*:\/\/)/, '');
|
||||
httpsDomain = resource.getDomain(httpsDomain, templates.https.subdomains);
|
||||
}
|
||||
return {
|
||||
http: http,
|
||||
https: https,
|
||||
http: httpDomain,
|
||||
https: httpsDomain,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -76,7 +76,8 @@ module.exports = function(serverOptions) {
|
||||
var userLimitsApi = new UserLimitsApi(metadataBackend, {
|
||||
limits: {
|
||||
cacheOnTimeout: serverOptions.renderer.mapnik.limits.cacheOnTimeout || false,
|
||||
render: serverOptions.renderer.mapnik.limits.render || 0
|
||||
render: serverOptions.renderer.mapnik.limits.render || 0,
|
||||
rateLimitsEnabled: global.environment.enabledFeatures.rateLimitsEnabled
|
||||
}
|
||||
});
|
||||
|
||||
@@ -256,12 +257,13 @@ module.exports = function(serverOptions) {
|
||||
previewBackend,
|
||||
surrogateKeysCache,
|
||||
tablesExtentApi,
|
||||
metadataBackend
|
||||
metadataBackend,
|
||||
userLimitsApi
|
||||
).register(app);
|
||||
|
||||
new controller.NamedMapsAdmin(authApi, templateMaps).register(app);
|
||||
new controller.NamedMapsAdmin(authApi, templateMaps, userLimitsApi).register(app);
|
||||
|
||||
new controller.Analyses(prepareContext).register(app);
|
||||
new controller.Analyses(prepareContext, userLimitsApi).register(app);
|
||||
|
||||
new controller.ServerInfo(versions).register(app);
|
||||
|
||||
@@ -377,7 +379,7 @@ function bootstrap(opts) {
|
||||
statsClient: global.statsClient
|
||||
}));
|
||||
|
||||
app.use(lzmaMiddleware);
|
||||
app.use(lzmaMiddleware());
|
||||
|
||||
// temporary measure until we upgrade to newer version expressjs so we can check err.status
|
||||
app.use(function(err, req, res, next) {
|
||||
|
||||
@@ -15,6 +15,7 @@ var rendererConfig = _.defaults(global.environment.renderer || {}, {
|
||||
bufferSize: 64,
|
||||
snapToGrid: false,
|
||||
clipByBox2d: false,
|
||||
metrics: false,
|
||||
limits: {}
|
||||
},
|
||||
http: {}
|
||||
|
||||
19
lib/cartodb/utils/icu_data_env_setter.js
Normal file
19
lib/cartodb/utils/icu_data_env_setter.js
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
const glob = require('glob');
|
||||
const path = require('path');
|
||||
|
||||
// See https://github.com/CartoDB/support/issues/984
|
||||
// CartoCSS properties text-wrap-width/text-wrap-character not working
|
||||
function setICUEnvVariable() {
|
||||
if (process.env.ICU_DATA === undefined) {
|
||||
const regexedPath = '/node_modules/mapnik/lib/binding/*/share/mapnik/icu/';
|
||||
const directory = glob.sync(path.join(__dirname, '../../..', regexedPath));
|
||||
|
||||
if (directory && directory.length > 0) {
|
||||
process.env.ICU_DATA = directory[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = setICUEnvVariable;
|
||||
168
lib/cartodb/utils/layergroup-metadata.js
Normal file
168
lib/cartodb/utils/layergroup-metadata.js
Normal file
@@ -0,0 +1,168 @@
|
||||
module.exports = class LayergroupMetadata {
|
||||
constructor (resourceLocator) {
|
||||
this.resourceLocator = resourceLocator;
|
||||
}
|
||||
|
||||
// TODO this should take into account several URL patterns
|
||||
addDataviewsAndWidgetsUrls (username, layergroup, mapConfig) {
|
||||
this._addDataviewsUrls(username, layergroup, mapConfig);
|
||||
this._addWidgetsUrl(username, layergroup, mapConfig);
|
||||
}
|
||||
|
||||
_addDataviewsUrls (username, layergroup, mapConfig) {
|
||||
layergroup.metadata.dataviews = layergroup.metadata.dataviews || {};
|
||||
var dataviews = mapConfig.dataviews || {};
|
||||
|
||||
Object.keys(dataviews).forEach((dataviewName) => {
|
||||
var resource = layergroup.layergroupid + '/dataview/' + dataviewName;
|
||||
layergroup.metadata.dataviews[dataviewName] = {
|
||||
url: this.resourceLocator.getUrls(username, resource)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
_addWidgetsUrl (username, layergroup, mapConfig) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map((layer, layerIndex) => {
|
||||
var mapConfigLayer = mapConfig.layers[layerIndex];
|
||||
if (mapConfigLayer.options && mapConfigLayer.options.widgets) {
|
||||
layer.widgets = layer.widgets || {};
|
||||
Object.keys(mapConfigLayer.options.widgets).forEach((widgetName) => {
|
||||
var resource = layergroup.layergroupid + '/' + layerIndex + '/widget/' + widgetName;
|
||||
layer.widgets[widgetName] = {
|
||||
type: mapConfigLayer.options.widgets[widgetName].type,
|
||||
url: this.resourceLocator.getUrls(username, resource)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addAnalysesMetadata (username, layergroup, analysesResults, includeQuery) {
|
||||
includeQuery = includeQuery || false;
|
||||
analysesResults = analysesResults || [];
|
||||
layergroup.metadata.analyses = [];
|
||||
|
||||
analysesResults.forEach((analysis) => {
|
||||
var nodes = analysis.getNodes();
|
||||
layergroup.metadata.analyses.push({
|
||||
nodes: nodes.reduce((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;
|
||||
}, {})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addAggregationContextMetadata (layergroup, mapConfig, context) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
if (context.aggregation && Array.isArray(context.aggregation.layers)) {
|
||||
layer.meta.aggregation = context.aggregation.layers[layerIndex];
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addTileJsonMetadata (layergroup, user, mapconfig) {
|
||||
const isVectorOnlyMapConfig = mapconfig.isVectorOnlyMapConfig();
|
||||
let hasMapnikLayers = false;
|
||||
|
||||
layergroup.metadata.layers.forEach((layerMetadata, index) => {
|
||||
const layerId = mapconfig.getLayerId(index);
|
||||
const rasterResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.png`;
|
||||
if (mapconfig.layerType(index) === 'mapnik') {
|
||||
hasMapnikLayers = true;
|
||||
const vectorResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.mvt`;
|
||||
const layerTilejson = {
|
||||
vector: this._getTilejson(this.resourceLocator.getTileUrls(user, vectorResource))
|
||||
};
|
||||
if (!isVectorOnlyMapConfig) {
|
||||
let grids = null;
|
||||
const layer = mapconfig.getLayer(index);
|
||||
if (layer.options.interactivity) {
|
||||
const gridResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.grid.json`;
|
||||
grids = this.resourceLocator.getTileUrls(user, gridResource);
|
||||
}
|
||||
layerTilejson.raster = this._getTilejson(
|
||||
this.resourceLocator.getTileUrls(user, rasterResource),
|
||||
grids
|
||||
);
|
||||
}
|
||||
layerMetadata.tilejson = layerTilejson;
|
||||
} else {
|
||||
layerMetadata.tilejson = {
|
||||
raster: this._getTilejson(this.resourceLocator.getTileUrls(user, rasterResource))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const tilejson = {};
|
||||
const url = {};
|
||||
|
||||
if (hasMapnikLayers) {
|
||||
const vectorResource = `${layergroup.layergroupid}/{z}/{x}/{y}.mvt`;
|
||||
tilejson.vector = this._getTilejson(
|
||||
this.resourceLocator.getTileUrls(user, vectorResource)
|
||||
);
|
||||
url.vector = this._getTemplateUrl(this.resourceLocator.getTemplateUrls(user, vectorResource));
|
||||
|
||||
if (!isVectorOnlyMapConfig) {
|
||||
const rasterResource = `${layergroup.layergroupid}/{z}/{x}/{y}.png`;
|
||||
tilejson.raster = this._getTilejson(
|
||||
this.resourceLocator.getTileUrls(user, rasterResource)
|
||||
);
|
||||
url.raster = this._getTemplateUrl(this.resourceLocator.getTemplateUrls(user, rasterResource));
|
||||
}
|
||||
}
|
||||
|
||||
layergroup.metadata.tilejson = tilejson;
|
||||
layergroup.metadata.url = url;
|
||||
}
|
||||
|
||||
_getTilejson(tiles, grids) {
|
||||
const tilejson = {
|
||||
tilejson: '2.2.0',
|
||||
tiles: tiles.https || tiles.http
|
||||
};
|
||||
|
||||
if (grids) {
|
||||
tilejson.grids = grids.https || grids.http;
|
||||
}
|
||||
|
||||
return tilejson;
|
||||
}
|
||||
|
||||
_getTemplateUrl(url) {
|
||||
return url.https || url.http;
|
||||
}
|
||||
|
||||
addTurboCartoContextMetadata(layergroup, mapConfig, context) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
if (context.turboCarto && Array.isArray(context.turboCarto.layers)) {
|
||||
layer.meta.cartocss_meta = context.turboCarto.layers[layerIndex];
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -32,8 +32,8 @@ module.exports.getAggregationMetadata = ctx => `
|
||||
${getQueryRowEstimation(ctx.query)}
|
||||
),
|
||||
geometryType AS (
|
||||
SELECT ST_GeometryType(the_geom) as geom_type
|
||||
FROM (${ctx.query}) AS __cdb_query WHERE the_geom IS NOT NULL LIMIT 1
|
||||
SELECT ST_GeometryType(${ctx.geometryColumn}) as geom_type
|
||||
FROM (${ctx.query}) AS __cdb_query WHERE ${ctx.geometryColumn} IS NOT NULL LIMIT 1
|
||||
)
|
||||
SELECT
|
||||
rows AS count,
|
||||
|
||||
19
package.json
19
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "4.6.0",
|
||||
"version": "6.0.0",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
@@ -24,29 +24,31 @@
|
||||
"Simon Martin <simon@carto.com>"
|
||||
],
|
||||
"dependencies": {
|
||||
"basic-auth": "^2.0.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"camshaft": "0.60.0",
|
||||
"camshaft": "0.61.8",
|
||||
"cartodb-psql": "0.10.2",
|
||||
"cartodb-query-tables": "0.3.0",
|
||||
"cartodb-redis": "0.14.0",
|
||||
"cartodb-redis": "1.0.0",
|
||||
"debug": "^3.1.0",
|
||||
"dot": "~1.0.2",
|
||||
"express": "~4.16.0",
|
||||
"fastly-purge": "~1.0.1",
|
||||
"glob": "^7.1.2",
|
||||
"log4js": "cartodb/log4js-node#cdb",
|
||||
"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",
|
||||
"redis-mpool": "0.5.0",
|
||||
"request": "2.85.0",
|
||||
"semver": "~5.3.0",
|
||||
"step": "~0.0.6",
|
||||
"step-profiler": "~0.3.0",
|
||||
"turbo-carto": "0.20.2",
|
||||
"underscore": "~1.6.0",
|
||||
"windshaft": "4.2.0",
|
||||
"windshaft": "4.6.0",
|
||||
"yargs": "~5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -55,7 +57,7 @@
|
||||
"mocha": "~3.4.1",
|
||||
"moment": "~2.18.1",
|
||||
"nock": "~2.11.0",
|
||||
"redis": "~0.12.1",
|
||||
"redis": "2.8.0",
|
||||
"strftime": "~0.8.2"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -66,8 +68,7 @@
|
||||
"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 ps --filter status=dead --filter status=exited -aq | xargs -r docker rm -v",
|
||||
"docker-bash": "docker run -it -v `pwd`:/srv cartoimages/windshaft-testing bash",
|
||||
"docker-publish": "docker push cartoimages/windshaft-carto-testing"
|
||||
"docker-bash": "docker run -it -v `pwd`:/srv cartoimages/windshaft-testing bash"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9",
|
||||
|
||||
20
run_tests.sh
20
run_tests.sh
@@ -6,6 +6,7 @@ OPT_DROP_REDIS=yes # drop the redis test environment
|
||||
OPT_DROP_PGSQL=yes # drop the PostgreSQL test environment
|
||||
OPT_COVERAGE=no # run tests with coverage
|
||||
OPT_DOWNLOAD_SQL=yes # download a fresh copy of sql files
|
||||
OPT_REDIS_CELL=yes # download redis cell
|
||||
|
||||
export PGAPPNAME=cartodb_tiler_tester
|
||||
|
||||
@@ -49,6 +50,17 @@ die() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
get_redis_cell() {
|
||||
if test x"$OPT_REDIS_CELL" = xyes; then
|
||||
echo "Downloading redis-cell"
|
||||
curl -L https://github.com/brandur/redis-cell/releases/download/v0.2.2/redis-cell-v0.2.2-x86_64-unknown-linux-gnu.tar.gz --output redis-cell.tar.gz > /dev/null 2>&1
|
||||
tar xvzf redis-cell.tar.gz > /dev/null 2>&1
|
||||
mv libredis_cell.so ${BASEDIR}/test/support/libredis_cell.so
|
||||
rm redis-cell.tar.gz
|
||||
rm libredis_cell.d
|
||||
fi
|
||||
}
|
||||
|
||||
trap 'cleanup_and_exit' 1 2 3 5 9 13
|
||||
|
||||
while [ -n "$1" ]; do
|
||||
@@ -88,6 +100,10 @@ while [ -n "$1" ]; do
|
||||
OPT_CREATE_PGSQL=no
|
||||
shift
|
||||
continue
|
||||
elif test "$1" = "--norediscell"; then
|
||||
OPT_REDIS_CELL=no
|
||||
shift
|
||||
continue
|
||||
else
|
||||
break
|
||||
fi
|
||||
@@ -99,14 +115,16 @@ if [ -z "$1" ]; then
|
||||
echo " --nocreate do not create the test environment on start" >&2
|
||||
echo " --nodrop do not drop the test environment on exit" >&2
|
||||
echo " --with-coverage use istanbul to determine code coverage" >&2
|
||||
echo " --norediscell do not download redis-cell" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TESTS=$@
|
||||
|
||||
if test x"$OPT_CREATE_REDIS" = xyes; then
|
||||
get_redis_cell
|
||||
echo "Starting redis on port ${REDIS_PORT}"
|
||||
echo "port ${REDIS_PORT}" | redis-server - > ${BASEDIR}/test.log &
|
||||
echo "port ${REDIS_PORT}" | redis-server - --loadmodule ${BASEDIR}/test/support/libredis_cell.so > ${BASEDIR}/test.log &
|
||||
PID_REDIS=$!
|
||||
echo ${PID_REDIS} > ${BASEDIR}/redis.pid
|
||||
fi
|
||||
|
||||
@@ -92,6 +92,14 @@ describe('aggregation', function () {
|
||||
}
|
||||
`;
|
||||
|
||||
const POINTS_SQL_ONLY_WEBMERCATOR = `
|
||||
select
|
||||
x + 4 as cartodb_id,
|
||||
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
|
||||
`;
|
||||
|
||||
function createVectorMapConfig (layers = [
|
||||
{
|
||||
type: 'cartodb',
|
||||
@@ -385,6 +393,44 @@ describe('aggregation', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('skip default aggregation by setting `aggregation: false` for just one layer', function (done) {
|
||||
const mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1,
|
||||
aggregation: {
|
||||
threshold: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_2,
|
||||
aggregation: false
|
||||
}
|
||||
}
|
||||
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
|
||||
this.testClient.getLayergroup((err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(typeof body.metadata, 'object');
|
||||
assert.ok(Array.isArray(body.metadata.layers));
|
||||
|
||||
assert.equal(body.metadata.layers[0].meta.aggregation.mvt, true);
|
||||
assert.equal(body.metadata.layers[1].meta.aggregation.mvt, false);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('when the aggregation param is not valid should respond with error', function (done) {
|
||||
const mapConfig = createVectorMapConfig([
|
||||
{
|
||||
@@ -471,6 +517,8 @@ describe('aggregation', function () {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POLYGONS_SQL_1,
|
||||
cartocss: '#layer { marker-width: [value]; }',
|
||||
cartocss_version: '2.3.0',
|
||||
aggregation: {
|
||||
threshold: 1
|
||||
}
|
||||
@@ -1233,6 +1281,200 @@ describe('aggregation', function () {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should skip aggregation w/o failing when is Vector Only MapConfig and layer has polygons',
|
||||
function (done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POLYGONS_SQL_1
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
|
||||
this.testClient.getLayergroup((err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(typeof body.metadata, 'object');
|
||||
assert.ok(Array.isArray(body.metadata.layers));
|
||||
|
||||
body.metadata.layers.forEach(layer => assert.ok(!layer.meta.aggregation.mvt));
|
||||
body.metadata.layers.forEach(layer => assert.ok(!layer.meta.aggregation.png));
|
||||
|
||||
const options = {
|
||||
format: 'mvt'
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, options, (err, res, tile) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const tileJSON = tile.toJSON();
|
||||
|
||||
assert.equal(tileJSON[0].features.length, 7);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip aggregation for polygons (w/o failing) and aggregate when the layer has points',
|
||||
function (done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POLYGONS_SQL_1
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1,
|
||||
aggregation: {
|
||||
threshold: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
|
||||
this.testClient.getLayergroup((err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(typeof body.metadata, 'object');
|
||||
assert.ok(Array.isArray(body.metadata.layers));
|
||||
|
||||
assert.equal(body.metadata.layers[0].meta.aggregation.mvt, false);
|
||||
assert.equal(body.metadata.layers[1].meta.aggregation.mvt, true);
|
||||
|
||||
const options = {
|
||||
format: 'mvt'
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, options, (err, res, tile) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const tileJSON = tile.toJSON();
|
||||
|
||||
assert.equal(tileJSON[0].features.length, 7);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
['centroid', 'point-sample', 'point-grid'].forEach(placement => {
|
||||
it(`cartodb_id should be present in ${placement} aggregation`, function(done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1,
|
||||
aggregation: {
|
||||
placement: placement,
|
||||
threshold: 1
|
||||
},
|
||||
cartocss: '#layer { marker-width: 1; }',
|
||||
cartocss_version: '2.3.0',
|
||||
interactivity: ['cartodb_id']
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
this.testClient.getLayergroup((err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(typeof body.metadata, 'object');
|
||||
assert.ok(Array.isArray(body.metadata.layers));
|
||||
|
||||
body.metadata.layers.forEach(layer => assert.ok(layer.meta.aggregation.mvt));
|
||||
body.metadata.layers.forEach(layer => assert.ok(layer.meta.aggregation.png));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should only require the_geom_webmercator for aggregation', function (done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_ONLY_WEBMERCATOR,
|
||||
aggregation: {
|
||||
threshold: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
|
||||
this.testClient.getLayergroup((err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(typeof body.metadata, 'object');
|
||||
assert.ok(Array.isArray(body.metadata.layers));
|
||||
|
||||
body.metadata.layers.forEach(layer => assert.ok(layer.meta.aggregation.mvt));
|
||||
body.metadata.layers.forEach(layer => assert.ok(!layer.meta.aggregation.png));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('aggregation should work with attributes', function (done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1,
|
||||
cartocss: '#layer { marker-width: 7; }',
|
||||
cartocss_version: '2.3.0',
|
||||
aggregation: {
|
||||
threshold: 1
|
||||
},
|
||||
attributes: {
|
||||
id: 'cartodb_id',
|
||||
columns: [
|
||||
'value'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
|
||||
this.testClient.getLayergroup((err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(typeof body.metadata, 'object');
|
||||
assert.ok(Array.isArray(body.metadata.layers));
|
||||
|
||||
body.metadata.layers.forEach(layer => assert.ok(layer.meta.aggregation.mvt));
|
||||
body.metadata.layers.forEach(layer => assert.ok(layer.meta.aggregation.png));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -188,7 +188,7 @@ describe('analysis-layers error cases', function() {
|
||||
]
|
||||
);
|
||||
|
||||
var testClient = new TestClient(mapConfig, 11111);
|
||||
var testClient = new TestClient(mapConfig); //No apikey provided -> using default public apikey
|
||||
|
||||
testClient.getLayergroup({ response: AUTH_ERROR_RESPONSE }, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
181
test/acceptance/auth/authorization-fallback.js
Normal file
181
test/acceptance/auth/authorization-fallback.js
Normal file
@@ -0,0 +1,181 @@
|
||||
//Remove this file when Auth fallback is not used anymore
|
||||
// AUTH_FALLBACK
|
||||
|
||||
const assert = require('../../support/assert');
|
||||
const testHelper = require('../../support/test_helper');
|
||||
const CartodbWindshaft = require('../../../lib/cartodb/server');
|
||||
const serverOptions = require('../../../lib/cartodb/server_options');
|
||||
const server = new CartodbWindshaft(serverOptions);
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
function singleLayergroupConfig(sql, cartocss) {
|
||||
return {
|
||||
version: '1.7.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss: cartocss,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function createRequest(layergroup, userHost, apiKey) {
|
||||
var url = layergroupUrl;
|
||||
if (apiKey) {
|
||||
url += '?api_key=' + apiKey;
|
||||
}
|
||||
return {
|
||||
url: url,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: userHost || 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
};
|
||||
}
|
||||
|
||||
var layergroupUrl = '/api/v1/map';
|
||||
var pointSqlMaster = "select * from test_table_private_1";
|
||||
var pointSqlPublic = "select * from test_table";
|
||||
var keysToDelete;
|
||||
|
||||
describe('authorization fallback', function () {
|
||||
beforeEach(function () {
|
||||
keysToDelete = {};
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
it("succeed with master", function (done) {
|
||||
var layergroup = singleLayergroupConfig(pointSqlMaster, '#layer { marker-fill:red; }');
|
||||
|
||||
assert.response(server,
|
||||
createRequest(layergroup, 'user_previous_to_project_auth', '4444'),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function (res, err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:user_previous_to_project_auth:mapviews:global'] = 5;
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it("succeed with default - sending default_public", function (done) {
|
||||
var layergroup = singleLayergroupConfig(pointSqlPublic, '#layer { marker-fill:red; }');
|
||||
|
||||
assert.response(server,
|
||||
createRequest(layergroup, 'user_previous_to_project_auth', 'default_public'),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function (res, err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:user_previous_to_project_auth:mapviews:global'] = 5;
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("succeed with default - sending no api key token", function (done) {
|
||||
var layergroup = singleLayergroupConfig(pointSqlPublic, '#layer { marker-fill:red; }');
|
||||
|
||||
assert.response(server,
|
||||
createRequest(layergroup, 'user_previous_to_project_auth'),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function (res, err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:user_previous_to_project_auth:mapviews:global'] = 5;
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("succeed with non-existent api key - defaults to default", function (done) {
|
||||
var layergroup = singleLayergroupConfig(pointSqlPublic, '#layer { marker-fill:red; }');
|
||||
|
||||
assert.response(server,
|
||||
createRequest(layergroup, 'user_previous_to_project_auth', 'THIS-API-KEY-DOESNT-EXIST'),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function (res, err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:user_previous_to_project_auth:mapviews:global'] = 5;
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("fail with default", function (done) {
|
||||
var layergroup = singleLayergroupConfig(pointSqlMaster, '#layer { marker-fill:red; }');
|
||||
|
||||
assert.response(server,
|
||||
createRequest(layergroup, 'user_previous_to_project_auth', 'default_public'),
|
||||
{
|
||||
status: 403
|
||||
},
|
||||
function (res, err) {
|
||||
assert.ifError(err);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("fail with non-existent api key - defaults to default", function (done) {
|
||||
var layergroup = singleLayergroupConfig(pointSqlMaster, '#layer { marker-fill:red; }');
|
||||
|
||||
assert.response(server,
|
||||
createRequest(layergroup, 'user_previous_to_project_auth', 'THIS-API-KEY-DOESNT-EXIST'),
|
||||
{
|
||||
status: 403
|
||||
},
|
||||
function (res, err) {
|
||||
assert.ifError(err);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
431
test/acceptance/auth/authorization.js
Normal file
431
test/acceptance/auth/authorization.js
Normal file
@@ -0,0 +1,431 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
const assert = require('../../support/assert');
|
||||
const TestClient = require('../../support/test-client');
|
||||
const mapnik = require('windshaft').mapnik;
|
||||
|
||||
const PERMISSION_DENIED_RESPONSE = {
|
||||
status: 403,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
describe('authorization', function() {
|
||||
it('should create a layergroup with regular apikey token', function(done) {
|
||||
const apikeyToken = 'regular1';
|
||||
const mapConfig = {
|
||||
version: '1.7.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select * FROM test_table_localhost_regular1',
|
||||
cartocss: TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const testClient = new TestClient(mapConfig, apikeyToken);
|
||||
|
||||
testClient.getLayergroup(function (err, layergroupResult) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.ok(layergroupResult.layergroupid);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create and get a named map tile using a regular apikey token', function (done) {
|
||||
const apikeyToken = 'regular1';
|
||||
const mapConfig = {
|
||||
version: '1.7.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select * FROM test_table_localhost_regular1',
|
||||
cartocss: TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const testClient = new TestClient(mapConfig, apikeyToken);
|
||||
|
||||
testClient.getTile(0, 0, 0, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.ok(tile instanceof mapnik.Image);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail getting a named map tile with default apikey token', function (done) {
|
||||
const apikeyTokenCreate = 'regular1';
|
||||
const apikeyTokenGet = 'default_public';
|
||||
const mapConfig = {
|
||||
version: '1.7.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select * FROM test_table_localhost_regular1',
|
||||
cartocss: TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const testClientCreate = new TestClient(mapConfig, apikeyTokenCreate);
|
||||
testClientCreate.getLayergroup(function (err, layergroupResult) {
|
||||
assert.ifError(err);
|
||||
const layergroupId = layergroupResult.layergroupid;
|
||||
|
||||
const testClientGet = new TestClient({}, apikeyTokenGet);
|
||||
|
||||
const params = {
|
||||
layergroupid: layergroupId,
|
||||
response: PERMISSION_DENIED_RESPONSE
|
||||
};
|
||||
|
||||
testClientGet.getTile(0, 0, 0, params, function(err, res, body) {
|
||||
|
||||
assert.ifError(err);
|
||||
|
||||
assert.ok(body.hasOwnProperty('errors'));
|
||||
assert.equal(body.errors.length, 1);
|
||||
assert.ok(body.errors[0].match(/permission denied/), body.errors[0]);
|
||||
|
||||
testClientGet.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail creating a layergroup with default apikey token', function (done) {
|
||||
const apikeyToken = 'default_public';
|
||||
const mapConfig = {
|
||||
version: '1.7.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select * FROM test_table_localhost_regular1',
|
||||
cartocss: TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const testClient = new TestClient(mapConfig, apikeyToken);
|
||||
|
||||
testClient.getLayergroup({ response: { status: 403 } }, function (err, layergroupResult) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.ok(layergroupResult.hasOwnProperty('errors'));
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
assert.ok(layergroupResult.errors[0].match(/permission denied/), layergroupResult.errors[0]);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a layergroup with default apikey token', function (done) {
|
||||
const apikeyToken = 'default_public';
|
||||
const mapConfig = {
|
||||
version: '1.7.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select * FROM test_table',
|
||||
cartocss: TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const testClient = new TestClient(mapConfig, apikeyToken);
|
||||
|
||||
testClient.getLayergroup(function (err, layergroupResult) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.ok(layergroupResult.layergroupid);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create and get a tile with default apikey token', function (done) {
|
||||
const apikeyToken = 'default_public';
|
||||
const mapConfig = {
|
||||
version: '1.7.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select * FROM test_table',
|
||||
cartocss: TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const testClient = new TestClient(mapConfig, apikeyToken);
|
||||
|
||||
testClient.getTile(0, 0, 0, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.ok(tile instanceof mapnik.Image);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail if apikey does not grant access to table', function (done) {
|
||||
const mapConfig = {
|
||||
version: '1.7.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select * FROM test_table_localhost_regular1',
|
||||
cartocss: TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const testClient = new TestClient(mapConfig); //no apikey provided, using default
|
||||
|
||||
testClient.getLayergroup({ response: { status: 403 } }, function (err, layergroupResult) { //TODO 401
|
||||
assert.ifError(err);
|
||||
|
||||
assert.ok(layergroupResult.hasOwnProperty('errors'));
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
assert.ok(layergroupResult.errors[0].match(/permission denied/), layergroupResult.errors[0]);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should forbide access to API if API key does not grant access', function (done) {
|
||||
const apikeyToken = 'regular2';
|
||||
const mapConfig = {
|
||||
version: '1.7.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select * FROM test_table_localhost_regular1',
|
||||
cartocss: TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const testClient = new TestClient(mapConfig, apikeyToken);
|
||||
|
||||
testClient.getLayergroup({ response: { status: 403 } }, function (err, layergroupResult) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.ok(layergroupResult.hasOwnProperty('errors'));
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
assert.ok(layergroupResult.errors[0].match(/Forbidden/), layergroupResult.errors[0]);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a layergroup with a source analysis using a default apikey token', function (done) {
|
||||
const apikeyToken = 'default_public';
|
||||
const mapConfig = {
|
||||
version: '1.7.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
source: {
|
||||
id: 'HEAD'
|
||||
},
|
||||
cartocss: TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
],
|
||||
analyses: [
|
||||
{
|
||||
id: 'HEAD',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from populated_places_simple_reduced'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const testClient = new TestClient(mapConfig, apikeyToken);
|
||||
|
||||
testClient.getLayergroup(function (err, layergroupResult) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.ok(layergroupResult.layergroupid);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a layergroup with a source analysis using a regular apikey token', function (done) {
|
||||
const apikeyToken = 'regular1';
|
||||
const mapConfig = {
|
||||
version: '1.7.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
source: {
|
||||
id: 'HEAD'
|
||||
},
|
||||
cartocss: TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
],
|
||||
analyses: [
|
||||
{
|
||||
id: 'HEAD',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from test_table_localhost_regular1'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const testClient = new TestClient(mapConfig, apikeyToken);
|
||||
|
||||
testClient.getLayergroup(function (err, layergroupResult) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.ok(layergroupResult.layergroupid);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
// Warning: TBA
|
||||
it('should create a layergroup with a buffer analysis using a regular apikey token', function (done) {
|
||||
const apikeyToken = 'regular1';
|
||||
const mapConfig = {
|
||||
version: '1.7.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
source: {
|
||||
id: 'HEAD1'
|
||||
},
|
||||
cartocss: TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
],
|
||||
analyses: [
|
||||
{
|
||||
id: "HEAD1",
|
||||
type: "buffer",
|
||||
params: {
|
||||
source: {
|
||||
id: 'HEAD2',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from test_table_localhost_regular1'
|
||||
}
|
||||
},
|
||||
radius: 50000
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const testClient = new TestClient(mapConfig, apikeyToken);
|
||||
|
||||
testClient.getLayergroup(function (err, layergroupResult) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.ok(layergroupResult.layergroupid);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create and get a named map tile using a regular apikey token', function (done) {
|
||||
const apikeyToken = 'regular1';
|
||||
|
||||
const template = {
|
||||
version: '0.0.1',
|
||||
name: 'auth-api-template',
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
type: 'number',
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
version: '1.7.0',
|
||||
layers: [{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select * from test_table_localhost_regular1',
|
||||
cartocss: TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version: '2.3.0',
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const testClient = new TestClient(template, apikeyToken);
|
||||
|
||||
testClient.getTile(0, 0, 0, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.ok(tile instanceof mapnik.Image);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail creating a named map using a regular apikey token and a private table', function (done) {
|
||||
const apikeyToken = 'regular1';
|
||||
|
||||
const template = {
|
||||
version: '0.0.1',
|
||||
name: 'auth-api-template-private',
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
type: 'number',
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
version: '1.7.0',
|
||||
layers: [{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced_private',
|
||||
cartocss: TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version: '2.3.0',
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const testClient = new TestClient(template, apikeyToken);
|
||||
|
||||
testClient.getTile(0, 0, 0, { response: PERMISSION_DENIED_RESPONSE }, function (err, res, body) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.ok(body.hasOwnProperty('errors'));
|
||||
assert.equal(body.errors.length, 1);
|
||||
assert.ok(body.errors[0].match(/permission denied/), body.errors[0]);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
1
test/acceptance/cache/cache_headers.js
vendored
1
test/acceptance/cache/cache_headers.js
vendored
@@ -155,6 +155,7 @@ describe('get requests with cache headers', function() {
|
||||
|
||||
assert.ok(res.headers['x-cache-channel']);
|
||||
assert.ok(res.headers['surrogate-key']);
|
||||
assert.equal(res.headers.vary, 'Authorization');
|
||||
if (expectedCacheHeaders) {
|
||||
validateXChannelHeaders(res.headers, expectedCacheHeaders);
|
||||
assert.equal(res.headers['surrogate-key'], expectedCacheHeaders.surrogate_keys);
|
||||
|
||||
@@ -70,12 +70,8 @@ describe('aggregations happy cases', function() {
|
||||
].join(' UNION ALL ');
|
||||
|
||||
operations.forEach(function (operation) {
|
||||
var not = operation === 'count' ? ' not ' : ' ';
|
||||
var description = 'should' +
|
||||
not +
|
||||
'handle NULL values in category and aggregation columns using "' +
|
||||
operation +
|
||||
'" as aggregation operation';
|
||||
var description = 'should handle NULL values in category and aggregation columns using "' +
|
||||
operation + '" as aggregation operation';
|
||||
|
||||
it(description, function (done) {
|
||||
this.testClient = new TestClient(aggregationOperationMapConfig(operation, query, 'cat', 'val'));
|
||||
@@ -96,12 +92,7 @@ describe('aggregations happy cases', function() {
|
||||
}
|
||||
});
|
||||
|
||||
if (operation === 'count') {
|
||||
assert.ok(hasNullCategory, 'aggregation has not a category NULL');
|
||||
} else {
|
||||
assert.ok(!hasNullCategory, 'aggregation has category NULL');
|
||||
}
|
||||
|
||||
assert.ok(!hasNullCategory, 'aggregation has category NULL');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -425,3 +416,79 @@ describe('aggregation dataview tuned by categories query param', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe('Count aggregation', 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: 'count'
|
||||
}
|
||||
}
|
||||
},
|
||||
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 null
|
||||
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 null
|
||||
END AS cat
|
||||
FROM generate_series(1, 1000) x
|
||||
`
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
it(`should handle null values correctly when aggregationColumn isn't provided`, function (done) {
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('categories', { own_filter: 0, categories: 0 }, (err, dataview) => {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.categories.length, 3);
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it(`should handle null values correctly when aggregationColumn is provided`, function (done) {
|
||||
mapConfig.dataviews.categories.options.aggregationColumn = 'val';
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('categories', { own_filter: 0, categories: 0 }, (err, dataview) => {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.categories.length, 3);
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
69
test/acceptance/label-wrap.js
Normal file
69
test/acceptance/label-wrap.js
Normal file
@@ -0,0 +1,69 @@
|
||||
require('../support/test_helper');
|
||||
var TestClient = require('../support/test-client');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var IMAGE_TOLERANCE = 5;
|
||||
|
||||
describe('CartoCSS wrap', function () {
|
||||
const options = {
|
||||
sql: `
|
||||
SELECT
|
||||
5 as cartodb_id,
|
||||
ST_Transform(ST_SetSRID(ST_MakePoint(-57.65625,-15.6230368),4326),3857) as the_geom_webmercator,
|
||||
ST_SetSRID(ST_MakePoint(-57.65625,-15.62303683),4326) as the_geom,
|
||||
'South America' as continent
|
||||
`,
|
||||
cartocss: `
|
||||
#continent_points::labels {
|
||||
text-name: [continent];
|
||||
text-face-name: 'DejaVu Sans Book';
|
||||
text-size: 10;
|
||||
text-fill: lighten(#000,40);
|
||||
text-transform: uppercase;
|
||||
text-wrap-width: 30;
|
||||
text-character-spacing: 2;
|
||||
text-placement: point;
|
||||
text-placement-type: dummy;
|
||||
[zoom >= 3]{
|
||||
text-character-spacing: 2;
|
||||
text-size: 11;
|
||||
}
|
||||
}
|
||||
`,
|
||||
cartocss_version: '3.0.12'
|
||||
};
|
||||
|
||||
const type = 'mapnik';
|
||||
|
||||
const mapConfig = {
|
||||
version: '1.6.0',
|
||||
layers: [
|
||||
{
|
||||
type,
|
||||
id: 'layerLabel',
|
||||
options
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
var keysToDelete;
|
||||
|
||||
beforeEach(function () {
|
||||
keysToDelete = {};
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
}
|
||||
});
|
||||
|
||||
it("Label should be text-wrapped", function (done) {
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
this.testClient.getTile(1, 0, 1, { layers: [0] }, (err, res, body) => {
|
||||
var textWrapPath = './test/fixtures/text_wrap.png';
|
||||
assert.imageIsSimilarToFile(body, textWrapPath, IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1336,7 +1336,7 @@ describe(suiteName, function() {
|
||||
status: 403
|
||||
},
|
||||
function(res) {
|
||||
assert.ok(res.body.match(/permission denied for relation test_table_private_1/));
|
||||
assert.ok(res.body.match(/permission denied for .+?test_table_private_1/));
|
||||
done();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -34,6 +34,48 @@ return function () {
|
||||
serverOptions.renderer.mvt.usePostGIS = originalUsePostGIS;
|
||||
});
|
||||
|
||||
describe('named map tile', function () {
|
||||
it('should get default named vector tile', function (done) {
|
||||
const apikeyToken = 1234;
|
||||
const templateName = `mvt-template-${usePostGIS ? 'postgis' : 'mapnik'}`;
|
||||
const template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
type: 'number',
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
version: '1.7.0',
|
||||
layers: [{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced limit 10',
|
||||
cartocss: TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version: '2.3.0',
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
const testClient = new TestClient(template, apikeyToken);
|
||||
|
||||
testClient.getNamedTile(templateName, 0, 0, 0, 'mvt', {}, (err, res, tile) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const tileJSON = tile.toJSON();
|
||||
|
||||
assert.equal(tileJSON[0].features.length, 10);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('analysis-layers-dataviews-mvt', function () {
|
||||
|
||||
function createMapConfig(layers, dataviews, analysis) {
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('named_layers', function() {
|
||||
});
|
||||
|
||||
beforeEach(function(done) {
|
||||
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: true};
|
||||
global.environment.enabledFeatures.cdbQueryTablesFromPostgres = true;
|
||||
templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
@@ -125,7 +125,7 @@ describe('named_layers', function() {
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: false};
|
||||
global.environment.enabledFeatures.cdbQueryTablesFromPostgres = false;
|
||||
templateMaps.delTemplate(username, nestedNamedMapTemplateName, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
@@ -659,7 +659,7 @@ describe('named_layers', function() {
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.ok(parsedBody.errors[0].match(/permission denied for relation test_table_private_1/));
|
||||
assert.ok(parsedBody.errors[0].match(/permission denied for .+?test_table_private_1/));
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
@@ -125,7 +125,7 @@ describe('attributes', function() {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors);
|
||||
var msg = parsed.errors[0];
|
||||
assert.ok(msg.match(/0 features.*identified by fid -666/), msg);
|
||||
assert.equal(msg, "Multiple features (0) identified by 'i' = -666 in layer 1");
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
|
||||
@@ -27,6 +27,7 @@ module.exports = _.extend({}, serverOptions, {
|
||||
snapToGrid: false,
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
scale_factors: [1, 2],
|
||||
metrics: false,
|
||||
limits: {
|
||||
render: 0,
|
||||
cacheOnTimeout: true
|
||||
|
||||
@@ -257,6 +257,14 @@ describe('torque boundary points', function() {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
|
||||
var parsed = JSON.parse(res.body);
|
||||
/* Order the JSON first by descending x__uint8 and ascending
|
||||
* y__uint8 */
|
||||
parsed.sort(function(a,b) {
|
||||
if (a.x__uint8 === b.x__uint8) {
|
||||
return (a.y__uint8 > b.y__uint8);
|
||||
}
|
||||
return (a.x__uint8 < b.x__uint8);
|
||||
});
|
||||
|
||||
var i = 0;
|
||||
tileRequest.expects.forEach(function(expected) {
|
||||
@@ -424,7 +432,7 @@ describe('torque boundary points', function() {
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
|
||||
assert.deepEqual(parsed, [
|
||||
assert.deepEqual(parsed.sort(function(a,b){return a.x__uint8 > b.x__uint8;}), [
|
||||
{
|
||||
x__uint8: 47,
|
||||
y__uint8: 127,
|
||||
@@ -438,7 +446,6 @@ describe('torque boundary points', function() {
|
||||
dates__uint16: [0]
|
||||
}
|
||||
]);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
260
test/acceptance/rate-limit.test.js
Normal file
260
test/acceptance/rate-limit.test.js
Normal file
@@ -0,0 +1,260 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
const assert = require('../support/assert');
|
||||
const redis = require('redis');
|
||||
const RedisPool = require('redis-mpool');
|
||||
const cartodbRedis = require('cartodb-redis');
|
||||
const TestClient = require('../support/test-client');
|
||||
const UserLimitsApi = require('../../lib/cartodb/api/user_limits_api');
|
||||
const rateLimitMiddleware = require('../../lib/cartodb/middleware/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimitMiddleware;
|
||||
|
||||
let userLimitsApi;
|
||||
let rateLimit;
|
||||
let redisClient;
|
||||
let testClient;
|
||||
let keysToDelete = ['user:localhost:mapviews:global'];
|
||||
const user = 'localhost';
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
ST_Transform('SRID=4326;POINT(-180 85.05112877)'::geometry, 3857) the_geom_webmercator,
|
||||
1 cartodb_id,
|
||||
2 val
|
||||
`;
|
||||
|
||||
const createMapConfig = ({
|
||||
version = '1.6.0',
|
||||
type = 'cartodb',
|
||||
sql = query,
|
||||
cartocss = TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version = '2.3.0',
|
||||
interactivity = 'cartodb_id',
|
||||
countBy = 'cartodb_id'
|
||||
} = {}) => ({
|
||||
version,
|
||||
layers: [{
|
||||
type,
|
||||
options: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
cartocss,
|
||||
cartocss_version,
|
||||
interactivity
|
||||
}
|
||||
}],
|
||||
analyses: [
|
||||
{
|
||||
id: 'a0',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: sql
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
count: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: countBy,
|
||||
operation: 'count'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function setLimit(count, period, burst) {
|
||||
redisClient.SELECT(8, err => {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `limits:rate:store:${user}:maps:${RATE_LIMIT_ENDPOINTS_GROUPS.ANONYMOUS}`;
|
||||
redisClient.rpush(key, burst);
|
||||
redisClient.rpush(key, count);
|
||||
redisClient.rpush(key, period);
|
||||
keysToDelete.push(key);
|
||||
});
|
||||
}
|
||||
|
||||
function getReqAndRes() {
|
||||
return {
|
||||
req: {},
|
||||
res: {
|
||||
headers: {},
|
||||
set(headers) {
|
||||
this.headers = headers;
|
||||
},
|
||||
locals: {
|
||||
user: 'localhost'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function assertGetLayergroupRequest (status, limit, remaining, reset, retry, done = null) {
|
||||
const response = {
|
||||
status,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Carto-Rate-Limit-Limit': limit,
|
||||
'Carto-Rate-Limit-Remaining': remaining,
|
||||
'Carto-Rate-Limit-Reset': reset,
|
||||
'Retry-After': retry
|
||||
}
|
||||
};
|
||||
|
||||
testClient.getLayergroup({ response }, err => {
|
||||
assert.ifError(err);
|
||||
if (done) {
|
||||
setTimeout(done, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function assertRateLimitRequest (status, limit, remaining, reset, retry, done = null) {
|
||||
const { req, res } = getReqAndRes();
|
||||
rateLimit(req, res, function (err) {
|
||||
assert.deepEqual(res.headers, {
|
||||
"Carto-Rate-Limit-Limit": limit,
|
||||
"Carto-Rate-Limit-Remaining": remaining,
|
||||
"Carto-Rate-Limit-Reset": reset,
|
||||
"Retry-After": retry
|
||||
});
|
||||
|
||||
if(status === 200) {
|
||||
assert.ifError(err);
|
||||
} else {
|
||||
assert.ok(err);
|
||||
assert.equal(err.message, 'You are over the limits.');
|
||||
assert.equal(err.http_status, 429);
|
||||
assert.equal(err.type, 'limit');
|
||||
assert.equal(err.subtype, 'rate-limit');
|
||||
}
|
||||
|
||||
if (done) {
|
||||
setTimeout(done, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe('rate limit', function() {
|
||||
before(function() {
|
||||
global.environment.enabledFeatures.rateLimitsEnabled = true;
|
||||
global.environment.enabledFeatures.rateLimitsByEndpoint.anonymous = true;
|
||||
|
||||
redisClient = redis.createClient(global.environment.redis.port);
|
||||
testClient = new TestClient(createMapConfig(), 1234);
|
||||
});
|
||||
|
||||
after(function() {
|
||||
global.environment.enabledFeatures.rateLimitsEnabled = false;
|
||||
global.environment.enabledFeatures.rateLimitsByEndpoint.anonymous = false;
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
keysToDelete.forEach( key => {
|
||||
redisClient.del(key);
|
||||
});
|
||||
|
||||
redisClient.SELECT(0, () => {
|
||||
redisClient.del('user:localhost:mapviews:global');
|
||||
|
||||
redisClient.SELECT(5, () => {
|
||||
redisClient.del('user:localhost:mapviews:global');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not be rate limited', function (done) {
|
||||
const count = 1;
|
||||
const period = 1;
|
||||
const burst = 1;
|
||||
setLimit(count, period, burst);
|
||||
|
||||
assertGetLayergroupRequest(200, '2', '1', '1', '-1', done);
|
||||
});
|
||||
|
||||
it("1 req/sec: 2 req/seg should be limited", function(done) {
|
||||
const count = 1;
|
||||
const period = 1;
|
||||
const burst = 1;
|
||||
setLimit(count, period, burst);
|
||||
|
||||
assertGetLayergroupRequest(200, '2', '1', '1', '-1');
|
||||
setTimeout( () => assertGetLayergroupRequest(200, '2', '0', '1', '-1'), 250);
|
||||
setTimeout( () => assertGetLayergroupRequest(429, '2', '0', '1', '0'), 500);
|
||||
setTimeout( () => assertGetLayergroupRequest(429, '2', '0', '1', '0'), 750);
|
||||
setTimeout( () => assertGetLayergroupRequest(429, '2', '0', '1', '0'), 950);
|
||||
setTimeout( () => assertGetLayergroupRequest(200, '2', '0', '1', '-1', done), 1050);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('rate limit middleware', function () {
|
||||
before(function (done) {
|
||||
global.environment.enabledFeatures.rateLimitsEnabled = true;
|
||||
global.environment.enabledFeatures.rateLimitsByEndpoint.anonymous = true;
|
||||
|
||||
const redisPool = new RedisPool(global.environment.redis);
|
||||
const metadataBackend = cartodbRedis({ pool: redisPool });
|
||||
userLimitsApi = new UserLimitsApi(metadataBackend, {
|
||||
limits: {
|
||||
rateLimitsEnabled: global.environment.enabledFeatures.rateLimitsEnabled
|
||||
}
|
||||
});
|
||||
rateLimit = rateLimitMiddleware(userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.ANONYMOUS);
|
||||
|
||||
redisClient = redis.createClient(global.environment.redis.port);
|
||||
testClient = new TestClient(createMapConfig(), 1234);
|
||||
|
||||
|
||||
const count = 1;
|
||||
const period = 1;
|
||||
const burst = 0;
|
||||
setLimit(count, period, burst);
|
||||
|
||||
setTimeout(done, 1000);
|
||||
});
|
||||
|
||||
after(function () {
|
||||
global.environment.enabledFeatures.rateLimitsEnabled = false;
|
||||
global.environment.enabledFeatures.rateLimitsByEndpoint.anonymous = false;
|
||||
|
||||
keysToDelete.forEach(key => {
|
||||
redisClient.del(key);
|
||||
});
|
||||
});
|
||||
|
||||
it("1 req/sec: 2 req/seg should be limited", function (done) {
|
||||
assertRateLimitRequest(200, 1, 0, 1, -1);
|
||||
setTimeout( () => assertRateLimitRequest(429, 1, 0, 0, 0), 250);
|
||||
setTimeout( () => assertRateLimitRequest(429, 1, 0, 0, 0), 500);
|
||||
setTimeout( () => assertRateLimitRequest(429, 1, 0, 0, 0), 750);
|
||||
setTimeout( () => assertRateLimitRequest(429, 1, 0, 0, 0), 950);
|
||||
setTimeout( () => assertRateLimitRequest(200, 1, 0, 1, -1, done), 1050);
|
||||
});
|
||||
|
||||
it("1 req/sec: 2 req/seg should be limited, removing SHA script from Redis", function (done) {
|
||||
userLimitsApi.metadataBackend.redisCmd(
|
||||
8,
|
||||
'SCRIPT',
|
||||
['FLUSH'],
|
||||
function () {
|
||||
assertRateLimitRequest(200, 1, 0, 1, -1);
|
||||
setTimeout( () => assertRateLimitRequest(429, 1, 0, 0, 0), 500);
|
||||
setTimeout( () => assertRateLimitRequest(429, 1, 0, 0, 0), 500);
|
||||
setTimeout( () => assertRateLimitRequest(429, 1, 0, 0, 0), 750);
|
||||
setTimeout( () => assertRateLimitRequest(429, 1, 0, 0, 0), 950);
|
||||
setTimeout( () => assertRateLimitRequest(200, 1, 0, 1, -1, done), 1050);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
219
test/acceptance/tilejson.js
Normal file
219
test/acceptance/tilejson.js
Normal file
@@ -0,0 +1,219 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
const assert = require('../support/assert');
|
||||
const TestClient = require('../support/test-client');
|
||||
|
||||
describe('tilejson', function() {
|
||||
|
||||
function tilejsonValidation(tilejson, shouldHaveGrid = false) {
|
||||
assert.equal(tilejson.tilejson, '2.2.0');
|
||||
|
||||
assert.ok(Array.isArray(tilejson.tiles), JSON.stringify(tilejson));
|
||||
assert.ok(tilejson.tiles.length > 0);
|
||||
|
||||
if (shouldHaveGrid) {
|
||||
assert.ok(Array.isArray(tilejson.grids));
|
||||
assert.ok(tilejson.grids.length > 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const sql = 'SELECT * FROM populated_places_simple_reduced';
|
||||
const cartocss = TestClient.CARTOCSS.POINTS;
|
||||
const cartocss_version = '3.0.12';
|
||||
|
||||
const RASTER_LAYER = {
|
||||
options: {
|
||||
sql, cartocss, cartocss_version
|
||||
}
|
||||
};
|
||||
const RASTER_INTERACTIVITY_LAYER = {
|
||||
options: {
|
||||
sql, cartocss, cartocss_version,
|
||||
interactivity: ['cartodb_id']
|
||||
}
|
||||
};
|
||||
const VECTOR_LAYER = {
|
||||
options: {
|
||||
sql
|
||||
}
|
||||
};
|
||||
const PLAIN_LAYER = {
|
||||
type: 'plain',
|
||||
options: {
|
||||
color: '#000000'
|
||||
}
|
||||
};
|
||||
|
||||
function mapConfig(layers) {
|
||||
return {
|
||||
version: '1.7.0',
|
||||
layers: Array.isArray(layers) ? layers : [layers]
|
||||
};
|
||||
}
|
||||
|
||||
describe('per layer', function() {
|
||||
it('should expose raster + vector tilejson for raster layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(RASTER_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 1);
|
||||
|
||||
const layer = metadata.layers[0];
|
||||
assert.deepEqual(Object.keys(layer.tilejson), ['vector', 'raster']);
|
||||
|
||||
Object.keys(layer.tilejson).forEach(k => {
|
||||
tilejsonValidation(layer.tilejson[k]);
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose just the vector tilejson vector only layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(VECTOR_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 1);
|
||||
|
||||
const layer = metadata.layers[0];
|
||||
assert.deepEqual(Object.keys(layer.tilejson), ['vector']);
|
||||
|
||||
Object.keys(layer.tilejson).forEach(k => {
|
||||
tilejsonValidation(layer.tilejson[k]);
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose just the raster tilejson plain layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(PLAIN_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 1);
|
||||
|
||||
const layer = metadata.layers[0];
|
||||
assert.deepEqual(Object.keys(layer.tilejson), ['raster']);
|
||||
|
||||
Object.keys(layer.tilejson).forEach(k => {
|
||||
tilejsonValidation(layer.tilejson[k]);
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose grids for the raster layer with interactivity', function(done) {
|
||||
var testClient = new TestClient(mapConfig(RASTER_INTERACTIVITY_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 1);
|
||||
|
||||
const layer = metadata.layers[0];
|
||||
assert.deepEqual(Object.keys(layer.tilejson), ['vector', 'raster']);
|
||||
|
||||
tilejsonValidation(layer.tilejson.vector);
|
||||
tilejsonValidation(layer.tilejson.raster, true);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with several layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig([RASTER_LAYER, RASTER_INTERACTIVITY_LAYER]));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 2);
|
||||
|
||||
assert.deepEqual(Object.keys(metadata.layers[0].tilejson), ['vector', 'raster']);
|
||||
tilejsonValidation(metadata.layers[0].tilejson.vector);
|
||||
tilejsonValidation(metadata.layers[0].tilejson.raster);
|
||||
|
||||
assert.deepEqual(Object.keys(metadata.layers[1].tilejson), ['vector', 'raster']);
|
||||
tilejsonValidation(metadata.layers[1].tilejson.vector);
|
||||
tilejsonValidation(metadata.layers[1].tilejson.raster, true);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('root tilejson', function() {
|
||||
|
||||
it('should expose just the `vector` tilejson and URL when for vector only mapnik layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(VECTOR_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
const tilejson = metadata.tilejson;
|
||||
assert.deepEqual(Object.keys(tilejson), ['vector']);
|
||||
|
||||
Object.keys(tilejson).forEach(k => {
|
||||
tilejsonValidation(tilejson[k]);
|
||||
});
|
||||
|
||||
const url = metadata.url;
|
||||
assert.deepEqual(Object.keys(url), ['vector']);
|
||||
|
||||
assert.ok(url.vector.urlTemplate);
|
||||
assert.ok(url.vector.subdomains);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose just the `vector` and `raster` tilejson and urls for mapnik layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(RASTER_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
const tilejson = metadata.tilejson;
|
||||
assert.deepEqual(Object.keys(tilejson), ['vector', 'raster']);
|
||||
|
||||
Object.keys(tilejson).forEach(k => {
|
||||
tilejsonValidation(tilejson[k]);
|
||||
});
|
||||
|
||||
const url = metadata.url;
|
||||
assert.deepEqual(Object.keys(url), ['vector', 'raster']);
|
||||
|
||||
assert.ok(url.vector.urlTemplate);
|
||||
assert.ok(url.vector.subdomains);
|
||||
|
||||
assert.ok(url.raster.urlTemplate);
|
||||
assert.ok(url.raster.subdomains);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -88,7 +88,7 @@ describe('turbo-carto regressions', function() {
|
||||
var turboCartoError = layergroup.errors_with_context[0];
|
||||
assert.ok(turboCartoError);
|
||||
assert.equal(turboCartoError.type, 'layer');
|
||||
assert.ok(turboCartoError.message.match(/permission\sdenied\sfor\srelation\stest_table_private_1/));
|
||||
assert.ok(turboCartoError.message.match(/permission\sdenied\sfor\s.+?test_table_private_1/));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
BIN
test/fixtures/text_wrap.png
vendored
Normal file
BIN
test/fixtures/text_wrap.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
test/fixtures/text_wrap_bad.png
vendored
Normal file
BIN
test/fixtures/text_wrap_bad.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -131,6 +131,19 @@ HMSET rails:users:cartodb250user id ${TESTUSERID} \
|
||||
map_key 4321
|
||||
EOF
|
||||
|
||||
|
||||
# Remove this block when Auth fallback is not used anymore
|
||||
# AUTH_FALLBACK
|
||||
# A user to test auth fallback to no api keys mode
|
||||
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 5
|
||||
HMSET rails:users:user_previous_to_project_auth id ${TESTUSERID} \
|
||||
database_name "${TEST_DB}" \
|
||||
database_host "localhost" \
|
||||
database_password "${TESTPASS}" \
|
||||
database_publicuser "${PUBLICUSER}"\
|
||||
map_key 4444
|
||||
EOF
|
||||
|
||||
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 0
|
||||
HSET rails:${TEST_DB}:my_table infowindow "this, that, the other"
|
||||
HSET rails:${TEST_DB}:test_table_private_1 privacy "0"
|
||||
@@ -138,4 +151,77 @@ EOF
|
||||
|
||||
fi
|
||||
|
||||
# API keys ==============================
|
||||
|
||||
# User localhost -----------------------
|
||||
|
||||
# API Key Master
|
||||
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 5
|
||||
HMSET api_keys:localhost:1234 \
|
||||
user "localhost" \
|
||||
type "master" \
|
||||
grants_sql "true" \
|
||||
grants_maps "true" \
|
||||
database_role "${TESTUSER}" \
|
||||
database_password "${TESTPASS}"
|
||||
EOF
|
||||
|
||||
# API Key Default public
|
||||
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 5
|
||||
HMSET api_keys:localhost:default_public \
|
||||
user "localhost" \
|
||||
type "default" \
|
||||
grants_sql "true" \
|
||||
grants_maps "true" \
|
||||
database_role "test_windshaft_publicuser" \
|
||||
database_password "public"
|
||||
EOF
|
||||
|
||||
# API Key Regular
|
||||
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 5
|
||||
HMSET api_keys:localhost:regular1 \
|
||||
user "localhost" \
|
||||
type "regular" \
|
||||
grants_sql "true" \
|
||||
grants_maps "true" \
|
||||
database_role "test_windshaft_regular1" \
|
||||
database_password "regular1"
|
||||
EOF
|
||||
|
||||
# API Key Regular 2 no Maps API access, only to check grants permissions to the API
|
||||
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 5
|
||||
HMSET api_keys:localhost:regular2 \
|
||||
user "localhost" \
|
||||
type "regular" \
|
||||
grants_sql "true" \
|
||||
grants_maps "false" \
|
||||
database_role "test_windshaft_publicuser" \
|
||||
database_password "public"
|
||||
EOF
|
||||
|
||||
# User cartodb250user -----------------------
|
||||
|
||||
# API Key Master
|
||||
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 5
|
||||
HMSET api_keys:cartodb250user:4321 \
|
||||
user "localhost" \
|
||||
type "master" \
|
||||
grants_sql "true" \
|
||||
grants_maps "true" \
|
||||
database_role "${TESTUSER}" \
|
||||
database_password "${TESTPASS}"
|
||||
EOF
|
||||
|
||||
# API Key Default
|
||||
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 5
|
||||
HMSET api_keys:cartodb250user:default_public \
|
||||
user "localhost" \
|
||||
type "default" \
|
||||
grants_sql "true" \
|
||||
grants_maps "true" \
|
||||
database_role "test_windshaft_publicuser" \
|
||||
database_password "public"
|
||||
EOF
|
||||
|
||||
|
||||
echo "Finished preparing data. Ready to run tests"
|
||||
|
||||
@@ -23,6 +23,12 @@ CREATE USER :PUBLICUSER WITH PASSWORD ':PUBLICPASS';
|
||||
DROP USER IF EXISTS :TESTUSER;
|
||||
CREATE USER :TESTUSER WITH PASSWORD ':TESTPASS';
|
||||
|
||||
-- regular user role 1
|
||||
DROP USER IF EXISTS test_windshaft_regular1;
|
||||
CREATE USER test_windshaft_regular1 WITH PASSWORD 'regular1';
|
||||
|
||||
GRANT test_windshaft_regular1 to :TESTUSER;
|
||||
|
||||
-- first table
|
||||
CREATE TABLE test_table (
|
||||
updated_at timestamp without time zone DEFAULT now(),
|
||||
@@ -189,6 +195,7 @@ INSERT INTO CDB_TableMetadata (tabname, updated_at) VALUES ('test_table_private_
|
||||
|
||||
-- GRANT SELECT ON CDB_TableMetadata TO :PUBLICUSER;
|
||||
GRANT SELECT ON CDB_TableMetadata TO :TESTUSER;
|
||||
GRANT SELECT ON CDB_TableMetadata TO test_windshaft_regular1; -- for analysis. Warning: TBA
|
||||
|
||||
-- long name table
|
||||
CREATE TABLE
|
||||
@@ -412,6 +419,52 @@ INSERT INTO _vovw_1_test_special_float_values_table_overviews VALUES
|
||||
(3, 'El Rey del Tallarín', 'Plaza Conde de Toreno 2, Madrid, Spain', 'NaN'::float, '0101000020E610000021C8410933AD0DC0CB0EF10F5B364440', '0101000020110F000053E71AC64D3419C10F664E4659CC5241', 1),
|
||||
(4, 'El Lacón', 'Manuel Fernández y González 8, Madrid, Spain', 'infinity'::float, '0101000020E6100000BC5983F755990DC07D923B6C22354440', '0101000020110F00005DACDB056F2319C1EC41A980FCCA5241', 2);
|
||||
|
||||
-- auth tables --------------------------------------------
|
||||
|
||||
CREATE TABLE test_table_localhost_regular1 (
|
||||
updated_at timestamp without time zone DEFAULT now(),
|
||||
created_at timestamp without time zone DEFAULT now(),
|
||||
cartodb_id integer NOT NULL,
|
||||
name character varying,
|
||||
address character varying,
|
||||
the_geom geometry,
|
||||
the_geom_webmercator geometry,
|
||||
CONSTRAINT enforce_dims_the_geom CHECK ((st_ndims(the_geom) = 2)),
|
||||
CONSTRAINT enforce_dims_the_geom_webmercator CHECK ((st_ndims(the_geom_webmercator) = 2)),
|
||||
CONSTRAINT enforce_geotype_the_geom CHECK (((geometrytype(the_geom) = 'POINT'::text) OR (the_geom IS NULL))),
|
||||
CONSTRAINT enforce_geotype_the_geom_webmercator CHECK (((geometrytype(the_geom_webmercator) = 'POINT'::text) OR (the_geom_webmercator IS NULL))),
|
||||
CONSTRAINT enforce_srid_the_geom CHECK ((st_srid(the_geom) = 4326)),
|
||||
CONSTRAINT enforce_srid_the_geom_webmercator CHECK ((st_srid(the_geom_webmercator) = 3857))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE test_table_localhost_regular1_cartodb_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE test_table_localhost_regular1_cartodb_id_seq OWNED BY test_table_localhost_regular1.cartodb_id;
|
||||
|
||||
SELECT pg_catalog.setval('test_table_localhost_regular1_cartodb_id_seq', 60, true);
|
||||
|
||||
ALTER TABLE test_table_localhost_regular1 ALTER COLUMN cartodb_id SET DEFAULT nextval('test_table_localhost_regular1_cartodb_id_seq'::regclass);
|
||||
|
||||
INSERT INTO test_table_localhost_regular1 VALUES
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.314252', 1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', '0101000020E6100000A6B73F170D990DC064E8D84125364440', '0101000020110F000076491621312319C122D4663F1DCC5241'),
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.319101', 2, 'El Estocolmo', 'Calle de la Palma 72, Madrid, Spain', '0101000020E6100000C90567F0F7AB0DC0AB07CC43A6364440', '0101000020110F0000C4356B29423319C15DD1092DADCC5241'),
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.324', 3, 'El Rey del Tallarín', 'Plaza Conde de Toreno 2, Madrid, Spain', '0101000020E610000021C8410933AD0DC0CB0EF10F5B364440', '0101000020110F000053E71AC64D3419C10F664E4659CC5241'),
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.329509', 4, 'El Lacón', 'Manuel Fernández y González 8, Madrid, Spain', '0101000020E6100000BC5983F755990DC07D923B6C22354440', '0101000020110F00005DACDB056F2319C1EC41A980FCCA5241'),
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.334931', 5, 'El Pico', 'Calle Divino Pastor 12, Madrid, Spain', '0101000020E61000003B6D8D08C6A10DC0371B2B31CF364440', '0101000020110F00005F716E91992A19C17DAAA4D6DACC5241');
|
||||
|
||||
ALTER TABLE ONLY test_table_localhost_regular1 ADD CONSTRAINT test_table_localhost_regular1_pkey PRIMARY KEY (cartodb_id);
|
||||
|
||||
CREATE INDEX test_table_localhost_regular1_the_geom_idx ON test_table_localhost_regular1 USING gist (the_geom);
|
||||
CREATE INDEX test_table_localhost_regular1_the_geom_webmercator_idx ON test_table_localhost_regular1 USING gist (the_geom_webmercator);
|
||||
|
||||
GRANT ALL ON TABLE test_table_localhost_regular1 TO :TESTUSER;
|
||||
GRANT ALL ON TABLE test_table_localhost_regular1 TO test_windshaft_regular1;
|
||||
|
||||
-- analysis tables -----------------------------------------------
|
||||
|
||||
ALTER TABLE cdb_analysis_catalog OWNER TO :TESTUSER;
|
||||
@@ -705,6 +758,7 @@ GRANT SELECT ON TABLE analysis_rent_listings TO :PUBLICUSER;
|
||||
|
||||
--
|
||||
GRANT SELECT, UPDATE, INSERT, DELETE ON cdb_analysis_catalog TO :TESTUSER;
|
||||
GRANT SELECT, UPDATE, INSERT, DELETE ON cdb_analysis_catalog TO test_windshaft_regular1; -- for analysis. Warning: TBA
|
||||
|
||||
DROP EXTENSION IF EXISTS crankshaft;
|
||||
CREATE SCHEMA IF NOT EXISTS cdb_crankshaft;
|
||||
|
||||
@@ -414,7 +414,7 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
|
||||
var urlParams = {};
|
||||
if (params.hasOwnProperty('no_filters')) {
|
||||
urlParams.no_filters = params.no_filters;
|
||||
}
|
||||
}
|
||||
if (params.hasOwnProperty('own_filter')) {
|
||||
urlParams.own_filter = params.own_filter;
|
||||
}
|
||||
@@ -1253,3 +1253,87 @@ TestClient.prototype.getAnalysesCatalog = function (params, callback) {
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
TestClient.prototype.getNamedTile = function (name, z, x, y, format, options, callback) {
|
||||
const { params } = options;
|
||||
|
||||
if (!this.apiKey) {
|
||||
return callback(new Error('apiKey param is mandatory to create a new template'));
|
||||
}
|
||||
|
||||
const createTemplateRequest = {
|
||||
url: `/api/v1/map/named?${qs.stringify({ api_key: this.apiKey })}`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(this.template)
|
||||
};
|
||||
|
||||
const createTemplateResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
assert.response(this.server, createTemplateRequest, createTemplateResponse, (res, err) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const templateId = JSON.parse(res.body).template_id;
|
||||
const queryParams = params ? `?${qs.stringify(params)}` : '';
|
||||
const url = `/api/v1/map/named/${templateId}/all/${[z,x,y].join('/')}.${format}${queryParams}`;
|
||||
const namedTileRequest = {
|
||||
url,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
};
|
||||
|
||||
let contentType;
|
||||
switch (format) {
|
||||
case 'png':
|
||||
contentType = 'image/png';
|
||||
break;
|
||||
case 'mvt':
|
||||
contentType = 'application/x-protobuf';
|
||||
break;
|
||||
default:
|
||||
contentType = 'application/json';
|
||||
break;
|
||||
}
|
||||
|
||||
const namedTileResponse = Object.assign({
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': contentType
|
||||
}
|
||||
}, options.response);
|
||||
|
||||
assert.response(this.server, namedTileRequest, namedTileResponse, (res, err) => {
|
||||
let 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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -15,12 +15,14 @@ var redis = require('redis');
|
||||
var nock = require('nock');
|
||||
var log4js = require('log4js');
|
||||
var pg = require('pg');
|
||||
const setICUEnvVariable = require('../../lib/cartodb/utils/icu_data_env_setter');
|
||||
|
||||
// set environment specific variables
|
||||
global.environment = require(__dirname + '/../../config/environments/test');
|
||||
global.environment.name = 'test';
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
setICUEnvVariable();
|
||||
|
||||
// don't output logs in test environment to reduce noise
|
||||
log4js.configure({ appenders: [] });
|
||||
@@ -111,7 +113,14 @@ afterEach(function(done) {
|
||||
'rails:test_windshaft_cartodb_user_1_db:my_table': true,
|
||||
'rails:users:localhost:map_key': true,
|
||||
'rails:users:cartodb250user': true,
|
||||
'rails:users:localhost': true
|
||||
'rails:users:localhost': true,
|
||||
'rails:users:user_previous_to_project_auth': true, // AUTH_FALLBACK
|
||||
'api_keys:localhost:1234': true,
|
||||
'api_keys:localhost:default_public': true,
|
||||
'api_keys:cartodb250user:4321': true,
|
||||
'api_keys:cartodb250user:default_public': true,
|
||||
'api_keys:localhost:regular1': true,
|
||||
'api_keys:localhost:regular2': true,
|
||||
};
|
||||
var databasesTasks = { 0: 'users', 5: 'meta'};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ describe('lzma-middleware', function() {
|
||||
}
|
||||
};
|
||||
testHelper.lzma_compress_to_base64(JSON.stringify(qo), 1, function(err, data) {
|
||||
const lzma = lzmaMiddleware();
|
||||
var req = {
|
||||
headers: {
|
||||
host:'localhost'
|
||||
@@ -19,9 +20,13 @@ describe('lzma-middleware', function() {
|
||||
query: {
|
||||
api_key: 'test',
|
||||
lzma: data
|
||||
},
|
||||
profiler: {
|
||||
done: function () {}
|
||||
}
|
||||
};
|
||||
lzmaMiddleware(req, {}, function(err) {
|
||||
|
||||
lzma(req, {}, function(err) {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ require('../../../support/test_helper');
|
||||
var assert = require('../../../support/assert');
|
||||
var ResourceLocator = require('../../../../lib/cartodb/models/resource-locator');
|
||||
|
||||
describe('ResourceLocator.getUrls', function() {
|
||||
describe('ResourceLocator', function() {
|
||||
var USERNAME = 'username';
|
||||
var RESOURCE = 'wadus';
|
||||
var TILE_RESOURCE = 'wadus/{z}/{x}/{y}.png';
|
||||
var HTTP_SUBDOMAINS = ['1', '2', '3', '4'];
|
||||
var HTTPS_SUBDOMAINS = ['a', 'b', 'c', 'd'];
|
||||
|
||||
@@ -15,118 +16,292 @@ describe('ResourceLocator.getUrls', function() {
|
||||
assert.ok(urls);
|
||||
});
|
||||
|
||||
var BASIC_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com'
|
||||
}
|
||||
}
|
||||
};
|
||||
it('should return default urls when basic http and https domains are provided', function() {
|
||||
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
describe('basic', function() {
|
||||
|
||||
var BASIC_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('getUrls', function() {
|
||||
it('should return default urls when basic http and https domains are provided', function() {
|
||||
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.equal(urls.http, ['http://cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
|
||||
assert.equal(urls.https, ['https://cdn.ssl.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTileUrls', function() {
|
||||
it('should return default urls when basic http and https domains are provided', function() {
|
||||
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(
|
||||
urls.http,
|
||||
[`http://cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`]
|
||||
);
|
||||
assert.deepEqual(
|
||||
urls.https,
|
||||
[`https://cdn.ssl.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTemplateUrls', function() {
|
||||
it('should return default urls when basic http and https domains are provided', function() {
|
||||
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTemplateUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(urls.http, {
|
||||
urlTemplate: `http://cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`,
|
||||
subdomains: []
|
||||
});
|
||||
assert.deepEqual(urls.https, {
|
||||
urlTemplate: `https://cdn.ssl.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`,
|
||||
subdomains: []
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(urls.http, ['http://cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
|
||||
assert.equal(urls.https, ['https://cdn.ssl.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
|
||||
});
|
||||
|
||||
var RESOURCE_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com'
|
||||
}
|
||||
},
|
||||
resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan/api/v1/map',
|
||||
https: 'https://{{=it.user}}.ssl.localhost.lan/api/v1/map'
|
||||
}
|
||||
};
|
||||
it('resources_url_templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
describe('resource templates', function() {
|
||||
|
||||
var RESOURCE_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com'
|
||||
}
|
||||
},
|
||||
resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan/api/v1/map',
|
||||
https: 'https://{{=it.user}}.ssl.localhost.lan/api/v1/map'
|
||||
}
|
||||
};
|
||||
|
||||
describe('getUrls', function() {
|
||||
it('resources_url_templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + USERNAME + '.localhost.lan', 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
['https://' + USERNAME + '.ssl.localhost.lan', 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTileUrls', function() {
|
||||
it('resources_url_templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(
|
||||
urls.http,
|
||||
[`http://${USERNAME}.localhost.lan/api/v1/map/${TILE_RESOURCE}`]
|
||||
);
|
||||
assert.deepEqual(
|
||||
urls.https,
|
||||
[`https://${USERNAME}.ssl.localhost.lan/api/v1/map/${TILE_RESOURCE}`]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTemplateUrls', function() {
|
||||
it('resources_url_templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTemplateUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(urls.http, {
|
||||
urlTemplate: `http://${USERNAME}.localhost.lan/api/v1/map/${TILE_RESOURCE}`,
|
||||
subdomains: []
|
||||
});
|
||||
assert.deepEqual(urls.https, {
|
||||
urlTemplate: `https://${USERNAME}.ssl.localhost.lan/api/v1/map/${TILE_RESOURCE}`,
|
||||
subdomains: []
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(urls.http, ['http://' + USERNAME + '.localhost.lan', 'api/v1/map', RESOURCE].join('/'));
|
||||
assert.equal(urls.https, ['https://' + USERNAME + '.ssl.localhost.lan', 'api/v1/map', RESOURCE].join('/'));
|
||||
});
|
||||
|
||||
var CDN_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com',
|
||||
templates: {
|
||||
http: {
|
||||
url: "http://{s}.cdn.carto.com",
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
},
|
||||
https: {
|
||||
url: "https://cdn_{s}.ssl.cdn.carto.com",
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
describe('cdn templates', function() {
|
||||
|
||||
var CDN_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com',
|
||||
templates: {
|
||||
http: {
|
||||
url: "http://{s}.cdn.carto.com",
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
},
|
||||
https: {
|
||||
url: "https://cdn_{s}.ssl.cdn.carto.com",
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
it('cdn_url templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
};
|
||||
|
||||
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
|
||||
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
|
||||
describe('getUrls', function() {
|
||||
it('cdn_url templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
|
||||
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + httpSubdomain + '.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTileUrls', function() {
|
||||
it('cdn_url templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(
|
||||
urls.http,
|
||||
HTTP_SUBDOMAINS
|
||||
.map(s => `http://${s}.cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
|
||||
);
|
||||
assert.deepEqual(
|
||||
urls.https,
|
||||
HTTPS_SUBDOMAINS
|
||||
.map(s => `https://cdn_${s}.ssl.cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTemplateUrls', function() {
|
||||
it('cdn_url templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTemplateUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(urls.http, {
|
||||
urlTemplate: `http://{s}.cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`,
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
});
|
||||
assert.deepEqual(urls.https, {
|
||||
urlTemplate: `https://cdn_{s}.ssl.cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`,
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + httpSubdomain + '.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
});
|
||||
|
||||
var CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com',
|
||||
templates: {
|
||||
http: {
|
||||
url: "http://{s}.cdn.carto.com",
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
},
|
||||
https: {
|
||||
url: "https://cdn_{s}.ssl.cdn.carto.com",
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
describe('cdn and resource templates', function() {
|
||||
|
||||
var CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com',
|
||||
templates: {
|
||||
http: {
|
||||
url: "http://{s}.cdn.carto.com",
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
},
|
||||
https: {
|
||||
url: "https://cdn_{s}.ssl.cdn.carto.com",
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
resources_url_templates: {
|
||||
http: 'http://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map',
|
||||
https: 'https://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map'
|
||||
}
|
||||
},
|
||||
resources_url_templates: {
|
||||
http: 'http://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map',
|
||||
https: 'https://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map'
|
||||
}
|
||||
};
|
||||
it('should mix cdn_url templates and resources_url_templates', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
};
|
||||
|
||||
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
|
||||
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
|
||||
describe('getUrls', function() {
|
||||
it('should mix cdn_url templates and resources_url_templates', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
|
||||
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + httpSubdomain + '.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
`https://cdn_${httpsSubdomain}.ssl.cdn.carto.com/u/${USERNAME}/api/v1/map/${RESOURCE}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTileUrls', function() {
|
||||
it('should mix cdn_url templates and resources_url_templates', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(
|
||||
urls.http,
|
||||
HTTP_SUBDOMAINS
|
||||
.map(s => `http://${s}.cdn.carto.com/u/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
|
||||
);
|
||||
assert.deepEqual(
|
||||
urls.https,
|
||||
HTTPS_SUBDOMAINS
|
||||
.map(s => `https://cdn_${s}.ssl.cdn.carto.com/u/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTemplateUrls', function() {
|
||||
it('should mix cdn_url templates and resources_url_templates', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTemplateUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(urls.http, {
|
||||
urlTemplate: `http://{s}.cdn.carto.com/u/${USERNAME}/api/v1/map/${TILE_RESOURCE}`,
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
});
|
||||
assert.deepEqual(urls.https, {
|
||||
urlTemplate: `https://cdn_{s}.ssl.cdn.carto.com/u/${USERNAME}/api/v1/map/${TILE_RESOURCE}`,
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + httpSubdomain + '.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
require('../../../support/test_helper.js');
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var LayergroupController = require('../../../../lib/cartodb/controllers/layergroup');
|
||||
|
||||
describe('tile stats', function() {
|
||||
|
||||
beforeEach(function () {
|
||||
this.statsClient = global.statsClient;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
global.statsClient = this.statsClient;
|
||||
});
|
||||
|
||||
it('finalizeGetTileOrGrid does not call statsClient when format is not supported', function() {
|
||||
var expectedCalls = 2, // it will call increment once for the general error
|
||||
invalidFormat = 'png2',
|
||||
invalidFormatRegexp = new RegExp('invalid'),
|
||||
formatMatched = false;
|
||||
mockStatsClientGetInstance({
|
||||
increment: function(label) {
|
||||
formatMatched = formatMatched || !!label.match(invalidFormatRegexp);
|
||||
expectedCalls--;
|
||||
}
|
||||
});
|
||||
|
||||
var layergroupController = new LayergroupController();
|
||||
|
||||
var reqMock = {
|
||||
profiler: { toJSONString:function() {} },
|
||||
params: {
|
||||
format: invalidFormat
|
||||
}
|
||||
};
|
||||
var resMock = {
|
||||
status: function() { return this; },
|
||||
set: function() {},
|
||||
json: function() {},
|
||||
jsonp: function() {},
|
||||
send: function() {}
|
||||
};
|
||||
|
||||
var next = function () {};
|
||||
layergroupController.finalizeGetTileOrGrid('Unsupported format png2', reqMock, resMock, null, null, next);
|
||||
|
||||
assert.ok(formatMatched, 'Format was never matched in increment method');
|
||||
assert.equal(expectedCalls, 0, 'Unexpected number of calls to increment method');
|
||||
});
|
||||
|
||||
it('finalizeGetTileOrGrid calls statsClient when format is supported', function() {
|
||||
var expectedCalls = 2, // general error + format error
|
||||
validFormat = 'png',
|
||||
validFormatRegexp = new RegExp(validFormat),
|
||||
formatMatched = false;
|
||||
mockStatsClientGetInstance({
|
||||
increment: function(label) {
|
||||
formatMatched = formatMatched || !!label.match(validFormatRegexp);
|
||||
expectedCalls--;
|
||||
}
|
||||
});
|
||||
var reqMock = {
|
||||
profiler: { toJSONString:function() {} },
|
||||
params: {
|
||||
format: validFormat
|
||||
}
|
||||
};
|
||||
var resMock = {
|
||||
status: function() { return this; },
|
||||
set: function() {},
|
||||
json: function() {},
|
||||
jsonp: function() {},
|
||||
send: function() {}
|
||||
};
|
||||
|
||||
var layergroupController = new LayergroupController();
|
||||
|
||||
var next = function () {};
|
||||
layergroupController.finalizeGetTileOrGrid('Another error happened', reqMock, resMock, null, null, next);
|
||||
|
||||
assert.ok(formatMatched, 'Format was never matched in increment method');
|
||||
assert.equal(expectedCalls, 0, 'Unexpected number of calls to increment method');
|
||||
});
|
||||
|
||||
function mockStatsClientGetInstance(instance) {
|
||||
global.statsClient = Object.assign(global.statsClient, instance);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -10,6 +10,7 @@ var TemplateMaps = require('../../../lib/cartodb/backends/template_maps');
|
||||
const cleanUpQueryParamsMiddleware = require('../../../lib/cartodb/middleware/context/clean-up-query-params');
|
||||
const authorizeMiddleware = require('../../../lib/cartodb/middleware/context/authorize');
|
||||
const dbConnSetupMiddleware = require('../../../lib/cartodb/middleware/context/db-conn-setup');
|
||||
const credentialsMiddleware = require('../../../lib/cartodb/middleware/context/credentials');
|
||||
const localsMiddleware = require('../../../lib/cartodb/middleware/context/locals');
|
||||
|
||||
var windshaft = require('windshaft');
|
||||
@@ -23,6 +24,7 @@ describe('prepare-context', function() {
|
||||
let cleanUpQueryParams;
|
||||
let dbConnSetup;
|
||||
let authorize;
|
||||
let setCredentials;
|
||||
|
||||
before(function() {
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
@@ -35,6 +37,7 @@ describe('prepare-context', function() {
|
||||
cleanUpQueryParams = cleanUpQueryParamsMiddleware();
|
||||
authorize = authorizeMiddleware(authApi);
|
||||
dbConnSetup = dbConnSetupMiddleware(pgConnection);
|
||||
setCredentials = credentialsMiddleware();
|
||||
});
|
||||
|
||||
|
||||
@@ -58,20 +61,23 @@ describe('prepare-context', function() {
|
||||
}
|
||||
res.locals.user = 'localhost';
|
||||
|
||||
res.set = function () {};
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
it('res.locals are created', function(done) {
|
||||
const locals = localsMiddleware();
|
||||
let req = {};
|
||||
let res = {};
|
||||
|
||||
localsMiddleware(prepareRequest(req), prepareResponse(res), function(err) {
|
||||
locals(prepareRequest(req), prepareResponse(res), function(err) {
|
||||
if ( err ) { done(err); return; }
|
||||
assert.ok(res.hasOwnProperty('locals'), 'response has locals');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('cleans up request', function(done){
|
||||
var req = {headers: { host:'localhost' }, query: {dbuser:'hacker',dbname:'secret'}};
|
||||
var res = {};
|
||||
@@ -103,8 +109,20 @@ describe('prepare-context', function() {
|
||||
});
|
||||
|
||||
it('sets also dbuser for authenticated requests', function(done){
|
||||
var req = { headers: { host: 'localhost' }, query: { map_key: '1234' }};
|
||||
var res = { set: function () {} };
|
||||
var req = {
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
query: {
|
||||
api_key: '1234'
|
||||
}
|
||||
};
|
||||
var res = {
|
||||
set: function () {},
|
||||
locals: {
|
||||
api_key: '1234'
|
||||
}
|
||||
};
|
||||
|
||||
// FIXME: review authorize-pgconnsetup workflow, It might we are doing authorization twice.
|
||||
authorize(prepareRequest(req), prepareResponse(res), function (err) {
|
||||
@@ -154,7 +172,7 @@ describe('prepare-context', function() {
|
||||
}
|
||||
};
|
||||
var res = {};
|
||||
|
||||
|
||||
cleanUpQueryParams(prepareRequest(req), prepareResponse(res), function (err) {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
@@ -168,4 +186,66 @@ describe('prepare-context', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Set apikey token', function(){
|
||||
it('from query param', function (done) {
|
||||
var req = {
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
query: {
|
||||
api_key: '1234',
|
||||
}
|
||||
};
|
||||
var res = {};
|
||||
setCredentials(prepareRequest(req), prepareResponse(res), function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
var query = res.locals;
|
||||
|
||||
assert.equal('1234', query.api_key);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('from body param', function (done) {
|
||||
var req = {
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
body: {
|
||||
api_key: '1234',
|
||||
}
|
||||
};
|
||||
var res = {};
|
||||
setCredentials(prepareRequest(req), prepareResponse(res), function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
var query = res.locals;
|
||||
|
||||
assert.equal('1234', query.api_key);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('from http header', function (done) {
|
||||
var req = {
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
authorization: 'Basic bG9jYWxob3N0OjEyMzQ=', // user: localhost, password: 1234
|
||||
}
|
||||
};
|
||||
var res = {};
|
||||
setCredentials(prepareRequest(req), prepareResponse(res), function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
var query = res.locals;
|
||||
|
||||
assert.equal('1234', query.api_key);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
294
yarn.lock
294
yarn.lock
@@ -2,19 +2,36 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"abaculus@github:cartodb/abaculus#2.0.3-cdb1":
|
||||
version "2.0.3-cdb1"
|
||||
resolved "https://codeload.github.com/cartodb/abaculus/tar.gz/f5f34e1c80cdd8d49edd1d6fe3b2220ab2e23aaf"
|
||||
"@carto/mapnik@3.6.2-carto.4":
|
||||
version "3.6.2-carto.4"
|
||||
resolved "https://registry.yarnpkg.com/@carto/mapnik/-/mapnik-3.6.2-carto.4.tgz#54042a5dbea293c54e1bd286b32277694c5dc2d2"
|
||||
dependencies:
|
||||
mapnik-vector-tile "1.5.0"
|
||||
nan "~2.7.0"
|
||||
node-pre-gyp "~0.6.30"
|
||||
protozero "1.5.1"
|
||||
|
||||
"@carto/tilelive-bridge@cartodb/tilelive-bridge#2.5.1-cdb4":
|
||||
version "2.5.1-cdb4"
|
||||
resolved "https://codeload.github.com/cartodb/tilelive-bridge/tar.gz/3eb554e5109199f50f457cec72ee288cffa5d6b3"
|
||||
dependencies:
|
||||
"@carto/mapnik" "3.6.2-carto.4"
|
||||
"@mapbox/sphericalmercator" "~1.0.1"
|
||||
mapnik-pool "~0.1.3"
|
||||
|
||||
"@mapbox/sphericalmercator@~1.0.1":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@mapbox/sphericalmercator/-/sphericalmercator-1.0.5.tgz#70237b9774095ed1cfdbcea7a8fd1fc82b2691f2"
|
||||
|
||||
abaculus@cartodb/abaculus#2.0.3-cdb5:
|
||||
version "2.0.3-cdb5"
|
||||
resolved "https://codeload.github.com/cartodb/abaculus/tar.gz/b899cbea04b3e6093aa3ef32331920acd5f839a1"
|
||||
dependencies:
|
||||
"@carto/mapnik" "3.6.2-carto.4"
|
||||
d3-queue "^2.0.2"
|
||||
mapnik "~3.5.0"
|
||||
sphericalmercator "1.0.x"
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
|
||||
abbrev@1.0.x:
|
||||
abbrev@1, abbrev@1.0.x:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
|
||||
|
||||
@@ -95,8 +112,8 @@ assert-plus@^0.2.0:
|
||||
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
|
||||
|
||||
assertion-error@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.0.2.tgz#13ca515d86206da0bac66e834dd397d87581094c"
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
|
||||
|
||||
async@1.x, async@^1.4.0, async@^1.5.2:
|
||||
version "1.5.2"
|
||||
@@ -126,6 +143,12 @@ balanced-match@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
|
||||
basic-auth@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.0.tgz#015db3f353e02e56377755f962742e8981e7bbba"
|
||||
dependencies:
|
||||
safe-buffer "5.1.1"
|
||||
|
||||
bcrypt-pbkdf@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
|
||||
@@ -215,18 +238,18 @@ camelcase@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
|
||||
|
||||
camshaft@0.60.0:
|
||||
version "0.60.0"
|
||||
resolved "https://registry.yarnpkg.com/camshaft/-/camshaft-0.60.0.tgz#0433b5a576e08cabbc9bae1e1b22305274b8b7b6"
|
||||
camshaft@0.61.8:
|
||||
version "0.61.8"
|
||||
resolved "https://registry.yarnpkg.com/camshaft/-/camshaft-0.61.8.tgz#75669c6c14791a93433e79a8892298e88cb0fce2"
|
||||
dependencies:
|
||||
async "^1.5.2"
|
||||
bunyan "1.8.1"
|
||||
cartodb-psql "^0.10.1"
|
||||
debug "^3.1.0"
|
||||
dot "^1.0.3"
|
||||
request "^2.69.0"
|
||||
request "2.85.0"
|
||||
|
||||
"canvas@github:cartodb/node-canvas#1.6.2-cdb2":
|
||||
canvas@cartodb/node-canvas#1.6.2-cdb2:
|
||||
version "1.6.2-cdb2"
|
||||
resolved "https://codeload.github.com/cartodb/node-canvas/tar.gz/8acf04557005c633f9e68524488a2657c04f3766"
|
||||
dependencies:
|
||||
@@ -244,15 +267,15 @@ carto@0.16.3:
|
||||
semver "^5.1.0"
|
||||
yargs "^4.2.0"
|
||||
|
||||
"carto@github:cartodb/carto#0.15.1-cdb1":
|
||||
carto@CartoDB/carto#0.15.1-cdb1:
|
||||
version "0.15.1-cdb1"
|
||||
resolved "https://codeload.github.com/cartodb/carto/tar.gz/8050ec843f1f32a6469e5d1cf49602773015d398"
|
||||
resolved "https://codeload.github.com/CartoDB/carto/tar.gz/8050ec843f1f32a6469e5d1cf49602773015d398"
|
||||
dependencies:
|
||||
mapnik-reference "~6.0.2"
|
||||
optimist "~0.6.0"
|
||||
underscore "~1.6.0"
|
||||
|
||||
"carto@github:cartodb/carto#0.15.1-cdb3":
|
||||
carto@cartodb/carto#0.15.1-cdb3:
|
||||
version "0.15.1-cdb3"
|
||||
resolved "https://codeload.github.com/cartodb/carto/tar.gz/945f5efb74fd1af1f5e1f69f409f9567f94fb5a7"
|
||||
dependencies:
|
||||
@@ -278,12 +301,12 @@ cartodb-query-tables@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/cartodb-query-tables/-/cartodb-query-tables-0.3.0.tgz#56e18d869666eb2e8e2cb57d0baf3acc923f8756"
|
||||
|
||||
cartodb-redis@0.14.0:
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/cartodb-redis/-/cartodb-redis-0.14.0.tgz#6f82fdb3e5b7c8005dbaccd6172c1706c4378df2"
|
||||
cartodb-redis@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cartodb-redis/-/cartodb-redis-1.0.0.tgz#83b4888ba7abb5d5895c8958b7e15cf4882602aa"
|
||||
dependencies:
|
||||
dot "~1.0.2"
|
||||
redis-mpool "~0.4.1"
|
||||
redis-mpool "^0.5.0"
|
||||
underscore "~1.6.0"
|
||||
|
||||
caseless@~0.12.0:
|
||||
@@ -480,10 +503,14 @@ delegates@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||
|
||||
depd@1.1.1, depd@~1.1.1:
|
||||
depd@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
|
||||
|
||||
depd@~1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||
|
||||
destroy@~1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
|
||||
@@ -524,14 +551,14 @@ domutils@1.5:
|
||||
dom-serializer "0"
|
||||
domelementtype "1"
|
||||
|
||||
dot@^1.0.3:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/dot/-/dot-1.1.2.tgz#c7377019fc4e550798928b2b9afeb66abfa1f2f9"
|
||||
|
||||
dot@~1.0.2:
|
||||
dot@^1.0.3, dot@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dot/-/dot-1.0.3.tgz#f8750bfb6b03c7664eb0e6cb1eb4c66419af9427"
|
||||
|
||||
double-ended-queue@^2.1.0-0:
|
||||
version "2.1.0-0"
|
||||
resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c"
|
||||
|
||||
dtrace-provider@~0.6:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.6.0.tgz#0b078d5517937d873101452d9146737557b75e51"
|
||||
@@ -549,8 +576,8 @@ ee-first@1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
|
||||
encodeurl@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
|
||||
entities@1.0:
|
||||
version "1.0.0"
|
||||
@@ -652,14 +679,10 @@ extend@~3.0.0, extend@~3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
|
||||
|
||||
extsprintf@1.3.0:
|
||||
extsprintf@1.3.0, extsprintf@^1.2.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
|
||||
|
||||
extsprintf@^1.2.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
|
||||
|
||||
fast-deep-equal@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
|
||||
@@ -766,7 +789,7 @@ gdal@~0.9.2:
|
||||
nan "~2.6.2"
|
||||
node-pre-gyp "~0.6.36"
|
||||
|
||||
generic-pool@2.4.3:
|
||||
generic-pool@2.4.3, generic-pool@~2.4.0, generic-pool@~2.4.1:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-2.4.3.tgz#780c36f69dfad05a5a045dd37be7adca11a4f6ff"
|
||||
|
||||
@@ -778,10 +801,6 @@ generic-pool@~2.2.0, generic-pool@~2.2.1:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-2.2.2.tgz#7a89f491d575b42f9f069a0e8e2c6dbaa3c241be"
|
||||
|
||||
generic-pool@~2.4.0, generic-pool@~2.4.1:
|
||||
version "2.4.6"
|
||||
resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-2.4.6.tgz#f1b55e572167dba2fe75d5aa91ebb1e9f72642d7"
|
||||
|
||||
get-caller-file@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
|
||||
@@ -823,7 +842,7 @@ glob@^6.0.1:
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^7.0.5, glob@^7.1.1:
|
||||
glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
|
||||
dependencies:
|
||||
@@ -842,9 +861,9 @@ graceful-fs@^4.1.2:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
|
||||
|
||||
grainstore@~1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/grainstore/-/grainstore-1.8.0.tgz#9398729df88f3aecb55ffbb415d541dcca4420af"
|
||||
grainstore@1.8.2:
|
||||
version "1.8.2"
|
||||
resolved "https://registry.yarnpkg.com/grainstore/-/grainstore-1.8.2.tgz#79dd7a91a098bf8b0ea3189961775c8cc7474319"
|
||||
dependencies:
|
||||
carto "0.16.3"
|
||||
debug "~3.1.0"
|
||||
@@ -1073,8 +1092,8 @@ istanbul@~0.4.3:
|
||||
wordwrap "^1.0.0"
|
||||
|
||||
js-base64@^2.1.9:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.0.tgz#9e566fee624751a1d720c966cd6226d29d4025aa"
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582"
|
||||
|
||||
js-string-escape@1.0.1:
|
||||
version "1.0.1"
|
||||
@@ -1232,8 +1251,8 @@ lodash@3.7.x:
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.7.0.tgz#3678bd8ab995057c07ade836ed2ef087da811d45"
|
||||
|
||||
lodash@^4.5.1:
|
||||
version "4.17.4"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
|
||||
version "4.17.5"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
|
||||
|
||||
log4js@cartodb/log4js-node#cdb:
|
||||
version "0.6.25"
|
||||
@@ -1273,18 +1292,9 @@ mapnik-reference@~8.5.3:
|
||||
dependencies:
|
||||
semver "^5.1.0"
|
||||
|
||||
mapnik-vector-tile@~1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/mapnik-vector-tile/-/mapnik-vector-tile-1.2.2.tgz#42795ca211dd274a9a4af5bf6cfe3b0bfe0ba243"
|
||||
|
||||
mapnik@3.5.14, mapnik@~3.5.0:
|
||||
version "3.5.14"
|
||||
resolved "https://registry.yarnpkg.com/mapnik/-/mapnik-3.5.14.tgz#632bd6635c72c0214a707549309ba416594afff7"
|
||||
dependencies:
|
||||
mapnik-vector-tile "~1.2.2"
|
||||
nan "~2.4.0"
|
||||
node-pre-gyp "~0.6.30"
|
||||
protozero "~1.4.2"
|
||||
mapnik-vector-tile@1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/mapnik-vector-tile/-/mapnik-vector-tile-1.5.0.tgz#c647bfb8027e9dc40db583505a436f35e2101407"
|
||||
|
||||
media-typer@0.3.0:
|
||||
version "0.3.0"
|
||||
@@ -1331,9 +1341,9 @@ mime@~1.2.11:
|
||||
version "1.2.11"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.2.11.tgz#58203eed86e3a5ef17aed2b7d9ebd47f0a60dd10"
|
||||
|
||||
mime@~1.3.4:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0"
|
||||
mime@~1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
|
||||
"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
|
||||
version "3.0.4"
|
||||
@@ -1341,7 +1351,7 @@ mime@~1.3.4:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@0.0.8:
|
||||
minimist@0.0.8, minimist@~0.0.1:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
|
||||
@@ -1349,10 +1359,6 @@ minimist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
|
||||
minimist@~0.0.1:
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
|
||||
|
||||
minimist@~0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.2.0.tgz#4dffe525dae2b864c66c2e23c6271d7afdecefce"
|
||||
@@ -1379,11 +1385,7 @@ mocha@~3.4.1:
|
||||
mkdirp "0.5.1"
|
||||
supports-color "3.1.2"
|
||||
|
||||
moment@^2.10.6:
|
||||
version "2.20.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd"
|
||||
|
||||
moment@~2.18.1:
|
||||
moment@^2.10.6, moment@~2.18.1:
|
||||
version "2.18.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
|
||||
|
||||
@@ -1497,14 +1499,10 @@ oauth-sign@~0.8.1, oauth-sign@~0.8.2:
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
|
||||
|
||||
object-assign@4.1.0:
|
||||
object-assign@4.1.0, object-assign@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0"
|
||||
|
||||
object-assign@^4.1.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
|
||||
object-keys@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336"
|
||||
@@ -1737,9 +1735,9 @@ propagate@0.3.x:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/propagate/-/propagate-0.3.1.tgz#e3a84404a7ece820dd6bbea9f6d924e3135ae09c"
|
||||
|
||||
protozero@~1.4.2:
|
||||
version "1.4.5"
|
||||
resolved "https://registry.yarnpkg.com/protozero/-/protozero-1.4.5.tgz#80eaa80a4f9c751465c4cb2620d8233b50ec1aff"
|
||||
protozero@1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/protozero/-/protozero-1.5.1.tgz#5a27df6fb6e1ed743f510812ae76c082f5b16638"
|
||||
|
||||
proxy-addr@~2.0.2:
|
||||
version "2.0.2"
|
||||
@@ -1778,8 +1776,8 @@ raw-body@2.3.2:
|
||||
unpipe "1.0.0"
|
||||
|
||||
rc@^1.1.7:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.2.tgz#d8ce9cb57e8d64d9c7badd9876c7c34cbe3c7077"
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.5.tgz#275cd687f6e3b36cc756baa26dfee80a790301fd"
|
||||
dependencies:
|
||||
deep-extend "~0.4.0"
|
||||
ini "~1.3.0"
|
||||
@@ -1801,7 +1799,7 @@ read-pkg@^1.0.0:
|
||||
normalize-package-data "^2.3.2"
|
||||
path-type "^1.0.0"
|
||||
|
||||
readable-stream@1.1:
|
||||
readable-stream@1.1, readable-stream@~1.1.9:
|
||||
version "1.1.13"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e"
|
||||
dependencies:
|
||||
@@ -1831,27 +1829,30 @@ readable-stream@~1.0.2:
|
||||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
readable-stream@~1.1.9:
|
||||
version "1.1.14"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.1"
|
||||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
redis-commands@^1.2.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.3.1.tgz#81d826f45fa9c8b2011f4cd7a0fe597d241d442b"
|
||||
|
||||
redis-mpool@0.4.1, redis-mpool@~0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/redis-mpool/-/redis-mpool-0.4.1.tgz#d917c0a4ed57a1291a9c6eb35434e6c0b7046f80"
|
||||
redis-mpool@0.5.0, redis-mpool@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/redis-mpool/-/redis-mpool-0.5.0.tgz#9a22dcffb4ad796ec88ce3038b991deae9d1fda7"
|
||||
dependencies:
|
||||
generic-pool "~2.1.1"
|
||||
hiredis "~0.5.0"
|
||||
redis "~0.12.1"
|
||||
redis "^2.8.0"
|
||||
underscore "~1.6.0"
|
||||
|
||||
redis@~0.12.1:
|
||||
version "0.12.1"
|
||||
resolved "https://registry.yarnpkg.com/redis/-/redis-0.12.1.tgz#64df76ad0fc8acebaebd2a0645e8a48fac49185e"
|
||||
redis-parser@^2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b"
|
||||
|
||||
redis@2.8.0, redis@^2.8.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/redis/-/redis-2.8.0.tgz#202288e3f58c49f6079d97af7a10e1303ae14b02"
|
||||
dependencies:
|
||||
double-ended-queue "^2.1.0-0"
|
||||
redis-commands "^1.2.0"
|
||||
redis-parser "^2.6.0"
|
||||
|
||||
repeat-string@^1.5.2:
|
||||
version "1.6.1"
|
||||
@@ -1884,7 +1885,34 @@ request@2.81.0:
|
||||
tunnel-agent "^0.6.0"
|
||||
uuid "^3.0.0"
|
||||
|
||||
request@2.x, request@^2.55.0, request@^2.69.0, request@^2.83.0:
|
||||
request@2.85.0:
|
||||
version "2.85.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa"
|
||||
dependencies:
|
||||
aws-sign2 "~0.7.0"
|
||||
aws4 "^1.6.0"
|
||||
caseless "~0.12.0"
|
||||
combined-stream "~1.0.5"
|
||||
extend "~3.0.1"
|
||||
forever-agent "~0.6.1"
|
||||
form-data "~2.3.1"
|
||||
har-validator "~5.0.3"
|
||||
hawk "~6.0.2"
|
||||
http-signature "~1.2.0"
|
||||
is-typedarray "~1.0.0"
|
||||
isstream "~0.1.2"
|
||||
json-stringify-safe "~5.0.1"
|
||||
mime-types "~2.1.17"
|
||||
oauth-sign "~0.8.2"
|
||||
performance-now "^2.1.0"
|
||||
qs "~6.5.1"
|
||||
safe-buffer "^5.1.1"
|
||||
stringstream "~0.0.5"
|
||||
tough-cookie "~2.3.3"
|
||||
tunnel-agent "^0.6.0"
|
||||
uuid "^3.1.0"
|
||||
|
||||
request@2.x, request@^2.55.0:
|
||||
version "2.83.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
|
||||
dependencies:
|
||||
@@ -1949,9 +1977,9 @@ safe-json-stringify@~1:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz#81a098f447e4bbc3ff3312a243521bc060ef5911"
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0, semver@~5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
|
||||
|
||||
semver@4.3.2:
|
||||
version "4.3.2"
|
||||
@@ -1965,10 +1993,6 @@ semver@~5.0.3:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a"
|
||||
|
||||
semver@~5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
|
||||
|
||||
send@0.16.1:
|
||||
version "0.16.1"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3"
|
||||
@@ -2066,14 +2090,10 @@ speedometer@~0.1.2:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-0.1.4.tgz#9876dbd2a169d3115402d48e6ea6329c8816a50d"
|
||||
|
||||
sphericalmercator@1.0.4:
|
||||
sphericalmercator@1.0.4, sphericalmercator@1.0.x, sphericalmercator@~1.0.1, sphericalmercator@~1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/sphericalmercator/-/sphericalmercator-1.0.4.tgz#baad4e34187f06e87f2e92fc1280199fa1b01d4e"
|
||||
|
||||
sphericalmercator@1.0.x, sphericalmercator@~1.0.1, sphericalmercator@~1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/sphericalmercator/-/sphericalmercator-1.0.5.tgz#ddc5a049e360e000d0fad9fc22c4071882584980"
|
||||
|
||||
split@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9"
|
||||
@@ -2111,11 +2131,7 @@ sshpk@^1.7.0:
|
||||
jsbn "~0.1.0"
|
||||
tweetnacl "~0.14.0"
|
||||
|
||||
"statuses@>= 1.3.1 < 2":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
|
||||
|
||||
statuses@~1.3.1:
|
||||
"statuses@>= 1.3.1 < 2", statuses@~1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
|
||||
|
||||
@@ -2223,21 +2239,13 @@ through@2:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
|
||||
"tilelive-bridge@github:cartodb/tilelive-bridge#2.3.1-cdb4":
|
||||
version "2.3.1-cdb4"
|
||||
resolved "https://codeload.github.com/cartodb/tilelive-bridge/tar.gz/faa2b638da2d119b78281575d40255cb523f6ca6"
|
||||
dependencies:
|
||||
mapnik "~3.5.0"
|
||||
mapnik-pool "~0.1.3"
|
||||
sphericalmercator "1.0.x"
|
||||
|
||||
"tilelive-mapnik@github:cartodb/tilelive-mapnik#0.6.18-cdb3":
|
||||
version "0.6.18-cdb3"
|
||||
resolved "https://codeload.github.com/cartodb/tilelive-mapnik/tar.gz/23bd1c31dd57d0b76c86b9f1eaf62462b3c17d01"
|
||||
tilelive-mapnik@cartodb/tilelive-mapnik#0.6.18-cdb8:
|
||||
version "0.6.18-cdb8"
|
||||
resolved "https://codeload.github.com/cartodb/tilelive-mapnik/tar.gz/9cb4546c8fdd34ced0a41dbf70e143475b4e2067"
|
||||
dependencies:
|
||||
"@carto/mapnik" "3.6.2-carto.4"
|
||||
generic-pool "~2.4.0"
|
||||
mapnik "3.5.14"
|
||||
mime "~1.3.4"
|
||||
mime "~1.6.0"
|
||||
sphericalmercator "~1.0.4"
|
||||
step "~0.0.5"
|
||||
|
||||
@@ -2346,8 +2354,8 @@ utils-merge@1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
|
||||
uuid@^3.0.0, uuid@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
|
||||
|
||||
validate-npm-package-license@^3.0.1:
|
||||
version "3.0.1"
|
||||
@@ -2392,27 +2400,27 @@ window-size@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
|
||||
|
||||
windshaft@4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/windshaft/-/windshaft-4.2.0.tgz#6a0409832a0d3bccfa09a88a8ab8288686b6762d"
|
||||
windshaft@4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/windshaft/-/windshaft-4.6.0.tgz#d9394aff73c0aa761207ad2b0f12d1c23ac41244"
|
||||
dependencies:
|
||||
abaculus cartodb/abaculus#2.0.3-cdb1
|
||||
"@carto/mapnik" "3.6.2-carto.4"
|
||||
"@carto/tilelive-bridge" cartodb/tilelive-bridge#2.5.1-cdb4
|
||||
abaculus cartodb/abaculus#2.0.3-cdb5
|
||||
canvas cartodb/node-canvas#1.6.2-cdb2
|
||||
carto cartodb/carto#0.15.1-cdb3
|
||||
cartodb-psql "^0.10.1"
|
||||
debug "^3.1.0"
|
||||
dot "~1.0.2"
|
||||
grainstore "~1.8.0"
|
||||
mapnik "3.5.14"
|
||||
grainstore "1.8.2"
|
||||
queue-async "~1.0.7"
|
||||
redis-mpool "0.4.1"
|
||||
request "^2.83.0"
|
||||
redis-mpool "^0.5.0"
|
||||
request "2.85.0"
|
||||
semver "~5.0.3"
|
||||
sphericalmercator "1.0.4"
|
||||
step "~0.0.6"
|
||||
tilelive "5.12.2"
|
||||
tilelive-bridge cartodb/tilelive-bridge#2.3.1-cdb4
|
||||
tilelive-mapnik cartodb/tilelive-mapnik#0.6.18-cdb3
|
||||
tilelive-mapnik cartodb/tilelive-mapnik#0.6.18-cdb8
|
||||
torque.js "~2.11.0"
|
||||
underscore "~1.6.0"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user