Compare commits

..

386 Commits

Author SHA1 Message Date
Daniel García Aubert
1396ca9fe3 Release 4.0.1 2017-10-18 15:17:17 +02:00
Daniel García Aubert
ddcb812218 Update NEWS 2017-10-18 15:14:03 +02:00
Daniel
66c3d58b92 Merge pull request #777 from CartoDB/upgrade-camshaft-0.59.4
Upgrade camshaft to version 0.59.4
2017-10-18 15:08:02 +02:00
Daniel García Aubert
9326217c18 Upgrade camshaft to version 0.59.4 2017-10-18 14:36:04 +02:00
Daniel
73a633ae7d Merge pull request #767 from CartoDB/stats-middleware
Implement stats middleware removing some duplicated code
2017-10-16 16:08:55 +02:00
Daniel García Aubert
3068ff1ea4 Update NEWS 2017-10-16 15:16:21 +02:00
Daniel García Aubert
9ad6d0cbcc Merge branch 'master' into stats-middleware 2017-10-16 15:06:20 +02:00
Daniel
86389382fa Merge pull request #773 from CartoDB/fix-test-postgis-2.3
Fix tests in master branch while testing in devel environment (NOT CI)
2017-10-16 14:49:27 +02:00
Daniel García Aubert
c2bf7b075c Fix travis to export environment variable (POSTGIS_VERSION) 2017-10-16 13:24:46 +02:00
Daniel García Aubert
294a222669 Configure travis to export environment variable (POSTGIS_VERSION) 2017-10-16 13:20:30 +02:00
Daniel García Aubert
515146bf28 Configure travis to export environment variable (POSTGIS_VERSION) 2017-10-16 13:12:06 +02:00
Daniel García Aubert
a1c08f9bf7 Revert docker-test config 2017-10-16 12:53:23 +02:00
Daniel García Aubert
f8ff41be01 Do not run test if postgis version is lower than or equal to 2.4 2017-10-16 12:15:28 +02:00
Daniel García Aubert
67ab12e8e7 Add environment variable to check whether to run test against postgis 2.4 or not 2017-10-16 11:08:46 +02:00
David M
d959ef5007 Merge pull request #772 from CartoDB/windshaft4
Target Windshaft ~4.0.0, yarn.lock updated
2017-10-11 11:44:40 +02:00
David Manzanares
8cc4fe5b56 Target Windshaft ~4.0.0, yarn.lock updated 2017-10-11 11:34:27 +02:00
David M
22a34d763c Merge pull request #762 from CartoDB/mvt-path-conf
Add configuration flag to enable/disable direct PgSQL MVT
2017-10-11 10:42:09 +02:00
David Manzanares
d30f710534 Merge remote-tracking branch 'origin/master' into mvt-path-conf 2017-10-10 17:53:28 +02:00
Daniel García Aubert
02304dc450 Merge branch 'master' into stats-middleware 2017-10-10 16:56:35 +02:00
Daniel García Aubert
893fac31a7 Update NEWS 2017-10-10 16:44:11 +02:00
Simon Martín
abef8918c0 Merge pull request #751 from CartoDB/middlewarify
Middlewarify
2017-10-10 15:10:14 +02:00
Simon Martín
8380d291d0 Merge branch 'master' into middlewarify 2017-10-10 14:59:50 +02:00
Daniel García Aubert
251e636ad2 Fix bad argument list while calling to staticMap function 2017-10-10 11:58:24 +02:00
Raul Ochoa
286059b8a3 Merge pull request #770 from CartoDB/test-client-consistent-signature-fix
Make all calls to finish to match (err, res) signature
2017-10-09 18:40:15 +02:00
David M
b18bf967fd Workaround lack of template creation clean-up 2017-10-09 18:27:57 +02:00
Daniel García Aubert
a81e98995a Merge branch 'middlewarify' into stats-middleware 2017-10-09 17:54:32 +02:00
Raul Ochoa
a797e13eb3 Make all calls to finish to match (err, res) signature 2017-10-09 15:51:42 +00:00
Daniel
5e073f39bd Merge pull request #765 from CartoDB/res-locals
req.params to res.locals
2017-10-09 17:44:01 +02:00
David M
8a88b29665 update dependencies 2017-10-09 17:16:29 +02:00
David M
d77739dfa4 PostGIS MVT v2 compliance test skipped without skipping mapnik 2017-10-09 16:46:23 +02:00
Simon Martín
484e0fda2f undo changing services params 2017-10-09 16:29:35 +02:00
David M
3827901535 Skip test to ignore MVT v2 compliance 2017-10-09 15:53:50 +02:00
David M
82648df21c Fix jshint 2017-10-09 15:49:51 +02:00
David M
1766cd0ad4 Buffersize test fixed to test PostGIS and mapnik MVT paths 2017-10-09 15:42:24 +02:00
David M
6af83d7630 Use yarn instead of npm in docker 2017-10-09 14:54:03 +02:00
David M
28501f6b9d MVT PostGIS limit test fixed 2017-10-09 14:46:01 +02:00
Simon Martín
e3405ea2fc doing changes after merge with middlewarify 2017-10-09 12:27:58 +02:00
Simon Martín
5c0f597cbb Merge branch 'middlewarify' into res-locals 2017-10-09 10:55:43 +02:00
Daniel García Aubert
7289394f6a Missing EOL 2017-10-07 19:16:15 +02:00
Daniel García Aubert
1ba1c488fa Do not decorate response methods to set header and send stats 2017-10-07 19:02:26 +02:00
David Manzanares
10f9f61e1e Merge branch 'mvt-path-conf' of github.com:CartoDB/Windshaft-cartodb into mvt-path-conf 2017-10-06 18:24:48 +02:00
David Manzanares
0e20958220 Remove dockerfile 2017-10-06 18:24:29 +02:00
David Manzanares
28cb05e45b Use windshaft docker image 2017-10-06 18:24:11 +02:00
Raul Ochoa
c004e105ef Drain client on after hooks 2017-10-06 16:17:26 +00:00
Raul Ochoa
f456237aa7 Drain client on after hook 2017-10-06 15:53:47 +00:00
David Manzanares
7f66189164 Fix travis.yaml 2017-10-06 16:44:28 +02:00
David Manzanares
cac16f8b66 Travis dockerified 2017-10-06 16:30:35 +02:00
David Manzanares
a706fd81ba Restore MVT path configuration after each suite pass 2017-10-06 16:21:34 +02:00
David Manzanares
43885f130b Fix HTTP status code distinction between 200 and 204 2017-10-06 16:19:00 +02:00
David Manzanares
58be2b8fc5 Merge remote-tracking branch 'origin/improve-test-client' into mvt-path-conf 2017-10-06 16:08:13 +02:00
David Manzanares
78671aa499 remove redundant format support 2017-10-06 16:07:47 +02:00
David Manzanares
d29da0bcc3 Test both MVT paths: mapnik and PostGIS 2017-10-06 16:07:24 +02:00
Raul Ochoa
e9d0b3b77d Merge pull request #769 from CartoDB/improve-test-client
Improve test client
2017-10-06 16:03:30 +02:00
David Manzanares
4e6253b717 Return HTTP 204 for empty tiles 2017-10-06 16:02:16 +02:00
David M
44eb323764 Merge pull request #768 from CartoDB/test-support-fixes
Test support fixes
2017-10-06 15:57:25 +02:00
Raul Ochoa
d4015085c7 Include test/support as part of jshint validation 2017-10-06 15:28:01 +02:00
Raul Ochoa
b9c511ee60 Remove unused file 2017-10-06 15:27:48 +02:00
Raul Ochoa
64fe070ab2 Put layergroupId handling close 2017-10-06 15:27:03 +02:00
Raul Ochoa
5d750f3b98 Several jshint fixes 2017-10-06 15:24:58 +02:00
Raul Ochoa
664892bba9 Complexity already fixed 2017-10-06 15:15:43 +02:00
Raul Ochoa
38c50e0bec Fix jshint hint 2017-10-06 15:15:33 +02:00
Raul Ochoa
6c0e6210d6 Split response validation 2017-10-06 15:15:16 +02:00
Raul Ochoa
f350206990 Strict check 2017-10-06 12:54:37 +00:00
Raul Ochoa
c8d2c9ea37 Do NOT throw error when not being in step context 2017-10-06 12:41:50 +00:00
David Manzanares
cab2d6d5d4 package.json docker-bash script added 2017-10-06 11:03:08 +02:00
Daniel García Aubert
242e63716f Merge branch 'middlewarify' into stats-middleware 2017-10-05 18:21:02 +02:00
Daniel García Aubert
c70b8cb5bf Set X-Served-By-DB-Host header in db-conn-setup middleware 2017-10-05 18:05:46 +02:00
Simon Martín
06138a82a8 Merge branch 'middlewarify' into res-locals 2017-10-05 17:53:15 +02:00
Daniel García Aubert
678fbb1c8f Remove bad argument to middleware callback 2017-10-05 17:28:41 +02:00
Simon Martín
2f310a15bd do not overwrite creation of res.locals 2017-10-05 17:23:07 +02:00
Daniel García Aubert
bf637ccd5b Implement stats middleware removing some duplicated code 2017-10-05 17:06:42 +02:00
David Manzanares
f387f2ee6f Testing dockerified 2017-10-05 16:08:31 +02:00
David Manzanares
34d9e5a4eb Fix MVT test 2017-10-05 16:08:05 +02:00
David Manzanares
54b7ee85c2 Geojson tests adapted to MVT 2017-10-05 14:38:43 +02:00
Simon Martín
9083fc2e20 fix forgotten comment 2017-10-05 12:44:03 +02:00
David Manzanares
72a9a3e097 updated deps 2017-10-05 12:26:19 +02:00
David Manzanares
102228c55b Merge remote-tracking branch 'origin/master' into mvt-path-conf 2017-10-05 12:19:24 +02:00
David Manzanares
148e6e6ae5 Merge branch 'master' into mvt-path-conf 2017-10-05 12:18:08 +02:00
David Manzanares
226653207a target windshaft master branch 2017-10-05 12:17:55 +02:00
Daniel García Aubert
b93c09959c Back to use just one router 2017-10-05 12:12:21 +02:00
Simon Martín
5abe25c316 undo style/format changes 2017-10-05 11:35:49 +02:00
Simon Martín
1f03a6b181 using res.locals instead of params in AuthApi 2017-10-05 11:28:41 +02:00
Simon Martín
16e8202782 stubs next version 2017-10-04 17:11:17 +02:00
Simon Martín
4afa7f70d7 release 4.0.0 2017-10-04 17:06:58 +02:00
Simon Martín
5045f81fe3 Merge pull request #764 from CartoDB/express-v4.15.5
upgrade node modules to enhance security
2017-10-04 15:54:41 +02:00
Simon Martín
ec8fcc7302 change param name and comments updated 2017-10-04 12:50:27 +02:00
Raul Ochoa
2afb6b5ac2 Regenerate yarn.lock to pickup grainstore version 2017-10-04 10:32:36 +00:00
Simon Martín
19e2515a8e Merge pull request #749 from CartoDB/dataview-factory-refactor
Dataview factory refactor
2017-10-04 12:18:55 +02:00
Raul Ochoa
5d6156a257 Add missing upgraded dep 2017-10-04 10:16:26 +00:00
Raul Ochoa
9e217d9199 Merge branch 'master' into express-v4.15.5 2017-10-04 10:16:07 +00:00
Raul Ochoa
a224a0bf91 Unify all pending changes in 4.0.0 version 2017-10-04 10:14:09 +00:00
Raul Ochoa
87bcb7ebf2 Update list of upgraded deps 2017-10-04 10:12:54 +00:00
Raul Ochoa
d06ba8b1f8 Merge remote-tracking branch 'origin/master' into express-v4.15.5 2017-10-04 10:11:08 +00:00
Simon Martín
2b37a406bc Merge pull request #763 from CartoDB/remove-list-dataview
Removes `list` dataview type
2017-10-04 12:01:42 +02:00
Simon Martín
0a507d02bc Merge branch 'master' into remove-list-dataview 2017-10-04 11:36:45 +02:00
Raul Ochoa
49fd75f0b6 Merge remote-tracking branch 'origin/master' into express-v4.15.5 2017-10-04 09:32:46 +00:00
Simon Martín
514aa53152 Merge pull request #748 from CartoDB/base-dataview-refactor
Base dataview refactor
2017-10-04 11:17:38 +02:00
Simon Martín
8fe31c45f3 fix 'this' scope with arrow function 2017-10-04 11:10:17 +02:00
Simon Martín
fe4c22d2ea Merge branch 'master' into base-dataview-refactor 2017-10-04 11:08:43 +02:00
Simon Martín
d27cce915c Merge pull request #747 from CartoDB/formula-dataview-refactor
Formula dataview refactor
2017-10-04 10:10:57 +02:00
Simon Martín
1c3f2b93e3 prepareRequest and prepareResponse in prepare-context.test 2017-10-03 17:58:16 +02:00
Simon Martín
21720267cf from req.context to res.locals 2017-10-03 17:47:57 +02:00
Raul Ochoa
da832263a4 Upgrade turbo-carto, camshaft, and cartodb-psql 2017-10-03 11:46:03 +00:00
Raul Ochoa
69bd14793f Upgrade to windshaft 3.3.3
Regenerate yarn.lock file
2017-10-03 11:21:47 +00:00
Raul Ochoa
54dd15c0b0 Merge remote-tracking branch 'origin/master' into express-v4.15.5 2017-10-03 11:15:31 +00:00
Simon Martín
3ce10690d6 send res.locals instead of res when possible 2017-10-03 13:06:12 +02:00
Simon Martín
6bfc5d8891 fix function name and removing comments of localsMiddleware 2017-10-03 13:03:02 +02:00
Simon Martín
430e1513d8 fix incorrect function parameter 2017-10-03 13:00:52 +02:00
Rafa de la Torre
28c8632532 Merge pull request #766 from CartoDB/update-yarn-v0.27.5
Update yarn version to v0.27.5
2017-10-03 12:54:46 +02:00
Rafa de la Torre
89172f280f Change yarn dep version range specification
In theory the caret `^` should work but as rochoa pointed out it does
not. So changing it (also for the sake of clarity).
2017-10-03 12:43:09 +02:00
Rafa de la Torre
0a7506e4b2 Update yarn version to v0.27.5 2017-10-03 12:10:43 +02:00
Simon Martín
4fd3c99531 Merge pull request #746 from CartoDB/list-dataview-refactor
List dataview refactor
2017-10-02 17:28:54 +02:00
Simon Martín
1e4c63a6dc Merge pull request #745 from CartoDB/aggregation-dataview-refactor
Aggregation dataview refactor
2017-10-02 17:24:12 +02:00
Simon Martín
742420b159 Merge pull request #744 from CartoDB/time-series-refactor
Histogram refactor
2017-10-02 17:18:21 +02:00
Rafa de la Torre
b8783a6447 Stub NEWS/package for next version 2017-10-02 14:49:01 +02:00
Rafa de la Torre
45dece65f2 Update 3.13.0 release date in NEWS.md 2017-10-02 14:45:43 +02:00
Simon Martín
c894414192 going green 2017-10-02 12:28:29 +02:00
Simon Martín
aa62529041 updating preprare-context test to allow the new res.locals usage 2017-10-02 12:09:19 +02:00
Simon Martín
55f593eae6 adding forgotten semicolon 2017-10-02 12:08:10 +02:00
Simon Martín
f9d87bc40f res.locals fixing controllers 2017-10-02 12:07:35 +02:00
Eneko Lakasta
818cdbd99b upgrade express again (4.16.0) 2017-09-29 17:41:57 +02:00
Simon Martín
783eb0eec7 res.locals format and layer in namep maps 2017-09-29 17:03:57 +02:00
Simon Martín
c22a35489d res.locals forgotten things and make jshint happy 2017-09-29 14:38:28 +02:00
Simon Martín
482feabce2 res.locals in named maps controller 2017-09-29 14:37:55 +02:00
Simon Martín
0a753400e0 res.locals in map controller 2017-09-29 12:54:21 +02:00
Simon Martín
a21648ab4a res.locals in layergroup controller 2017-09-29 12:32:46 +02:00
Simon Martín
b4d03c074a not move db params to res.locals.db 2017-09-29 11:07:11 +02:00
Eneko Lakasta
75c8a73423 upgrade debug to version 3.1 2017-09-29 10:42:20 +02:00
Eneko Lakasta
649383a3df upgrade request module to version 2.83.0 2017-09-28 18:28:04 +02:00
Eneko Lakasta
52402c0333 Revert "upgrade request module"
This reverts commit cacb92b0c4.
2017-09-28 18:25:40 +02:00
Eneko Lakasta
cacb92b0c4 upgrade request module 2017-09-28 18:24:50 +02:00
David Manzanares
5463248578 Changed PostGIS MVT flag name 2017-09-28 17:57:59 +02:00
Eneko Lakasta
aaf95b9223 upgrade body-parser (package.json and yarn.lock) 2017-09-28 17:41:49 +02:00
Eneko Lakasta
6149df1810 upgrade body-parser to version 1.17.2
version 1.17.2 requires a patched query string (qs) module
2017-09-28 17:37:43 +02:00
Eneko Lakasta
ff47027a51 upgrade express version to 4.15.5 2017-09-28 17:20:07 +02:00
David Manzanares
e5cae8b8e3 Added flag documentation 2017-09-28 13:24:50 +02:00
Raul Ochoa
78b75c7a88 Removes list dataview type 2017-09-28 10:47:44 +00:00
Javier Goizueta
56d7c2c140 Merge pull request #760 from CartoDB/upgrade-camshaft-to-0.59.1
Upgrade camshsft to 0.59.1
2017-09-28 12:09:46 +02:00
David Manzanares
ad1abb28af Add configuration flag to enable/disable direct PgSQL MVT 2017-09-28 12:08:22 +02:00
Simon Martín
f824fc5243 base and analyses controller 2017-09-28 12:02:34 +02:00
Simon Martín
4a2cc6a5f8 res.locals in auth_api 2017-09-28 11:55:36 +02:00
Simon
ca612dd02a res.locals in context middlewares 2017-09-28 11:43:12 +02:00
Unknown
fedcb0d0f9 remove unused middleware 2017-09-28 11:23:53 +02:00
Javier Goizueta
c9f0902703 Upgrade camshsft to 0.59.1
This fixes duplicate column names in aggregate-intersection analysis
2017-09-27 18:44:06 +02:00
Simon Martín
1739cee11d Merge pull request #752 from CartoDB/layergroup-token-param
Layergroup token param
2017-09-27 16:44:58 +02:00
Simon
178b9e8563 moving layergroup-token middleware to middlewarify style 2017-09-27 16:32:49 +02:00
Simon
ac474cb253 Merge branch 'middlewarify' into layergroup-token-param 2017-09-27 16:31:08 +02:00
Simon Martín
79510185da Merge pull request #753 from CartoDB/layergroup-token-middleware
Layergroup Token parsing as middleware
2017-09-27 11:44:15 +02:00
Raul Ochoa
c960535709 Merge pull request #756 from CartoDB/upgrade-deps
Upgrades camshaft, cartodb-query-tables, and turbo-carto
2017-09-26 18:32:31 +02:00
Simon
84cd93b1b0 make jshint happy 2017-09-26 18:25:47 +02:00
Simon
134cc9ac0c changing req.locals to res.locals 2017-09-26 18:23:49 +02:00
Daniel García Aubert
615229fc31 Remove comment 2017-09-26 17:32:50 +02:00
Daniel García Aubert
4600005a86 Bring ported test back 2017-09-26 17:31:57 +02:00
Simon
383c8305cc Merge branch 'middlewarify' of github.com:CartoDB/Windshaft-cartodb into middlewarify 2017-09-26 15:40:00 +02:00
Simon
b94dfe066d removing some repeated things 2017-09-26 15:39:48 +02:00
Daniel García Aubert
de267917f4 Merge branch 'middlewarify' of github.com:CartoDB/Windshaft-cartodb into middlewarify 2017-09-26 15:17:20 +02:00
Simon
3f6afb4530 validation middleware for layer route (conflicting route) 2017-09-26 14:56:20 +02:00
Simon
540fda1e6c Merge branch 'master' into middlewarify 2017-09-26 14:53:57 +02:00
Daniel García Aubert
e0e67df91c Merge branch 'master' into middlewarify 2017-09-25 20:04:33 +02:00
Daniel García Aubert
4899c7ffef Inject prepare context middleware to controllers 2017-09-25 19:40:27 +02:00
Raul Ochoa
ac42223439 Allow 6.9.0 version 2017-09-25 15:34:16 +00:00
Raul Ochoa
1110abaa9a Merge pull request #644 from CartoDB/check-node-version
Check node version
2017-09-25 17:33:26 +02:00
Raul Ochoa
3023111896 Reuse existing log methods 2017-09-25 14:55:17 +00:00
Raul Ochoa
66380197f4 Do not explode by - token 2017-09-25 14:53:17 +00:00
Raul Ochoa
8daa4bb08a Merge remote-tracking branch 'origin/master' into check-node-version 2017-09-25 14:51:50 +00:00
Raul Ochoa
b943b09532 Merge pull request #736 from CartoDB/static-named-maps-override-bbox
Static named maps override bbox
2017-09-25 14:10:27 +02:00
Raul Ochoa
eda18726fd Merge pull request #755 from CartoDB/analyses-use-cases-tests
Analyses use cases tests
2017-09-25 14:06:44 +02:00
Daniel García Aubert
f0920aedef Remove unused module 2017-09-25 13:43:15 +02:00
Daniel García Aubert
b236112069 Split prepare context middleware and fix unit test 2017-09-25 13:40:22 +02:00
Raul Ochoa
d3dafc8a40 Regenerate lock file 2017-09-25 11:24:33 +00:00
Raul Ochoa
c734f43643 Upgrades camshaft, cartodb-query-tables, and turbo-carto
Better support for query variables.
2017-09-25 11:11:05 +00:00
Daniel García Aubert
0e8fb68794 Extract token param to a middleware 2017-09-22 18:49:21 +02:00
Daniel García Aubert
f7b9287c93 Return an array of middlewares instead of big one in prepare context 2017-09-22 18:24:16 +02:00
Daniel García Aubert
85d4c81e58 Remove legacy hack 2017-09-22 18:15:48 +02:00
Daniel García Aubert
ff19a8a2fe Rename test 2017-09-22 17:59:51 +02:00
Daniel García Aubert
3bab081438 Rename req2params by prepareContext 2017-09-22 17:56:47 +02:00
Daniel García Aubert
6dc9cc0b23 Remove req2params dependency 2017-09-22 17:56:08 +02:00
Raul Ochoa
3134f40eac Remove advanced use cases that no longer make sense 2017-09-22 15:42:52 +00:00
Raul Ochoa
5cc31cabe2 Fix and enable some old tests related to analyses use cases 2017-09-22 15:41:58 +00:00
Daniel García Aubert
8fd35849c7 Merge branch 'middlewarify' of github.com:CartoDB/Windshaft-cartodb into middlewarify 2017-09-22 17:15:41 +02:00
Simon
c09899913f finishing integration of lzma middleware 2017-09-22 16:46:39 +02:00
Daniel García Aubert
0bdeee64a7 Use express router to group controllers' enpoints and reuse common middleware for named maps admin controller 2017-09-22 16:45:34 +02:00
Daniel García Aubert
ee8619c470 Use express router to group controllers' enpoints and reuse common middleware for analysis controller 2017-09-22 16:28:52 +02:00
Simon
9d81321d78 Merge branch 'master' into middlewarify 2017-09-22 16:26:56 +02:00
Simon Martín
ca63c2ef1a Merge pull request #721 from CartoDB/lzma-middleware
Implement LZMA query param inflating as middleware
2017-09-22 16:16:36 +02:00
Daniel García Aubert
b0486f9bae Use express router to group enpoints and reuse common middlewares for layergroup controller 2017-09-22 15:44:12 +02:00
Raul Ochoa
2eb1c0f3e0 Remove unused import 2017-09-22 12:59:14 +00:00
Raul Ochoa
22b7828725 Layergroup Token parsing as middleware
Reuses LayergroupToken model from tests.
2017-09-22 12:05:40 +00:00
Raul Ochoa
78404b1308 Merge remote-tracking branch 'origin/master' into layergroup-token-param 2017-09-22 11:12:42 +00:00
Raul Ochoa
45698207d9 Merge remote-tracking branch 'origin/master' into static-named-maps-override-bbox 2017-09-22 11:08:12 +00:00
Daniel García Aubert
9bd862ffaf Remove req2params from BaseController and update related test to use the middleware 2017-09-22 01:08:46 +02:00
Daniel García Aubert
8139cdf8b2 Use req2params middleware for name maps static views endpoint 2017-09-22 00:58:44 +02:00
Daniel García Aubert
a8898a8022 Use req2params middleware for name maps tile endpoint 2017-09-22 00:48:44 +02:00
Daniel García Aubert
df5ec0f4d9 Use req2params middleware for analysis catalog endpoint 2017-09-22 00:42:17 +02:00
Daniel García Aubert
51ba3db4ac Use req2params middleware for instantiate named map endpoint 2017-09-22 00:31:16 +02:00
Daniel García Aubert
d31e52a625 Fix format, break line in bad position 2017-09-21 22:55:30 +02:00
Daniel García Aubert
3a8b99a14e Use req2params middleware for tile and layer endpoint 2017-09-21 22:53:31 +02:00
Daniel García Aubert
fac1ab4a1c Use req2params middleware for attributes endpoint 2017-09-21 22:47:08 +02:00
Daniel García Aubert
a9b0acc317 Use req2params middleware for static map (bbox & center) endpoint 2017-09-21 22:43:59 +02:00
Daniel García Aubert
5cb2e5d3c5 Skip temporaly ported test 2017-09-21 21:53:05 +02:00
Daniel García Aubert
e2ed0058d8 Use req2params middleware for layergroup create endpoint 2017-09-21 21:52:34 +02:00
Daniel García Aubert
2f499a148a Use req2params middleware for dataview endpoint 2017-09-21 14:33:32 +02:00
Daniel García Aubert
49204650c6 Use req2params middleware for datavie search endpoint 2017-09-21 14:30:19 +02:00
Daniel García Aubert
234576ab5f Use req2params middleware for analisys node status endpoint 2017-09-21 13:37:32 +02:00
Daniel García Aubert
02cd6a43ad Move req2params method to a its own middleware 2017-09-21 13:27:22 +02:00
Daniel García Aubert
429f070372 Pass node's response object to req2params 2017-09-21 12:22:33 +02:00
Daniel García Aubert
3b9c561cee Change signature of req2params to follow express' middleware pattern 2017-09-21 11:54:37 +02:00
Daniel García Aubert
daeae5d95c Implement error-middleware to handle errors at top level 2017-09-21 11:46:42 +02:00
Raul Ochoa
33121871b0 Stubs next version 2017-09-18 12:47:37 +00:00
Raul Ochoa
f133d983e8 Release 3.12.10 2017-09-18 12:46:40 +00:00
Raul Ochoa
b6237c7bfa Merge pull request #750 from CartoDB/upgrade-windshaft
Upgrades windshaft to 3.3.2
2017-09-18 13:17:12 +02:00
Raul Ochoa
a5d9bfa0ec Upgrades windshaft to 3.3.2 2017-09-18 10:34:04 +00:00
Simon
222cfb90fd Removing 'self' vars using arrow functions 2017-09-18 12:20:59 +02:00
Simon
f63fab40ed Removing 'self' vars using arrow functions 2017-09-18 11:34:18 +02:00
Daniel García Aubert
61ea05d1c2 Do not assign a value by default for special float values counters 2017-09-15 14:51:02 +02:00
Daniel García Aubert
64c3e68303 Fix double declaration of 'result' 2017-09-15 14:48:54 +02:00
Daniel García Aubert
d4bb4edd1d Applyy extract method to check input options 2017-09-15 14:43:41 +02:00
Daniel García Aubert
419b29e609 Do not prefix with '_' template context 2017-09-15 14:43:26 +02:00
Daniel García Aubert
c7ed3d34e8 Use const instead of var to declare variables 2017-09-15 14:43:13 +02:00
Daniel García Aubert
1959a841fd Use arrow function to take advantage of bound context 2017-09-15 14:26:22 +02:00
Daniel García Aubert
ef5049f28f Use destructuring assignment to improve readability 2017-09-15 14:07:46 +02:00
Daniel García Aubert
d5d9044686 Use const keyword to declare variables 2017-09-15 14:05:23 +02:00
Daniel García Aubert
5d632d936e Use ES6 class syntax 2017-09-15 14:04:09 +02:00
Daniel García Aubert
90c4796d4e Remove empty line 2017-09-15 13:41:54 +02:00
Daniel García Aubert
ada58f6ea2 Use const keyword to declare varibles 2017-09-15 13:35:00 +02:00
Daniel García Aubert
b4ce13e429 Use object shorthand notation 2017-09-15 11:56:59 +02:00
Daniel García Aubert
11f7b38c69 Do not use dot module to build column type query 2017-09-15 11:54:56 +02:00
Daniel García Aubert
9771979b8f Missing call to super class in constructor 2017-09-15 10:59:07 +02:00
Daniel García Aubert
c00a93f414 Use destruturing assignment to format the formula result 2017-09-15 10:58:11 +02:00
Daniel García Aubert
ecbc7a28e7 Declare constants with const keyword 2017-09-15 10:49:20 +02:00
Daniel García Aubert
68dfed8b85 Use ES6 class syntax 2017-09-15 10:48:44 +02:00
Daniel García Aubert
2437288d9d Replace widget word by dataview 2017-09-15 10:37:51 +02:00
Daniel García Aubert
9c64d674b3 Do not use underscore 2017-09-14 18:02:13 +02:00
Daniel García Aubert
a4ecc18f2f Use default values for constructor's arguments 2017-09-14 17:57:24 +02:00
Daniel García Aubert
1063d81c1b Rename debug namespace 2017-09-14 17:56:40 +02:00
Daniel García Aubert
dcb9b8ec52 Rename BaseWidget by BaseDataview 2017-09-14 17:56:17 +02:00
Daniel García Aubert
dbb23bf9f0 Remove jshint's complaints 2017-09-14 17:24:13 +02:00
Daniel García Aubert
2a0b15f085 Remove prefix '_' while passing context param to templates 2017-09-14 17:22:34 +02:00
Daniel García Aubert
d0e2c9f898 Use debug module to print sql for debugging purposes 2017-09-14 17:21:21 +02:00
Daniel García Aubert
d328b534a5 Replace widget word by dataview 2017-09-14 17:19:16 +02:00
Daniel García Aubert
050e9776d1 Use const for requirements, constants and variables that are initialized once 2017-09-14 17:18:20 +02:00
Daniel García Aubert
c8ff61c531 Use ES6 class syntax 2017-09-14 17:15:43 +02:00
Daniel García Aubert
cdc56e703c Rename BaseWidget by BaseDataview 2017-09-14 17:10:51 +02:00
Daniel García Aubert
9a4794ee10 Remove dot requirement and use template strings to build list aggregation query 2017-09-14 17:09:55 +02:00
Daniel García Aubert
51907b9545 Apply extract method to condition 2017-09-14 16:56:55 +02:00
Daniel García Aubert
1f3b0beddf Fix missing parameter 2017-09-14 16:47:05 +02:00
Daniel García Aubert
38e2c040d1 Use template string to escape literals 2017-09-14 16:45:45 +02:00
Daniel García Aubert
46860541fe Apply extract method to validate input options 2017-09-14 16:42:25 +02:00
Daniel García Aubert
c2e99219ef Use ES6 goodies to refactor format method 2017-09-14 16:30:46 +02:00
Daniel García Aubert
cc2cf78264 Rename all 'widget' ocurrences by 'dataview' 2017-09-14 16:12:39 +02:00
Daniel García Aubert
746292610a Rename debug namespace 2017-09-14 16:10:23 +02:00
Daniel García Aubert
b05083bcfc Move search's templates along aggregation's templates 2017-09-14 16:09:44 +02:00
Daniel García Aubert
cd13107a4d Use ES6 let & const to declare variables 2017-09-14 16:08:12 +02:00
Daniel García Aubert
46254eaf74 rename BaseWidget by BaseDataview 2017-09-14 15:59:15 +02:00
Daniel García Aubert
086eff01a9 Use ES6 class syntax 2017-09-14 15:57:18 +02:00
Daniel García Aubert
02949003a9 Build search query in two steps 2017-09-14 15:48:54 +02:00
Daniel García Aubert
0a894da0df Remove prefix to context's properties 2017-09-14 15:27:57 +02:00
Daniel García Aubert
e2ab48bee2 Remove prefix to context's properties 2017-09-14 15:22:58 +02:00
Daniel García Aubert
132fce84c5 Remove string template in the middle 2017-09-14 15:07:30 +02:00
Daniel García Aubert
b1508af007 Remove prefix to context's props in order to share it throught all templates avoiding to duplicate passing custom params again and again 2017-09-14 13:14:12 +02:00
Daniel García Aubert
65dca454f4 Move aggragation query to its own query template 2017-09-14 12:18:03 +02:00
Daniel García Aubert
3682740f08 Build aggregation query with string templates avoiding to join all inner templates 2017-09-14 12:01:41 +02:00
Daniel García Aubert
a434015d5b Move categories CTE template out of aggregation class 2017-09-14 11:27:03 +02:00
Daniel García Aubert
2f4f719f55 Use object properties in class method instead of passing them as parameters 2017-09-13 19:42:25 +02:00
Daniel García Aubert
75645e2d7a Use string templates to build categories CTE sql 2017-09-13 19:34:09 +02:00
Daniel García Aubert
4d1a53c20f Use string templates to build error message 2017-09-13 19:27:25 +02:00
Daniel García Aubert
ee471184b9 Use default values for input params 2017-09-13 19:19:25 +02:00
Daniel García Aubert
4518b7cb6e Declare requirement with const keyword 2017-09-13 19:17:16 +02:00
Daniel García Aubert
306df5be5a Replace underscore's function by ES6 equivalents 2017-09-13 19:16:08 +02:00
Daniel García Aubert
33e8657e35 Declare constants with const keyword 2017-09-13 18:40:09 +02:00
Daniel García Aubert
6fd3388fa2 Replace dot templates by ES6 string templates 2017-09-13 18:38:54 +02:00
Daniel García Aubert
4a89ad57d7 Remove '_' as prefix for template's context properties 2017-09-12 13:05:46 +02:00
Daniel García Aubert
c0cfdad7d1 Use hasOwnProperty method to check if histogram is a time-series 2017-09-12 10:38:53 +02:00
Daniel García Aubert
8f797c3c41 Fix EOF 2017-09-12 10:16:16 +02:00
Daniel García Aubert
2576c3e7d5 Rename _shouldOverrideRange & _shouldOverrideBins methods 2017-09-12 10:14:55 +02:00
Daniel García Aubert
3a936474cf Fix bad merge with master 2017-09-11 19:48:46 +02:00
Daniel García Aubert
a98f5bf08b Merge branch 'master' into time-series-refactor 2017-09-11 19:38:21 +02:00
Daniel García Aubert
03babcb43b Simplify condition and remove unused method 2017-09-11 19:31:38 +02:00
Daniel García Aubert
9aa5a9e850 Improve comment 2017-09-11 19:26:28 +02:00
Daniel García Aubert
e3bffcd39d Use inline functions to filter desired fields of the row 2017-09-11 19:24:01 +02:00
Daniel García Aubert
5fc2b46d56 Fix bad condition 2017-09-11 19:14:11 +02:00
Daniel García Aubert
7c69240748 Use parameter default value 2017-09-11 19:10:06 +02:00
Daniel García Aubert
ee43378c68 Use arrow function 2017-09-11 19:09:05 +02:00
Daniel García Aubert
09981c2560 Extract method to check valid aggregation 2017-09-11 18:57:16 +02:00
Daniel García Aubert
fd9534797c Minor refactors 2017-09-11 18:44:14 +02:00
Daniel García Aubert
38e7e71328 Implement template method pattern to format histogram query output 2017-09-11 17:19:02 +02:00
Daniel García Aubert
271932a80d Extract condition to a method 2017-09-11 17:17:42 +02:00
Daniel García Aubert
4f33e0d794 Rename Histogram.dataview by Histogram.histogramImplementation 2017-09-11 15:34:42 +02:00
Daniel García Aubert
ec23bfc79b Rename HistogramBase by BaseHistogram 2017-09-11 13:54:46 +02:00
Daniel García Aubert
6c3fa045cd Rename HistogramBase by BaseHistogram 2017-09-11 13:53:05 +02:00
Simon
d75ee965ae changing some 'var' to 'let/const' 2017-09-11 11:48:33 +02:00
Simon
5e9b2e45c7 creating HistogramBase with the common functions of NumericHistogram and DateHistogram 2017-09-11 11:32:20 +02:00
Simon
e4a20fa954 adding forgotten return 2017-09-08 17:57:35 +02:00
Simon
a20900210d removing unneeded _isDateHistogram function 2017-09-08 17:54:25 +02:00
Simon
2650c3b3e6 removing self=this assignment 2017-09-08 17:51:02 +02:00
Simon
25ef2610aa Varible declarations to let/const 2017-09-08 17:43:10 +02:00
Simon
92f6f59e07 Fix jshint style errors 2017-09-08 16:13:23 +02:00
Simon
5e07cc2ad1 Remove unneeded condittion 2017-09-08 16:06:54 +02:00
Simon
5593d92c4b Do not choose histogram implementation until getResult() 2017-09-08 15:55:23 +02:00
Simon
29f32cb9cc Expose dataview's methods to bypass concrete overview's implementations 2017-09-08 15:53:00 +02:00
Simon
1d4935cc9a Fix undefined while destrutcuring assignment 2017-09-08 15:50:01 +02:00
Simon
f75b4312a1 Fix undefined while destructuring assignment 2017-09-08 15:49:25 +02:00
Simon
23dd143fa5 Make Histogram class as context of state pattern 2017-09-08 12:48:08 +02:00
Simon
7d42afcdb4 remove unnecessary properties of NumericHistogram 2017-09-08 12:22:24 +02:00
Simon
78b95d05d0 make private functions 2017-09-08 12:21:22 +02:00
Simon
fb753e50a2 remove getOffset function 2017-09-08 12:19:05 +02:00
Simon
c863cdd9f6 remove getAggregation function 2017-09-08 12:17:29 +02:00
Simon
a4ebce52db remove unnecessary properties in format function 2017-09-08 12:16:56 +02:00
Simon
4a00a2d673 rename buildQueryTpl by _buildQueryTpl 2017-09-08 12:15:21 +02:00
Simon
38f0e23efe rename buildNumericHistogramQueryTpl by buildQueryTpl 2017-09-08 12:10:31 +02:00
Simon
7f14785091 fix namespace for debugging 2017-09-08 12:07:49 +02:00
Simon
db969a51ad make public some private functions 2017-09-08 12:07:07 +02:00
Simon
3441ad6aa9 rename _buildDateHistogramQueryTpl by _buildQueryTpl 2017-09-08 12:04:25 +02:00
Simon
347dea8f66 naming private functions 2017-09-08 12:01:15 +02:00
Simon
a3112aa929 fix function name in recursion 2017-09-08 11:55:28 +02:00
Simon
157946cc42 Rename DateHistogram class 2017-09-08 11:54:30 +02:00
Simon
8ce25d958c fix namespace for debugging 2017-09-08 11:53:47 +02:00
Daniel García Aubert
7e099be134 Add specific implementations of histograms based on column type; still not used 2017-09-08 10:29:54 +02:00
Javier Torres
6b2e2b2241 Stub next release (package.json) 2017-09-07 16:00:27 +02:00
Javier Torres
855e5c9e4c Stub next release 2017-09-07 15:59:32 +02:00
Javier Torres
a24792f46d Release 3.12.9 2017-09-07 15:57:11 +02:00
Javier Torres
0eb57f6801 Merge pull request #743 from CartoDB/718-quantiles_turbo_carto
Do not use distinct when calculating quantiles
2017-09-07 15:55:47 +02:00
Javier Torres
f1246cb060 Bump to 3.12.9 2017-09-07 15:54:43 +02:00
Javier Torres
7dd5c5b15d Do not use distinct when calculating quantiles 2017-09-07 14:39:25 +02:00
Ivan Malagon
806c13beac Release 3.12.8 2017-09-07 10:51:23 +02:00
Ivan Malagon
69f110e037 Merge pull request #742 from CartoDB/fix-histogram-out-of-range
Fix out of range bug in date histograms
2017-09-07 10:49:49 +02:00
Ivan Malagon
d77075295e Update version and NEWS 2017-09-07 10:48:21 +02:00
Daniel García Aubert
63a7ee08d0 Avoid nested ternaries for date histograms 2017-09-06 19:15:39 +02:00
Daniel García Aubert
b63a67a5b8 Avoid nested ternaries 2017-09-06 18:33:51 +02:00
Daniel García Aubert
1ac8455dc2 Use template strings to build histogram query 2017-09-06 18:13:34 +02:00
Daniel García Aubert
9f52e58be8 Rename BaseWidget by BaseDataview 2017-09-06 16:54:08 +02:00
Daniel García Aubert
4edf18f77a Remove underscore requirement 2017-09-06 16:52:17 +02:00
Daniel García Aubert
b5d2de8edc Do not use _.omit() 2017-09-06 16:49:00 +02:00
Ivan Malagon
bd8d147a7d Fix out of range bug in date histograms 2017-09-06 16:21:01 +02:00
Daniel García Aubert
8ac041805c Use typeof !== string instead of underscore's equivalent 2017-09-06 16:20:01 +02:00
Daniel García Aubert
6e0dc8666d Use .hasOwnProperty() instead of underscore's equivalent 2017-09-06 16:14:29 +02:00
Daniel García Aubert
3e55bd2abb Make happy to jshint 2017-09-06 15:56:52 +02:00
Daniel García Aubert
da1d0550f6 Use const keyword for constants 2017-09-06 15:52:13 +02:00
Daniel García Aubert
c37ef36a61 Move parseOffset function to a class method 2017-09-06 15:49:05 +02:00
Daniel García Aubert
9e3e1cad9a Move getWidth function to a class method 2017-09-06 15:47:21 +02:00
Daniel García Aubert
e84f30488f Move getBinsCount function to a class method 2017-09-06 15:45:51 +02:00
Daniel García Aubert
49a60caffc Move getBinEnd function to a class method 2017-09-06 15:42:29 +02:00
Daniel García Aubert
392e004879 Move getBinStart and populateBinStart function to a class method 2017-09-06 15:38:23 +02:00
Daniel García Aubert
288656301b Move getOffset function to a class method 2017-09-06 15:37:28 +02:00
Daniel García Aubert
96740b82ed Move getAggregation function to a class method 2017-09-06 13:52:02 +02:00
Daniel García Aubert
1be66e1552 Use const for requirements 2017-09-06 13:47:19 +02:00
Daniel García Aubert
5ba2dfbbd6 Use ES6 class syntax 2017-09-06 13:43:54 +02:00
Daniel García Aubert
af4b3d81cd make happy to jshint 2017-09-06 11:55:40 +02:00
Daniel García Aubert
bbd42b73f2 Remove dot requirement 2017-09-06 11:44:52 +02:00
Daniel García Aubert
8e2535745e Use template string for columnCastTpl 2017-09-06 11:44:25 +02:00
Daniel García Aubert
4f75f6c07b Use template string for dateBinsQueryTpl and dateHistogramQueryTpl 2017-09-06 11:41:24 +02:00
Daniel García Aubert
0ede3013db Use template string for dateOverrideBasicsQueryTpl 2017-09-06 10:57:38 +02:00
Daniel García Aubert
0b79ac76db Use template string for dateBasicsQueryTpl 2017-09-06 10:39:17 +02:00
Daniel García Aubert
2739364193 Use template string for histogramQueryTpl 2017-09-05 15:53:21 +02:00
Daniel García Aubert
adcff54589 Use template string for nansQueryTpl 2017-09-05 15:53:06 +02:00
Daniel García Aubert
734cfa6d83 Fix undefined argument 2017-09-05 15:51:31 +02:00
Daniel García Aubert
7ea6b3e371 Use template string for infinitiesQueryTpl 2017-09-05 12:21:30 +02:00
Daniel García Aubert
f1018f3272 Use template string for nullsQueryTpl 2017-09-05 12:18:42 +02:00
Daniel García Aubert
151bdec1fd Use template string for overrideBinsQueryTpl 2017-09-05 12:16:38 +02:00
Daniel García Aubert
5d413ac1f9 Use template string for overrideBasicsQueryTpl 2017-09-05 11:26:38 +02:00
Daniel García Aubert
37b1376767 Fix bad find & replace 2017-09-05 11:26:27 +02:00
Daniel García Aubert
00741bc0a4 Use template string for basicsQueryTpl 2017-09-05 10:41:48 +02:00
Daniel García Aubert
c580600590 Extract template to filter out special numeric values 2017-09-05 10:36:18 +02:00
Daniel García Aubert
6373fe8652 Use template string for filteredQueryTpl 2017-09-04 19:01:58 +02:00
Daniel García Aubert
5ce419d863 Use template string for dateIntervalQueryTpl 2017-09-04 18:42:30 +02:00
Mario de Frutos
7be5361433 Upgrade to camshaft 0.58.1 (#739) 2017-09-01 11:31:06 +02:00
Mario de Frutos
5332fd3baa Stubs next version 2017-08-31 10:21:00 +02:00
Mario de Frutos
77f1aa7e0c Update to camshaft 0.58.0 (#737) 2017-08-31 10:12:58 +02:00
Raul Ochoa
e1990fc2f9 Use the correct fixture image 2017-08-29 13:29:39 +00:00
Raul Ochoa
ca6eb609b2 Update news 2017-08-29 13:08:14 +00:00
Raul Ochoa
91ce3a5489 Going green: allow to use bbox param, along lon, lat, and zoom
The `bbox` param was removed from the base controller, the rest kept
working as they are declared in the base, but it's better to declare
them here as well.

Fixes #735.
2017-08-29 13:05:03 +00:00
Raul Ochoa
fc0dbaaab1 Going red: Bounding box parameter ignored in static named maps 2017-08-29 13:04:20 +00:00
Mario de Frutos
03dc260104 Stubs new version 2017-08-24 12:09:51 +02:00
Mario de Frutos
d644376f88 Update to camshaft 0.57.0 (#733)
* Update to camshaft 0.57.0

* Yarn.lock with update to camshaft 0.57.0
2017-08-24 12:07:43 +02:00
Mario de Frutos
ed0bfa5f63 Stubs next version 2017-08-23 16:53:01 +02:00
Mario de Frutos
ca0b927f51 Update camshaft to 0.56.0 (#731) 2017-08-23 16:34:42 +02:00
Simon
cd27d6aa02 Stub NEWS/package for next version 2017-08-22 16:37:23 +02:00
Simon
7aefca3f82 updating NEWS.md 2017-08-22 16:26:36 +02:00
Simon Martín
3d409274e0 Merge pull request #730 from CartoDB/upgrade-camshaft
Upgrade camshaft to 0.55.8

All working fine in local and staging
2017-08-22 16:13:06 +02:00
Raul Ochoa
4491fa2faf Upgrade camshaft to 0.55.8 2017-08-22 14:17:17 +02:00
Raul Ochoa
0ed46930dd Stubs next version 2017-08-16 15:37:29 +02:00
Raul Ochoa
e9d925334c Move layergroup-token to models
We will share it between tests and a middleware to parse the token.
2017-08-04 17:51:10 +02:00
Raul Ochoa
399561d076 Implement LZMA query param inflating as middleware
The req2params method is doing too many things,
this is an initial step to do fewer things in that method.
2017-08-04 17:30:46 +02:00
Daniel García Aubert
6a4b412cd3 Avoid lint 2017-03-27 18:27:55 +02:00
Daniel García Aubert
2374711d63 Remove reference to issue 2017-03-24 17:34:05 +01:00
Daniel García Aubert
213a3e297c Check node version 2017-03-24 17:28:23 +01:00
92 changed files with 3597 additions and 3869 deletions

View File

@@ -1,4 +1,3 @@
test/results/
test/monkey/
test/benchmark.js
test/support/

View File

@@ -1,27 +1,14 @@
sudo: required
dist: trusty
addons:
postgresql: "9.5"
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- postgresql-9.5-postgis-2.3
- postgresql-plpython-9.5
- pkg-config
- libcairo2-dev
- libjpeg8-dev
- libgif-dev
- libpango1.0-dev
- g++-4.9
services:
- docker
before_install:
- createdb template_postgis
- createuser publicuser
- psql -c "CREATE EXTENSION postgis" template_postgis
- docker pull cartoimages/windshaft-carto-testing
env:
- NPROCS=1 JOBS=1 PGUSER=postgres CXX=g++-4.9
script:
- docker run -e POSTGIS_VERSION=2.4 -v `pwd`:/srv cartoimages/windshaft-carto-testing
language: generic
language: node_js
node_js:
- "6"

View File

@@ -5,7 +5,7 @@ Make sure that you have the requirements needed. These are
- Core
- Node.js >=6.9.x
- yarn >=0.21.3
- yarn >=0.27.5 <1.0.0
- PostgreSQL >8.3.x, PostGIS >1.5.x
- Redis >2.4.0 (http://www.redis.io)
- Mapnik >3.x. See [Installing Mapnik](https://github.com/CartoDB/Windshaft#installing-mapnik).

View File

@@ -16,7 +16,7 @@ config.status--test:
./configure --environment=test
config/environments/test.js: config.status--test
./config.status--test
./config.status--test
TEST_SUITE := $(shell find test/{acceptance,integration,unit} -name "*.js")
TEST_SUITE_UNIT := $(shell find test/unit -name "*.js")

84
NEWS.md
View File

@@ -1,5 +1,89 @@
# Changelog
## 4.0.1
Released 2017-10-18
Announcements:
- Upgrades camshaft to [0.59.4](https://github.com/CartoDB/camshaft/releases/tag/0.59.4).
- Split and move `req2params` method to multiple middlewares.
- Use express error handler middleware to respond in case of something went wrong.
- Use `res.locals` object to share info between middlewares and leave `req.params` as an object containing properties mapped to the named route params.
- Move `LZMA` decompression to its own middleware.
- Implement stats middleware removing some duplicated code while sending response.
## 4.0.0
Released 2017-10-04
Backward incompatible changes:
- Removes `list` dataview type.
Announcements:
- Upgrades body-parser to 1.18.2.
- Upgrades express to 4.16.0.
- Upgrades debug to 3.1.0.
- Upgrades request to 2.83.0.
- Upgrades turbo-carto to [0.20.1](https://github.com/CartoDB/turbo-carto/releases/tag/0.20.1)
- Upgrades cartodb-psql to [0.10.2](https://github.com/CartoDB/node-cartodb-psql/releases/tag/0.10.2).
- Upgrades camshaft to [0.59.2](https://github.com/CartoDB/camshaft/releases/tag/0.59.2).
- Upgrades windshaft to [3.3.3](https://github.com/CartoDB/windshaft/releases/tag/3.3.3).
- Upgrades yarn minimum version requirement to v0.27.5
## 3.13.0
Released 2017-10-02
- Upgrades camshaft, cartodb-query-tables, and turbo-carto: better support for query variables.
Bugfixes:
- Bounding box parameter ignored in static named maps #735.
- camhaft 0.59.1 fixes duplicate columns in aggregate-intersection analysis
## 3.12.10
Released 2017-09-18
- Upgrades windshaft to [3.3.2](https://github.com/CartoDB/windshaft/releases/tag/3.3.2).
## 3.12.9
Released 2017-09-07
Bug fixes:
- Do not use distinct when calculating quantiles. #743
## 3.12.8
Released 2017-09-07
Bug fixes:
- Integer out of range in date histograms. (https://github.com/CartoDB/support/issues/962)
## 3.12.7
Released 2017-09-01
- Upgrades camshaft to [0.58.1](https://github.com/CartoDB/camshaft/releases/tag/0.58.1).
## 3.12.6
Released 2017-08-31
- Upgrades camshaft to [0.58.0](https://github.com/CartoDB/camshaft/releases/tag/0.58.0).
## 3.12.5
Released 2017-08-24
- Upgrades camshaft to [0.57.0](https://github.com/CartoDB/camshaft/releases/tag/0.57.0).
## 3.12.4
Released 2017-08-23
Announcements:
- Upgrades camshaft to [0.56.0](https://github.com/CartoDB/camshaft/releases/tag/0.56.0).
## 3.12.3
Released 2017-08-22
Announcements:
- Upgrades camshaft to [0.55.8](https://github.com/CartoDB/camshaft/releases/tag/0.55.8).
## 3.12.2
Released 2017-08-16

8
app.js
View File

@@ -2,14 +2,20 @@ var http = require('http');
var https = require('https');
var path = require('path');
var fs = require('fs');
var _ = require('underscore');
var semver = require('semver');
// jshint undef:false
var log = console.log.bind(console);
var logError = console.error.bind(console);
// jshint undef:true
var nodejsVersion = process.versions.node;
if (!semver.satisfies(nodejsVersion, '>=6.9.0')) {
logError(`Node version ${nodejsVersion} is not supported, please use Node.js 6.9 or higher.`);
process.exit(1);
}
var argv = require('yargs')
.usage('Usage: $0 <environment> [options]')
.help('h')

View File

@@ -107,6 +107,20 @@ var config = {
// Milliseconds since last access before renderer cache item expires
cache_ttl: 60000,
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
mvt: {
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
//PostGIS 2.4 is required for this to work
//If disabled it will use Mapnik MVT generation
usePostGIS: false,
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
}
},
mapnik: {
// The size of the pool of internal mapnik backend
// This pool size is per mapnik renderer created in Windshaft's RendererFactory

View File

@@ -101,6 +101,20 @@ var config = {
// Milliseconds since last access before renderer cache item expires
cache_ttl: 60000,
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
mvt: {
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
//PostGIS 2.4 is required for this to work
//If disabled it will use Mapnik MVT generation
usePostGIS: false,
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
}
},
mapnik: {
// The size of the pool of internal mapnik backend
// This pool size is per mapnik renderer created in Windshaft's RendererFactory

View File

@@ -101,6 +101,20 @@ var config = {
// Milliseconds since last access before renderer cache item expires
cache_ttl: 60000,
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
mvt: {
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
//PostGIS 2.4 is required for this to work
//If disabled it will use Mapnik MVT generation
usePostGIS: false,
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
}
},
mapnik: {
// The size of the pool of internal mapnik backend
// This pool size is per mapnik renderer created in Windshaft's RendererFactory

View File

@@ -100,6 +100,20 @@ var config = {
// Milliseconds since last access before renderer cache item expires
cache_ttl: 60000,
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
mvt: {
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
//PostGIS 2.4 is required for this to work
//If disabled it will use Mapnik MVT generation
usePostGIS: false,
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
}
},
mapnik: {
// The size of the pool of internal mapnik backend
// This pool size is per mapnik renderer created in Windshaft's RendererFactory

11
docker-test.sh Normal file
View File

@@ -0,0 +1,11 @@
export NPROCS=1 && export JOBS=1 && export CXX=g++-4.9 && export PGUSER=postgres
npm install -g yarn@0.27.5
yarn
/etc/init.d/postgresql start
createdb template_postgis && createuser publicuser
psql -c "CREATE EXTENSION postgis" template_postgis
POSTGIS_VERSION=2.4 npm test

View File

@@ -8,50 +8,13 @@ This specification describes an extension for
This extension depends on Analyses extension. It extends MapConfig with a new attribute: `dataviews`.
It makes possible to get tabular data from analysis nodes: lists, aggregated lists, aggregations, and histograms.
It makes possible to get tabular data from analysis nodes: aggregated lists, aggregations, and histograms.
## 2.1. Dataview types
### List
A list is a simple result set per row where is possible to retrieve several columns from the original layer query.
Definition
```
{
// REQUIRED
// string, `type` the list type
“type”: “list”,
// REQUIRED
// object, `options` dataview params
“options”: {
// REQUIRED
// array, `columns` to select for the list
“columns”: [“name”, “description”]
}
}
```
Expected output
```
{
"type": "list",
"rows": [
{
"{columnName1}": "val1",
"{columnName2}": 100
},
{
"{columnName1}": "val2",
"{columnName2}": 200
}
]
}
```
### Aggregation
An aggregation is very similar to a list but results are aggregated by a column and a given aggregation function.
An aggregation is a list with aggregated results by a column and a given aggregation function.
Definition
```

View File

@@ -19,22 +19,22 @@ function AuthApi(pgConnection, metadataBackend, mapStore, templateMaps) {
module.exports = AuthApi;
// Check if a request is authorized by a signer
// Check if the user is authorized by a signer
//
// @param req express request object
// @param res express response object
// @param callback function(err, signed_by) signed_by will be
// null if the request is not signed by anyone
// or will be a string cartodb username otherwise.
//
AuthApi.prototype.authorizedBySigner = function(req, callback) {
if ( ! req.params.token || ! req.params.signer ) {
AuthApi.prototype.authorizedBySigner = function(res, callback) {
if ( ! res.locals.token || ! res.locals.signer ) {
return callback(null, false); // no signer requested
}
var self = this;
var layergroup_id = req.params.token;
var auth_token = req.params.auth_token;
var layergroup_id = res.locals.token;
var auth_token = res.locals.auth_token;
this.mapStore.load(layergroup_id, function(err, mapConfig) {
if (err) {
@@ -84,11 +84,12 @@ AuthApi.prototype.authorizedByAPIKey = function(user, req, callback) {
* Check access authorization
*
* @param req - standard req object. Importantly contains table and host information
* @param res - standard res object. Contains the auth parameters in locals
* @param callback function(err, allowed) is access allowed not?
*/
AuthApi.prototype.authorize = function(req, callback) {
AuthApi.prototype.authorize = function(req, res, callback) {
var self = this;
var user = req.context.user;
var user = res.locals.user;
step(
function () {
@@ -101,11 +102,11 @@ AuthApi.prototype.authorize = function(req, callback) {
// if not authorized by api_key, continue
if (!authorized) {
// not authorized by api_key, check if authorized by signer
return self.authorizedBySigner(req, this);
return self.authorizedBySigner(res, this);
}
// authorized by api key, login as the given username and stop
self.pgConnection.setDBAuth(user, req.params, function(err) {
self.pgConnection.setDBAuth(user, res.locals, function(err) {
callback(err, true); // authorized (or error)
});
},
@@ -120,7 +121,7 @@ AuthApi.prototype.authorize = function(req, callback) {
// if no signer name was given, let dbparams and
// PostgreSQL do the rest.
//
if ( ! req.params.signer ) {
if ( ! res.locals.signer ) {
return callback(null, true); // authorized so far
}
@@ -128,7 +129,7 @@ AuthApi.prototype.authorize = function(req, callback) {
return callback(null, false);
}
self.pgConnection.setDBAuth(user, req.params, function(err) {
self.pgConnection.setDBAuth(user, res.locals, function(err) {
req.profiler.done('setDBAuth');
callback(err, true); // authorized (or error)
});

View File

@@ -24,7 +24,7 @@ module.exports = DataviewBackend;
DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, params, callback) {
var dataviewName = params.dataviewName;
var dataviewName = params.dataviewName;
step(
function getMapConfig() {
mapConfigProvider.getMapConfig(this);
@@ -113,9 +113,7 @@ function getOverrideParams(params, ownFilter) {
return overrideParams;
}
DataviewBackend.prototype.search = function (mapConfigProvider, user, params, callback) {
var dataviewName = params.dataviewName;
DataviewBackend.prototype.search = function (mapConfigProvider, user, dataviewName, params, callback) {
step(
function getMapConfig() {
mapConfigProvider.getMapConfig(this);

View File

@@ -21,10 +21,10 @@ function createTemplate(method) {
}
var methods = {
quantiles: 'CDB_QuantileBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as quantiles',
quantiles: 'CDB_QuantileBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as quantiles',
equal: 'CDB_EqualIntervalBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as equal',
jenks: 'CDB_JenksBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as jenks',
headtails: 'CDB_HeadsTailsBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as headtails'
jenks: 'CDB_JenksBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as jenks',
headtails: 'CDB_HeadsTailsBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as headtails'
};
var methodTemplates = Object.keys(methods).reduce(function(methodTemplates, methodName) {

View File

@@ -10,8 +10,10 @@ var BaseController = require('./base');
var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');
function AnalysesController(authApi, pgConnection) {
BaseController.call(this, authApi, pgConnection);
function AnalysesController(prepareContext) {
BaseController.call(this);
this.prepareContext = prepareContext;
}
util.inherits(AnalysesController, BaseController);
@@ -19,7 +21,13 @@ util.inherits(AnalysesController, BaseController);
module.exports = AnalysesController;
AnalysesController.prototype.register = function(app) {
app.get(app.base_url_mapconfig + '/analyses/catalog', cors(), userMiddleware, this.catalog.bind(this));
app.get(
app.base_url_mapconfig + '/analyses/catalog',
cors(),
userMiddleware,
this.prepareContext,
this.catalog.bind(this)
);
};
AnalysesController.prototype.sendResponse = function(req, res, resource) {
@@ -27,17 +35,13 @@ AnalysesController.prototype.sendResponse = function(req, res, resource) {
this.send(req, res, resource, 200);
};
AnalysesController.prototype.catalog = function(req, res) {
AnalysesController.prototype.catalog = function (req, res, next) {
var self = this;
var username = req.context.user;
var username = res.locals.user;
step(
function reqParams() {
self.req2params(req, this);
},
function catalogQuery(err) {
assert.ifError(err);
var pg = new PSQL(dbParamsFromReqParams(req.params));
function catalogQuery() {
var pg = new PSQL(dbParamsFromReqParams(res.locals));
getMetadata(username, pg, this);
},
function prepareResponse(err, results) {
@@ -80,7 +84,8 @@ AnalysesController.prototype.catalog = function(req, res) {
err = new Error('Unauthorized');
err.http_status = 401;
}
self.sendError(req, res, err);
next(req, res, err);
} else {
self.sendResponse(req, res, { catalog: catalogWithTables });
}

View File

@@ -1,164 +1,9 @@
var assert = require('assert');
var _ = require('underscore');
var step = require('step');
var debug = require('debug')('windshaft:cartodb');
var LZMA = require('lzma').LZMA;
var lzmaWorker = new LZMA();
// Whitelist query parameters and attach format
var REQUEST_QUERY_PARAMS_WHITELIST = [
'config',
'map_key',
'api_key',
'auth_token',
'callback',
'zoom',
'lon',
'lat',
// analysis
'filters' // json
];
function BaseController(authApi, pgConnection) {
this.authApi = authApi;
this.pgConnection = pgConnection;
function BaseController() {
}
module.exports = BaseController;
// jshint maxcomplexity:10
/**
* Whitelist input and get database name & default geometry type from
* subdomain/user metadata held in CartoDB Redis
* @param req - standard express request obj. Should have host & table
* @param callback
*/
BaseController.prototype.req2params = function(req, callback){
var self = this;
if ( req.query.lzma ) {
// 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) {
req.profiler.done('lzma');
try {
delete req.query.lzma;
_.extend(req.query, JSON.parse(result));
self.req2params(req, callback);
} catch (err) {
req.profiler.done('req2params');
callback(new Error('Error parsing lzma as JSON: ' + err));
}
}
);
return;
}
var allowedQueryParams = REQUEST_QUERY_PARAMS_WHITELIST;
if (Array.isArray(req.context.allowedQueryParams)) {
allowedQueryParams = allowedQueryParams.concat(req.context.allowedQueryParams);
}
req.query = _.pick(req.query, allowedQueryParams);
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
var user = req.context.user;
if ( req.params.token ) {
// Token might match the following patterns:
// - {user}@{tpl_id}@{token}:{cache_buster}
var tksplit = req.params.token.split(':');
req.params.token = tksplit[0];
if ( tksplit.length > 1 ) {
req.params.cache_buster= tksplit[1];
}
tksplit = req.params.token.split('@');
if ( tksplit.length > 1 ) {
req.params.signer = tksplit.shift();
if ( ! req.params.signer ) {
req.params.signer = user;
}
else if ( req.params.signer !== user ) {
var err = new Error(
'Cannot use map signature of user "' + req.params.signer + '" on db of user "' + user + '"'
);
err.http_status = 403;
req.profiler.done('req2params');
callback(err);
return;
}
if ( tksplit.length > 1 ) {
/*var template_hash = */tksplit.shift(); // unused
}
req.params.token = tksplit.shift();
}
}
// bring all query values onto req.params object
_.extend(req.params, req.query);
req.profiler.done('req2params.setup');
step(
function getPrivacy(){
self.authApi.authorize(req, this);
},
function validateAuthorization(err, authorized) {
req.profiler.done('authorize');
assert.ifError(err);
if(!authorized) {
err = new Error("Sorry, you are unauthorized (permission denied)");
err.http_status = 403;
throw err;
}
return null;
},
function getDatabase(err){
assert.ifError(err);
self.pgConnection.setDBConn(user, req.params, this);
},
function finishSetup(err) {
if ( err ) {
req.profiler.done('req2params');
return callback(err, req);
}
// Add default database connection parameters
// if none given
_.defaults(req.params, {
dbuser: global.environment.postgres.user,
dbpassword: global.environment.postgres.password,
dbhost: global.environment.postgres.host,
dbport: global.environment.postgres.port
});
req.profiler.done('req2params');
callback(null, req);
}
);
};
// jshint maxcomplexity:6
// jshint maxcomplexity:9
BaseController.prototype.send = function(req, res, body, status, headers) {
if (req.params.dbhost) {
res.set('X-Served-By-DB-Host', req.params.dbhost);
}
res.set('X-Tiler-Profiler', req.profiler.toJSONString());
if (headers) {
res.set(headers);
}
@@ -174,160 +19,4 @@ BaseController.prototype.send = function(req, res, body, status, headers) {
} else {
res.send(body);
}
try {
// May throw due to dns, see
// See http://github.com/CartoDB/Windshaft/issues/166
req.profiler.sendStats();
} catch (err) {
debug("error sending profiling stats: " + err);
}
};
// jshint maxcomplexity:6
BaseController.prototype.sendError = function(req, res, err, label) {
var allErrors = Array.isArray(err) ? err : [err];
allErrors = populateTimeoutErrors(allErrors);
label = label || 'UNKNOWN';
err = allErrors[0] || new Error(label);
allErrors[0] = err;
var statusCode = findStatusCode(err);
if (err.message === 'Tile does not exist' && req.params.format === 'mvt') {
statusCode = 204;
}
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
// If a callback was requested, force status to 200
if (req.query && req.query.callback) {
statusCode = 200;
}
var errorResponseBody = {
errors: allErrors.map(errorMessage),
errors_with_context: allErrors.map(errorMessageWithContext)
};
this.send(req, res, errorResponseBody, statusCode);
};
function stripConnectionInfo(message) {
// Strip connection info, if any
return message
// See https://github.com/CartoDB/Windshaft/issues/173
.replace(/Connection string: '[^']*'\n\s/im, '')
// See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644
.replace(/is the server.*encountered/im, 'encountered');
}
var ERROR_INFO_TO_EXPOSE = {
message: true,
layer: true,
type: true,
analysis: true,
subtype: true
};
function shouldBeExposed (prop) {
return !!ERROR_INFO_TO_EXPOSE[prop];
}
function errorMessage(err) {
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
return stripConnectionInfo(message);
}
function errorMessageWithContext(err) {
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
var error = {
type: err.type || 'unknown',
message: stripConnectionInfo(message),
};
for (var prop in err) {
// type & message are properties from Error's prototype and will be skipped
if (err.hasOwnProperty(prop) && shouldBeExposed(prop)) {
error[prop] = err[prop];
}
}
return error;
}
module.exports.errorMessage = errorMessage;
function findStatusCode(err) {
var statusCode;
if ( err.http_status ) {
statusCode = err.http_status;
} else {
statusCode = statusFromErrorMessage('' + err);
}
return statusCode;
}
module.exports.findStatusCode = findStatusCode;
function statusFromErrorMessage(errMsg) {
// Find an appropriate statusCode based on message
// jshint maxcomplexity:7
var statusCode = 400;
if ( -1 !== errMsg.indexOf('permission denied') ) {
statusCode = 403;
}
else if ( -1 !== errMsg.indexOf('authentication failed') ) {
statusCode = 403;
}
else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) {
statusCode = 400;
}
else if ( -1 !== errMsg.indexOf('does not exist') ) {
if ( -1 !== errMsg.indexOf(' role ') ) {
statusCode = 403; // role 'xxx' does not exist
} else if ( errMsg.match(/function .* does not exist/) ) {
statusCode = 400; // invalid SQL (SQL function does not exist)
} else {
statusCode = 404;
}
}
return statusCode;
}
function isRenderTimeoutError (err) {
return err.message === 'Render timed out';
}
function isDatasourceTimeoutError (err) {
return err.message && err.message.match(/canceling statement due to statement timeout/i);
}
function isTimeoutError (err) {
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
}
function populateTimeoutErrors (errors) {
return errors.map(function (error) {
if (isRenderTimeoutError(error)) {
error.subtype = 'render';
}
if (isDatasourceTimeoutError(error)) {
error.subtype = 'datasource';
}
if (isTimeoutError(error)) {
error.message = 'You are over platform\'s limits. Please contact us to know more details';
error.type = 'limit';
error.http_status = 429;
}
return error;
});
}

View File

@@ -28,9 +28,9 @@ var QueryTables = require('cartodb-query-tables');
* @param {AnalysisBackend} analysisBackend
* @constructor
*/
function LayergroupController(authApi, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
function LayergroupController(prepareContext, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, analysisBackend) {
BaseController.call(this, authApi, pgConnection);
BaseController.call(this);
this.pgConnection = pgConnection;
this.mapStore = mapStore;
@@ -43,39 +43,65 @@ function LayergroupController(authApi, pgConnection, mapStore, tileBackend, prev
this.dataviewBackend = new DataviewBackend(analysisBackend);
this.analysisStatusBackend = new AnalysisStatusBackend();
this.prepareContext = prepareContext;
}
util.inherits(LayergroupController, BaseController);
module.exports = LayergroupController;
LayergroupController.prototype.register = function(app) {
app.get(app.base_url_mapconfig +
'/:token/:z/:x/:y@:scale_factor?x.:format', cors(), userMiddleware,
this.tile.bind(this));
app.get(
app.base_url_mapconfig + '/:token/:z/:x/:y@:scale_factor?x.:format',
cors(),
userMiddleware,
this.prepareContext,
this.tile.bind(this)
);
app.get(app.base_url_mapconfig +
'/:token/:z/:x/:y.:format', cors(), userMiddleware,
this.tile.bind(this));
app.get(
app.base_url_mapconfig + '/:token/:z/:x/:y.:format',
cors(),
userMiddleware,
this.prepareContext,
this.tile.bind(this)
);
app.get(app.base_url_mapconfig +
'/:token/:layer/:z/:x/:y.(:format)', cors(), userMiddleware,
this.layer.bind(this));
app.get(
app.base_url_mapconfig + '/:token/:layer/:z/:x/:y.(:format)',
cors(),
userMiddleware,
validateLayerRouteMiddleware,
this.prepareContext,
this.layer.bind(this)
);
app.get(app.base_url_mapconfig +
'/:token/:layer/attributes/:fid', cors(), userMiddleware,
this.attributes.bind(this));
app.get(
app.base_url_mapconfig + '/:token/:layer/attributes/:fid',
cors(),
userMiddleware,
this.prepareContext,
this.attributes.bind(this)
);
app.get(app.base_url_mapconfig +
'/static/center/:token/:z/:lat/:lng/:width/:height.:format',
cors(), userMiddleware, allowQueryParams(['layer']),
this.center.bind(this));
app.get(
app.base_url_mapconfig + '/static/center/:token/:z/:lat/:lng/:width/:height.:format',
cors(),
userMiddleware,
allowQueryParams(['layer']),
this.prepareContext,
this.center.bind(this)
);
app.get(app.base_url_mapconfig +
'/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format',
cors(), userMiddleware, allowQueryParams(['layer']),
this.bbox.bind(this));
app.get(
app.base_url_mapconfig + '/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format',
cors(),
userMiddleware,
allowQueryParams(['layer']),
this.prepareContext,
this.bbox.bind(this)
);
// Undocumented/non-supported API endpoint methods.
// Use at your own peril.
@@ -98,6 +124,7 @@ LayergroupController.prototype.register = function(app) {
cors(),
userMiddleware,
allowQueryParams(allowedDataviewQueryParams),
this.prepareContext,
this.dataview.bind(this)
);
@@ -106,6 +133,7 @@ LayergroupController.prototype.register = function(app) {
cors(),
userMiddleware,
allowQueryParams(allowedDataviewQueryParams),
this.prepareContext,
this.dataview.bind(this)
);
@@ -114,6 +142,7 @@ LayergroupController.prototype.register = function(app) {
cors(),
userMiddleware,
allowQueryParams(allowedDataviewQueryParams),
this.prepareContext,
this.dataviewSearch.bind(this)
);
@@ -122,30 +151,32 @@ LayergroupController.prototype.register = function(app) {
cors(),
userMiddleware,
allowQueryParams(allowedDataviewQueryParams),
this.prepareContext,
this.dataviewSearch.bind(this)
);
app.get(app.base_url_mapconfig +
'/:token/analysis/node/:nodeId', cors(), userMiddleware,
this.analysisNodeStatus.bind(this));
app.get(
app.base_url_mapconfig + '/:token/analysis/node/:nodeId',
cors(),
userMiddleware,
this.prepareContext,
this.analysisNodeStatus.bind(this)
);
};
LayergroupController.prototype.analysisNodeStatus = function(req, res) {
LayergroupController.prototype.analysisNodeStatus = function(req, res, next) {
var self = this;
step(
function setupParams() {
self.req2params(req, this);
},
function retrieveNodeStatus(err) {
assert.ifError(err);
self.analysisStatusBackend.getNodeStatus(req.params, this);
function retrieveNodeStatus() {
self.analysisStatusBackend.getNodeStatus(res.locals, this);
},
function finish(err, nodeStatus, stats) {
req.profiler.add(stats || {});
if (err) {
self.sendError(req, res, err, 'GET NODE STATUS');
err.label = 'GET NODE STATUS';
next(err);
} else {
self.sendResponse(req, res, nodeStatus, 200, {
'Cache-Control': 'public,max-age=5',
@@ -156,54 +187,50 @@ LayergroupController.prototype.analysisNodeStatus = function(req, res) {
);
};
LayergroupController.prototype.dataview = function(req, res) {
LayergroupController.prototype.dataview = function(req, res, next) {
var self = this;
step(
function setupParams() {
self.req2params(req, this);
},
function retrieveDataview(err) {
assert.ifError(err);
function retrieveDataview() {
var mapConfigProvider = new MapStoreMapConfigProvider(
self.mapStore, req.context.user, self.userLimitsApi, req.params
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
);
self.dataviewBackend.getDataview(
mapConfigProvider,
res.locals.user,
res.locals,
this
);
self.dataviewBackend.getDataview(mapConfigProvider, req.context.user, req.params, this);
},
function finish(err, dataview, stats) {
req.profiler.add(stats || {});
if (err) {
self.sendError(req, res, err, 'GET DATAVIEW');
err.label = 'GET DATAVIEW';
next(err);
} else {
self.sendResponse(req, res, dataview, 200);
}
}
);
};
LayergroupController.prototype.dataviewSearch = function(req, res) {
LayergroupController.prototype.dataviewSearch = function(req, res, next) {
var self = this;
step(
function setupParams() {
self.req2params(req, this);
},
function searchDataview(err) {
assert.ifError(err);
function searchDataview() {
var mapConfigProvider = new MapStoreMapConfigProvider(
self.mapStore, req.context.user, self.userLimitsApi, req.params
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
);
self.dataviewBackend.search(mapConfigProvider, req.context.user, req.params, this);
self.dataviewBackend.search(mapConfigProvider, res.locals.user, req.params.dataviewName, res.locals, this);
},
function finish(err, searchResult, stats) {
req.profiler.add(stats || {});
if (err) {
self.sendError(req, res, err, 'GET DATAVIEW SEARCH');
err.label = 'GET DATAVIEW SEARCH';
next(err);
} else {
self.sendResponse(req, res, searchResult, 200);
}
@@ -212,28 +239,24 @@ LayergroupController.prototype.dataviewSearch = function(req, res) {
};
LayergroupController.prototype.attributes = function(req, res) {
LayergroupController.prototype.attributes = function(req, res, next) {
var self = this;
req.profiler.start('windshaft.maplayer_attribute');
step(
function setupParams() {
self.req2params(req, this);
},
function retrieveFeatureAttributes(err) {
assert.ifError(err);
function retrieveFeatureAttributes() {
var mapConfigProvider = new MapStoreMapConfigProvider(
self.mapStore, req.context.user, self.userLimitsApi, req.params
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
);
self.attributesBackend.getFeatureAttributes(mapConfigProvider, req.params, false, this);
self.attributesBackend.getFeatureAttributes(mapConfigProvider, res.locals, false, this);
},
function finish(err, tile, stats) {
req.profiler.add(stats || {});
if (err) {
self.sendError(req, res, err, 'GET ATTRIBUTES');
err.label = 'GET ATTRIBUTES';
next(err);
} else {
self.sendResponse(req, res, tile, 200);
}
@@ -243,44 +266,41 @@ LayergroupController.prototype.attributes = function(req, res) {
};
// Gets a tile for a given token and set of tile ZXY coords. (OSM style)
LayergroupController.prototype.tile = function(req, res) {
LayergroupController.prototype.tile = function(req, res, next) {
req.profiler.start('windshaft.map_tile');
this.tileOrLayer(req, res);
this.tileOrLayer(req, res, next);
};
// Gets a tile for a given token, layer set of tile ZXY coords. (OSM style)
LayergroupController.prototype.layer = function(req, res, next) {
if (req.params.token === 'static') {
return next();
}
req.profiler.start('windshaft.maplayer_tile');
this.tileOrLayer(req, res);
this.tileOrLayer(req, res, next);
};
LayergroupController.prototype.tileOrLayer = function (req, res) {
LayergroupController.prototype.tileOrLayer = function (req, res, next) {
var self = this;
step(
function mapController$prepareParams() {
self.req2params(req, this);
},
function mapController$getTileOrGrid(err) {
assert.ifError(err);
function mapController$getTileOrGrid() {
self.tileBackend.getTile(
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals),
req.params, this
);
},
function mapController$finalize(err, tile, headers, stats) {
req.profiler.add(stats);
self.finalizeGetTileOrGrid(err, req, res, tile, headers);
self.finalizeGetTileOrGrid(err, req, res, tile, headers, next);
}
);
};
function getStatusCode(tile, format){
return tile.length===0 && format==='mvt'? 204:200;
}
// This function is meant for being called as the very last
// step by all endpoints serving tiles or grids
LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers) {
LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers, next) {
var supportedFormats = {
grid_json: true,
json_torque: true,
@@ -309,52 +329,50 @@ LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, t
}
err.message = errMsg;
this.sendError(req, res, err, 'TILE RENDER');
err.label = 'TILE RENDER';
next(err);
global.statsClient.increment('windshaft.tiles.error');
global.statsClient.increment('windshaft.tiles.' + formatStat + '.error');
} else {
this.sendResponse(req, res, tile, 200, headers);
this.sendResponse(req, res, tile, getStatusCode(tile, formatStat), headers);
global.statsClient.increment('windshaft.tiles.success');
global.statsClient.increment('windshaft.tiles.' + formatStat + '.success');
}
};
LayergroupController.prototype.bbox = function(req, res) {
LayergroupController.prototype.bbox = function(req, res, next) {
this.staticMap(req, res, +req.params.width, +req.params.height, {
west: +req.params.west,
north: +req.params.north,
east: +req.params.east,
south: +req.params.south
});
}, null, next);
};
LayergroupController.prototype.center = function(req, res) {
LayergroupController.prototype.center = function(req, res, next) {
this.staticMap(req, res, +req.params.width, +req.params.height, +req.params.z, {
lng: +req.params.lng,
lat: +req.params.lat
});
}, next);
};
LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center) {
LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center, next) {
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
req.params.layer = 'all';
req.params.format = 'png';
req.params.format = req.params.format || 'png';
res.locals.layer = res.locals.layer || 'all';
var self = this;
step(
function reqParams() {
self.req2params(req, this);
},
function getImage(err) {
assert.ifError(err);
function getImage() {
if (center) {
self.previewBackend.getImage(
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals),
format, width, height, zoom, center, this);
} else {
self.previewBackend.getImage(
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals),
format, width, height, zoom /* bounds */, this);
}
},
@@ -363,7 +381,8 @@ LayergroupController.prototype.staticMap = function(req, res, width, height, zoo
req.profiler.add(stats || {});
if (err) {
self.sendError(req, res, err, 'STATIC_MAP');
err.label = 'STATIC_MAP';
next(err);
} else {
res.set('Content-Type', headers['Content-Type'] || 'image/' + format);
self.sendResponse(req, res, image, 200);
@@ -381,18 +400,18 @@ LayergroupController.prototype.sendResponse = function(req, res, body, status, h
// Set Last-Modified header
var lastUpdated;
if (req.params.cache_buster) {
if (res.locals.cache_buster) {
// Assuming cache_buster is a timestamp
lastUpdated = new Date(parseInt(req.params.cache_buster));
lastUpdated = new Date(parseInt(res.locals.cache_buster));
} else {
lastUpdated = new Date();
}
res.set('Last-Modified', lastUpdated.toUTCString());
var dbName = req.params.dbname;
var dbName = res.locals.dbname;
step(
function getAffectedTables() {
self.getAffectedTables(req.context.user, dbName, req.params.token, this);
self.getAffectedTables(res.locals.user, dbName, res.locals.token, this);
},
function sendResponse(err, affectedTables) {
req.profiler.done('affectedTables');
@@ -468,3 +487,12 @@ LayergroupController.prototype.getAffectedTables = function(user, dbName, layerg
callback
);
};
function validateLayerRouteMiddleware(req, res, next) {
if (req.params.token === 'static') {
return next('route');
}
next();
}

View File

@@ -20,7 +20,6 @@ var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
var NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider');
var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/create-layergroup-provider');
/**
* @param {AuthApi} authApi
* @param {PgConnection} pgConnection
@@ -34,11 +33,11 @@ var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/cr
* @param {StatsBackend} statsBackend
* @constructor
*/
function MapController(authApi, pgConnection, templateMaps, mapBackend, metadataBackend,
function MapController(prepareContext, pgConnection, templateMaps, mapBackend, metadataBackend,
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, mapConfigAdapter,
statsBackend) {
BaseController.call(this, authApi, pgConnection);
BaseController.call(this);
this.pgConnection = pgConnection;
this.templateMaps = templateMaps;
@@ -52,6 +51,7 @@ function MapController(authApi, pgConnection, templateMaps, mapBackend, metadata
this.resourceLocator = new ResourceLocator(global.environment);
this.statsBackend = statsBackend;
this.prepareContext = prepareContext;
}
util.inherits(MapController, BaseController);
@@ -60,38 +60,60 @@ module.exports = MapController;
MapController.prototype.register = function(app) {
app.get(app.base_url_mapconfig, cors(), userMiddleware, this.createGet.bind(this));
app.post(app.base_url_mapconfig, cors(), userMiddleware, this.createPost.bind(this));
app.get(app.base_url_templated + '/:template_id/jsonp', cors(), userMiddleware, this.jsonp.bind(this));
app.post(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.instantiate.bind(this));
app.get(
app.base_url_mapconfig,
cors(),
userMiddleware,
this.prepareContext,
this.createGet.bind(this)
);
app.post(
app.base_url_mapconfig,
cors(),
userMiddleware,
this.prepareContext,
this.createPost.bind(this)
);
app.get(
app.base_url_templated + '/:template_id/jsonp',
cors(),
userMiddleware,
this.prepareContext,
this.jsonp.bind(this)
);
app.post(
app.base_url_templated + '/:template_id',
cors(),
userMiddleware,
this.prepareContext,
this.instantiate.bind(this)
);
app.options(app.base_url_mapconfig, cors('Content-Type'));
};
MapController.prototype.createGet = function(req, res){
MapController.prototype.createGet = function(req, res, next){
req.profiler.start('windshaft.createmap_get');
this.create(req, res, function createGet$prepareConfig(err, req) {
assert.ifError(err);
if ( ! req.params.config ) {
this.create(req, res, function createGet$prepareConfig(req, config) {
if ( ! config ) {
throw new Error('layergroup GET needs a "config" parameter');
}
return JSON.parse(req.params.config);
});
return JSON.parse(config);
}, next);
};
MapController.prototype.createPost = function(req, res) {
MapController.prototype.createPost = function(req, res, next) {
req.profiler.start('windshaft.createmap_post');
this.create(req, res, function createPost$prepareConfig(err, req) {
assert.ifError(err);
this.create(req, res, function createPost$prepareConfig(req) {
if (!req.is('application/json')) {
throw new Error('layergroup POST data must be of type application/json');
}
return req.body;
});
}, next);
};
MapController.prototype.instantiate = function(req, res) {
MapController.prototype.instantiate = function(req, res, next) {
req.profiler.start('windshaft-cartodb.instance_template_post');
this.instantiateTemplate(req, res, function prepareTemplateParams(callback) {
@@ -99,10 +121,10 @@ MapController.prototype.instantiate = function(req, res) {
return callback(new Error('Template POST data must be of type application/json'));
}
return callback(null, req.body);
});
}, next);
};
MapController.prototype.jsonp = function(req, res) {
MapController.prototype.jsonp = function(req, res, next) {
req.profiler.start('windshaft-cartodb.instance_template_get');
this.instantiateTemplate(req, res, function prepareJsonTemplateParams(callback) {
@@ -121,10 +143,10 @@ MapController.prototype.jsonp = function(req, res) {
}
return callback(err, templateParams);
});
}, next);
};
MapController.prototype.create = function(req, res, prepareConfigFn) {
MapController.prototype.create = function(req, res, prepareConfigFn, next) {
var self = this;
var mapConfig;
@@ -132,35 +154,36 @@ MapController.prototype.create = function(req, res, prepareConfigFn) {
var context = {};
step(
function setupParams(){
self.req2params(req, this);
function prepareConfig () {
const requestMapConfig = prepareConfigFn(req, res.locals.config);
return requestMapConfig;
},
prepareConfigFn,
function prepareAdapterMapConfig(err, requestMapConfig) {
assert.ifError(err);
context.analysisConfiguration = {
user: req.context.user,
user: res.locals.user,
db: {
host: req.params.dbhost,
port: req.params.dbport,
dbname: req.params.dbname,
user: req.params.dbuser,
pass: req.params.dbpassword
host: res.locals.dbhost,
port: res.locals.dbport,
dbname: res.locals.dbname,
user: res.locals.dbuser,
pass: res.locals.dbpassword
},
batch: {
username: req.context.user,
apiKey: req.params.api_key
username: res.locals.user,
apiKey: res.locals.api_key
}
};
self.mapConfigAdapter.getMapConfig(req.context.user, requestMapConfig, req.params, context, this);
self.mapConfigAdapter.getMapConfig(res.locals.user, requestMapConfig, res.locals, context, this);
},
function createLayergroup(err, requestMapConfig) {
assert.ifError(err);
var datasource = context.datasource || Datasource.EmptyDatasource();
mapConfig = new MapConfig(requestMapConfig, datasource);
self.mapBackend.createLayergroup(
mapConfig, req.params,
new CreateLayergroupMapConfigProvider(mapConfig, req.context.user, self.userLimitsApi, req.params),
mapConfig,
res.locals,
new CreateLayergroupMapConfigProvider(mapConfig, res.locals.user, self.userLimitsApi, res.locals),
this
);
},
@@ -188,11 +211,12 @@ MapController.prototype.create = function(req, res, prepareConfigFn) {
err = error;
}
self.sendError(req, res, err, 'ANONYMOUS LAYERGROUP');
err.label = 'ANONYMOUS LAYERGROUP';
next(err);
} else {
var analysesResults = context.analysesResults || [];
self.addDataviewsAndWidgetsUrls(req.context.user, layergroup, mapConfig.obj());
self.addAnalysesMetadata(req.context.user, layergroup, analysesResults, true);
self.addDataviewsAndWidgetsUrls(res.locals.user, layergroup, mapConfig.obj());
self.addAnalysesMetadata(res.locals.user, layergroup, analysesResults, true);
addContextMetadata(layergroup, mapConfig.obj(), context);
res.set('X-Layergroup-Id', layergroup.layergroupid);
self.send(req, res, layergroup, 200);
@@ -212,17 +236,14 @@ function addContextMetadata(layergroup, mapConfig, context) {
}
}
MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn) {
MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn, next) {
var self = this;
var cdbuser = req.context.user;
var cdbuser = res.locals.user;
var mapConfigProvider;
var mapConfig;
step(
function setupParams(){
self.req2params(req, this);
},
function getTemplateParams() {
prepareParamsFn(this);
},
@@ -237,8 +258,8 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
cdbuser,
req.params.template_id,
templateParams,
req.query.auth_token,
req.params
res.locals.auth_token,
res.locals
);
mapConfigProvider.getMapConfig(this);
},
@@ -259,7 +280,8 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
},
function finishTemplateInstantiation(err, layergroup) {
if (err) {
self.sendError(req, res, err, 'NAMED MAP LAYERGROUP');
err.label = 'NAMED MAP LAYERGROUP';
next(err);
} else {
var templateHash = self.templateMaps.fingerPrint(mapConfigProvider.template).substring(0, 8);
layergroup.layergroupid = cdbuser + '@' + templateHash + '@' + layergroup.layergroupid;
@@ -282,7 +304,7 @@ MapController.prototype.afterLayergroupCreate =
function(req, res, mapconfig, layergroup, analysesResults, callback) {
var self = this;
var username = req.context.user;
var username = res.locals.user;
var tasksleft = 2; // redis key and affectedTables
var errors = [];
@@ -323,7 +345,7 @@ function(req, res, mapconfig, layergroup, analysesResults, callback) {
}
});
var dbName = req.params.dbname;
var dbName = res.locals.dbname;
var layergroupId = layergroup.layergroupid;
var dbConnection;

View File

@@ -10,9 +10,9 @@ var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');
var allowQueryParams = require('../middleware/allow-query-params');
function NamedMapsController(authApi, pgConnection, namedMapProviderCache, tileBackend, previewBackend,
function NamedMapsController(prepareContext, namedMapProviderCache, tileBackend, previewBackend,
surrogateKeysCache, tablesExtentApi, metadataBackend) {
BaseController.call(this, authApi, pgConnection);
BaseController.call(this);
this.namedMapProviderCache = namedMapProviderCache;
this.tileBackend = tileBackend;
@@ -20,6 +20,7 @@ function NamedMapsController(authApi, pgConnection, namedMapProviderCache, tileB
this.surrogateKeysCache = surrogateKeysCache;
this.tablesExtentApi = tablesExtentApi;
this.metadataBackend = metadataBackend;
this.prepareContext = prepareContext;
}
util.inherits(NamedMapsController, BaseController);
@@ -27,17 +28,26 @@ util.inherits(NamedMapsController, BaseController);
module.exports = NamedMapsController;
NamedMapsController.prototype.register = function(app) {
app.get(app.base_url_templated +
'/:template_id/:layer/:z/:x/:y.(:format)', cors(), userMiddleware,
this.tile.bind(this));
app.get(
app.base_url_templated + '/:template_id/:layer/:z/:x/:y.(:format)',
cors(),
userMiddleware,
this.prepareContext,
this.tile.bind(this)
);
app.get(app.base_url_mapconfig +
'/static/named/:template_id/:width/:height.:format', cors(), userMiddleware, allowQueryParams(['layer']),
this.staticMap.bind(this));
app.get(
app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format',
cors(),
userMiddleware,
allowQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
this.prepareContext,
this.staticMap.bind(this)
);
};
NamedMapsController.prototype.sendResponse = function(req, res, resource, headers, namedMapProvider) {
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(req.context.user, namedMapProvider.getTemplateName()));
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');
@@ -74,24 +84,20 @@ NamedMapsController.prototype.sendResponse = function(req, res, resource, header
);
};
NamedMapsController.prototype.tile = function(req, res) {
NamedMapsController.prototype.tile = function(req, res, next) {
var self = this;
var cdbUser = req.context.user;
var cdbUser = res.locals.user;
var namedMapProvider;
step(
function reqParams() {
self.req2params(req, this);
},
function getNamedMapProvider(err) {
assert.ifError(err);
function getNamedMapProvider() {
self.namedMapProviderCache.get(
cdbUser,
req.params.template_id,
req.query.config,
req.query.auth_token,
req.params,
res.locals,
this
);
},
@@ -103,7 +109,8 @@ NamedMapsController.prototype.tile = function(req, res) {
function handleImage(err, tile, headers, stats) {
req.profiler.add(stats);
if (err) {
self.sendError(req, res, err, 'NAMED_MAP_TILE');
err.label = 'NAMED_MAP_TILE';
next(err);
} else {
self.sendResponse(req, res, tile, headers, namedMapProvider);
}
@@ -111,28 +118,24 @@ NamedMapsController.prototype.tile = function(req, res) {
);
};
NamedMapsController.prototype.staticMap = function(req, res) {
NamedMapsController.prototype.staticMap = function(req, res, next) {
var self = this;
var cdbUser = req.context.user;
var cdbUser = res.locals.user;
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
req.params.format = 'png';
req.params.layer = 'all';
res.locals.format = req.params.format || 'png';
res.locals.layer = res.locals.layer || 'all';
var namedMapProvider;
step(
function reqParams() {
self.req2params(req, this);
},
function getNamedMapProvider(err) {
assert.ifError(err);
function getNamedMapProvider() {
self.namedMapProviderCache.get(
cdbUser,
req.params.template_id,
req.query.config,
req.query.auth_token,
req.params,
res.locals,
this
);
},
@@ -140,12 +143,12 @@ NamedMapsController.prototype.staticMap = function(req, res) {
assert.ifError(err);
namedMapProvider = _namedMapProvider;
self.prepareLayerFilterFromPreviewLayers(cdbUser, req, namedMapProvider, this);
self.prepareLayerFilterFromPreviewLayers(cdbUser, req, res.locals, namedMapProvider, this);
},
function prepareImageOptions(err) {
assert.ifError(err);
self.getStaticImageOptions(cdbUser, req.params, namedMapProvider, this);
self.getStaticImageOptions(cdbUser, res.locals, namedMapProvider, this);
},
function getImage(err, imageOpts) {
assert.ifError(err);
@@ -179,7 +182,8 @@ NamedMapsController.prototype.staticMap = function(req, res) {
req.profiler.add(stats || {});
if (err) {
self.sendError(req, res, err, 'STATIC_VIZ_MAP');
err.label = 'STATIC_VIZ_MAP';
next(err);
} else {
self.sendResponse(req, res, image, headers, namedMapProvider);
}
@@ -187,7 +191,13 @@ NamedMapsController.prototype.staticMap = function(req, res) {
);
};
NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (user, req, namedMapProvider, callback) {
NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (
user,
req,
params,
namedMapProvider,
callback
) {
var self = this;
namedMapProvider.getTemplate(function (err, template) {
if (err) {
@@ -212,7 +222,7 @@ NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (us
}
// overwrites 'all' default filter
req.params.layer = layerVisibilityFilter.join(',');
params.layer = layerVisibilityFilter.join(',');
// recreates the provider
self.namedMapProviderCache.get(
@@ -220,7 +230,7 @@ NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (us
req.params.template_id,
req.query.config,
req.query.auth_token,
req.params,
params,
callback
);
});

View File

@@ -15,8 +15,8 @@ var userMiddleware = require('../middleware/user');
* @param {TemplateMaps} templateMaps
* @constructor
*/
function NamedMapsAdminController(authApi, pgConnection, templateMaps) {
BaseController.call(this, authApi, pgConnection);
function NamedMapsAdminController(authApi, templateMaps) {
BaseController.call(this);
this.authApi = authApi;
this.templateMaps = templateMaps;
@@ -26,19 +26,52 @@ util.inherits(NamedMapsAdminController, BaseController);
module.exports = NamedMapsAdminController;
NamedMapsAdminController.prototype.register = function(app) {
app.post(app.base_url_templated, cors(), userMiddleware, this.create.bind(this));
app.put(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.update.bind(this));
app.get(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.retrieve.bind(this));
app.delete(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.destroy.bind(this));
app.get(app.base_url_templated, cors(), userMiddleware, this.list.bind(this));
app.options(app.base_url_templated + '/:template_id', cors('Content-Type'));
NamedMapsAdminController.prototype.register = function (app) {
app.post(
app.base_url_templated + '/',
cors(),
userMiddleware,
this.create.bind(this)
);
app.put(
app.base_url_templated + '/:template_id',
cors(),
userMiddleware,
this.update.bind(this)
);
app.get(
app.base_url_templated + '/:template_id',
cors(),
userMiddleware,
this.retrieve.bind(this)
);
app.delete(
app.base_url_templated + '/:template_id',
cors(),
userMiddleware,
this.destroy.bind(this)
);
app.get(
app.base_url_templated + '/',
cors(),
userMiddleware,
this.list.bind(this)
);
app.options(
app.base_url_templated + '/:template_id',
cors('Content-Type')
);
};
NamedMapsAdminController.prototype.create = function(req, res) {
NamedMapsAdminController.prototype.create = function(req, res, next) {
var self = this;
var cdbuser = req.context.user;
var cdbuser = res.locals.user;
step(
function checkPerms(){
@@ -55,14 +88,14 @@ NamedMapsAdminController.prototype.create = function(req, res) {
assert.ifError(err);
return { template_id: tpl_id };
},
finishFn(self, req, res, 'POST TEMPLATE')
finishFn(self, req, res, 'POST TEMPLATE', null, next)
);
};
NamedMapsAdminController.prototype.update = function(req, res) {
NamedMapsAdminController.prototype.update = function(req, res, next) {
var self = this;
var cdbuser = req.context.user;
var cdbuser = res.locals.user;
var template;
var tpl_id;
@@ -84,16 +117,16 @@ NamedMapsAdminController.prototype.update = function(req, res) {
return { template_id: tpl_id };
},
finishFn(self, req, res, 'PUT TEMPLATE')
finishFn(self, req, res, 'PUT TEMPLATE', null, next)
);
};
NamedMapsAdminController.prototype.retrieve = function(req, res) {
NamedMapsAdminController.prototype.retrieve = function(req, res, next) {
var self = this;
req.profiler.start('windshaft-cartodb.get_template');
var cdbuser = req.context.user;
var cdbuser = res.locals.user;
var tpl_id;
step(
function checkPerms(){
@@ -118,16 +151,16 @@ NamedMapsAdminController.prototype.retrieve = function(req, res) {
delete tpl_val.auth_id;
return { template: tpl_val };
},
finishFn(self, req, res, 'GET TEMPLATE')
finishFn(self, req, res, 'GET TEMPLATE', null, next)
);
};
NamedMapsAdminController.prototype.destroy = function(req, res) {
NamedMapsAdminController.prototype.destroy = function(req, res, next) {
var self = this;
req.profiler.start('windshaft-cartodb.delete_template');
var cdbuser = req.context.user;
var cdbuser = res.locals.user;
var tpl_id;
step(
function checkPerms(){
@@ -144,15 +177,15 @@ NamedMapsAdminController.prototype.destroy = function(req, res) {
assert.ifError(err);
return '';
},
finishFn(self, req, res, 'DELETE TEMPLATE', 204)
finishFn(self, req, res, 'DELETE TEMPLATE', 204, next)
);
};
NamedMapsAdminController.prototype.list = function(req, res) {
NamedMapsAdminController.prototype.list = function(req, res, next) {
var self = this;
req.profiler.start('windshaft-cartodb.get_template_list');
var cdbuser = req.context.user;
var cdbuser = res.locals.user;
step(
function checkPerms(){
@@ -168,14 +201,15 @@ NamedMapsAdminController.prototype.list = function(req, res) {
assert.ifError(err);
return { template_ids: tpl_ids };
},
finishFn(self, req, res, 'GET TEMPLATE LIST')
finishFn(self, req, res, 'GET TEMPLATE LIST', null, next)
);
};
function finishFn(controller, req, res, description, status) {
function finishFn(controller, req, res, description, status, next) {
return function finish(err, response){
if (err) {
controller.sendError(req, res, err, description);
err.label = description;
next(err);
} else {
controller.send(req, res, response, status || 200);
}

View File

@@ -3,7 +3,7 @@ module.exports = function allowQueryParams(params) {
throw new Error('allowQueryParams must receive an Array of params');
}
return function allowQueryParamsMiddleware(req, res, next) {
req.context.allowedQueryParams = params;
res.locals.allowedQueryParams = params;
next();
};
};

View File

@@ -0,0 +1,20 @@
module.exports = function authorizeMiddleware (authApi) {
return function (req, res, next) {
req.profiler.done('req2params.setup');
authApi.authorize(req, res, (err, authorized) => {
req.profiler.done('authorize');
if (err) {
return next(err);
}
if(!authorized) {
err = new Error("Sorry, you are unauthorized (permission denied)");
err.http_status = 403;
return next(err);
}
return next();
});
};
};

View File

@@ -0,0 +1,32 @@
const _ = require('underscore');
// Whitelist query parameters and attach format
const REQUEST_QUERY_PARAMS_WHITELIST = [
'config',
'map_key',
'api_key',
'auth_token',
'callback',
'zoom',
'lon',
'lat',
// analysis
'filters' // json
];
module.exports = function cleanUpQueryParamsMiddleware () {
return function cleanUpQueryParams (req, res, next) {
var allowedQueryParams = REQUEST_QUERY_PARAMS_WHITELIST;
if (Array.isArray(res.locals.allowedQueryParams)) {
allowedQueryParams = allowedQueryParams.concat(res.locals.allowedQueryParams);
}
req.query = _.pick(req.query, allowedQueryParams);
// bring all query values onto res.locals object
_.extend(res.locals, req.query);
next();
};
};

View File

@@ -0,0 +1,31 @@
const _ = require('underscore');
module.exports = function dbConnSetupMiddleware(pgConnection) {
return function dbConnSetup(req, res, next) {
const user = res.locals.user;
pgConnection.setDBConn(user, res.locals, (err) => {
if (err) {
if (err.message && -1 !== err.message.indexOf('name not found')) {
err.http_status = 404;
}
req.profiler.done('req2params');
return next(err);
}
// Add default database connection parameters
// if none given
_.defaults(res.locals, {
dbuser: global.environment.postgres.user,
dbpassword: global.environment.postgres.password,
dbhost: global.environment.postgres.host,
dbport: global.environment.postgres.port
});
res.set('X-Served-By-DB-Host', res.locals.dbhost);
req.profiler.done('req2params');
next(null);
});
};
};

View File

@@ -0,0 +1,15 @@
const locals = require('./locals');
const cleanUpQueryParams = require('./clean-up-query-params');
const layergroupToken = require('./layergroup-token');
const authorize = require('./authorize');
const dbConnSetup = require('./db-conn-setup');
module.exports = function prepareContextMiddleware(authApi, pgConnection) {
return [
locals,
cleanUpQueryParams(),
layergroupToken,
authorize(authApi),
dbConnSetup(pgConnection)
];
};

View File

@@ -0,0 +1,32 @@
var LayergroupToken = require('../../models/layergroup-token');
module.exports = function layergroupTokenMiddleware(req, res, next) {
if (!res.locals.token) {
return next();
}
var user = res.locals.user;
var layergroupToken = LayergroupToken.parse(res.locals.token);
res.locals.token = layergroupToken.token;
res.locals.cache_buster = layergroupToken.cacheBuster;
if (layergroupToken.signer) {
res.locals.signer = layergroupToken.signer;
if (!res.locals.signer) {
res.locals.signer = user;
} else if (res.locals.signer !== user) {
var err = new Error(`Cannot use map signature of user "${res.locals.signer}" on db of user "${user}"`);
err.type = 'auth';
err.http_status = 403;
if (req.query && req.query.callback) {
err.http_status = 200;
}
req.profiler.done('req2params');
return next(err);
}
}
return next();
};

View File

@@ -0,0 +1,8 @@
const _ = require('underscore');
module.exports = function localsMiddleware(req, res, next) {
_.extend(res.locals, req.params);
next();
};

View File

@@ -0,0 +1,162 @@
const _ = require('underscore');
const debug = require('debug')('windshaft:cartodb:error-middleware');
module.exports = function errorMiddleware (/* options */) {
return function error (err, req, res, next) {
// jshint unused:false
// jshint maxcomplexity:9
var allErrors = Array.isArray(err) ? err : [err];
allErrors = populateTimeoutErrors(allErrors);
const label = err.label || 'UNKNOWN';
err = allErrors[0] || new Error(label);
allErrors[0] = err;
var statusCode = findStatusCode(err);
if (err.message === 'Tile does not exist' && res.locals.format === 'mvt') {
statusCode = 204;
}
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
// If a callback was requested, force status to 200
if (req.query && req.query.callback) {
statusCode = 200;
}
var errorResponseBody = {
errors: allErrors.map(errorMessage),
errors_with_context: allErrors.map(errorMessageWithContext)
};
res.status(statusCode);
if (req.query && req.query.callback) {
res.jsonp(errorResponseBody);
} else {
res.json(errorResponseBody);
}
};
};
function isRenderTimeoutError (err) {
return err.message === 'Render timed out';
}
function isDatasourceTimeoutError (err) {
return err.message && err.message.match(/canceling statement due to statement timeout/i);
}
function isTimeoutError (err) {
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
}
function populateTimeoutErrors (errors) {
return errors.map(function (error) {
if (isRenderTimeoutError(error)) {
error.subtype = 'render';
}
if (isDatasourceTimeoutError(error)) {
error.subtype = 'datasource';
}
if (isTimeoutError(error)) {
error.message = 'You are over platform\'s limits. Please contact us to know more details';
error.type = 'limit';
error.http_status = 429;
}
return error;
});
}
function findStatusCode(err) {
var statusCode;
if ( err.http_status ) {
statusCode = err.http_status;
} else {
statusCode = statusFromErrorMessage('' + err);
}
return statusCode;
}
module.exports.findStatusCode = findStatusCode;
function statusFromErrorMessage(errMsg) {
// Find an appropriate statusCode based on message
// jshint maxcomplexity:7
var statusCode = 400;
if ( -1 !== errMsg.indexOf('permission denied') ) {
statusCode = 403;
}
else if ( -1 !== errMsg.indexOf('authentication failed') ) {
statusCode = 403;
}
else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) {
statusCode = 400;
}
else if ( -1 !== errMsg.indexOf('does not exist') ) {
if ( -1 !== errMsg.indexOf(' role ') ) {
statusCode = 403; // role 'xxx' does not exist
} else if ( errMsg.match(/function .* does not exist/) ) {
statusCode = 400; // invalid SQL (SQL function does not exist)
} else {
statusCode = 404;
}
}
return statusCode;
}
function errorMessage(err) {
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
return stripConnectionInfo(message);
}
module.exports.errorMessage = errorMessage;
function stripConnectionInfo(message) {
// Strip connection info, if any
return message
// See https://github.com/CartoDB/Windshaft/issues/173
.replace(/Connection string: '[^']*'\n\s/im, '')
// See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644
.replace(/is the server.*encountered/im, 'encountered');
}
var ERROR_INFO_TO_EXPOSE = {
message: true,
layer: true,
type: true,
analysis: true,
subtype: true
};
function shouldBeExposed (prop) {
return !!ERROR_INFO_TO_EXPOSE[prop];
}
function errorMessageWithContext(err) {
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
var error = {
type: err.type || 'unknown',
message: stripConnectionInfo(message),
};
for (var prop in err) {
// type & message are properties from Error's prototype and will be skipped
if (err.hasOwnProperty(prop) && shouldBeExposed(prop)) {
error[prop] = err[prop];
}
}
return error;
}

View File

@@ -0,0 +1,30 @@
'use strict';
const LZMA = require('lzma').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));
}
});
};

View File

@@ -0,0 +1,27 @@
const Profiler = require('../stats/profiler_proxy');
const debug = require('debug')('windshaft:cartodb:stats');
const onHeaders = require('on-headers');
module.exports = function statsMiddleware(options) {
const { enabled = true, statsClient } = options;
return function stats(req, res, next) {
req.profiler = new Profiler({
statsd_client: statsClient,
profile: enabled
});
onHeaders(res, () => res.set('X-Tiler-Profiler', req.profiler.toJSONString()));
res.on('finish', () => {
try {
// May throw due to dns, see: http://github.com/CartoDB/Windshaft/issues/166
req.profiler.sendStats();
} catch (err) {
debug("error sending profiling stats: " + err);
}
});
next();
};
};

View File

@@ -2,6 +2,13 @@ var CdbRequest = require('../models/cdb_request');
var cdbRequest = new CdbRequest();
module.exports = function userMiddleware(req, res, next) {
req.context.user = cdbRequest.userByReq(req);
res.locals.user = cdbRequest.userByReq(req);
// avoid a req.params.user equals to undefined
// overwrites res.locals.user
if(!req.params.user) {
delete req.params.user;
}
next();
};

View File

@@ -1,95 +1,178 @@
var _ = require('underscore');
var BaseWidget = require('./base');
var debug = require('debug')('windshaft:widget:aggregation');
const BaseDataview = require('./base');
const debug = require('debug')('windshaft:dataview:aggregation');
var dot = require('dot');
dot.templateSettings.strip = false;
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` :
''
}
)
`;
var filteredQueryTpl = dot.template([
'filtered_source AS (',
' SELECT *',
' FROM ({{=it._query}}) _cdb_filtered_source',
' {{?it._aggregationColumn && it._isFloatColumn}}WHERE',
' {{=it._aggregationColumn}} != \'infinity\'::float',
' AND',
' {{=it._aggregationColumn}} != \'-infinity\'::float',
' AND',
' {{=it._aggregationColumn}} != \'NaN\'::float{{?}}',
')'
].join(' \n'));
const summaryQueryTpl = ctx => `
summary AS (
SELECT
count(1) AS count,
sum(CASE WHEN ${ctx.column} IS NULL THEN 1 ELSE 0 END) AS nulls_count
${ctx.isFloatColumn ? `,
sum(
CASE
WHEN ${ctx.aggregationColumn} = 'infinity'::float OR ${ctx.aggregationColumn} = '-infinity'::float
THEN 1
ELSE 0
END
) AS infinities_count,
sum(CASE WHEN ${ctx.aggregationColumn} = 'NaN'::float THEN 1 ELSE 0 END) AS nans_count` :
''
}
FROM (${ctx.query}) _cdb_aggregation_nulls
)
`;
var summaryQueryTpl = dot.template([
'summary AS (',
' SELECT',
' count(1) AS count,',
' sum(CASE WHEN {{=it._column}} IS NULL THEN 1 ELSE 0 END) AS nulls_count',
' {{?it._isFloatColumn}},sum(',
' CASE',
' WHEN {{=it._aggregationColumn}} = \'infinity\'::float OR {{=it._aggregationColumn}} = \'-infinity\'::float',
' THEN 1',
' ELSE 0',
' END',
' ) AS infinities_count,',
' sum(CASE WHEN {{=it._aggregationColumn}} = \'NaN\'::float THEN 1 ELSE 0 END) AS nans_count{{?}}',
' FROM ({{=it._query}}) _cdb_aggregation_nulls',
')'
].join('\n'));
const rankedCategoriesQueryTpl = ctx => `
categories AS(
SELECT
${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` : ''}
GROUP BY ${ctx.column}
ORDER BY 2 DESC
)
`;
var rankedCategoriesQueryTpl = dot.template([
'categories AS(',
' SELECT {{=it._column}} AS category, {{=it._aggregation}} AS value,',
' row_number() OVER (ORDER BY {{=it._aggregation}} desc) as rank',
' FROM filtered_source',
' {{?it._aggregationColumn!==null}}WHERE {{=it._aggregationColumn}} IS NOT NULL{{?}}',
' GROUP BY {{=it._column}}',
' ORDER BY 2 DESC',
')'
].join('\n'));
const categoriesSummaryMinMaxQueryTpl = () => `
categories_summary_min_max AS(
SELECT
max(value) max_val,
min(value) min_val
FROM categories
)
`;
var categoriesSummaryMinMaxQueryTpl = dot.template([
'categories_summary_min_max AS(',
' SELECT max(value) max_val, min(value) min_val',
' FROM categories',
')'
].join('\n'));
const categoriesSummaryCountQueryTpl = ctx => `
categories_summary_count AS(
SELECT count(1) AS categories_count
FROM (
SELECT ${ctx.column} AS category
FROM (${ctx.query}) _cdb_categories
GROUP BY ${ctx.column}
) _cdb_categories_count
)
`;
var categoriesSummaryCountQueryTpl = dot.template([
'categories_summary_count AS(',
' SELECT count(1) AS categories_count',
' FROM (',
' SELECT {{=it._column}} AS category',
' FROM ({{=it._query}}) _cdb_categories',
' GROUP BY {{=it._column}}',
' ) _cdb_categories_count',
')'
].join('\n'));
const specialNumericValuesColumns = () => `, nans_count, infinities_count`;
var rankedAggregationQueryTpl = dot.template([
'SELECT CAST(category AS text), value, false as agg, nulls_count, min_val, max_val,',
' count, categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
' FROM categories, summary, categories_summary_min_max, categories_summary_count',
' WHERE rank < {{=it._limit}}',
'UNION ALL',
'SELECT \'Other\' category, {{=it._aggregationFn}}(value) as value, true as agg, nulls_count,',
' min_val, max_val, count, categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
' FROM categories, summary, categories_summary_min_max, categories_summary_count',
' WHERE rank >= {{=it._limit}}',
'GROUP BY nulls_count, min_val, max_val, count,',
' categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}'
].join('\n'));
const rankedAggregationQueryTpl = ctx => `
SELECT
CAST(category AS text),
value,
false as agg,
nulls_count,
min_val,
max_val,
count,
categories_count
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
FROM categories, summary, categories_summary_min_max, categories_summary_count
WHERE rank < ${ctx.limit}
UNION ALL
SELECT
'Other' category,
${ctx.aggregation !== 'count' ? ctx.aggregation : 'sum'}(value) as value,
true as agg,
nulls_count,
min_val,
max_val,
count,
categories_count
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
FROM categories, summary, categories_summary_min_max, categories_summary_count
WHERE rank >= ${ctx.limit}
GROUP BY
nulls_count,
min_val,
max_val,
count,
categories_count
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
`;
var aggregationQueryTpl = dot.template([
'SELECT CAST({{=it._column}} AS text) AS category, {{=it._aggregation}} AS value, false as agg,',
' nulls_count, min_val, max_val, count, categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
'FROM ({{=it._query}}) _cdb_aggregation_all, summary, categories_summary_min_max, categories_summary_count',
'GROUP BY category, nulls_count, min_val, max_val, count,',
' categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
'ORDER BY value DESC'
].join('\n'));
const aggregationQueryTpl = ctx => `
SELECT
CAST(${ctx.column} AS text) AS category,
${ctx.aggregationFn} AS value,
false as agg,
nulls_count,
min_val,
max_val,
count,
categories_count
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
FROM (${ctx.query}) _cdb_aggregation_all, summary, categories_summary_min_max, categories_summary_count
GROUP BY
category,
nulls_count,
min_val,
max_val,
count,
categories_count
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
ORDER BY value DESC
`;
var CATEGORIES_LIMIT = 6;
const aggregationFnQueryTpl = ctx => `${ctx.aggregation}(${ctx.aggregationColumn})`;
var VALID_OPERATIONS = {
const aggregationDataviewQueryTpl = ctx => `
WITH
${filteredQueryTpl(ctx)},
${summaryQueryTpl(ctx)},
${rankedCategoriesQueryTpl(ctx)},
${categoriesSummaryMinMaxQueryTpl(ctx)},
${categoriesSummaryCountQueryTpl(ctx)}
${!!ctx.override.ownFilter ? `${aggregationQueryTpl(ctx)}` : `${rankedAggregationQueryTpl(ctx)}`}
`;
const filterCategoriesQueryTpl = ctx => `
SELECT
${ctx.column} AS category,
${ctx.value} AS value
FROM (${ctx.query}) _cdb_aggregation_search
WHERE CAST(${ctx.column} as text) ILIKE ${ctx.userQuery}
GROUP BY ${ctx.column}
`;
const searchQueryTpl = ctx => `
WITH
search_unfiltered AS (
${ctx.searchUnfiltered}
),
search_filtered AS (
${ctx.searchFiltered}
),
search_union AS (
SELECT * FROM search_unfiltered
UNION ALL
SELECT * FROM search_filtered
)
SELECT category, sum(value) AS value
FROM search_union
GROUP BY category
ORDER BY value desc
`;
const CATEGORIES_LIMIT = 6;
const VALID_OPERATIONS = {
count: [],
sum: ['aggregationColumn'],
avg: ['aggregationColumn'],
@@ -97,7 +180,7 @@ var VALID_OPERATIONS = {
max: ['aggregationColumn']
};
var TYPE = 'aggregation';
const TYPE = 'aggregation';
/**
{
@@ -108,256 +191,150 @@ var TYPE = 'aggregation';
}
}
*/
function Aggregation(query, options, queries) {
if (!_.isString(options.column)) {
throw new Error('Aggregation expects `column` in widget options');
module.exports = class Aggregation extends BaseDataview {
constructor (query, options = {}, queries = {}) {
super();
this._checkOptions(options);
this.query = query;
this.queries = queries;
this.column = options.column;
this.aggregation = options.aggregation;
this.aggregationColumn = options.aggregationColumn;
this._isFloatColumn = null;
}
if (!_.isString(options.aggregation)) {
throw new Error('Aggregation expects `aggregation` operation in widget options');
}
if (!VALID_OPERATIONS[options.aggregation]) {
throw new Error("Aggregation does not support '" + options.aggregation + "' operation");
}
var requiredOptions = VALID_OPERATIONS[options.aggregation];
var missingOptions = _.difference(requiredOptions, Object.keys(options));
if (missingOptions.length > 0) {
throw new Error(
"Aggregation '" + options.aggregation + "' is missing some options: " + missingOptions.join(',')
);
}
BaseWidget.apply(this);
this.query = query;
this.queries = queries;
this.column = options.column;
this.aggregation = options.aggregation;
this.aggregationColumn = options.aggregationColumn;
this._isFloatColumn = null;
}
Aggregation.prototype = new BaseWidget();
Aggregation.prototype.constructor = Aggregation;
module.exports = Aggregation;
Aggregation.prototype.sql = function(psql, override, callback) {
var self = this;
if (!callback) {
callback = override;
override = {};
}
if (this.aggregationColumn && this._isFloatColumn === null) {
this._isFloatColumn = false;
this.getColumnType(psql, this.aggregationColumn, this.queries.no_filters, function (err, type) {
if (!err && !!type) {
self._isFloatColumn = type.float;
}
self.sql(psql, override, callback);
});
return null;
}
var _query = this.query;
var aggregationSql;
if (!!override.ownFilter) {
aggregationSql = [
this.getCategoriesCTESql(
_query,
this.column,
this.aggregation,
this.aggregationColumn,
this._isFloatColumn
),
aggregationQueryTpl({
_isFloatColumn: this._isFloatColumn,
_query: _query,
_column: this.column,
_aggregation: this.getAggregationSql(),
_limit: CATEGORIES_LIMIT
})
].join('\n');
} else {
aggregationSql = [
this.getCategoriesCTESql(
_query,
this.column,
this.aggregation,
this.aggregationColumn,
this._isFloatColumn
),
rankedAggregationQueryTpl({
_isFloatColumn: this._isFloatColumn,
_query: _query,
_column: this.column,
_aggregationFn: this.aggregation !== 'count' ? this.aggregation : 'sum',
_limit: CATEGORIES_LIMIT
})
].join('\n');
}
debug(aggregationSql);
return callback(null, aggregationSql);
};
Aggregation.prototype.getCategoriesCTESql = function(query, column, aggregation, aggregationColumn, isFloatColumn) {
return [
"WITH",
[
filteredQueryTpl({
_isFloatColumn: isFloatColumn,
_query: this.query,
_column: this.column,
_aggregationColumn: aggregation !== 'count' ? aggregationColumn : null
}),
summaryQueryTpl({
_isFloatColumn: isFloatColumn,
_query: query,
_column: column,
_aggregationColumn: aggregation !== 'count' ? aggregationColumn : null
}),
rankedCategoriesQueryTpl({
_query: query,
_column: column,
_aggregation: this.getAggregationSql(),
_aggregationColumn: aggregation !== 'count' ? aggregationColumn : null
}),
categoriesSummaryMinMaxQueryTpl({
_query: query,
_column: column
}),
categoriesSummaryCountQueryTpl({
_query: query,
_column: column
})
].join(',\n')
].join('\n');
};
var aggregationFnQueryTpl = dot.template('{{=it._aggregationFn}}({{=it._aggregationColumn}})');
Aggregation.prototype.getAggregationSql = function() {
return aggregationFnQueryTpl({
_aggregationFn: this.aggregation,
_aggregationColumn: this.aggregationColumn || 1
});
};
Aggregation.prototype.format = function(result) {
var categories = [];
var count = 0;
var nulls = 0;
var nans = 0;
var infinities = 0;
var minValue = 0;
var maxValue = 0;
var categoriesCount = 0;
if (result.rows.length) {
var firstRow = result.rows[0];
count = firstRow.count;
nulls = firstRow.nulls_count;
nans = firstRow.nans_count;
infinities = firstRow.infinities_count;
minValue = firstRow.min_val;
maxValue = firstRow.max_val;
categoriesCount = firstRow.categories_count;
result.rows.forEach(function(row) {
categories.push(_.omit(row, 'count', 'nulls_count', 'min_val',
'max_val', 'categories_count', 'nans_count', 'infinities_count'));
});
}
return {
aggregation: this.aggregation,
count: count,
nulls: nulls,
nans: nans,
infinities: infinities,
min: minValue,
max: maxValue,
categoriesCount: categoriesCount,
categories: categories
};
};
var filterCategoriesQueryTpl = dot.template([
'SELECT {{=it._column}} AS category, {{=it._value}} AS value',
'FROM ({{=it._query}}) _cdb_aggregation_search',
'WHERE CAST({{=it._column}} as text) ILIKE {{=it._userQuery}}',
'GROUP BY {{=it._column}}'
].join('\n'));
var searchQueryTpl = dot.template([
'WITH',
'search_unfiltered AS (',
' {{=it._searchUnfiltered}}',
'),',
'search_filtered AS (',
' {{=it._searchFiltered}}',
'),',
'search_union AS (',
' SELECT * FROM search_unfiltered',
' UNION ALL',
' SELECT * FROM search_filtered',
')',
'SELECT category, sum(value) AS value',
'FROM search_union',
'GROUP BY category',
'ORDER BY value desc'
].join('\n'));
Aggregation.prototype.search = function(psql, userQuery, callback) {
var self = this;
var _userQuery = psql.escapeLiteral('%' + userQuery + '%');
var _value = this.aggregation !== 'count' && this.aggregationColumn ?
this.aggregation + '(' + this.aggregationColumn + ')' : 'count(1)';
// TODO unfiltered will be wrong as filters are already applied at this point
var query = searchQueryTpl({
_searchUnfiltered: filterCategoriesQueryTpl({
_query: this.query,
_column: this.column,
_value: '0',
_userQuery: _userQuery
}),
_searchFiltered: filterCategoriesQueryTpl({
_query: this.query,
_column: this.column,
_value: _value,
_userQuery: _userQuery
})
});
psql.query(query, function(err, result) {
if (err) {
return callback(err, result);
_checkOptions (options) {
if (typeof options.column !== 'string') {
throw new Error(`Aggregation expects 'column' in dataview options`);
}
return callback(null, {type: self.getType(), categories: result.rows });
}, true); // use read-only transaction
};
if (typeof options.aggregation !== 'string') {
throw new Error(`Aggregation expects 'aggregation' operation in dataview options`);
}
Aggregation.prototype.getType = function() {
return TYPE;
};
if (!VALID_OPERATIONS[options.aggregation]) {
throw new Error(`Aggregation does not support '${options.aggregation}' operation`);
}
Aggregation.prototype.toString = function() {
return JSON.stringify({
_type: TYPE,
_query: this.query,
_column: this.column,
_aggregation: this.aggregation
});
const requiredOptions = VALID_OPERATIONS[options.aggregation];
const missingOptions = requiredOptions.filter(requiredOption => !options.hasOwnProperty(requiredOption));
if (missingOptions.length > 0) {
throw new Error(
`Aggregation '${options.aggregation}' is missing some options: ${missingOptions.join(',')}`
);
}
}
sql (psql, override, callback) {
if (!callback) {
callback = override;
override = {};
}
if (this._shouldCheckColumnType()) {
this._isFloatColumn = false;
this.getColumnType(psql, this.aggregationColumn, this.queries.no_filters, (err, type) => {
if (!err && !!type) {
this._isFloatColumn = type.float;
}
this.sql(psql, override, callback);
});
return null;
}
const aggregationSql = aggregationDataviewQueryTpl({
override: override,
query: this.query,
column: this.column,
aggregation: this.aggregation,
aggregationColumn: this.aggregation !== 'count' ? this.aggregationColumn : null,
aggregationFn: aggregationFnQueryTpl({
aggregation: this.aggregation,
aggregationColumn: this.aggregationColumn || 1
}),
isFloatColumn: this._isFloatColumn,
limit: CATEGORIES_LIMIT
});
debug(aggregationSql);
return callback(null, aggregationSql);
}
_shouldCheckColumnType () {
return this.aggregationColumn && this._isFloatColumn === null;
}
format (result) {
const {
count = 0,
nulls_count = 0,
nans_count = 0,
infinities_count = 0,
min_val = 0,
max_val = 0,
categories_count = 0
} = result.rows[0] || {};
return {
aggregation: this.aggregation,
count: count,
nulls: nulls_count,
nans: nans_count,
infinities: infinities_count,
min: min_val,
max: max_val,
categoriesCount: categories_count,
categories: result.rows.map(({ category, value, agg }) => ({ category, value, agg }))
};
}
search (psql, userQuery, callback) {
const escapedUserQuery = psql.escapeLiteral(`%${userQuery}%`);
const value = this.aggregation !== 'count' && this.aggregationColumn ?
`${this.aggregation}(${this.aggregationColumn})` :
'count(1)';
// TODO unfiltered will be wrong as filters are already applied at this point
const query = searchQueryTpl({
searchUnfiltered: filterCategoriesQueryTpl({
query: this.query,
column: this.column,
value: '0',
userQuery: escapedUserQuery
}),
searchFiltered: filterCategoriesQueryTpl({
query: this.query,
column: this.column,
value: value,
userQuery: escapedUserQuery
})
});
debug(query);
psql.query(query, (err, result) => {
if (err) {
return callback(err, result);
}
return callback(null, {type: this.getType(), categories: result.rows });
}, true); // use read-only transaction
}
getType () {
return TYPE;
}
toString () {
return JSON.stringify({
_type: TYPE,
_query: this.query,
_column: this.column,
_aggregation: this.aggregation
});
}
};

View File

@@ -1,67 +1,16 @@
var dot = require('dot');
dot.templateSettings.strip = false;
function BaseDataview() {}
module.exports = BaseDataview;
BaseDataview.prototype.getResult = function(psql, override, callback) {
var self = this;
this.sql(psql, override, function(err, query) {
if (err) {
return callback(err);
}
psql.query(query, function(err, result) {
if (err) {
return callback(err, result);
}
result = self.format(result, override);
result.type = self.getType();
return callback(null, result);
}, true); // use read-only transaction
});
};
BaseDataview.prototype.search = function(psql, userQuery, callback) {
return callback(null, this.format({ rows: [] }));
};
var FLOAT_OIDS = {
const FLOAT_OIDS = {
700: true,
701: true,
1700: true
};
var DATE_OIDS = {
const DATE_OIDS = {
1082: true,
1114: true,
1184: true
};
var columnTypeQueryTpl = dot.template(
'SELECT pg_typeof({{=it.column}})::oid FROM ({{=it.query}}) _cdb_column_type limit 1'
);
BaseDataview.prototype.getColumnType = function (psql, column, query, callback) {
var readOnlyTransaction = true;
var columnTypeQuery = columnTypeQueryTpl({
column: column, query: query
});
psql.query(columnTypeQuery, function(err, result) {
if (err) {
return callback(err);
}
var pgType = result.rows[0].pg_typeof;
callback(null, getPGTypeName(pgType));
}, readOnlyTransaction);
};
const columnTypeQueryTpl = ctx => `SELECT pg_typeof(${ctx.column})::oid FROM (${ctx.query}) _cdb_column_type limit 1`;
function getPGTypeName (pgType) {
return {
@@ -69,3 +18,42 @@ function getPGTypeName (pgType) {
date: DATE_OIDS.hasOwnProperty(pgType)
};
}
module.exports = class BaseDataview {
getResult (psql, override, callback) {
this.sql(psql, override, (err, query) => {
if (err) {
return callback(err);
}
psql.query(query, (err, result) => {
if (err) {
return callback(err, result);
}
result = this.format(result, override);
result.type = this.getType();
return callback(null, result);
}, true); // use read-only transaction
});
}
search (psql, userQuery, callback) {
return callback(null, this.format({ rows: [] }));
}
getColumnType (psql, column, query, callback) {
const readOnlyTransaction = true;
const columnTypeQuery = columnTypeQueryTpl({ column, query });
psql.query(columnTypeQuery, (err, result) => {
if (err) {
return callback(err);
}
const pgType = result.rows[0].pg_typeof;
callback(null, getPGTypeName(pgType));
}, readOnlyTransaction);
}
};

View File

@@ -1,18 +1,20 @@
var dataviews = require('./');
const dataviews = require('./');
var DataviewFactory = {
dataviews: Object.keys(dataviews).reduce(function(allDataviews, dataviewClassName) {
allDataviews[dataviewClassName.toLowerCase()] = dataviews[dataviewClassName];
return allDataviews;
}, {}),
module.exports = class DataviewFactory {
static get dataviews() {
return Object.keys(dataviews).reduce((allDataviews, dataviewClassName) => {
allDataviews[dataviewClassName.toLowerCase()] = dataviews[dataviewClassName];
return allDataviews;
}, {});
}
static getDataview (query, dataviewDefinition) {
const { type, options, sql } = dataviewDefinition;
getDataview: function(query, dataviewDefinition) {
var type = dataviewDefinition.type;
if (!this.dataviews[type]) {
throw new Error('Invalid dataview type: "' + type + '"');
}
return new this.dataviews[type](query, dataviewDefinition.options, dataviewDefinition.sql);
return new this.dataviews[type](query, options, sql);
}
};
module.exports = DataviewFactory;

View File

@@ -1,28 +1,36 @@
var _ = require('underscore');
var BaseWidget = require('./base');
var debug = require('debug')('windshaft:widget:formula');
const BaseDataview = require('./base');
const debug = require('debug')('windshaft:dataview:formula');
var dot = require('dot');
dot.templateSettings.strip = false;
const countInfinitiesQueryTpl = ctx => `
SELECT count(1) FROM (${ctx.query}) __cdb_formula_infinities
WHERE ${ctx.column} = 'infinity'::float OR ${ctx.column} = '-infinity'::float
`;
var formulaQueryTpl = dot.template([
'SELECT',
' {{=it._operation}}({{=it._column}}) 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_nulls',
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count',
' ,(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls',
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
'FROM ({{=it._query}}) _cdb_formula',
'{{?it._isFloatColumn && it._operation !== \'count\'}}WHERE',
' {{=it._column}} != \'infinity\'::float',
'AND',
' {{=it._column}} != \'-infinity\'::float',
'AND',
' {{=it._column}} != \'NaN\'::float{{?}}'
].join('\n'));
const countNansQueryTpl = ctx => `
SELECT count(1) FROM (${ctx.query}) __cdb_formula_nans
WHERE ${ctx.column} = 'NaN'::float
`;
var VALID_OPERATIONS = {
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 VALID_OPERATIONS = {
count: true,
avg: true,
sum: true,
@@ -30,7 +38,7 @@ var VALID_OPERATIONS = {
max: true
};
var TYPE = 'formula';
const TYPE = 'formula';
/**
{
@@ -41,93 +49,90 @@ var TYPE = 'formula';
}
}
*/
function Formula(query, options, queries) {
if (!_.isString(options.operation)) {
throw new Error('Formula expects `operation` in widget options');
module.exports = class Formula extends BaseDataview {
constructor (query, options = {}, queries = {}) {
super();
this._checkOptions(options);
this.query = query;
this.queries = queries;
this.column = options.column || '1';
this.operation = options.operation;
this._isFloatColumn = null;
}
if (!VALID_OPERATIONS[options.operation]) {
throw new Error("Formula does not support '" + options.operation + "' operation");
_checkOptions (options) {
if (typeof options.operation !== 'string') {
throw new Error(`Formula expects 'operation' in dataview options`);
}
if (!VALID_OPERATIONS[options.operation]) {
throw new Error(`Formula does not support '${options.operation}' operation`);
}
if (options.operation !== 'count' && typeof options.column !== 'string') {
throw new Error(`Formula expects 'column' in dataview options`);
}
}
if (options.operation !== 'count' && !_.isString(options.column)) {
throw new Error('Formula expects `column` in widget options');
}
BaseWidget.apply(this);
sql (psql, override, callback) {
if (!callback) {
callback = override;
override = {};
}
this.query = query;
this.queries = queries;
this.column = options.column || '1';
this.operation = options.operation;
this._isFloatColumn = null;
}
if (this._isFloatColumn === null) {
this._isFloatColumn = false;
this.getColumnType(psql, this.column, this.queries.no_filters, (err, type) => {
if (!err && !!type) {
this._isFloatColumn = type.float;
}
this.sql(psql, override, callback);
});
return null;
}
Formula.prototype = new BaseWidget();
Formula.prototype.constructor = Formula;
module.exports = Formula;
Formula.prototype.sql = function(psql, override, callback) {
var self = this;
if (!callback) {
callback = override;
override = {};
}
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);
const formulaSql = formulaQueryTpl({
isFloatColumn: this._isFloatColumn,
query: this.query,
operation: this.operation,
column: this.column
});
return null;
debug(formulaSql);
return callback(null, formulaSql);
}
var formulaSql = formulaQueryTpl({
_isFloatColumn: this._isFloatColumn,
_query: this.query,
_operation: this.operation,
_column: this.column
});
format (res) {
const {
result = 0,
nulls_count = 0,
nans_count,
infinities_count
} = res.rows[0] || {};
debug(formulaSql);
return callback(null, formulaSql);
};
Formula.prototype.format = function(result) {
var formattedResult = {
operation: this.operation,
result: 0,
nulls: 0,
nans: 0,
infinities: 0
};
if (result.rows.length) {
formattedResult.operation = this.operation;
formattedResult.result = result.rows[0].result;
formattedResult.nulls = result.rows[0].nulls_count;
formattedResult.nans = result.rows[0].nans_count;
formattedResult.infinities = result.rows[0].infinities_count;
return {
operation: this.operation,
result,
nulls: nulls_count,
nans: nans_count,
infinities: infinities_count
};
}
return formattedResult;
};
getType () {
return TYPE;
}
Formula.prototype.getType = function() {
return TYPE;
};
Formula.prototype.toString = function() {
return JSON.stringify({
_type: TYPE,
_query: this.query,
_column: this.column,
_operation: this.operation
});
toString () {
return JSON.stringify({
_type: TYPE,
_query: this.query,
_column: this.column,
_operation: this.operation
});
}
};

View File

@@ -1,717 +1,72 @@
var _ = require('underscore');
var BaseWidget = require('./base');
var debug = require('debug')('windshaft:dataview:histogram');
const debug = require('debug')('windshaft:dataview:histogram');
const NumericHistogram = require('./histograms/numeric-histogram');
const DateHistogram = require('./histograms/date-histogram');
var dot = require('dot');
dot.templateSettings.strip = false;
const DATE_HISTOGRAM = 'DateHistogram';
const NUMERIC_HISTOGRAM = 'NumericHistogram';
var columnCastTpl = dot.template("date_part('epoch', {{=it.column}})");
module.exports = class Histogram {
constructor (query, options, queries) {
this.query = query;
this.options = options || {};
this.queries = queries;
var dateIntervalQueryTpl = dot.template([
'WITH',
'__cdb_dates AS (',
' SELECT',
' MAX({{=it.column}}::timestamp) AS __cdb_end,',
' MIN({{=it.column}}::timestamp) AS __cdb_start',
' FROM ({{=it.query}}) __cdb_source',
'),',
'__cdb_interval_in_days AS (',
' SELECT' ,
' DATE_PART(\'day\', __cdb_end - __cdb_start) AS __cdb_days',
' FROM __cdb_dates',
'),',
'__cdb_interval_in_hours AS (',
' SELECT',
' __cdb_days * 24 + DATE_PART(\'hour\', __cdb_end - __cdb_start) AS __cdb_hours',
' FROM __cdb_interval_in_days, __cdb_dates',
'),',
'__cdb_interval_in_minutes AS (',
' SELECT',
' __cdb_hours * 60 + DATE_PART(\'minute\', __cdb_end - __cdb_start) AS __cdb_minutes',
' FROM __cdb_interval_in_hours, __cdb_dates',
'),',
'__cdb_interval_in_seconds AS (',
' SELECT',
' __cdb_minutes * 60 + DATE_PART(\'second\', __cdb_end - __cdb_start) AS __cdb_seconds',
' FROM __cdb_interval_in_minutes, __cdb_dates',
')',
'SELECT',
' ROUND(__cdb_days / 365) AS year,',
' ROUND(__cdb_days / 90) AS quarter,',
' ROUND(__cdb_days / 30) AS month,',
' ROUND(__cdb_days / 7) AS week,',
' __cdb_days AS day,',
' __cdb_hours AS hour,',
' __cdb_minutes AS minute,',
' __cdb_seconds AS second',
'FROM __cdb_interval_in_days, __cdb_interval_in_hours, __cdb_interval_in_minutes, __cdb_interval_in_seconds'
].join('\n'));
var MAX_INTERVAL_VALUE = 366;
var BIN_MIN_NUMBER = 6;
var BIN_MAX_NUMBER = 48;
var filteredQueryTpl = dot.template([
'__cdb_filtered_source AS (',
' SELECT *',
' FROM ({{=it._query}}) __cdb_filtered_source_query',
' WHERE',
' {{=it._column}} IS NOT NULL',
' {{?it._isFloatColumn}}AND',
' {{=it._column}} != \'infinity\'::float',
' AND',
' {{=it._column}} != \'-infinity\'::float',
' AND',
' {{=it._column}} != \'NaN\'::float{{?}}',
')'
].join(' \n'));
var basicsQueryTpl = dot.template([
'__cdb_basics AS (',
' SELECT',
' max({{=it._column}}) AS __cdb_max_val, min({{=it._column}}) AS __cdb_min_val,',
' avg({{=it._column}}) AS __cdb_avg_val, count(1) AS __cdb_total_rows',
' FROM __cdb_filtered_source',
')'
].join(' \n'));
var overrideBasicsQueryTpl = dot.template([
'__cdb_basics AS (',
' SELECT',
' max({{=it._end}}) AS __cdb_max_val, min({{=it._start}}) AS __cdb_min_val,',
' avg({{=it._column}}) AS __cdb_avg_val, count(1) AS __cdb_total_rows',
' FROM __cdb_filtered_source',
')'
].join('\n'));
var iqrQueryTpl = dot.template([
'__cdb_iqrange AS (',
' SELECT max(quartile_max) - min(quartile_max) AS __cdb_iqr',
' FROM (',
' SELECT quartile, max(_cdb_iqr_column) AS quartile_max from (',
' SELECT {{=it._column}} AS _cdb_iqr_column, ntile(4) over (order by {{=it._column}}',
' ) AS quartile',
' FROM __cdb_filtered_source) _cdb_quartiles',
' WHERE quartile = 1 or quartile = 3',
' GROUP BY quartile',
' ) __cdb_iqr',
')'
].join('\n'));
var binsQueryTpl = dot.template([
'__cdb_bins AS (',
' SELECT CASE WHEN __cdb_total_rows = 0 OR __cdb_iqr = 0',
' THEN 1',
' ELSE GREATEST(',
' LEAST({{=it._minBins}}, CAST(__cdb_total_rows AS INT)),',
' LEAST(',
' CAST(((__cdb_max_val - __cdb_min_val) / (2 * __cdb_iqr * power(__cdb_total_rows, 1/3))) AS INT),',
' {{=it._maxBins}}',
' )',
' )',
' END AS __cdb_bins_number',
' FROM __cdb_basics, __cdb_iqrange, __cdb_filtered_source',
' LIMIT 1',
')'
].join('\n'));
var overrideBinsQueryTpl = dot.template([
'__cdb_bins AS (',
' SELECT {{=it._bins}} AS __cdb_bins_number',
')'
].join('\n'));
var nullsQueryTpl = dot.template([
'__cdb_nulls AS (',
' SELECT',
' count(*) AS __cdb_nulls_count',
' FROM ({{=it._query}}) __cdb_histogram_nulls',
' WHERE {{=it._column}} IS NULL',
')'
].join('\n'));
var infinitiesQueryTpl = dot.template([
'__cdb_infinities AS (',
' SELECT',
' count(*) AS __cdb_infinities_count',
' FROM ({{=it._query}}) __cdb_infinities_query',
' WHERE',
' {{=it._column}} = \'infinity\'::float',
' OR',
' {{=it._column}} = \'-infinity\'::float',
')'
].join('\n'));
var nansQueryTpl = dot.template([
'__cdb_nans AS (',
' SELECT',
' count(*) AS __cdb_nans_count',
' FROM ({{=it._query}}) __cdb_nans_query',
' WHERE {{=it._column}} = \'NaN\'::float',
')'
].join('\n'));
var histogramQueryTpl = dot.template([
'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,',
' {{?it._isFloatColumn}}__cdb_infinities_count AS infinities_count,',
' __cdb_nans_count AS nans_count,{{?}}',
' __cdb_avg_val AS avg_val,',
' CASE WHEN __cdb_min_val = __cdb_max_val',
' THEN 0',
' ELSE GREATEST(',
' 1,',
' LEAST(',
' WIDTH_BUCKET({{=it._column}}, __cdb_min_val, __cdb_max_val, __cdb_bins_number),',
' __cdb_bins_number',
' )',
' ) - 1',
' END AS bin,',
' min({{=it._column}})::numeric AS min,',
' max({{=it._column}})::numeric AS max,',
' avg({{=it._column}})::numeric AS avg,',
' count(*) AS freq',
'FROM __cdb_filtered_source, __cdb_basics, __cdb_nulls,',
' __cdb_bins{{?it._isFloatColumn}}, __cdb_infinities, __cdb_nans{{?}}',
'GROUP BY bin, bins_number, bin_width, nulls_count,',
' avg_val{{?it._isFloatColumn}}, infinities_count, nans_count{{?}}',
'ORDER BY bin'
].join('\n'));
var dateBasicsQueryTpl = dot.template([
'__cdb_basics AS (',
' SELECT',
' max(date_part(\'epoch\', {{=it._column}})) AS __cdb_max_val,',
' min(date_part(\'epoch\', {{=it._column}})) AS __cdb_min_val,',
' avg(date_part(\'epoch\', {{=it._column}})) AS __cdb_avg_val,',
' min(date_trunc(',
' \'{{=it._aggregation}}\', {{=it._column}}::timestamp AT TIME ZONE \'{{=it._offset}}\'',
' )) AS __cdb_start_date,',
' max({{=it._column}}::timestamp AT TIME ZONE \'{{=it._offset}}\') AS __cdb_end_date,',
' count(1) AS __cdb_total_rows',
' FROM ({{=it._query}}) __cdb_basics_query',
')'
].join(' \n'));
var dateOverrideBasicsQueryTpl = dot.template([
'__cdb_basics AS (',
' SELECT',
' max({{=it._end}}) AS __cdb_max_val,',
' min({{=it._start}}) AS __cdb_min_val,',
' avg(date_part(\'epoch\', {{=it._column}})) AS __cdb_avg_val,',
' min(',
' date_trunc(',
' \'{{=it._aggregation}}\',',
' TO_TIMESTAMP({{=it._start}})::timestamp AT TIME ZONE \'{{=it._offset}}\'',
' )',
' ) AS __cdb_start_date,',
' max(',
' TO_TIMESTAMP({{=it._end}})::timestamp AT TIME ZONE \'{{=it._offset}}\'',
' ) AS __cdb_end_date,',
' count(1) AS __cdb_total_rows',
' FROM ({{=it._query}}) __cdb_basics_query',
')'
].join(' \n'));
var dateBinsQueryTpl = dot.template([
'__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,',
' {{?it._aggregation==="quarter"}}\'3 month\'{{??}}\'1 {{=it._aggregation}}\'{{?}}::interval',
' )',
' ) AS __cdb_bins_array',
' FROM __cdb_basics',
' ) __cdb_bins_array_query',
')'
].join('\n'));
var dateHistogramQueryTpl = dot.template([
'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(',
' {{=it._column}}::timestamp AT TIME ZONE \'{{=it._offset}}\',',
' __cdb_bins_array',
' ),',
' __cdb_bins_number',
' )) - 1',
' END AS bin,',
' min(',
' date_part(',
' \'epoch\', ',
' date_trunc(',
' \'{{=it._aggregation}}\', {{=it._column}}::timestamp AT TIME ZONE \'{{=it._offset}}\'',
' ) AT TIME ZONE \'{{=it._offset}}\'',
' )',
' )::numeric AS timestamp,',
' date_part(\'epoch\', __cdb_start_date)::numeric AS timestamp_start,',
' min(date_part(\'epoch\', {{=it._column}}))::numeric AS min,',
' max(date_part(\'epoch\', {{=it._column}}))::numeric AS max,',
' avg(date_part(\'epoch\', {{=it._column}}))::numeric AS avg,',
' count(*) AS freq',
'FROM ({{=it._query}}) __cdb_histogram, __cdb_basics, __cdb_bins, __cdb_nulls',
'WHERE date_part(\'epoch\', {{=it._column}}) IS NOT NULL',
'GROUP BY bin, bins_number, bin_width, nulls_count, timestamp_start',
'ORDER BY bin'
].join('\n'));
var TYPE = 'histogram';
/**
Numeric histogram:
{
type: 'histogram',
options: {
column: 'name', // column data type: numeric
bins: 10 // OPTIONAL
}
}
Time series:
{
type: 'histogram',
options: {
column: 'date', // column data type: date
aggregation: 'day' // OPTIONAL (if undefined then it'll be built as numeric)
offset: -7200 // OPTIONAL (UTC offset in seconds)
}
}
*/
function Histogram(query, options, queries) {
if (!_.isString(options.column)) {
throw new Error('Histogram expects `column` in widget options');
this.histogramImplementation = this._getHistogramImplementation();
}
this.query = query;
this.queries = queries;
this.column = options.column;
this.bins = options.bins;
this.aggregation = options.aggregation;
this.offset = options.offset;
_getHistogramImplementation (override) {
let implementation = null;
this._columnType = null;
}
Histogram.prototype = new BaseWidget();
Histogram.prototype.constructor = Histogram;
module.exports = Histogram;
Histogram.prototype.sql = function(psql, override, callback) {
var self = this;
if (!callback) {
callback = override;
override = {};
}
if (this._columnType === null) {
this.getColumnType(psql, this.column, this.queries.no_filters, function (err, type) {
// assume numeric, will fail later
self._columnType = 'numeric';
if (!err && !!type) {
self._columnType = Object.keys(type).find(function (key) {
return type[key];
});
}
self.sql(psql, override, callback);
}, true); // use read-only transaction
return null;
}
this._buildQuery(psql, override, callback);
};
Histogram.prototype.isDateHistogram = function (override) {
return this._columnType === 'date' && (this.aggregation !== undefined || override.aggregation !== undefined);
};
Histogram.prototype._buildQuery = function (psql, override, callback) {
var filteredQuery, basicsQuery, binsQuery;
var _column = this.column;
var _query = this.query;
if (this.isDateHistogram(override)) {
return this._buildDateHistogramQuery(psql, override, callback);
}
if (this._columnType === 'date') {
_column = columnCastTpl({column: _column});
}
filteredQuery = filteredQueryTpl({
_isFloatColumn: this._columnType === 'float',
_query: _query,
_column: _column
});
if (this._shouldOverride(override)) {
debug('overriding with %j', override);
basicsQuery = overrideBasicsQueryTpl({
_query: _query,
_column: _column,
_start: getBinStart(override),
_end: getBinEnd(override)
});
binsQuery = [
overrideBinsQueryTpl({
_bins: override.bins
})
].join(',\n');
} else {
basicsQuery = basicsQueryTpl({
_query: _query,
_column: _column
});
if (this._shouldOverrideBins(override)) {
binsQuery = [
overrideBinsQueryTpl({
_bins: override.bins
})
].join(',\n');
} else {
binsQuery = [
iqrQueryTpl({
_query: _query,
_column: _column
}),
binsQueryTpl({
_query: _query,
_minBins: BIN_MIN_NUMBER,
_maxBins: BIN_MAX_NUMBER
})
].join(',\n');
}
}
var cteSql = [
filteredQuery,
basicsQuery,
binsQuery,
nullsQueryTpl({
_query: _query,
_column: _column
})
];
if (this._columnType === 'float') {
cteSql.push(
infinitiesQueryTpl({
_query: _query,
_column: _column
}),
nansQueryTpl({
_query: _query,
_column: _column
})
);
}
var histogramSql = [
"WITH",
cteSql.join(',\n'),
histogramQueryTpl({
_isFloatColumn: this._columnType === 'float',
_query: _query,
_column: _column
})
].join('\n');
debug(histogramSql);
return callback(null, histogramSql);
};
Histogram.prototype._shouldOverride = function (override) {
return override && _.has(override, 'start') && _.has(override, 'end') && _.has(override, 'bins');
};
Histogram.prototype._shouldOverrideBins = function (override) {
return override && _.has(override, 'bins');
};
var DATE_AGGREGATIONS = {
'auto': true,
'minute': true,
'hour': true,
'day': true,
'week': true,
'month': true,
'quarter': true,
'year': true
};
Histogram.prototype._buildDateHistogramQuery = function (psql, override, callback) {
var _column = this.column;
var _query = this.query;
var _aggregation = override && override.aggregation ? override.aggregation : this.aggregation;
var _offset = override && Number.isFinite(override.offset) ? override.offset : this.offset;
if (!DATE_AGGREGATIONS.hasOwnProperty(_aggregation)) {
return callback(new Error('Invalid aggregation value. Valid ones: ' +
Object.keys(DATE_AGGREGATIONS).join(', ')
));
}
if (_aggregation === 'auto') {
this.getAutomaticAggregation(psql, function (err, aggregation) {
if (err || aggregation === 'none') {
this.aggregation = 'day';
} else {
this.aggregation = aggregation;
}
override.aggregation = this.aggregation;
this._buildDateHistogramQuery(psql, override, callback);
}.bind(this));
return null;
}
var dateBasicsQuery;
if (override && _.has(override, 'start') && _.has(override, 'end')) {
dateBasicsQuery = dateOverrideBasicsQueryTpl({
_query: _query,
_column: _column,
_aggregation: _aggregation,
_start: getBinStart(override),
_end: getBinEnd(override),
_offset: parseOffset(_offset, _aggregation)
});
} else {
dateBasicsQuery = dateBasicsQueryTpl({
_query: _query,
_column: _column,
_aggregation: _aggregation,
_offset: parseOffset(_offset, _aggregation)
});
}
var dateBinsQuery = [
dateBinsQueryTpl({
_aggregation: _aggregation
})
].join(',\n');
var nullsQuery = nullsQueryTpl({
_query: _query,
_column: _column
});
var dateHistogramQuery = dateHistogramQueryTpl({
_query: _query,
_column: _column,
_aggregation: _aggregation,
_offset: parseOffset(_offset, _aggregation)
});
var histogramSql = [
"WITH",
[
dateBasicsQuery,
dateBinsQuery,
nullsQuery
].join(',\n'),
dateHistogramQuery
].join('\n');
debug(histogramSql);
return callback(null, histogramSql);
};
Histogram.prototype.getAutomaticAggregation = function (psql, callback) {
var dateIntervalQuery = dateIntervalQueryTpl({
query: this.query,
column: this.column
});
debug(dateIntervalQuery);
psql.query(dateIntervalQuery, function (err, result) {
if (err) {
return callback(err);
switch (this._getHistogramSubtype(override)) {
case DATE_HISTOGRAM:
debug('Delegating to DateHistogram with options: %j and overriding: %j', this.options, override);
implementation = new DateHistogram(this.query, this.options, this.queries);
break;
case NUMERIC_HISTOGRAM:
debug('Delegating to NumericHistogram with options: %j and overriding: %j', this.options, override);
implementation = new NumericHistogram(this.query, this.options, this.queries);
break;
default:
throw new Error('Unsupported Histogram type');
}
var aggegations = result.rows[0];
var aggregation = Object.keys(aggegations)
.map(function (key) {
return {
name: key,
value: aggegations[key]
};
})
.reduce(function (closer, current) {
if (current.value > MAX_INTERVAL_VALUE) {
return closer;
}
return implementation;
}
var closerDiff = MAX_INTERVAL_VALUE - closer.value;
var currentDiff = MAX_INTERVAL_VALUE - current.value;
if (Number.isFinite(current.value) && closerDiff > currentDiff) {
return current;
}
return closer;
}, { name: 'none', value: -1 });
callback(null, aggregation.name);
});
};
Histogram.prototype.format = function(result, override) {
override = override || {};
var buckets = [];
var binsCount = getBinsCount(override);
var width = getWidth(override);
var binsStart = getBinStart(override);
var nulls = 0;
var infinities = 0;
var nans = 0;
var avg;
var timestampStart;
var aggregation;
var offset;
if (result.rows.length) {
var firstRow = result.rows[0];
binsCount = firstRow.bins_number;
width = firstRow.bin_width || width;
avg = firstRow.avg_val;
nulls = firstRow.nulls_count;
timestampStart = firstRow.timestamp_start;
infinities = firstRow.infinities_count;
nans = firstRow.nans_count;
binsStart = populateBinStart(override, firstRow);
if (Number.isFinite(timestampStart)) {
aggregation = getAggregation(override, this.aggregation);
offset = getOffset(override, this.offset);
_getHistogramSubtype (override) {
if(this._isDateHistogram(override)) {
return DATE_HISTOGRAM;
}
buckets = result.rows.map(function(row) {
return _.omit(
row,
'bins_number',
'bin_width',
'nulls_count',
'infinities_count',
'nans_count',
'avg_val',
'timestamp_start'
);
});
return NUMERIC_HISTOGRAM;
}
return {
aggregation: aggregation,
offset: offset,
timestamp_start: timestampStart,
bin_width: width,
bins_count: binsCount,
bins_start: binsStart,
nulls: nulls,
infinities: infinities,
nans: nans,
avg: avg,
bins: buckets
};
};
function getAggregation(override, aggregation) {
return override && override.aggregation ? override.aggregation : aggregation;
}
function getOffset(override, offset) {
if (override && override.offset) {
return override.offset;
}
if (offset) {
return offset;
}
return 0;
}
function getBinStart(override) {
if (override.hasOwnProperty('start') && override.hasOwnProperty('end')) {
return Math.min(override.start, override.end);
}
return override.start || 0;
}
function getBinEnd(override) {
if (override.hasOwnProperty('start') && override.hasOwnProperty('end')) {
return Math.max(override.start, override.end);
}
return override.end || 0;
}
function getBinsCount(override) {
return override.bins || 0;
}
function getWidth(override) {
var width = 0;
var binsCount = override.bins;
if (binsCount && Number.isFinite(override.start) && Number.isFinite(override.end)) {
width = (override.end - override.start) / binsCount;
}
return width;
}
function parseOffset(offset, aggregation) {
if (!offset) {
return '0';
}
if (aggregation === 'hour' || aggregation === 'minute') {
return '0';
}
var offsetInHours = Math.ceil(offset / 3600);
return '' + offsetInHours;
}
function populateBinStart(override, firstRow) {
var binStart;
if (firstRow.hasOwnProperty('timestamp')) {
binStart = firstRow.timestamp;
} else if (override.hasOwnProperty('start')) {
binStart = getBinStart(override);
} else {
binStart = firstRow.min;
}
return binStart;
}
Histogram.prototype.getType = function() {
return TYPE;
};
Histogram.prototype.toString = function() {
return JSON.stringify({
_type: TYPE,
_column: this.column,
_query: this.query
});
_isDateHistogram (override = {}) {
return (this.options.hasOwnProperty('aggregation') || override.hasOwnProperty('aggregation'));
}
getResult (psql, override, callback) {
this.histogramImplementation = this._getHistogramImplementation(override);
this.histogramImplementation.getResult(psql, override, callback);
}
// In order to keep previous behaviour with overviews,
// we have to expose the following methods to bypass
// the concrete overview implementation
sql (psql, override, callback) {
this.histogramImplementation.sql(psql, override, callback);
}
format (result, override) {
return this.histogramImplementation.format(result, override);
}
getType () {
return this.histogramImplementation.getType();
}
toString () {
return this.histogramImplementation.toString();
}
};

View File

@@ -0,0 +1,85 @@
const BaseDataview = require('../base');
const TYPE = 'histogram';
module.exports = class BaseHistogram extends BaseDataview {
constructor (query, options, queries) {
super();
if (typeof options.column !== 'string') {
throw new Error('Histogram expects `column` in widget options');
}
this.query = query;
this.queries = queries;
this.column = options.column;
this.bins = options.bins;
this._columnType = null;
}
sql (psql, override, callback) {
if (!callback) {
callback = override;
override = {};
}
if (this._columnType === null) {
this.getColumnType(psql, this.column, this.queries.no_filters, (err, type) => {
// assume numeric, will fail later
this._columnType = 'numeric';
if (!err && !!type) {
this._columnType = Object.keys(type).find(function (key) {
return type[key];
});
}
this.sql(psql, override, callback);
}, true); // use read-only transaction
return null;
}
return this._buildQuery(psql, override, callback);
}
format (result, override) {
const histogram = this._getSummary(result, override);
histogram.bins = this._getBuckets(result);
return histogram;
}
getType () {
return TYPE;
}
toString () {
return JSON.stringify({
_type: TYPE,
_column: this.column,
_query: this.query
});
}
_hasOverridenRange (override) {
return override && override.hasOwnProperty('start') && override.hasOwnProperty('end');
}
_getBinStart (override = {}) {
if (this._hasOverridenRange(override)) {
return Math.min(override.start, override.end);
}
return override.start || 0;
}
_getBinEnd (override = {}) {
if (this._hasOverridenRange(override)) {
return Math.max(override.start, override.end);
}
return override.end || 0;
}
_getBinsCount (override = {}) {
return override.bins || 0;
}
};

View File

@@ -0,0 +1,302 @@
const BaseHistogram = require('./base-histogram');
const debug = require('debug')('windshaft:dataview:date-histogram');
const dateIntervalQueryTpl = ctx => `
WITH
__cdb_dates AS (
SELECT
MAX(${ctx.column}::timestamp) AS __cdb_end,
MIN(${ctx.column}::timestamp) AS __cdb_start
FROM (${ctx.query}) __cdb_source
),
__cdb_interval_in_days AS (
SELECT
DATE_PART('day', __cdb_end - __cdb_start) AS __cdb_days
FROM __cdb_dates
),
__cdb_interval_in_hours AS (
SELECT
__cdb_days * 24 + DATE_PART('hour', __cdb_end - __cdb_start) AS __cdb_hours
FROM __cdb_interval_in_days, __cdb_dates
),
__cdb_interval_in_minutes AS (
SELECT
__cdb_hours * 60 + DATE_PART('minute', __cdb_end - __cdb_start) AS __cdb_minutes
FROM __cdb_interval_in_hours, __cdb_dates
),
__cdb_interval_in_seconds AS (
SELECT
__cdb_minutes * 60 + DATE_PART('second', __cdb_end - __cdb_start) AS __cdb_seconds
FROM __cdb_interval_in_minutes, __cdb_dates
)
SELECT
ROUND(__cdb_days / 365) AS year,
ROUND(__cdb_days / 90) AS quarter,
ROUND(__cdb_days / 30) AS month,
ROUND(__cdb_days / 7) AS week,
__cdb_days AS day,
__cdb_hours AS hour,
__cdb_minutes AS minute,
__cdb_seconds AS second
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;
const DATE_AGGREGATIONS = {
'auto': true,
'minute': true,
'hour': true,
'day': true,
'week': true,
'month': true,
'quarter': true,
'year': true
};
/**
date_histogram: {
type: 'histogram',
options: {
column: 'date', // column data type: date
aggregation: 'day' // MANDATORY
offset: -7200 // OPTIONAL (UTC offset in seconds)
}
}
*/
module.exports = class DateHistogram extends BaseHistogram {
constructor (query, options, queries) {
super(query, options, queries);
this.aggregation = options.aggregation;
this.offset = options.offset;
}
_buildQueryTpl (ctx) {
return `
WITH
${this._hasOverridenRange(ctx.override) ? dateOverrideBasicsQueryTpl(ctx) : dateBasicsQueryTpl(ctx)},
${dateBinsQueryTpl(ctx)},
${nullsQueryTpl(ctx)}
${dateHistogramQueryTpl(ctx)}
`;
}
_buildQuery (psql, override, callback) {
if (!this._isValidAggregation(override)) {
return callback(new Error('Invalid aggregation value. Valid ones: ' +
Object.keys(DATE_AGGREGATIONS).join(', ')
));
}
if (this._getAggregation(override) === 'auto') {
this._getAutomaticAggregation(psql, function (err, aggregation) {
if (err || aggregation === 'none') {
this.aggregation = 'day';
} else {
this.aggregation = aggregation;
}
override.aggregation = this.aggregation;
this._buildQuery(psql, override, callback);
}.bind(this));
return null;
}
const histogramSql = this._buildQueryTpl({
override: override,
query: this.query,
column: this.column,
aggregation: this._getAggregation(override),
start: this._getBinStart(override),
end: this._getBinEnd(override),
offset: this._parseOffset(override)
});
debug(histogramSql);
return callback(null, histogramSql);
}
_isValidAggregation (override) {
return DATE_AGGREGATIONS.hasOwnProperty(this._getAggregation(override));
}
_getAutomaticAggregation (psql, callback) {
const dateIntervalQuery = dateIntervalQueryTpl({
query: this.query,
column: this.column
});
psql.query(dateIntervalQuery, function (err, result) {
if (err) {
return callback(err);
}
const aggegations = result.rows[0];
const aggregation = Object.keys(aggegations)
.map(key => ({ name: key, value: aggegations[key] }))
.reduce((closer, current) => {
if (current.value > MAX_INTERVAL_VALUE) {
return closer;
}
const closerDiff = MAX_INTERVAL_VALUE - closer.value;
const currentDiff = MAX_INTERVAL_VALUE - current.value;
if (Number.isFinite(current.value) && closerDiff > currentDiff) {
return current;
}
return closer;
}, { name: 'none', value: -1 });
callback(null, aggregation.name);
});
}
_getSummary (result, override) {
const firstRow = result.rows[0] || {};
return {
aggregation: this._getAggregation(override),
offset: this._getOffset(override),
timestamp_start: firstRow.timestamp_start,
bin_width: firstRow.bin_width,
bins_count: firstRow.bins_number,
bins_start: firstRow.timestamp,
nulls: firstRow.nulls_count,
infinities: firstRow.infinities_count,
nans: firstRow.nans_count,
avg: firstRow.avg_val
};
}
_getBuckets (result) {
return result.rows.map(({ bin, min, max, avg, freq, timestamp }) => ({ bin, min, max, avg, freq, timestamp }));
}
_getAggregation (override = {}) {
return override.aggregation ? override.aggregation : this.aggregation;
}
_getOffset (override = {}) {
return Number.isFinite(override.offset) ? override.offset : (this.offset || 0);
}
_parseOffset (override) {
if (this._shouldIgnoreOffset(override)) {
return '0';
}
const offsetInHours = Math.ceil(this._getOffset(override) / 3600);
return '' + offsetInHours;
}
_shouldIgnoreOffset (override) {
return (this._getAggregation(override) === 'hour' || this._getAggregation(override) === 'minute');
}
};

View File

@@ -0,0 +1,234 @@
const BaseHistogram = require('./base-histogram');
const debug = require('debug')('windshaft:dataview:numeric-histogram');
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 => `
__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 => `
__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
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 => `
__cdb_iqrange AS (
SELECT max(quartile_max) - min(quartile_max) AS __cdb_iqr
FROM (
SELECT quartile, max(_cdb_iqr_column) AS quartile_max from (
SELECT ${ctx.column} AS _cdb_iqr_column, ntile(4) over (order by ${ctx.column}
) AS quartile
FROM __cdb_filtered_source) _cdb_quartiles
WHERE quartile = 1 or quartile = 3
GROUP BY quartile
) __cdb_iqr
)
`;
const binsQueryTpl = ctx => `
__cdb_bins AS (
SELECT
CASE WHEN __cdb_total_rows = 0 OR __cdb_iqr = 0
THEN 1
ELSE GREATEST(
LEAST(${ctx.minBins}, CAST(__cdb_total_rows AS INT)),
LEAST(
CAST(((__cdb_max_val - __cdb_min_val) / (2 * __cdb_iqr * power(__cdb_total_rows, 1/3))) AS INT),
${ctx.maxBins}
)
)
END AS __cdb_bins_number
FROM __cdb_basics, __cdb_iqrange, __cdb_filtered_source
LIMIT 1
)
`;
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;
/**
Numeric histogram:
{
type: 'histogram',
options: {
column: 'name', // column data type: numeric
bins: 10 // OPTIONAL
}
}
*/
module.exports = class NumericHistogram extends BaseHistogram {
constructor (query, options, queries) {
super(query, options, queries);
}
_buildQuery (psql, override, callback) {
const histogramSql = this._buildQueryTpl({
override: override,
column: this._columnType === 'date' ? columnCastTpl({ column: this.column }) : this.column,
isFloatColumn: this._columnType === 'float',
query: this.query,
start: this._getBinStart(override),
end: this._getBinEnd(override),
minBins: BIN_MIN_NUMBER,
maxBins: BIN_MAX_NUMBER,
});
debug(histogramSql);
return callback(null, histogramSql);
}
_buildQueryTpl (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)}
`;
}
_hasOverridenBins (override) {
return override && override.hasOwnProperty('bins');
}
_getSummary (result, override) {
const firstRow = result.rows[0] || {};
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,
};
}
_getBuckets (result) {
return result.rows.map(({ bin, min, max, avg, freq }) => ({ bin, min, max, avg, freq }));
}
_populateBinStart (firstRow, override = {}) {
let binStart;
if (override.hasOwnProperty('start')) {
binStart = this._getBinStart(override);
} else {
binStart = firstRow.min;
}
return binStart;
}
};

View File

@@ -1,6 +1,5 @@
module.exports = {
Aggregation: require('./aggregation'),
Formula: require('./formula'),
Histogram: require('./histogram'),
List: require('./list')
Histogram: require('./histogram')
};

View File

@@ -1,66 +0,0 @@
var dot = require('dot');
dot.templateSettings.strip = false;
var BaseWidget = require('./base');
var TYPE = 'list';
var listSqlTpl = dot.template('select {{=it._columns}} from ({{=it._query}}) as _cdb_list');
/**
{
type: 'list',
options: {
columns: ['name', 'description']
}
}
*/
function List(query, options) {
options = options || {};
if (!Array.isArray(options.columns)) {
throw new Error('List expects `columns` array in widget options');
}
BaseWidget.apply(this);
this.query = query;
this.columns = options.columns;
}
List.prototype = new BaseWidget();
List.prototype.constructor = List;
module.exports = List;
List.prototype.sql = function(psql, override, callback) {
if (!callback) {
callback = override;
}
var listSql = listSqlTpl({
_query: this.query,
_columns: this.columns.join(', ')
});
return callback(null, listSql);
};
List.prototype.format = function(result) {
return {
rows: result.rows
};
};
List.prototype.getType = function() {
return TYPE;
};
List.prototype.toString = function() {
return JSON.stringify({
_type: TYPE,
_query: this.query,
_columns: this.columns.join(', ')
});
};

View File

@@ -1,6 +1,5 @@
module.exports = {
Aggregation: require('./aggregation'),
Formula: require('./formula'),
Histogram: require('./histogram'),
List: require('./list')
Histogram: require('./histogram')
};

View File

@@ -1,11 +0,0 @@
var BaseOverviewsDataview = require('./base');
var BaseDataview = require('../list');
function List(query, options, queryRewriter, queryRewriteData, params, queries) {
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params, queries);
}
List.prototype = Object.create(BaseOverviewsDataview.prototype);
List.prototype.constructor = List;
module.exports = List;

View File

@@ -12,7 +12,8 @@ var VarnishHttpCacheBackend = require('./cache/backend/varnish_http');
var FastlyCacheBackend = require('./cache/backend/fastly');
var StatsClient = require('./stats/client');
var Profiler = require('./stats/profiler_proxy');
const stats = require('./middleware/stats');
var RendererStatsReporter = require('./stats/reporter/renderer');
var windshaft = require('windshaft');
@@ -44,6 +45,11 @@ var MapConfigAdapter = require('./models/mapconfig/adapter');
var StatsBackend = require('./backends/stats');
const lzmaMiddleware = require('./middleware/lzma');
const errorMiddleware = require('./middleware/error-middleware');
const prepareContextMiddleware = require('./middleware/context');
module.exports = function(serverOptions) {
// Make stats client globally accessible
global.statsClient = StatsClient.getInstance(serverOptions.statsd);
@@ -152,7 +158,8 @@ module.exports = function(serverOptions) {
grainstore: serverOptions.grainstore,
mapnik: serverOptions.renderer.mapnik
},
http: serverOptions.renderer.http
http: serverOptions.renderer.http,
mvt: serverOptions.renderer.mvt
});
// initialize render cache
@@ -206,12 +213,16 @@ module.exports = function(serverOptions) {
var versions = getAndValidateVersions(serverOptions);
const prepareContext = typeof serverOptions.req2params === 'function' ?
serverOptions.req2params :
prepareContextMiddleware(authApi, pgConnection);
/*******************************************************************************************************************
* Routing
******************************************************************************************************************/
new controller.Layergroup(
authApi,
prepareContext,
pgConnection,
mapStore,
tileBackend,
@@ -224,7 +235,7 @@ module.exports = function(serverOptions) {
).register(app);
new controller.Map(
authApi,
prepareContext,
pgConnection,
templateMaps,
mapBackend,
@@ -237,8 +248,7 @@ module.exports = function(serverOptions) {
).register(app);
new controller.NamedMaps(
authApi,
pgConnection,
prepareContext,
namedMapProviderCache,
tileBackend,
previewBackend,
@@ -247,9 +257,9 @@ module.exports = function(serverOptions) {
metadataBackend
).register(app);
new controller.NamedMapsAdmin(authApi, pgConnection, templateMaps).register(app);
new controller.NamedMapsAdmin(authApi, templateMaps).register(app);
new controller.Analyses(authApi, pgConnection).register(app);
new controller.Analyses(prepareContext).register(app);
new controller.ServerInfo(versions).register(app);
@@ -257,6 +267,8 @@ module.exports = function(serverOptions) {
* END Routing
******************************************************************************************************************/
app.use(errorMiddleware());
return app;
};
@@ -351,12 +363,6 @@ function bootstrap(opts) {
app.use(bodyParser.json());
app.use(function bootstrap$prepareRequestResponse(req, res, next) {
req.context = req.context || {};
req.profiler = new Profiler({
statsd_client: global.statsClient,
profile: opts.useProfiler
});
if (global.environment && global.environment.api_hostname) {
res.set('X-Served-By-Host', global.environment.api_hostname);
}
@@ -364,6 +370,13 @@ function bootstrap(opts) {
next();
});
app.use(stats({
enabled: opts.useProfiler,
statsClient: global.statsClient
}));
app.use(lzmaMiddleware);
// temporary measure until we upgrade to newer version expressjs so we can check err.status
app.use(function(err, req, res, next) {
if (err) {

View File

@@ -81,6 +81,7 @@ module.exports = {
statsInterval: rendererConfig.statsInterval
},
renderer: {
mvt: rendererConfig.mvt,
mapnik: _.defaults(rendererConfig.mapnik, {
geojson: {
dbPoolParams: {

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "windshaft-cartodb",
"version": "3.12.2",
"version": "4.0.1",
"description": "A map tile server for CartoDB",
"keywords": [
"cartodb"
@@ -17,30 +17,34 @@
"Simon Tokumine <simon@vizzuality.com>",
"Javi Santana <jsantana@vizzuality.com>",
"Sandro Santilli <strk@vizzuality.com>",
"Carlos Matallín <matallo@carto.com>"
"Carlos Matallín <matallo@carto.com>",
"Daniel Garcia Aubert <dgaubert@carto.com>",
"Mario de Frutos <mario.defrutos@carto.com>"
],
"dependencies": {
"body-parser": "~1.14.0",
"camshaft": "0.55.7",
"cartodb-psql": "0.10.1",
"cartodb-query-tables": "0.2.0",
"body-parser": "^1.18.2",
"camshaft": "0.59.4",
"cartodb-psql": "0.10.2",
"cartodb-query-tables": "0.3.0",
"cartodb-redis": "0.14.0",
"debug": "~2.2.0",
"debug": "^3.1.0",
"dot": "~1.0.2",
"express": "~4.13.3",
"express": "~4.16.0",
"fastly-purge": "~1.0.1",
"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.79.0",
"request": "^2.83.0",
"semver": "~5.3.0",
"step": "~0.0.6",
"step-profiler": "~0.3.0",
"turbo-carto": "0.19.2",
"turbo-carto": "0.20.1",
"underscore": "~1.6.0",
"windshaft": "3.3.1",
"windshaft": "~4.0.0",
"yargs": "~5.0.0"
},
"devDependencies": {
@@ -50,16 +54,21 @@
"moment": "~2.18.1",
"nock": "~2.11.0",
"redis": "~0.12.1",
"semver": "~1.1.4",
"strftime": "~0.8.2"
},
"scripts": {
"lint": "jshint lib test",
"preinstall": "make pre-install",
"test": "make test-all"
"test": "make test-all",
"update-internal-deps": "rm -rf node_modules && rm -f yarn.lock && yarn",
"docker-install": "sudo apt install docker.io && sudo usermod -aG docker $(whoami)",
"docker-pull": "docker pull cartoimages/windshaft-testing",
"docker-test": "docker run -v `pwd`:/srv cartoimages/windshaft-testing bash docker-test.sh",
"docker-bash": "docker run -it -v `pwd`:/srv cartoimages/windshaft-testing bash",
"docker-publish": "docker push cartoimages/windshaft-carto-testing"
},
"engines": {
"node": ">=6.9",
"yarn": "^0.21.3"
"yarn": ">=0.27.5 <1.0.0"
}
}

View File

@@ -137,7 +137,7 @@ if test x"$OPT_COVERAGE" = xyes; then
./node_modules/.bin/istanbul cover node_modules/.bin/_mocha -- -u tdd -t 5000 ${TESTS}
else
echo "Running tests"
mocha -u tdd -t 5000 ${TESTS}
./node_modules/.bin/_mocha -c -u tdd -t 5000 ${TESTS}
fi
ret=$?

View File

@@ -1,81 +0,0 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var TestClient = require('../../support/test-client');
describe('analysis-layers-dataviews-geojson', function() {
function createMapConfig(layers, dataviews, analysis) {
return {
version: '1.5.0',
layers: layers,
dataviews: dataviews || {},
analyses: analysis || []
};
}
var CARTOCSS = [
"#points {",
" marker-fill-opacity: 1.0;",
" marker-line-color: #FFF;",
" marker-line-width: 0.5;",
" marker-line-opacity: 1.0;",
" marker-placement: point;",
" marker-type: ellipse;",
" marker-width: 8;",
" marker-fill: red;",
" marker-allow-overlap: true;",
"}"
].join('\n');
var mapConfig = createMapConfig(
[
{
"type": "cartodb",
"options": {
"source": {
"id": "2570e105-7b37-40d2-bdf4-1af889598745"
},
"cartocss": CARTOCSS,
"cartocss_version": "2.3.0"
}
}
],
{
pop_max_histogram: {
source: {
id: '2570e105-7b37-40d2-bdf4-1af889598745'
},
type: 'histogram',
options: {
column: 'pop_max'
}
}
},
[
{
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
"type": "source",
"params": {
"query": "select * from populated_places_simple_reduced"
}
}
]
);
it('should get pop_max column from dataview', function(done) {
var testClient = new TestClient(mapConfig, 1234);
testClient.getTile(0, 0, 0, {format: 'geojson', layers: 0}, function(err, res, geojson) {
assert.ok(!err, err);
assert.ok(Array.isArray(geojson.features));
assert.ok(geojson.features.length > 0);
var feature = geojson.features[0];
assert.ok(feature.properties.hasOwnProperty('pop_max'), 'Missing pop_max property');
testClient.drain(done);
});
});
});

View File

@@ -5,34 +5,34 @@ var TestClient = require('../../support/test-client');
var dot = require('dot');
var debug = require('debug')('windshaft:cartodb:test');
describe('analysis-layers use cases', function() {
describe('analysis-layers use cases', function () {
var multitypeStyleTemplate = dot.template([
"#points['mapnik::geometry_type'=1] {",
" marker-fill-opacity: {{=it._opacity}};",
" marker-line-color: #FFF;",
" marker-line-width: 0.5;",
" marker-line-opacity: {{=it._opacity}};",
" marker-placement: point;",
" marker-type: ellipse;",
" marker-width: 8;",
" marker-fill: {{=it._color}};",
" marker-allow-overlap: true;",
"}",
"#lines['mapnik::geometry_type'=2] {",
" line-color: {{=it._color}};",
" line-width: 2;",
" line-opacity: {{=it._opacity}};",
"}",
"#polygons['mapnik::geometry_type'=3] {",
" polygon-fill: {{=it._color}};",
" polygon-opacity: {{=it._opacity}};",
" line-color: #FFF;",
" line-width: 0.5;",
" line-opacity: {{=it._opacity}};",
"}"
].join('\n'));
var multitypeStyleTemplate = dot.template(
`#points['mapnik::geometry_type'=1] {
marker-fill-opacity: {{=it._opacity}};
marker-line-color: #FFF;
marker-line-width: 0.5;
marker-line-opacity: {{=it._opacity}};
marker-placement: point;
marker-type: ellipse;
marker-width: 8;
marker-fill: {{=it._color}};
marker-allow-overlap: true;
}
#lines['mapnik::geometry_type'=2] {
line-color: {{=it._color}};
line-width: 2;
line-opacity: {{=it._opacity}};
}
#polygons['mapnik::geometry_type'=3] {
polygon-fill: {{=it._color}};
polygon-opacity: {{=it._opacity}};
line-color: #FFF;
line-width: 0.5;
line-opacity: {{=it._opacity}};
}`
);
function cartocss(color, opacity) {
@@ -47,18 +47,53 @@ describe('analysis-layers use cases', function() {
version: '1.5.0',
layers: layers,
dataviews: dataviews || {},
analysis: analysis || []
analyses: analysis || []
};
}
function analysisDef(analysis) {
return JSON.stringify(analysis);
}
var DEFAULT_MULTITYPE_STYLE = cartocss();
var TILE_ANALYSIS_TABLES = { z: 14, x: 8023, y: 6177 };
var pointInPolygonDef = {
id: 'a1',
type: 'point-in-polygon',
params: {
points_source: {
type: 'source',
params: {
query: 'select * from analysis_rent_listings'
}
},
polygons_source: {
type: 'buffer',
params: {
source: {
type: 'source',
params: {
query: 'select * from analysis_banks'
}
},
radius: 250
}
}
}
};
var bufferDef = {
id: 'b1',
type: 'buffer',
params: {
source: {
type: 'source',
params: {
query: 'select * from analysis_banks'
}
},
radius: 250
}
};
var useCases = [
{
desc: '1 mapnik layer',
@@ -68,7 +103,7 @@ describe('analysis-layers use cases', function() {
{
type: 'cartodb',
options: {
sql: "select * from analysis_rent_listings",
sql: 'select * from analysis_rent_listings',
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
@@ -83,7 +118,7 @@ describe('analysis-layers use cases', function() {
{
type: 'cartodb',
options: {
sql: "select * from analysis_banks",
sql: 'select * from analysis_banks',
cartocss: cartocss('#2167AB'),
cartocss_version: '2.3.0'
}
@@ -91,7 +126,7 @@ describe('analysis-layers use cases', function() {
{
type: 'cartodb',
options: {
sql: "select * from analysis_rent_listings",
sql: 'select * from analysis_rent_listings',
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
@@ -105,30 +140,27 @@ describe('analysis-layers use cases', function() {
{
type: 'cartodb',
options: {
sql: "select * from analysis_rent_listings",
sql: 'select * from analysis_rent_listings',
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
},
{
type: 'analysis',
type: 'cartodb',
options: {
def: analysisDef({
"type": "buffer",
"params": {
"source": {
"type": "source",
"params": {
"query": "select * from analysis_banks"
}
},
"radius": 250
}
}),
cartocss: cartocss('black', 0.5)
source: {
id: 'b1'
},
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
}
])
],
{},
[
bufferDef
]
)
},
{
@@ -137,531 +169,115 @@ describe('analysis-layers use cases', function() {
{
type: 'cartodb',
options: {
sql: "select * from analysis_rent_listings",
sql: 'select * from analysis_rent_listings',
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
},
{
type: 'analysis',
type: 'cartodb',
options: {
def: analysisDef({
"type": "point-in-polygon",
"params": {
"pointsSource": {
"type": "source",
"params": {
"query": "select * from analysis_rent_listings"
}
},
"polygonsSource": {
"type": "buffer",
"params": {
"source": {
"type": "source",
"params": {
"query": "select * from analysis_banks"
}
},
"radius": 250
}
}
}
}),
cartocss: cartocss('green', 1.0)
source: {
id: 'a1'
},
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
}
])
],
{},
[
pointInPolygonDef
]
)
},
{
desc: 'point-in-polygon from buffer atm-machines and rent listings + rent listings',
mapConfig: mapConfig([
{
type: 'analysis',
options: {
def: analysisDef({
"type": "point-in-polygon",
"params": {
"pointsSource": {
"type": "source",
"params": {
"query": "select * from analysis_rent_listings"
}
},
"polygonsSource": {
"type": "buffer",
"params": {
"source": {
"type": "source",
"params": {
"query": "select * from analysis_banks"
}
},
"radius": 250
}
}
}
}),
cartocss: cartocss('green', 1.0)
}
},
{
type: 'cartodb',
options: {
sql: "select * from analysis_rent_listings",
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
}
])
},
{
desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings',
mapConfig: mapConfig([
{
type: 'cartodb',
options: {
sql: "select * from analysis_rent_listings",
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
},
{
type: 'analysis',
options: {
def: analysisDef({
"type": "buffer",
"params": {
"source": {
"type": "source",
"params": {
"query": "select * from analysis_banks"
}
},
"radius": 300
}
}),
cartocss: cartocss('magenta', 0.5)
}
},
{
type: 'analysis',
options: {
def: analysisDef({
"type": "point-in-polygon",
"params": {
"pointsSource": {
"type": "source",
"params": {
"query": "select * from analysis_rent_listings"
}
},
"polygonsSource": {
"type": "buffer",
"params": {
"source": {
"type": "source",
"params": {
"query": "select * from analysis_banks"
}
},
"radius": 300
}
}
}
}),
cartocss: cartocss('green', 1.0)
}
}
])
},
{
skip: true,
desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings',
mapConfig: mapConfig([
{
type: 'cartodb',
options: {
"source": { id: "a" },
"cartocss": DEFAULT_MULTITYPE_STYLE,
"cartocss_version": "2.3.0"
}
},
{
type: 'cartodb',
options: {
"source": { id: "b1" },
"cartocss": cartocss('green', 1.0),
"cartocss_version": "2.3.0"
}
},
{
type: 'cartodb',
options: {
"source": { id: "b2" },
"cartocss": cartocss('magenta', 0.5),
"cartocss_version": "2.3.0"
}
}
],
[
{
id: "b2",
options: {
def: analysisDef({
"type": "count-in-polygon",
"id": "a0",
"params": {
"columnName": 'count_airbnb',
"pointsSource": {
"type": "source",
"params": {
query: "select * from analysis_rent_listings"
},
dataviews: {
price_histogram: {
type: 'histogram',
options: {
column: 'price'
}
}
}
},
"polygonsSource": {
"id": "b1",
"type": "buffer",
"params": {
"source": {
"id": "b0",
"type": "source",
"params": {
query: "select * from analysis_banks"
}
},
"radius": 250
},
dataviews: {
bank_category: {
type: 'aggregation',
options: {
column: 'bank'
}
}
}
}
},
dataviews: {
count_histogram: {
type: 'histogram',
options: {
column: 'count_airbnb'
}
}
}
}),
cartocss: cartocss('green', 1.0)
}
}
])
},
{
skip: true,
desc: 'I. Distribution centers',
mapConfig: mapConfig(
// layers
[
{
type: 'cartodb',
options: {
"source": { id: "b0" },
"cartocss": [
"#distribution_centers {",
" marker-fill-opacity: 1.0;",
" marker-line-color: #FFF;",
" marker-line-width: 0.5;",
" marker-line-opacity: 0.7;",
" marker-placement: point;",
" marker-type: ellipse;",
" marker-width: 8;",
" marker-fill: blue;",
" marker-allow-overlap: true;",
"}"
].join('\n'),
"cartocss_version": "2.3.0"
source: {
id: 'a1'
},
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
},
{
type: 'cartodb',
options: {
"source": { id: "a0" },
"cartocss": [
"#shops {",
" marker-fill-opacity: 1.0;",
" marker-line-color: #FFF;",
" marker-line-width: 0.5;",
" marker-line-opacity: 0.7;",
" marker-placement: point;",
" marker-type: ellipse;",
" marker-width: 8;",
" marker-fill: red;",
" marker-allow-overlap: true;",
"}"
].join('\n'),
"cartocss_version": "2.3.0"
}
},
{
type: 'cartodb',
options: {
"source": { id: "a1" },
"cartocss": [
"#routing {",
" line-color: ramp([routing_time], colorbrewer(Reds));",
" line-width: ramp([routing_time], 2, 8);",
" line-opacity: 1.0;",
"}"
].join('\n'),
"cartocss_version": "2.3.0"
sql: 'select * from analysis_rent_listings',
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
}
],
// dataviews
{
distribution_center_name_category: {
source: { id: 'b0' },
type: 'aggregation',
options: {
column: 'name'
}
},
time_histogram: {
source: { id: 'a1' },
type: 'histogram',
options: {
column: 'routing_time'
}
},
distance_histogram: {
source: { id: 'a1' },
type: 'histogram',
options: {
column: 'routing_distance'
}
}
},
// analysis
{},
[
{
id: 'a1',
type: 'routing-n-to-n',
params: {
// distanceColumn: 'routing_distance',
// timeColumn: 'routing_time',
originSource: {
id: 'b0',
type: 'source',
params: {
query: 'select * from distribution_centers'
}
},
destinationSource: {
id: 'a0',
type: 'source',
params: {
query: 'select * from shops'
}
}
}
}
pointInPolygonDef
]
)
},
{
skip: true,
desc: 'II. Population analysis',
desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings',
mapConfig: mapConfig(
// layers
[
{
type: 'cartodb',
options: {
"source": { id: "a2" },
"cartocss": [
"#count_in_polygon {",
" polygon-opacity: 1.0",
" line-color: #FFF;",
" line-width: 0.5;",
" line-opacity: 0.7",
" polygon-fill: ramp([estimated_people], colorbrewer(Reds));",
"}"
].join('\n'),
"cartocss_version": "2.3.0"
}
},
{
type: 'cartodb',
options: {
"source": { id: "a0" },
"cartocss": DEFAULT_MULTITYPE_STYLE,
"cartocss_version": "2.3.0"
}
}
],
// dataviews
{
total_population_formula: {
"source": { id: "a3" },
type: 'formula',
options: {
column: 'total_population',
operation: 'sum'
}
},
people_histogram: { // this injects a range filter at `a2` node output
"source": { id: "a2" },
type: 'histogram',
options: {
column: 'estimated_people'
}
},
subway_line_category: { // this injects a category filter at `a0` node output
"source": { id: "a0" },
type: 'aggregation',
options: {
column: 'subway_line'
}
}
},
// analysis
[
{
id: 'a3',
// this will union the polygons, produce just one polygon, and calculate the total population for it
type: 'total-population',
params: {
columnName: 'total_population',
source: {
id: 'a2',
type: 'estimated-population',
params: {
columnName: 'estimated_people',
source: {
id: 'a1',
type: 'trade-area',
params: {
source: {
"id": "a0",
"type": "source",
"params": {
query: "select * from subway_stops"
}
},
kind: 'walk',
time: 300
}
}
}
}
}
}
])
},
{
skip: true,
desc: 'III. Point in polygon',
mapConfig: mapConfig(
// layers
[
{
type: 'cartodb',
options: {
"source": { id: "a1" },
"cartocss": [
"#count_in_polygon {",
" polygon-opacity: 1.0",
" line-color: #FFF;",
" line-width: 0.5;",
" line-opacity: 0.7",
" polygon-fill: ramp([count_people], colorbrewer(Reds));",
"}"
].join('\n'),
"cartocss_version": "2.3.0"
sql: 'select * from analysis_rent_listings',
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
},
{
type: 'cartodb',
options: {
source: {
id: 'a1'
},
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
},
{
type: 'cartodb',
options: {
source: {
id: 'b1'
},
cartocss: DEFAULT_MULTITYPE_STYLE,
cartocss_version: '2.3.0'
}
}
],
// dataviews
{
age_histogram: {
"source": { id: "a0" },
type: 'histogram',
options: {
column: 'age'
}
},
income_histogram: {
"source": { id: "a0" },
type: 'histogram',
options: {
column: 'income'
}
},
gender_category: {
"source": { id: "a0" },
type: 'aggregation',
options: {
column: 'gender'
}
}
},
// analysis
{},
[
{
"id": "a1",
"type": "count-in-polygon",
"params": {
"columnName": 'count_people',
"pointsSource": {
"id": 'a0',
"type": "source",
"params": {
query: "select the_geom, age, gender, income from people"
}
},
"polygonsSource": {
"id": "b0",
"type": "source",
"params": {
query: "select * from postal_codes"
}
}
}
}
bufferDef,
pointInPolygonDef
]
)
}
];
useCases.forEach(function(useCase, imageIdx) {
useCases.forEach(function (useCase) {
if (!!useCase.skip) {
debug(JSON.stringify(useCase.mapConfig, null, 4));
return debug(JSON.stringify(useCase.mapConfig, null, 4));
}
it.skip('should implement use case: "' + useCase.desc + '"', function(done) {
it(`should implement use case: '${useCase.desc}'`, function (done) {
var testClient = new TestClient(useCase.mapConfig, 1234);
var tile = useCase.tile || TILE_ANALYSIS_TABLES;
testClient.getTile(tile.z, tile.x, tile.y, function(err, res, image) {
testClient.getTile(tile.z, tile.x, tile.y, function (err, res, image) {
assert.ok(!err, err);
image.save('/tmp/tests/' + imageIdx + '---' + useCase.desc.replace(/\s/g, '-') + '.png');
//image.save('/tmp/tests/' + imageIdx + '---' + useCase.desc.replace(/\s/g, '-') + '.png');
assert.equal(image.width(), 256);

View File

@@ -7,7 +7,7 @@ var serverOptions = require('../../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
var TestClient = require('../../support/test-client');
var LayergroupToken = require('../../support/layergroup-token');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
describe('named-maps analysis', function() {

View File

@@ -3,6 +3,7 @@ require('../support/test_helper');
var fs = require('fs');
var assert = require('../support/assert');
var TestClient = require('../support/test-client');
var serverOptions = require('../../lib/cartodb/server_options');
var mapnik = require('windshaft').mapnik;
var IMAGE_TOLERANCE_PER_MIL = 5;
@@ -124,24 +125,37 @@ describe('buffer size per format', function () {
}
];
afterEach(function(done) {
if (this.testClient) {
return this.testClient.drain(done);
}
return done();
});
const originalUsePostGIS = serverOptions.renderer.mvt.usePostGIS;
testCases.forEach(function (test) {
it(test.desc, function (done) {
var testClient = new TestClient(test.mapConfig, 1234);
var coords = test.coords;
var options = {
format: test.format,
layers: test.layers
};
testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
assert.ifError(err);
// To generate images use:
// tile.save(test.fixturePath);
test.assert(tile, function (err) {
var testFn = (usePostGIS) => {
it(test.desc, function (done) {
serverOptions.renderer.mvt.usePostGIS = usePostGIS;
this.testClient = new TestClient(test.mapConfig, 1234);
serverOptions.renderer.mvt.usePostGIS = originalUsePostGIS;
var coords = test.coords;
var options = {
format: test.format,
layers: test.layers
};
this.testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
assert.ifError(err);
testClient.drain(done);
// To generate images use:
// tile.save(test.fixturePath);
test.assert(tile, done);
});
});
});
};
if (process.env.POSTGIS_VERSION === '2.4' && test.format === 'mvt'){
testFn(true);
}
testFn(false);
});
});
@@ -260,23 +274,27 @@ describe('buffer size per format for named maps', function () {
}
];
afterEach(function(done) {
if (this.testClient) {
return this.testClient.drain(done);
}
return done();
});
testCases.forEach(function (test) {
it(test.desc, function (done) {
var testClient = new TestClient(test.template, 1234);
this.testClient = new TestClient(test.template, 1234);
var coords = test.coords;
var options = {
format: test.format,
placeholders: test.placeholders,
layers: test.layers
};
testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
this.testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
assert.ifError(err);
// To generate images use:
//tile.save('./test/fixtures/buffer-size/tile-7.64.48-buffer-size-0-test.png');
test.assert(tile, function (err) {
assert.ifError(err);
testClient.drain(done);
});
test.assert(tile, done);
});
});
});
@@ -416,26 +434,40 @@ describe('buffer size per format for named maps w/o placeholders', function () {
];
afterEach(function(done) {
if (this.testClient) {
return this.testClient.drain(done);
}
return done();
});
const originalUsePostGIS = serverOptions.renderer.mvt.usePostGIS;
testCases.forEach(function (test) {
it(test.desc, function (done) {
var testClient = new TestClient(test.template, 1234);
var coords = test.coords;
var options = {
format: test.format,
placeholders: test.placeholders,
layers: test.layers
};
testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
assert.ifError(err);
// To generate images use:
//tile.save(test.fixturePath);
// require('fs').writeFileSync(test.fixturePath, JSON.stringify(tile));
// require('fs').writeFileSync(test.fixturePath, tile.getDataSync());
test.assert(tile, function (err) {
assert.ifError(err);
testClient.drain(done);
var testFn = (usePostGIS) => {
it(test.desc + `(${usePostGIS? 'PostGIS':'mapnik'})`, function (done) {
serverOptions.renderer.mvt.usePostGIS = usePostGIS;
test.template.name += '_1';
this.testClient = new TestClient(test.template, 1234);
serverOptions.renderer.mvt.usePostGIS = originalUsePostGIS;
var coords = test.coords;
var options = {
format: test.format,
placeholders: test.placeholders,
layers: test.layers
};
this.testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
assert.ifError(err);
// To generate images use:
//tile.save(test.fixturePath);
// require('fs').writeFileSync(test.fixturePath, JSON.stringify(tile));
// require('fs').writeFileSync(test.fixturePath, tile.getDataSync());
test.assert(tile, done);
});
});
});
});
};
if (process.env.POSTGIS_VERSION === '2.4' && test.format === 'mvt'){
testFn(true);
}
testFn(false);
});
});

View File

@@ -8,7 +8,7 @@ var serverOptions = require('../../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
var LayergroupToken = require('../../support/layergroup-token');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
describe('get requests with cache headers', function() {

View File

@@ -332,6 +332,21 @@ 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
};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview(test.dataviewId, 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);
done();
});
});
it('should return same histogram ' + test.desc, function (done) {
var params = {

View File

@@ -1,6 +1,6 @@
var assert = require('../support/assert');
var step = require('step');
var LayergroupToken = require('../support/layergroup-token');
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
var testHelper = require(__dirname + '/../support/test_helper');
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');

View File

@@ -1,343 +0,0 @@
require('../support/test_helper');
var assert = require('../support/assert');
var TestClient = require('../support/test-client');
describe('use only needed columns', function() {
function getFeatureByCartodbId(features, cartodbId) {
for (var i = 0, len = features.length; i < len; i++) {
if (features[i].properties.cartodb_id === cartodbId) {
return features[i];
}
}
return {};
}
var options = { format: 'geojson', layer: 0 };
afterEach(function(done) {
if (this.testClient) {
this.testClient.drain(done);
} else {
done();
}
});
it('with aggregation widget, interactivity and cartocss columns', function(done) {
var widgetMapConfig = {
version: '1.5.0',
layers: [{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced',
cartocss: '#layer0 { marker-fill: red; marker-width: 10; [name="Madrid"] { marker-fill: green; } }',
cartocss_version: '2.0.1',
widgets: {
adm0name: {
type: 'aggregation',
options: {
column: 'adm0name',
aggregation: 'sum',
aggregationColumn: 'pop_max'
}
}
},
interactivity: "cartodb_id,pop_min"
}
}]
};
this.testClient = new TestClient(widgetMapConfig);
this.testClient.getTile(0, 0, 0, options, function (err, res, geojsonTile) {
assert.ok(!err, err);
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
cartodb_id: 1109,
name: 'Mardin',
adm0name: 'Turkey',
pop_max: 71373,
pop_min: 57586
});
done();
});
});
it('should not duplicate columns', function(done) {
var widgetMapConfig = {
version: '1.5.0',
layers: [{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced',
cartocss: ['#layer0 {',
'marker-fill: red;',
'marker-width: 10;',
'[name="Madrid"] { marker-fill: green; } ',
'[pop_max>100000] { marker-fill: black; } ',
'}'].join('\n'),
cartocss_version: '2.3.0',
widgets: {
adm0name: {
type: 'aggregation',
options: {
column: 'adm0name',
aggregation: 'sum',
aggregationColumn: 'pop_max'
}
}
},
interactivity: "cartodb_id,pop_max"
}
}]
};
this.testClient = new TestClient(widgetMapConfig);
this.testClient.getTile(0, 0, 0, options, function (err, res, geojsonTile) {
assert.ok(!err, err);
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
cartodb_id: 1109,
name: 'Mardin',
adm0name: 'Turkey',
pop_max: 71373
});
done();
});
});
it('with formula widget, no interactivity and no cartocss columns', function(done) {
var formulaWidgetMapConfig = {
version: '1.5.0',
layers: [{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced where pop_max > 0 and pop_max < 600000',
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }',
cartocss_version: '2.0.1',
interactivity: 'cartodb_id',
widgets: {
pop_max_f: {
type: 'formula',
options: {
column: 'pop_max',
operation: 'count'
}
}
}
}
}]
};
this.testClient = new TestClient(formulaWidgetMapConfig);
this.testClient.getTile(0, 0, 0, options, function (err, res, geojsonTile) {
assert.ok(!err, err);
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
cartodb_id: 1109,
pop_max: 71373
});
done();
});
});
it('with cartocss with multiple expressions', function(done) {
var formulaWidgetMapConfig = {
version: '1.5.0',
layers: [{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced where pop_max > 0 and pop_max < 600000',
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }' +
'#layer0 { marker-width: 14; [name="Madrid"] { marker-width: 20; } }' +
'#layer0[pop_max>1000] { marker-width: 14; [name="Madrid"] { marker-width: 20; } }' +
'#layer0[adm0name=~".*Turkey*"] { marker-width: 14; [name="Madrid"] { marker-width: 20; } }',
cartocss_version: '2.0.1',
interactivity: 'cartodb_id'
}
}]
};
this.testClient = new TestClient(formulaWidgetMapConfig);
this.testClient.getTile(0, 0, 0, options, function (err, res, geojsonTile) {
assert.ok(!err, err);
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
cartodb_id: 1109,
pop_max:71373,
name:"Mardin",
adm0name:"Turkey"
});
done();
});
});
it('should work with mapnik substitution tokens', function(done) {
var cartocss = [
"#layer {",
" line-width: 2;",
" line-color: #3B3B58;",
" line-opacity: 1;",
" polygon-opacity: 0.7;",
" polygon-fill: ramp([points_count], (#E5F5F9,#99D8C9,#2CA25F))",
"}"
].join('\n');
var sql = [
'WITH hgrid AS (',
' SELECT CDB_HexagonGrid(',
' ST_Expand(!bbox!, greatest(!pixel_width!,!pixel_height!) * 100),',
' greatest(!pixel_width!,!pixel_height!) * 100',
' ) as cell',
')',
'SELECT',
' hgrid.cell as the_geom_webmercator,',
' count(1) as points_count,',
' count(1)/power(100 * CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!)), 2) as points_density,',
' 1 as cartodb_id',
'FROM hgrid, (SELECT * FROM populated_places_simple_reduced) i',
'where ST_Intersects(i.the_geom_webmercator, hgrid.cell)',
'GROUP BY hgrid.cell'
].join('\n');
var mapConfig = {
"version": "1.4.0",
"layers": [
{
"type": 'mapnik',
"options": {
"cartocss_version": '2.3.0',
"sql": sql,
"cartocss": cartocss
}
}
]
};
this.testClient = new TestClient(mapConfig);
this.testClient.getTile(0, 0, 0, { format: 'geojson', layer: 0 }, function(err, res, geojson) {
assert.ok(!err, err);
assert.ok(geojson);
assert.equal(geojson.features.length, 5);
done();
});
});
it('should skip empty and null columns for geojson tiles', function(done) {
var mapConfig = {
"analyses": [
{
"id": "a0",
"params": {
"query": "SELECT * FROM test_table"
},
"type": "source"
}
],
"dataviews": {
"4e7b0e07-6d21-4b83-9adb-6d7e17eea6ca": {
"options": {
"aggregationColumn": null,
"column": "cartodb_id",
"operation": "avg"
},
"source": {
"id": "a0"
},
"type": "formula"
},
"74f590f8-625c-4e95-922f-34ad3e9919c0": {
"options": {
"aggregation": "sum",
"aggregationColumn": "cartodb_id",
"column": "name"
},
"source": {
"id": "a0"
},
"type": "aggregation"
},
"98a75757-3006-400a-b028-fb613a6c0b69": {
"options": {
"aggregationColumn": null,
"column": "cartodb_id",
"operation": "sum"
},
"source": {
"id": "a0"
},
"type": "formula"
},
"ebbc97b2-87d2-4895-9e1f-2f012df3679d": {
"options": {
"aggregationColumn": null,
"bins": "12",
"column": "cartodb_id"
},
"source": {
"id": "a0"
},
"type": "histogram"
},
"ebc0653f-3581-469c-8b31-c969e440a865": {
"options": {
"aggregationColumn": null,
"column": "cartodb_id",
"operation": "avg"
},
"source": {
"id": "a0"
},
"type": "formula"
}
},
"layers": [
{
"options": {
"subdomains": "abcd",
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png"
},
"type": "http"
},
{
"options": {
"attributes": {
"columns": [
"name",
"address"
],
"id": "cartodb_id"
},
"cartocss": "#layer { marker-width: 10; marker-fill: red; }",
"cartocss_version": "2.3.0",
"interactivity": "cartodb_id",
"layer_name": "wadus",
"source": {
"id": "a0"
}
},
"type": "cartodb"
},
{
"options": {
"subdomains": "abcd",
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png"
},
"type": "http"
}
]
};
this.testClient = new TestClient(mapConfig);
this.testClient.getTile(0, 0, 0, { format: 'geojson', layer: 0 }, function(err, res, geojson) {
assert.ok(!err, err);
assert.ok(geojson);
assert.equal(geojson.features.length, 5);
assert.deepEqual(Object.keys(geojson.features[0].properties), ['cartodb_id', 'name']);
done();
});
});
});

View File

@@ -9,7 +9,7 @@ var mapnik = require('windshaft').mapnik;
var semver = require('semver');
var helper = require(__dirname + '/../support/test_helper');
var LayergroupToken = require('../support/layergroup-token');
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
var windshaft_fixtures = __dirname + '/../../node_modules/windshaft/test/fixtures';

View File

@@ -4,7 +4,7 @@ var assert = require('../support/assert');
var _ = require('underscore');
var LayergroupToken = require('../support/layergroup-token');
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
var PgQueryRunner = require('../../lib/cartodb/backends/pg_query_runner');
var QueryTables = require('cartodb-query-tables');

View File

@@ -1,9 +1,10 @@
require('../support/test_helper');
const assert = require('../support/assert');
const TestClient = require('../support/test-client');
var assert = require('../support/assert');
var TestClient = require('../support/test-client');
var serverOptions = require('../../lib/cartodb/server_options');
function createMapConfig (sql = TestClient.SQL.ONE_POINT) {
function createMapConfig(sql = TestClient.SQL.ONE_POINT) {
return {
version: '1.6.0',
layers: [{
@@ -18,7 +19,100 @@ function createMapConfig (sql = TestClient.SQL.ONE_POINT) {
};
}
describe('mvt', function () {
describe('mvt (mapnik)', mvt(false));
if (process.env.POSTGIS_VERSION === '2.4') {
describe('mvt (postgis)', mvt(true));
}
function mvt(usePostGIS) {
return function () {
const originalUsePostGIS = serverOptions.renderer.mvt.usePostGIS;
before(function () {
serverOptions.renderer.mvt.usePostGIS = usePostGIS;
});
after(function (){
serverOptions.renderer.mvt.usePostGIS = originalUsePostGIS;
});
describe('analysis-layers-dataviews-mvt', function () {
function createMapConfig(layers, dataviews, analysis) {
return {
version: '1.5.0',
layers: layers,
dataviews: dataviews || {},
analyses: analysis || []
};
}
var CARTOCSS = [
"#points {",
" marker-fill-opacity: 1.0;",
" marker-line-color: #FFF;",
" marker-line-width: 0.5;",
" marker-line-opacity: 1.0;",
" marker-placement: point;",
" marker-type: ellipse;",
" marker-width: 8;",
" marker-fill: red;",
" marker-allow-overlap: true;",
"}"
].join('\n');
var mapConfig = createMapConfig(
[
{
"type": "cartodb",
"options": {
"source": {
"id": "2570e105-7b37-40d2-bdf4-1af889598745"
},
"cartocss": CARTOCSS,
"cartocss_version": "2.3.0"
}
}
],
{
pop_max_histogram: {
source: {
id: '2570e105-7b37-40d2-bdf4-1af889598745'
},
type: 'histogram',
options: {
column: 'pop_max'
}
}
},
[
{
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
"type": "source",
"params": {
"query": "select * from populated_places_simple_reduced"
}
}
]
);
it('should get pop_max column from dataview', function (done) {
var testClient = new TestClient(mapConfig);
testClient.getTile(0, 0, 0, { format: 'mvt', layers: 0 }, function (err, res, MVT) {
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
assert.ok(!err, err);
assert.ok(Array.isArray(geojsonTile.features));
assert.ok(geojsonTile.features.length > 0);
var feature = geojsonTile.features[0];
assert.ok(feature.properties.hasOwnProperty('pop_max'), 'Missing pop_max property');
testClient.drain(done);
});
});
});
const testCases = [
{
desc: 'should get empty mvt with code 204 (no content)',
@@ -48,16 +142,366 @@ describe('mvt', function () {
testCases.forEach(function (test) {
it(test.desc, done => {
const testClient = new TestClient(test.mapConfig, 1234);
var testClient = new TestClient(test.mapConfig);
const { z, x, y } = test.coords;
const { format, response } = test;
testClient.getTile(z, x, y, { format, response }, (err, res) => {
testClient.getTile(z, x, y, { format, response }, err => {
assert.ifError(err);
assert.equal(res.statusCode, test.response.status);
testClient.drain(done);
});
});
});
});
if (usePostGIS){
describe('use only needed columns', onlyNeededColumns);
}else{
describe.skip('use only needed columns', onlyNeededColumns);
}
function onlyNeededColumns() {
function getFeatureByCartodbId(features, cartodbId) {
for (var i = 0, len = features.length; i < len; i++) {
if (features[i].properties.cartodb_id === cartodbId) {
return features[i];
}
}
return {};
}
var options = { format: 'mvt', layer: 0 };
afterEach(function (done) {
if (this.testClient) {
this.testClient.drain(done);
} else {
done();
}
});
it('with aggregation widget, interactivity and cartocss columns', function (done) {
var widgetMapConfig = {
version: '1.5.0',
layers: [{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced',
cartocss:
'#layer0 { marker-fill: red; marker-width: 10; [name="Madrid"] { marker-fill: green; } }',
cartocss_version: '2.0.1',
widgets: {
adm0name: {
type: 'aggregation',
options: {
column: 'adm0name',
aggregation: 'sum',
aggregationColumn: 'pop_max'
}
}
},
interactivity: "cartodb_id,pop_min"
}
}]
};
this.testClient = new TestClient(widgetMapConfig);
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
assert.ok(!err, err);
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
cartodb_id: 1109,
name: 'Mardin',
adm0name: 'Turkey',
pop_max: 71373,
pop_min: 57586
});
done();
});
});
it('should not duplicate columns', function (done) {
var widgetMapConfig = {
version: '1.5.0',
layers: [{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced',
cartocss: ['#layer0 {',
'marker-fill: red;',
'marker-width: 10;',
'[name="Madrid"] { marker-fill: green; } ',
'[pop_max>100000] { marker-fill: black; } ',
'}'].join('\n'),
cartocss_version: '2.3.0',
widgets: {
adm0name: {
type: 'aggregation',
options: {
column: 'adm0name',
aggregation: 'sum',
aggregationColumn: 'pop_max'
}
}
},
interactivity: "cartodb_id,pop_max"
}
}]
};
this.testClient = new TestClient(widgetMapConfig);
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
assert.ok(!err, err);
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
cartodb_id: 1109,
name: 'Mardin',
adm0name: 'Turkey',
pop_max: 71373
});
done();
});
});
it('with formula widget, no interactivity and no cartocss columns', function (done) {
var formulaWidgetMapConfig = {
version: '1.5.0',
layers: [{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced where pop_max > 0 and pop_max < 600000',
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }',
cartocss_version: '2.0.1',
interactivity: 'cartodb_id',
widgets: {
pop_max_f: {
type: 'formula',
options: {
column: 'pop_max',
operation: 'count'
}
}
}
}
}]
};
this.testClient = new TestClient(formulaWidgetMapConfig);
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
assert.ok(!err, err);
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
cartodb_id: 1109,
pop_max: 71373
});
done();
});
});
it('with cartocss with multiple expressions', function (done) {
var formulaWidgetMapConfig = {
version: '1.5.0',
layers: [{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced where pop_max > 0 and pop_max < 600000',
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }' +
'#layer0 { marker-width: 14; [name="Madrid"] { marker-width: 20; } }' +
'#layer0[pop_max>1000] { marker-width: 14; [name="Madrid"] { marker-width: 20; } }' +
'#layer0[adm0name=~".*Turkey*"] { marker-width: 14; [name="Madrid"] { marker-width: 20; } }',
cartocss_version: '2.0.1',
interactivity: 'cartodb_id'
}
}]
};
this.testClient = new TestClient(formulaWidgetMapConfig);
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
assert.ok(!err, err);
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
cartodb_id: 1109,
pop_max: 71373,
name: "Mardin",
adm0name: "Turkey"
});
done();
});
});
var skipOnPostGIS = usePostGIS ? it.skip: it;
skipOnPostGIS('should work with mapnik substitution tokens', function (done) {
var cartocss = [
"#layer {",
" line-width: 2;",
" line-color: #3B3B58;",
" line-opacity: 1;",
" polygon-opacity: 0.7;",
" polygon-fill: ramp([points_count], (#E5F5F9,#99D8C9,#2CA25F))",
"}"
].join('\n');
var sql = [
'WITH hgrid AS (',
' SELECT CDB_HexagonGrid(',
' ST_Expand(!bbox!, greatest(!pixel_width!,!pixel_height!) * 100),',
' greatest(!pixel_width!,!pixel_height!) * 100',
' ) as cell',
')',
'SELECT',
' hgrid.cell as the_geom_webmercator,',
' count(1) as points_count,',
' count(1)/power(100 * CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!)), 2) as points_density,',
' 1 as cartodb_id',
'FROM hgrid, (SELECT * FROM populated_places_simple_reduced) i',
'where ST_Intersects(i.the_geom_webmercator, hgrid.cell)',
'GROUP BY hgrid.cell'
].join('\n');
var mapConfig = {
"version": "1.4.0",
"layers": [
{
"type": 'mapnik',
"options": {
"cartocss_version": '2.3.0',
"sql": sql,
"cartocss": cartocss
}
}
]
};
this.testClient = new TestClient(mapConfig);
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
assert.ok(!err, err);
assert.ok(geojsonTile);
assert.equal(geojsonTile.features.length, 5);
done();
});
});
it('should skip empty and null columns for geojson tiles', function (done) {
var mapConfig = {
"analyses": [
{
"id": "a0",
"params": {
"query": "SELECT * FROM test_table"
},
"type": "source"
}
],
"dataviews": {
"4e7b0e07-6d21-4b83-9adb-6d7e17eea6ca": {
"options": {
"aggregationColumn": null,
"column": "cartodb_id",
"operation": "avg"
},
"source": {
"id": "a0"
},
"type": "formula"
},
"74f590f8-625c-4e95-922f-34ad3e9919c0": {
"options": {
"aggregation": "sum",
"aggregationColumn": "cartodb_id",
"column": "name"
},
"source": {
"id": "a0"
},
"type": "aggregation"
},
"98a75757-3006-400a-b028-fb613a6c0b69": {
"options": {
"aggregationColumn": null,
"column": "cartodb_id",
"operation": "sum"
},
"source": {
"id": "a0"
},
"type": "formula"
},
"ebbc97b2-87d2-4895-9e1f-2f012df3679d": {
"options": {
"aggregationColumn": null,
"bins": "12",
"column": "cartodb_id"
},
"source": {
"id": "a0"
},
"type": "histogram"
},
"ebc0653f-3581-469c-8b31-c969e440a865": {
"options": {
"aggregationColumn": null,
"column": "cartodb_id",
"operation": "avg"
},
"source": {
"id": "a0"
},
"type": "formula"
}
},
"layers": [
{
"options": {
"subdomains": "abcd",
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png"
},
"type": "http"
},
{
"options": {
"attributes": {
"columns": [
"name",
"address"
],
"id": "cartodb_id"
},
"cartocss": "#layer { marker-width: 10; marker-fill: red; }",
"cartocss_version": "2.3.0",
"interactivity": "cartodb_id",
"layer_name": "wadus",
"source": {
"id": "a0"
}
},
"type": "cartodb"
},
{
"options": {
"subdomains": "abcd",
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png"
},
"type": "http"
}
]
};
this.testClient = new TestClient(mapConfig);
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
assert.ok(!err, err);
assert.ok(geojsonTile);
assert.equal(geojsonTile.features.length, 5);
assert.deepEqual(Object.keys(geojsonTile.features[0].properties), ['cartodb_id', 'name']);
done();
});
});
}
};
}

View File

@@ -5,7 +5,7 @@ var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
var LayergroupToken = require('../support/layergroup-token');
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
var RedisPool = require('redis-mpool');
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');

View File

@@ -192,12 +192,36 @@ describe('named maps static view', function() {
}
getStaticMap({ zoom: 3 }, function(err, img) {
assert.ok(!err);
img.save('/tmp/static.png');
assert.imageIsSimilarToFile(img, previewFixture('override-zoom'), IMAGE_TOLERANCE, done);
});
});
});
it('should return override bbox', function (done) {
var view = {
bounds: {
west: 0,
south: 0,
east: 45,
north: 45
},
zoom: 4,
center: {
lng: 40,
lat: 20
}
};
templateMaps.addTemplate(username, createTemplate(view), function (err) {
if (err) {
return done(err);
}
getStaticMap({ bbox: '0,45,90,45' }, function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('override-bbox'), IMAGE_TOLERANCE, done);
});
});
});
it('should allow to select the layers to render', function (done) {
var view = {
bounds: {

View File

@@ -5,7 +5,7 @@ var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
var LayergroupToken = require('../support/layergroup-token');
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
var RedisPool = require('redis-mpool');

View File

@@ -5,7 +5,7 @@ var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
var LayergroupToken = require('../support/layergroup-token');
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
var RedisPool = require('redis-mpool');

View File

@@ -6,7 +6,7 @@ var cartodbServer = require('../../../lib/cartodb/server');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../support/layergroup-token');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
describe('attributes', function() {

View File

@@ -7,7 +7,7 @@ var step = require('step');
var mapnik = require('windshaft').mapnik;
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var LayergroupToken = require('../../support/layergroup-token');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('multilayer', function() {

View File

@@ -5,7 +5,7 @@ var _ = require('underscore');
var cartodbServer = require('../../../lib/cartodb/server');
var getLayerTypeFn = require('windshaft').model.MapConfig.prototype.getType;
var PortedServerOptions = require('./support/ported_server_options');
var LayergroupToken = require('../../support/layergroup-token');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
var BaseController = require('../../../lib/cartodb/controllers/base');

View File

@@ -6,7 +6,7 @@ var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../support/layergroup-token');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
describe('raster', function() {

View File

@@ -6,7 +6,7 @@ var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../support/layergroup-token');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
describe('retina support', function() {

View File

@@ -7,7 +7,7 @@ var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../support/layergroup-token');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 85;

View File

@@ -1,7 +1,7 @@
var _ = require('underscore');
var serverOptions = require('../../../../lib/cartodb/server_options');
var LayergroupToken = require('../../../support/layergroup-token');
var mapnik = require('windshaft').mapnik;
var LayergroupToken = require('../../../../lib/cartodb/models/layergroup-token');
var OverviewsQueryRewriter = require('../../../../lib/cartodb/utils/overviews_query_rewriter');
var overviewsQueryRewriter = new OverviewsQueryRewriter({
zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)'
@@ -48,7 +48,7 @@ module.exports = _.extend({}, serverOptions, {
log_format: null, // do not log anything
afterLayergroupCreateCalls: 0,
useProfiler: true,
req2params: function(req, callback){
req2params: function(req, res, callback){
if ( req.query.testUnexpectedError ) {
return callback('test unexpected error');
@@ -56,13 +56,14 @@ module.exports = _.extend({}, serverOptions, {
// this is in case you want to test sql parameters eg ...png?sql=select * from my_table limit 10
req.params = _.extend({}, req.params);
if (req.params.token) {
req.params.token = LayergroupToken.parse(req.params.token).token;
}
_.extend(req.params, req.query);
req.params.user = 'localhost';
req.context = {user: 'localhost'};
res.locals.user = 'localhost';
req.params.dbhost = global.environment.postgres.host;
req.params.dbport = req.params.dbport || global.environment.postgres.port;
@@ -73,6 +74,9 @@ module.exports = _.extend({}, serverOptions, {
}
req.params.dbname = 'test_windshaft_cartodb_user_1_db';
// add all params to res.locals
res.locals = _.extend({}, req.params);
// increment number of calls counter
global.req2params_calls = global.req2params_calls ? global.req2params_calls + 1 : 1;

View File

@@ -1,5 +1,5 @@
var testHelper = require('../../../support/test_helper');
var LayergroupToken = require('../../../support/layergroup-token');
var LayergroupToken = require('../../../../lib/cartodb/models/layergroup-token');
var step = require('step');
var assert = require('../../../support/assert');

View File

@@ -7,7 +7,7 @@ var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../support/layergroup-token');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
describe('torque', function() {

View File

@@ -5,7 +5,7 @@ var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../support/layergroup-token');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
describe('torque boundary points', function() {

View File

@@ -23,7 +23,7 @@ var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
var LayergroupToken = require('../support/layergroup-token');
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
describe('template_api', function() {
server.layergroupAffectedTablesCache.cache.reset();

View File

@@ -1,6 +1,6 @@
var assert = require('../../support/assert');
var step = require('step');
var LayergroupToken = require('../../support/layergroup-token');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
var testHelper = require('../../support/test_helper');
var CartodbWindshaft = require('../../../lib/cartodb/server');
var serverOptions = require('../../../lib/cartodb/server_options');

View File

@@ -2,6 +2,7 @@ require('../support/test_helper');
const assert = require('../support/assert');
const TestClient = require('../support/test-client');
var serverOptions = require('../../lib/cartodb/server_options');
const timeoutErrorTilePath = `${process.cwd()}/assets/render-timeout-fallback.png`;
@@ -200,49 +201,59 @@ describe('user render timeout limit', function () {
});
});
describe('vector', function () {
beforeEach(function (done) {
const mapconfig = createMapConfig();
this.testClient = new TestClient(mapconfig, 1234);
this.testClient.setUserRenderTimeoutLimit('localhost', 50, done);
});
if (process.env.POSTGIS_VERSION === '2.4') {
describe('vector (PostGIS)', vector(true));
}
afterEach(function (done) {
this.testClient.setUserRenderTimeoutLimit('localhost', 0, (err) => {
if (err) {
return done(err);
}
this.testClient.drain(done);
describe('vector (mapnik)', vector(false));
function vector(usePostGIS) {
const originalUsePostGIS = serverOptions.renderer.mvt.usePostGIS;
return function () {
beforeEach(function (done) {
serverOptions.renderer.mvt.usePostGIS = usePostGIS;
const mapconfig = createMapConfig();
this.testClient = new TestClient(mapconfig, 1234);
this.testClient.setUserDatabaseTimeoutLimit(50, done);
});
});
it('layergroup creation works but vector tile request fails due to render timeout', function (done) {
const params = {
format: 'mvt',
response: {
status: 429,
headers: {
'Content-Type': 'application/json; charset=utf-8'
afterEach(function (done) {
serverOptions.renderer.mvt.usePostGIS = originalUsePostGIS;
this.testClient.setUserDatabaseTimeoutLimit(0, (err) => {
if (err) {
return done(err);
}
}
};
this.testClient.getTile(0, 0, 0, params, (err, res, tile) => {
assert.ifError(err);
assert.deepEqual(tile, {
errors: ['You are over platform\'s limits. Please contact us to know more details'],
errors_with_context: [{
type: 'limit',
subtype: 'render',
message: 'You are over platform\'s limits. Please contact us to know more details'
}]
this.testClient.drain(done);
});
done();
});
});
});
it('layergroup creation works but vector tile request fails due to render timeout', function (done) {
const params = {
format: 'mvt',
response: {
status: 429,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
}
};
this.testClient.getTile(0, 0, 0, params, (err, res, tile) => {
assert.ifError(err);
assert.deepEqual(tile, {
errors: ['You are over platform\'s limits. Please contact us to know more details'],
errors_with_context: [{
type: 'limit',
subtype: 'datasource',
message: 'You are over platform\'s limits. Please contact us to know more details'
}]
});
done();
});
});
};
}
describe('interativity', function () {
beforeEach(function (done) {

View File

@@ -1,51 +0,0 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var TestClient = require('../../support/test-client');
describe('list widgets', function() {
it("should expose layer list", function(done) {
var listWidgetMapConfig = {
version: '1.5.0',
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from test_table',
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
cartocss_version: '2.3.0',
widgets: {
names: {
type: 'list',
options: {
columns: ['name']
}
}
}
}
}
]
};
var testClient = new TestClient(listWidgetMapConfig);
testClient.getWidget('names', function(err, res) {
if (err) {
return done(err);
}
var expectedList = [
{name:"Hawai"},
{name:"El Estocolmo"},
{name:"El Rey del Tallarín"},
{name:"El Lacón"},
{name:"El Pico"}
];
assert.deepEqual(JSON.parse(res.body).rows, expectedList);
testClient.drain(done);
});
});
});

View File

@@ -10,7 +10,7 @@ var CartodbWindshaft = require('../../../lib/cartodb/server');
var serverOptions = require('../../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
var LayergroupToken = require('../../support/layergroup-token');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
describe('named-maps widgets', function() {

View File

@@ -1,106 +0,0 @@
require('../../../support/test_helper');
var assert = require('../../../support/assert');
var TestClient = require('../../../support/test-client');
var _ = require('underscore');
describe('widgets', function() {
describe('lists', function() {
afterEach(function(done) {
if (this.testClient) {
this.testClient.drain(done);
} else {
done();
}
});
function listsMapConfig(columns) {
return {
version: '1.5.0',
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from test_table',
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }',
cartocss_version: '2.0.1',
widgets: {
places: {
type: 'list',
options: {
columns: columns || ['name', 'address']
}
}
}
}
}
]
};
}
var EXPECTED_NAMES = ['Hawai', 'El Estocolmo', 'El Rey del Tallarín', 'El Lacón', 'El Pico'];
it('can be fetched from a valid list', function(done) {
var columns = ['name', 'address'];
this.testClient = new TestClient(listsMapConfig(columns));
this.testClient.getWidget('places', function (err, res, list) {
assert.ok(!err, err);
assert.ok(list);
assert.equal(list.type, 'list');
assert.equal(list.rows.length, 5);
assert.ok(onlyHasFields(list, columns));
var names = list.rows.map(function (item) {
return item.name;
});
assert.deepEqual(names, EXPECTED_NAMES);
var expectedAddresses = [
'Calle de Pérez Galdós 9, Madrid, Spain',
'Calle de la Palma 72, Madrid, Spain',
'Plaza Conde de Toreno 2, Madrid, Spain',
'Manuel Fernández y González 8, Madrid, Spain',
'Calle Divino Pastor 12, Madrid, Spain'
];
var addresses = list.rows.map(function (item) {
return item.address;
});
assert.deepEqual(addresses, expectedAddresses);
done();
});
});
it('should fetch just one column', function(done) {
var columns = ['name'];
this.testClient = new TestClient(listsMapConfig(columns));
this.testClient.getWidget('places', function (err, res, list) {
assert.ok(!err, err);
assert.ok(list);
assert.equal(list.type, 'list');
assert.equal(list.rows.length, 5);
assert.ok(onlyHasFields(list, columns));
var names = list.rows.map(function (item) {
return item.name;
});
assert.deepEqual(names, EXPECTED_NAMES);
done();
});
});
function onlyHasFields(list, expectedFields) {
var fields = (!!list.rows[0]) ? Object.keys(list.rows[0]) : [];
return _.difference(fields, expectedFields).length === 0 &&
_.difference(expectedFields, fields).length === 0;
}
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -77,7 +77,6 @@ function randomImagePath() {
return path.resolve('test/results/png/image-test-' + Date.now() + '.png');
}
// jshint maxcomplexity:9
assert.response = function(server, req, res, callback) {
if (!callback) {
callback = res;
@@ -106,7 +105,6 @@ assert.response = function(server, req, res, callback) {
// jshint maxcomplexity:9
function onServerListening() {
var status = res.status || res.statusCode;
var requestParams = {
url: 'http://' + host + ':' + port + req.url,
method: req.method || 'GET',
@@ -122,61 +120,74 @@ assert.response = function(server, req, res, callback) {
request(requestParams, function assert$response$requestHandler(error, response, body) {
listener.close(function() {
response.body = response.body || body;
// Assert response body
if (res.body) {
var eql = res.body instanceof RegExp ? res.body.test(response.body) : res.body === response.body;
if (!eql) {
return callback(response, new Error(colorize(
'[red]{Invalid response body.}\n' +
' Expected: [green]{' + res.body + '}\n' +
' Got: [red]{' + response.body + '}'))
);
}
}
// Assert response status
if (typeof status === 'number') {
if (response.statusCode != status) {
return callback(response, new Error(colorize(
'[red]{Invalid response status code.}\n' +
' Expected: [green]{' + status + '}\n' +
' Got: [red]{' + response.statusCode + '}\n' +
' Body: ' + response.body))
);
}
}
// Assert response headers
if (res.headers) {
var keys = Object.keys(res.headers);
for (var i = 0, len = keys.length; i < len; ++i) {
var name = keys[i],
actual = response.headers[name.toLowerCase()],
expected = res.headers[name],
headerEql = expected instanceof RegExp ? expected.test(actual) : expected === actual;
if (!headerEql) {
return callback(response, new Error(colorize(
'Invalid response header [bold]{' + name + '}.\n' +
' Expected: [green]{' + expected + '}\n' +
' Got: [red]{' + actual + '}'))
);
}
}
}
// Callback
callback(response);
var err = validateResponse(response, res);
return callback(response, err);
});
});
}
};
// jshint maxcomplexity:6
function validateResponseBody(response, expected) {
if (expected.body) {
var eql = expected.body instanceof RegExp ?
expected.body.test(response.body) :
expected.body === response.body;
if (!eql) {
return new Error(colorize(
'[red]{Invalid response body.}\n' +
' Expected: [green]{' + expected.body + '}\n' +
' Got: [red]{' + response.body + '}')
);
}
}
}
function validateResponseStatus(response, expected) {
var status = expected.status || expected.statusCode;
// Assert response status
if (typeof status === 'number') {
if (response.statusCode !== status) {
return new Error(colorize(
'[red]{Invalid response status code.}\n' +
' Expected: [green]{' + status + '}\n' +
' Got: [red]{' + response.statusCode + '}\n' +
' Body: ' + response.body)
);
}
}
}
function validateResponseHeaders(response, expected) {
// Assert response headers
if (expected.headers) {
var keys = Object.keys(expected.headers);
for (var i = 0, len = keys.length; i < len; ++i) {
var name = keys[i],
actual = response.headers[name.toLowerCase()],
expectedHeader = expected.headers[name],
headerEql = expectedHeader instanceof RegExp ? expectedHeader.test(actual) : expectedHeader === actual;
if (!headerEql) {
return new Error(colorize(
'Invalid response header [bold]{' + name + '}.\n' +
' Expected: [green]{' + expectedHeader + '}\n' +
' Got: [red]{' + actual + '}')
);
}
}
}
}
function validateResponse(response, expected) {
// Assert response body
return validateResponseBody(response, expected) ||
validateResponseStatus(response, expected) ||
validateResponseHeaders(response, expected);
}
// @param tolerance number of tolerated grid cell differences
// jshint maxcomplexity:9
assert.utfgridEqualsFile = function(buffer, file_b, tolerance, callback) {
// jshint maxcomplexity:9
fs.writeFileSync('/tmp/grid.json', buffer, 'binary'); // <-- to debug/update
var expected_json = JSON.parse(fs.readFileSync(file_b, 'utf8'));

View File

@@ -1,21 +0,0 @@
var _ = require('underscore');
require(__dirname + '/test_helper');
module.exports = function(opts) {
var config = {
redis_pool: {
max: 10,
idleTimeoutMillis: 1,
reapIntervalMillis: 1,
port: global.environment.redis.port
}
}
_.extend(config, opts || {});
return config;
}();

View File

@@ -7,7 +7,7 @@ var PSQL = require('cartodb-psql');
var _ = require('underscore');
var mapnik = require('windshaft').mapnik;
var LayergroupToken = require('./layergroup-token');
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
var assert = require('./assert');
var helper = require('./test_helper');
@@ -20,9 +20,8 @@ const MAPNIK_SUPPORTED_FORMATS = {
'png': true,
'png32': true,
'grid.json': true,
'geojson': true,
'mvt': true
}
};
function TestClient(config, apiKey) {
this.mapConfig = isMapConfig(config) ? config : null;
@@ -115,6 +114,15 @@ module.exports.CARTOCSS = {
module.exports.SQL = {
EMPTY: 'select 1 as cartodb_id, null::geometry as the_geom_webmercator',
ONE_POINT: 'select 1 as cartodb_id, \'SRID=3857;POINT(0 0)\'::geometry the_geom_webmercator'
};
function resErr2errRes(callback) {
return (res, err) => {
if (err) {
return callback(err);
}
return callback(err, res);
};
}
TestClient.prototype.getWidget = function(widgetName, params, callback) {
@@ -130,7 +138,6 @@ TestClient.prototype.getWidget = function(widgetName, params, callback) {
url += '?' + qs.stringify({ filters: JSON.stringify(params.filters) });
}
var layergroupId;
step(
function createLayergroup() {
var next = this;
@@ -176,11 +183,12 @@ TestClient.prototype.getWidget = function(widgetName, params, callback) {
}
);
},
function getWidgetResult(err, _layergroupId) {
function getWidgetResult(err, layergroupId) {
assert.ifError(err);
var next = this;
layergroupId = _layergroupId;
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
var urlParams = {
own_filter: params.hasOwnProperty('own_filter') ? params.own_filter : 1
@@ -217,8 +225,6 @@ TestClient.prototype.getWidget = function(widgetName, params, callback) {
);
},
function finish(err, res) {
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
var widget;
if (!err && res.body) {
widget = JSON.parse(res.body);
@@ -241,7 +247,6 @@ TestClient.prototype.widgetSearch = function(widgetName, userQuery, params, call
url += '?' + qs.stringify({ filters: JSON.stringify(params.filters) });
}
var layergroupId;
step(
function createLayergroup() {
var next = this;
@@ -287,11 +292,12 @@ TestClient.prototype.widgetSearch = function(widgetName, userQuery, params, call
}
);
},
function getWidgetSearchResult(err, _layergroupId) {
function getWidgetSearchResult(err, layergroupId) {
assert.ifError(err);
var next = this;
layergroupId = _layergroupId;
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
var urlParams = {
q: userQuery,
@@ -326,8 +332,6 @@ TestClient.prototype.widgetSearch = function(widgetName, userQuery, params, call
);
},
function finish(err, res) {
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
var searchResult;
if (!err && res.body) {
searchResult = JSON.parse(res.body);
@@ -365,7 +369,6 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
}
};
var layergroupId;
step(
function createLayergroup() {
var next = this;
@@ -401,11 +404,12 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
}
);
},
function getDataviewResult(err, _layergroupId) {
function getDataviewResult(err, layergroupId) {
assert.ifError(err);
var next = this;
layergroupId = _layergroupId;
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
var urlParams = {
own_filter: params.hasOwnProperty('own_filter') ? params.own_filter : 1
@@ -444,12 +448,6 @@ TestClient.prototype.getDataview = function(dataviewName, params, callback) {
if (err) {
return callback(err);
}
if (layergroupId) {
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
}
return callback(null, dataview);
}
);
@@ -483,7 +481,6 @@ TestClient.prototype.getFeatureAttributes = function(featureId, layerId, params,
}
};
var layergroupId;
step(
function createLayergroup() {
var next = this;
@@ -572,7 +569,7 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
var layergroupId;
if (params.layergroupid) {
layergroupId = params.layergroupid
layergroupId = params.layergroupid;
}
step(
@@ -587,7 +584,7 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
return next(new Error('apiKey param is mandatory to create a new template'));
}
params.placeholders = params.placeholders || {};
params.placeholders = params.placeholders || {};
assert.response(self.server,
{
@@ -620,7 +617,7 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
return next(null, layergroupId);
}
var data = templateId ? params.placeholders : self.mapConfig
var data = templateId ? params.placeholders : self.mapConfig;
var path = templateId ?
urlNamed + '/' + templateId + '?' + qs.stringify({api_key: self.apiKey}) :
url;
@@ -649,11 +646,12 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
}
);
},
function getTileResult(err, _layergroupId) {
function getTileResult(err, layergroupId) {
// jshint maxcomplexity:12
assert.ifError(err);
var next = this;
layergroupId = _layergroupId;
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
url = '/api/v1/map/' + layergroupId + '/';
@@ -727,35 +725,31 @@ TestClient.prototype.getTile = function(z, x, y, params, callback) {
expectedResponse.headers['Content-Type'] = 'application/json; charset=utf-8';
}
assert.response(self.server, request, expectedResponse, function(res, err) {
assert.ifError(err);
var body;
switch (res.headers['content-type']) {
case 'image/png':
body = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
break;
case 'application/x-protobuf':
body = new mapnik.VectorTile(z, x, y);
body.setDataSync(new Buffer(res.body, 'binary'));
break;
case 'application/json; charset=utf-8':
body = JSON.parse(res.body);
break;
default:
body = res.body
break;
}
next(null, res, body);
});
assert.response(self.server, request, expectedResponse, resErr2errRes(this));
},
function finish(err, res, image) {
if (layergroupId) {
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
function finish(err, res) {
if (err) {
return callback(err);
}
return callback(err, res, image);
var body;
switch (res.headers['content-type']) {
case 'image/png':
body = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
break;
case 'application/x-protobuf':
body = new mapnik.VectorTile(z, x, y);
body.setDataSync(new Buffer(res.body, 'binary'));
break;
case 'application/json; charset=utf-8':
body = JSON.parse(res.body);
break;
default:
body = res.body;
break;
}
return callback(err, res, body);
}
);
};
@@ -791,10 +785,11 @@ TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
},
expectedResponse,
function(res, err) {
var parsedBody;
// If there is a response, we are still interested in catching the created keys
// to be able to delete them on the .drain() method.
if (res) {
var parsedBody = JSON.parse(res.body);
parsedBody = JSON.parse(res.body);
if (parsedBody.layergroupid) {
self.keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
@@ -812,7 +807,7 @@ TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
TestClient.prototype.getStaticCenter = function (params, callback) {
var self = this;
let { layergroupid, z, lat, lng, width, height, format } = params
let { layergroupid, z, lat, lng, width, height, format } = params;
var url = `/api/v1/map/`;
@@ -828,7 +823,7 @@ TestClient.prototype.getStaticCenter = function (params, callback) {
return next(null, layergroupid);
}
var data = self.mapConfig
var data = self.mapConfig;
var path = url;
assert.response(self.server,
@@ -855,14 +850,13 @@ TestClient.prototype.getStaticCenter = function (params, callback) {
}
);
},
function getStaticResult(err, _layergroupid) {
function getStaticResult(err, layergroupId) {
assert.ifError(err);
var next = this;
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
layergroupid = _layergroupid;
url = `/api/v1/map/static/center/${layergroupid}/${z}/${lat}/${lng}/${width}/${height}.${format}`
url = `/api/v1/map/static/center/${layergroupId}/${z}/${lat}/${lng}/${width}/${height}.${format}`;
if (self.apiKey) {
url += '?' + qs.stringify({api_key: self.apiKey});
@@ -884,31 +878,27 @@ TestClient.prototype.getStaticCenter = function (params, callback) {
}
}, params.response);
assert.response(self.server, request, expectedResponse, function(res, err) {
assert.ifError(err);
var body;
switch (res.headers['content-type']) {
case 'image/png':
body = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
break;
case 'application/json; charset=utf-8':
body = JSON.parse(res.body);
break;
default:
body = res.body
break;
}
next(null, res, body);
});
assert.response(self.server, request, expectedResponse, resErr2errRes(this));
},
function finish(err, res, image) {
if (layergroupid) {
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupid).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
function(err, res) {
if (err) {
return callback(err);
}
return callback(err, res, image);
var body;
switch (res.headers['content-type']) {
case 'image/png':
body = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
break;
case 'application/json; charset=utf-8':
body = JSON.parse(res.body);
break;
default:
body = res.body;
break;
}
return callback(err, res, body);
}
);
};
@@ -922,7 +912,6 @@ TestClient.prototype.getNodeStatus = function(nodeName, callback) {
url += '?' + qs.stringify({api_key: this.apiKey});
}
var layergroupId;
var nodes = {};
step(
function createLayergroup() {
@@ -961,11 +950,11 @@ TestClient.prototype.getNodeStatus = function(nodeName, callback) {
}
);
},
function getNodeStatusResult(err, _layergroupId) {
function getNodeStatusResult(err, layergroupId) {
assert.ifError(err);
var next = this;
layergroupId = _layergroupId;
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
url = urlParser.parse(nodes[nodeName]).path;
@@ -988,15 +977,13 @@ TestClient.prototype.getNodeStatus = function(nodeName, callback) {
}
};
assert.response(self.server, request, expectedResponse, function(res, err) {
assert.ifError(err);
next(null, res, JSON.parse(res.body));
});
assert.response(self.server, request, expectedResponse, resErr2errRes(this));
},
function finish(err, res, image) {
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
return callback(err, res, image);
function finish(err, res) {
if (err) {
return callback(err);
}
return callback(null, res, JSON.parse(res.body));
}
);
};
@@ -1005,11 +992,11 @@ TestClient.prototype.getAttributes = function(params, callback) {
var self = this;
if (!Number.isFinite(params.featureId)) {
throw new Error('featureId param must be a number')
throw new Error('featureId param must be a number');
}
if (!Number.isFinite(params.layer)) {
throw new Error('layer param must be a number')
throw new Error('layer param must be a number');
}
var url = '/api/v1/map';
@@ -1021,7 +1008,7 @@ TestClient.prototype.getAttributes = function(params, callback) {
var layergroupid;
if (params.layergroupid) {
layergroupid = params.layergroupid
layergroupid = params.layergroupid;
}
step(
@@ -1058,14 +1045,13 @@ TestClient.prototype.getAttributes = function(params, callback) {
}
);
},
function getAttributes(err, _layergroupid) {
function getAttributes(err, layergroupId) {
assert.ifError(err);
var next = this;
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
layergroupid = _layergroupid;
url = `/api/v1/map/${layergroupid}/${params.layer}/attributes/${params.featureId}`;
url = `/api/v1/map/${layergroupId}/${params.layer}/attributes/${params.featureId}`;
if (self.apiKey) {
url += '?' + qs.stringify({api_key: self.apiKey});
@@ -1086,21 +1072,14 @@ TestClient.prototype.getAttributes = function(params, callback) {
}
};
assert.response(self.server, request, expectedResponse, function (res, err) {
assert.ifError(err);
var attributes = JSON.parse(res.body);
next(null, res, attributes);
});
assert.response(self.server, request, expectedResponse, resErr2errRes(this));
},
function finish(err, res, attributes) {
if (layergroupid) {
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupid).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
function finish(err, res) {
if (err) {
return callback(err);
}
return callback(err, res, attributes);
var attributes = JSON.parse(res.body);
return callback(null, res, attributes);
}
);
};
@@ -1144,7 +1123,7 @@ module.exports.getStaticMap = function getStaticMap(templateName, params, callba
// this could be removed once named maps are invalidated, otherwise you hits the cache
var server = new CartodbWindshaft(serverOptions);
assert.response(self.server, requestOptions, expectedResponse, function (res, err) {
assert.response(server, requestOptions, expectedResponse, function (res, err) {
helper.deleteRedisKeys({'user:localhost:mapviews:global': 5}, function() {
return callback(err, mapnik.Image.fromBytes(new Buffer(res.body, 'binary')));
});
@@ -1166,12 +1145,11 @@ TestClient.prototype.setUserRenderTimeoutLimit = function (user, userTimeoutLimi
TestClient.prototype.setUserDatabaseTimeoutLimit = function (timeoutLimit, callback) {
const dbname = _.template(global.environment.postgres_auth_user, { user_id: 1 }) + '_db';
const dbuser = _.template(global.environment.postgres_auth_user, { user_id: 1 })
const pass = _.template(global.environment.postgres_auth_pass, { user_id: 1 })
const dbuser = _.template(global.environment.postgres_auth_user, { user_id: 1 });
const publicuser = global.environment.postgres.user;
// we need to guarantee all new connections have the new settings
helper.cleanPGPoolConnections()
helper.cleanPGPoolConnections();
const psql = new PSQL({
user: 'postgres',

View File

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

View File

@@ -2,7 +2,7 @@ require('../../support/test_helper');
var assert = require('assert');
var BaseController = require('../../../lib/cartodb/controllers/base');
var errorMiddleware = require('../../../lib/cartodb/middleware/error-middleware');
describe('error messages clean up', function() {
@@ -15,7 +15,7 @@ describe('error messages clean up', function() {
" encountered during parsing of layer 'layer0' in Layer"
].join('\n');
var outMessage = BaseController.errorMessage(inMessage);
var outMessage = errorMiddleware.errorMessage(inMessage);
assert.ok(outMessage.match('connect'), outMessage);
assert.ok(!outMessage.match(/666/), outMessage);

View File

@@ -0,0 +1,36 @@
var assert = require('assert');
var testHelper = require('../../support/test_helper');
var lzmaMiddleware = require('../../../lib/cartodb/middleware/lzma');
describe('lzma-middleware', function() {
it('it should extend params with decoded lzma', function(done) {
var qo = {
config: {
version: '1.3.0'
}
};
testHelper.lzma_compress_to_base64(JSON.stringify(qo), 1, function(err, data) {
var req = {
headers: {
host:'localhost'
},
query: {
api_key: 'test',
lzma: data
}
};
lzmaMiddleware(req, {}, function(err) {
if ( err ) {
return done(err);
}
var query = req.query;
assert.deepEqual(qo.config, query.config);
assert.equal('test', query.api_key);
done();
});
});
});
});

View File

@@ -143,79 +143,6 @@ describe('dataviews-widgets-adapter', function() {
}
}
},
{
"input": {
"version": "1.4.0",
"layers": [
{
"type": "mapnik",
"options": {
"sql": "select * from test_table",
"cartocss": "#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }",
"cartocss_version": "2.3.0",
"widgets": {
"names": {
"type": "list",
"options": {
"columns": [
"name"
]
}
}
}
}
}
]
},
"expected": {
"version": "1.4.0",
"layers": [
{
"type": "mapnik",
"options": {
"source": {
"id": "cdb-layer-source-0"
},
"cartocss": "#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }",
"cartocss_version": "2.3.0",
// keep them for now
"widgets": {
"names": {
"type": "list",
"options": {
"columns": [
"name"
]
}
}
}
}
}
],
"analyses": [
{
"id": "cdb-layer-source-0",
"type": "source",
"params": {
"query": "select * from test_table"
}
}
],
"dataviews": {
"names": {
"source": {
"id": "cdb-layer-source-0"
},
"type": "list",
"options": {
"columns": [
"name"
]
}
}
}
}
},
{
"input": {
"version": "1.4.0",

View File

@@ -41,7 +41,9 @@ describe('tile stats', function() {
jsonp: function() {},
send: function() {}
};
layergroupController.finalizeGetTileOrGrid('Unsupported format png2', reqMock, resMock, null, null);
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');
@@ -74,7 +76,8 @@ describe('tile stats', function() {
var layergroupController = new LayergroupController();
layergroupController.finalizeGetTileOrGrid('Another error happened', reqMock, resMock, null, null);
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');

View File

@@ -0,0 +1,171 @@
var assert = require('assert');
var _ = require('underscore');
var RedisPool = require('redis-mpool');
var cartodbRedis = require('cartodb-redis');
var PgConnection = require('../../../lib/cartodb/backends/pg_connection');
var AuthApi = require('../../../lib/cartodb/api/auth_api');
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 localsMiddleware = require('../../../lib/cartodb/middleware/context/locals');
var windshaft = require('windshaft');
describe('prepare-context', function() {
var test_user = _.template(global.environment.postgres_auth_user, {user_id:1});
var test_pubuser = global.environment.postgres.user;
var test_database = test_user + '_db';
let cleanUpQueryParams;
let dbConnSetup;
let authorize;
before(function() {
var redisPool = new RedisPool(global.environment.redis);
var mapStore = new windshaft.storage.MapStore();
var metadataBackend = cartodbRedis({pool: redisPool});
var pgConnection = new PgConnection(metadataBackend);
var templateMaps = new TemplateMaps(redisPool);
var authApi = new AuthApi(pgConnection, metadataBackend, mapStore, templateMaps);
cleanUpQueryParams = cleanUpQueryParamsMiddleware();
authorize = authorizeMiddleware(authApi);
dbConnSetup = dbConnSetupMiddleware(pgConnection);
});
it('can be found in server_options', function(){
assert.ok(_.isFunction(authorize));
assert.ok(_.isFunction(dbConnSetup));
assert.ok(_.isFunction(cleanUpQueryParams));
});
function prepareRequest(req) {
req.profiler = {
done: function() {}
};
return req;
}
function prepareResponse(res) {
if(!res.locals) {
res.locals = {};
}
res.locals.user = 'localhost';
return res;
}
it('res.locals are created', function(done) {
let req = {};
let res = {};
localsMiddleware(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 = {};
cleanUpQueryParams(prepareRequest(req), prepareResponse(res), function(err) {
if ( err ) { done(err); return; }
assert.ok(_.isObject(req.query), 'request has query');
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
assert.ok(res.hasOwnProperty('locals'), 'response has locals');
assert.ok(!res.locals.hasOwnProperty('interactivity'), 'response locals do not have interactivity');
done();
});
});
it('sets dbname from redis metadata', function(done){
var req = {headers: { host:'localhost' }, query: {} };
var res = { set: function () {} };
dbConnSetup(prepareRequest(req), prepareResponse(res), function(err) {
if ( err ) { done(err); return; }
assert.ok(_.isObject(req.query), 'request has query');
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
assert.ok(res.hasOwnProperty('locals'), 'response has locals');
assert.ok(!res.locals.hasOwnProperty('interactivity'), 'response locals do not have interactivity');
assert.equal(res.locals.dbname, test_database);
assert.ok(res.locals.dbuser === test_pubuser, 'could inject dbuser ('+res.locals.dbuser+')');
done();
});
});
it('sets also dbuser for authenticated requests', function(done){
var req = { headers: { host: 'localhost' }, query: { map_key: '1234' }};
var res = { set: function () {} };
// FIXME: review authorize-pgconnsetup workflow, It might we are doing authorization twice.
authorize(prepareRequest(req), prepareResponse(res), function (err) {
if (err) { done(err); return; }
dbConnSetup(req, res, function(err) {
if ( err ) { done(err); return; }
assert.ok(_.isObject(req.query), 'request has query');
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
assert.ok(res.hasOwnProperty('locals'), 'response has locals');
assert.ok(!res.locals.hasOwnProperty('interactivity'), 'request params do not have interactivity');
assert.equal(res.locals.dbname, test_database);
assert.equal(res.locals.dbuser, test_user);
req = {
headers: {
host:'localhost'
},
query: {
map_key: '1235'
}
};
res = { set: function () {} };
dbConnSetup(prepareRequest(req), prepareResponse(res), function() {
// wrong key resets params to no user
assert.ok(res.locals.dbuser === test_pubuser, 'could inject dbuser ('+res.locals.dbuser+')');
done();
});
});
});
});
it('it should remove invalid params', function(done) {
var config = {
version: '1.3.0'
};
var req = {
headers: {
host:'localhost'
},
query: {
non_included: 'toberemoved',
api_key: 'test',
style: 'override',
config: config
}
};
var res = {};
cleanUpQueryParams(prepareRequest(req), prepareResponse(res), function (err) {
if ( err ) {
return done(err);
}
var query = res.locals;
assert.deepEqual(config, query.config);
assert.equal('test', query.api_key);
assert.equal(undefined, query.non_included);
done();
});
});
});

View File

@@ -1,132 +0,0 @@
var assert = require('assert');
var _ = require('underscore');
var test_helper = require('../../support/test_helper');
var RedisPool = require('redis-mpool');
var cartodbRedis = require('cartodb-redis');
var PgConnection = require('../../../lib/cartodb/backends/pg_connection');
var AuthApi = require('../../../lib/cartodb/api/auth_api');
var TemplateMaps = require('../../../lib/cartodb/backends/template_maps');
var BaseController = require('../../../lib/cartodb/controllers/base');
var windshaft = require('windshaft');
describe('req2params', function() {
var test_user = _.template(global.environment.postgres_auth_user, {user_id:1});
var test_pubuser = global.environment.postgres.user;
var test_database = test_user + '_db';
var baseController;
before(function() {
var redisPool = new RedisPool(global.environment.redis);
var mapStore = new windshaft.storage.MapStore();
var metadataBackend = cartodbRedis({pool: redisPool});
var pgConnection = new PgConnection(metadataBackend);
var templateMaps = new TemplateMaps(redisPool);
var authApi = new AuthApi(pgConnection, metadataBackend, mapStore, templateMaps);
baseController = new BaseController(authApi, pgConnection);
});
it('can be found in server_options', function(){
assert.ok(_.isFunction(baseController.req2params));
});
function prepareRequest(req) {
req.profiler = {
done: function() {}
};
req.context = { user: 'localhost' };
return req;
}
it('cleans up request', function(done){
var req = {headers: { host:'localhost' }, query: {dbuser:'hacker',dbname:'secret'}};
baseController.req2params(prepareRequest(req), function(err, req) {
if ( err ) { done(err); return; }
assert.ok(_.isObject(req.query), 'request has query');
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
assert.ok(req.hasOwnProperty('params'), 'request has params');
assert.ok(!req.params.hasOwnProperty('interactivity'), 'request params do not have interactivity');
assert.equal(req.params.dbname, test_database, 'could forge dbname: '+ req.params.dbname);
assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')');
done();
});
});
it('sets dbname from redis metadata', function(done){
var req = {headers: { host:'localhost' }, query: {} };
baseController.req2params(prepareRequest(req), function(err, req) {
if ( err ) { done(err); return; }
assert.ok(_.isObject(req.query), 'request has query');
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
assert.ok(req.hasOwnProperty('params'), 'request has params');
assert.ok(!req.params.hasOwnProperty('interactivity'), 'request params do not have interactivity');
assert.equal(req.params.dbname, test_database);
assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')');
done();
});
});
it('sets also dbuser for authenticated requests', function(done){
var req = {headers: { host:'localhost' }, query: {map_key: '1234'} };
baseController.req2params(prepareRequest(req), function(err, req) {
if ( err ) { done(err); return; }
assert.ok(_.isObject(req.query), 'request has query');
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
assert.ok(req.hasOwnProperty('params'), 'request has params');
assert.ok(!req.params.hasOwnProperty('interactivity'), 'request params do not have interactivity');
assert.equal(req.params.dbname, test_database);
assert.equal(req.params.dbuser, test_user);
req = {
headers: {
host:'localhost'
},
query: {
map_key: '1235'
}
};
baseController.req2params(prepareRequest(req), function(err, req) {
// wrong key resets params to no user
assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')');
done();
});
});
});
it('it should extend params with decoded lzma', function(done) {
var qo = {
config: {
version: '1.3.0'
}
};
test_helper.lzma_compress_to_base64(JSON.stringify(qo), 1, function(err, data) {
var req = {
headers: {
host:'localhost'
},
query: {
non_included: 'toberemoved',
api_key: 'test',
style: 'override',
lzma: data
}
};
baseController.req2params(prepareRequest(req), function(err, req) {
if ( err ) {
return done(err);
}
var query = req.params;
assert.deepEqual(qo.config, query.config);
assert.equal('test', query.api_key);
assert.equal(undefined, query.non_included);
done();
});
});
});
});

691
yarn.lock

File diff suppressed because it is too large Load Diff