Compare commits

..

631 Commits
4.2.0 ... 5.4.0

Author SHA1 Message Date
Rafa de la Torre
70dab149ba Update release date in NEWS.md 2018-03-15 12:11:03 +01:00
Rafa de la Torre
ed4b44a78a Merge pull request #905 from CartoDB/mapnik-3.6.2-carto.4
Upgrade @carto/mapnik to 3.6.2-carto.4
2018-03-15 12:08:32 +01:00
Rafa de la Torre
de1aaf3808 Update image because of new mapnik cache 2018-03-15 09:55:07 +01:00
Rafa de la Torre
769aee1107 Do not use the @github: notation in yarn.lock
The little advantage is that from a clean dir a `yarn install` won't
modify the `yarn.lock` file.
2018-03-15 09:36:35 +01:00
Rafa de la Torre
3663e6d12a More accurate msg in NEWS.md 2018-03-15 09:33:23 +01:00
Rafa de la Torre
a6d9984453 Upgrade @carto/mapnik to 3.6.2-carto.4 2018-03-15 09:29:08 +01:00
Daniel
f49d7478d7 Merge pull request #899 from CartoDB/refactor-named-maps-admin
Named Maps Admin: Extract middlewares form controller's context
2018-03-14 17:55:03 +01:00
Daniel
b4a1c9d648 Merge pull request #898 from CartoDB/refactor-analysis-controller
Refactor analysis controller
2018-03-14 17:50:15 +01:00
Daniel
692246ec44 Merge pull request #901 from CartoDB/refactor-named-maps-controller
Refactor named maps controller
2018-03-14 17:50:06 +01:00
Daniel García Aubert
04146f897d Use template strings 2018-03-14 17:33:54 +01:00
Daniel García Aubert
a34658c97f Use objects instead of param list 2018-03-14 17:31:37 +01:00
Daniel García Aubert
8d37e00869 Use objects instead of parameter list 2018-03-14 17:25:58 +01:00
Raúl Marín
584d6ae9cf Merge pull request #902 from CartoDB/camshaft0614
Update camshaft to 0.61.4
2018-03-14 17:25:25 +01:00
Daniel García Aubert
421e611356 Use objects instead of a list of parameters 2018-03-14 17:22:47 +01:00
Daniel García Aubert
f078713d28 typo 2018-03-14 17:15:50 +01:00
Daniel García Aubert
a8d31d52cf Pass only required params 2018-03-14 17:08:04 +01:00
Raul Marin
d9213b2fe2 Update camshaft to 0.61.4 2018-03-14 15:56:35 +01:00
Daniel García Aubert
091efe52fc Extract sendResponse middleware from context 2018-03-14 13:32:43 +01:00
Daniel García Aubert
a5c508733a Extract setContentTypeHeader middleware from controller's context 2018-03-14 13:31:39 +01:00
Daniel García Aubert
ce944d9a7d Extract setCacheControlHeader from controller's context 2018-03-14 13:30:27 +01:00
Daniel García Aubert
8321b5adba Extract setLastModifiedHeader from controller's context 2018-03-14 13:27:56 +01:00
Daniel García Aubert
667c972308 Extract setCacheChannelHeader and setSurrogateKeyHeader from controller's context 2018-03-14 13:25:42 +01:00
Daniel García Aubert
3dbe05be3a Extarct setCacheChannelHeader middleware from controller's context 2018-03-14 13:19:56 +01:00
Daniel García Aubert
2d4ce19250 Extract incrementMapViews middleware from controllers middleware 2018-03-14 13:18:37 +01:00
Daniel García Aubert
3b3e0c0acd Extract getImage middleware from controller's context 2018-03-14 13:15:38 +01:00
Daniel García Aubert
c3ddb933bb Extract getStaticImageOptions middleware from controller's context 2018-03-14 13:11:17 +01:00
Daniel García Aubert
6aae60ece7 Extract getTile middleware from controller's context 2018-03-14 13:07:40 +01:00
Daniel García Aubert
6b3dc8ece0 Extract prepareLayerFilterFromPreviewLayers middleware from controller's context 2018-03-14 13:05:05 +01:00
Daniel García Aubert
7dd231a8c9 Extract getTemplate middleware form controller's context 2018-03-14 13:02:49 +01:00
Daniel García Aubert
35a3219012 Extract getAffectedTables middleware from controller's context 2018-03-14 13:01:07 +01:00
Daniel García Aubert
7598e6ab4b Extract getNamedMapProvider middleware from controller's context 2018-03-14 12:58:56 +01:00
Raúl Marín
75870dc6c1 Merge pull request #900 from Algunenano/windshaft457
Update Windshaft to 4.5.7
2018-03-14 10:39:05 +01:00
Raul Marin
42900b5d0e Update Windshaft to 4.5.7 2018-03-14 10:28:34 +01:00
Daniel García Aubert
2cc4161239 Missing semicolon 2018-03-13 15:02:44 +01:00
Daniel García Aubert
fc8f3fdf27 Create send response middleware 2018-03-13 13:31:49 +01:00
Daniel García Aubert
24b76208ac Improve naming 2018-03-13 13:21:40 +01:00
Daniel García Aubert
0de272b195 Extract middlewares form controller's context 2018-03-13 13:12:18 +01:00
Daniel
7faf40004c Merge pull request #891 from CartoDB/refactor-map-controler
Refactor map controler
2018-03-13 12:44:53 +01:00
Daniel García Aubert
88ae2d473a Typo 2018-03-13 11:43:08 +01:00
Daniel García Aubert
337b47685c Remove controler context from middlewares 2018-03-13 11:42:25 +01:00
Raúl Marín
ed3f9be655 Merge pull request #897 from Algunenano/windshaft456
Update Windshaft to 4.5.6
2018-03-12 19:21:09 +01:00
Raul Marin
248c6d5f22 Update Windshaft to 4.5.6 2018-03-12 19:13:56 +01:00
Raúl Marín
dd4aa09d21 Merge pull request #896 from Algunenano/master_camshaft0613
Update camshaft to 0.61.3
2018-03-12 17:02:25 +01:00
Raul Marin
132f2226ca Update camshaft to 0.61.3 2018-03-12 16:52:43 +01:00
Daniel
2eb6e95fed Merge pull request #895 from CartoDB/fix-performance-regression
Fix performance regression
2018-03-12 15:48:20 +01:00
Daniel García Aubert
db8130be4f Honor jshint 2018-03-12 14:18:07 +01:00
Daniel García Aubert
379b649e95 Don't get mapconfig prematurely 2018-03-12 13:29:40 +01:00
Raúl Marín
e5619492ef Merge pull request #894 from Algunenano/master_windshaft455
Update Windshaft to 4.5.5
2018-03-12 13:23:15 +01:00
Daniel García Aubert
cc76ccc626 Fix undeclared variable 2018-03-12 13:10:20 +01:00
Raul Marin
3d6512dd11 Update Windshaft to 4.5.5 2018-03-12 13:01:17 +01:00
Eneko Lakasta
b19d97e01f Merge pull request #886 from CartoDB/vary-header
Vary header
2018-03-12 12:46:42 +01:00
Daniel García Aubert
25931a618b Do not calculate affected tables when there are no affacted tables 2018-03-12 12:33:29 +01:00
Eneko Lakasta
ffab576399 Merge branch 'master' into vary-header 2018-03-12 12:31:23 +01:00
Raúl Marín
e7067ab9cf Merge pull request #893 from Algunenano/master_request_update
Update request to 2.85.0
2018-03-12 12:25:19 +01:00
Eneko Lakasta
7cfcf6d579 merge master 2018-03-12 12:16:55 +01:00
Raul Marin
a4b586055a Update request to 2.85.0 2018-03-12 12:16:29 +01:00
Eneko Lakasta
5ad1e1b645 merge master 2018-03-12 11:52:38 +01:00
Daniel García Aubert
01ed513a79 Use 'const' 2018-03-09 17:02:13 +01:00
Daniel García Aubert
504f68b8aa Missing semicolon 2018-03-09 16:18:33 +01:00
Daniel García Aubert
cbb08f5642 Extract function 2018-03-09 15:49:03 +01:00
Raúl Marín
a4b5d681ce Merge pull request #890 from Algunenano/windshaft_454
Update Windshaft to 4.5.4
2018-03-09 13:12:37 +01:00
Daniel García Aubert
02f93f3a14 Extract layergroup-metadata class 2018-03-09 12:58:05 +01:00
Raul Marin
ad2f4573f8 Update Windshaft to 4.5.4 2018-03-09 11:44:42 +01:00
Raúl Marín
06604cd738 Merge pull request #888 from Algunenano/support_1368
Aggregation count: Do not return null categories
2018-03-09 11:06:40 +01:00
Raul Marin
089be35b5d Aggregation count: Do not return null categories 2018-03-08 18:13:20 +01:00
Daniel García Aubert
bbcb335d60 Merge branch 'master' into refactor-map-controler 2018-03-08 13:18:43 +01:00
Daniel
6ef2e0bb5f Merge pull request #887 from CartoDB/middlewarify-layergroup-controller
Middlewarify layergroup controller
2018-03-08 13:04:39 +01:00
Daniel García Aubert
d8202d881d Remove legacy test 2018-03-08 12:46:04 +01:00
Daniel García Aubert
aae814a156 Use template strings 2018-03-08 12:35:54 +01:00
Daniel García Aubert
49bcc5368d Use base number as radix to pare intergers 2018-03-08 12:30:27 +01:00
Daniel García Aubert
555e04f9e7 Use ternary operator 2018-03-08 12:27:49 +01:00
Daniel García Aubert
3f6f2e4e23 Use template string 2018-03-08 12:23:43 +01:00
Daniel García Aubert
abffc4b067 Uppercase for actual constants 2018-03-08 12:23:00 +01:00
Daniel García Aubert
363cb0b679 Extract middlewares from map-controller class 2018-03-08 12:16:24 +01:00
Daniel García Aubert
d26910ba9c Extract checkJsonContentType middleware from MapController class 2018-03-07 19:11:03 +01:00
Daniel García Aubert
74b2f305ea Extract initProfiler middleware from map-controller 2018-03-07 19:09:52 +01:00
Daniel García Aubert
6c2f893651 Rename map-store-map-config-provider middleware 2018-03-07 18:53:20 +01:00
Daniel García Aubert
faaf121eb6 Rename center and bbox middlewares 2018-03-07 18:51:43 +01:00
Daniel García Aubert
83ab65163d Rename attributes middleware 2018-03-07 18:43:35 +01:00
Daniel García Aubert
9dcd5ff332 Impreve naming 2018-03-07 15:56:16 +01:00
Daniel García Aubert
c6635f63c1 Unify layer and tile middlewares 2018-03-07 15:39:59 +01:00
Daniel García Aubert
56213219e4 Rename middleware 2018-03-07 15:25:30 +01:00
Daniel García Aubert
7c2dc20dbe Merge branch 'master' into middlewarify-layergroup-controller 2018-03-07 15:24:34 +01:00
Daniel García Aubert
c8e8317ea4 Do not attach middleware to LayergroupController classs 2018-03-07 15:20:47 +01:00
Daniel
8509796743 Merge pull request #882 from CartoDB/middleware-refactor
Middleware refactor
2018-03-07 15:19:04 +01:00
Daniel García Aubert
90aaed0f2c Typo 2018-03-07 15:05:36 +01:00
Daniel García Aubert
48be15b742 Use const in favour of var 2018-03-07 15:01:04 +01:00
Daniel García Aubert
a95b3f2f99 Fix comment 2018-03-07 14:54:09 +01:00
Daniel García Aubert
b2cc7ab84f Move functions to improve readablity 2018-03-07 14:53:13 +01:00
Daniel García Aubert
eb3414f07f Follow middleware pattern 2018-03-07 14:48:21 +01:00
Daniel García Aubert
292dad130d Move middlewares to the right place 2018-03-07 14:42:21 +01:00
Daniel García Aubert
ec41cddb19 Do not pass the whole res.locals to backends 2018-03-07 12:52:44 +01:00
Daniel García Aubert
5871f8290d Use default param values 2018-03-07 12:46:18 +01:00
Daniel García Aubert
33089be2cd Do not attach header middlewares to node status endpoint 2018-03-07 12:30:59 +01:00
Daniel García Aubert
d351c8d14c Define var as const 2018-03-07 12:09:41 +01:00
Daniel García Aubert
82446e5ffa Use template string to define routes 2018-03-07 12:05:53 +01:00
Daniel García Aubert
b786164e8a Middlewarify metrics increment whether success or error 2018-03-07 11:56:57 +01:00
Eneko Lakasta
f9cbb3aac8 use assert.equal instead of assert.ok 2018-03-07 10:51:17 +01:00
Daniel García Aubert
a66c19c6c7 Do not bind context when unneeded 2018-03-06 20:05:55 +01:00
Daniel García Aubert
94d1667d70 Refactor affected tables 2018-03-06 20:01:43 +01:00
Daniel García Aubert
3399db1cff Add comment 2018-03-06 18:58:09 +01:00
Daniel García Aubert
874ea99d19 Remove step 2018-03-06 18:43:23 +01:00
Daniel García Aubert
7022fb87b4 Extract header, affected-tables and response middlewares 2018-03-06 18:28:40 +01:00
Daniel García Aubert
7c1e2a6af0 Avoid nested steps 2018-03-06 17:08:39 +01:00
Daniel García Aubert
2f011c3266 Remove nested steps 2018-03-06 17:01:51 +01:00
Daniel García Aubert
4762aa0897 Remove step from sendResponse function 2018-03-06 16:55:27 +01:00
Daniel García Aubert
f30f83331f Extract tile error middleware 2018-03-06 16:44:37 +01:00
Daniel García Aubert
3695e1e3e5 Place function closer to where is called 2018-03-06 16:21:46 +01:00
Daniel García Aubert
585b5929aa Middlewarify tile and layer endpoints 2018-03-06 16:19:53 +01:00
Eneko Lakasta
0185cdf785 please jshint 2018-03-06 15:34:29 +01:00
Eneko Lakasta
8d22ca66ba fix tests 2018-03-06 15:26:35 +01:00
Eneko Lakasta
b0eacb2a79 add vary header to honor authorization header when caching 2018-03-06 12:46:38 +01:00
Daniel García Aubert
9b40370794 Now that mapConfigProvider is linked to 'res.locals' do not pass the whole 'res.locals' to map-config-provider to avoid converting circular structure to JSON 2018-03-06 12:44:17 +01:00
Daniel García Aubert
95f3d58383 Make jshint happy 2018-03-05 19:33:46 +01:00
Daniel García Aubert
0f0cde1093 Middlewarify static-api (bbox/center) endpoints 2018-03-05 19:26:26 +01:00
Daniel García Aubert
ca56df5cfe Middlewarify attributes endpoint 2018-03-05 18:28:52 +01:00
Daniel García Aubert
d8a4209768 Middlewarify analysis-node-status endpoint 2018-03-05 18:13:19 +01:00
Daniel García Aubert
40712a2e62 Middlewarify search dataview endpoint 2018-03-05 18:05:42 +01:00
Daniel García Aubert
acb9ce33b1 Pass dataview-backend as middleware option 2018-03-05 18:04:50 +01:00
Daniel García Aubert
5e43a7145a Middlewarify dataview endpoint 2018-03-05 17:44:04 +01:00
Daniel
39bd6694f2 Merge pull request #883 from CartoDB/fix-named-map-format
Do not force format for default named tiles
2018-03-05 11:29:43 +01:00
Daniel García Aubert
5de8c4f9c3 Make explicit that forceFormat is optional 2018-03-05 11:19:17 +01:00
Daniel García Aubert
f03d98cd0d Going green: fix test 2018-03-02 18:37:13 +01:00
Daniel García Aubert
6331bebb30 Going green: be able to request defaul named tiles in vector format 2018-03-02 18:25:23 +01:00
Daniel García Aubert
fdd4c4aaa0 Going red: get default named map vector tile 2018-03-02 18:22:53 +01:00
Daniel García Aubert
8656fcd8d1 Use 'const' 2018-03-02 14:04:29 +01:00
Daniel García Aubert
f2f6b9d49c ES6 goodies 2018-03-02 13:29:30 +01:00
Daniel García Aubert
82f1e6753b Remove unreachable code 2018-03-02 13:14:02 +01:00
Daniel García Aubert
7ed717607a Missing space before paramenter list 2018-03-02 13:08:57 +01:00
Daniel García Aubert
0ec9491d21 Fix test: Add stub for profiling 2018-03-02 11:16:46 +01:00
Daniel García Aubert
416970c819 Remove empty line 2018-03-01 19:10:35 +01:00
Daniel García Aubert
ccc28f3617 Add profiler step to lzma 2018-03-01 19:09:11 +01:00
Daniel García Aubert
5bac36b30f Remove bad profiler usage 2018-03-01 18:53:05 +01:00
Daniel García Aubert
ef3ffddec7 Cosmetic changes 2018-03-01 18:49:44 +01:00
Daniel García Aubert
e6ba467d98 ES6 goodies 2018-03-01 18:47:07 +01:00
Daniel García Aubert
314508bcd8 Middleware naming convention 2018-03-01 18:46:04 +01:00
Daniel García Aubert
da18506e41 Follow middleware factory pattern 2018-03-01 18:45:04 +01:00
Daniel García Aubert
5eaee0b71e Follow middleware naming convention 2018-03-01 18:12:07 +01:00
Daniel García Aubert
bd93e7dc7e Follow middleware pattern 2018-03-01 18:09:49 +01:00
Daniel García Aubert
2c762813ba Follow middleware pattern, return a function as the actual middleware 2018-03-01 15:52:48 +01:00
Daniel García Aubert
3caa1d9c4a ES6 cosmetics 2018-03-01 15:42:46 +01:00
Daniel García Aubert
b0c924ca03 Follow middleware pattern, should return a function as the actual middleware 2018-03-01 15:42:03 +01:00
Daniel García Aubert
f6f59023b4 Ungroup middlewares 2018-02-28 19:46:46 +01:00
Daniel García Aubert
9dc4e7c955 Use the right step name for profiling 2018-02-28 19:29:53 +01:00
Daniel García Aubert
faa44e54ae Cosmetic changes 2018-02-28 19:29:10 +01:00
Daniel García Aubert
bfb743b851 Improve profiling steps 2018-02-28 19:27:49 +01:00
Daniel García Aubert
dad2e92dd3 Follow middleware naming convention 2018-02-28 19:26:47 +01:00
Daniel García Aubert
59c312ea40 Require modules at the beginning of module 2018-02-28 19:25:50 +01:00
Daniel García Aubert
48c5a458f3 Remove bad use of profiling step 2018-02-28 19:22:22 +01:00
Daniel García Aubert
c0830862c8 Follow middleware naming convention 2018-02-28 19:21:44 +01:00
Daniel García Aubert
42deb7abbe Rename middleware 2018-02-28 19:20:51 +01:00
Daniel García Aubert
62deda6470 Improve naming 2018-02-28 19:13:49 +01:00
Daniel García Aubert
3e0981978a Update NEWS 2018-02-28 17:31:03 +01:00
Eneko Lakasta
35e5170907 Merge pull request #877 from CartoDB/project-auth-api
Auth API
2018-02-28 17:20:21 +01:00
Eneko Lakasta
8eba5dcc01 update cartodb-redis to 0.16.0 2018-02-28 17:07:31 +01:00
Daniel García Aubert
5c2248d419 Merge branch 'master' into project-auth-api 2018-02-28 14:43:12 +01:00
Daniel García Aubert
102b11b1b5 Follow middleware naming convention 2018-02-28 13:10:46 +01:00
Eneko Lakasta
26df09b13f require debug at the top of file 2018-02-28 11:42:44 +01:00
Eneko Lakasta
8867cdbc02 use anonymous function instead of arrow function in middleware export to don't bind this 2018-02-26 15:57:42 +01:00
Eneko Lakasta
521b441da5 default apikey is returned by metadata module if no apikey found, remove this code because is never going to be run 2018-02-20 12:53:33 +01:00
Eneko Lakasta
59ca00b33b move apikey credentials getter to middleware file 2018-02-20 12:31:36 +01:00
Daniel García Aubert
7c7d606aa7 Remove trailing spaces 2018-02-19 19:05:13 +01:00
Daniel
46587e3cf1 Merge pull request #874 from CartoDB/project-auth-api-fallback
add fallback for using metadata fallback
2018-02-19 18:55:24 +01:00
Daniel García Aubert
603ef4044c Reduce cyclomatic complexity 2018-02-19 18:48:02 +01:00
Daniel García Aubert
2e3abfb2cd Catch "name not found" errors from metadata backend and set http code status 404 2018-02-19 18:28:58 +01:00
Daniel García Aubert
47ccb7ded8 Point to main development branch of cartodb-redis 2018-02-19 17:57:46 +01:00
Eneko Lakasta
7e14247ea9 remove cause of unreachable code/dead code. Not necessary because carto redis assures at least the default api key 2018-02-19 17:06:59 +01:00
Eneko Lakasta
5823859b2a update package.json to use the same branch form carto redis 2018-02-16 12:00:05 +01:00
Eneko Lakasta
7b21bd26d0 Merge branch 'project-auth-api' into project-auth-api-fallback 2018-02-16 11:29:36 +01:00
Eneko Lakasta
4ac224688c in fallback mode, use default api key if api key token doesnt exist 2018-02-16 11:20:04 +01:00
Eneko Lakasta
e84d88b7a3 remove test 2018-02-15 17:53:15 +01:00
Eneko Lakasta
cda2616a8a get and check api key credentials from api key: username and token 2018-02-15 17:49:47 +01:00
Eneko Lakasta
11aa4d12bd add tests for getting api key token from requests 2018-02-15 15:27:41 +01:00
Eneko Lakasta
18dbeea003 get apikey token from request in named maps admin middleware 2018-02-15 15:20:52 +01:00
Eneko Lakasta
3e916c6054 check if req.query exist before getting req.query.api_key/map_key 2018-02-15 15:20:05 +01:00
Eneko Lakasta
fc420c2c0f use for compatibility res.locals.api_key instead of res.locals.apikeyToken 2018-02-15 15:19:09 +01:00
Eneko Lakasta
140441b777 fix test 2018-02-15 12:53:01 +01:00
Eneko Lakasta
5db0e9c8d8 add middleware for apikeyToken 2018-02-15 12:50:42 +01:00
Eneko Lakasta
963737d3fb create lib for getting api key token from request 2018-02-15 11:36:42 +01:00
Eneko Lakasta
890f0d1ef6 add fallback for using metadata fallback 2018-02-14 17:31:05 +01:00
Raul Marin
6b0ab45e63 Update Windshaft to 4.5.3 2018-02-13 12:09:59 +01:00
Rafa de la Torre
0b475ab5e2 Stub next version 2018-02-13 09:07:48 +01:00
Rafa de la Torre
97972ac73f Update NEWS.md with v5.3.1 2018-02-13 09:05:06 +01:00
Rafa de la Torre
c3b38b2f60 Merge pull request #865 from CartoDB/perf-boost-agg-dataview
Improve the speed of the aggregation dataview
2018-02-13 09:04:00 +01:00
Rafa de la Torre
b4e06ec1ac Update NEWS.md and stub version 2018-02-12 19:35:34 +01:00
Rafa de la Torre
d0a8bd428f Merge remote-tracking branch 'origin/master' into perf-boost-agg-dataview 2018-02-12 19:33:35 +01:00
Rafa de la Torre
251fe96509 Cosmetic fix, as suggested in PR 2018-02-12 19:24:53 +01:00
Eneko Lakasta
32986e3ebd Merge branch 'master' into project-auth-api 2018-02-12 12:25:46 +01:00
Eneko Lakasta
f106f27df4 Merge branch 'master' into project-auth-api 2018-02-12 12:21:09 +01:00
Raul Marin
3539b658fb Update Windshaft to 4.5.2 2018-02-12 12:19:36 +01:00
Javier Goizueta
abf33a1c68 Release 5.3.0 2018-02-12 12:03:02 +01:00
Javier Goizueta
fc6790ea1e Merge pull request #866 from CartoDB/update-camshaft-to-0.61.2
Update camshaft to 0.61.2
2018-02-12 11:56:31 +01:00
Raul Marin
ac6f0e1c67 Update Windshaft to 4.5.1 2018-02-12 11:35:46 +01:00
Eneko Lakasta
041cd40ec2 please jshint 2018-02-09 19:26:52 +01:00
Eneko Lakasta
8721f56269 add auth test for getting tiles 2018-02-09 19:19:18 +01:00
Eneko Lakasta
1d3045c799 add tests should create/fail creating named maps and regular api key 2018-02-09 12:33:33 +01:00
Daniel García Aubert
52a1ed869c Update to development version of cartodb-redis 2018-02-08 19:28:43 +01:00
Eneko Lakasta
04f60baec5 Set the master role inheritance from regular roles as TBA 2018-02-08 19:01:58 +01:00
Eneko Lakasta
a8de436424 add test should create a layergroup with a buffer analysis using a regular apikey token AND grant privileges to master and regular roles in bootstraping sql 2018-02-08 18:54:14 +01:00
Javier Goizueta
ee7917676b Update camshaft to 0.61.2
This is to fixe a bug in the line-sequential analyses (length was incorrect)
2018-02-08 17:00:13 +01:00
Eneko Lakasta
c7780e9f42 add tests should create a layergroup with a source analysis 2018-02-08 15:20:43 +01:00
Eneko Lakasta
3edd7b8b01 add test should create a layergroup with default apikey token 2018-02-08 14:50:17 +01:00
Eneko Lakasta
455202cd1a organize prepare db api keys 2018-02-08 14:49:42 +01:00
Eneko Lakasta
8bdb82c7be add test should fail creating a layergroup with default apikey token 2018-02-08 14:48:00 +01:00
Eneko Lakasta
fa503ee66a fix test typo 2018-02-08 14:43:12 +01:00
Eneko Lakasta
e1a2ee2381 control API access grants 2018-02-08 13:07:25 +01:00
Eneko Lakasta
b82d26527a remove comment 2018-02-08 12:35:44 +01:00
Eneko Lakasta
1c50dd6b48 add first tests for auth 2018-02-08 12:34:24 +01:00
Eneko Lakasta
b0e9df1400 add pgConnection.getDatabaseParams 2018-02-08 12:04:03 +01:00
Eneko Lakasta
6ebf51ce45 let select apikey type in setDBAuth: regular, default, master 2018-02-08 11:29:17 +01:00
Eneko Lakasta
d9a34f3384 add cartodb250user api keys to redis 2018-02-08 11:13:21 +01:00
Eneko Lakasta
8136a1e136 fix test 2018-02-07 19:12:26 +01:00
Eneko Lakasta
41f3606572 return unauthorized error when api key not found 2018-02-07 19:12:14 +01:00
Eneko Lakasta
ea0542dcb1 remove use of step 2018-02-07 18:48:59 +01:00
Eneko Lakasta
a4dbc1bac2 remove step and check existance of proper api key 2018-02-07 18:20:56 +01:00
Rafa de la Torre
065f56e161 Improve the speed of the aggregation dataview
Improve the performance of the aggregation dataview.

