Compare commits

..

282 Commits
4.3.1 ... 4.7.0

Author SHA1 Message Date
Raul Ochoa
d949d1c27f Release 4.7.0 2018-01-03 19:18:39 +00:00
Raul Ochoa
aa1d411fb8 Update news and bump version 2018-01-03 19:17:58 +00:00
Raul Ochoa
f297374449 Merge pull request #837 from CartoDB/tilejson
Return tilejson in metadata
2018-01-03 20:13:44 +01:00
Raul Ochoa
060b93c314 Rename middleware fn name 2018-01-03 18:44:10 +00:00
Raul Ochoa
3ceeaedf02 Fix test after breaking it with linting changes 2018-01-03 16:19:14 +00:00
Raul Ochoa
c6ba9e6102 Fix linting 2018-01-03 16:10:09 +00:00
Raul Ochoa
bf40b240d3 Return tilejson in metadata
It returns tilejson for each individual layer and also for all vector and raster layers.
2018-01-03 16:54:45 +01:00
Raul Ochoa
5d4d2bddd6 Implementation for getTilesUrls
This will be useful for generating the tilejson in the metadata
2018-01-03 16:05:19 +01:00
Raul Ochoa
95dfd87c96 Add test cases for getTilesUrls 2018-01-03 16:04:31 +01:00
Raul Ochoa
eab9e8846e Reorg suite to accommodate getTileURLs 2018-01-03 12:57:01 +00:00
Daniel
788bc302a0 Merge pull request #833 from CartoDB/middlewarify-named-map-admin-controller
Middlewarify named map admin controller
2018-01-03 13:20:14 +01:00
Daniel García Aubert
1ba240d099 Rename middleware function 2018-01-03 13:15:11 +01:00
Daniel
ee0405da1e Merge pull request #836 from CartoDB/disable-default-aggregation
Add test to check layer aggregation disabling
2018-01-03 13:09:31 +01:00
Daniel
5e9b326d03 Merge pull request #835 from CartoDB/skip-polygon-layer-vector-map-config
In vector-only map-config, only aggregate layers with points
2018-01-03 13:09:07 +01:00
Daniel García Aubert
1f30367e59 Add test to check layer aggregation disabling 2018-01-03 12:40:00 +01:00
Daniel García Aubert
26a2f73c2a Update NEWS 2018-01-03 12:30:46 +01:00
Daniel García Aubert
60005e2f7f Fix bad assertion 2018-01-03 12:24:07 +01:00
Daniel García Aubert
1c7da2c4b3 Going green: do not fail when map-config is vector-only and a layer doesn't have points 2018-01-03 12:00:25 +01:00
Daniel García Aubert
3799dd2574 Going red: fail when vector only map-config has a polygon layer 2018-01-03 11:14:20 +01:00
Raul Ochoa
7efb2a2344 Stubs next version 2018-01-02 15:40:54 +00:00
Raul Ochoa
88777abc2c Release 4.6.0 2018-01-02 15:40:10 +00:00
Raul Ochoa
4d9a6f8fbe update news 2018-01-02 15:39:37 +00:00
Daniel
3d9c2e66c5 Merge pull request #830 from CartoDB/pg-mvt-do-not-filter-columns
Aggregation: be able to return a complete row sample as default aggregation
2018-01-02 15:36:08 +01:00
Daniel García Aubert
6bbe715aa6 Update NEWS 2018-01-02 12:57:54 +01:00
Daniel García Aubert
ba002fdb2c Update windshaft to 4.20 2018-01-02 12:38:58 +01:00
Raul Ochoa
10d1381e51 Merge remote-tracking branch 'origin/master' into pg-mvt-do-not-filter-columns 2017-12-29 17:50:09 +00:00
Daniel García Aubert
dfef7ff3c0 Use spread assignment 2017-12-29 18:45:45 +01:00
Daniel García Aubert
83d0ce4040 Rename method 2017-12-29 18:25:08 +01:00
Daniel García Aubert
75f72c4d07 Return empty aggregation configuration when the map-config is vector-only and the layer has no aggregation 2017-12-29 17:52:28 +01:00
Daniel García Aubert
adb9e55fb2 Avoid snake_case notation 2017-12-29 16:30:42 +01:00
Daniel García Aubert
5d3726de44 Use original variable name 2017-12-29 16:24:38 +01:00
Daniel García Aubert
f186e4736b Use template string 2017-12-29 16:19:00 +01:00
Daniel García Aubert
a00c2b1eef Now main middlewares return a named function with the right context bound 2017-12-29 16:15:48 +01:00
Daniel García Aubert
64d601179d Use const instead of var 2017-12-29 15:22:17 +01:00
Daniel García Aubert
cf2b73e473 Move to up intermediate middlewares 2017-12-29 15:19:52 +01:00
Daniel García Aubert
70932c23df Remove step and assert dependencies 2017-12-29 15:17:29 +01:00
Daniel García Aubert
519d49bd10 Remove finish function and respond in the main middleware 2017-12-29 15:04:44 +01:00
Daniel García Aubert
bf814c4442 keep error label 2017-12-29 13:05:01 +01:00
Daniel García Aubert
f136993c50 Use checkContentType middleware 2017-12-29 12:44:56 +01:00
Daniel García Aubert
ba008ab518 Remove unused function 2017-12-29 12:36:56 +01:00
Daniel García Aubert
e4ed6ee1cc Use authorizedByAPIKey middleware 2017-12-29 12:34:50 +01:00
Daniel García Aubert
fda7661dad Create authorizedByAPIKey middleware 2017-12-29 12:23:52 +01:00
Raul Ochoa
79233471c6 Merge pull request #832 from CartoDB/layers-filters
Support individual layer id filters
2017-12-28 19:48:18 +01:00
Raul Ochoa
a75beefe6e Upgrades windshaft to 4.1.1 2017-12-28 18:34:53 +00:00
Raul Ochoa
e43ccf4f12 Going red: individual layer id filters fail
Depends on https://github.com/CartoDB/Windshaft/pull/584.
2017-12-28 18:19:52 +00:00
Raul Ochoa
cd8e320534 Merge pull request #831 from CartoDB/fail-on-uncaught-exceptions
Make tests to fail if they got an uncaught exception
2017-12-28 19:15:29 +01:00
Raul Ochoa
d7f4d39aa2 Don't not override the full client but only the provided methods 2017-12-28 18:09:41 +00:00
Raul Ochoa
89333185a9 Make tests to fail if they got an uncaught exception 2017-12-28 16:37:17 +00:00
Daniel García Aubert
99b95cf839 Move check of default-aggregation to mapconfig model 2017-12-28 13:50:59 +01:00
Daniel García Aubert
9fbc56b82c Remove FIXME notes to test against PostGIS vector renderer 2017-12-28 13:13:32 +01:00
Daniel García Aubert
9a1bc51fdb Use aggregation-mapconfig's method to discover columns to be exposed used to aggregate 2017-12-28 13:12:41 +01:00
Daniel García Aubert
d42257127b Add method to discover required columns or all of them if it's a default aggregation 2017-12-28 13:11:32 +01:00
Daniel García Aubert
5a730c6df1 Remove exited containers after running test against docker 2017-12-28 13:10:11 +01:00
Daniel García Aubert
418c8691d1 Support default full-sample aggregation for postgis vector renderer 2017-12-27 20:08:43 +01:00
Daniel García Aubert
9885045b41 Do not default to null 2017-12-27 12:48:06 +01:00
Daniel García Aubert
062e6f9594 Merge branch 'full-sample' of github.com:CartoDB/Windshaft-cartodb into pg-mvt-do-not-filter-columns 2017-12-27 12:45:43 +01:00
Daniel
d8428938ae Merge pull request #829 from CartoDB/vector-tiles-doc
Update vector tiles documentation
2017-12-26 11:42:06 +01:00
Simon Martín
ca5f280cb3 updating NEWS.md 2017-12-26 11:03:12 +01:00
Simon Martín
524d5a5597 Merge pull request #828 from Algunenano/i827_timestamp
Timeseries: Avoid collisions with columns named 'timestamp'
2017-12-26 09:55:46 +01:00
Daniel García Aubert
a43779b050 Get columns from layer query a set them into layer opptions 2017-12-22 18:19:57 +01:00
Javier Goizueta
ef3917fa6f Update vector tiles documentation
This update reflects the fact that CartoCSS is now optional for vector tiles.
2017-12-22 17:18:42 +01:00
Raul Marin
031e1253ca Numeric histograms: Avoid conflicts with 'bin' named columns 2017-12-22 17:12:57 +01:00
Raul Marin
8012d76b68 Timeseries: Avoid collisions with columns named 'timestamp' 2017-12-22 15:58:40 +01:00
Javier Goizueta
d726c9ad01 Fix point-sample aggregation
it failed in  the case of aggregate columns with the name of base columns
2017-12-22 15:48:30 +01:00
Javier Goizueta
1ce8076699 Change default aggregation placement to point-sample
For consistency with the default aggregation.
2017-12-22 15:46:29 +01:00
Javier Goizueta
54f32113f3 Add some aggregation tests 2017-12-22 15:45:34 +01:00
Javier Goizueta
19bf079f2d Exclude test from PostGIS 2.4 2017-12-22 15:45:14 +01:00
Daniel García Aubert
b7ecde5c9d Add function get columns for layer's query 2017-12-22 13:43:30 +01:00
Daniel García Aubert
a2f804d79f Use full-sample aggregation mode 2017-12-22 12:15:37 +01:00
Daniel García Aubert
efdfabf3e9 Remove method 2017-12-22 12:14:34 +01:00
Javier Goizueta
e9a4fc4b2c Use full-sample aggregation only as default
Sampling is performed only when placement, columns or dimensions are specified;
otherwise the regular centroid/grid-point/grid-center is used without sampling.
2017-12-22 11:31:33 +01:00
Daniel García Aubert
a1d536642e Merge branch 'full-sample' into pg-mvt-do-not-filter-columns 2017-12-21 20:01:07 +01:00
Daniel García Aubert
3c00266666 Add support for aggregated columns in mvt format 2017-12-21 20:00:17 +01:00
Daniel García Aubert
7f64d15944 Merge branch 'master' into full-sample 2017-12-20 15:58:58 +01:00
Daniel
8259271184 Merge pull request #826 from CartoDB/fix-typo-dimensions
Fix typo
2017-12-20 15:02:32 +01:00
Daniel García Aubert
20366cedb4 Skip test for PostGis 2.4 2017-12-20 14:53:34 +01:00
Daniel García Aubert
a102d1d366 jshint, I hate you 2017-12-20 14:41:29 +01:00
Daniel García Aubert
4b97b4fd26 Fix typo 2017-12-20 14:35:47 +01:00
Daniel
b94debf10e Merge pull request #825 from CartoDB/export-supported-placements
Export supported placements and create static methods to expose them
2017-12-20 13:27:23 +01:00
Daniel García Aubert
60030784c1 Export supported placements and create static methods to expose them in a fancy way 2017-12-20 12:42:29 +01:00
Daniel García Aubert
cc9b190e5d Minor style formats 2017-12-19 16:17:37 +01:00
Daniel García Aubert
4946ca688c Add test to check full-sample query 2017-12-19 16:17:13 +01:00
Daniel García Aubert
d2828ecaff Update test 2017-12-19 13:07:57 +01:00
Daniel García Aubert
5a3dd6a914 Use supported placemets of aggregation-query 2017-12-19 13:00:18 +01:00
Daniel García Aubert
bcd2fd8f88 Export supported placements 2017-12-19 12:59:33 +01:00
Daniel García Aubert
94a5e66881 Merge branch 'master' into full-sample 2017-12-19 12:47:36 +01:00
Daniel García Aubert
d55b78f76b Update next release version in NEWS 2017-12-19 12:46:29 +01:00
Daniel García Aubert
42149f9ae7 Update NEWS 2017-12-19 12:45:30 +01:00
Daniel
1e08d946b1 Merge pull request #822 from CartoDB/aggregation-validation
Validate aggregation input params
2017-12-19 12:42:28 +01:00
Daniel García Aubert
f22216e6d2 Catch error threw from constructor and follow node callback pattern 2017-12-19 12:23:54 +01:00
Raul Marin
d9cf830fb4 Stub for next release 2017-12-19 12:23:27 +01:00
Raul Marin
b762008c79 Release 4.5.0 2017-12-19 12:23:27 +01:00
Raul Marin
ca2c2b80d8 Update NEWS 2017-12-19 12:09:14 +01:00
Raul Marin
f946dfa65f Date histograms: Add tests for the new aggregation modes 2017-12-19 12:09:14 +01:00
Raul Marin
418f5faa11 Date histogram: Reduce the threshold to change in auto mode to 100 2017-12-19 12:09:14 +01:00
Raul Marin
bba6db9dbf Date histogram: Add second, decade, century and millenium aggregations 2017-12-19 12:09:14 +01:00
Daniel García Aubert
326cad2f2c Typo 2017-12-19 10:54:20 +01:00
Daniel García Aubert
34808d6147 Improve naming 2017-12-19 10:50:53 +01:00
Daniel García Aubert
79b04bbdfd Rename param 2017-12-19 10:47:53 +01:00
Daniel García Aubert
45a663d5ae Split columns validator 2017-12-19 10:43:34 +01:00
Daniel García Aubert
cace6169c0 Add function to create layer errors 2017-12-19 10:25:41 +01:00
Daniel García Aubert
bdce2f95f2 Add validations for columns 2017-12-18 20:42:26 +01:00
Javier Goizueta
506e16fc87 Experimental full-sample aggregation 2017-12-18 20:18:37 +01:00
Daniel García Aubert
c367743d76 Export SUPPORTED_AGGREGATE_FUNCTIONS 2017-12-18 20:06:16 +01:00
Daniel García Aubert
fa7140e736 Rename argument 2017-12-18 19:52:50 +01:00
Daniel García Aubert
c63226cd26 Improve function naming 2017-12-18 19:51:55 +01:00
Daniel García Aubert
777df6337b Style typo 2017-12-18 19:47:11 +01:00
Daniel García Aubert
2dda0a80da Improve error context 2017-12-18 19:35:12 +01:00
Daniel García Aubert
e2bd97eea6 Move validation to the constructor 2017-12-18 19:19:02 +01:00
Daniel García Aubert
fb03cd3424 Move aggregation validation to its own module 2017-12-18 19:17:43 +01:00
Daniel García Aubert
8a48b96c53 Rename file 2017-12-18 19:06:01 +01:00
Daniel García Aubert
76b0c94835 Rename file 2017-12-18 19:05:49 +01:00
Daniel García Aubert
6a36aa1f13 Order checks to validate if a layer should be adapted 2017-12-18 18:56:53 +01:00
Daniel García Aubert
800870e783 Remove local variable 2017-12-18 18:55:32 +01:00
Daniel García Aubert
6638ba91c3 Refactor supported geometry types 2017-12-18 18:53:44 +01:00
Daniel García Aubert
47e4b9da0d Encapsulate threshold layer validation in aggregation-mapconfig 2017-12-18 18:43:14 +01:00
Daniel García Aubert
81e0c3a098 Add RESOLUTION default getter 2017-12-18 18:26:08 +01:00
Daniel García Aubert
2068861988 Add PLACEMENT default getter 2017-12-18 18:24:09 +01:00
Daniel García Aubert
878f3bd627 Move .sql() to aggregation-mapconfig 2017-12-18 18:17:01 +01:00
Daniel García Aubert
170fcc1973 Move static methods 2017-12-18 17:42:12 +01:00
Daniel García Aubert
d0c88ce21d Improve naming 2017-12-18 17:26:41 +01:00
Javier Goizueta
86d8f28661 Merge pull request #818 from CartoDB/fix-aggr-grid-point
Fix grid-point aggregation placement
2017-12-18 16:37:39 +01:00
Javier Goizueta
e97147ddb4 Fix grid-point aggregation placement 2017-12-18 15:48:51 +01:00
Simon Martín
a40bc4a527 Merge pull request #816 from CartoDB/stringify-for-error-log
Stringify for error log
2017-12-18 15:03:28 +01:00
Simon Martín
77f64bee8c stringifyForLogs more usual case first 2017-12-18 14:54:36 +01:00
Daniel García Aubert
e81a16ce0d Improve validation by applying refactor 2017-12-18 14:31:53 +01:00
Daniel García Aubert
153a792fcb Improve validation by applying refactor 2017-12-18 14:25:44 +01:00
Daniel García Aubert
5c1b1e3214 Improve validation by applying refactor 2017-12-18 14:21:03 +01:00
Daniel García Aubert
0bca3d6f33 Validate placement, threshold and resolution 2017-12-18 13:42:27 +01:00
Simon Martín
14e90a6c76 add line at EOF 2017-12-18 12:59:44 +01:00
Simon Martín
a57cd25bec test escape chars function 2017-12-18 12:35:44 +01:00
Simon Martín
a46f7b3099 nested options and using it 2017-12-18 12:34:56 +01:00
Simon Martín
cb7fb97a13 escape chars function 2017-12-18 11:14:27 +01:00
Daniel García Aubert
e4ae3e235d Merge branch 'master' of github.com:CartoDB/Windshaft-cartodb 2017-12-18 10:17:04 +01:00
Javier Goizueta
423620b6c5 Merge pull request #813 from CartoDB/point-grid-bug
Fix point-grid aggregation bug
2017-12-15 11:27:56 +01:00
Javier Goizueta
877ed63090 Add tests for the different aggregation placement values 2017-12-15 11:14:17 +01:00
Ivan Malagon
8e9f61f9f1 Merge pull request #809 from CartoDB/analyses-filters-params
Add `no_filters` param to dataviews
2017-12-15 11:13:56 +01:00
Javier Goizueta
81e54660bb Fix point-grid aggregation bug 2017-12-15 10:52:41 +01:00
Daniel García Aubert
4d6c501fa5 Merge branch 'master' of github.com:CartoDB/Windshaft-cartodb 2017-12-15 10:31:55 +01:00
Raul Ochoa
55d9c02f8d Merge pull request #812 from CartoDB/fix-agg-point-sample-grid
Use source query as attribute instead of function
2017-12-14 19:58:46 +01:00
Raul Ochoa
a0c24d132e Use source query as attribute instead of function 2017-12-14 18:43:59 +00:00
Daniel García Aubert
6dd4914460 Update NEWS 2017-12-14 18:52:54 +01:00
Daniel
ee4e7b01a9 Merge pull request #806 from CartoDB/mapconfig-aggregation
Mapconfig aggregation
2017-12-14 18:38:00 +01:00
Daniel García Aubert
434de7786c Fix test from merge 2017-12-14 18:26:15 +01:00
Daniel García Aubert
07b4cb78b1 Merge branch 'master' into mapconfig-aggregation 2017-12-14 18:19:54 +01:00
Javier Goizueta
6b472c0b20 Experimental aggregation dimensions
This is not meant por public consumption (exposing SQL expressions is undesiderable)
2017-12-14 17:51:49 +01:00
Ivan Malagon
4b0a4dd675 Update NEWS.md 2017-12-14 17:44:46 +01:00
Ivan Malagon
97f8c361ed Merge branch 'master' into analyses-filters-params 2017-12-14 17:40:14 +01:00
Javier Goizueta
0c044636ef Fix mode aggregation 2017-12-14 17:22:50 +01:00
Javier Goizueta
f95c310462 Redefine aggregation torque to match Torque
Now the resolution aggregation parameter has the same meaning as in Torque (-torque-resolution in CartoCSS)
2017-12-14 17:04:23 +01:00
Javier Goizueta
9d8ce6bc44 Refactor aggregation resolution 2017-12-14 17:04:23 +01:00
Daniel García Aubert
e4407ece84 Merge branch 'mapconfig-aggregation' of github.com:CartoDB/Windshaft-cartodb into mapconfig-aggregation 2017-12-14 16:39:07 +01:00
Javier Goizueta
507d105ab2 Add mode aggregation 2017-12-14 16:37:40 +01:00
Javier Goizueta
ba6cca46a1 Fix aggregation queries 2017-12-14 16:37:15 +01:00
Javier Goizueta
753ada0e76 Add cartodb_id to test datasets 2017-12-14 16:36:24 +01:00
Daniel García Aubert
d311dccce8 Add test to check tangram compatibility 2017-12-14 16:35:09 +01:00
Javier Goizueta
b81cfe418a Always add a _cdb_feature_count to aggregated queries 2017-12-14 15:02:03 +01:00
Daniel García Aubert
8ee4a2c049 Merge branch 'mapconfig-aggregation' of github.com:CartoDB/Windshaft-cartodb into mapconfig-aggregation 2017-12-14 14:15:20 +01:00
Daniel García Aubert
a987f6ac05 Fix issue when the sql has single quotes defined and the aggregation metadata query was not able to estimate row count 2017-12-14 14:14:55 +01:00
Javier Goizueta
b0e47ecc62 Fix aggregation resolution parameter
It was implemented as the inverse of the intended value
2017-12-14 12:23:02 +01:00
Javier Goizueta
daa3fdca11 Fix bug in point-grid aggregation 2017-12-14 12:14:31 +01:00
Daniel García Aubert
bcfc43a517 jshint, my old friend 2017-12-14 11:22:00 +01:00
Daniel García Aubert
b83351a504 Use last release of windshaft 2017-12-13 20:07:23 +01:00
Daniel García Aubert
1edf684475 Fix test 2017-12-13 20:04:06 +01:00
Daniel García Aubert
0bc68d7144 Do not override sql_raw 2017-12-13 19:46:25 +01:00
Daniel García Aubert
52d1cd47db Do not validate aggregation missing columns. It will fail afterwards in map validation 2017-12-13 19:24:17 +01:00
Daniel García Aubert
98e8d745b1 Support sql_wrap for aggregation 2017-12-13 17:01:43 +01:00
Daniel García Aubert
e8740af6ef Fix issue when sql_wrap is provided and aggregation metadata query fails 2017-12-13 16:34:36 +01:00
Simon Martín
a00f468e62 NEWS.md 2017-12-13 15:07:55 +01:00
Simon Martín
27a52b66c6 Merge pull request #800 from CartoDB/errorLogs
Logging all errors
2017-12-13 15:05:11 +01:00
Simon Martín
96b9d498fd Merge branch 'master' into errorLogs 2017-12-13 14:50:51 +01:00
Javier Goizueta
6d30903531 Merge branch 'mapconfig-aggregation' of github.com:CartoDB/Windshaft-cartodb into mapconfig-aggregation
# Conflicts:
#	lib/cartodb/models/mapconfig/adapter/aggregation-mapconfig-adapter.js
2017-12-13 12:53:20 +01:00
Javier Goizueta
4a63fed943 Simplify Aggregation classes
We're using the same aggregation queries for the Raster and Vector cases, so we don't need the class hierarchies used to handled them differently.
AggregationProxy has been renamed to Aggregation
2017-12-13 12:35:17 +01:00
Daniel García Aubert
6fe73862f3 Create a MapConfig's subclass to delegate aggregation 2017-12-13 11:42:51 +01:00
Ivan Malagon
1664975dd1 Add spec 2017-12-13 10:43:43 +01:00
Ivan Malagon
02ac25181e Return error if no_filters and own_filter are present 2017-12-13 09:45:35 +01:00
Daniel García Aubert
239aa12622 Merge branch 'mapconfig-aggregation' of github.com:CartoDB/Windshaft-cartodb into mapconfig-aggregation 2017-12-12 20:28:50 +01:00
Daniel García Aubert
aa43eb8953 Remove aggregation validation and use MapConfig validation 2017-12-12 20:10:42 +01:00
Daniel García Aubert
6d46a21005 Validate aggregation query param 2017-12-12 19:23:21 +01:00
Raul Ochoa
fb7f79594d Merge remote-tracking branch 'origin/master' into mapconfig-aggregation 2017-12-12 17:15:22 +00:00
Daniel García Aubert
f390a10830 Remove methods that check map-config aggregation and use the ones that MapConfig model provides 2017-12-12 17:58:42 +01:00
Javier Goizueta
4193f96c03 Fix point-grid aggregation query 2017-12-12 17:38:39 +01:00
Raul Ochoa
afa1e2881f Stubs next version 2017-12-12 16:24:12 +00:00
Raul Ochoa
3fa6750f9a Release 4.4.0 2017-12-12 16:23:37 +00:00
Raul Ochoa
1c842c1592 Merge pull request #810 from CartoDB/upgrade-camshaft
Upgrades camshaft to 0.60.0
2017-12-12 17:22:06 +01:00
Raul Ochoa
3c88634d09 Upgrades camshaft to 0.60.0 2017-12-12 16:14:49 +00:00
Simon Martín
19bb11adc5 line at EOF 2017-12-12 16:59:07 +01:00
Javier Goizueta
4405d61845 Remove support for arbitrary aggregation SQL expressions.
Only the supported aggregate functions can be used now, currently count, sum, avg, min & max.
2017-12-12 16:17:42 +01:00
Simon Martín
1bb716ef33 Merge branch 'master' into errorLogs 2017-12-12 16:15:30 +01:00
Javier Goizueta
eb2825eea8 Merge branch 'mapconfig-aggregation' of github.com:CartoDB/Windshaft-cartodb into mapconfig-aggregation
# Conflicts:
#	lib/cartodb/models/aggregation/aggregation-templates.js
2017-12-12 16:08:58 +01:00
Ivan Malagon
811f2bdae3 Fix linter problem 2017-12-12 16:04:25 +01:00
Ivan Malagon
53bc14bc9e Add missing contributors 2017-12-12 16:02:39 +01:00
Ivan Malagon
50ddfaa968 Fix broken spec 2017-12-12 15:56:16 +01:00
Daniel García Aubert
ae35acd21d typo 2017-12-12 15:54:36 +01:00
Daniel García Aubert
d4d32bdfa3 Make jshint more happy 2017-12-12 15:53:35 +01:00
Javier Goizueta
3b7db0b08f Fix typo 2017-12-12 15:48:25 +01:00
Ivan Malagon
43fec74372 Modify params specs 2017-12-12 15:12:33 +01:00
Mario de Frutos
c7f5f310f0 Stubs next version 2017-12-12 13:46:54 +01:00
Daniel García Aubert
e26cfb2efb Remove magic number 2017-12-12 13:32:01 +01:00
Mario de Frutos
b32d056efe Updated NEWS.md 2017-12-12 13:22:57 +01:00
Daniel García Aubert
0b27d174ef Check if query retrieves results 2017-12-12 12:53:29 +01:00
Daniel García Aubert
869f2ac322 Improve error message 2017-12-12 12:39:12 +01:00
Daniel García Aubert
5bc1903677 Add test to check if cartoccs and aggregation definition are fully compatible 2017-12-12 12:15:13 +01:00
Daniel García Aubert
faaebaa07d Remove console.log 2017-12-12 12:02:10 +01:00
Daniel García Aubert
eceffda87f Do not use control flag 2017-12-12 12:01:25 +01:00
Daniel García Aubert
e93fe13b41 Get the right columns from aggregation 2017-12-12 11:57:38 +01:00
Ivan Malagon
245d24ea29 Merge branch 'master' into analyses-filters-params 2017-12-12 11:54:32 +01:00
Ivan Malagon
605be77a04 Add nofilters query param 2017-12-12 11:54:09 +01:00
Daniel García Aubert
acd0610517 Merge branch 'mapconfig-aggregation' of github.com:CartoDB/Windshaft-cartodb into mapconfig-aggregation 2017-12-12 11:31:56 +01:00
Javier Goizueta
e37682403c Fix test
Note that the CartoCSS should reference columns of the aggregated table
2017-12-12 11:22:10 +01:00
Javier Goizueta
b2fcbdd8a3 Implement aggregation columns 2017-12-12 11:22:09 +01:00
Daniel García Aubert
2f68d658f0 Remove local variable 2017-12-12 11:10:12 +01:00
Daniel García Aubert
85e7245a33 Remove control flag 2017-12-12 11:07:04 +01:00
Simon Martín
2db2546cca changing error log format 2017-12-12 11:04:06 +01:00
Simon Martín
b3d7909849 removing default error log value 2017-12-12 11:03:52 +01:00
Simon Martín
b035b5d384 Merge branch 'master' into errorLogs 2017-12-12 10:59:07 +01:00
Daniel García Aubert
f52cc276be Remove control flag 2017-12-12 10:57:50 +01:00
Daniel García Aubert
c637caf9c9 Replace nested conditional with guard clause 2017-12-12 10:56:23 +01:00
Daniel García Aubert
d405987a96 Replace nested conditional with guard clause 2017-12-12 10:49:05 +01:00
Daniel García Aubert
06efe410ef Replace nested conditional with guard clause (early return) 2017-12-12 10:43:49 +01:00
Daniel García Aubert
5bf4eba215 Remove unused thenable 2017-12-11 19:35:59 +01:00
Daniel García Aubert
87c4848e19 Improve namig 2017-12-11 19:22:15 +01:00
Daniel García Aubert
3f075ca432 Remove unused argument 2017-12-11 19:18:29 +01:00
Daniel García Aubert
6725025e1a Improve naming for a method 2017-12-11 19:17:32 +01:00
Daniel García Aubert
8d42909eab Change argument order to be more consistent 2017-12-11 19:14:16 +01:00
Daniel García Aubert
d947700646 Get connection at the begining of adapt layers functionality 2017-12-11 19:12:10 +01:00
Daniel García Aubert
cc68b84212 Extract checkLayerAggregationMetadata method 2017-12-11 19:06:53 +01:00
Daniel García Aubert
446449bbde Move variable declaration close to the place that it's used 2017-12-11 18:47:20 +01:00
Daniel García Aubert
b1f788fb57 Remove unuseful callback 2017-12-11 18:42:03 +01:00
Daniel García Aubert
f80e7112bc Merge branch 'mapconfig-aggregation' of github.com:CartoDB/Windshaft-cartodb into mapconfig-aggregation 2017-12-11 18:35:54 +01:00
Daniel García Aubert
68f967e582 Extract adaptLayer method 2017-12-11 18:34:22 +01:00
Javier Goizueta
2edcbb4724 Implement aggregation queries.
Implmented for placements: centroid, point-gird, point-sample.
Aggregated columns not yet implemented (only count).
Aggregation could be made more efficient by using quadkeys
2017-12-11 18:33:06 +01:00
Daniel García Aubert
006dd86614 Merge branch 'master' into mapconfig-aggregation 2017-12-11 17:36:58 +01:00
Daniel García Aubert
dab204ea71 Do not aggregate if rows cout is lower than threshold or the layer's sql has geometries distinct of points 2017-12-11 17:32:06 +01:00
Simon Martín
1c6c3962db Merge branch 'master' into errorLogs 2017-12-11 12:59:42 +01:00
Daniel García Aubert
214d684fcc Adapt layer when is vector only map-caonfig 2017-12-05 20:39:30 +01:00
Daniel García Aubert
9118e2dc5e Add tests 2017-12-05 20:21:20 +01:00
Daniel García Aubert
e7592ee570 Improve error message 2017-12-05 17:44:52 +01:00
Daniel García Aubert
81d99ca655 Make test to pass 2017-12-05 16:52:15 +01:00
Daniel García Aubert
7b35701fa8 Extract method 2017-12-05 16:50:18 +01:00
Daniel García Aubert
4f8b541010 Mark aggregation queries 2017-12-05 13:12:25 +01:00
Daniel García Aubert
55dd049812 Be able to skip aggregation to create a layergroup with aggregation defined already 2017-12-05 12:59:32 +01:00
Daniel García Aubert
66b41a6ae7 Now .getLayergroup() in test client accepts params to perform custom instantiations 2017-12-05 12:09:31 +01:00
Daniel García Aubert
499e9de75d Use devel branch of windshaft 2017-12-04 19:49:35 +01:00
Daniel García Aubert
855f47e446 Detect incompatible CartoCSS or interactivity for raster aggregation 2017-12-04 19:48:06 +01:00
Daniel García Aubert
fc472e65b6 Update yarn.lock 2017-12-04 15:31:45 +01:00
Daniel García Aubert
91e0e0fd18 Merge branch 'master' into mapconfig-aggregation 2017-12-04 14:49:44 +01:00
Daniel García Aubert
077f19d506 Integrate aggregation and get metadata for layergroup 2017-12-04 12:40:53 +01:00
Simon Martín
ed51513b5e adding error header acceptance test 2017-12-01 17:52:20 +01:00
Daniel García Aubert
52630b8084 Minor improvementes 2017-12-01 17:06:42 +01:00
Daniel García Aubert
6f04214f5d Simplify to pass test 2017-12-01 17:06:03 +01:00
Daniel García Aubert
f376a7cdd5 Use aggregation adapter before the overviews one 2017-12-01 17:05:01 +01:00
Daniel García Aubert
b7c6f5acdf Merge branch 'vr-aggregation' into mapconfig-aggregation 2017-12-01 16:23:54 +01:00
Daniel García Aubert
0887e5d5f7 Extract method 2017-12-01 15:43:15 +01:00
Daniel García Aubert
d01857923e Plug aggregation mapconfig adapter 2017-11-30 19:31:00 +01:00
Daniel García Aubert
deb29f2c77 Implement aggregation mapconfig adapter (happy case) 2017-11-30 19:20:59 +01:00
Daniel García Aubert
d937ed31d5 Add params to instantiate aggregation 2017-11-30 19:10:57 +01:00
Daniel García Aubert
73ae736603 Add aggregation proxy 2017-11-30 19:02:30 +01:00
Daniel García Aubert
1767b83d09 Aggregation query models: bootstrap hierarchy classes 2017-11-30 15:34:20 +01:00
Simon Martín
ba3af551e3 update test file name 2017-11-30 15:04:38 +01:00
Simon Martín
e0d4a9e596 change funcion name 2017-11-30 15:04:07 +01:00
Simon Martín
555d3f558c changing error log structure 2017-11-28 18:22:55 +01:00
Simon Martín
386d6bfea8 removing unneeded check 2017-11-28 18:19:28 +01:00
Simon Martín
479b8be639 ensuring errored JSONP write a error status code in log 2017-11-28 17:27:05 +01:00
Simon Martín
a007fce913 ensuring vars 2017-11-28 16:02:12 +01:00
Simon Martín
100a2986b9 ensuring all properties in errors headers 2017-11-27 18:43:48 +01:00
Simon Martín
752bfe779e forgotten 'only' 2017-11-27 18:15:27 +01:00
Simon Martín
8cf878f723 testing X-Tiler-Errors existence 2017-11-27 18:14:02 +01:00
Simon Martín
605d7057c9 fix copying array of errors and adding error.label to logs 2017-11-27 18:12:44 +01:00
Simon Martín
60e4defa66 default value in errors header 2017-11-27 17:04:50 +01:00
Simon Martín
e7b8d9b223 moving logErrors to right position 2017-11-27 16:55:11 +01:00
Simon Martín
e041b5b8a9 removing ~lost space 2017-11-27 16:52:19 +01:00
Simon Martín
4a2950796b rest of environments config 2017-11-27 16:48:25 +01:00
Simon Martín
9a8f72b8db format details 2017-11-27 16:47:45 +01:00
Simon Martín
667925c455 adding error name, ensuring data and moving errors copy 2017-11-27 16:43:04 +01:00
Simon Martín
f24217a400 cloning object and removing logs 2017-11-24 18:06:17 +01:00
Simon Martín
84fd01535c adding errors to errors header 2017-11-24 17:53:07 +01:00
Simon Martín
e362fca9eb adding new header 2017-11-24 17:52:26 +01:00
Daniel García Aubert
00f81db57e Fixed default value for own_filter 2017-10-20 16:47:56 +02:00
Daniel García Aubert
0c9d60b573 Add support for no_filters params in dataviews 2017-10-20 16:19:24 +02:00
Raul Ochoa
ad227a5240 Merge remote-tracking branch 'origin/master' into analyses-filters 2017-10-10 16:35:11 +00:00
Raul Ochoa
2b1f12e9d5 Allow to instantiate maps with analyses filters
This decouples filters from dataviews. They are more verbose now.