Instead of using a CTE (WITH) for filtered_source, which is only used in
one place to calculate ranks, inject it as a subquery.

This way the planner has a chance to ignore uneeded columns as well as
to parallelize the exectution of the window function (WindowAgg in the
query plan).

That is the part that takes most of the time of the query.

The improvement is about 20-40% in speed on PG10 with 4 cores.
2018-02-07 18:10:13 +01:00
Eneko Lakasta
6b5d6648de fix unit test 2018-02-07 17:14:46 +01:00
Eneko Lakasta
95538707c9 add parameter asMaster to setDBAuth 2018-02-07 17:14:13 +01:00
Eneko Lakasta
4c76a921b1 use res.locals instead of req.params 2018-02-07 16:02:13 +01:00
Eneko Lakasta
85c1c987af refactor setDBConn to not use step 2018-02-07 15:49:32 +01:00
Eneko Lakasta
bde86323fd use master api key in setDBAuth 2018-02-07 15:36:24 +01:00
Eneko Lakasta
880e3f388d remove use of _.extend calls 2018-02-07 12:46:10 +01:00
Eneko Lakasta
c1535b1a12 refactor setDBAuth to not use step 2018-02-07 12:40:36 +01:00
Eneko Lakasta
232ff1ba33 add apikeys keys to be removed after each test 2018-02-07 11:59:00 +01:00
Eneko Lakasta
1b63dcd4e5 add api keys to prepare db for testing 2018-02-07 11:10:50 +01:00
Raúl Marín
b32a0a6547 Merge pull request #864 from Algunenano/master_metrics
Add metrics option to Mapnik renderer (off by default)
2018-02-06 16:48:22 +01:00
Raul Marin
d634be0c30 Update NEWs and Windshaft to 4.5.0 2018-02-06 16:10:44 +01:00
Raul Marin
f9fe3ace37 Add mapnik metrics option 2018-02-06 16:07:22 +01:00
Simon Martín
6cd8131888 updating NEWS 2018-02-06 11:30:34 +01:00
Simon Martín
0ea76f7d15 Merge pull request #849 from CartoDB/redis4
Redis4 support
2018-02-06 11:24:02 +01:00
Simon Martín
51e5b5c255 upgrading cartodb-redis, redis-mpool and windshaft versions 2018-02-05 18:09:14 +01:00
Simon Martín
cedcc094e6 Merge branch 'master' into redis4 2018-02-05 12:42:59 +01:00
Javier Goizueta
bbe8d4e820 Stub next version 2018-02-01 16:39:43 +01:00
Javier Goizueta
5aa98c4ab2 Release 5.2.1 2018-02-01 16:37:05 +01:00
Javier Goizueta
d6a9103779 Merge pull request #861 from CartoDB/860-aggregation-min-res
Limit the minimum size of the the aggregation grid
2018-02-01 16:32:35 +01:00
Javier Goizueta
2e7784ddf2 Add comment to clafify aggregation resolution limit 2018-02-01 10:26:52 +01:00
Raul Marin
086be461b2 Stub next version 2018-02-01 09:33:18 +01:00
Raul Marin
a7157532f1 Release 5.2.0 2018-02-01 09:31:46 +01:00
Raúl Marín
55fd660d69 Merge pull request #859 from Algunenano/master_cache_features
Add Mapnik 'cache-features' option
2018-02-01 09:27:07 +01:00
Javier Goizueta
80604b739a Add test for aggregation with attributes
This reveals #860
2018-01-31 18:56:24 +01:00
Javier Goizueta
d88fbbaa87 Use camelCase 2018-01-31 18:55:28 +01:00
Javier Goizueta
7db0744f67 Simplify expression 2018-01-31 17:54:40 +01:00
Javier Goizueta
d1fcd797a3 Limit the minimum size of the the aggregation grid
Fixes #860
2018-01-31 17:46:13 +01:00
Raul Marin
150c6ee4be Update to Windshaft 5.3.3 2018-01-31 17:24:15 +01:00
Raul Marin
d0df8b1533 Update yarn.lock 2018-01-31 15:40:23 +01:00
Raul Marin
43e1de31fa Add Mapnik 'cache-features' option 2018-01-31 12:18:28 +01:00
Rafa de la Torre
33ed9ab47d Stub next version 2018-01-30 10:16:08 +01:00
Rafa de la Torre
749a08336a Merge pull request #857 from CartoDB/upgrade-to-mapnik-3.6.2-carto.2
Upgrade to node mapnik 3.6.2-carto.2
2018-01-30 10:13:15 +01:00
Rafa de la Torre
467097b3cc Merge remote-tracking branch 'origin/master' into upgrade-to-mapnik-3.6.2-carto.2 2018-01-30 10:07:01 +01:00
Javier Goizueta
487aca52d0 Stub next version 2018-01-29 18:25:14 +01:00
Javier Goizueta
072956addd Release 5.0.1 2018-01-29 18:16:30 +01:00
Rafa de la Torre
781d2d3a28 Update NEWS.md 2018-01-29 17:41:46 +01:00
Rafa de la Torre
2a767cdb83 Update yarn.lock 2018-01-29 17:40:49 +01:00
Rafa de la Torre
e3cf69ac1a Bump windshaft version
That version contains our flavor of mapnik 3.0.15 with a bunch of
patches. See
https://github.com/CartoDB/Windshaft/blob/master/NEWS.md#version-431
2018-01-29 17:40:17 +01:00
Javier Goizueta
27b5420358 Merge pull request #856 from CartoDB/841-the_geom_webmercator-type
Check the type of the_geom_webmercator for aggregation
2018-01-29 16:13:19 +01:00
Javier Goizueta
7641542e67 Check the type of the_geom_webmercator for aggregation
Fixes #841
2018-01-29 15:48:35 +01:00
Javier Goizueta
debb174af4 Add test for aggregation without the_geom
Only the_geom_webmercator is required for aggregation
See #841
2018-01-29 15:44:24 +01:00
Eneko Lakasta
2bd4c9e814 Merge pull request #851 from CartoDB/1259-category-widget-error-group-by-missing
use original column name in group by instead of alias
2018-01-29 15:38:07 +01:00
Simon Martín
0dc7872256 stubs next version 2018-01-29 15:26:41 +01:00
Simon Martín
1e56ba1de9 Merge pull request #854 from CartoDB/respect-types-aggreagation
Respect category type of aggregation dataview
2018-01-29 15:24:38 +01:00
Simon Martín
6f4e338dcb version 5.0.0 2018-01-29 15:19:07 +01:00
Eneko Lakasta
941ebf7d80 Merge branch 'master' into 1259-category-widget-error-group-by-missing 2018-01-29 14:51:59 +01:00
Simon Martín
c38bf6ade8 Merge branch 'master' into respect-types-aggreagation 2018-01-29 14:51:20 +01:00
Javier Goizueta
44c4db93da Merge pull request #855 from CartoDB/846-fix-point-grid
Add cartodb_id to point-grid aggregations
2018-01-29 14:48:17 +01:00
Javier Goizueta
f644b3a226 Add cartodb_id to point-grid aggregation
Fixes #846
2018-01-29 12:49:27 +01:00
Javier Goizueta
7c9b4b7283 Add test to check that cartodb_id is preseent in aggregations
See #846
This revealss that point-grid aggregation is missing cartodb_id
2018-01-29 12:40:59 +01:00
Simon Martín
8c839e214d changing the value of string 2018-01-26 15:44:21 +01:00
Simon Martín
99421b613c moving 'other' outside of the query allowing queries of different types 2018-01-26 15:24:21 +01:00
Simon Martín
bc7a556297 removing category cast to string in aggregation 2018-01-26 12:37:10 +01:00
Simon Martín
2c703e5c16 updating node-redis-mpool dev version 2018-01-19 14:40:27 +01:00
Eneko Lakasta
220f1d6a73 use original column name in group by instead of alias 2018-01-18 15:27:54 +01:00
Simon Martín
e68ba95fed adding cartodb-redis with redis4 support 2018-01-18 15:16:40 +01:00
Simon Martín
83d00a8aca Merge branch 'master' into redis4 2018-01-17 10:29:32 +01:00
Daniel
767dde0b1e Merge pull request #850 from CartoDB/fix-named-map-force-all-layer
Fix named map regression: default to all layer
2018-01-16 19:22:56 +01:00
Daniel García Aubert
da32d96607 Fix regression: default to all layers if layer filter is not provided 2018-01-16 17:57:22 +01:00
Daniel García Aubert
76da828168 Use error label as middleware argument 2018-01-16 17:55:09 +01:00
Simon Martín
479e8970a1 Merge branch 'master' into redis4 2018-01-16 15:10:08 +01:00
Simon Martín
068c242148 Merge pull request #848 from CartoDB/removing-windshaft-carto-testing-image
Removing docker windshaft-carto-testing image
2018-01-16 15:00:34 +01:00
Simon Martín
e4c409f9a5 removing docker windshaft-carto-testing image 2018-01-16 13:01:32 +01:00
Simon Martín
00ffd75781 removing docker-publish command 2018-01-16 12:59:13 +01:00
Simon Martín
cf5e797f90 supporting redis4 2018-01-16 11:59:23 +01:00
Daniel
128ab53c55 Merge pull request #847 from CartoDB/fix-res-locals-named-maps
Do not pass the entire res.locals to named maps provider cache
2018-01-15 19:02:01 +01:00
Daniel García Aubert
ce4050e3e3 Extrac method to get only user params 2018-01-15 18:09:54 +01:00
Daniel García Aubert
b82767c60d Pass a copy of res.locals w/o new data boud to named-map-provider-cache 2018-01-15 17:40:34 +01:00
Raul Marin
0fdab08600 Torque boundaries tests: Sort objects before comparison
Order is not guaranteed by torque and changed behaviour from PG 9.5 to 10
2018-01-15 16:44:10 +01:00
Javier Goizueta
4ba2632a92 Merge pull request #839 from CartoDB/mapconfig-aggregation-spec
Aggregation documetation
2018-01-12 15:04:45 +01:00
Raul Ochoa
d9e66c5964 Link to overviews doc 2018-01-11 15:15:33 +00:00
Raul Ochoa
72bebf1960 Fix typo 2018-01-11 15:15:25 +00:00
Eneko Lakasta
3fa2869665 Merge pull request #840 from CartoDB/984-ICU-dat-not-loading
984 icu dat not loading
2018-01-11 15:22:42 +01:00
Raul Ochoa
e57c4c824b fix invalid json 2018-01-11 11:45:44 +00:00
Eneko Lakasta
8e68e5395d remove .only from test 2018-01-11 12:23:16 +01:00
Eneko Lakasta
0236935212 please jshint 2018-01-11 12:22:51 +01:00
Eneko Lakasta
86e20b4b26 recreate test images with new font 2018-01-11 12:15:23 +01:00
Eneko Lakasta
86d58fea7b use DejaVu Sans Book instead of Open Sans Italic in test 2018-01-11 12:09:04 +01:00
Eneko Lakasta
9934d69736 adjust test image tolerance 2018-01-11 11:57:36 +01:00
Eneko Lakasta
ae48a01e26 extract setICUEnvVariable() to it's own module 2018-01-11 11:57:11 +01:00
Eneko Lakasta
4d11403be2 console.log error in test. For testing purposes only. 2018-01-11 10:49:46 +01:00
Eneko Lakasta
bcd14e4f77 add test to check that labels are wrapped 2018-01-10 22:20:19 +01:00
Eneko Lakasta
60d2cc0a4f set ICU_DATA env variable also in tests 2018-01-10 21:06:47 +01:00
Eneko Lakasta
5e53920aae move glob require to the beginning of the file 2018-01-10 16:27:51 +01:00
Eneko Lakasta
9c556964e5 use glob module to get the icu_data directory 2018-01-10 15:15:43 +01:00
Eneko Lakasta
d292a922f6 set ICU_DATA 3 alternatives 2018-01-10 14:51:48 +01:00
Eneko Lakasta
c016175a23 please jshint 2018-01-10 11:24:08 +01:00
Eneko Lakasta
1b85951e06 Merge branch 'master' into 984-ICU-dat-not-loading 2018-01-10 11:19:29 +01:00
Eneko Lakasta
a4e98163fb set ICU_DATA env variable at app bootstrap 2018-01-10 11:13:49 +01:00
Javier Goizueta
99324b15ef Remove placement examples 2018-01-09 15:58:20 +01:00
Javier Goizueta
e34410fd2c Add references to general aggregation documentation in MapConfig spec 2018-01-09 15:08:46 +01:00
Javier Goizueta
cef7545c17 Add documentation section for aggregation 2018-01-09 14:51:55 +01:00
Javier Goizueta
de8ed27207 Document the tilejon and url metadata. 2018-01-09 14:51:37 +01:00
Javier Goizueta
0cfb204c04 Add MapConfig extension for aggregation 2018-01-09 14:49:33 +01:00
Daniel
fc82ca7490 Merge pull request #834 from CartoDB/middlewarify-named-maps-controller
Middlewarify named maps controller
2018-01-09 11:40:59 +01:00
Daniel García Aubert
183c8291bc Use arrow functions when it applies 2018-01-09 11:20:20 +01:00
Daniel García Aubert
d908ffdbca Don't use arrow functions when there is no needed 2018-01-09 11:17:07 +01:00
Raul Ochoa
00a4f481f6 stubs next version 2018-01-04 02:04:48 +00:00
Raul Ochoa
e0bd042bde Release 4.8.0 2018-01-04 02:04:04 +00:00
Raul Ochoa
f881efdc11 Update news 2018-01-04 02:03:28 +00:00
Raul Ochoa
bda5022811 Merge pull request #838 from CartoDB/url-template-metadata
Add urlTemplate URLs to metadata
2018-01-04 00:44:10 +01:00
Raul Ochoa
d5b5ef584d Be explicit about requesting urlTemplate+subdomains format 2018-01-03 23:33:59 +00:00
Raul Ochoa
2cda43dc8d Promote https urls over http 2018-01-03 22:18:59 +00:00
Raul Ochoa
f7f513a61a Add urlTemplate URLs to metadata
This is useful when using client libraries like leaflet.
2018-01-03 20:53:03 +00:00
Raul Ochoa
940c982b68 Stubs next version 2018-01-03 19:22:38 +00:00
Raul Ochoa
d949d1c27f Release 4.7.0 2018-01-03 19:18:39 +00:00
Raul Ochoa
aa1d411fb8 Update news and bump version 2018-01-03 19:17:58 +00:00
Raul Ochoa
f297374449 Merge pull request #837 from CartoDB/tilejson
Return tilejson in metadata
2018-01-03 20:13:44 +01:00
Raul Ochoa
060b93c314 Rename middleware fn name 2018-01-03 18:44:10 +00:00
Raul Ochoa
3ceeaedf02 Fix test after breaking it with linting changes 2018-01-03 16:19:14 +00:00
Raul Ochoa
c6ba9e6102 Fix linting 2018-01-03 16:10:09 +00:00
Raul Ochoa
bf40b240d3 Return tilejson in metadata
It returns tilejson for each individual layer and also for all vector and raster layers.
2018-01-03 16:54:45 +01:00
Raul Ochoa
5d4d2bddd6 Implementation for getTilesUrls
This will be useful for generating the tilejson in the metadata
2018-01-03 16:05:19 +01:00
Raul Ochoa
95dfd87c96 Add test cases for getTilesUrls 2018-01-03 16:04:31 +01:00
Raul Ochoa
eab9e8846e Reorg suite to accommodate getTileURLs 2018-01-03 12:57:01 +00:00
Daniel
788bc302a0 Merge pull request #833 from CartoDB/middlewarify-named-map-admin-controller
Middlewarify named map admin controller
2018-01-03 13:20:14 +01:00
Daniel García Aubert
1ba240d099 Rename middleware function 2018-01-03 13:15:11 +01:00
Daniel
ee0405da1e Merge pull request #836 from CartoDB/disable-default-aggregation
Add test to check layer aggregation disabling
2018-01-03 13:09:31 +01:00
Daniel
5e9b326d03 Merge pull request #835 from CartoDB/skip-polygon-layer-vector-map-config
In vector-only map-config, only aggregate layers with points
2018-01-03 13:09:07 +01:00
Daniel García Aubert
1f30367e59 Add test to check layer aggregation disabling 2018-01-03 12:40:00 +01:00
Daniel García Aubert
26a2f73c2a Update NEWS 2018-01-03 12:30:46 +01:00
Daniel García Aubert
60005e2f7f Fix bad assertion 2018-01-03 12:24:07 +01:00
Daniel García Aubert
1c7da2c4b3 Going green: do not fail when map-config is vector-only and a layer doesn't have points 2018-01-03 12:00:25 +01:00
Daniel García Aubert
3799dd2574 Going red: fail when vector only map-config has a polygon layer 2018-01-03 11:14:20 +01:00
Raul Ochoa
7efb2a2344 Stubs next version 2018-01-02 15:40:54 +00:00
Raul Ochoa
88777abc2c Release 4.6.0 2018-01-02 15:40:10 +00:00
Raul Ochoa
4d9a6f8fbe update news 2018-01-02 15:39:37 +00:00
Daniel
3d9c2e66c5 Merge pull request #830 from CartoDB/pg-mvt-do-not-filter-columns
Aggregation: be able to return a complete row sample as default aggregation
2018-01-02 15:36:08 +01:00
Daniel García Aubert
6bbe715aa6 Update NEWS 2018-01-02 12:57:54 +01:00
Daniel García Aubert
ba002fdb2c Update windshaft to 4.20 2018-01-02 12:38:58 +01:00
Daniel García Aubert
49c97e2cf2 Use default argument 2018-01-02 10:56:45 +01:00
Daniel García Aubert
41e65a9633 Remove max cyclomatic complexity 2018-01-01 18:06:56 +01:00
Daniel García Aubert
feae766e62 Create middleware to fetch named map template 2018-01-01 16:54:35 +01:00
Daniel García Aubert
e3bdeec8ca Simplify middleware 2018-01-01 16:21:22 +01:00
Daniel García Aubert
80c4207c74 Remove underscore dependencie 2017-12-30 18:18:37 +01:00
Daniel García Aubert
80e4306fbc Remove step and assert dependencies 2017-12-30 18:03:26 +01:00
Daniel García Aubert
543d257a20 Move sendResponse to a middleware 2017-12-30 17:18:12 +01:00
Daniel García Aubert
8a023e3d2f Keep error label 2017-12-30 16:08:46 +01:00
Daniel García Aubert
f13b45862d Move incrementMapViews to a middlewares 2017-12-30 16:04:24 +01:00
Daniel García Aubert
731fe4c00f Move getStaticImageOptions and getImage to a middlewares 2017-12-30 15:21:20 +01:00
Daniel García Aubert
500cbb959f Move method to a middleware 2017-12-30 14:13:23 +01:00
Daniel García Aubert
108a319143 Do not use step 2017-12-29 19:33:49 +01:00
Daniel García Aubert
ef5ea5b4cb Create and use getNamedMapProvider middleware 2017-12-29 19:31:02 +01:00
Raul Ochoa
10d1381e51 Merge remote-tracking branch 'origin/master' into pg-mvt-do-not-filter-columns 2017-12-29 17:50:09 +00:00
Daniel García Aubert
dfef7ff3c0 Use spread assignment 2017-12-29 18:45:45 +01:00
Daniel García Aubert
83d0ce4040 Rename method 2017-12-29 18:25:08 +01:00
Daniel García Aubert
75f72c4d07 Return empty aggregation configuration when the map-config is vector-only and the layer has no aggregation 2017-12-29 17:52:28 +01:00
Daniel García Aubert
adb9e55fb2 Avoid snake_case notation 2017-12-29 16:30:42 +01:00
Daniel García Aubert
5d3726de44 Use original variable name 2017-12-29 16:24:38 +01:00
Daniel García Aubert
f186e4736b Use template string 2017-12-29 16:19:00 +01:00
Daniel García Aubert
a00c2b1eef Now main middlewares return a named function with the right context bound 2017-12-29 16:15:48 +01:00
Daniel García Aubert
64d601179d Use const instead of var 2017-12-29 15:22:17 +01:00
Daniel García Aubert
cf2b73e473 Move to up intermediate middlewares 2017-12-29 15:19:52 +01:00
Daniel García Aubert
70932c23df Remove step and assert dependencies 2017-12-29 15:17:29 +01:00
Daniel García Aubert
519d49bd10 Remove finish function and respond in the main middleware 2017-12-29 15:04:44 +01:00
Daniel García Aubert
bf814c4442 keep error label 2017-12-29 13:05:01 +01:00
Daniel García Aubert
f136993c50 Use checkContentType middleware 2017-12-29 12:44:56 +01:00
Daniel García Aubert
ba008ab518 Remove unused function 2017-12-29 12:36:56 +01:00
Daniel García Aubert
e4ed6ee1cc Use authorizedByAPIKey middleware 2017-12-29 12:34:50 +01:00
Daniel García Aubert
fda7661dad Create authorizedByAPIKey middleware 2017-12-29 12:23:52 +01:00
Raul Ochoa
79233471c6 Merge pull request #832 from CartoDB/layers-filters
Support individual layer id filters
2017-12-28 19:48:18 +01:00
Raul Ochoa
a75beefe6e Upgrades windshaft to 4.1.1 2017-12-28 18:34:53 +00:00
Raul Ochoa
e43ccf4f12 Going red: individual layer id filters fail
Depends on https://github.com/CartoDB/Windshaft/pull/584.
2017-12-28 18:19:52 +00:00
Raul Ochoa
cd8e320534 Merge pull request #831 from CartoDB/fail-on-uncaught-exceptions
Make tests to fail if they got an uncaught exception
2017-12-28 19:15:29 +01:00
Raul Ochoa
d7f4d39aa2 Don't not override the full client but only the provided methods 2017-12-28 18:09:41 +00:00
Raul Ochoa
89333185a9 Make tests to fail if they got an uncaught exception 2017-12-28 16:37:17 +00:00
Daniel García Aubert
99b95cf839 Move check of default-aggregation to mapconfig model 2017-12-28 13:50:59 +01:00
Daniel García Aubert
9fbc56b82c Remove FIXME notes to test against PostGIS vector renderer 2017-12-28 13:13:32 +01:00
Daniel García Aubert
9a1bc51fdb Use aggregation-mapconfig's method to discover columns to be exposed used to aggregate 2017-12-28 13:12:41 +01:00
Daniel García Aubert
d42257127b Add method to discover required columns or all of them if it's a default aggregation 2017-12-28 13:11:32 +01:00
Daniel García Aubert
5a730c6df1 Remove exited containers after running test against docker 2017-12-28 13:10:11 +01:00
Daniel García Aubert
418c8691d1 Support default full-sample aggregation for postgis vector renderer 2017-12-27 20:08:43 +01:00
Daniel García Aubert
9885045b41 Do not default to null 2017-12-27 12:48:06 +01:00
Daniel García Aubert
062e6f9594 Merge branch 'full-sample' of github.com:CartoDB/Windshaft-cartodb into pg-mvt-do-not-filter-columns 2017-12-27 12:45:43 +01:00
Daniel
d8428938ae Merge pull request #829 from CartoDB/vector-tiles-doc
Update vector tiles documentation
2017-12-26 11:42:06 +01:00
Simon Martín
ca5f280cb3 updating NEWS.md 2017-12-26 11:03:12 +01:00
Simon Martín
524d5a5597 Merge pull request #828 from Algunenano/i827_timestamp
Timeseries: Avoid collisions with columns named 'timestamp'
2017-12-26 09:55:46 +01:00
Daniel García Aubert
a43779b050 Get columns from layer query a set them into layer opptions 2017-12-22 18:19:57 +01:00
Javier Goizueta
ef3917fa6f Update vector tiles documentation
This update reflects the fact that CartoCSS is now optional for vector tiles.
2017-12-22 17:18:42 +01:00
Raul Marin
031e1253ca Numeric histograms: Avoid conflicts with 'bin' named columns 2017-12-22 17:12:57 +01:00
Raul Marin
8012d76b68 Timeseries: Avoid collisions with columns named 'timestamp' 2017-12-22 15:58:40 +01:00
Javier Goizueta
d726c9ad01 Fix point-sample aggregation
it failed in  the case of aggregate columns with the name of base columns
2017-12-22 15:48:30 +01:00
Javier Goizueta
1ce8076699 Change default aggregation placement to point-sample
For consistency with the default aggregation.
2017-12-22 15:46:29 +01:00
Javier Goizueta
54f32113f3 Add some aggregation tests 2017-12-22 15:45:34 +01:00
Javier Goizueta
19bf079f2d Exclude test from PostGIS 2.4 2017-12-22 15:45:14 +01:00
Daniel García Aubert
b7ecde5c9d Add function get columns for layer's query 2017-12-22 13:43:30 +01:00
Daniel García Aubert
a2f804d79f Use full-sample aggregation mode 2017-12-22 12:15:37 +01:00
Daniel García Aubert
efdfabf3e9 Remove method 2017-12-22 12:14:34 +01:00
Javier Goizueta
e9a4fc4b2c Use full-sample aggregation only as default
Sampling is performed only when placement, columns or dimensions are specified;
otherwise the regular centroid/grid-point/grid-center is used without sampling.
2017-12-22 11:31:33 +01:00
Daniel García Aubert
a1d536642e Merge branch 'full-sample' into pg-mvt-do-not-filter-columns 2017-12-21 20:01:07 +01:00
Daniel García Aubert
3c00266666 Add support for aggregated columns in mvt format 2017-12-21 20:00:17 +01:00
Daniel García Aubert
7f64d15944 Merge branch 'master' into full-sample 2017-12-20 15:58:58 +01:00
Daniel
8259271184 Merge pull request #826 from CartoDB/fix-typo-dimensions
Fix typo
2017-12-20 15:02:32 +01:00
Daniel García Aubert
20366cedb4 Skip test for PostGis 2.4 2017-12-20 14:53:34 +01:00
Daniel García Aubert
a102d1d366 jshint, I hate you 2017-12-20 14:41:29 +01:00
Daniel García Aubert
4b97b4fd26 Fix typo 2017-12-20 14:35:47 +01:00
Daniel
b94debf10e Merge pull request #825 from CartoDB/export-supported-placements
Export supported placements and create static methods to expose them
2017-12-20 13:27:23 +01:00
Daniel García Aubert
60030784c1 Export supported placements and create static methods to expose them in a fancy way 2017-12-20 12:42:29 +01:00
Daniel García Aubert
cc9b190e5d Minor style formats 2017-12-19 16:17:37 +01:00
Daniel García Aubert
4946ca688c Add test to check full-sample query 2017-12-19 16:17:13 +01:00
Daniel García Aubert
d2828ecaff Update test 2017-12-19 13:07:57 +01:00
Daniel García Aubert
5a3dd6a914 Use supported placemets of aggregation-query 2017-12-19 13:00:18 +01:00
Daniel García Aubert
bcd2fd8f88 Export supported placements 2017-12-19 12:59:33 +01:00
Daniel García Aubert
94a5e66881 Merge branch 'master' into full-sample 2017-12-19 12:47:36 +01:00
Daniel García Aubert
d55b78f76b Update next release version in NEWS 2017-12-19 12:46:29 +01:00
Daniel García Aubert
42149f9ae7 Update NEWS 2017-12-19 12:45:30 +01:00
Daniel
1e08d946b1 Merge pull request #822 from CartoDB/aggregation-validation
Validate aggregation input params
2017-12-19 12:42:28 +01:00
Daniel García Aubert
f22216e6d2 Catch error threw from constructor and follow node callback pattern 2017-12-19 12:23:54 +01:00
Raul Marin
d9cf830fb4 Stub for next release 2017-12-19 12:23:27 +01:00
Raul Marin
b762008c79 Release 4.5.0 2017-12-19 12:23:27 +01:00
Raul Marin
ca2c2b80d8 Update NEWS 2017-12-19 12:09:14 +01:00
Raul Marin
f946dfa65f Date histograms: Add tests for the new aggregation modes 2017-12-19 12:09:14 +01:00
Raul Marin
418f5faa11 Date histogram: Reduce the threshold to change in auto mode to 100 2017-12-19 12:09:14 +01:00
Raul Marin
bba6db9dbf Date histogram: Add second, decade, century and millenium aggregations 2017-12-19 12:09:14 +01:00
Daniel García Aubert
326cad2f2c Typo 2017-12-19 10:54:20 +01:00
Daniel García Aubert
34808d6147 Improve naming 2017-12-19 10:50:53 +01:00
Daniel García Aubert
79b04bbdfd Rename param 2017-12-19 10:47:53 +01:00
Daniel García Aubert
45a663d5ae Split columns validator 2017-12-19 10:43:34 +01:00
Daniel García Aubert
cace6169c0 Add function to create layer errors 2017-12-19 10:25:41 +01:00
Daniel García Aubert
bdce2f95f2 Add validations for columns 2017-12-18 20:42:26 +01:00
Javier Goizueta
506e16fc87 Experimental full-sample aggregation 2017-12-18 20:18:37 +01:00
Daniel García Aubert
c367743d76 Export SUPPORTED_AGGREGATE_FUNCTIONS 2017-12-18 20:06:16 +01:00
Daniel García Aubert
fa7140e736 Rename argument 2017-12-18 19:52:50 +01:00
Daniel García Aubert
c63226cd26 Improve function naming 2017-12-18 19:51:55 +01:00
Daniel García Aubert
777df6337b Style typo 2017-12-18 19:47:11 +01:00
Daniel García Aubert
2dda0a80da Improve error context 2017-12-18 19:35:12 +01:00
Daniel García Aubert
e2bd97eea6 Move validation to the constructor 2017-12-18 19:19:02 +01:00
Daniel García Aubert
fb03cd3424 Move aggregation validation to its own module 2017-12-18 19:17:43 +01:00
Daniel García Aubert
8a48b96c53 Rename file 2017-12-18 19:06:01 +01:00
Daniel García Aubert
76b0c94835 Rename file 2017-12-18 19:05:49 +01:00
Daniel García Aubert
6a36aa1f13 Order checks to validate if a layer should be adapted 2017-12-18 18:56:53 +01:00
Daniel García Aubert
800870e783 Remove local variable 2017-12-18 18:55:32 +01:00
Daniel García Aubert
6638ba91c3 Refactor supported geometry types 2017-12-18 18:53:44 +01:00
Daniel García Aubert
47e4b9da0d Encapsulate threshold layer validation in aggregation-mapconfig 2017-12-18 18:43:14 +01:00
Daniel García Aubert
81e0c3a098 Add RESOLUTION default getter 2017-12-18 18:26:08 +01:00
Daniel García Aubert
2068861988 Add PLACEMENT default getter 2017-12-18 18:24:09 +01:00
Daniel García Aubert
878f3bd627 Move .sql() to aggregation-mapconfig 2017-12-18 18:17:01 +01:00
Daniel García Aubert
170fcc1973 Move static methods 2017-12-18 17:42:12 +01:00
Daniel García Aubert
d0c88ce21d Improve naming 2017-12-18 17:26:41 +01:00
Javier Goizueta
86d8f28661 Merge pull request #818 from CartoDB/fix-aggr-grid-point
Fix grid-point aggregation placement
2017-12-18 16:37:39 +01:00
Javier Goizueta
e97147ddb4 Fix grid-point aggregation placement 2017-12-18 15:48:51 +01:00
Simon Martín
a40bc4a527 Merge pull request #816 from CartoDB/stringify-for-error-log
Stringify for error log
2017-12-18 15:03:28 +01:00
Simon Martín
77f64bee8c stringifyForLogs more usual case first 2017-12-18 14:54:36 +01:00
Daniel García Aubert
e81a16ce0d Improve validation by applying refactor 2017-12-18 14:31:53 +01:00
Daniel García Aubert
153a792fcb Improve validation by applying refactor 2017-12-18 14:25:44 +01:00
Daniel García Aubert
5c1b1e3214 Improve validation by applying refactor 2017-12-18 14:21:03 +01:00
Daniel García Aubert
0bca3d6f33 Validate placement, threshold and resolution 2017-12-18 13:42:27 +01:00
Simon Martín
14e90a6c76 add line at EOF 2017-12-18 12:59:44 +01:00
Simon Martín
a57cd25bec test escape chars function 2017-12-18 12:35:44 +01:00
Simon Martín
a46f7b3099 nested options and using it 2017-12-18 12:34:56 +01:00
Simon Martín
cb7fb97a13 escape chars function 2017-12-18 11:14:27 +01:00
Daniel García Aubert
e4ae3e235d Merge branch 'master' of github.com:CartoDB/Windshaft-cartodb 2017-12-18 10:17:04 +01:00
Javier Goizueta
423620b6c5 Merge pull request #813 from CartoDB/point-grid-bug
Fix point-grid aggregation bug
2017-12-15 11:27:56 +01:00
Javier Goizueta
877ed63090 Add tests for the different aggregation placement values 2017-12-15 11:14:17 +01:00
Ivan Malagon
8e9f61f9f1 Merge pull request #809 from CartoDB/analyses-filters-params
Add `no_filters` param to dataviews
2017-12-15 11:13:56 +01:00
Javier Goizueta
81e54660bb Fix point-grid aggregation bug 2017-12-15 10:52:41 +01:00
Daniel García Aubert
4d6c501fa5 Merge branch 'master' of github.com:CartoDB/Windshaft-cartodb 2017-12-15 10:31:55 +01:00
Raul Ochoa
55d9c02f8d Merge pull request #812 from CartoDB/fix-agg-point-sample-grid
Use source query as attribute instead of function
2017-12-14 19:58:46 +01:00
Raul Ochoa
a0c24d132e Use source query as attribute instead of function 2017-12-14 18:43:59 +00:00
Daniel García Aubert
6dd4914460 Update NEWS 2017-12-14 18:52:54 +01:00
Daniel
ee4e7b01a9 Merge pull request #806 from CartoDB/mapconfig-aggregation
Mapconfig aggregation
2017-12-14 18:38:00 +01:00
Daniel García Aubert
434de7786c Fix test from merge 2017-12-14 18:26:15 +01:00
Daniel García Aubert
07b4cb78b1 Merge branch 'master' into mapconfig-aggregation 2017-12-14 18:19:54 +01:00
Javier Goizueta
6b472c0b20 Experimental aggregation dimensions
This is not meant por public consumption (exposing SQL expressions is undesiderable)
2017-12-14 17:51:49 +01:00
Ivan Malagon
4b0a4dd675 Update NEWS.md 2017-12-14 17:44:46 +01:00
Ivan Malagon
97f8c361ed Merge branch 'master' into analyses-filters-params 2017-12-14 17:40:14 +01:00
Javier Goizueta
0c044636ef Fix mode aggregation 2017-12-14 17:22:50 +01:00
Javier Goizueta
f95c310462 Redefine aggregation torque to match Torque
Now the resolution aggregation parameter has the same meaning as in Torque (-torque-resolution in CartoCSS)
2017-12-14 17:04:23 +01:00
Javier Goizueta
9d8ce6bc44 Refactor aggregation resolution 2017-12-14 17:04:23 +01:00
Daniel García Aubert
e4407ece84 Merge branch 'mapconfig-aggregation' of github.com:CartoDB/Windshaft-cartodb into mapconfig-aggregation 2017-12-14 16:39:07 +01:00
Javier Goizueta
507d105ab2 Add mode aggregation 2017-12-14 16:37:40 +01:00
Javier Goizueta
ba6cca46a1 Fix aggregation queries 2017-12-14 16:37:15 +01:00
Javier Goizueta
753ada0e76 Add cartodb_id to test datasets 2017-12-14 16:36:24 +01:00
Daniel García Aubert
d311dccce8 Add test to check tangram compatibility 2017-12-14 16:35:09 +01:00
Javier Goizueta
b81cfe418a Always add a _cdb_feature_count to aggregated queries 2017-12-14 15:02:03 +01:00
Daniel García Aubert
8ee4a2c049 Merge branch 'mapconfig-aggregation' of github.com:CartoDB/Windshaft-cartodb into mapconfig-aggregation 2017-12-14 14:15:20 +01:00
Daniel García Aubert
a987f6ac05 Fix issue when the sql has single quotes defined and the aggregation metadata query was not able to estimate row count 2017-12-14 14:14:55 +01:00
Javier Goizueta
b0e47ecc62 Fix aggregation resolution parameter
It was implemented as the inverse of the intended value
2017-12-14 12:23:02 +01:00
Javier Goizueta
daa3fdca11 Fix bug in point-grid aggregation 2017-12-14 12:14:31 +01:00
Daniel García Aubert
bcfc43a517 jshint, my old friend 2017-12-14 11:22:00 +01:00
Daniel García Aubert
b83351a504 Use last release of windshaft 2017-12-13 20:07:23 +01:00
Daniel García Aubert
1edf684475 Fix test 2017-12-13 20:04:06 +01:00
Daniel García Aubert
0bc68d7144 Do not override sql_raw 2017-12-13 19:46:25 +01:00
Daniel García Aubert
52d1cd47db Do not validate aggregation missing columns. It will fail afterwards in map validation 2017-12-13 19:24:17 +01:00
Daniel García Aubert
98e8d745b1 Support sql_wrap for aggregation 2017-12-13 17:01:43 +01:00
Daniel García Aubert
e8740af6ef Fix issue when sql_wrap is provided and aggregation metadata query fails 2017-12-13 16:34:36 +01:00
Simon Martín
a00f468e62 NEWS.md 2017-12-13 15:07:55 +01:00
Simon Martín
27a52b66c6 Merge pull request #800 from CartoDB/errorLogs
Logging all errors
2017-12-13 15:05:11 +01:00
Simon Martín
96b9d498fd Merge branch 'master' into errorLogs 2017-12-13 14:50:51 +01:00
Javier Goizueta
6d30903531 Merge branch 'mapconfig-aggregation' of github.com:CartoDB/Windshaft-cartodb into mapconfig-aggregation
# Conflicts:
#	lib/cartodb/models/mapconfig/adapter/aggregation-mapconfig-adapter.js
2017-12-13 12:53:20 +01:00
Javier Goizueta
4a63fed943 Simplify Aggregation classes
We're using the same aggregation queries for the Raster and Vector cases, so we don't need the class hierarchies used to handled them differently.
AggregationProxy has been renamed to Aggregation
2017-12-13 12:35:17 +01:00
Daniel García Aubert
6fe73862f3 Create a MapConfig's subclass to delegate aggregation 2017-12-13 11:42:51 +01:00
Ivan Malagon
1664975dd1 Add spec 2017-12-13 10:43:43 +01:00
Ivan Malagon
02ac25181e Return error if no_filters and own_filter are present 2017-12-13 09:45:35 +01:00
Daniel García Aubert
239aa12622 Merge branch 'mapconfig-aggregation' of github.com:CartoDB/Windshaft-cartodb into mapconfig-aggregation 2017-12-12 20:28:50 +01:00
Daniel García Aubert
aa43eb8953 Remove aggregation validation and use MapConfig validation 2017-12-12 20:10:42 +01:00
Daniel García Aubert
6d46a21005 Validate aggregation query param 2017-12-12 19:23:21 +01:00
Raul Ochoa
fb7f79594d Merge remote-tracking branch 'origin/master' into mapconfig-aggregation 2017-12-12 17:15:22 +00:00
Daniel García Aubert
f390a10830 Remove methods that check map-config aggregation and use the ones that MapConfig model provides 2017-12-12 17:58:42 +01:00
Javier Goizueta
4193f96c03 Fix point-grid aggregation query 2017-12-12 17:38:39 +01:00
Raul Ochoa
afa1e2881f Stubs next version 2017-12-12 16:24:12 +00:00
Raul Ochoa
3fa6750f9a Release 4.4.0 2017-12-12 16:23:37 +00:00
Raul Ochoa
1c842c1592 Merge pull request #810 from CartoDB/upgrade-camshaft
Upgrades camshaft to 0.60.0
2017-12-12 17:22:06 +01:00
Raul Ochoa
3c88634d09 Upgrades camshaft to 0.60.0 2017-12-12 16:14:49 +00:00
Simon Martín
19bb11adc5 line at EOF 2017-12-12 16:59:07 +01:00
Javier Goizueta
4405d61845 Remove support for arbitrary aggregation SQL expressions.
Only the supported aggregate functions can be used now, currently count, sum, avg, min & max.
2017-12-12 16:17:42 +01:00
Simon Martín
1bb716ef33 Merge branch 'master' into errorLogs 2017-12-12 16:15:30 +01:00
Javier Goizueta
eb2825eea8 Merge branch 'mapconfig-aggregation' of github.com:CartoDB/Windshaft-cartodb into mapconfig-aggregation
# Conflicts:
#	lib/cartodb/models/aggregation/aggregation-templates.js
2017-12-12 16:08:58 +01:00
Ivan Malagon
811f2bdae3 Fix linter problem 2017-12-12 16:04:25 +01:00
Ivan Malagon
53bc14bc9e Add missing contributors 2017-12-12 16:02:39 +01:00
Ivan Malagon
50ddfaa968 Fix broken spec 2017-12-12 15:56:16 +01:00
Daniel García Aubert
ae35acd21d typo 2017-12-12 15:54:36 +01:00
Daniel García Aubert
d4d32bdfa3 Make jshint more happy 2017-12-12 15:53:35 +01:00
Javier Goizueta
3b7db0b08f Fix typo 2017-12-12 15:48:25 +01:00
Ivan Malagon
43fec74372 Modify params specs 2017-12-12 15:12:33 +01:00
Mario de Frutos
c7f5f310f0 Stubs next version 2017-12-12 13:46:54 +01:00
Daniel García Aubert
e26cfb2efb Remove magic number 2017-12-12 13:32:01 +01:00
Mario de Frutos
b32d056efe Updated NEWS.md 2017-12-12 13:22:57 +01:00
Mario de Frutos
65308ea2eb Updated NEWS.md 2017-12-12 13:21:27 +01:00
Mario de Frutos
8d16bf566d Force png tile generation for static maps (#808)
* Force png tile generation for static maps

If the user tries to generate a static map requesting JPG it will fail
because is going to try to generate the tiles using JPG as format which
is not supported by now, this bug was introduced in the version 4.0.1

So we now force, again, the tiles to be generated as PNG but we pass
the requested format, JPG, to windshaft to generate the final image as
the user reqests

* Added support to define image format in the image assertions

* Added test for JPEG static image generation

Also I've added support for:

- JPEG images
- Different tolerance based on the file type, it seems that due to
  different compression we need different tolerance for JPG images
2017-12-12 13:20:22 +01:00
Daniel García Aubert
0b27d174ef Check if query retrieves results 2017-12-12 12:53:29 +01:00
Daniel García Aubert
869f2ac322 Improve error message 2017-12-12 12:39:12 +01:00
Daniel García Aubert
5bc1903677 Add test to check if cartoccs and aggregation definition are fully compatible 2017-12-12 12:15:13 +01:00
Daniel García Aubert
faaebaa07d Remove console.log 2017-12-12 12:02:10 +01:00
Daniel García Aubert
eceffda87f Do not use control flag 2017-12-12 12:01:25 +01:00
Daniel García Aubert
e93fe13b41 Get the right columns from aggregation 2017-12-12 11:57:38 +01:00
Ivan Malagon
245d24ea29 Merge branch 'master' into analyses-filters-params 2017-12-12 11:54:32 +01:00
Ivan Malagon
605be77a04 Add nofilters query param 2017-12-12 11:54:09 +01:00
Daniel García Aubert
acd0610517 Merge branch 'mapconfig-aggregation' of github.com:CartoDB/Windshaft-cartodb into mapconfig-aggregation 2017-12-12 11:31:56 +01:00
Javier Goizueta
e37682403c Fix test
Note that the CartoCSS should reference columns of the aggregated table
2017-12-12 11:22:10 +01:00
Javier Goizueta
b2fcbdd8a3 Implement aggregation columns 2017-12-12 11:22:09 +01:00
Daniel García Aubert
2f68d658f0 Remove local variable 2017-12-12 11:10:12 +01:00
Daniel García Aubert
85e7245a33 Remove control flag 2017-12-12 11:07:04 +01:00
Simon Martín
2db2546cca changing error log format 2017-12-12 11:04:06 +01:00
Simon Martín
b3d7909849 removing default error log value 2017-12-12 11:03:52 +01:00
Simon Martín
b035b5d384 Merge branch 'master' into errorLogs 2017-12-12 10:59:07 +01:00
Daniel García Aubert
f52cc276be Remove control flag 2017-12-12 10:57:50 +01:00
Daniel García Aubert
c637caf9c9 Replace nested conditional with guard clause 2017-12-12 10:56:23 +01:00
Daniel García Aubert
d405987a96 Replace nested conditional with guard clause 2017-12-12 10:49:05 +01:00
Daniel García Aubert
06efe410ef Replace nested conditional with guard clause (early return) 2017-12-12 10:43:49 +01:00
Daniel García Aubert
5bf4eba215 Remove unused thenable 2017-12-11 19:35:59 +01:00
Daniel García Aubert
87c4848e19 Improve namig 2017-12-11 19:22:15 +01:00
Daniel García Aubert
3f075ca432 Remove unused argument 2017-12-11 19:18:29 +01:00
Daniel García Aubert
6725025e1a Improve naming for a method 2017-12-11 19:17:32 +01:00
Daniel García Aubert
8d42909eab Change argument order to be more consistent 2017-12-11 19:14:16 +01:00
Daniel García Aubert
d947700646 Get connection at the begining of adapt layers functionality 2017-12-11 19:12:10 +01:00
Daniel García Aubert
cc68b84212 Extract checkLayerAggregationMetadata method 2017-12-11 19:06:53 +01:00
Daniel García Aubert
446449bbde Move variable declaration close to the place that it's used 2017-12-11 18:47:20 +01:00
Daniel García Aubert
b1f788fb57 Remove unuseful callback 2017-12-11 18:42:03 +01:00
Daniel García Aubert
f80e7112bc Merge branch 'mapconfig-aggregation' of github.com:CartoDB/Windshaft-cartodb into mapconfig-aggregation 2017-12-11 18:35:54 +01:00
Daniel García Aubert
68f967e582 Extract adaptLayer method 2017-12-11 18:34:22 +01:00
Javier Goizueta
2edcbb4724 Implement aggregation queries.
Implmented for placements: centroid, point-gird, point-sample.
Aggregated columns not yet implemented (only count).
Aggregation could be made more efficient by using quadkeys
2017-12-11 18:33:06 +01:00
Daniel García Aubert
006dd86614 Merge branch 'master' into mapconfig-aggregation 2017-12-11 17:36:58 +01:00
Daniel García Aubert
dab204ea71 Do not aggregate if rows cout is lower than threshold or the layer's sql has geometries distinct of points 2017-12-11 17:32:06 +01:00
Raul Marin
f2fa650661 Stub for next release 2017-12-11 17:24:26 +01:00
Simon Martín
1c6c3962db Merge branch 'master' into errorLogs 2017-12-11 12:59:42 +01:00
Raul Marin
aa57cdefb3 Release 4.3.0 2017-12-11 11:27:13 +01:00
Raul Marin
7c5b7641d8 Date histogram: Add missing GROUP BY column 2017-12-11 11:27:13 +01:00
Raul Marin
eb4a49ec92 Update NEWs file with the changes 2017-12-11 11:27:13 +01:00
Raul Marin
88f02458db Timeseries tests: Make jshint happy 2017-12-11 11:27:13 +01:00
Raul Marin
1b405e42c2 Date histogram optimizations 2017-12-11 11:27:13 +01:00
Raul Marin
bb5bfd10ee Timeseries tests: Make them work with any DB setup 2017-12-11 11:27:13 +01:00
Raul Marin
088a8b81a6 Timeseries tests: Set timestamps to the start of the aggregations
Also, increase the end of the range check so it includes some data
2017-12-11 11:27:13 +01:00
Raul Marin
243e982bd6 Optimize formulae queries 2017-12-11 11:27:13 +01:00
Raul Marin
dfe01c836c Escape getQueryRowCount with $$ 2017-12-11 11:27:13 +01:00
Raul Marin
fcbf5ffcc5 Move sql helper functions to query-utils.js 2017-12-11 11:27:13 +01:00
Raul Marin
90c9ad18e0 Optimize histogram queries 2017-12-11 11:27:13 +01:00
Daniel García Aubert
214d684fcc Adapt layer when is vector only map-caonfig 2017-12-05 20:39:30 +01:00
Daniel García Aubert
9118e2dc5e Add tests 2017-12-05 20:21:20 +01:00
Daniel García Aubert
e7592ee570 Improve error message 2017-12-05 17:44:52 +01:00
Daniel García Aubert
81d99ca655 Make test to pass 2017-12-05 16:52:15 +01:00
Daniel García Aubert
7b35701fa8 Extract method 2017-12-05 16:50:18 +01:00
Daniel García Aubert
4f8b541010 Mark aggregation queries 2017-12-05 13:12:25 +01:00
Daniel García Aubert
55dd049812 Be able to skip aggregation to create a layergroup with aggregation defined already 2017-12-05 12:59:32 +01:00
Daniel García Aubert
66b41a6ae7 Now .getLayergroup() in test client accepts params to perform custom instantiations 2017-12-05 12:09:31 +01:00
Daniel García Aubert
499e9de75d Use devel branch of windshaft 2017-12-04 19:49:35 +01:00
Daniel García Aubert
855f47e446 Detect incompatible CartoCSS or interactivity for raster aggregation 2017-12-04 19:48:06 +01:00
Daniel García Aubert
fc472e65b6 Update yarn.lock 2017-12-04 15:31:45 +01:00
Daniel García Aubert
91e0e0fd18 Merge branch 'master' into mapconfig-aggregation 2017-12-04 14:49:44 +01:00
Daniel García Aubert
16a36a9d7a Stubs next version 2017-12-04 13:35:23 +01:00
Daniel García Aubert
077f19d506 Integrate aggregation and get metadata for layergroup 2017-12-04 12:40:53 +01:00
Simon Martín
ed51513b5e adding error header acceptance test 2017-12-01 17:52:20 +01:00
Daniel García Aubert
52630b8084 Minor improvementes 2017-12-01 17:06:42 +01:00
Daniel García Aubert
6f04214f5d Simplify to pass test 2017-12-01 17:06:03 +01:00
Daniel García Aubert
f376a7cdd5 Use aggregation adapter before the overviews one 2017-12-01 17:05:01 +01:00
Daniel García Aubert
b7c6f5acdf Merge branch 'vr-aggregation' into mapconfig-aggregation 2017-12-01 16:23:54 +01:00
Daniel García Aubert
0887e5d5f7 Extract method 2017-12-01 15:43:15 +01:00
Daniel García Aubert
d01857923e Plug aggregation mapconfig adapter 2017-11-30 19:31:00 +01:00
Daniel García Aubert
deb29f2c77 Implement aggregation mapconfig adapter (happy case) 2017-11-30 19:20:59 +01:00
Daniel García Aubert
d937ed31d5 Add params to instantiate aggregation 2017-11-30 19:10:57 +01:00
Daniel García Aubert
73ae736603 Add aggregation proxy 2017-11-30 19:02:30 +01:00
Daniel García Aubert
1767b83d09 Aggregation query models: bootstrap hierarchy classes 2017-11-30 15:34:20 +01:00
Simon Martín
ba3af551e3 update test file name 2017-11-30 15:04:38 +01:00
Simon Martín
e0d4a9e596 change funcion name 2017-11-30 15:04:07 +01:00
Simon Martín
555d3f558c changing error log structure 2017-11-28 18:22:55 +01:00
Simon Martín
386d6bfea8 removing unneeded check 2017-11-28 18:19:28 +01:00
Simon Martín
479b8be639 ensuring errored JSONP write a error status code in log 2017-11-28 17:27:05 +01:00
Simon Martín
a007fce913 ensuring vars 2017-11-28 16:02:12 +01:00
Simon Martín
100a2986b9 ensuring all properties in errors headers 2017-11-27 18:43:48 +01:00
Simon Martín
752bfe779e forgotten 'only' 2017-11-27 18:15:27 +01:00
Simon Martín
8cf878f723 testing X-Tiler-Errors existence 2017-11-27 18:14:02 +01:00
Simon Martín
605d7057c9 fix copying array of errors and adding error.label to logs 2017-11-27 18:12:44 +01:00
Simon Martín
60e4defa66 default value in errors header 2017-11-27 17:04:50 +01:00
Simon Martín
e7b8d9b223 moving logErrors to right position 2017-11-27 16:55:11 +01:00
Simon Martín
e041b5b8a9 removing ~lost space 2017-11-27 16:52:19 +01:00
Simon Martín
4a2950796b rest of environments config 2017-11-27 16:48:25 +01:00
Simon Martín
9a8f72b8db format details 2017-11-27 16:47:45 +01:00
Simon Martín
667925c455 adding error name, ensuring data and moving errors copy 2017-11-27 16:43:04 +01:00
Simon Martín
f24217a400 cloning object and removing logs 2017-11-24 18:06:17 +01:00
Simon Martín
84fd01535c adding errors to errors header 2017-11-24 17:53:07 +01:00
Simon Martín
e362fca9eb adding new header 2017-11-24 17:52:26 +01:00
Daniel García Aubert
00f81db57e Fixed default value for own_filter 2017-10-20 16:47:56 +02:00
Daniel García Aubert
0c9d60b573 Add support for no_filters params in dataviews 2017-10-20 16:19:24 +02:00
Raul Ochoa
ad227a5240 Merge remote-tracking branch 'origin/master' into analyses-filters 2017-10-10 16:35:11 +00:00
Raul Ochoa
2b1f12e9d5 Allow to instantiate maps with analyses filters
This decouples filters from dataviews. They are more verbose now.

Misses validation of filters.
2017-10-02 19:16:44 +02:00
97 changed files with 8254 additions and 2210 deletions

View File

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

127
NEWS.md
View File

@@ -1,5 +1,132 @@
# Changelog
## 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
Announcements:
- Upgrades windshaft to [4.2.0](https://github.com/CartoDB/windshaft/releases/tag/4.2.0).
- Validate aggregation input params.
- Fix column names collisions in histograms [#828](https://github.com/CartoDB/Windshaft-cartodb/pull/828).
- Add full-sample aggregation support for vector map-config.
## 4.5.0
Released 2017-12-19
Announcements:
- Date histograms: Add second, decade, century and millenium aggregations
- Date histograms: Switch the auto threshold from 366 buckets to 100.
- Logging all errors.
- Add support for aggregated visualizations.
- Allow vector-only map-config creation.
- Histograms: Now they accept a `no_filters` parameter.
## 4.4.0
Released 2017-12-12
Announcements:
- Upgrades camshaft to [0.60.0](https://github.com/CartoDB/camshaft/releases/tag/0.60.0).
## 4.3.1
Released 2017-12-12
Bug fix:
- Fixed bug introduced in version 4.0.1 that brokes the static map generation using JPG as format #808
## 4.3.0
Released 2017-12-11
Announcements:
- Optimize Formula queries.
- Optimize Formula queries in overviews.
- Optimize Numeric Histogram queries.
- Optimize Date Histogram queries.
- Date Histograms: Now returns the same value for max/min/avg/timestamp per bin.
- Date Histograms: Now it should return the same no matter the DB/Client time zone.
## 4.2.0
Released 2017-12-04

4
app.js
View File

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

View File

@@ -55,7 +55,7 @@ var config = {
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: false
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler]) (:res[X-Tiler-Errors])'
// If log_filename is given logs will be written
// there, in append mode. Otherwise stdout is used (default).
// Log file will be re-opened on receiving the HUP signal
@@ -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

View File

@@ -55,7 +55,7 @@ var config = {
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: true
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler]) (:res[X-Tiler-Errors])'
// If log_filename is given logs will be written
// there, in append mode. Otherwise stdout is used (default).
// Log file will be re-opened on receiving the HUP signal
@@ -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: {

View File

@@ -55,7 +55,7 @@ var config = {
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: true
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type]'
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type] (:res[X-Tiler-Errors])'
// If log_filename is given logs will be written
// there, in append mode. Otherwise stdout is used (default).
// Log file will be re-opened on receiving the HUP signal
@@ -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: {

View File

@@ -54,7 +54,7 @@ var config = {
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: false
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler]) (:res[X-Tiler-Errors])'
// If log_filename is given logs will be written
// there, in append mode. Otherwise stdout is used (default).
// Log file will be re-opened on receiving the HUP signal
@@ -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

View 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
View 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
}
}
}
]
}
```

View File

@@ -1,6 +1,7 @@
# Anonymous Maps
Anonymous Maps allows you to instantiate a map given SQL and CartoCSS. It also allows you to add interaction capabilities using [UTF Grid.](https://github.com/mapbox/utfgrid-spec)
Anonymous Maps allows you to instantiate a map given SQL and CartoCSS. It also allows you to add interaction capabilities using [UTF Grid.](https://github.com/mapbox/utfgrid-spec).
Alternatively, you can get the data for the map (geometry and attributes for each layer) using vector tiles (in which case CartoCSS is not required).
## Instantiate
@@ -41,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
@@ -61,30 +69,49 @@ 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" },
}
}
}
```
## Map Tile Rendering
Map tiles create the graphical representation of your map in a web browser. The performance rendering of map tiles is dependent on the type of geospatial data model (raster or vector) that you are using.
Map tiles are used to create the graphic representation of your map in a web browser. Tiles can be requested either as pre-rendered *raster* tiles (images) or as *vector* map data to be rendered by the client (browser).
- **Raster**: Generates map tiles based on a grid of pixels to represent your data. Each cell is a fixed size and contains values for particular map features. On the server-side, each request queries a dataset to retrieve data for each map tile. The grid size of map tiles can often lead to graphic quality issues.
- **Raster**: If a tile is requested as a raster image format, like PNG, the map will be rendered on the server, using the CartoCSS styles defined in the layers of the map. It is necessary that all the layers of a map define CartoCSS styles in order to obtain raster tiles. Raster tiles are made up of 256x256 pixels; to avoid graphic quality issues tiles should be used unscaled to represent the zoom level (Z) for which they are requested. In order to render tiles, data will be retrieved from the database (in vector format) on the server-side.
- **Vector**: Generates map tiles based on pre-defined coordinates to represent your data, similar to how basemap image tiles are rendered. On the client-side, map tiles represent real-world geometries of a map. Depending on the coordinates, vertices are used to connect the data and display points, lines, or polygons for the map tiles.
- **Vector**: Tiles can also be requested as MVT (Mapbox Vector Tiles). In this case, only the geospatial vector data, without any styling, is returned. These tiles should be processed in the client-side to render the map. In this case layers do not need to define CartoCSS, as any rendering and styling will be performed on the client side. The vector data of a tile represents real-world geometries by defining the vertices of points, lines or polygons in a tile-specific coordinate system.
## Retrieve resources from the layergroup
When you have a layergroup, there are several resources for retrieving layergoup details such as, accessing Mapnik tiles, getting individual layers, accessing defined Attributes, and blending and layer selection.
### Mapnik tiles
### Raster tiles
These raster tiles retrieve just the Mapnik layers. See [individual layers](#individual-layers) for details about how to retrieve other layers.
These raster tiles are PNG images that represent only the Mapnik layers of a map. See [individual layers](#individual-layers) for details about how to retrieve other layers.
```bash
https://{username}.carto.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png
@@ -92,9 +119,9 @@ https://{username}.carto.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png
### Mapbox Vector Tiles (MVT)
[Mapbox Vector Tiles (MVT)](https://www.mapbox.com/vector-tiles/specification/) are map tiles that store geographic vector data on the client-side. Browser performance is fast since you can pan and zoom without having to query the server.
[Mapbox Vector Tiles (MVT)](https://www.mapbox.com/vector-tiles/specification/) are map tiles that transfer geographic vector data to the client-side. Browser performance is fast since you can pan and zoom without having to query the server.
CARTO uses a Web Graphics Library (WebGL) to process MVT files. This is useful since WebGL's are compatible with most web browsers, include support for multiple client-side mapping engines, and do not require additional information from the server; which makes it more efficient for rendering map tiles. However, you can use any implementation tool for processing MVT files.
CARTO uses Web Graphics Library (WebGL) to process MVT files on the browser. This is useful since WebGL is compatible with most web browsers, include support for multiple client-side mapping engines, and do not require additional information from the server; which makes it more efficient for rendering map tiles. However, you can use any implementation tool for processing MVT files.
The following examples describe how to fetch MVT tiles with a cURL request.
@@ -245,7 +272,7 @@ center: [30, 0]
map.setStyle({
"version": 7,
"glyphs": "...",
"constants": {...},
"constants": {...},
"sources": {
"cartodb": {
"type": "vector",

View File

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

View File

@@ -37,12 +37,19 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
throw new Error("Dataview '" + dataviewName + "' does not exists");
}
var pg = new PSQL(dbParamsFromReqParams(params));
var ownFilter = +params.own_filter;
ownFilter = !!ownFilter;
var noFilters = +params.no_filters;
if (Number.isFinite(ownFilter) && Number.isFinite(noFilters)) {
err = new Error();
err.message = 'Both own_filter and no_filters cannot be sent in the same request';
err.type = 'dataview';
err.http_status = 400;
return callback(err);
}
var query = (ownFilter) ? dataviewDefinition.sql.own_filter_on : dataviewDefinition.sql.own_filter_off;
var pg = new PSQL(dbParamsFromReqParams(params));
var query = getDataviewQuery(dataviewDefinition, ownFilter, noFilters);
if (params.bbox) {
var bboxFilter = new BBoxFilter({column: 'the_geom_webmercator', srid: 3857}, {bbox: params.bbox});
query = bboxFilter.sql(query);
@@ -55,7 +62,7 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
);
var dataview = dataviewFactory.getDataview(query, dataviewDefinition);
dataview.getResult(pg, getOverrideParams(params, ownFilter), this);
dataview.getResult(pg, getOverrideParams(params, !!ownFilter), this);
},
function returnCallback(err, result) {
return callback(err, result);
@@ -63,6 +70,16 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
);
};
function getDataviewQuery(dataviewDefinition, ownFilter, noFilters) {
if (noFilters) {
return dataviewDefinition.sql.no_filters;
} else if (ownFilter === 1) {
return dataviewDefinition.sql.own_filter_on;
} else {
return dataviewDefinition.sql.own_filter_off;
}
}
function getQueryRewriteData(mapConfig, dataviewDefinition, params) {
var sourceId = dataviewDefinition.source.id; // node.id
var layer = _.find(mapConfig.obj().layers, function(l) {

View File

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

View File

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

View File

@@ -12,26 +12,26 @@ AnalysesController.prototype.register = function (app) {
app.get(
`${app.base_url_mapconfig}/analyses/catalog`,
cors(),
userMiddleware,
userMiddleware(),
this.prepareContext,
this.createPGClient(),
this.getDataFromQuery({ queryTemplate: catalogQueryTpl, key: 'catalog' }),
this.getDataFromQuery({ queryTemplate: tablesQueryTpl, key: 'tables' }),
this.prepareResponse(),
this.setCacheControlHeader(),
this.sendResponse(),
this.unathorizedError()
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 +48,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 +91,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 +110,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 +121,7 @@ AnalysesController.prototype.unathorizedError = function () {
next(err);
};
};
}
const catalogQueryTpl = ctx => `
SELECT analysis_def->>'type' as type, * FROM cdb_analysis_catalog WHERE username = '${ctx._username}'

View File

@@ -1,20 +1,22 @@
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 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
@@ -46,66 +48,122 @@ 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(),
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(),
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(),
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(),
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(),
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(),
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
'bbox', // w,s,e,n
'start', // number
'end', // number
@@ -118,393 +176,465 @@ LayergroupController.prototype.register = function(app) {
];
app.get(
app.base_url_mapconfig + '/:token/dataview/:dataviewName',
`${basePath}/:token/dataview/:dataviewName`,
cors(),
userMiddleware,
userMiddleware(),
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(),
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(),
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(),
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(),
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';
req.params.format = req.params.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;
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();
}
);
};
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");
mapStore.load(token, (err, mapconfig) => {
if (err) {
global.logger.warn('ERROR generating cache channel:', err);
return next();
}
step(
function getConnection() {
self.pgConnection.getConnection(user, this);
},
function getAffectedTables(err, connection) {
assert.ifError(err);
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`);
});
}
});
QueryTables.getAffectedTablesFromQuery(connection, sql, this);
},
this
);
},
function buildCacheChannel(err, tables) {
assert.ifError(err);
self.layergroupAffectedTables.set(dbName, layergroupId, tables);
const sql = queries.length ? queries.join(';') : null;
return tables;
},
callback
);
};
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();
}
function validateLayerRouteMiddleware(req, res, next) {
if (req.params.token === 'static') {
return next('route');
}
QueryTables.getAffectedTablesFromQuery(connection, sql, (err, affectedTables) => {
req.profiler.done('getAffectedTablesFromQuery');
if (err) {
global.logger.warn('ERROR generating cache channel: ', err);
return next();
}
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);
};
}

View File

@@ -1,19 +1,16 @@
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');
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 _ = 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');
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');
/**
* @param {AuthApi} authApi
@@ -40,7 +37,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;
@@ -68,32 +66,55 @@ MapController.prototype.composeCreateMapMiddleware = function (useTemplate = fal
return [
cors(),
userMiddleware,
userMiddleware(),
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.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) {
@@ -101,9 +122,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'));
@@ -113,9 +134,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;
@@ -137,9 +158,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;
@@ -158,19 +179,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,
@@ -190,10 +211,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;
@@ -215,7 +236,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);
@@ -226,22 +247,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);
@@ -251,16 +272,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);
@@ -277,15 +298,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) {
@@ -294,10 +315,10 @@ MapController.prototype.incrementMapViewCount = function () {
next();
});
}.bind(this);
};
};
}
MapController.prototype.augmentLayergroupData = function () {
function augmentLayergroupData () {
return function augmentLayergroupDataMiddleware (req, res, next) {
const { layergroup } = res.locals;
@@ -308,13 +329,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);
}
@@ -336,17 +357,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;
@@ -356,9 +377,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());
@@ -366,9 +387,9 @@ MapController.prototype.setLastModified = function () {
next();
};
};
}
MapController.prototype.setLastUpdatedTimeToLayergroup = function () {
function setLastUpdatedTimeToLayergroup () {
return function setLastUpdatedTimeToLayergroupMiddleware (req, res, next) {
const { affectedTables, layergroup, analysesResults } = res.locals;
@@ -382,7 +403,7 @@ MapController.prototype.setLastUpdatedTimeToLayergroup = function () {
next();
};
};
}
function getLastUpdatedTime(analysesResults, lastUpdateTime) {
if (!Array.isArray(analysesResults)) {
@@ -397,7 +418,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;
@@ -406,18 +427,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);
}
@@ -431,149 +452,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());
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) {
return function setAnalysesMetadataToLayergroupMiddleware (req, res, next) {
const { layergroup, user, analysesResults = [] } = res.locals;
this.addAnalysesMetadata(user, layergroup, analysesResults, includeQuery);
next();
}.bind(this);
};
MapController.prototype.addAnalysesMetadata = function(username, layergroup, analysesResults, includeQuery) {
includeQuery = includeQuery || false;
analysesResults = analysesResults || [];
layergroup.metadata.analyses = [];
analysesResults.forEach(function(analysis) {
var nodes = analysis.getNodes();
layergroup.metadata.analyses.push({
nodes: nodes.reduce(function(nodesIdMap, node) {
if (node.params.id) {
var nodeResource = layergroup.layergroupid + '/analysis/node/' + node.id();
var nodeRepr = {
status: node.getStatus(),
url: this.resourceLocator.getUrls(username, nodeResource)
};
if (includeQuery) {
nodeRepr.query = node.getQuery();
}
if (node.getStatus() === 'failed') {
nodeRepr.error_message = node.getErrorMessage();
}
nodesIdMap[node.params.id] = nodeRepr;
}
return nodesIdMap;
}.bind(this), {})
});
}.bind(this));
};
MapController.prototype.setTurboCartoMetadataToLayergroup = function () {
return function setTurboCartoMetadataToLayergroupMiddleware (req, res, next) {
const { layergroup, mapconfig, context } = res.locals;
addContextMetadata(layergroup, mapconfig.obj(), context);
layergroupMetadata.addDataviewsAndWidgetsUrls(user, layergroup, mapconfig.obj());
next();
};
};
function addContextMetadata(layergroup, mapConfig, context) {
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
if (context.turboCarto && Array.isArray(context.turboCarto.layers)) {
layer.meta.cartocss_meta = context.turboCarto.layers[layerIndex];
}
return layer;
});
}
}
MapController.prototype.setSurrogateKeyHeader = function () {
function setAnalysesMetadataToLayergroup (layergroupMetadata, includeQuery) {
return function setAnalysesMetadataToLayergroupMiddleware (req, res, next) {
const { layergroup, user, analysesResults = [] } = res.locals;
layergroupMetadata.addAnalysesMetadata(user, layergroup, analysesResults, includeQuery);
next();
};
}
function setTurboCartoMetadataToLayergroup (layergroupMetadata) {
return function setTurboCartoMetadataToLayergroupMiddleware (req, res, next) {
const { layergroup, mapconfig, context } = res.locals;
layergroupMetadata.addTurboCartoContextMetadata(layergroup, mapconfig.obj(), context);
next();
};
}
function setAggregationMetadataToLayergroup (layergroupMetadata) {
return function setAggregationMetadataToLayergroupMiddleware (req, res, next) {
const { layergroup, mapconfig, context } = res.locals;
layergroupMetadata.addAggregationContextMetadata(layergroup, mapconfig.obj(), context);
next();
};
}
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;
@@ -586,9 +549,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) {
@@ -603,7 +566,7 @@ MapController.prototype.augmentError = function (options) {
next(err);
};
};
}
function populateError(err, mapConfig) {
var error = new Error(err.message);

View File

@@ -1,12 +1,31 @@
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');
var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');
var allowQueryParams = require('../middleware/allow-query-params');
var vectorError = require('../middleware/vector-error');
const DEFAULT_ZOOM_CENTER = {
zoom: 1,
center: {
lng: 0,
lat: 0
}
};
function numMapper(n) {
return +n;
}
function getRequestParams(locals) {
const params = Object.assign({}, locals);
delete params.template;
delete params.affectedTablesAndLastUpdate;
delete params.namedMapProvider;
delete params.allowedQueryParams;
return params;
}
function NamedMapsController(prepareContext, namedMapProviderCache, tileBackend, previewBackend,
surrogateKeysCache, tablesExtentApi, metadataBackend) {
@@ -22,311 +41,357 @@ function NamedMapsController(prepareContext, namedMapProviderCache, tileBackend,
module.exports = NamedMapsController;
NamedMapsController.prototype.register = function(app) {
const { base_url_mapconfig, base_url_templated } = app;
app.get(
app.base_url_templated + '/:template_id/:layer/:z/:x/:y.(:format)',
`${base_url_templated}/:template_id/:layer/:z/:x/:y.(:format)`,
cors(),
userMiddleware,
userMiddleware(),
this.prepareContext,
this.tile.bind(this),
vectorError()
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()
);
app.get(
app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format',
`${base_url_mapconfig}/static/named/:template_id/:width/:height.:format`,
cors(),
userMiddleware,
userMiddleware(),
allowQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
this.prepareContext,
this.staticMap.bind(this)
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()
);
};
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');
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;
var self = this;
if (forcedFormat) {
res.locals.format = forcedFormat;
res.locals.layer = res.locals.layer || 'all';
}
step(
function getAffectedTablesAndLastUpdatedTime() {
namedMapProvider.getAffectedTablesAndLastUpdatedTime(this);
},
function sendResponse(err, result) {
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) {
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);
return next(err);
}
}
);
};
NamedMapsController.prototype.staticMap = function(req, res, next) {
var self = this;
res.locals.affectedTablesAndLastUpdate = affectedTablesAndLastUpdate;
var cdbUser = res.locals.user;
next();
});
};
}
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
res.locals.format = req.params.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 || {});
function getTemplate ({ label }) {
return function getTemplateMiddleware (req, res, next) {
const { namedMapProvider } = res.locals;
namedMapProvider.getTemplate((err, template) => {
if (err) {
err.label = 'STATIC_VIZ_MAP';
next(err);
} else {
self.sendResponse(req, res, image, headers, namedMapProvider);
err.label = label;
return next(err);
}
}
);
};
NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (
user,
req,
params,
namedMapProvider,
callback
) {
var self = this;
namedMapProvider.getTemplate(function (err, template) {
if (err) {
return callback(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 callback();
return next();
}
var previewLayers = template.view.preview_layers;
var layerVisibilityFilter = [];
template.layergroup.layers.forEach(function (layer, index) {
template.layergroup.layers.forEach((layer, index) => {
if (previewLayers[''+index] !== false && previewLayers[layer.id] !== false) {
layerVisibilityFilter.push(''+index);
}
});
if (!layerVisibilityFilter.length) {
return callback();
return next();
}
const params = getRequestParams(res.locals);
// 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
);
});
};
namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, provider) => {
if (err) {
err.label = label;
return next(err);
}
var DEFAULT_ZOOM_CENTER = {
zoom: 1,
center: {
lng: 0,
lat: 0
}
};
res.locals.namedMapProvider = provider;
function numMapper(n) {
return +n;
next();
});
};
}
NamedMapsController.prototype.getStaticImageOptions = function(cdbUser, params, namedMapProvider, callback) {
var self = this;
function getTile ({ tileBackend, label }) {
return function getTileMiddleware (req, res, next) {
const { namedMapProvider } = res.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
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();
});
}
};
}
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]
}
});
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();
}
}
step(
function getTemplate() {
namedMapProvider.getTemplate(this);
},
function handleTemplateView(err, template) {
assert.ifError(err);
res.locals.imageOpts = DEFAULT_ZOOM_CENTER;
if (template.view) {
var zoomCenter = templateZoomCenter(template.view);
if (zoomCenter) {
if (Number.isFinite(+params.zoom)) {
zoomCenter.zoom = +params.zoom;
}
return zoomCenter;
}
var bounds = templateBounds(template.view);
if (bounds) {
return bounds;
}
namedMapProvider.getAffectedTablesAndLastUpdatedTime((err, affectedTablesAndLastUpdate) => {
if (err) {
return next();
}
return false;
},
function estimateBoundsIfNoImageOpts(err, imageOpts) {
if (imageOpts) {
return imageOpts;
var affectedTables = affectedTablesAndLastUpdate.tables || [];
if (affectedTables.length === 0) {
return next();
}
var next = this;
namedMapProvider.getAffectedTablesAndLastUpdatedTime(function(err, affectedTablesAndLastUpdate) {
tablesExtentApi.getBounds(user, affectedTables, (err, bounds) => {
if (err) {
return next(null);
return next();
}
var affectedTables = affectedTablesAndLastUpdate.tables || [];
res.locals.imageOpts = bounds;
if (affectedTables.length === 0) {
return next(null);
}
self.tablesExtentApi.getBounds(cdbUser, affectedTables, function(err, result) {
return next(null, result);
});
return next();
});
});
};
}
},
function returnCallback(err, imageOpts) {
return callback(err, imageOpts || DEFAULT_ZOOM_CENTER);
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
@@ -337,9 +402,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: {
@@ -355,3 +419,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);
};
}

View File

@@ -1,10 +1,8 @@
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 localsMiddleware = require('../middleware/context/locals');
const credentialsMiddleware = require('../middleware/context/credentials');
/**
* @param {AuthApi} authApi
@@ -20,211 +18,210 @@ function NamedMapsAdminController(authApi, templateMaps) {
module.exports = NamedMapsAdminController;
NamedMapsAdminController.prototype.register = function (app) {
const { base_url_templated } = app;
app.post(
app.base_url_templated + '/',
`${base_url_templated}/`,
cors(),
userMiddleware,
this.create.bind(this)
userMiddleware(),
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(),
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(),
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(),
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(),
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);
};
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,8 @@ module.exports = function errorMiddleware (/* options */) {
if (err.message === 'Tile does not exist' && res.locals.format === 'mvt') {
statusCode = 204;
}
setErrorHeader(allErrors, statusCode, res);
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
// If a callback was requested, force status to 200
@@ -160,3 +161,53 @@ function errorMessageWithContext(err) {
return error;
}
function setErrorHeader(errors, statusCode, res) {
let errorsCopy = errors.slice(0);
const mainError = errorsCopy.shift();
let errorsLog = {
mainError: {
statusCode: statusCode || 200,
message: mainError.message,
name: mainError.name,
label: mainError.label,
type: mainError.type,
subtype: mainError.subtype
}
};
errorsLog.moreErrors = errorsCopy.map(error => {
return {
message: error.message,
name: error.name,
label: error.label,
type: error.type,
subtype: error.subtype
};
});
res.set('X-Tiler-Errors', stringifyForLogs(errorsLog));
}
/**
* Remove problematic nested characters
* from object for logs RegEx
*
* @param {Object} object
*/
function stringifyForLogs(object) {
Object.keys(object).map(key => {
if(typeof object[key] === 'string') {
object[key] = object[key].replace(/[^a-zA-Z0-9]/g, ' ');
} else if (typeof object[key] === 'object') {
stringifyForLogs(object[key]);
} else if (object[key] instanceof Array) {
for (let element of object[key]) {
stringifyForLogs(element);
}
}
});
return JSON.stringify(object);
}

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
const fs = require('fs');
const timeoutErrorVectorTile = fs.readFileSync(__dirname + '/../../../assets/render-timeout-fallback.mvt');
module.exports = function vectorError() {

View File

@@ -0,0 +1,233 @@
const MapConfig = require('windshaft').model.MapConfig;
const aggregationQuery = require('./aggregation-query');
const aggregationValidator = require('./aggregation-validator');
const {
createPositiveNumberValidator,
createIncludesValueValidator,
createAggregationColumnsValidator
} = aggregationValidator;
const SubstitutionTokens = require('../../utils/substitution-tokens');
const removeDuplicates = arr => [...new Set(arr)];
function prepareSql(sql) {
return sql && SubstitutionTokens.replace(sql, {
bbox: 'ST_MakeEnvelope(0,0,0,0)',
scale_denominator: '0',
pixel_width: '1',
pixel_height: '1'
});
}
module.exports = class AggregationMapConfig extends MapConfig {
static get AGGREGATIONS () {
return aggregationQuery.SUPPORTED_AGGREGATE_FUNCTIONS;
}
static get PLACEMENTS () {
return aggregationQuery.SUPPORTED_PLACEMENTS;
}
static get THRESHOLD () {
return 1e5; // 100K
}
static get RESOLUTION () {
return 1;
}
static get SUPPORTED_GEOMETRY_TYPES () {
return [
'ST_Point'
];
}
static supportsGeometryType(geometryType) {
return AggregationMapConfig.SUPPORTED_GEOMETRY_TYPES.includes(geometryType);
}
static getAggregationGeometryColumn() {
return aggregationQuery.GEOMETRY_COLUMN;
}
constructor (user, config, connection, datasource) {
super(config, datasource);
const validate = aggregationValidator(this);
const positiveNumberValidator = createPositiveNumberValidator(this);
const includesValidPlacementsValidator = createIncludesValueValidator(this, AggregationMapConfig.PLACEMENTS);
const aggregationColumnsValidator = createAggregationColumnsValidator(this, AggregationMapConfig.AGGREGATIONS);
validate('resolution', positiveNumberValidator);
validate('placement', includesValidPlacementsValidator);
validate('threshold', positiveNumberValidator);
validate('columns', aggregationColumnsValidator);
this.user = user;
this.pgConnection = connection;
}
getAggregatedQuery (index) {
const { sql_raw, sql } = this.getLayer(index).options;
const {
// The default aggregation has no placement, columns or dimensions;
// this enables the special "full-sample" aggregation.
resolution = AggregationMapConfig.RESOLUTION,
threshold = AggregationMapConfig.THRESHOLD,
placement,
columns = {},
dimensions = {}
} = this.getAggregation(index);
return aggregationQuery({
query: sql_raw || sql,
resolution,
threshold,
placement,
columns,
dimensions,
isDefaultAggregation: this._isDefaultLayerAggregation(index)
});
}
isAggregationMapConfig () {
return this.isVectorOnlyMapConfig() || this.hasAnyLayerAggregation();
}
isAggregationLayer (index) {
return this.isVectorOnlyMapConfig() || this.hasLayerAggregation(index);
}
hasAnyLayerAggregation () {
const layers = this.getLayers();
for (let index = 0; index < layers.length; index++) {
if (this.hasLayerAggregation(index)) {
return true;
}
}
return false;
}
hasLayerAggregation (index) {
const layer = this.getLayer(index);
const { aggregation } = layer.options;
return aggregation !== undefined && (typeof aggregation === 'object' || typeof aggregation === 'boolean');
}
getAggregation (index) {
if (this.isVectorOnlyMapConfig() && !this.hasLayerAggregation(index)) {
return {};
}
const { aggregation } = this.getLayer(index).options;
if (typeof aggregation === 'boolean') {
return {};
}
return aggregation;
}
getLayerAggregationColumns (index, callback) {
if (this._isDefaultLayerAggregation(index)) {
const skipGeoms = true;
return this.getLayerColumns(index, skipGeoms, (err, columns) => {
if (err) {
return callback(err);
}
return callback(null, columns);
});
}
const columns = this._getLayerAggregationRequiredColumns(index);
return callback(null, columns);
}
_getLayerAggregationRequiredColumns (index) {
const { columns, dimensions } = this.getAggregation(index);
let aggregatedColumns = [];
if (columns) {
aggregatedColumns = Object.keys(columns)
.map(key => columns[key].aggregated_column)
.filter(aggregatedColumn => typeof aggregatedColumn === 'string');
}
let dimensionsColumns = [];
if (dimensions) {
dimensionsColumns = Object.keys(dimensions)
.map(key => dimensions[key])
.filter(dimension => typeof dimension === 'string');
}
return removeDuplicates(aggregatedColumns.concat(dimensionsColumns));
}
doesLayerReachThreshold(index, featureCount) {
const threshold = this.getAggregation(index) && this.getAggregation(index).threshold ?
this.getAggregation(index).threshold :
AggregationMapConfig.THRESHOLD;
return featureCount >= threshold;
}
getLayerColumns (index, skipGeoms, callback) {
const geomColumns = ['the_geom', 'the_geom_webmercator'];
const limitedQuery = ctx => `SELECT * FROM (${ctx.query}) __cdb_schema LIMIT 0`;
const layer = this.getLayer(index);
this.pgConnection.getConnection(this.user, (err, connection) => {
if (err) {
return callback(err);
}
const sql = limitedQuery({
query: prepareSql(layer.options.sql)
});
connection.query(sql, (err, result) => {
if (err) {
return callback(err);
}
let columns = result.fields || [];
columns = columns.map(({ name }) => name);
if (skipGeoms) {
columns = columns.filter((column) => !geomColumns.includes(column));
}
return callback(err, columns);
});
});
}
_isDefaultLayerAggregation (index) {
const aggregation = this.getAggregation(index);
return (this.isVectorOnlyMapConfig() && !this.hasLayerAggregation(index)) ||
aggregation === true ||
this._isDefaultAggregation(aggregation);
}
_isDefaultAggregation (aggregation) {
return aggregation.placement === undefined &&
aggregation.columns === undefined &&
this._isEmptyParameter(aggregation.dimensions);
}
_isEmptyParameter(parameter) {
return parameter === undefined || parameter === null || this._isEmptyObject(parameter);
}
_isEmptyObject (parameter) {
return typeof parameter === 'object' && Object.keys(parameter).length === 0;
}
};

View File

@@ -0,0 +1,258 @@
const DEFAULT_PLACEMENT = 'point-sample';
/**
* Returns a template function (function that accepts template parameters and returns a string)
* to generate an aggregation query.
* Valid options to define the query template are:
* - placement
* - columns
* - dimensions*
* The query template parameters taken by the result template function are:
* - sourceQuery
* - res
* - columns
* - dimensions
*/
const templateForOptions = (options) => {
let templateFn = defaultAggregationQueryTemplate;
if (!options.isDefaultAggregation) {
templateFn = aggregationQueryTemplates[options.placement || DEFAULT_PLACEMENT];
if (!templateFn) {
throw new Error("Invalid Aggregation placement: '" + options.placement + "'");
}
}
return templateFn;
};
/**
* Generates an aggregation query given the aggregation options:
* - query
* - resolution - defined as in torque:
* aggregation cell is resolution*resolution pixels, where tiles are always 256x256 pixels
* - columns
* - placement
* - dimensions
*
* The default aggregation (when no explicit placement, columns or dimensions are present) returns
* a sample record (with all the original columns and _cdb_feature_count) for each aggregation group.
* When placement, columns or dimensions are specified, columns are aggregated as requested
* (by default only _cdb_feature_count) and with the_geom_webmercator as defined by placement.
*/
const queryForOptions = (options) => templateForOptions(options)({
sourceQuery: options.query,
res: 256/options.resolution,
columns: options.columns,
dimensions: options.dimensions
});
module.exports = queryForOptions;
const SUPPORTED_AGGREGATE_FUNCTIONS = {
'count': {
sql: (column_name, params) => `count(${params.aggregated_column || '*'})`
},
'avg': {
sql: (column_name, params) => `avg(${params.aggregated_column || column_name})`
},
'sum': {
sql: (column_name, params) => `sum(${params.aggregated_column || column_name})`
},
'min': {
sql: (column_name, params) => `min(${params.aggregated_column || column_name})`
},
'max': {
sql: (column_name, params) => `max(${params.aggregated_column || column_name})`
},
'mode': {
sql: (column_name, params) => `_cdb_mode(${params.aggregated_column || column_name})`
}
};
module.exports.SUPPORTED_AGGREGATE_FUNCTIONS = Object.keys(SUPPORTED_AGGREGATE_FUNCTIONS);
const sep = (list) => {
let expr = list.join(', ');
return expr ? ', ' + expr : expr;
};
const aggregateColumns = ctx => {
return Object.assign({
_cdb_feature_count: {
aggregate_function: 'count'
}
}, ctx.columns || {});
};
const aggregateColumnNames = (ctx, table) => {
let columns = aggregateColumns(ctx);
if (table) {
return sep(Object.keys(columns).map(
column_name => `${table}.${column_name}`
));
}
return sep(Object.keys(columns));
};
const aggregateColumnDefs = ctx => {
let columns = aggregateColumns(ctx);
return sep(Object.keys(columns).map(column_name => {
const aggregate_function = columns[column_name].aggregate_function || 'count';
const aggregate_definition = SUPPORTED_AGGREGATE_FUNCTIONS[aggregate_function];
if (!aggregate_definition) {
throw new Error("Invalid Aggregate function: '" + aggregate_function + "'");
}
const aggregate_expression = aggregate_definition.sql(column_name, columns[column_name]);
return `${aggregate_expression} AS ${column_name}`;
}));
};
const aggregateDimensions = ctx => ctx.dimensions || {};
const dimensionNames = (ctx, table) => {
let dimensions = aggregateDimensions(ctx);
if (table) {
return sep(Object.keys(dimensions).map(
dimension_name => `${table}.${dimension_name}`
));
}
return sep(Object.keys(dimensions));
};
const dimensionDefs = ctx => {
let dimensions = aggregateDimensions(ctx);
return sep(Object.keys(dimensions).map(dimension_name => {
const expression = dimensions[dimension_name];
return `${expression} AS ${dimension_name}`;
}));
};
// SQL expression to compute the aggregation resolution (grid cell size).
// This is equivalent to `${256/ctx.res}*CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!))`
// This is defined by the ctx.res parameter, which is the number of grid cells per tile linear dimension
// (i.e. each tile is divided into ctx.res*ctx.res cells).
// 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
// the filter added by Mapnik (wrapping the query)
// is only applied after the aggregation.
// * This queries are used for rendering and the_geom is omitted in the results for better performance
// The special default aggregation includes all the columns of a sample row per grid cell and
// the count (_cdb_feature_count) of the aggregated rows.
const defaultAggregationQueryTemplate = ctx => `
WITH
_cdb_params AS (
SELECT
${gridResolution(ctx)} AS res,
!bbox! AS bbox
),
_cdb_clusters AS (
SELECT
MIN(cartodb_id) AS cartodb_id
${dimensionDefs(ctx)}
${aggregateColumnDefs(ctx)}
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
GROUP BY
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
${dimensionNames(ctx)}
) SELECT
_cdb_query.*
${aggregateColumnNames(ctx)}
FROM
_cdb_clusters INNER JOIN (${ctx.sourceQuery}) _cdb_query
ON (_cdb_clusters.cartodb_id = _cdb_query.cartodb_id)
`;
const aggregationQueryTemplates = {
'centroid': ctx => `
WITH
_cdb_params AS (
SELECT
${gridResolution(ctx)} AS res,
!bbox! AS bbox
)
SELECT
row_number() over() AS cartodb_id,
ST_SetSRID(
ST_MakePoint(
AVG(ST_X(_cdb_query.the_geom_webmercator)),
AVG(ST_Y(_cdb_query.the_geom_webmercator))
), 3857
) AS the_geom_webmercator
${dimensionDefs(ctx)}
${aggregateColumnDefs(ctx)}
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
GROUP BY
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
${dimensionNames(ctx)}
`,
'point-grid': ctx => `
WITH
_cdb_params AS (
SELECT
${gridResolution(ctx)} AS res,
!bbox! AS bbox
),
_cdb_clusters AS (
SELECT
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gx,
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gy
${dimensionDefs(ctx)}
${aggregateColumnDefs(ctx)}
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
WHERE the_geom_webmercator && _cdb_params.bbox
GROUP BY _cdb_gx, _cdb_gy ${dimensionNames(ctx)}
)
SELECT
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)}
FROM _cdb_clusters, _cdb_params
`,
'point-sample': ctx => `
WITH
_cdb_params AS (
SELECT
${gridResolution(ctx)} AS res,
!bbox! AS bbox
),
_cdb_clusters AS (
SELECT
MIN(cartodb_id) AS cartodb_id
${dimensionDefs(ctx)}
${aggregateColumnDefs(ctx)}
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
GROUP BY
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
${dimensionNames(ctx)}
)
SELECT
_cdb_clusters.cartodb_id,
the_geom, the_geom_webmercator
${dimensionNames(ctx, '_cdb_query')}
${aggregateColumnNames(ctx, '_cdb_clusters')}
FROM
_cdb_clusters INNER JOIN (${ctx.sourceQuery}) _cdb_query
ON (_cdb_clusters.cartodb_id = _cdb_query.cartodb_id)
`
};
module.exports.SUPPORTED_PLACEMENTS = Object.keys(aggregationQueryTemplates);
module.exports.GEOMETRY_COLUMN = 'the_geom_webmercator';

View File

@@ -0,0 +1,93 @@
module.exports = function aggregationValidator (mapconfig) {
return function validateProperty (key, validator) {
for (let index = 0; index < mapconfig.getLayers().length; index++) {
const aggregation = mapconfig.getAggregation(index);
if (aggregation === undefined || aggregation[key] === undefined) {
continue;
}
validator(aggregation[key], key, index);
}
};
};
module.exports.createIncludesValueValidator = function (mapconfig, validValues) {
return function validateIncludesValue (value, key, index) {
if (!validValues.includes(value)) {
const message = `Invalid ${key}. Valid values: ${validValues.join(', ')}`;
throw createLayerError(message, mapconfig, index);
}
};
};
module.exports.createPositiveNumberValidator = function (mapconfig) {
return function validatePositiveNumber (value, key, index) {
if (!Number.isFinite(value) || value <= 0) {
const message = `Invalid ${key}, should be a number greather than 0`;
throw createLayerError(message, mapconfig, index);
}
};
};
module.exports.createAggregationColumnsValidator = function (mapconfig, validAggregatedFunctions) {
const validateAggregationColumnNames = createAggregationColumnNamesValidator(mapconfig);
const validateAggregateFunction = createAggregateFunctionValidator(mapconfig, validAggregatedFunctions);
const validateAggregatedColumn = createAggregatedColumnValidator(mapconfig);
return function validateAggregationColumns (value, key, index) {
validateAggregationColumnNames(value, key, index);
validateAggregateFunction(value, key, index);
validateAggregatedColumn(value, key, index);
};
};
function createAggregationColumnNamesValidator(mapconfig) {
return function validateAggregationColumnNames (value, key, index) {
Object.keys(value).forEach((columnName) => {
if (columnName.length <= 0) {
const message = `Invalid column name, should be a non empty string`;
throw createLayerError(message, mapconfig, index);
}
});
};
}
function createAggregateFunctionValidator (mapconfig, validAggregatedFunctions) {
return function validateAggregateFunction (value, key, index) {
Object.keys(value).forEach((columnName) => {
const { aggregate_function } = value[columnName];
if (!validAggregatedFunctions.includes(aggregate_function)) {
const message = `Unsupported aggregation function ${aggregate_function},` +
` valid ones: ${validAggregatedFunctions.join(', ')}`;
throw createLayerError(message, mapconfig, index);
}
});
};
}
function createAggregatedColumnValidator (mapconfig) {
return function validateAggregatedColumn (value, key, index) {
Object.keys(value).forEach((columnName) => {
const { aggregated_column } = value[columnName];
if (typeof aggregated_column !== 'string' || aggregated_column <= 0) {
const message = `Invalid aggregated column, should be a non empty string`;
throw createLayerError(message, mapconfig, index);
}
});
};
}
function createLayerError(message, mapconfig, index) {
const error = new Error(message);
error.type = 'layer';
error.layer = {
id: mapconfig.getLayerId(index),
index: index,
type: mapconfig.layerType(index)
};
return error;
}

View File

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

View File

@@ -1,34 +1,14 @@
const BaseDataview = require('./base');
const debug = require('debug')('windshaft:dataview:formula');
const utils = require('../../utils/query-utils');
const countInfinitiesQueryTpl = ctx => `
SELECT count(1) FROM (${ctx.query}) __cdb_formula_infinities
WHERE ${ctx.column} = 'infinity'::float OR ${ctx.column} = '-infinity'::float
`;
const countNansQueryTpl = ctx => `
SELECT count(1) FROM (${ctx.query}) __cdb_formula_nans
WHERE ${ctx.column} = 'NaN'::float
`;
const filterOutSpecialNumericValuesTpl = ctx => `
WHERE
${ctx.column} != 'infinity'::float
AND
${ctx.column} != '-infinity'::float
AND
${ctx.column} != 'NaN'::float
`;
const formulaQueryTpl = ctx => `
SELECT
${ctx.operation}(${ctx.column}) AS result,
(SELECT count(1) FROM (${ctx.query}) _cdb_formula_nulls WHERE ${ctx.column} IS NULL) AS nulls_count
${ctx.isFloatColumn ? `,(${countInfinitiesQueryTpl(ctx)}) AS infinities_count` : ''}
${ctx.isFloatColumn ? `,(${countNansQueryTpl(ctx)}) AS nans_count` : ''}
FROM (${ctx.query}) __cdb_formula
${ctx.isFloatColumn && ctx.operation !== 'count' ? `${filterOutSpecialNumericValuesTpl(ctx)}` : ''}
`;
const formulaQueryTpl = ctx =>
`SELECT
${ctx.operation}(${utils.handleFloatColumn(ctx)}) AS result,
${utils.countNULLs(ctx)} AS nulls_count
${ctx.isFloatColumn ? `,${utils.countInfinites(ctx)} AS infinities_count,` : ``}
${ctx.isFloatColumn ? `${utils.countNaNs(ctx)} AS nans_count` : ``}
FROM (${ctx.query}) __cdb_formula`;
const VALID_OPERATIONS = {
count: true,

View File

@@ -1,5 +1,102 @@
const BaseHistogram = require('./base-histogram');
const debug = require('debug')('windshaft:dataview:date-histogram');
const utils = require('../../../utils/query-utils');
/**
* Gets the name of a timezone with the same offset as the required
* using the pg_timezone_names table. We do this because it's simpler to pass
* the name than to pass the offset itself as PostgreSQL uses different
* sign convention. For example: TIME ZONE 'CET' is equal to TIME ZONE 'UTC-1',
* not 'UTC+1' which would be expected.
* Gives priority to Etc/GMT±N timezones but still support odd offsets like 8.5
* hours for Asia/Pyongyang.
* It also makes it easier to, in the future, support the input of expected timezone
* instead of the offset; that is using 'Europe/Madrid' instead of
* '+3600' or '+7200'. The daylight saving status can be handled by postgres.
*/
const offsetNameQueryTpl = ctx => `
WITH __wd_tz AS
(
SELECT name
FROM pg_timezone_names
WHERE utc_offset = interval '${ctx.offset} hours'
ORDER BY CASE WHEN name LIKE 'Etc/GMT%' THEN 0 ELSE 1 END
LIMIT 1
),`;
/**
* Function to get the subquery that places each row in its bin depending on
* the aggregation. Since the data stored is in epoch we need to adapt it to
* our timezone so when calling date_trunc it falls into the correct bin
*/
function dataBucketsQuery(ctx) {
var condition_str = '';
if (ctx.start !== 0) {
condition_str = `WHERE ${ctx.column} >= to_timestamp(${ctx.start})`;
}
if (ctx.end !== 0) {
if (condition_str === '') {
condition_str = `WHERE ${ctx.column} <= to_timestamp(${ctx.end})`;
}
else {
condition_str += ` and ${ctx.column} <= to_timestamp(${ctx.end})`;
}
}
return `
__wd_buckets AS
(
SELECT
date_trunc('${ctx.aggregation}', timezone(__wd_tz.name, ${ctx.column}::timestamptz)) as timestamp,
count(*) as freq,
${utils.countNULLs(ctx)} as nulls_count
FROM
(
${ctx.query}
) __source, __wd_tz
${condition_str}
GROUP BY 1, __wd_tz.name
),`;
}
/**
* Function that generates an array with all the possible bins between the
* start and end date. If not provided we use the min and max generated from
* the dataBucketsQuery
*/
function allBucketsArrayQuery(ctx) {
var extra_from = ``;
var series_start = ``;
var series_end = ``;
if (ctx.start === 0) {
extra_from = `, __wd_buckets GROUP BY __wd_tz.name`;
series_start = `min(__wd_buckets.timestamp)`;
} else {
series_start = `date_trunc('${ctx.aggregation}', timezone(__wd_tz.name, to_timestamp(${ctx.start})))`;
}
if (ctx.end === 0) {
extra_from = `, __wd_buckets GROUP BY __wd_tz.name`;
series_end = `max(__wd_buckets.timestamp)`;
} else {
series_end = `date_trunc('${ctx.aggregation}', timezone(__wd_tz.name, to_timestamp(${ctx.end})))`;
}
return `
__wd_all_buckets AS
(
SELECT ARRAY(
SELECT
generate_series(
${series_start},
${series_end},
interval '${ctx.interval}') as bin_start
FROM __wd_tz${extra_from}
) as bins
)`;
}
const dateIntervalQueryTpl = ctx => `
WITH
@@ -30,8 +127,11 @@ const dateIntervalQueryTpl = ctx => `
FROM __cdb_interval_in_minutes, __cdb_dates
)
SELECT
ROUND(__cdb_days / 365243) AS millennium,
ROUND(__cdb_days / 36525) AS century,
ROUND(__cdb_days / 3652) AS decade,
ROUND(__cdb_days / 365) AS year,
ROUND(__cdb_days / 90) AS quarter,
ROUND(__cdb_days / 91) AS quarter,
ROUND(__cdb_days / 30) AS month,
ROUND(__cdb_days / 7) AS week,
__cdb_days AS day,
@@ -41,119 +141,22 @@ const dateIntervalQueryTpl = ctx => `
FROM __cdb_interval_in_days, __cdb_interval_in_hours, __cdb_interval_in_minutes, __cdb_interval_in_seconds
`;
const nullsQueryTpl = ctx => `
__cdb_nulls AS (
SELECT
count(*) AS __cdb_nulls_count
FROM (${ctx.query}) __cdb_histogram_nulls
WHERE ${ctx.column} IS NULL
)
`;
const dateBasicsQueryTpl = ctx => `
__cdb_basics AS (
SELECT
max(date_part('epoch', ${ctx.column})) AS __cdb_max_val,
min(date_part('epoch', ${ctx.column})) AS __cdb_min_val,
avg(date_part('epoch', ${ctx.column})) AS __cdb_avg_val,
min(
date_trunc(
'${ctx.aggregation}', ${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}'
)
) AS __cdb_start_date,
max(${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}') AS __cdb_end_date,
count(1) AS __cdb_total_rows
FROM (${ctx.query}) __cdb_basics_query
)
`;
const dateOverrideBasicsQueryTpl = ctx => `
__cdb_basics AS (
SELECT
max(${ctx.end})::float AS __cdb_max_val,
min(${ctx.start})::float AS __cdb_min_val,
avg(date_part('epoch', ${ctx.column})) AS __cdb_avg_val,
min(
date_trunc(
'${ctx.aggregation}',
TO_TIMESTAMP(${ctx.start})::timestamp AT TIME ZONE '${ctx.offset}'
)
) AS __cdb_start_date,
max(
TO_TIMESTAMP(${ctx.end})::timestamp AT TIME ZONE '${ctx.offset}'
) AS __cdb_end_date,
count(1) AS __cdb_total_rows
FROM (${ctx.query}) __cdb_basics_query
)
`;
const dateBinsQueryTpl = ctx => `
__cdb_bins AS (
SELECT
__cdb_bins_array,
ARRAY_LENGTH(__cdb_bins_array, 1) AS __cdb_bins_number
FROM (
SELECT
ARRAY(
SELECT GENERATE_SERIES(
__cdb_start_date::timestamptz,
__cdb_end_date::timestamptz,
${ctx.aggregation === 'quarter' ? `'3 month'::interval` : `'1 ${ctx.aggregation}'::interval`}
)
) AS __cdb_bins_array
FROM __cdb_basics
) __cdb_bins_array_query
)
`;
const dateHistogramQueryTpl = ctx => `
SELECT
(__cdb_max_val - __cdb_min_val) / cast(__cdb_bins_number as float) AS bin_width,
__cdb_bins_number AS bins_number,
__cdb_nulls_count AS nulls_count,
CASE WHEN __cdb_min_val = __cdb_max_val
THEN 0
ELSE GREATEST(
1,
LEAST(
WIDTH_BUCKET(
${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}',
__cdb_bins_array
),
__cdb_bins_number
)
) - 1
END AS bin,
min(
date_part(
'epoch',
date_trunc(
'${ctx.aggregation}', ${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}'
) AT TIME ZONE '${ctx.offset}'
)
)::numeric AS timestamp,
date_part('epoch', __cdb_start_date)::numeric AS timestamp_start,
min(date_part('epoch', ${ctx.column}))::numeric AS min,
max(date_part('epoch', ${ctx.column}))::numeric AS max,
avg(date_part('epoch', ${ctx.column}))::numeric AS avg,
count(*) AS freq
FROM (${ctx.query}) __cdb_histogram, __cdb_basics, __cdb_bins, __cdb_nulls
WHERE date_part('epoch', ${ctx.column}) IS NOT NULL
GROUP BY bin, bins_number, bin_width, nulls_count, timestamp_start
ORDER BY bin
`;
const MAX_INTERVAL_VALUE = 366;
/** Constant to switch between aggregations in auto mode */
const MAX_INTERVAL_VALUE = 100;
const DATE_AGGREGATIONS = {
'auto': true,
'second' : true,
'minute': true,
'hour': true,
'day': true,
'week': true,
'month': true,
'quarter': true,
'year': true
'year': true,
'decade' : true,
'century' : true,
'millennium' : true
};
/**
@@ -176,12 +179,21 @@ module.exports = class DateHistogram extends BaseHistogram {
_buildQueryTpl (ctx) {
return `
WITH
${this._hasOverridenRange(ctx.override) ? dateOverrideBasicsQueryTpl(ctx) : dateBasicsQueryTpl(ctx)},
${dateBinsQueryTpl(ctx)},
${nullsQueryTpl(ctx)}
${dateHistogramQueryTpl(ctx)}
`;
${offsetNameQueryTpl(ctx)}
${dataBucketsQuery(ctx)}
${allBucketsArrayQuery(ctx)}
SELECT
array_position(__wd_all_buckets.bins, __wd_buckets.timestamp) - 1 as bin,
date_part('epoch', timezone(__wd_tz.name, __wd_buckets.timestamp)) AS timestamp,
__wd_buckets.freq as freq,
date_part('epoch', timezone(__wd_tz.name, (__wd_all_buckets.bins)[1])) as timestamp_start,
array_length(__wd_all_buckets.bins, 1) as bins_number,
date_part('epoch', interval '${ctx.interval}') as bin_width,
__wd_buckets.nulls_count as nulls_count
FROM __wd_buckets, __wd_all_buckets, __wd_tz
GROUP BY __wd_tz.name, __wd_all_buckets.bins, __wd_buckets.timestamp, __wd_buckets.nulls_count, __wd_buckets.freq
ORDER BY bin ASC;
`;
}
_buildQuery (psql, override, callback) {
@@ -204,6 +216,9 @@ module.exports = class DateHistogram extends BaseHistogram {
return null;
}
var interval = this._getAggregation(override) === 'quarter' ?
'3 months' : '1 ' + this._getAggregation(override);
const histogramSql = this._buildQueryTpl({
override: override,
query: this.query,
@@ -211,7 +226,8 @@ module.exports = class DateHistogram extends BaseHistogram {
aggregation: this._getAggregation(override),
start: this._getBinStart(override),
end: this._getBinEnd(override),
offset: this._parseOffset(override)
offset: this._parseOffset(override),
interval: interval
});
debug(histogramSql);
@@ -234,9 +250,9 @@ module.exports = class DateHistogram extends BaseHistogram {
return callback(err);
}
const aggegations = result.rows[0];
const aggregation = Object.keys(aggegations)
.map(key => ({ name: key, value: aggegations[key] }))
const aggregations = result.rows[0];
const aggregation = Object.keys(aggregations)
.map(key => ({ name: key, value: aggregations[key] }))
.reduce((closer, current) => {
if (current.value > MAX_INTERVAL_VALUE) {
return closer;
@@ -264,8 +280,8 @@ module.exports = class DateHistogram extends BaseHistogram {
offset: this._getOffset(override),
timestamp_start: firstRow.timestamp_start,
bin_width: firstRow.bin_width,
bins_count: firstRow.bins_number,
bin_width: firstRow.bin_width || 0,
bins_count: firstRow.bins_number || 0,
bins_start: firstRow.timestamp,
nulls: firstRow.nulls_count,
infinities: firstRow.infinities_count,
@@ -275,6 +291,10 @@ module.exports = class DateHistogram extends BaseHistogram {
}
_getBuckets (result) {
result.rows.forEach(function(row) {
row.min = row.max = row.avg = row.timestamp;
});
return result.rows.map(({ bin, min, max, avg, freq, timestamp }) => ({ bin, min, max, avg, freq, timestamp }));
}

View File

@@ -1,44 +1,25 @@
const BaseHistogram = require('./base-histogram');
const debug = require('debug')('windshaft:dataview:numeric-histogram');
const utils = require('../../../utils/query-utils');
const columnCastTpl = ctx => `date_part('epoch', ${ctx.column})`;
const filterOutSpecialNumericValues = ctx => `
${ctx.column} != 'infinity'::float
AND
${ctx.column} != '-infinity'::float
AND
${ctx.column} != 'NaN'::float
`;
const filteredQueryTpl = ctx => `
/** Query to get min and max values from the query */
const irqQueryTpl = ctx => `
__cdb_filtered_source AS (
SELECT *
FROM (${ctx.query}) __cdb_filtered_source_query
WHERE ${ctx.column} IS NOT NULL
${ctx.isFloatColumn ? `AND ${filterOutSpecialNumericValues(ctx)}` : ''}
)
`;
const basicsQueryTpl = ctx => `
WHERE ${utils.handleFloatColumn(ctx)} IS NOT NULL
),
__cdb_basics AS (
SELECT
max(${ctx.column}) AS __cdb_max_val, min(${ctx.column}) AS __cdb_min_val,
avg(${ctx.column}) AS __cdb_avg_val, count(1) AS __cdb_total_rows
max(${ctx.column}) AS __cdb_max_val,
min(${ctx.column}) AS __cdb_min_val,
count(1) AS __cdb_total_rows
FROM __cdb_filtered_source
)
`;
const overrideBasicsQueryTpl = ctx => `
__cdb_basics AS (
SELECT
max(${ctx.end}) AS __cdb_max_val, min(${ctx.start}) AS __cdb_min_val,
avg(${ctx.column}) AS __cdb_avg_val, count(1) AS __cdb_total_rows
FROM __cdb_filtered_source
)
`;
const iqrQueryTpl = ctx => `
/* Query to calculate the number of bins (needs irqQueryTpl before it*/
const binsQueryTpl = ctx => `
__cdb_iqrange AS (
SELECT max(quartile_max) - min(quartile_max) AS __cdb_iqr
FROM (
@@ -47,12 +28,9 @@ const iqrQueryTpl = ctx => `
) AS quartile
FROM __cdb_filtered_source) _cdb_quartiles
WHERE quartile = 1 or quartile = 3
GROUP BY quartile
GROUP BY 1
) __cdb_iqr
)
`;
const binsQueryTpl = ctx => `
),
__cdb_bins AS (
SELECT
CASE WHEN __cdb_total_rows = 0 OR __cdb_iqr = 0
@@ -70,83 +48,6 @@ const binsQueryTpl = ctx => `
)
`;
const overrideBinsQueryTpl = ctx => `
__cdb_bins AS (
SELECT ${ctx.override.bins} AS __cdb_bins_number
)
`;
const nullsQueryTpl = ctx => `
__cdb_nulls AS (
SELECT
count(*) AS __cdb_nulls_count
FROM (${ctx.query}) __cdb_histogram_nulls
WHERE ${ctx.column} IS NULL
)
`;
const infinitiesQueryTpl = ctx => `
__cdb_infinities AS (
SELECT
count(*) AS __cdb_infinities_count
FROM (${ctx.query}) __cdb_infinities_query
WHERE
${ctx.column} = 'infinity'::float
OR
${ctx.column} = '-infinity'::float
)
`;
const nansQueryTpl = ctx => `
__cdb_nans AS (
SELECT
count(*) AS __cdb_nans_count
FROM (${ctx.query}) __cdb_nans_query
WHERE ${ctx.column} = 'NaN'::float
)
`;
const specialNumericValuesColumnDefinitionTpl = () => `
__cdb_infinities_count AS infinities_count,
__cdb_nans_count AS nans_count
`;
const specialNumericValuesCTETpl = () => `
__cdb_infinities, __cdb_nans
`;
const specialNumericValuesColumnTpl = () => `
infinities_count, nans_count
`;
const histogramQueryTpl = ctx => `
SELECT
(__cdb_max_val - __cdb_min_val) / cast(__cdb_bins_number as float) AS bin_width,
__cdb_bins_number AS bins_number,
__cdb_nulls_count AS nulls_count,
${ctx.isFloatColumn ? `${specialNumericValuesColumnDefinitionTpl()},` : ''}
__cdb_avg_val AS avg_val,
CASE WHEN __cdb_min_val = __cdb_max_val
THEN 0
ELSE GREATEST(
1,
LEAST(
WIDTH_BUCKET(${ctx.column}, __cdb_min_val, __cdb_max_val, __cdb_bins_number),
__cdb_bins_number
)
) - 1
END AS bin,
min(${ctx.column})::numeric AS min,
max(${ctx.column})::numeric AS max,
avg(${ctx.column})::numeric AS avg,
count(*) AS freq
FROM __cdb_filtered_source, __cdb_basics, __cdb_nulls, __cdb_bins
${ctx.isFloatColumn ? `, ${specialNumericValuesCTETpl()}` : ''}
GROUP BY bin, bins_number, bin_width, nulls_count, avg_val
${ctx.isFloatColumn ? `, ${specialNumericValuesColumnTpl()}` : ''}
ORDER BY bin
`;
const BIN_MIN_NUMBER = 6;
const BIN_MAX_NUMBER = 48;
@@ -167,14 +68,14 @@ module.exports = class NumericHistogram extends BaseHistogram {
_buildQuery (psql, override, callback) {
const histogramSql = this._buildQueryTpl({
override: override,
column: this._columnType === 'date' ? columnCastTpl({ column: this.column }) : this.column,
column: this._columnType === 'date' ? utils.columnCastTpl({ column: this.column }) : this.column,
isFloatColumn: this._columnType === 'float',
query: this.query,
start: this._getBinStart(override),
end: this._getBinEnd(override),
bins: this._getBinsCount(override),
minBins: BIN_MIN_NUMBER,
maxBins: BIN_MAX_NUMBER,
maxBins: BIN_MAX_NUMBER
});
debug(histogramSql);
@@ -182,19 +83,62 @@ module.exports = class NumericHistogram extends BaseHistogram {
return callback(null, histogramSql);
}
/**
* ctx: Object with the following values
* ctx.column -- Column for the histogram
* ctx.isFloatColumn - Whether the column is float or not
* ctx.query -- Subquery to extract data
* ctx.start -- Start value for the bins. [>= end to force calculation]
* ctx.end -- End value for the bins.
* ctx.bins -- Numbers of bins to generate [<0 to force calculation]
* ctx.minBins - If !full min bins to calculate [Optional]
* ctx.maxBins - If !full max bins to calculate [Optional]
*/
_buildQueryTpl (ctx) {
var extra_tables = ``;
var extra_queries = ``;
var extra_groupby = ``;
if (ctx.start >= ctx.end) {
ctx.end = `__cdb_basics.__cdb_max_val`;
ctx.start = `__cdb_basics.__cdb_min_val`;
extra_groupby = `, __cdb_basics.__cdb_max_val, __cdb_basics.__cdb_min_val`;
extra_tables = `, __cdb_basics`;
extra_queries = `WITH ${irqQueryTpl(ctx)}`;
}
if (ctx.bins <= 0) {
ctx.bins = `__cdb_bins.__cdb_bins_number`;
extra_groupby += `, __cdb_bins.__cdb_bins_number`;
extra_tables += `, __cdb_bins`;
extra_queries = `WITH ${irqQueryTpl(ctx)}, ${binsQueryTpl(ctx)}`;
}
return `
WITH
${filteredQueryTpl(ctx)},
${this._hasOverridenRange(ctx.override) ? overrideBasicsQueryTpl(ctx) : basicsQueryTpl(ctx)},
${this._hasOverridenBins(ctx.override) ?
overrideBinsQueryTpl(ctx) :
`${iqrQueryTpl(ctx)}, ${binsQueryTpl(ctx)}`
},
${nullsQueryTpl(ctx)}
${ctx.isFloatColumn ? `,${infinitiesQueryTpl(ctx)}, ${nansQueryTpl(ctx)}` : ''}
${histogramQueryTpl(ctx)}
`;
${extra_queries}
SELECT
(${ctx.end} - ${ctx.start}) / ${ctx.bins}::float AS bin_width,
${ctx.bins} as bins_number,
${utils.countNULLs(ctx)} AS nulls_count,
${utils.countInfinites(ctx)} AS infinities_count,
${utils.countNaNs(ctx)} AS nans_count,
min(${utils.handleFloatColumn(ctx)}) AS min,
max(${utils.handleFloatColumn(ctx)}) AS max,
avg(${utils.handleFloatColumn(ctx)}) AS avg,
sum(CASE WHEN (${utils.handleFloatColumn(ctx)} is not NULL) THEN 1 ELSE 0 END) as freq,
CASE WHEN ${ctx.start} = ${ctx.end}
THEN 0
ELSE GREATEST(1, LEAST(
${ctx.bins},
WIDTH_BUCKET(${utils.handleFloatColumn(ctx)}, ${ctx.start}, ${ctx.end}, ${ctx.bins}))) - 1
END AS bin
FROM
(
${ctx.query}
) __cdb_filtered_source_query${extra_tables}
GROUP BY 10${extra_groupby}
ORDER BY 10;`;
}
_hasOverridenBins (override) {
@@ -204,14 +148,31 @@ module.exports = class NumericHistogram extends BaseHistogram {
_getSummary (result, override) {
const firstRow = result.rows[0] || {};
var total_nulls = 0;
var total_infinities = 0;
var total_nans = 0;
var total_avg = 0;
var total_count = 0;
result.rows.forEach(function(row) {
total_nulls += row.nulls_count;
total_infinities += row.infinities_count;
total_nans += row.nans_count;
total_avg += row.avg * row.freq;
total_count += row.freq;
});
if (total_count !== 0) {
total_avg /= total_count;
}
return {
bin_width: firstRow.bin_width,
bins_count: firstRow.bins_number,
bins_start: this._populateBinStart(firstRow, override),
nulls: firstRow.nulls_count,
infinities: firstRow.infinities_count,
nans: firstRow.nans_count,
avg: firstRow.avg_val,
nulls: total_nulls,
infinities: total_infinities,
nans: total_nans,
avg: total_avg
};
}

View File

@@ -1,55 +1,38 @@
var BaseOverviewsDataview = require('./base');
var BaseDataview = require('../formula');
var debug = require('debug')('windshaft:widget:formula:overview');
const utils = require('../../../utils/query-utils');
var dot = require('dot');
dot.templateSettings.strip = false;
var formulaQueryTpls = {
'count': dot.template([
'SELECT',
'sum(_feature_count) AS result,',
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
'{{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_infinities',
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count,',
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nans',
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
'FROM ({{=it._query}}) _cdb_formula'
].join('\n')),
'sum': dot.template([
'SELECT',
'sum({{=it._column}}*_feature_count) AS result,',
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
'{{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_infinities',
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count',
',(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nans',
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
'FROM ({{=it._query}}) _cdb_formula',
'{{?it._isFloatColumn}}WHERE',
' {{=it._column}} != \'infinity\'::float',
'AND',
' {{=it._column}} != \'-infinity\'::float',
'AND',
' {{=it._column}} != \'NaN\'::float{{?}}'
].join('\n')),
'avg': dot.template([
'SELECT',
'sum({{=it._column}}*_feature_count)/sum(_feature_count) AS result,',
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
'{{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_infinities',
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count',
',(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nans',
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
'FROM ({{=it._query}}) _cdb_formula',
'{{?it._isFloatColumn}}WHERE',
' {{=it._column}} != \'infinity\'::float',
'AND',
' {{=it._column}} != \'-infinity\'::float',
'AND',
' {{=it._column}} != \'NaN\'::float{{?}}'
].join('\n')),
const VALID_OPERATIONS = {
count: true,
sum: true,
avg: true
};
/** Formulae to calculate the end result using _feature_count from the overview table*/
function dataviewResult(ctx) {
switch (ctx.operation) {
case 'count':
return `sum(_feature_count)`;
case 'sum':
return `sum(${utils.handleFloatColumn(ctx)}*_feature_count)`;
case 'avg':
return `sum(${utils.handleFloatColumn(ctx)}*_feature_count)/sum(_feature_count) `;
}
return `${ctx.operation}(${utils.handleFloatColumn(ctx)})`;
}
const formulaQueryTpl = ctx =>
`SELECT
${dataviewResult(ctx)} AS result,
${utils.countNULLs(ctx)} AS nulls_count
${ctx.isFloatColumn ? `,${utils.countInfinites(ctx)} AS infinities_count,` : ``}
${ctx.isFloatColumn ? `${utils.countNaNs(ctx)} AS nans_count` : ``}
FROM (${ctx.query}) __cdb_formula`;
function Formula(query, options, queryRewriter, queryRewriteData, params, queries) {
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params, queries);
this.column = options.column || '1';
@@ -65,36 +48,31 @@ module.exports = Formula;
Formula.prototype.sql = function (psql, override, callback) {
var self = this;
var formulaQueryTpl = formulaQueryTpls[this.operation];
if (formulaQueryTpl) {
// supported formula for use with overviews
if (this._isFloatColumn === null) {
this._isFloatColumn = false;
this.getColumnType(psql, this.column, this.queries.no_filters, function (err, type) {
if (!err && !!type) {
self._isFloatColumn = type.float;
}
self.sql(psql, override, callback);
});
return null;
}
var formulaSql = formulaQueryTpl({
_isFloatColumn: this._isFloatColumn,
_query: this.rewrittenQuery(this.query),
_operation: this.operation,
_column: this.column
});
callback = callback || override;
debug(formulaSql);
return callback(null, formulaSql);
if (!VALID_OPERATIONS[this.operation]) {
return this.defaultSql(psql, override, callback);
}
if (this._isFloatColumn === null) {
this._isFloatColumn = false;
this.getColumnType(psql, this.column, this.queries.no_filters, function (err, type) {
if (!err && !!type) {
self._isFloatColumn = type.float;
}
self.sql(psql, override, callback);
});
return null;
}
// default behaviour
return this.defaultSql(psql, override, callback);
var formulaSql = formulaQueryTpl({
isFloatColumn: this._isFloatColumn,
query: this.rewrittenQuery(this.query),
operation: this.operation,
column: this.column
});
callback = callback || override;
debug(formulaSql);
return callback(null, formulaSql);
};

View File

@@ -0,0 +1,172 @@
const AggregationMapConfig = require('../../aggregation/aggregation-mapconfig');
const queryUtils = require('../../../utils/query-utils');
const unsupportedGeometryTypeErrorMessage = ctx =>
`Unsupported geometry type: ${ctx.geometryType}. ` +
`Aggregation is available only for geometry type: ${AggregationMapConfig.SUPPORTED_GEOMETRY_TYPES}`;
const invalidAggregationParamValueErrorMessage = ctx =>
`Invalid value for 'aggregation' query param: ${ctx.value}. Valid ones are 'true' or 'false'`;
module.exports = class AggregationMapConfigAdapter {
constructor (pgConnection) {
this.pgConnection = pgConnection;
}
getMapConfig (user, requestMapConfig, params, context, callback) {
if (!this._isValidAggregationQueryParam(params)) {
return callback(new Error(invalidAggregationParamValueErrorMessage({ value: params.aggregation })));
}
let mapConfig;
try {
mapConfig = new AggregationMapConfig(user, requestMapConfig, this.pgConnection);
} catch (err) {
return callback(err);
}
if (!this._shouldAdapt(mapConfig, params)) {
return callback(null, requestMapConfig);
}
this.pgConnection.getConnection(user, (err, connection) => {
if (err) {
return callback(err);
}
this._adaptLayers(connection, mapConfig, requestMapConfig, context, callback);
});
}
_isValidAggregationQueryParam (params) {
const { aggregation } = params;
return aggregation === undefined || aggregation === 'true' || aggregation === 'false';
}
_shouldAdapt (mapConfig, params) {
const { aggregation } = params;
if (aggregation === 'false') {
return false;
}
if (aggregation === 'true' || mapConfig.isAggregationMapConfig()) {
return true;
}
return false;
}
_adaptLayers (connection, mapConfig, requestMapConfig, context, callback) {
const adaptLayerPromises = requestMapConfig.layers.map((layer, index) => {
return this._adaptLayer(connection, mapConfig, layer, index);
});
Promise.all(adaptLayerPromises)
.then(results => {
context.aggregation = {
layers: []
};
results.forEach(({ layer, index, adapted }) => {
if (adapted) {
requestMapConfig.layers[index] = layer;
}
const aggregatedFormats = this._getAggregationMetadata(mapConfig, layer, adapted);
context.aggregation.layers.push(aggregatedFormats);
});
callback(null, requestMapConfig);
})
.catch(err => callback(err));
}
_adaptLayer (connection, mapConfig, layer, index) {
return new Promise((resolve, reject) => {
this._shouldAdaptLayer(connection, mapConfig, layer, index, (err, shouldAdapt) => {
if (err) {
return reject(err);
}
if (!shouldAdapt) {
return resolve({ layer, index, adapted: shouldAdapt });
}
const sqlQueryWrap = layer.options.sql_wrap;
let aggregationSql = mapConfig.getAggregatedQuery(index);
if (sqlQueryWrap) {
aggregationSql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, aggregationSql);
}
layer.options.sql = aggregationSql;
mapConfig.getLayerAggregationColumns(index, (err, columns) => {
if (err) {
return reject(err);
}
layer.options.columns = columns;
return resolve({ layer, index, adapted: shouldAdapt });
});
});
});
}
_shouldAdaptLayer (connection, mapConfig, layer, index, callback) {
if (!mapConfig.isAggregationLayer(index)) {
return callback(null, false);
}
const aggregationMetadata = queryUtils.getAggregationMetadata({
query: layer.options.sql_raw ? layer.options.sql_raw : layer.options.sql,
geometryColumn: AggregationMapConfig.getAggregationGeometryColumn()
});
connection.query(aggregationMetadata, (err, res) => {
if (err) {
return callback(null, false);
}
const result = res.rows[0] || {};
if (!mapConfig.isVectorOnlyMapConfig() && !AggregationMapConfig.supportsGeometryType(result.type)) {
const message = unsupportedGeometryTypeErrorMessage({ geometryType: result.type });
const error = new Error(message);
error.type = 'layer';
error.layer = {
id: mapConfig.getLayerId(index),
index: index,
type: mapConfig.layerType(index)
};
return callback(error);
}
if (mapConfig.isVectorOnlyMapConfig() && !AggregationMapConfig.supportsGeometryType(result.type)) {
return callback(null, false);
}
if (!mapConfig.doesLayerReachThreshold(index, result.count)) {
return callback(null, false);
}
callback(null, true);
});
}
_getAggregationMetadata (mapConfig, layer, adapted) {
if (!adapted) {
return { png: false, mvt: false };
}
if (mapConfig.isVectorOnlyMapConfig()) {
return { png: false, mvt: true };
}
return { png: true, mvt: true };
}
};

View File

@@ -58,6 +58,13 @@ AnalysisMapConfigAdapter.prototype.getMapConfig = function(user, requestMapConfi
requestMapConfig = appendFiltersToNodes(requestMapConfig, dataviewsFiltersBySourceId);
// Expected format for analyses filters
// filters = {analyses: {
// a1: [{min, max}, {accept, reject}],
// b1: [{range, column, min, max}, {category, column, accept, reject}]
// }}
requestMapConfig = appendFiltersToNodes(requestMapConfig, filters.analyses);
function createAnalysis(analysisDefinition, done) {
self.analysisBackend.create(analysisConfiguration, analysisDefinition, function (err, analysis) {
if (err) {
@@ -200,6 +207,7 @@ function dataviewQuery(node, dataviewName, ownFilter) {
function appendFiltersToNodes(requestMapConfig, dataviewsFiltersBySourceId) {
var analyses = requestMapConfig.analyses || [];
dataviewsFiltersBySourceId = dataviewsFiltersBySourceId || {};
requestMapConfig.analyses = analyses.map(function(analysisDefinition) {
var analysisGraph = new camshaft.reference.AnalysisGraph(analysisDefinition);

View File

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

View File

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

View File

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

View File

@@ -41,6 +41,7 @@ var AnalysisMapConfigAdapter = require('./models/mapconfig/adapter/analysis-mapc
var MapConfigOverviewsAdapter = require('./models/mapconfig/adapter/mapconfig-overviews-adapter');
var TurboCartoAdapter = require('./models/mapconfig/adapter/turbo-carto-adapter');
var DataviewsWidgetsAdapter = require('./models/mapconfig/adapter/dataviews-widgets-adapter');
var AggregationMapConfigAdapter = require('./models/mapconfig/adapter/aggregation-mapconfig-adapter');
var MapConfigAdapter = require('./models/mapconfig/adapter');
var StatsBackend = require('./backends/stats');
@@ -190,6 +191,7 @@ module.exports = function(serverOptions) {
new SqlWrapMapConfigAdapter(),
new DataviewsWidgetsAdapter(),
new AnalysisMapConfigAdapter(analysisBackend),
new AggregationMapConfigAdapter(pgConnection),
new MapConfigOverviewsAdapter(overviewsMetadataApi, filterStatsApi),
new TurboCartoAdapter()
);
@@ -375,7 +377,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) {

View File

@@ -15,6 +15,7 @@ var rendererConfig = _.defaults(global.environment.renderer || {}, {
bufferSize: 64,
snapToGrid: false,
clipByBox2d: false,
metrics: false,
limits: {}
},
http: {}

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

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

View File

@@ -21,6 +21,53 @@ module.exports.extractTableNames = function extractTableNames(query) {
].join('');
};
module.exports.getQueryRowCount = function getQueryRowEstimation(query) {
return 'select CDB_EstimateRowCount(\'' + query + '\') as rows';
function getQueryRowEstimation(query) {
return 'select CDB_EstimateRowCount($windshaft$' + query + '$windshaft$) as rows';
}
module.exports.getQueryRowCount = getQueryRowEstimation;
module.exports.getAggregationMetadata = ctx => `
WITH
rowEstimation AS (
${getQueryRowEstimation(ctx.query)}
),
geometryType AS (
SELECT ST_GeometryType(${ctx.geometryColumn}) as geom_type
FROM (${ctx.query}) AS __cdb_query WHERE ${ctx.geometryColumn} IS NOT NULL LIMIT 1
)
SELECT
rows AS count,
geom_type AS type
FROM rowEstimation, geometryType;
`;
/** Cast the column to epoch */
module.exports.columnCastTpl = function columnCastTpl(ctx) {
return `date_part('epoch', ${ctx.column})`;
};
/** If the column type is float, ignore any non numeric result (infinity / NaN) */
module.exports.handleFloatColumn = function handleFloatColumn(ctx) {
return `${!ctx.isFloatColumn ? `${ctx.column}` :
`nullif(nullif(nullif(${ctx.column}, 'infinity'::float), '-infinity'::float), 'NaN'::float)`
}`;
};
/** Count NULL appearances */
module.exports.countNULLs= function countNULLs(ctx) {
return `sum(CASE WHEN (${ctx.column} IS NULL) THEN 1 ELSE 0 END)`;
};
/** Count only infinity (positive and negative) appearances */
module.exports.countInfinites = function countInfinites(ctx) {
return `${!ctx.isFloatColumn ? `0` :
`sum(CASE WHEN (${ctx.column} = 'infinity'::float OR ${ctx.column} = '-infinity'::float) THEN 1 ELSE 0 END)`
}`;
};
/** Count only NaNs appearances*/
module.exports.countNaNs = function countNaNs(ctx) {
return `${!ctx.isFloatColumn ? `0` :
`sum(CASE WHEN (${ctx.column} = 'NaN'::float) THEN 1 ELSE 0 END)`
}`;
};

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "windshaft-cartodb",
"version": "4.2.0",
"version": "5.4.0",
"description": "A map tile server for CartoDB",
"keywords": [
"cartodb"
@@ -19,32 +19,36 @@
"Sandro Santilli <strk@vizzuality.com>",
"Carlos Matallín <matallo@carto.com>",
"Daniel Garcia Aubert <dgaubert@carto.com>",
"Mario de Frutos <mario.defrutos@carto.com>"
"Mario de Frutos <mario.defrutos@carto.com>",
"Ivan Malagon <ivan@carto.com>",
"Simon Martin <simon@carto.com>"
],
"dependencies": {
"basic-auth": "^2.0.0",
"body-parser": "^1.18.2",
"camshaft": "0.59.4",
"camshaft": "0.61.4",
"cartodb-psql": "0.10.2",
"cartodb-query-tables": "0.3.0",
"cartodb-redis": "0.14.0",
"cartodb-redis": "0.16.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.1.0",
"windshaft": "4.6.0",
"yargs": "~5.0.0"
},
"devDependencies": {
@@ -53,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": {
@@ -63,9 +67,8 @@
"update-internal-deps": "rm -rf node_modules && rm -f yarn.lock && yarn",
"docker-install": "sudo apt install docker.io && sudo usermod -aG docker $(whoami)",
"docker-pull": "docker pull cartoimages/windshaft-testing",
"docker-test": "docker run -v `pwd`:/srv cartoimages/windshaft-testing bash docker-test.sh",
"docker-bash": "docker run -it -v `pwd`:/srv cartoimages/windshaft-testing bash",
"docker-publish": "docker push cartoimages/windshaft-carto-testing"
"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"
},
"engines": {
"node": ">=6.9",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,150 @@
require('../../support/test_helper');
const assert = require('../../support/assert');
const TestClient = require('../../support/test-client');
describe('analysis-filters-params', () => {
const CARTOCSS = `#layer {
marker-fill-opacity: 1;
marker-line-color: white;
marker-line-width: 0.5;
marker-line-opacity: 1;
marker-placement: point;
marker-type: ellipse;
marker-width: 8;
marker-fill: red;
marker-allow-overlap: true;
}`;
const mapConfig = {
version: '1.6.0',
layers: [
{
"type": "cartodb",
"options": {
"source": {
"id": "a1"
},
"cartocss": CARTOCSS,
"cartocss_version": "2.3.0"
}
}
],
dataviews: {
pop_max_histogram: {
source: {
id: 'a1'
},
type: 'histogram',
options: {
column: 'pop_max'
}
},
pop_min_histogram: {
source: {
id: 'a1'
},
type: 'histogram',
options: {
column: 'pop_min'
}
}
},
analyses: [
{
"id": "a1",
"type": "source",
"params": {
"query": "select * from populated_places_simple_reduced"
}
}
]
};
var params = {
filters: {
dataviews: {
pop_max_histogram: {
min: 2e6
},
pop_min_histogram: {
max: 2e6
}
}
}
};
it('should get a filtered histogram dataview with all filters', function(done) {
const testClient = new TestClient(mapConfig, 1234);
const testParams = Object.assign({}, params, {
own_filter: 1
});
testClient.getDataview('pop_max_histogram', testParams, (err, dataview) => {
assert.ok(!err, err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.bins_count, 6);
testClient.drain(done);
});
});
it('should get a filtered histogram dataview with all filters except my own filter', function(done) {
const testClient = new TestClient(mapConfig, 1234);
const testParams = Object.assign({}, params, {
own_filter: 0
});
testClient.getDataview('pop_max_histogram', testParams, (err, dataview) => {
assert.ok(!err, err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.bins_count, 24);
testClient.drain(done);
});
});
it('should get a filtered histogram dataview without filters', function(done) {
const testClient = new TestClient(mapConfig, 1234);
const testParams = Object.assign({}, params, {
no_filters: 1
});
testClient.getDataview('pop_max_histogram', testParams, (err, dataview) => {
assert.ok(!err, err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.bins_count, 48);
testClient.drain(done);
});
});
it('should return an error if both no_filters and own_filter params are present', function (done) {
const testClient = new TestClient(mapConfig, 1234);
const expectedError = {
errors: ['Both own_filter and no_filters cannot be sent in the same request'],
errors_with_context: [{
type: 'dataview',
message: 'Both own_filter and no_filters cannot be sent in the same request'
}]
};
const testParams = Object.assign({}, params, {
no_filters: 1,
own_filter: 0,
response: {
status: 400
}
});
testClient.getDataview('pop_max_histogram', testParams, (err, dataview) => {
assert.deepEqual(dataview, expectedError);
testClient.drain(done);
});
});
});

View File

@@ -0,0 +1,84 @@
require('../../support/test_helper');
const assert = require('../../support/assert');
const TestClient = require('../../support/test-client');
describe('analysis-layers-dataviews', () => {
const CARTOCSS = `#layer {
marker-fill-opacity: 1;
marker-line-color: white;
marker-line-width: 0.5;
marker-line-opacity: 1;
marker-placement: point;
marker-type: ellipse;
marker-width: 8;
marker-fill: red;
marker-allow-overlap: true;
}`;
const mapConfig = {
version: '1.6.0',
layers: [
{
"type": "cartodb",
"options": {
"source": {
"id": "a1"
},
"cartocss": CARTOCSS,
"cartocss_version": "2.3.0"
}
}
],
dataviews: {
pop_max_histogram: {
source: {
id: 'a1'
},
type: 'histogram',
options: {
column: 'pop_max'
}
}
},
analyses: [
{
"id": "a1",
"type": "source",
"params": {
"query": "select * from populated_places_simple_reduced"
}
}
]
};
it('should get a filtered histogram dataview', function(done) {
const testClient = new TestClient(mapConfig, 1234);
const params = {
filters: {
analyses: {
'a1': [
{
type: 'range',
column: 'pop_max',
params: {
min: 2e6
}
}
]
}
}
};
testClient.getDataview('pop_max_histogram', params, (err, dataview) => {
assert.ok(!err, err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.bins_start, 2008000);
testClient.drain(done);
});
});
});

View File

@@ -109,7 +109,8 @@ describe('analysis-layers-dataviews', function() {
min: 2e6
}
}
}
},
own_filter: 1
};
testClient.getDataview('pop_max_histogram', params, function(err, dataview) {

View File

@@ -174,7 +174,9 @@ describe('analysis-layers', function() {
}
};
testClient.getLayergroup(PERMISSION_DENIED_RESPONSE, function(err, layergroupResult) {
testClient.getLayergroup({ response: PERMISSION_DENIED_RESPONSE }, function(err, layergroupResult) {
assert.ok(!err, err);
assert.deepEqual(
layergroupResult.errors,

View File

@@ -62,7 +62,7 @@ describe('analysis-layers error cases', function() {
var testClient = new TestClient(mapConfig, 1234);
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
assert.ok(!err, err);
assert.equal(layergroupResult.errors.length, 1);
@@ -97,7 +97,7 @@ describe('analysis-layers error cases', function() {
var testClient = new TestClient(mapConfig, 1234);
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
assert.ok(!err, err);
assert.equal(layergroupResult.errors.length, 1);
@@ -144,7 +144,7 @@ describe('analysis-layers error cases', function() {
var testClient = new TestClient(mapConfig, 1234);
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
assert.ok(!err, err);
assert.equal(layergroupResult.errors.length, 1);
@@ -188,9 +188,9 @@ 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(AUTH_ERROR_RESPONSE, function(err, layergroupResult) {
testClient.getLayergroup({ response: AUTH_ERROR_RESPONSE }, function(err, layergroupResult) {
assert.ok(!err, err);
assert.equal(layergroupResult.errors.length, 1);
@@ -246,7 +246,7 @@ describe('analysis-layers error cases', function() {
var testClient = new TestClient(mapConfig, 1234);
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
assert.ok(!err, err);
assert.equal(layergroupResult.errors.length, 1);
@@ -298,7 +298,7 @@ describe('analysis-layers error cases', function() {
var testClient = new TestClient(mapConfig, 1234);
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
assert.ok(!err, err);
assert.equal(layergroupResult.errors.length, 1);
@@ -351,7 +351,7 @@ describe('analysis-layers error cases', function() {
var testClient = new TestClient(mapConfig, 1234);
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
assert.ok(!err, err);
assert.equal(layergroupResult.errors.length, 1);
@@ -415,7 +415,7 @@ describe('analysis-layers error cases', function() {
var testClient = new TestClient(mapConfig, 1234);
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
assert.ok(!err, err);
assert.equal(layergroupResult.errors.length, 1);

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

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

View File

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

View File

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

View File

@@ -51,7 +51,7 @@ describe('histogram-dataview', function() {
it('should fail when invalid dataviews object is provided, string case', function(done) {
var mapConfig = createMapConfig("wadus-string");
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, errObj) {
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, errObj) {
assert.ok(!err, err);
assert.deepEqual(errObj.errors, [ '"dataviews" must be a valid JSON object: "string" type found' ]);
@@ -63,7 +63,7 @@ describe('histogram-dataview', function() {
it('should fail when invalid dataviews object is provided, array case', function(done) {
var mapConfig = createMapConfig([]);
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, errObj) {
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, errObj) {
assert.ok(!err, err);
assert.deepEqual(errObj.errors, [ '"dataviews" must be a valid JSON object: "array" type found' ]);

View File

@@ -124,7 +124,7 @@ describe('histogram-dataview for date column type', function() {
"type": "cartodb",
"options": {
"source": {
"id": "datetime-histogram-source"
"id": "datetime-histogram-source-week"
},
"cartocss": "#points { marker-width: 10; marker-fill: red; }",
"cartocss_version": "2.3.0"
@@ -134,7 +134,7 @@ describe('histogram-dataview for date column type', function() {
{
datetime_histogram: {
source: {
id: 'datetime-histogram-source'
id: 'datetime-histogram-source-week'
},
type: 'histogram',
options: {
@@ -154,9 +154,109 @@ describe('histogram-dataview for date column type', function() {
offset: -14400 // EDT Eastern Daylight Time (GMT-4) in seconds
}
},
datetime_histogram_automatic: {
datetime_histogram_automatic_second: {
source: {
id: 'datetime-histogram-source'
id: 'datetime-histogram-source-second'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_minute: {
source: {
id: 'datetime-histogram-source-minute'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_hour: {
source: {
id: 'datetime-histogram-source-hour'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_day: {
source: {
id: 'datetime-histogram-source-day'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_week: {
source: {
id: 'datetime-histogram-source-week'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_month: {
source: {
id: 'datetime-histogram-source-month'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_quarter: {
source: {
id: 'datetime-histogram-source-quarter'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_year: {
source: {
id: 'datetime-histogram-source-year'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_decade: {
source: {
id: 'datetime-histogram-source-decade'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_century: {
source: {
id: 'datetime-histogram-source-century'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_millennium: {
source: {
id: 'datetime-histogram-source-millennium'
},
type: 'histogram',
options: {
@@ -186,7 +286,7 @@ describe('histogram-dataview for date column type', function() {
},
minute_histogram: {
source: {
id: 'minute-histogram-source'
id: 'minute-histogram-source-tz'
},
type: 'histogram',
options: {
@@ -197,13 +297,144 @@ describe('histogram-dataview for date column type', function() {
},
[
{
"id": "datetime-histogram-source",
"id": "datetime-histogram-source-second",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'2007-02-15 01:00:00'::timestamp, '2008-04-09 01:00:00'::timestamp, '1 day'::interval",
"'2007-02-15 01:00:00'::timestamp, '2007-02-15 01:00:57'::timestamp,",
"'0.9 second'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-minute",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'2007-02-15 01:00:00'::timestamp, '2007-02-15 02:00:57'::timestamp,",
"'75 second'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-hour",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'2007-02-15 01:00:00'::timestamp, '2007-02-18 02:00:57'::timestamp,",
"'47 minutes'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-day",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'2007-02-15 01:00:00'::timestamp, '2007-04-18 02:00:57'::timestamp,",
"'24 hours'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-week",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'2007-02-15 01:00:00'::timestamp, '2008-04-09 01:00:00'::timestamp,",
"'1 day'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-month",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'2007-02-15 01:00:00'::timestamp, '2010-04-09 01:00:00'::timestamp,",
"'30 day'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-quarter",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'2007-02-15 01:00:00'::timestamp, '2020-04-09 01:00:00'::timestamp,",
"'30 day'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-year",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'1990-02-15 01:00:00'::timestamp, '2018-04-09 01:00:00'::timestamp,",
"'30 day'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-decade",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'1850-02-15 01:00:00'::timestamp, '2018-04-09 01:00:00'::timestamp,",
"'30 day'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-century",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'0650-02-15 01:00:00'::timestamp, '1918-04-09 01:00:00'::timestamp,",
"'6 years'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-millennium",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'0005-02-15 01:00:00'::timestamp, '12000-04-09 01:00:00'::timestamp,",
"'72 years'::interval",
") date"
].join(' ')
}
@@ -214,8 +445,8 @@ describe('histogram-dataview for date column type', function() {
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'2007-02-15 01:00:00'::timestamptz, '2008-04-09 01:00:00'::timestamptz, '1 day'::interval",
"from generate_series('2007-02-15 01:00:00+00'::timestamptz,",
"'2008-04-09 01:00:00+00'::timestamptz, '1 day'::interval",
") date"
].join(' ')
}
@@ -233,13 +464,13 @@ describe('histogram-dataview for date column type', function() {
}
},
{
"id": "minute-histogram-source",
"id": "minute-histogram-source-tz",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'2007-02-15 23:50:00'::timestamp, '2007-02-16 00:10:00'::timestamp, '1 minute'::interval",
"from generate_series('2007-02-15 23:50:00+00'::timestamptz,",
"'2007-02-16 00:10:00+00'::timestamptz, '1 minute'::interval",
") date"
].join(' ')
}
@@ -256,6 +487,7 @@ describe('histogram-dataview for date column type', function() {
}];
dateHistogramsUseCases.forEach(function (test) {
it('should create a date histogram aggregated in months (EDT) ' + test.desc, function (done) {
var OFFSET_EDT_IN_MINUTES = -4 * 60; // EDT Eastern Daylight Time (GMT-4) in minutes
@@ -323,7 +555,7 @@ describe('histogram-dataview for date column type', function() {
assert.ok(!err, err);
assert.equal(dataview.type, 'histogram');
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
assert.equal(dataview.bins.length, 6);
assert.equal(dataview.bins_count, 6);
dataview.bins.forEach(function (bin) {
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
});
@@ -335,7 +567,7 @@ describe('histogram-dataview for date column type', function() {
it('should cast overridden start and end to float to avoid out of range errors ' + test.desc, function (done) {
var params = {
start: -2145916800,
end: 1009843199
end: 1193792400
};
this.testClient = new TestClient(mapConfig, 1234);
@@ -348,27 +580,6 @@ describe('histogram-dataview for date column type', function() {
});
});
it('should return same histogram ' + test.desc, function (done) {
var params = {
start: 1171501200, // 2007-02-15 01:00:00 = min(date_colum)
end: 1207702800 // 2008-04-09 01:00:00 = max(date_colum)
};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview(test.dataviewId, {}, function (err, dataview) {
assert.ok(!err, err);
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview(test.dataviewId, params, function (err, filteredDataview) {
assert.ok(!err, err);
assert.deepEqual(dataview, filteredDataview);
done();
});
});
});
it('should aggregate histogram overriding default offset to CEST ' + test.desc, function (done) {
var OFFSET_CEST_IN_SECONDS = 2 * 3600; // Central European Summer Time (Daylight Saving Time)
var OFFSET_CEST_IN_MINUTES = 2 * 60; // Central European Summer Time (Daylight Saving Time)
@@ -449,6 +660,47 @@ describe('histogram-dataview for date column type', function() {
});
});
it('should aggregate histogram using "second" aggregation ' + test.desc, function (done) {
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
var params = {
offset: OFFSET_UTC_IN_SECONDS,
aggregation: 'second'
};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_second', params, function (err, dataview) {
assert.ok(!err, err);
assert.equal(dataview.type, 'histogram');
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
assert.equal(dataview.bins.length, 57);
var initialTimestamp = '2007-02-15T01:00:00Z';
var binsStartInMilliseconds = dataview.bins_start * 1000;
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.format();
assert.equal(binsStartFormatted, initialTimestamp);
dataview.bins.forEach(function (bin, index) {
var binTimestampExpected = moment.utc(initialTimestamp)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.add(index, 'second')
.format();
var binsTimestampInMilliseconds = bin.timestamp * 1000;
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.format();
assert.equal(binTimestampFormatted, binTimestampExpected);
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
});
done();
});
});
it('should aggregate histogram using "quarter" aggregation ' + test.desc, function (done) {
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
@@ -490,6 +742,132 @@ describe('histogram-dataview for date column type', function() {
});
});
it('should aggregate histogram using "decade" aggregation ' + test.desc, function (done) {
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
var params = {
offset: OFFSET_UTC_IN_SECONDS,
aggregation: 'decade'
};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_decade', params, function (err, dataview) {
assert.ok(!err, err);
assert.equal(dataview.type, 'histogram');
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
assert.equal(dataview.bins.length, 17);
var initialTimestamp = '1850-01-01T00:00:00Z';
var binsStartInMilliseconds = dataview.bins_start * 1000;
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.format();
assert.equal(binsStartFormatted, initialTimestamp);
dataview.bins.forEach(function (bin, index) {
var binTimestampExpected = moment.utc(initialTimestamp)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.add(index * 10, 'year')
.format();
var binsTimestampInMilliseconds = bin.timestamp * 1000;
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.format();
assert.equal(binTimestampFormatted, binTimestampExpected);
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
});
done();
});
});
it('should aggregate histogram using "century" aggregation ' + test.desc, function (done) {
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
var params = {
offset: OFFSET_UTC_IN_SECONDS,
aggregation: 'century'
};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_century', params, function (err, dataview) {
assert.ok(!err, err);
assert.equal(dataview.type, 'histogram');
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
assert.equal(dataview.bins.length, 14);
var initialTimestamp = '0601-01-01T00:00:00Z';
var binsStartInMilliseconds = dataview.bins_start * 1000;
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.format();
assert.equal(binsStartFormatted, initialTimestamp);
dataview.bins.forEach(function (bin, index) {
var binTimestampExpected = moment.utc(initialTimestamp)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.add(index * 100, 'year')
.format();
var binsTimestampInMilliseconds = bin.timestamp * 1000;
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.format();
assert.equal(binTimestampFormatted, binTimestampExpected);
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
});
done();
});
});
it('should aggregate histogram using "millennium" aggregation ' + test.desc, function (done) {
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
var params = {
offset: OFFSET_UTC_IN_SECONDS,
aggregation: 'millennium'
};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_millennium', params, function (err, dataview) {
assert.ok(!err, err);
assert.equal(dataview.type, 'histogram');
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
assert.equal(dataview.bins.length, 12);
var initialTimestamp = '0001-01-01T00:00:00Z';
var binsStartInMilliseconds = dataview.bins_start * 1000;
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.format();
assert.equal(binsStartFormatted, initialTimestamp);
dataview.bins.forEach(function (bin, index) {
var binTimestampExpected = moment.utc(initialTimestamp)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.add(index * 1000, 'year')
.format();
var binsTimestampInMilliseconds = bin.timestamp * 1000;
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.format();
assert.equal(binTimestampFormatted, binTimestampExpected);
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
});
done();
});
});
it('bins_count should be equal to bins length filtered by start and end ' + test.desc, function (done) {
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
var params = {
@@ -533,19 +911,149 @@ describe('histogram-dataview for date column type', function() {
});
});
it('should find the best aggregation (automatic mode) to build the histogram', function (done) {
it('should return same histogram ', function (done) {
var params = {
start: 1171501200, // 2007-02-15 01:00:00 = min(date_colum)
end: 1207702800 // 2008-04-09 01:00:00 = max(date_colum)
};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_tz', {}, function (err, dataview) {
assert.ok(!err, err);
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_tz', params, function (err, filteredDataview) {
assert.ok(!err, err);
assert.deepEqual(dataview, filteredDataview);
done();
});
});
});
it('should find the best aggregation (automatic mode) to build the histogram: second', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic', params, function (err, dataview) {
this.testClient.getDataview('datetime_histogram_automatic_second', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'week');
assert.equal(dataview.bins.length, 61);
assert.equal(dataview.bins_count, 61);
assert.equal(dataview.aggregation, 'second');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: minute', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_minute', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'minute');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: hour', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_hour', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'hour');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: day', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_day', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'day');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: week', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_week', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'week');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: month', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_month', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'month');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: quarter', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_quarter', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'quarter');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: year', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_year', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'year');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: decade', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_decade', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'decade');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: century', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_century', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'century');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: millennium', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_millennium', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'millennium');
done();
});
});
it('should work with dates', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
@@ -560,19 +1068,6 @@ describe('histogram-dataview for date column type', function() {
});
it('should find the best aggregation (automatic mode) to build the histogram with dates', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('date_histogram_automatic', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'week');
assert.equal(dataview.bins.length, 61);
assert.equal(dataview.bins_count, 61);
done();
});
});
it('should not apply offset for a histogram aggregated by minutes', function (done) {
var self = this;
var params = {
@@ -640,7 +1135,7 @@ describe('histogram-dataview for date column type', function() {
var dataviewWithDailyAggFixture = {
aggregation: 'day',
bin_width: 600,
bin_width: 86400,
bins_count: 2,
bins_start: 1171497600,
timestamp_start: 1171497600,
@@ -650,17 +1145,17 @@ describe('histogram-dataview for date column type', function() {
[{
bin: 0,
timestamp: 1171497600,
min: 1171583400,
max: 1171583940,
avg: 1171583670,
min: 1171497600,
max: 1171497600,
avg: 1171497600,
freq: 10
},
{
bin: 1,
timestamp: 1171584000,
min: 1171584000,
max: 1171584600,
avg: 1171584300,
max: 1171584000,
avg: 1171584000,
freq: 11
}],
type: 'histogram'
@@ -687,19 +1182,19 @@ describe('histogram-dataview for date column type', function() {
var dataviewWithDailyAggAndOffsetFixture = {
aggregation: 'day',
bin_width: 1200,
bin_width: 86400,
bins_count: 1,
bins_start: 1171501200,
timestamp_start: 1171497600,
timestamp_start: 1171501200,
nulls: 0,
offset: -3600,
bins:
[{
bin: 0,
timestamp: 1171501200,
min: 1171583400,
max: 1171584600,
avg: 1171584000,
min: 1171501200,
max: 1171501200,
avg: 1171501200,
freq: 21
}],
type: 'histogram'
@@ -858,13 +1353,15 @@ describe('histogram-dates: aggregation input value', function() {
assert.deepEqual(dataviewError, {
errors: [
'Invalid aggregation value. Valid ones: auto, minute, hour, day, week, month, quarter, year'
'Invalid aggregation value. Valid ones: auto, second, minute, ' +
'hour, day, week, month, quarter, year, decade, century, millennium'
],
errors_with_context: [{
type: 'unknown',
message: [
'Invalid aggregation value. ',
'Valid ones: auto, minute, hour, day, week, month, quarter, year'
'Valid ones: auto, second, minute, hour, day, week, month, ' +
'quarter, year, decade, century, millennium'
].join('')
}]
});
@@ -887,13 +1384,15 @@ describe('histogram-dates: aggregation input value', function() {
assert.deepEqual(dataviewError, {
errors: [
'Invalid aggregation value. Valid ones: auto, minute, hour, day, week, month, quarter, year'
'Invalid aggregation value. Valid ones: auto, second, minute, ' +
'hour, day, week, month, quarter, year, decade, century, millennium'
],
errors_with_context: [{
type: 'unknown',
message: [
'Invalid aggregation value. ',
'Valid ones: auto, minute, hour, day, week, month, quarter, year'
'Valid ones: auto, second, minute, hour, day, week, month, ' +
'quarter, year, decade, century, millennium'
].join('')
}]
});
@@ -966,7 +1465,7 @@ describe('histogram-dates: timestamp starts at epoch', function() {
const { aggregation, timestamp_start } = dataview;
assert.equal(timestamp_start, 0);
assert.equal(aggregation, 'month');
assert.equal(aggregation, 'quarter');
done();
});

View File

@@ -393,7 +393,8 @@ describe('dataviews using tables with overviews', function() {
var params = {
filters: {
dataviews: { test_histogram: { min: 2 } }
}
},
own_filter: 1
};
var testClient = new TestClient(overviewsMapConfig);
testClient.getDataview('test_histogram', params, function (err, histogram) {
@@ -412,7 +413,8 @@ describe('dataviews using tables with overviews', function() {
var params = {
filters: {
dataviews: { test_histogram: { max: -1 } }
}
},
own_filter: 1
};
var testClient = new TestClient(overviewsMapConfig);
testClient.getDataview('test_histogram', params, function (err, histogram) {
@@ -433,7 +435,8 @@ describe('dataviews using tables with overviews', function() {
var params = {
filters: {
dataviews: { test_histogram_date: { max: -1 } }
}
},
own_filter: 1
};
var testClient = new TestClient(overviewsMapConfig);
testClient.getDataview('test_histogram_date', params, function (err, histogram) {

View File

@@ -0,0 +1,42 @@
const assert = require('../support/assert');
const TestClient = require('../support/test-client');
describe('error middleware', function () {
it('should returns a errors header', function (done) {
const mapConfig = {
version: '1.6.0',
layers: [{
type: 'mapnik',
options: {}
}]
};
const errorHeader = {
mainError: {
statusCode: 400,
message: "Missing cartocss for layer 0 options",
name: "Error",
label: "ANONYMOUS LAYERGROUP",
type: "layer",
},
moreErrors: []
};
this.testClient = new TestClient(mapConfig, 1234);
const params = {
response: {
status: 400,
headers: {
'Content-Type': 'application/json; charset=utf-8',
'X-Tiler-Errors': JSON.stringify(errorHeader)
}
}
};
this.testClient.getLayergroup(params, (err) => {
assert.ifError(err);
done();
});
});
});

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

View File

@@ -0,0 +1,54 @@
require('../support/test_helper');
var TestClient = require('../support/test-client');
describe('layers filters', function() {
const type = 'mapnik';
const sql = 'select * from populated_places_simple_reduced';
const cartocss = `#points {
marker-fill-opacity: 1.0;
marker-line-color: #FFF;
marker-line-width: 0.5;
marker-line-opacity: 1.0;
marker-placement: point;
marker-type: ellipse;
marker-width: 8;
marker-fill: red;
marker-allow-overlap: true;
}`;
const cartocss_version = '3.0.12';
const options = {
sql,
cartocss,
cartocss_version
};
const mapConfig = {
version: '1.6.0',
layers: [
{
type,
id: 'layerA',
options
},
{
type,
id: 'layerB',
options
}
]
};
afterEach(function(done) {
if (this.testClient) {
this.testClient.drain(done);
}
});
['layerA', 'layerB'].forEach(layer => {
it(`should work for individual layer ids: ${layer}`, function (done) {
this.testClient = new TestClient(mapConfig);
this.testClient.getTile(0, 0, 0, { layers: layer }, done);
});
});
});

View File

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

View File

@@ -19,7 +19,8 @@ describe('named maps static view', function() {
var username = 'localhost';
var templateName = 'template_with_view';
var IMAGE_TOLERANCE = 20;
var PNG_IMAGE_TOLERANCE = 20;
var JPG_IMAGE_TOLERANCE = 100;
function createTemplate(view, layers) {
return {
@@ -92,8 +93,8 @@ describe('named maps static view', function() {
});
}
function previewFixture(version) {
return './test/fixtures/previews/populated_places_simple_reduced-' + version + '.png';
function previewFixture(version, format='png') {
return './test/fixtures/previews/populated_places_simple_reduced-' + version + '.' + format;
}
it('should return an image estimating its bounds based on dataset', function (done) {
@@ -103,7 +104,7 @@ describe('named maps static view', function() {
}
getStaticMap(function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('estimated'), IMAGE_TOLERANCE, done);
assert.imageIsSimilarToFile(img, previewFixture('estimated'), PNG_IMAGE_TOLERANCE, done);
});
});
});
@@ -122,7 +123,7 @@ describe('named maps static view', function() {
}
getStaticMap(function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), IMAGE_TOLERANCE, done);
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), PNG_IMAGE_TOLERANCE, done);
});
});
});
@@ -142,7 +143,7 @@ describe('named maps static view', function() {
}
getStaticMap(function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('bounds'), IMAGE_TOLERANCE, done);
assert.imageIsSimilarToFile(img, previewFixture('bounds'), PNG_IMAGE_TOLERANCE, done);
});
});
});
@@ -167,7 +168,7 @@ describe('named maps static view', function() {
}
getStaticMap(function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), IMAGE_TOLERANCE, done);
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), PNG_IMAGE_TOLERANCE, done);
});
});
});
@@ -192,7 +193,7 @@ describe('named maps static view', function() {
}
getStaticMap({ zoom: 3 }, function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('override-zoom'), IMAGE_TOLERANCE, done);
assert.imageIsSimilarToFile(img, previewFixture('override-zoom'), PNG_IMAGE_TOLERANCE, done);
});
});
});
@@ -217,7 +218,7 @@ describe('named maps static view', function() {
}
getStaticMap({ bbox: '0,45,90,45' }, function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('override-bbox'), IMAGE_TOLERANCE, done);
assert.imageIsSimilarToFile(img, previewFixture('override-bbox'), PNG_IMAGE_TOLERANCE, done);
});
});
});
@@ -256,7 +257,27 @@ describe('named maps static view', function() {
}
getStaticMap({ layer: 0 }, function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('bounds'), IMAGE_TOLERANCE, done);
assert.imageIsSimilarToFile(img, previewFixture('bounds'), PNG_IMAGE_TOLERANCE, done);
});
});
});
it('should return jpg static map', function (done) {
var view = {
zoom: 4,
center: {
lng: 40,
lat: 20
}
};
templateMaps.addTemplate(username, createTemplate(view), function (err) {
if (err) {
return done(err);
}
getStaticMap({ format: 'jpeg' }, function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('zoom-center', 'jpeg'),
JPG_IMAGE_TOLERANCE, done, 'jpeg');
});
});
});

View File

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

View File

@@ -151,7 +151,7 @@ describe('multilayer error cases', function() {
};
ServerOptions.afterLayergroupCreateCalls = 0;
this.client = new TestClient(layergroup);
this.client.getLayergroup({status: 400}, function(err, parsed) {
this.client.getLayergroup({ response: { status: 400 } }, function(err, parsed) {
assert.ok(!err, err);
// See http://github.com/CartoDB/Windshaft/issues/159
assert.equal(ServerOptions.afterLayergroupCreateCalls, 0);

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ describe('regressions', function() {
var testClient = new TestClient(mapConfig, 1234);
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroupResult) {
assert.ok(!err, err);
assert.equal(layergroupResult.errors.length, 1);

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

View File

@@ -51,7 +51,7 @@ describe('turbo-carto error cases', function() {
it('should return invalid number of ramp error', function(done) {
this.testClient = new TestClient(makeMapconfig('ramp([pop_max], 8, 96, 3, (8,24,96,128))'));
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
assert.ok(!err, err);
assert.ok(layergroup.hasOwnProperty('errors'));
@@ -65,7 +65,7 @@ describe('turbo-carto error cases', function() {
it('should return invalid column from datasource', function(done) {
this.testClient = new TestClient(makeMapconfig(null, 'ramp([wadus_column], (red, green, blue))'));
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
assert.ok(!err, err);
assert.ok(layergroup.hasOwnProperty('errors'));
@@ -80,7 +80,7 @@ describe('turbo-carto error cases', function() {
it('should return invalid method from datasource', function(done) {
this.testClient = new TestClient(makeMapconfig(null, 'ramp([wadus_column], (red, green, blue), wadusmethod)'));
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
assert.ok(!err, err);
assert.ok(layergroup.hasOwnProperty('errors'));
@@ -95,7 +95,7 @@ describe('turbo-carto error cases', function() {
it('should fail by falling back to normal carto parser', function(done) {
this.testClient = new TestClient(makeMapconfig('ramp([price], (8,24,96), (8,24,96));//(red, green, blue))'));
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
assert.ok(!err, err);
assert.ok(layergroup.hasOwnProperty('errors'));
@@ -109,7 +109,7 @@ describe('turbo-carto error cases', function() {
it('turbo-carto: should return error invalid column from datasource with some context', function(done) {
this.testClient = new TestClient(makeMapconfig(null, 'ramp([wadus_column], (red, green, blue))'));
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
assert.ok(!err, err);
assert.ok(layergroup.hasOwnProperty('errors'));
@@ -164,7 +164,7 @@ describe('turbo-carto error cases', function() {
};
this.testClient = new TestClient(multipleErrorsMapConfig);
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
this.testClient.getLayergroup({ response: ERROR_RESPONSE }, function(err, layergroup) {
assert.ok(!err, err);
assert.ok(layergroup.hasOwnProperty('errors'));

View File

@@ -79,7 +79,7 @@ describe('turbo-carto regressions', function() {
].join('\n');
this.testClient = new TestClient(makeMapconfig('SELECT * FROM test_table_private_1', cartocss));
this.testClient.getLayergroup(TestClient.RESPONSE.ERROR, function(err, layergroup) {
this.testClient.getLayergroup({ response: TestClient.RESPONSE.ERROR }, function(err, layergroup) {
assert.ok(!err, err);
assert.ok(!layergroup.hasOwnProperty('layergroupid'));

View File

@@ -138,7 +138,7 @@ describe('user database timeout limit', function () {
}
};
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
assert.deepEqual(timeoutError, {
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
errors_with_context: [{
@@ -177,7 +177,7 @@ describe('user database timeout limit', function () {
return done(err);
}
this.testClient.getLayergroup(expectedResponse, (err, res) => {
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
if (err) {
return done(err);
}
@@ -259,7 +259,7 @@ describe('user database timeout limit', function () {
return done(err);
}
this.testClient.getLayergroup(expectedResponse, (err, res) => {
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
if (err) {
return done(err);
}
@@ -360,7 +360,7 @@ describe('user database timeout limit', function () {
}
};
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
assert.deepEqual(timeoutError, {
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
errors_with_context: [{
@@ -387,7 +387,7 @@ describe('user database timeout limit', function () {
}
};
this.testClient.getLayergroup(expectedResponse, (err, res) => {
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
if (err) {
return done(err);
}
@@ -426,16 +426,16 @@ describe('user database timeout limit', function () {
this.testClient.getTile(0, 0, 0, params, (err, res, tile) => {
assert.ifError(err);
var tileJSON = tile.toJSON();
assert.equal(Array.isArray(tileJSON), true);
assert.equal(tileJSON.length, 2);
assert.equal(tileJSON[0].name, 'errorTileSquareLayer');
assert.equal(tileJSON[1].name, 'errorTileStripesLayer');
done();
});
});
});
});
@@ -467,7 +467,7 @@ describe('user database timeout limit', function () {
}
};
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
assert.deepEqual(timeoutError, {
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
errors_with_context: [{
@@ -494,7 +494,7 @@ describe('user database timeout limit', function () {
}
};
this.testClient.getLayergroup(expectedResponse, (err, res) => {
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
if (err) {
return done(err);
}
@@ -571,7 +571,7 @@ describe('user database timeout limit', function () {
}
};
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
assert.deepEqual(timeoutError, {
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
errors_with_context: [{
@@ -601,7 +601,7 @@ describe('user database timeout limit', function () {
}
};
this.testClient.getLayergroup(expectedResponse, (err, res) => {
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
if (err) {
return done(err);
}
@@ -702,7 +702,7 @@ describe('user database timeout limit', function () {
}
};
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
assert.deepEqual(timeoutError, {
errors: [ 'You are over platform\'s limits. Please contact us to know more details' ],
errors_with_context: [{
@@ -740,7 +740,7 @@ describe('user database timeout limit', function () {
}
};
this.testClient.getLayergroup(expectedResponse, (err, res) => {
this.testClient.getLayergroup({ response: expectedResponse }, (err, res) => {
if (err) {
return done(err);
}

View File

@@ -92,7 +92,7 @@ describe('user render timeout limit', function () {
}
};
this.testClient.getLayergroup(expectedResponse, (err, timeoutError) => {
this.testClient.getLayergroup({ response: expectedResponse }, (err, timeoutError) => {
assert.ifError(err);
assert.deepEqual(timeoutError, {
@@ -245,7 +245,7 @@ describe('user render timeout limit', function () {
assert.equal(tileJSON.length, 2);
assert.equal(tileJSON[0].name, 'errorTileSquareLayer');
assert.equal(tileJSON[1].name, 'errorTileStripesLayer');
done();
});
});
@@ -399,4 +399,3 @@ describe('user render timeout limit', function () {
});
});
});

View File

@@ -206,7 +206,7 @@ suites.forEach((suite) => {
this.testClient.mapConfig.layers[0].options.cartocss = cartocss;
this.testClient.mapConfig.layers[0].options.cartocss_version = cartocssVersion;
this.testClient.getLayergroup(response, (err, body) => {
this.testClient.getLayergroup({ response }, (err, body) => {
if (err) {
return done(err);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

3
test/results/jpeg/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*.jpeg
*.jpg
*.js

View File

@@ -37,7 +37,7 @@ assert.imageBuffersAreSimilar = function(bufferA, bufferB, tolerance, callback)
imagesAreSimilar(testImage, referenceImage, tolerance, callback);
};
assert.imageIsSimilarToFile = function(testImage, referenceImageRelativeFilePath, tolerance, callback) {
assert.imageIsSimilarToFile = function(testImage, referenceImageRelativeFilePath, tolerance, callback, format='png') {
callback = callback || function(err) { assert.ifError(err); };
var referenceImageFilePath = path.resolve(referenceImageRelativeFilePath);
@@ -46,19 +46,23 @@ assert.imageIsSimilarToFile = function(testImage, referenceImageRelativeFilePath
imagesAreSimilar(testImage, referenceImage, tolerance, function(err) {
if (err) {
var testImageFilePath = randomImagePath();
testImage.save(testImageFilePath);
var testImageFilePath = randomImagePath(format);
testImage.save(testImageFilePath, format);
}
callback(err);
});
}, format);
};
function imagesAreSimilar(testImage, referenceImage, tolerance, callback) {
function imagesAreSimilar(testImage, referenceImage, tolerance, callback, format='png') {
if (testImage.width() !== referenceImage.width() || testImage.height() !== referenceImage.height()) {
return callback(new Error('Images are not the same size'));
}
var pixelsDifference = referenceImage.compare(testImage);
var options = {};
if (format === 'jpeg') {
options.alpha = false;
}
var pixelsDifference = referenceImage.compare(testImage, options);
var similarity = pixelsDifference / (referenceImage.width() * referenceImage.height());
var tolerancePerMil = (tolerance / 1000);
@@ -73,8 +77,8 @@ function imagesAreSimilar(testImage, referenceImage, tolerance, callback) {
}
}
function randomImagePath() {
return path.resolve('test/results/png/image-test-' + Date.now() + '.png');
function randomImagePath(format='png') {
return path.resolve('test/results/' + format + '/image-test-' + Date.now() + '.' + format);
}
assert.response = function(server, req, res, callback) {

View File

@@ -77,7 +77,7 @@ if test x"$PREPARE_PGSQL" = xyes; then
createdb -Ttemplate_postgis -EUTF8 "${TEST_DB}" || die "Could not create test database"
LOCAL_SQL_SCRIPTS='analysis_catalog windshaft.test gadm4 ported/populated_places_simple_reduced cdb_analysis_check cdb_invalidate_varnish'
REMOTE_SQL_SCRIPTS='CDB_QueryStatements CDB_QueryTables CDB_CartodbfyTable CDB_TableMetadata CDB_ForeignTable CDB_UserTables CDB_ColumnNames CDB_ZoomFromScale CDB_OverviewsSupport CDB_Overviews CDB_QuantileBins CDB_JenksBins CDB_HeadsTailsBins CDB_EqualIntervalBins CDB_Hexagon CDB_XYZ CDB_EstimateRowCount'
REMOTE_SQL_SCRIPTS='CDB_QueryStatements CDB_QueryTables CDB_CartodbfyTable CDB_TableMetadata CDB_ForeignTable CDB_UserTables CDB_ColumnNames CDB_ZoomFromScale CDB_OverviewsSupport CDB_Overviews CDB_QuantileBins CDB_JenksBins CDB_HeadsTailsBins CDB_EqualIntervalBins CDB_Hexagon CDB_XYZ CDB_EstimateRowCount CDB_RectangleGrid'
CURL_ARGS=""
for i in ${REMOTE_SQL_SCRIPTS}
@@ -99,7 +99,7 @@ if test x"$PREPARE_PGSQL" = xyes; then
sed -e 's/PARALLEL \= [A-Z]*,/''/g' \
-e 's/PARALLEL [A-Z]*/''/g' sql/$i.sql > $TMPFILE
mv $TMPFILE sql/$i.sql
fi
fi
cat sql/${i}.sql |
sed -e 's/cartodb\./public./g' -e "s/''cartodb''/''public''/g" |
sed "s/:PUBLICUSER/${PUBLICUSER}/" |
@@ -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"

View File

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

View File

@@ -411,9 +411,13 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
var urlParams = {
own_filter: params.hasOwnProperty('own_filter') ? params.own_filter : 1
};
var urlParams = {};
if (params.hasOwnProperty('no_filters')) {
urlParams.no_filters = params.no_filters;
}
if (params.hasOwnProperty('own_filter')) {
urlParams.own_filter = params.own_filter;
}
['bbox', 'bins', 'start', 'end', 'aggregation', 'offset', 'categories'].forEach(function(extraParam) {
if (params.hasOwnProperty(extraParam)) {
@@ -618,8 +622,19 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
}
var data = templateId ? params.placeholders : self.mapConfig;
const queryParams = {};
if (self.apiKey) {
queryParams.api_key = self.apiKey;
}
if (params.aggregation !== undefined) {
queryParams.aggregation = params.aggregation;
}
var path = templateId ?
urlNamed + '/' + templateId + '?' + qs.stringify({api_key: self.apiKey}) :
urlNamed + '/' + templateId + '?' + qs.stringify(queryParams) :
url;
assert.response(self.server,
@@ -647,7 +662,7 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
);
},
function getTileResult(err, layergroupId) {
// jshint maxcomplexity:12
// jshint maxcomplexity:13
assert.ifError(err);
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
@@ -671,8 +686,14 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
url += [z,x,y].join('/');
url += '.' + format;
const queryParams = {};
if (self.apiKey) {
url += '?' + qs.stringify({api_key: self.apiKey});
queryParams.api_key = self.apiKey;
}
if (Object.keys(queryParams).length) {
url += '?' + qs.stringify(queryParams);
}
var request = {
@@ -754,12 +775,21 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
);
};
TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
TestClient.prototype.getLayergroup = function (params, callback) {
// jshint maxcomplexity: 7
var self = this;
if (!callback) {
callback = expectedResponse;
expectedResponse = {
callback = params;
params = null;
}
if (!params) {
params = {};
}
if (!params.response) {
params.response = {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
@@ -769,8 +799,18 @@ TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
var url = '/api/v1/map';
if (this.apiKey) {
url += '?' + qs.stringify({api_key: this.apiKey});
const queryParams = {};
if (self.apiKey) {
queryParams.api_key = self.apiKey;
}
if (params.aggregation !== undefined) {
queryParams.aggregation = params.aggregation;
}
if (Object.keys(queryParams).length) {
url += '?' + qs.stringify(queryParams);
}
assert.response(self.server,
@@ -783,7 +823,7 @@ TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
},
data: JSON.stringify(self.mapConfig)
},
expectedResponse,
params.response,
function(res, err) {
var parsedBody;
// If there is a response, we are still interested in catching the created keys
@@ -1213,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);
});
});
};

View File

@@ -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: [] });
@@ -75,6 +77,22 @@ function checkSurrogateKey(res, expectedKey) {
assert.deepEqual(keys, expectedKeys);
}
var uncaughtExceptions = [];
process.on('uncaughtException', function(err) {
uncaughtExceptions.push(err);
});
beforeEach(function() {
uncaughtExceptions = [];
});
//global afterEach to capture uncaught exceptions
afterEach(function() {
assert.equal(
uncaughtExceptions.length,
0,
'uncaughtException:\n\n' + uncaughtExceptions.map(err => err.stack).join('\n\n'));
});
var redisClient;
beforeEach(function() {
@@ -95,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'};

View File

@@ -1,23 +0,0 @@
require('../../support/test_helper.js');
var assert = require('assert');
var errorMiddleware = require('../../../lib/cartodb/middleware/error-middleware');
describe('error-middleware', function() {
it('different formats for postgis plugin error returns 400 as status code', function() {
var expectedStatusCode = 400;
assert.equal(
errorMiddleware.findStatusCode("Postgis Plugin: ERROR: column \"missing\" does not exist\n"),
expectedStatusCode,
"Error status code for single line does not match"
);
assert.equal(
errorMiddleware.findStatusCode("Postgis Plugin: PSQL error:\nERROR: column \"missing\" does not exist\n"),
expectedStatusCode,
"Error status code for multiline/PSQL does not match"
);
});
});

View File

@@ -0,0 +1,178 @@
require('../../support/test_helper.js');
var assert = require('assert');
var errorMiddleware = require('../../../lib/cartodb/middleware/error-middleware');
describe('error-middleware', function() {
it('different formats for postgis plugin error returns 400 as status code', function() {
var expectedStatusCode = 400;
assert.equal(
errorMiddleware.findStatusCode("Postgis Plugin: ERROR: column \"missing\" does not exist\n"),
expectedStatusCode,
"Error status code for single line does not match"
);
assert.equal(
errorMiddleware.findStatusCode("Postgis Plugin: PSQL error:\nERROR: column \"missing\" does not exist\n"),
expectedStatusCode,
"Error status code for multiline/PSQL does not match"
);
});
it('should return a header with errors', function (done) {
const error = new Error('error test');
error.label = 'test label';
error.type = 'test type';
error.subtype = 'test subtype';
const errors = [error, error];
const req = {};
const res = {
headers: {},
set (key, value) {
this.headers[key] = value;
},
statusCode: 0,
status (status) {
this.statusCode = status;
},
json () {},
send () {}
};
const errorHeader = {
mainError: {
statusCode: 400,
message: error.message,
name: error.name,
label: error.label,
type: error.type,
subtype: error.subtype,
},
moreErrors: [{
message: error.message,
name: error.name,
label: error.label,
type: error.type,
subtype: error.subtype
}]
};
const errorFn = errorMiddleware();
errorFn(errors, req, res);
assert.deepEqual(res.headers, {
'X-Tiler-Errors': JSON.stringify(errorHeader)
});
done();
});
it('JSONP should return a header with error status code', function (done) {
const error = new Error('error test');
error.label = 'test label';
error.type = 'test type';
error.subtype = 'test subtype';
const errors = [error, error];
const req = {
query: { callback: true }
};
const res = {
headers: {},
set (key, value) {
this.headers[key] = value;
},
statusCode: 0,
status (status) {
this.statusCode = status;
},
jsonp () {},
send () {}
};
const errorHeader = {
mainError: {
statusCode: 400,
message: error.message,
name: error.name,
label: error.label,
type: error.type,
subtype: error.subtype,
},
moreErrors: [{
message: error.message,
name: error.name,
label: error.label,
type: error.type,
subtype: error.subtype
}]
};
const errorFn = errorMiddleware();
errorFn(errors, req, res);
assert.deepEqual(res.headers, {
'X-Tiler-Errors': JSON.stringify(errorHeader)
});
done();
});
it('should escape chars that broke logs regex', function (done) {
const badString = 'error: ( ) = " \" \' * $ & |';
const escapedString = 'error ';
const error = new Error(badString);
error.label = badString;
error.type = badString;
error.subtype = badString;
const errors = [error, error];
const req = {};
const res = {
headers: {},
set (key, value) {
this.headers[key] = value;
},
statusCode: 0,
status (status) {
this.statusCode = status;
},
json () {},
send () {}
};
const errorHeader = {
mainError: {
statusCode: 400,
message: escapedString,
name: error.name,
label: escapedString,
type: escapedString,
subtype: escapedString,
},
moreErrors: [{
message: escapedString,
name: error.name,
label: escapedString,
type: escapedString,
subtype: escapedString
}]
};
const errorFn = errorMiddleware();
errorFn(errors, req, res);
assert.deepEqual(res.headers, {
'X-Tiler-Errors': JSON.stringify(errorHeader)
});
done();
});
});

View File

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

View File

@@ -3,9 +3,10 @@ require('../../../support/test_helper');
var assert = require('../../../support/assert');
var ResourceLocator = require('../../../../lib/cartodb/models/resource-locator');
describe('ResourceLocator.getUrls', function() {
describe('ResourceLocator', function() {
var USERNAME = 'username';
var RESOURCE = 'wadus';
var TILE_RESOURCE = 'wadus/{z}/{x}/{y}.png';
var HTTP_SUBDOMAINS = ['1', '2', '3', '4'];
var HTTPS_SUBDOMAINS = ['a', 'b', 'c', 'd'];
@@ -15,118 +16,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('/')
);
});
});

View File

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

View File

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

297
yarn.lock
View File

@@ -2,12 +2,33 @@
# 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, abbrev@1.0.x:
@@ -29,13 +50,13 @@ ajv@^4.9.1:
json-stable-stringify "^1.0.1"
ajv@^5.1.0:
version "5.2.3"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.3.tgz#c06f598778c44c6b161abafe3466b81ad1814ed2"
version "5.5.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
dependencies:
co "^4.6.0"
fast-deep-equal "^1.0.0"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.3.0"
json-stable-stringify "^1.0.1"
align-text@^0.1.1, align-text@^0.1.3:
version "0.1.4"
@@ -91,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"
@@ -122,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"
@@ -211,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.59.4:
version "0.59.4"
resolved "https://registry.yarnpkg.com/camshaft/-/camshaft-0.59.4.tgz#7d4d0b8585fa180b5e750206aa84940870ebabfc"
camshaft@0.61.4:
version "0.61.4"
resolved "https://registry.yarnpkg.com/camshaft/-/camshaft-0.61.4.tgz#eeab7e69a07d6fbc39ebc01ad19acf979de8fb2a"
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:
@@ -240,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:
@@ -274,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@0.16.0:
version "0.16.0"
resolved "https://registry.yarnpkg.com/cartodb-redis/-/cartodb-redis-0.16.0.tgz#969312fd329b24a76bf6e5a4dd961445f2eda734"
dependencies:
dot "~1.0.2"
redis-mpool "~0.4.1"
redis-mpool "^0.5.0"
underscore "~1.6.0"
caseless@~0.12.0:
@@ -476,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,6 +555,10 @@ 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"
@@ -541,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,6 +687,10 @@ fast-deep-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
fast-json-stable-stringify@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
fast-levenshtein@~2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
@@ -750,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"
@@ -762,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"
@@ -776,7 +811,7 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
glob@7.1.1, glob@^7.1.1:
glob@7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
dependencies:
@@ -807,7 +842,7 @@ glob@^6.0.1:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.0.5:
glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
@@ -826,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.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/grainstore/-/grainstore-1.7.0.tgz#28d78895c82e6201f7d0ff63af1056f3c0fda0d3"
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"
@@ -845,8 +880,8 @@ growl@1.9.2:
resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f"
handlebars@^4.0.1:
version "4.0.10"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f"
version "4.0.11"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc"
dependencies:
async "^1.4.0"
optimist "^0.6.1"
@@ -998,8 +1033,8 @@ is-arrayish@^0.2.1:
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
is-buffer@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
is-builtin-module@^1.0.0:
version "1.0.0"
@@ -1057,8 +1092,8 @@ istanbul@~0.4.3:
wordwrap "^1.0.0"
js-base64@^2.1.9:
version "2.3.2"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf"
version "2.4.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"
@@ -1216,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"
@@ -1257,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"
@@ -1315,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"
@@ -1379,11 +1405,7 @@ mv@~2:
ncp "~2.0.0"
rimraf "~2.4.0"
nan@^2.0.8, nan@^2.3.4, nan@~2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
nan@^2.4.0:
nan@^2.0.8, nan@^2.3.4, nan@^2.4.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
@@ -1395,6 +1417,10 @@ nan@~2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
nan@~2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
ncp@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
@@ -1473,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"
@@ -1588,6 +1610,10 @@ pg-connection-string@0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7"
pg-int8@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
pg-pool@1.*:
version "1.8.0"
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-1.8.0.tgz#f7ec73824c37a03f076f51bfdf70e340147c4f37"
@@ -1596,9 +1622,10 @@ pg-pool@1.*:
object-assign "4.1.0"
pg-types@1.*:
version "1.12.1"
resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-1.12.1.tgz#d64087e3903b58ffaad279e7595c52208a14c3d2"
version "1.13.0"
resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-1.13.0.tgz#75f490b8a8abf75f1386ef5ec4455ecf6b345c63"
dependencies:
pg-int8 "1.0.1"
postgres-array "~1.0.0"
postgres-bytea "~1.0.0"
postgres-date "~1.0.0"
@@ -1708,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"
@@ -1749,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"
@@ -1772,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:
@@ -1802,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"
@@ -1855,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:
@@ -1920,7 +1977,7 @@ safe-json-stringify@~1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz#81a098f447e4bbc3ff3312a243521bc060ef5911"
"semver@2 || 3 || 4 || 5", semver@~5.3.0:
"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"
@@ -1928,10 +1985,6 @@ semver@4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7"
semver@^5.1.0, semver@^5.3.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
semver@~4.3.3:
version "4.3.6"
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
@@ -1998,8 +2051,8 @@ sntp@1.x.x:
hoek "2.x.x"
sntp@2.x.x:
version "2.0.2"
resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.0.2.tgz#5064110f0af85f7cfdb7d6b67a40028ce52b4b2b"
version "2.1.0"
resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8"
dependencies:
hoek "4.x.x"
@@ -2037,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"
@@ -2190,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"
@@ -2313,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"
@@ -2359,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.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/windshaft/-/windshaft-4.1.0.tgz#dc17c8369570c305171d1ab5ca130369bba04d58"
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.7.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"