Misses validation of filters.
2017-10-02 19:16:44 +02:00
49 changed files with 4295 additions and 486 deletions

41
NEWS.md
View File

@@ -1,10 +1,49 @@
# Changelog
## 4.7.0
Released 2018-01-03
New features:
- Return tilejson in metadata #837.
Bug fixes:
- Allow to create vector map-config for layers that doesn't have points. Layers with lines or polygons won't be aggregated by default.
## 4.6.0
Released 2018-01-02
Announcements:
- Upgrades windshaft to [4.2.0](https://github.com/CartoDB/windshaft/releases/tag/4.2.0).
- Validate aggregation input params.
- Fix column names collisions in histograms [#828](https://github.com/CartoDB/Windshaft-cartodb/pull/828).
- Add full-sample aggregation support for vector map-config.
## 4.5.0
Released 2017-12-19
Announcements:
- Date histograms: Add second, decade, century and millenium aggregations
- Date histograms: Switch the auto threshold from 366 buckets to 100.
- Logging all errors.
- Add support for aggregated visualizations.
- Allow vector-only map-config creation.
- Histograms: Now they accept a `no_filters` parameter.
## 4.4.0
Released 2017-12-12
Announcements:
- Upgrades camshaft to [0.60.0](https://github.com/CartoDB/camshaft/releases/tag/0.60.0).
## 4.3.1
Released 2017-12-12
Bug fix:
- Fixed bug introduced in version 4.0.1 that brokes the static map generation using JPG as format
- Fixed bug introduced in version 4.0.1 that brokes the static map generation using JPG as format #808
## 4.3.0
Released 2017-12-11

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -106,6 +106,7 @@ LayergroupController.prototype.register = function(app) {
var allowedDataviewQueryParams = [
'filters', // json
'own_filter', // 0, 1
'no_filters', // 0, 1
'bbox', // w,s,e,n
'start', // number
'end', // number

View File

@@ -6,6 +6,7 @@ var ResourceLocator = require('../models/resource-locator');
var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');
const allowQueryParams = require('../middleware/allow-query-params');
var MapConfig = windshaft.model.MapConfig;
var Datasource = windshaft.model.Datasource;
@@ -69,6 +70,7 @@ MapController.prototype.composeCreateMapMiddleware = function (useTemplate = fal
return [
cors(),
userMiddleware,
allowQueryParams(['aggregation']),
this.prepareContext,
this.initProfiler(isTemplateInstantiation),
this.checkJsonContentType(),
@@ -87,6 +89,8 @@ MapController.prototype.composeCreateMapMiddleware = function (useTemplate = fal
this.setDataviewsAndWidgetsUrlsToLayergroupMetadata(),
this.setAnalysesMetadataToLayergroup(includeQuery),
this.setTurboCartoMetadataToLayergroup(),
this.setAggregationMetadataToLayergroup(),
this.setTilejsonMetadataToLayergroup(),
this.setSurrogateKeyHeader(),
this.sendResponse(),
this.augmentError({ label, addContext })
@@ -310,6 +314,74 @@ MapController.prototype.augmentLayergroupData = function () {
};
};
function getTilejson(tiles, grids) {
const tilejson = {
tilejson: '2.2.0',
tiles: tiles.https || tiles.http
};
if (grids) {
tilejson.grids = grids.https || grids.http;
}
return tilejson;
}
MapController.prototype.setTilejsonMetadataToLayergroup = function () {
return function augmentLayergroupTilejsonMiddleware (req, res, next) {
const { layergroup, user, mapconfig } = res.locals;
const isVectorOnlyMapConfig = mapconfig.isVectorOnlyMapConfig();
let hasMapnikLayers = false;
layergroup.metadata.layers.forEach((layerMetadata, index) => {
const layerId = mapconfig.getLayerId(index);
const rasterResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.png`;
if (mapconfig.layerType(index) === 'mapnik') {
hasMapnikLayers = true;
const vectorResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.mvt`;
const layerTilejson = {
vector: getTilejson(this.resourceLocator.getTileUrls(user, vectorResource))
};
if (!isVectorOnlyMapConfig) {
let grids = null;
const layer = mapconfig.getLayer(index);
if (layer.options.interactivity) {
const gridResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.grid.json`;
grids = this.resourceLocator.getTileUrls(user, gridResource);
}
layerTilejson.raster = getTilejson(
this.resourceLocator.getTileUrls(user, rasterResource),
grids
);
}
layerMetadata.tilejson = layerTilejson;
} else {
layerMetadata.tilejson = {
raster: getTilejson(this.resourceLocator.getTileUrls(user, rasterResource))
};
}
});
const tilejson = {};
if (hasMapnikLayers) {
tilejson.vector = getTilejson(
this.resourceLocator.getTileUrls(user, `${layergroup.layergroupid}/{z}/{x}/{y}.mvt`)
);
if (!isVectorOnlyMapConfig) {
tilejson.raster = getTilejson(
this.resourceLocator.getTileUrls(user, `${layergroup.layergroupid}/{z}/{x}/{y}.png`)
);
}
}
layergroup.metadata.tilejson = tilejson;
next();
}.bind(this);
};
MapController.prototype.getAffectedTables = function () {
return function getAffectedTablesMiddleware (req, res, next) {
const { dbname, layergroup, user, mapconfig } = res.locals;
@@ -540,13 +612,13 @@ MapController.prototype.setTurboCartoMetadataToLayergroup = function () {
return function setTurboCartoMetadataToLayergroupMiddleware (req, res, next) {
const { layergroup, mapconfig, context } = res.locals;
addContextMetadata(layergroup, mapconfig.obj(), context);
addTurboCartoContextMetadata(layergroup, mapconfig.obj(), context);
next();
};
};
function addContextMetadata(layergroup, mapConfig, context) {
function addTurboCartoContextMetadata(layergroup, mapConfig, context) {
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
if (context.turboCarto && Array.isArray(context.turboCarto.layers)) {
@@ -557,6 +629,28 @@ function addContextMetadata(layergroup, mapConfig, context) {
}
}
// TODO: see how evolve this function, it's a good candidate to be refactored
MapController.prototype.setAggregationMetadataToLayergroup = function () {
return function setAggregationMetadataToLayergroupMiddleware (req, res, next) {
const { layergroup, mapconfig, context } = res.locals;
addAggregationContextMetadata(layergroup, mapconfig.obj(), context);
next();
};
};
function addAggregationContextMetadata(layergroup, mapConfig, context) {
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
if (context.aggregation && Array.isArray(context.aggregation.layers)) {
layer.meta.aggregation = context.aggregation.layers[layerIndex];
}
return layer;
});
}
}
MapController.prototype.setSurrogateKeyHeader = function () {
return function setSurrogateKeyHeaderMiddleware(req, res, next) {
const { affectedTables, user, templateName } = res.locals;

View File

@@ -1,10 +1,6 @@
var step = require('step');
var assert = require('assert');
var templateName = require('../backends/template_maps').templateName;
var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');
const { templateName } = require('../backends/template_maps');
const cors = require('../middleware/cors');
const userMiddleware = require('../middleware/user');
/**
* @param {AuthApi} authApi
@@ -20,211 +16,189 @@ function NamedMapsAdminController(authApi, templateMaps) {
module.exports = NamedMapsAdminController;
NamedMapsAdminController.prototype.register = function (app) {
const { base_url_templated } = app;
app.post(
app.base_url_templated + '/',
`${base_url_templated}/`,
cors(),
userMiddleware,
this.create.bind(this)
this.checkContentType('POST', 'POST TEMPLATE'),
this.authorizedByAPIKey('create', 'POST TEMPLATE'),
this.create()
);
app.put(
app.base_url_templated + '/:template_id',
`${base_url_templated}/:template_id`,
cors(),
userMiddleware,
this.update.bind(this)
this.checkContentType('PUT', 'PUT TEMPLATE'),
this.authorizedByAPIKey('update', 'PUT TEMPLATE'),
this.update()
);
app.get(
app.base_url_templated + '/:template_id',
`${base_url_templated}/:template_id`,
cors(),
userMiddleware,
this.retrieve.bind(this)
this.authorizedByAPIKey('get', 'GET TEMPLATE'),
this.retrieve()
);
app.delete(
app.base_url_templated + '/:template_id',
`${base_url_templated}/:template_id`,
cors(),
userMiddleware,
this.destroy.bind(this)
this.authorizedByAPIKey('delete', 'DELETE TEMPLATE'),
this.destroy()
);
app.get(
app.base_url_templated + '/',
`${base_url_templated}/`,
cors(),
userMiddleware,
this.list.bind(this)
this.authorizedByAPIKey('list', 'GET TEMPLATE LIST'),
this.list()
);
app.options(
app.base_url_templated + '/:template_id',
`${base_url_templated}/:template_id`,
cors('Content-Type')
);
};
NamedMapsAdminController.prototype.create = function(req, res, next) {
var self = this;
NamedMapsAdminController.prototype.authorizedByAPIKey = function (action, label) {
return function authorizedByAPIKeyMiddleware (req, res, next) {
const { user } = res.locals;
var cdbuser = res.locals.user;
this.authApi.authorizedByAPIKey(user, req, (err, authenticated) => {
if (err) {
return next(err);
}
step(
function checkPerms(){
self.authApi.authorizedByAPIKey(cdbuser, req, this);
},
function addTemplate(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
ifInvalidContentType(req, 'template POST data must be of type application/json');
var cfg = req.body;
self.templateMaps.addTemplate(cdbuser, cfg, this);
},
function prepareResponse(err, tpl_id){
assert.ifError(err);
return { template_id: tpl_id };
},
finishFn(self, req, res, 'POST TEMPLATE', null, next)
);
if (!authenticated) {
const error = new Error(`Only authenticated user can ${action} templated maps`);
error.http_status = 403;
error.label = label;
return next(error);
}
next();
});
}.bind(this);
};
NamedMapsAdminController.prototype.update = function(req, res, next) {
var self = this;
var cdbuser = res.locals.user;
var template;
var tpl_id;
step(
function checkPerms(){
self.authApi.authorizedByAPIKey(cdbuser, req, this);
},
function updateTemplate(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated user can update templated maps');
ifInvalidContentType(req, 'template PUT data must be of type application/json');
template = req.body;
tpl_id = templateName(req.params.template_id);
self.templateMaps.updTemplate(cdbuser, tpl_id, template, this);
},
function prepareResponse(err){
assert.ifError(err);
return { template_id: tpl_id };
},
finishFn(self, req, res, 'PUT TEMPLATE', null, next)
);
NamedMapsAdminController.prototype.checkContentType = function (action, label) {
return function checkContentTypeMiddleware (req, res, next) {
if (!req.is('application/json')) {
const error = new Error(`template ${action} data must be of type application/json`);
error.label = label;
return next(error);
}
next();
};
};
NamedMapsAdminController.prototype.retrieve = function(req, res, next) {
var self = this;
NamedMapsAdminController.prototype.create = function () {
return function createTemplateMiddleware (req, res, next) {
const { user } = res.locals;
const template = req.body;
req.profiler.start('windshaft-cartodb.get_template');
this.templateMaps.addTemplate(user, template, (err, templateId) => {
if (err) {
return next(err);
}
var cdbuser = res.locals.user;
var tpl_id;
step(
function checkPerms(){
self.authApi.authorizedByAPIKey(cdbuser, req, this);
},
function getTemplate(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
res.status(200);
tpl_id = templateName(req.params.template_id);
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
},
function prepareResponse(err, tpl_val) {
assert.ifError(err);
if ( ! tpl_val ) {
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
err.http_status = 404;
throw err;
const method = req.query.callback ? 'jsonp' : 'json';
res[method]({ template_id: templateId });
});
}.bind(this);
};
NamedMapsAdminController.prototype.update = function () {
return function updateTemplateMiddleware (req, res, next) {
const { user } = res.locals;
const template = req.body;
const templateId = templateName(req.params.template_id);
this.templateMaps.updTemplate(user, templateId, template, (err) => {
if (err) {
return next(err);
}
res.status(200);
const method = req.query.callback ? 'jsonp' : 'json';
res[method]({ template_id: templateId });
});
}.bind(this);
};
NamedMapsAdminController.prototype.retrieve = function () {
return function retrieveTemplateMiddleware (req, res, next) {
req.profiler.start('windshaft-cartodb.get_template');
const { user } = res.locals;
const templateId = templateName(req.params.template_id);
this.templateMaps.getTemplate(user, templateId, (err, template) => {
if (err) {
return next(err);
}
if (!template) {
const error = new Error(`Cannot find template '${templateId}' of user '${user}'`);
error.http_status = 404;
return next(error);
}
// auth_id was added by ourselves,
// so we remove it before returning to the user
delete tpl_val.auth_id;
return { template: tpl_val };
},
finishFn(self, req, res, 'GET TEMPLATE', null, next)
);
delete template.auth_id;
res.status(200);
const method = req.query.callback ? 'jsonp' : 'json';
res[method]({ template });
});
}.bind(this);
};
NamedMapsAdminController.prototype.destroy = function(req, res, next) {
var self = this;
NamedMapsAdminController.prototype.destroy = function () {
return function destroyTemplateMiddleware (req, res, next) {
req.profiler.start('windshaft-cartodb.delete_template');
req.profiler.start('windshaft-cartodb.delete_template');
const { user } = res.locals;
const templateId = templateName(req.params.template_id);
var cdbuser = res.locals.user;
var tpl_id;
step(
function checkPerms(){
self.authApi.authorizedByAPIKey(cdbuser, req, this);
},
function deleteTemplate(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated users can delete template maps');
tpl_id = templateName(req.params.template_id);
self.templateMaps.delTemplate(cdbuser, tpl_id, this);
},
function prepareResponse(err/*, tpl_val*/){
assert.ifError(err);
return '';
},
finishFn(self, req, res, 'DELETE TEMPLATE', 204, next)
);
};
NamedMapsAdminController.prototype.list = function(req, res, next) {
var self = this;
req.profiler.start('windshaft-cartodb.get_template_list');
var cdbuser = res.locals.user;
step(
function checkPerms(){
self.authApi.authorizedByAPIKey(cdbuser, req, this);
},
function listTemplates(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated user can list templated maps');
self.templateMaps.listTemplates(cdbuser, this);
},
function prepareResponse(err, tpl_ids){
assert.ifError(err);
return { template_ids: tpl_ids };
},
finishFn(self, req, res, 'GET TEMPLATE LIST', null, next)
);
};
function finishFn(controller, req, res, description, status, next) {
return function finish(err, body){
if (err) {
err.label = description;
next(err);
} else {
res.status(status || 200);
if (req.query && req.query.callback) {
res.jsonp(body);
} else {
res.json(body);
this.templateMaps.delTemplate(user, templateId, (err/* , tpl_val */) => {
if (err) {
return next(err);
}
}
};
}
function ifUnauthenticated(authenticated, description) {
if (!authenticated) {
var err = new Error(description);
err.http_status = 403;
throw err;
}
}
res.status(204);
function ifInvalidContentType(req, description) {
if (!req.is('application/json')) {
throw new Error(description);
}
}
const method = req.query.callback ? 'jsonp' : 'json';
res[method]('');
});
}.bind(this);
};
NamedMapsAdminController.prototype.list = function () {
return function listTemplatesMiddleware (req, res, next) {
req.profiler.start('windshaft-cartodb.get_template_list');
const { user } = res.locals;
this.templateMaps.listTemplates(user, (err, templateIds) => {
if (err) {
return next(err);
}
res.status(200);
const method = req.query.callback ? 'jsonp' : 'json';
res[method]({ template_ids: templateIds });
});
}.bind(this);
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -56,7 +56,7 @@ __wd_buckets AS
${ctx.query}
) __source, __wd_tz
${condition_str}
GROUP BY timestamp, __wd_tz.name
GROUP BY 1, __wd_tz.name
),`;
}
@@ -127,8 +127,11 @@ const dateIntervalQueryTpl = ctx => `
FROM __cdb_interval_in_minutes, __cdb_dates
)
SELECT
ROUND(__cdb_days / 365243) AS millennium,
ROUND(__cdb_days / 36525) AS century,
ROUND(__cdb_days / 3652) AS decade,
ROUND(__cdb_days / 365) AS year,
ROUND(__cdb_days / 90) AS quarter,
ROUND(__cdb_days / 91) AS quarter,
ROUND(__cdb_days / 30) AS month,
ROUND(__cdb_days / 7) AS week,
__cdb_days AS day,
@@ -138,18 +141,22 @@ const dateIntervalQueryTpl = ctx => `
FROM __cdb_interval_in_days, __cdb_interval_in_hours, __cdb_interval_in_minutes, __cdb_interval_in_seconds
`;
const MAX_INTERVAL_VALUE = 366;
/** Constant to switch between aggregations in auto mode */
const MAX_INTERVAL_VALUE = 100;
const DATE_AGGREGATIONS = {
'auto': true,
'second' : true,
'minute': true,
'hour': true,
'day': true,
'week': true,
'month': true,
'quarter': true,
'year': true
'year': true,
'decade' : true,
'century' : true,
'millennium' : true
};
/**
@@ -243,9 +250,9 @@ ORDER BY bin ASC;
return callback(err);
}
const aggegations = result.rows[0];
const aggregation = Object.keys(aggegations)
.map(key => ({ name: key, value: aggegations[key] }))
const aggregations = result.rows[0];
const aggregation = Object.keys(aggregations)
.map(key => ({ name: key, value: aggregations[key] }))
.reduce((closer, current) => {
if (current.value > MAX_INTERVAL_VALUE) {
return closer;

View File

@@ -28,7 +28,7 @@ const binsQueryTpl = ctx => `
) AS quartile
FROM __cdb_filtered_source) _cdb_quartiles
WHERE quartile = 1 or quartile = 3
GROUP BY quartile
GROUP BY 1
) __cdb_iqr
),
__cdb_bins AS (
@@ -137,8 +137,8 @@ FROM
(
${ctx.query}
) __cdb_filtered_source_query${extra_tables}
GROUP BY bin${extra_groupby}
ORDER BY bin;`;
GROUP BY 10${extra_groupby}
ORDER BY 10;`;
}
_hasOverridenBins (override) {

View File

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

View File

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

View File

@@ -21,20 +21,36 @@ function ResourceLocator(environment) {
module.exports = ResourceLocator;
ResourceLocator.prototype.getUrls = function(username, resource) {
ResourceLocator.prototype.getTileUrls = function(username, resourcePath) {
if (this.resourcesUrlTemplates) {
return this.getUrlsFromTemplate(username, resource);
}
var cdnDomain = getCdnDomain(this.environment.serverMetadata, resource);
if (cdnDomain) {
const urls = this.getUrlsFromTemplate(username, new TileResource(resourcePath));
return {
http: 'http://' + cdnDomain.http + '/' + username + '/api/v1/map/' + resource,
https: 'https://' + cdnDomain.https + '/' + username + '/api/v1/map/' + resource
http: Array.isArray(urls.http) ? urls.http : [urls.http],
https: Array.isArray(urls.https) ? urls.https : [urls.https]
};
}
var cdnUrls = getCdnUrls(this.environment.serverMetadata, username, new TileResource(resourcePath));
if (cdnUrls) {
return cdnUrls;
} else {
var port = this.environment.port;
return {
http: 'http://' + username + '.' + 'localhost.lan:' + port + '/api/v1/map/' + resource
http: [`http://${username}.localhost.lan:${port}/api/v1/map/${resourcePath}`]
};
}
};
ResourceLocator.prototype.getUrls = function(username, resourcePath) {
if (this.resourcesUrlTemplates) {
return this.getUrlsFromTemplate(username, new Resource(resourcePath));
}
var cdnUrls = getCdnUrls(this.environment.serverMetadata, username, new Resource(resourcePath));
if (cdnUrls) {
return cdnUrls;
} else {
var port = this.environment.port;
return {
http: `http://${username}.localhost.lan:${port}/api/v1/map/${resourcePath}`
};
}
};
@@ -45,45 +61,125 @@ ResourceLocator.prototype.getUrlsFromTemplate = function(username, resource) {
var cdnDomain = getCdnDomain(this.environment.serverMetadata, resource) || {};
if (this.resourcesUrlTemplates.http) {
urls.http = this.resourcesUrlTemplates.http({
cdn_url: cdnDomain.http,
user: username,
port: this.environment.port,
resource: resource
});
if (Array.isArray(cdnDomain.http)) {
urls.http = cdnDomain.http.map(d => this.resourcesUrlTemplates.http({
cdn_url: d,
user: username,
port: this.environment.port,
resource: resource.getPath()
}));
} else {
urls.http = this.resourcesUrlTemplates.http({
cdn_url: cdnDomain.http,
user: username,
port: this.environment.port,
resource: resource.getPath()
});
}
}
if (this.resourcesUrlTemplates.https) {
urls.https = this.resourcesUrlTemplates.https({
cdn_url: cdnDomain.https,
user: username,
port: this.environment.port,
resource: resource
});
if (Array.isArray(cdnDomain.https)) {
urls.https = cdnDomain.https.map(d => this.resourcesUrlTemplates.https({
cdn_url: d,
user: username,
port: this.environment.port,
resource: resource.getPath()
}));
} else {
urls.https = this.resourcesUrlTemplates.https({
cdn_url: cdnDomain.https,
user: username,
port: this.environment.port,
resource: resource.getPath()
});
}
}
return urls;
};
class Resource {
constructor (resourcePath) {
this.resourcePath = resourcePath;
}
getPath () {
return this.resourcePath;
}
getDomain (domain, subdomains) {
return domain.replace('{s}', subdomain(subdomains, this.resourcePath));
}
getUrl (baseUrl, username, subdomains) {
let urls = getUrl(baseUrl, username, this.resourcePath);
if (subdomains) {
urls = urls.replace('{s}', subdomain(subdomains, this.resourcePath));
}
return urls;
}
}
class TileResource extends Resource {
constructor (resourcePath) {
super(resourcePath);
}
getDomain (domain, subdomains) {
return subdomains.map(s => domain.replace('{s}', s));
}
getUrl (baseUrl, username, subdomains) {
if (!subdomains) {
return [super.getUrl(baseUrl, username)];
}
return subdomains.map(subdomain => {
return getUrl(baseUrl, username, this.resourcePath)
.replace('{s}', subdomain);
});
}
}
function getUrl(baseUrl, username, path) {
return `${baseUrl}/${username}/api/v1/map/${path}`;
}
function getCdnUrls(serverMetadata, username, resource) {
if (serverMetadata && serverMetadata.cdn_url) {
var cdnUrl = serverMetadata.cdn_url;
var httpUrls = resource.getUrl(`http://${cdnUrl.http}`, username);
var httpsUrls = resource.getUrl(`https://${cdnUrl.https}`, username);
if (cdnUrl.templates) {
var templates = cdnUrl.templates;
httpUrls = resource.getUrl(templates.http.url, username, templates.http.subdomains);
httpsUrls = resource.getUrl(templates.https.url, username, templates.https.subdomains);
}
return {
http: httpUrls,
https: httpsUrls,
};
}
return null;
}
function getCdnDomain(serverMetadata, resource) {
if (serverMetadata && serverMetadata.cdn_url) {
var cdnUrl = serverMetadata.cdn_url;
var http = cdnUrl.http;
var https = cdnUrl.https;
var httpDomain = cdnUrl.http;
var httpsDomain = cdnUrl.https;
if (cdnUrl.templates) {
var templates = cdnUrl.templates;
var httpUrlTemplate = templates.http.url;
var httpsUrlTemplate = templates.https.url;
http = httpUrlTemplate
.replace(/^(http[s]*:\/\/)/, '')
.replace('{s}', subdomain(templates.http.subdomains, resource));
https = httpsUrlTemplate
.replace(/^(http[s]*:\/\/)/, '')
.replace('{s}', subdomain(templates.https.subdomains, resource));
httpDomain = httpUrlTemplate.replace(/^(http[s]*:\/\/)/, '');
httpDomain = resource.getDomain(httpDomain, templates.http.subdomains);
httpsDomain = httpsUrlTemplate.replace(/^(http[s]*:\/\/)/, '');
httpsDomain = resource.getDomain(httpsDomain, templates.https.subdomains);
}
return {
http: http,
https: https,
http: httpDomain,
https: httpsDomain,
};
}
return null;

View File

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

View File

@@ -21,9 +21,25 @@ module.exports.extractTableNames = function extractTableNames(query) {
].join('');
};
module.exports.getQueryRowCount = function getQueryRowEstimation(query) {
return 'select CDB_EstimateRowCount($$' + query + '$$) as rows';
};
function getQueryRowEstimation(query) {
return 'select CDB_EstimateRowCount($windshaft$' + query + '$windshaft$) as rows';
}
module.exports.getQueryRowCount = getQueryRowEstimation;
module.exports.getAggregationMetadata = ctx => `
WITH
rowEstimation AS (
${getQueryRowEstimation(ctx.query)}
),
geometryType AS (
SELECT ST_GeometryType(the_geom) as geom_type
FROM (${ctx.query}) AS __cdb_query WHERE the_geom IS NOT NULL LIMIT 1
)
SELECT
rows AS count,
geom_type AS type
FROM rowEstimation, geometryType;
`;
/** Cast the column to epoch */
module.exports.columnCastTpl = function columnCastTpl(ctx) {
@@ -54,4 +70,4 @@ module.exports.countNaNs = function countNaNs(ctx) {
return `${!ctx.isFloatColumn ? `0` :
`sum(CASE WHEN (${ctx.column} = 'NaN'::float) THEN 1 ELSE 0 END)`
}`;
};
};

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "windshaft-cartodb",
"version": "4.3.1",
"version": "4.7.0",
"description": "A map tile server for CartoDB",
"keywords": [
"cartodb"
@@ -19,11 +19,13 @@
"Sandro Santilli <strk@vizzuality.com>",
"Carlos Matallín <matallo@carto.com>",
"Daniel Garcia Aubert <dgaubert@carto.com>",
"Mario de Frutos <mario.defrutos@carto.com>"
"Mario de Frutos <mario.defrutos@carto.com>",
"Ivan Malagon <ivan@carto.com>",
"Simon Martin <simon@carto.com>"
],
"dependencies": {
"body-parser": "^1.18.2",
"camshaft": "0.59.4",
"camshaft": "0.60.0",
"cartodb-psql": "0.10.2",
"cartodb-query-tables": "0.3.0",
"cartodb-redis": "0.14.0",
@@ -44,7 +46,7 @@
"step-profiler": "~0.3.0",
"turbo-carto": "0.20.2",
"underscore": "~1.6.0",
"windshaft": "4.1.0",
"windshaft": "4.2.0",
"yargs": "~5.0.0"
},
"devDependencies": {
@@ -63,7 +65,7 @@
"update-internal-deps": "rm -rf node_modules && rm -f yarn.lock && yarn",
"docker-install": "sudo apt install docker.io && sudo usermod -aG docker $(whoami)",
"docker-pull": "docker pull cartoimages/windshaft-testing",
"docker-test": "docker run -v `pwd`:/srv cartoimages/windshaft-testing bash docker-test.sh",
"docker-test": "docker run -v `pwd`:/srv cartoimages/windshaft-testing bash docker-test.sh && docker ps --filter status=dead --filter status=exited -aq | xargs -r docker rm -v",
"docker-bash": "docker run -it -v `pwd`:/srv cartoimages/windshaft-testing bash",
"docker-publish": "docker push cartoimages/windshaft-carto-testing"
},

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -124,7 +124,7 @@ describe('histogram-dataview for date column type', function() {
"type": "cartodb",
"options": {
"source": {
"id": "datetime-histogram-source"
"id": "datetime-histogram-source-week"
},
"cartocss": "#points { marker-width: 10; marker-fill: red; }",
"cartocss_version": "2.3.0"
@@ -134,7 +134,7 @@ describe('histogram-dataview for date column type', function() {
{
datetime_histogram: {
source: {
id: 'datetime-histogram-source'
id: 'datetime-histogram-source-week'
},
type: 'histogram',
options: {
@@ -154,9 +154,109 @@ describe('histogram-dataview for date column type', function() {
offset: -14400 // EDT Eastern Daylight Time (GMT-4) in seconds
}
},
datetime_histogram_automatic: {
datetime_histogram_automatic_second: {
source: {
id: 'datetime-histogram-source'
id: 'datetime-histogram-source-second'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_minute: {
source: {
id: 'datetime-histogram-source-minute'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_hour: {
source: {
id: 'datetime-histogram-source-hour'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_day: {
source: {
id: 'datetime-histogram-source-day'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_week: {
source: {
id: 'datetime-histogram-source-week'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_month: {
source: {
id: 'datetime-histogram-source-month'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_quarter: {
source: {
id: 'datetime-histogram-source-quarter'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_year: {
source: {
id: 'datetime-histogram-source-year'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_decade: {
source: {
id: 'datetime-histogram-source-decade'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_century: {
source: {
id: 'datetime-histogram-source-century'
},
type: 'histogram',
options: {
column: 'd',
aggregation: 'auto'
}
},
datetime_histogram_automatic_millennium: {
source: {
id: 'datetime-histogram-source-millennium'
},
type: 'histogram',
options: {
@@ -197,13 +297,144 @@ describe('histogram-dataview for date column type', function() {
},
[
{
"id": "datetime-histogram-source",
"id": "datetime-histogram-source-second",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'2007-02-15 01:00:00'::timestamp, '2008-04-09 01:00:00'::timestamp, '1 day'::interval",
"'2007-02-15 01:00:00'::timestamp, '2007-02-15 01:00:57'::timestamp,",
"'0.9 second'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-minute",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'2007-02-15 01:00:00'::timestamp, '2007-02-15 02:00:57'::timestamp,",
"'75 second'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-hour",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'2007-02-15 01:00:00'::timestamp, '2007-02-18 02:00:57'::timestamp,",
"'47 minutes'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-day",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'2007-02-15 01:00:00'::timestamp, '2007-04-18 02:00:57'::timestamp,",
"'24 hours'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-week",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'2007-02-15 01:00:00'::timestamp, '2008-04-09 01:00:00'::timestamp,",
"'1 day'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-month",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'2007-02-15 01:00:00'::timestamp, '2010-04-09 01:00:00'::timestamp,",
"'30 day'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-quarter",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'2007-02-15 01:00:00'::timestamp, '2020-04-09 01:00:00'::timestamp,",
"'30 day'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-year",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'1990-02-15 01:00:00'::timestamp, '2018-04-09 01:00:00'::timestamp,",
"'30 day'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-decade",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'1850-02-15 01:00:00'::timestamp, '2018-04-09 01:00:00'::timestamp,",
"'30 day'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-century",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'0650-02-15 01:00:00'::timestamp, '1918-04-09 01:00:00'::timestamp,",
"'6 years'::interval",
") date"
].join(' ')
}
},
{
"id": "datetime-histogram-source-millennium",
"type": "source",
"params": {
"query": [
"select null::geometry the_geom_webmercator, date AS d",
"from generate_series(",
"'0005-02-15 01:00:00'::timestamp, '12000-04-09 01:00:00'::timestamp,",
"'72 years'::interval",
") date"
].join(' ')
}
@@ -429,6 +660,47 @@ describe('histogram-dataview for date column type', function() {
});
});
it('should aggregate histogram using "second" aggregation ' + test.desc, function (done) {
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
var params = {
offset: OFFSET_UTC_IN_SECONDS,
aggregation: 'second'
};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_second', params, function (err, dataview) {
assert.ok(!err, err);
assert.equal(dataview.type, 'histogram');
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
assert.equal(dataview.bins.length, 57);
var initialTimestamp = '2007-02-15T01:00:00Z';
var binsStartInMilliseconds = dataview.bins_start * 1000;
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.format();
assert.equal(binsStartFormatted, initialTimestamp);
dataview.bins.forEach(function (bin, index) {
var binTimestampExpected = moment.utc(initialTimestamp)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.add(index, 'second')
.format();
var binsTimestampInMilliseconds = bin.timestamp * 1000;
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.format();
assert.equal(binTimestampFormatted, binTimestampExpected);
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
});
done();
});
});
it('should aggregate histogram using "quarter" aggregation ' + test.desc, function (done) {
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
@@ -470,6 +742,132 @@ describe('histogram-dataview for date column type', function() {
});
});
it('should aggregate histogram using "decade" aggregation ' + test.desc, function (done) {
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
var params = {
offset: OFFSET_UTC_IN_SECONDS,
aggregation: 'decade'
};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_decade', params, function (err, dataview) {
assert.ok(!err, err);
assert.equal(dataview.type, 'histogram');
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
assert.equal(dataview.bins.length, 17);
var initialTimestamp = '1850-01-01T00:00:00Z';
var binsStartInMilliseconds = dataview.bins_start * 1000;
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.format();
assert.equal(binsStartFormatted, initialTimestamp);
dataview.bins.forEach(function (bin, index) {
var binTimestampExpected = moment.utc(initialTimestamp)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.add(index * 10, 'year')
.format();
var binsTimestampInMilliseconds = bin.timestamp * 1000;
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.format();
assert.equal(binTimestampFormatted, binTimestampExpected);
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
});
done();
});
});
it('should aggregate histogram using "century" aggregation ' + test.desc, function (done) {
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
var params = {
offset: OFFSET_UTC_IN_SECONDS,
aggregation: 'century'
};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_century', params, function (err, dataview) {
assert.ok(!err, err);
assert.equal(dataview.type, 'histogram');
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
assert.equal(dataview.bins.length, 14);
var initialTimestamp = '0601-01-01T00:00:00Z';
var binsStartInMilliseconds = dataview.bins_start * 1000;
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.format();
assert.equal(binsStartFormatted, initialTimestamp);
dataview.bins.forEach(function (bin, index) {
var binTimestampExpected = moment.utc(initialTimestamp)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.add(index * 100, 'year')
.format();
var binsTimestampInMilliseconds = bin.timestamp * 1000;
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.format();
assert.equal(binTimestampFormatted, binTimestampExpected);
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
});
done();
});
});
it('should aggregate histogram using "millennium" aggregation ' + test.desc, function (done) {
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
var OFFSET_UTC_IN_MINUTES = 0 * 60; // UTC
var params = {
offset: OFFSET_UTC_IN_SECONDS,
aggregation: 'millennium'
};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_millennium', params, function (err, dataview) {
assert.ok(!err, err);
assert.equal(dataview.type, 'histogram');
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
assert.equal(dataview.bins.length, 12);
var initialTimestamp = '0001-01-01T00:00:00Z';
var binsStartInMilliseconds = dataview.bins_start * 1000;
var binsStartFormatted = moment.utc(binsStartInMilliseconds)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.format();
assert.equal(binsStartFormatted, initialTimestamp);
dataview.bins.forEach(function (bin, index) {
var binTimestampExpected = moment.utc(initialTimestamp)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.add(index * 1000, 'year')
.format();
var binsTimestampInMilliseconds = bin.timestamp * 1000;
var binTimestampFormatted = moment.utc(binsTimestampInMilliseconds)
.utcOffset(OFFSET_UTC_IN_MINUTES)
.format();
assert.equal(binTimestampFormatted, binTimestampExpected);
assert.ok(bin.timestamp <= bin.min, 'bin timestamp < bin min: ' + JSON.stringify(bin));
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
});
done();
});
});
it('bins_count should be equal to bins length filtered by start and end ' + test.desc, function (done) {
var OFFSET_UTC_IN_SECONDS = 0 * 3600; // UTC
var params = {
@@ -533,19 +931,129 @@ describe('histogram-dataview for date column type', function() {
});
});
it('should find the best aggregation (automatic mode) to build the histogram', function (done) {
it('should find the best aggregation (automatic mode) to build the histogram: second', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic', params, function (err, dataview) {
this.testClient.getDataview('datetime_histogram_automatic_second', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'week');
assert.equal(dataview.bins.length, 61);
assert.equal(dataview.bins_count, 61);
assert.equal(dataview.aggregation, 'second');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: minute', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_minute', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'minute');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: hour', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_hour', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'hour');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: day', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_day', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'day');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: week', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_week', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'week');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: month', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_month', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'month');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: quarter', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_quarter', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'quarter');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: year', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_year', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'year');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: decade', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_decade', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'decade');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: century', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_century', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'century');
done();
});
});
it('should find the best aggregation (automatic mode) to build the histogram: millennium', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('datetime_histogram_automatic_millennium', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'millennium');
done();
});
});
it('should work with dates', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
@@ -560,19 +1068,6 @@ describe('histogram-dataview for date column type', function() {
});
it('should find the best aggregation (automatic mode) to build the histogram with dates', function (done) {
var params = {};
this.testClient = new TestClient(mapConfig, 1234);
this.testClient.getDataview('date_histogram_automatic', params, function (err, dataview) {
assert.ifError(err);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.aggregation, 'week');
assert.equal(dataview.bins.length, 61);
assert.equal(dataview.bins_count, 61);
done();
});
});
it('should not apply offset for a histogram aggregated by minutes', function (done) {
var self = this;
var params = {
@@ -858,13 +1353,15 @@ describe('histogram-dates: aggregation input value', function() {
assert.deepEqual(dataviewError, {
errors: [
'Invalid aggregation value. Valid ones: auto, minute, hour, day, week, month, quarter, year'
'Invalid aggregation value. Valid ones: auto, second, minute, ' +
'hour, day, week, month, quarter, year, decade, century, millennium'
],
errors_with_context: [{
type: 'unknown',
message: [
'Invalid aggregation value. ',
'Valid ones: auto, minute, hour, day, week, month, quarter, year'
'Valid ones: auto, second, minute, hour, day, week, month, ' +
'quarter, year, decade, century, millennium'
].join('')
}]
});
@@ -887,13 +1384,15 @@ describe('histogram-dates: aggregation input value', function() {
assert.deepEqual(dataviewError, {
errors: [
'Invalid aggregation value. Valid ones: auto, minute, hour, day, week, month, quarter, year'
'Invalid aggregation value. Valid ones: auto, second, minute, ' +
'hour, day, week, month, quarter, year, decade, century, millennium'
],
errors_with_context: [{
type: 'unknown',
message: [
'Invalid aggregation value. ',
'Valid ones: auto, minute, hour, day, week, month, quarter, year'
'Valid ones: auto, second, minute, hour, day, week, month, ' +
'quarter, year, decade, century, millennium'
].join('')
}]
});
@@ -966,7 +1465,7 @@ describe('histogram-dates: timestamp starts at epoch', function() {
const { aggregation, timestamp_start } = dataview;
assert.equal(timestamp_start, 0);
assert.equal(aggregation, 'month');
assert.equal(aggregation, 'quarter');
done();
});

View File

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

View File

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

View File

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

View File

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

View File

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

204
test/acceptance/tilejson.js Normal file
View File

@@ -0,0 +1,204 @@
require('../support/test_helper');
const assert = require('../support/assert');
const TestClient = require('../support/test-client');
describe('tilejson', function() {
function tilejsonValidation(tilejson, shouldHaveGrid = false) {
assert.equal(tilejson.tilejson, '2.2.0');
assert.ok(Array.isArray(tilejson.tiles));
assert.ok(tilejson.tiles.length > 0);
if (shouldHaveGrid) {
assert.ok(Array.isArray(tilejson.grids));
assert.ok(tilejson.grids.length > 0);
}
}
const sql = 'SELECT * FROM populated_places_simple_reduced';
const cartocss = TestClient.CARTOCSS.POINTS;
const cartocss_version = '3.0.12';
const RASTER_LAYER = {
options: {
sql, cartocss, cartocss_version
}
};
const RASTER_INTERACTIVITY_LAYER = {
options: {
sql, cartocss, cartocss_version,
interactivity: ['cartodb_id']
}
};
const VECTOR_LAYER = {
options: {
sql
}
};
const PLAIN_LAYER = {
type: 'plain',
options: {
color: '#000000'
}
};
function mapConfig(layers) {
return {
version: '1.7.0',
layers: Array.isArray(layers) ? layers : [layers]
};
}
describe('per layer', function() {
it('should expose raster + vector tilejson for raster layers', function(done) {
var testClient = new TestClient(mapConfig(RASTER_LAYER));
testClient.getLayergroup(function(err, layergroupResult) {
assert.ok(!err, err);
const metadata = layergroupResult.metadata;
assert.ok(metadata);
assert.equal(metadata.layers.length, 1);
const layer = metadata.layers[0];
assert.deepEqual(Object.keys(layer.tilejson), ['vector', 'raster']);
Object.keys(layer.tilejson).forEach(k => {
tilejsonValidation(layer.tilejson[k]);
});
testClient.drain(done);
});
});
it('should expose just the vector tilejson vector only layers', function(done) {
var testClient = new TestClient(mapConfig(VECTOR_LAYER));
testClient.getLayergroup(function(err, layergroupResult) {
assert.ok(!err, err);
const metadata = layergroupResult.metadata;
assert.ok(metadata);
assert.equal(metadata.layers.length, 1);
const layer = metadata.layers[0];
assert.deepEqual(Object.keys(layer.tilejson), ['vector']);
Object.keys(layer.tilejson).forEach(k => {
tilejsonValidation(layer.tilejson[k]);
});
testClient.drain(done);
});
});
it('should expose just the raster tilejson plain layers', function(done) {
var testClient = new TestClient(mapConfig(PLAIN_LAYER));
testClient.getLayergroup(function(err, layergroupResult) {
assert.ok(!err, err);
const metadata = layergroupResult.metadata;
assert.ok(metadata);
assert.equal(metadata.layers.length, 1);
const layer = metadata.layers[0];
assert.deepEqual(Object.keys(layer.tilejson), ['raster']);
Object.keys(layer.tilejson).forEach(k => {
tilejsonValidation(layer.tilejson[k]);
});
testClient.drain(done);
});
});
it('should expose grids for the raster layer with interactivity', function(done) {
var testClient = new TestClient(mapConfig(RASTER_INTERACTIVITY_LAYER));
testClient.getLayergroup(function(err, layergroupResult) {
assert.ok(!err, err);
const metadata = layergroupResult.metadata;
assert.ok(metadata);
assert.equal(metadata.layers.length, 1);
const layer = metadata.layers[0];
assert.deepEqual(Object.keys(layer.tilejson), ['vector', 'raster']);
tilejsonValidation(layer.tilejson.vector);
tilejsonValidation(layer.tilejson.raster, true);
testClient.drain(done);
});
});
it('should work with several layers', function(done) {
var testClient = new TestClient(mapConfig([RASTER_LAYER, RASTER_INTERACTIVITY_LAYER]));
testClient.getLayergroup(function(err, layergroupResult) {
assert.ok(!err, err);
const metadata = layergroupResult.metadata;
assert.ok(metadata);
assert.equal(metadata.layers.length, 2);
assert.deepEqual(Object.keys(metadata.layers[0].tilejson), ['vector', 'raster']);
tilejsonValidation(metadata.layers[0].tilejson.vector);
tilejsonValidation(metadata.layers[0].tilejson.raster);
assert.deepEqual(Object.keys(metadata.layers[1].tilejson), ['vector', 'raster']);
tilejsonValidation(metadata.layers[1].tilejson.vector);
tilejsonValidation(metadata.layers[1].tilejson.raster, true);
testClient.drain(done);
});
});
});
describe('root tilejson', function() {
it('should expose just the `vector` tilejson when for vector only mapnik layers', function(done) {
var testClient = new TestClient(mapConfig(VECTOR_LAYER));
testClient.getLayergroup(function(err, layergroupResult) {
assert.ok(!err, err);
const metadata = layergroupResult.metadata;
assert.ok(metadata);
const tilejson = metadata.tilejson;
assert.deepEqual(Object.keys(tilejson), ['vector']);
Object.keys(tilejson).forEach(k => {
tilejsonValidation(tilejson[k]);
});
testClient.drain(done);
});
});
it('should expose just the `vector` and `raster` tilejson for mapnik layers', function(done) {
var testClient = new TestClient(mapConfig(RASTER_LAYER));
testClient.getLayergroup(function(err, layergroupResult) {
assert.ok(!err, err);
const metadata = layergroupResult.metadata;
assert.ok(metadata);
const tilejson = metadata.tilejson;
assert.deepEqual(Object.keys(tilejson), ['vector', 'raster']);
Object.keys(tilejson).forEach(k => {
tilejsonValidation(tilejson[k]);
});
testClient.drain(done);
});
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,9 +3,10 @@ require('../../../support/test_helper');
var assert = require('../../../support/assert');
var ResourceLocator = require('../../../../lib/cartodb/models/resource-locator');
describe('ResourceLocator.getUrls', function() {
describe('ResourceLocator', function() {
var USERNAME = 'username';
var RESOURCE = 'wadus';
var TILE_RESOURCE = 'wadus/{z}/{x}/{y}.png';
var HTTP_SUBDOMAINS = ['1', '2', '3', '4'];
var HTTPS_SUBDOMAINS = ['a', 'b', 'c', 'd'];
@@ -15,118 +16,221 @@ describe('ResourceLocator.getUrls', function() {
assert.ok(urls);
});
var BASIC_ENVIRONMENT = {
serverMetadata: {
cdn_url: {
http: 'cdn.carto.com',
https: 'cdn.ssl.carto.com'
}
}
};
it('should return default urls when basic http and https domains are provided', function() {
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
assert.ok(urls);
describe('basic', function() {
assert.equal(urls.http, ['http://cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
assert.equal(urls.https, ['https://cdn.ssl.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
var BASIC_ENVIRONMENT = {
serverMetadata: {
cdn_url: {
http: 'cdn.carto.com',
https: 'cdn.ssl.carto.com'
}
}
};
describe('getUrls', function() {
it('should return default urls when basic http and https domains are provided', function() {
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
assert.ok(urls);
assert.equal(urls.http, ['http://cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
assert.equal(urls.https, ['https://cdn.ssl.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
});
});
describe('getTileUrls', function() {
it('should return default urls when basic http and https domains are provided', function() {
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
assert.ok(urls);
assert.deepEqual(
urls.http,
[`http://cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`]
);
assert.deepEqual(
urls.https,
[`https://cdn.ssl.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`]
);
});
});
});
var RESOURCE_TEMPLATES_ENVIRONMENT = {
serverMetadata: {
cdn_url: {
http: 'cdn.carto.com',
https: 'cdn.ssl.carto.com'
}
},
resources_url_templates: {
http: 'http://{{=it.user}}.localhost.lan/api/v1/map',
https: 'https://{{=it.user}}.ssl.localhost.lan/api/v1/map'
}
};
it('resources_url_templates should take precedence over http and https domains', function() {
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
assert.ok(urls);
describe('resource templates', function() {
assert.equal(urls.http, ['http://' + USERNAME + '.localhost.lan', 'api/v1/map', RESOURCE].join('/'));
assert.equal(urls.https, ['https://' + USERNAME + '.ssl.localhost.lan', 'api/v1/map', RESOURCE].join('/'));
var RESOURCE_TEMPLATES_ENVIRONMENT = {
serverMetadata: {
cdn_url: {
http: 'cdn.carto.com',
https: 'cdn.ssl.carto.com'
}
},
resources_url_templates: {
http: 'http://{{=it.user}}.localhost.lan/api/v1/map',
https: 'https://{{=it.user}}.ssl.localhost.lan/api/v1/map'
}
};
describe('getUrls', function() {
it('resources_url_templates should take precedence over http and https domains', function() {
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
assert.ok(urls);
assert.equal(
urls.http,
['http://' + USERNAME + '.localhost.lan', 'api/v1/map', RESOURCE].join('/')
);
assert.equal(
urls.https,
['https://' + USERNAME + '.ssl.localhost.lan', 'api/v1/map', RESOURCE].join('/')
);
});
});
describe('getTileUrls', function() {
it('resources_url_templates should take precedence over http and https domains', function() {
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
assert.ok(urls);
assert.deepEqual(
urls.http,
[`http://${USERNAME}.localhost.lan/api/v1/map/${TILE_RESOURCE}`]
);
assert.deepEqual(
urls.https,
[`https://${USERNAME}.ssl.localhost.lan/api/v1/map/${TILE_RESOURCE}`]
);
});
});
});
var CDN_TEMPLATES_ENVIRONMENT = {
serverMetadata: {
cdn_url: {
http: 'cdn.carto.com',
https: 'cdn.ssl.carto.com',
templates: {
http: {
url: "http://{s}.cdn.carto.com",
subdomains: HTTP_SUBDOMAINS
},
https: {
url: "https://cdn_{s}.ssl.cdn.carto.com",
subdomains: HTTPS_SUBDOMAINS
describe('cdn templates', function() {
var CDN_TEMPLATES_ENVIRONMENT = {
serverMetadata: {
cdn_url: {
http: 'cdn.carto.com',
https: 'cdn.ssl.carto.com',
templates: {
http: {
url: "http://{s}.cdn.carto.com",
subdomains: HTTP_SUBDOMAINS
},
https: {
url: "https://cdn_{s}.ssl.cdn.carto.com",
subdomains: HTTPS_SUBDOMAINS
}
}
}
}
}
};
it('cdn_url templates should take precedence over http and https domains', function() {
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
assert.ok(urls);
};
describe('getUrls', function() {
it('cdn_url templates should take precedence over http and https domains', function() {
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
assert.ok(urls);
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
assert.equal(
urls.http,
['http://' + httpSubdomain + '.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
);
assert.equal(
urls.https,
['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
);
});
});
describe('getTileUrls', function() {
it('cdn_url templates should take precedence over http and https domains', function() {
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
assert.ok(urls);
assert.deepEqual(
urls.http,
HTTP_SUBDOMAINS
.map(s => `http://${s}.cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
);
assert.deepEqual(
urls.https,
HTTPS_SUBDOMAINS
.map(s => `https://cdn_${s}.ssl.cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
);
});
});
assert.equal(
urls.http,
['http://' + httpSubdomain + '.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
);
assert.equal(
urls.https,
['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
);
});
var CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT = {
serverMetadata: {
cdn_url: {
http: 'cdn.carto.com',
https: 'cdn.ssl.carto.com',
templates: {
http: {
url: "http://{s}.cdn.carto.com",
subdomains: HTTP_SUBDOMAINS
},
https: {
url: "https://cdn_{s}.ssl.cdn.carto.com",
subdomains: HTTPS_SUBDOMAINS
describe('cdn and resource templates', function() {
var CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT = {
serverMetadata: {
cdn_url: {
http: 'cdn.carto.com',
https: 'cdn.ssl.carto.com',
templates: {
http: {
url: "http://{s}.cdn.carto.com",
subdomains: HTTP_SUBDOMAINS
},
https: {
url: "https://cdn_{s}.ssl.cdn.carto.com",
subdomains: HTTPS_SUBDOMAINS
}
}
}
},
resources_url_templates: {
http: 'http://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map',
https: 'https://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map'
}
},
resources_url_templates: {
http: 'http://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map',
https: 'https://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map'
}
};
it('should mix cdn_url templates and resources_url_templates', function() {
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
assert.ok(urls);
};
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
describe('getUrls', function() {
it('should mix cdn_url templates and resources_url_templates', function() {
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
assert.ok(urls);
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
assert.equal(
urls.http,
['http://' + httpSubdomain + '.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/')
);
assert.equal(
urls.https,
`https://cdn_${httpsSubdomain}.ssl.cdn.carto.com/u/${USERNAME}/api/v1/map/${RESOURCE}`
);
});
});
describe('getTileUrls', function() {
it('should mix cdn_url templates and resources_url_templates', function() {
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
assert.ok(urls);
assert.deepEqual(
urls.http,
HTTP_SUBDOMAINS
.map(s => `http://${s}.cdn.carto.com/u/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
);
assert.deepEqual(
urls.https,
HTTPS_SUBDOMAINS
.map(s => `https://cdn_${s}.ssl.cdn.carto.com/u/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
);
});
});
assert.equal(
urls.http,
['http://' + httpSubdomain + '.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/')
);
assert.equal(
urls.https,
['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/')
);
});
});

View File

@@ -11,7 +11,7 @@ describe('tile stats', function() {
});
afterEach(function() {
global.statsClient = this.statsClient;
global.statsClient = this.statsClient;
});
it('finalizeGetTileOrGrid does not call statsClient when format is not supported', function() {
@@ -84,7 +84,7 @@ describe('tile stats', function() {
});
function mockStatsClientGetInstance(instance) {
global.statsClient = instance;
global.statsClient = Object.assign(global.statsClient, instance);
}
});

119
yarn.lock
View File

@@ -10,7 +10,11 @@
mapnik "~3.5.0"
sphericalmercator "1.0.x"
abbrev@1, abbrev@1.0.x:
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
abbrev@1.0.x:
version "1.0.9"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
@@ -29,13 +33,13 @@ ajv@^4.9.1:
json-stable-stringify "^1.0.1"
ajv@^5.1.0:
version "5.2.3"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.3.tgz#c06f598778c44c6b161abafe3466b81ad1814ed2"
version "5.5.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
dependencies:
co "^4.6.0"
fast-deep-equal "^1.0.0"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.3.0"
json-stable-stringify "^1.0.1"
align-text@^0.1.1, align-text@^0.1.3:
version "0.1.4"
@@ -211,9 +215,9 @@ camelcase@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
camshaft@0.59.4:
version "0.59.4"
resolved "https://registry.yarnpkg.com/camshaft/-/camshaft-0.59.4.tgz#7d4d0b8585fa180b5e750206aa84940870ebabfc"
camshaft@0.60.0:
version "0.60.0"
resolved "https://registry.yarnpkg.com/camshaft/-/camshaft-0.60.0.tgz#0433b5a576e08cabbc9bae1e1b22305274b8b7b6"
dependencies:
async "^1.5.2"
bunyan "1.8.1"
@@ -520,7 +524,11 @@ domutils@1.5:
dom-serializer "0"
domelementtype "1"
dot@^1.0.3, dot@~1.0.2:
dot@^1.0.3:
version "1.1.2"
resolved "https://registry.yarnpkg.com/dot/-/dot-1.1.2.tgz#c7377019fc4e550798928b2b9afeb66abfa1f2f9"
dot@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/dot/-/dot-1.0.3.tgz#f8750bfb6b03c7664eb0e6cb1eb4c66419af9427"
@@ -644,14 +652,22 @@ extend@~3.0.0, extend@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
extsprintf@1.3.0, extsprintf@^1.2.0:
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
extsprintf@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
fast-deep-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
fast-json-stable-stringify@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
fast-levenshtein@~2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
@@ -776,7 +792,7 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
glob@7.1.1, glob@^7.1.1:
glob@7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
dependencies:
@@ -807,7 +823,7 @@ glob@^6.0.1:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.0.5:
glob@^7.0.5, glob@^7.1.1:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
@@ -826,9 +842,9 @@ graceful-fs@^4.1.2:
version "1.0.1"
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
grainstore@1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/grainstore/-/grainstore-1.7.0.tgz#28d78895c82e6201f7d0ff63af1056f3c0fda0d3"
grainstore@~1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/grainstore/-/grainstore-1.8.0.tgz#9398729df88f3aecb55ffbb415d541dcca4420af"
dependencies:
carto "0.16.3"
debug "~3.1.0"
@@ -845,8 +861,8 @@ growl@1.9.2:
resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f"
handlebars@^4.0.1:
version "4.0.10"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.10.tgz#3d30c718b09a3d96f23ea4cc1f403c4d3ba9ff4f"
version "4.0.11"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc"
dependencies:
async "^1.4.0"
optimist "^0.6.1"
@@ -998,8 +1014,8 @@ is-arrayish@^0.2.1:
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
is-buffer@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
is-builtin-module@^1.0.0:
version "1.0.0"
@@ -1057,8 +1073,8 @@ istanbul@~0.4.3:
wordwrap "^1.0.0"
js-base64@^2.1.9:
version "2.3.2"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf"
version "2.4.0"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.0.tgz#9e566fee624751a1d720c966cd6226d29d4025aa"
js-string-escape@1.0.1:
version "1.0.1"
@@ -1325,7 +1341,7 @@ mime@~1.3.4:
dependencies:
brace-expansion "^1.1.7"
minimist@0.0.8, minimist@~0.0.1:
minimist@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
@@ -1333,6 +1349,10 @@ minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
minimist@~0.0.1:
version "0.0.10"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
minimist@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.2.0.tgz#4dffe525dae2b864c66c2e23c6271d7afdecefce"
@@ -1359,7 +1379,11 @@ mocha@~3.4.1:
mkdirp "0.5.1"
supports-color "3.1.2"
moment@^2.10.6, moment@~2.18.1:
moment@^2.10.6:
version "2.20.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd"
moment@~2.18.1:
version "2.18.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
@@ -1379,11 +1403,7 @@ mv@~2:
ncp "~2.0.0"
rimraf "~2.4.0"
nan@^2.0.8, nan@^2.3.4, nan@~2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
nan@^2.4.0:
nan@^2.0.8, nan@^2.3.4, nan@^2.4.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
@@ -1395,6 +1415,10 @@ nan@~2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
nan@~2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46"
ncp@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
@@ -1588,6 +1612,10 @@ pg-connection-string@0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7"
pg-int8@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
pg-pool@1.*:
version "1.8.0"
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-1.8.0.tgz#f7ec73824c37a03f076f51bfdf70e340147c4f37"
@@ -1596,9 +1624,10 @@ pg-pool@1.*:
object-assign "4.1.0"
pg-types@1.*:
version "1.12.1"
resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-1.12.1.tgz#d64087e3903b58ffaad279e7595c52208a14c3d2"
version "1.13.0"
resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-1.13.0.tgz#75f490b8a8abf75f1386ef5ec4455ecf6b345c63"
dependencies:
pg-int8 "1.0.1"
postgres-array "~1.0.0"
postgres-bytea "~1.0.0"
postgres-date "~1.0.0"
@@ -1920,18 +1949,14 @@ safe-json-stringify@~1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz#81a098f447e4bbc3ff3312a243521bc060ef5911"
"semver@2 || 3 || 4 || 5", semver@~5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
semver@4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7"
semver@^5.1.0, semver@^5.3.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
semver@~4.3.3:
version "4.3.6"
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
@@ -1940,6 +1965,10 @@ semver@~5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.0.3.tgz#77466de589cd5d3c95f138aa78bc569a3cb5d27a"
semver@~5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
send@0.16.1:
version "0.16.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3"
@@ -1998,8 +2027,8 @@ sntp@1.x.x:
hoek "2.x.x"
sntp@2.x.x:
version "2.0.2"
resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.0.2.tgz#5064110f0af85f7cfdb7d6b67a40028ce52b4b2b"
version "2.1.0"
resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8"
dependencies:
hoek "4.x.x"
@@ -2082,7 +2111,11 @@ sshpk@^1.7.0:
jsbn "~0.1.0"
tweetnacl "~0.14.0"
"statuses@>= 1.3.1 < 2", statuses@~1.3.1:
"statuses@>= 1.3.1 < 2":
version "1.4.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
statuses@~1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
@@ -2359,9 +2392,9 @@ window-size@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
windshaft@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/windshaft/-/windshaft-4.1.0.tgz#dc17c8369570c305171d1ab5ca130369bba04d58"
windshaft@4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/windshaft/-/windshaft-4.2.0.tgz#6a0409832a0d3bccfa09a88a8ab8288686b6762d"
dependencies:
abaculus cartodb/abaculus#2.0.3-cdb1
canvas cartodb/node-canvas#1.6.2-cdb2
@@ -2369,7 +2402,7 @@ windshaft@^4.1.0:
cartodb-psql "^0.10.1"
debug "^3.1.0"
dot "~1.0.2"
grainstore "1.7.0"
grainstore "~1.8.0"
mapnik "3.5.14"
queue-async "~1.0.7"
redis-mpool "0.4.1"