Compare commits

...

970 Commits
5.0.2 ... 6.2.0

Author SHA1 Message Date
Daniel García Aubert
a6dd3d8354 Release 6.2.0 2018-07-20 13:30:34 +02:00
Javier Goizueta
028d17f149 Merge pull request #1008 from CartoDB/windshaft-4.8.3
Upgrade Windshaft to 4.8.3
2018-07-19 16:08:19 +02:00
Javier Goizueta
1cba4a1a9b Update NEWS for windshaft 4.8.3 2018-07-19 13:04:27 +02:00
Javier Goizueta
7243e4a0e2 Upgrade Windshaft to 4.8.3
This fixes problem of very slow instantiantion of large tables with aggregations
2018-07-19 12:30:10 +02:00
Javier Goizueta
fcfa763890 Merge pull request #1002 from CartoDB/aggregation-fixes
Aggregation fixes
2018-07-18 11:54:13 +02:00
Raúl Marín
942b7ef923 Merge pull request #1005 from Algunenano/master_union_all
Tests: Use "UNION ALL" instead of "UNION" to improve performance
2018-07-18 10:49:05 +02:00
Daniel G. Aubert
bc61f79b3a Merge pull request #1006 from CartoDB/upgrade-windshaft-4.8.2
Upgrade windshaft@4.8.2
2018-07-17 18:01:51 +02:00
Daniel García Aubert
d649d7eb1d Update NEWS 2018-07-17 17:49:58 +02:00
Daniel García Aubert
cf20728711 Upgrade winshaft@4.8.2 2018-07-17 17:34:41 +02:00
Raul Marin
cd31f998dd Tests: Use "UNION ALL" instead of "UNION" to improve performance 2018-07-17 12:43:50 +02:00
Javier Goizueta
51c3215137 Change UNION by UNION ALL in tests
It seems UNION (without ALL) can be much slower in recent PG versions
2018-07-17 12:42:33 +02:00
Daniel G. Aubert
3e0cb0ed37 Merge pull request #997 from CartoDB/last-modified-always
Remove parameter `now` for last-modified-header middleware
2018-07-17 11:32:37 +02:00
Eneko Lakasta
7d5469ed1c Merge pull request #1003 from CartoDB/add-create-layergroup-stats
Add create layergroup stats
2018-07-16 17:54:18 +02:00
Eneko Lakasta
d6b081255c Merge pull request #1004 from CartoDB/upgrade-turbo-carto
Upgrade turbo carto
2018-07-16 17:54:05 +02:00
Daniel García Aubert
f9b59d8549 Do not override stats 2018-07-16 17:18:58 +02:00
Eneko Lakasta
1534c26050 update NEWS with new version of turbo carto 2018-07-16 17:07:53 +02:00
Daniel García Aubert
81eb849aff Add stats first 2018-07-16 16:58:53 +02:00
Eneko Lakasta
f72ec17c5f upgrade turbo carto to 0.20.4 2018-07-16 16:57:41 +02:00
Daniel García Aubert
9f9b933607 Add create layergroup stats 2018-07-16 16:52:26 +02:00
Javier Goizueta
e5d4369203 Fix test 2018-07-16 14:59:26 +02:00
Javier Goizueta
e1ec38446d Add aggregation test with buffer overlapping cells partially
Also fix resolution parameter incrrectly placed in some tests
2018-07-16 14:27:39 +02:00
Javier Goizueta
2d102c4810 Compensate for bbox lack of accuracy 2018-07-16 12:44:30 +02:00
Javier Goizueta
e59d0f520a Restore tests 2018-07-16 12:02:37 +02:00
Javier Goizueta
2063ac15ee Fix long lines 2018-07-16 11:58:56 +02:00
Javier Goizueta
7b597e0223 Update NEWS 2018-07-15 22:27:49 +02:00
Javier Goizueta
716f983e71 Filter out partially aggregated clusters
Fixes #889
2018-07-15 22:26:37 +02:00
Javier Goizueta
31feb58e1f Compute aggregation cell size accurately
This fixes #1001
2018-07-15 22:25:16 +02:00
Javier Goizueta
92ef0a60fc Test aggregation accuracy
This tests #1001
2018-07-15 22:17:00 +02:00
Javier Goizueta
e21ab12e4c Test that tiles do not contain partially aggregated clusters
This tests #889
2018-07-15 22:15:57 +02:00
Javier Goizueta
b5a0c6505a Merge pull request #998 from CartoDB/994-aggr-tests
Adapt tests for more accurate PROJ
2018-07-11 17:23:02 +02:00
Javier Goizueta
dfb4e20219 Merge pull request #999 from Algunenano/test_fixes
Multiple test fixes
2018-07-11 17:22:50 +02:00
Javier Goizueta
9cbcd43fda Fix another test 2018-07-11 16:56:37 +02:00
Javier Goizueta
7cadbcc533 Fix tests, this time for good :fingers_crossed: 2018-07-11 16:47:10 +02:00
Javier Goizueta
d3a3a7353a Fix tests
They were not numerical accuracy independent, as intended
2018-07-11 16:34:39 +02:00
Javier Goizueta
b8365e9f6e Make sampling tests more stable
Some sample tests, which cannot use RNG seeding, are nondeterministical.
Increment size of test table used in sampling tests to make less likely that the sample is empty
2018-07-11 15:21:23 +02:00
Javier Goizueta
e17dd4b5fa Make tests independent of coordinate accuracy by not placing points near tile boundaries 2018-07-11 13:19:07 +02:00
Raul Marin
8dcab568bd Tests: Adapt torque tests to work with PROJ 5.1 2018-07-11 12:33:02 +02:00
Raul Marin
a0020804c9 Test (regressions.js): Remove all redis keys so tests can be run individually 2018-07-11 12:21:06 +02:00
Raul Marin
ea2126a301 Tests: Fix DB flags being ignored 2018-07-11 12:20:58 +02:00
Javier Goizueta
8442a9a711 Shorten long lines 2018-07-11 11:29:22 +02:00
Javier Goizueta
c07b3de43d Adapt tests for more accurate PROJ
Fixes #994
With exact point 0,0 transformations, the point is between tiles and can appear in several
2018-07-11 10:33:58 +02:00
Daniel García Aubert
4f81f402f5 Remove parameter now for last-modified-header 2018-07-10 13:22:38 +02:00
Eneko Lakasta
10d21a4a0f Merge pull request #996 from CartoDB/upgrade-yarn-lock-turbo-carto
upgrade turbo-carto 0.20.3 in yarn.lock
2018-07-09 17:41:07 +02:00
Eneko Lakasta
42bcae0e4a upgrade turbo-carto 0.20.3 2018-07-09 16:58:50 +02:00
Eneko Lakasta
04c5b2aa36 Merge pull request #995 from CartoDB/14075-2buckets-bubble-legends
Use new version of turbo-carto 0.20.3
2018-07-09 16:23:35 +02:00
Eneko Lakasta
b30f7264f1 upgrade package.json turbo-carto to 0.20.3 2018-07-09 16:01:51 +02:00
Eneko Lakasta
2800a50f19 upgrade package.json turbo-carto to 0.2.30 2018-07-09 15:53:16 +02:00
Simon Martín
287ecf5ce2 Updating NEWS 2018-07-06 11:44:37 +02:00
Simon Martín
53b7969753 Merge pull request #989 from CartoDB/fix-rate-limit-endpoint-several-limits
Upgrading cartodb-redis
2018-07-06 11:41:14 +02:00
Simon Martín
9530f17194 Merge branch 'master' into fix-rate-limit-endpoint-several-limits 2018-07-06 11:06:23 +02:00
Rafa de la Torre
0829d5bc7d Merge pull request #991 from CartoDB/fix-numeric-histogram-bounds
Fix numeric histogram bounds
2018-07-05 18:12:19 +02:00
Rafa de la Torre
b59712ee10 Update NEWS.md with reference to fix 2018-07-05 17:24:29 +02:00
Rafa de la Torre
e247e45f96 Qualify columns and improve if/else style
As suggested by Algunenano: qualify column names with the
table/subquery/cte to avoid name clashing, and polish the code style a
little.
2018-07-05 17:21:38 +02:00
Simon Martín
df71d93dd9 upgrading cartodb-redis to 2.0.1 2018-07-05 15:16:45 +02:00
Simon Martín
928f10b420 updating version cartodb-redis (still using github) 2018-07-05 12:39:28 +02:00
Rafa de la Torre
a1807fd0c3 A better solution to the start-end problem 2018-07-05 12:39:26 +02:00
Rafa de la Torre
d937ce4982 Fix the min >= start and max <= end case (WIP)
This fixes the 'should get bins with min >= start and max <= end' test
case but probably breaks a number of other cases (those with no start
and/or no end).
2018-07-05 11:56:26 +02:00
Rafa de la Torre
6411556a97 Test for histogram bins beyond limits 2018-07-04 18:33:02 +02:00
Rafa de la Torre
2c334570c3 run_tests.sh is a *bash* script 2018-07-04 17:39:13 +02:00
Eneko Lakasta
c1671abaa4 Merge pull request #988 from CartoDB/984-sql-timeout-error-message
984 limits error messages
2018-07-03 16:44:58 +02:00
Eneko Lakasta
d82cc98b75 update package.json to use turbo-carto git branch 2018-07-03 16:20:42 +02:00
Eneko Lakasta
5d7af03228 remove .only from test 2018-07-03 16:20:09 +02:00
Eneko Lakasta
529368e858 Merge branch '984-sql-timeout-error-message' into 14075-2buckets-bubble-legends 2018-07-03 15:58:10 +02:00
Eneko Lakasta
fdc061b7ee Please jshint 2018-07-03 15:02:58 +02:00
Eneko Lakasta
923b23871f Please jshint 2018-07-03 14:48:54 +02:00
Eneko Lakasta
409a103990 update rate limit error message 2018-07-03 14:36:29 +02:00
Eneko Lakasta
6a6815d893 update render limits error message 2018-07-03 14:32:10 +02:00
Eneko Lakasta
37737e7941 Merge branch '984-sql-timeout-error-message' into 14075-2buckets-bubble-legends 2018-07-03 12:45:41 +02:00
Simon Martín
af0bc09d52 updating to temporal branch of cartodb-redis 2018-07-03 11:57:02 +02:00
Eneko Lakasta
a8d791e2d3 update tests with new error message 2018-07-02 13:03:01 +02:00
Eneko Lakasta
c874a734fd fix db limit error message set order 2018-07-02 13:02:36 +02:00
Eneko Lakasta
23e5cefdf1 Update NEWS 2018-07-02 11:29:47 +02:00
Eneko Lakasta
9226b67ab7 add tests 2018-06-29 15:03:45 +02:00
Eneko Lakasta
ee25585f06 add new error message 2018-06-29 15:01:12 +02:00
Simon Martín
028c3f9aec Merge pull request #987 from CartoDB/ignore-null-values-formula-dataview
ensuring formula dataview filters infinities, nans and nulls
2018-06-28 13:12:42 +02:00
Simon Martín
cc048c41d8 Merge branch 'master' into ignore-null-values-formula-dataview 2018-06-28 13:03:40 +02:00
Simon Martín
ef39a76371 Merge pull request #986 from CartoDB/upgrading-dependencies
upgrading dependencies due to vulnerabilities
2018-06-28 12:43:11 +02:00
Simon Martín
1329f1f535 fix aggregation-dataview test filter 2018-06-28 12:41:52 +02:00
Simon Martín
66f38e8ecd keep the original test name 2018-06-27 18:38:20 +02:00
Simon Martín
e45b41f55a ensuring formula dataview filters infinities, nans and nulls 2018-06-27 18:04:48 +02:00
Simon Martín
995ff52cf3 upgrading dependencies 2018-06-26 17:56:16 +02:00
Simon Martín
960eaa5724 Merge pull request #983 from CartoDB/tilejson-url-apikey
tiles base urls with api keys
2018-06-26 10:37:04 +02:00
Simon Martín
37f6ac0c87 Merge branch 'master' into tilejson-url-apikey 2018-06-26 10:23:09 +02:00
Simon Martín
fcc36ddc83 News 2018-06-25 16:31:13 +02:00
Simon Martín
79fdd07d8f adding api key to tiles base urls with querystring 2018-06-25 16:23:23 +02:00
Simon Martín
32ce033c06 improve tests titles 2018-06-25 15:09:18 +02:00
Simon Martín
abb194ca9c test tiles base urls with default_public api key 2018-06-25 15:06:27 +02:00
Javier Goizueta
3161cb0322 Merge pull request #981 from CartoDB/980-dates-as-numbers-tokens
Fix dates_as_numbers for queries with tokens
2018-06-25 14:46:18 +02:00
Daniel G. Aubert
f36cadb809 Merge pull request #985 from CartoDB/add-release-note
Add a release note to advice that a rake task should be performed
2018-06-25 12:55:10 +02:00
Daniel García Aubert
863e42691a Add a release note to advice that a rake task should be performed 2018-06-25 12:51:30 +02:00
Simon Martín
762dce7853 adding api key to tiles base urls 2018-06-22 17:31:45 +02:00
Simon Martín
f0d190d157 tests tiles base urls with api key 2018-06-22 17:31:05 +02:00
Eneko Lakasta
6103d3b8bd Merge pull request #982 from CartoDB/update-camshaft-0.61.11
Upgrades Camshaft to 0.61.11
2018-06-19 15:14:51 +02:00
Eneko Lakasta
7a89f303db Upgrades Camshaft to 0.61.11 2018-06-19 13:58:23 +02:00
Javier Goizueta
b05d9a0a75 Remove only from tests 2018-06-18 13:33:08 +02:00
Javier Goizueta
e89503e1fa Subsitute tokens to get columns for dates_as_numbers
Fixes #980
2018-06-18 13:16:49 +02:00
Javier Goizueta
e0cd1aba29 Refactor: move common token substitutions to query-utils 2018-06-18 13:15:45 +02:00
Javier Goizueta
a5f985257c Tests for dates_as_numbers with tokens 2018-06-18 13:14:17 +02:00
Javier Goizueta
90efe14bfb Merge pull request #971 from CartoDB/cartovl-130
Send dates as unix epoch instead strings in .mvt files
2018-06-13 13:07:35 +02:00
Javier Goizueta
fc9ae9ca20 Update NEWS 2018-06-13 12:54:57 +02:00
IagoLast
649297df83 Use early return 2018-06-13 09:42:53 +02:00
IagoLast
ae7e7578db Use early return 2018-06-13 09:38:24 +02:00
Javier Goizueta
0cf6605b8d Use more precise function name 2018-06-12 12:59:10 +02:00
Javier Goizueta
4a52620d83 Various fixes
This avoids errors when trying to wrap dates or detect wrapped dates in non-mapnik layers
2018-06-12 12:05:31 +02:00
Javier Goizueta
251570b638 Fix tests 2018-06-12 12:04:13 +02:00
Daniel G. Aubert
7660a694cb Merge pull request #970 from CartoDB/mapconfig-provirers-refactor
Mapconfig providers refactor
2018-06-12 10:23:31 +02:00
Raúl Marín
7e7b268a66 Merge pull request #976 from Algunenano/auto_postgis
Test: Make POSTGIS_VERSION detection automatic
2018-06-12 10:09:40 +02:00
Javier Goizueta
660c1777e3 Fix per-layer date wrapping 2018-06-11 19:31:42 +02:00
Javier Goizueta
5407df03fa Implement missing tests 2018-06-11 19:31:15 +02:00
Javier Goizueta
84c34361a0 Fix loop limits, add per layer options in test mapconfig Factory 2018-06-11 19:30:48 +02:00
Raul Marin
50cf5e5c7a Avoid infinite loop 2018-06-11 16:01:29 +02:00
Daniel García Aubert
aba737c61b Merge branch 'master' into mapconfig-provirers-refactor 2018-06-11 15:50:52 +02:00
Raul Marin
9bbbe9e7c1 Make POSTGIS_VERSION detection automatic 2018-06-11 14:47:39 +02:00
Eneko Lakasta
163f494b8a Merge pull request #949 from CartoDB/remove-auth-fallback
Remove auth fallback
2018-06-11 12:35:07 +02:00
Eneko Lakasta
7f7eb78d8c Merge branch 'master' into remove-auth-fallback 2018-06-11 12:02:58 +02:00
Daniel G. Aubert
462d25bebc Merge pull request #974 from CartoDB/fix-statsd-prefix
Use external module to get full qualified domain name properly
2018-06-08 15:30:42 +02:00
Daniel García Aubert
2bf8caf7fc Update fqdn-sync to version 0.2.2 2018-06-08 14:41:31 +02:00
Daniel García Aubert
c6a74b66ce Use external module to get full qualified domain name properly 2018-06-08 13:46:42 +02:00
Raúl Marín
d22619c1f9 Merge pull request #973 from CartoDB/export_fix
Accept `layer` option in static map url request
2018-06-07 16:59:03 +02:00
Daniel G. Aubert
d8b59a18ed Merge pull request #972 from CartoDB/unify-configuration
Unify configuration
2018-06-07 16:36:42 +02:00
Daniel García Aubert
fae6bdff05 Remove duplicated property 2018-06-07 16:33:52 +02:00
Eneko Lakasta
37182f5138 use new cartodb-redis version 2018-06-07 16:10:36 +02:00
Raul Marin
70e8ab8349 Update NEWs 2018-06-07 12:14:31 +02:00
Raul Marin
102244f467 Regression: Accept layer option in the static map urls 2018-06-07 12:14:31 +02:00
Raul Marin
db946b93ec Test: Use cartodb-psql to reset connections instead of calling node-postgres directly
- Avoids an issue with newer npm not finding node-postgres during the tests
as it was required directly but not declared in package.json.
- Avoids an issue with the torque timeouts tests
2018-06-06 13:18:28 +02:00
Javier Goizueta
5b637577c8 Fix conflict resolution gone wrong 2018-06-05 16:30:27 +02:00
Javier Goizueta
0258051f06 Merge branch 'cartovl-130' of github.com:CartoDB/Windshaft-cartodb into cartovl-130
# Conflicts:
#	test/acceptance/date-wrapping.spec.js
2018-06-05 15:43:13 +02:00
Javier Goizueta
2ab22882d6 Slight code trimming 2018-06-05 15:39:15 +02:00
Javier Goizueta
55f6241769 Add date wrapping metadata 2018-06-05 15:39:01 +02:00
Daniel García Aubert
9ec3325cd0 Remove duplicated comments 2018-06-05 15:10:52 +02:00
Daniel García Aubert
e0ab901600 Disable cache-features for test environment 2018-06-05 12:03:26 +02:00
Daniel García Aubert
fcaab30fe7 Enable cahce-features and disable twkb for test environment 2018-06-05 11:51:55 +02:00
IagoLast
e8ecd9b2e0 Fix new test 2018-06-05 11:44:30 +02:00
Daniel García Aubert
22da5a1ff0 Mapnik renderer: do not cache features 2018-06-05 11:35:42 +02:00
Daniel García Aubert
6d0c38371a Do not log date twice 2018-06-05 11:28:42 +02:00
Javier Goizueta
b10cf4bebb New test for casted dates metadata 2018-06-05 11:25:00 +02:00
Daniel García Aubert
6e9871ba4e Revert change: comment analysis logger filename 2018-06-05 11:24:34 +02:00
Daniel García Aubert
13b23d9ec9 Revert change, comment log file configuration again 2018-06-05 11:14:13 +02:00
IagoLast
1491f29f96 Fix tests 2018-06-05 10:10:56 +02:00
IagoLast
9f4b6d5f43 Fix linter 2018-06-05 09:57:48 +02:00
IagoLast
a883514c8a Remove control flag 2018-06-05 09:35:01 +02:00
IagoLast
9ee6d7fc91 Implement multiple layer date wrapping 2018-06-05 09:34:44 +02:00
IagoLast
7df1a19da4 Add test for multiple-layer date wrap 2018-06-05 08:58:44 +02:00
IagoLast
2ee6c8487d PR style fixes 2018-06-05 08:44:20 +02:00
Daniel García Aubert
de3dbb8c1e Missing attribute twkb_encoding set to true 2018-06-04 15:22:32 +02:00
Daniel García Aubert
f851423b68 Unify app configuration among the different enviroments 2018-06-01 16:52:23 +02:00
IagoLast
8ec2b35557 Fix tests 2018-06-01 12:25:36 +02:00
Ubuntu
ae4b233458 Pass tests 2018-06-01 10:18:07 +00:00
Ubuntu
6872d57581 Add tests 2018-06-01 10:08:34 +00:00
elenatorro
79962a7566 Refactor long line 2018-05-31 19:07:57 +02:00
elenatorro
d4c6282455 Refactor date wrapper 2018-05-31 18:53:01 +02:00
elenatorro
db3370cd21 Use wrapDates function from vector adapter 2018-05-31 18:46:23 +02:00
elenatorro
4213e3163a Move queryPromise function 2018-05-31 18:37:43 +02:00
IagoLast
3850bbb68e Send dates as unix epoch instead strings in .mvt files
This commit creates a new ConfigAdapter used in vector maps instantiations.
This adapter generates a new sql query for ONE SINGLE LAYER (carto-vl currently only supports one layer per mvt)
where the date columns are wrapped into a epoch using the `date_part` function.

Due this mvt files are smaller since we use numbers instead strings to represent dates, this is also faster in carto-gl
where we interpolate linearly between 0 and 1 to create animations.

Notice we should add a parameter to make this transformation optional.
We also should take into account the epoch precission.
2018-05-31 17:49:40 +02:00
Daniel García Aubert
ea95050d43 Extract base class in order to share createAffectedTables and getAffectedTables methods 2018-05-31 14:57:34 +02:00
Daniel García Aubert
c32dba1ecb Use ES6 class syntax 2018-05-30 20:28:15 +02:00
Daniel García Aubert
94a5020faf Do not use underscore 2018-05-30 20:10:59 +02:00
Daniel García Aubert
c0e6bf1299 Use ES6 class syntax 2018-05-30 20:08:35 +02:00
Daniel García Aubert
9fe8958e8c Use ES6 class syntax 2018-05-30 20:02:38 +02:00
Daniel García Aubert
7c51895b0f Use const and let instead of var 2018-05-30 19:50:07 +02:00
Daniel García Aubert
16bca85438 Do not use underscore 2018-05-30 19:43:23 +02:00
Daniel García Aubert
9d415d0dbe Extract method to get and build context 2018-05-30 19:30:34 +02:00
Daniel García Aubert
1d0210a372 Reorder code 2018-05-30 19:07:28 +02:00
Daniel García Aubert
2fb6c08702 Remove unnecessary variables 2018-05-30 18:37:11 +02:00
Daniel García Aubert
f7c712f6eb Avoid functions with side effects 2018-05-30 18:31:59 +02:00
Daniel García Aubert
eabd25ee6a Use the returned template instead of the cached one 2018-05-30 18:18:42 +02:00
Daniel García Aubert
5fc49ab3c2 Remove duplicated assignment 2018-05-30 18:16:42 +02:00
Daniel García Aubert
1b6a722c0c Remove step 2018-05-30 18:15:51 +02:00
Eneko Lakasta
6414cd52c0 Merge branch 'master' into remove-auth-fallback 2018-05-30 17:53:24 +02:00
Daniel G. Aubert
f732ed970b Merge pull request #969 from CartoDB/remove-deprecated-base-url-configuration
Remove deprecated base url configuration
2018-05-30 17:36:52 +02:00
Daniel García Aubert
5cfffcfa83 Use const instead of var 2018-05-30 17:22:12 +02:00
Daniel García Aubert
4e28f7bb4e Do not require assert and step 2018-05-30 17:10:18 +02:00
Daniel García Aubert
532d50ad7a Return 'null' explicitly 2018-05-30 17:09:01 +02:00
Daniel García Aubert
d8b0d338c0 Use 'const' instead of 'var' 2018-05-30 17:02:15 +02:00
Daniel García Aubert
cd0be5f79d Do not require 'step' 2018-05-30 17:00:55 +02:00
Daniel García Aubert
3820894454 Do not use 'step' 2018-05-30 17:00:15 +02:00
Daniel García Aubert
9a60ab07a8 Remove deprecated base url configuration 2018-05-30 16:51:37 +02:00
Daniel G. Aubert
12b91e7671 Merge pull request #966 from CartoDB/unify-connection-pool-config
Unify connection pool config
2018-05-30 14:31:39 +02:00
Daniel García Aubert
fe79ee0315 Merge branch 'master' into unify-connection-pool-config 2018-05-29 16:44:21 +02:00
Daniel García Aubert
9c3b3e698e Update yarn.lock 2018-05-29 16:28:23 +02:00
Raúl Marín
ca4eeb332a Merge pull request #967 from CartoDB/psql_update
Update Windshaft, camshaft and cartodb-psql to use cartodb-psql 0.11.0
2018-05-28 18:07:40 +02:00
Raul Marin
794c3efb7d Update Windshaft, camshaft and cartodb-psql to use cartodb-psql 0.11.0 2018-05-28 17:59:25 +02:00
Javier Goizueta
20fe9c45cf Merge pull request #968 from CartoDB/sample-columns
Sample columns
2018-05-28 17:51:38 +02:00
Javier Goizueta
26da872704 Leave sample exclude_columns for later 2018-05-28 17:37:45 +02:00
Javier Goizueta
8a1d5d3a48 Be careful and detect type invalid types 2018-05-28 17:36:58 +02:00
Javier Goizueta
0392a2a343 Merge pull request #963 from CartoDB/refactor-metadata
Refactor metadata queryPromise
2018-05-28 17:20:23 +02:00
Javier Goizueta
34a2f3b32b Tests were missing in previous commit 2018-05-28 16:50:53 +02:00
Javier Goizueta
4bb8914d9a Add parameters to select metadata sample columns 2018-05-28 16:08:31 +02:00
Javier Goizueta
f7c80a0101 Merge branch 'master' into refactor-metadata 2018-05-28 10:49:58 +02:00
Daniel García Aubert
2d417b4a37 Merge branch 'master' into unify-connection-pool-config 2018-05-28 10:47:02 +02:00
Daniel García Aubert
46bb400ffd Cast x-windshaft-cache header in order to fix assertions 2018-05-28 10:46:04 +02:00
Daniel García Aubert
8b05d75f97 Revert test runner filters 2018-05-28 10:32:31 +02:00
Daniel García Aubert
4861d35628 Cast to integer before comparing cache headers 2018-05-28 10:31:48 +02:00
Daniel García Aubert
4a8bfcf647 Revert this commit: try to see what's happening here 2018-05-28 10:03:07 +02:00
Daniel García Aubert
b311f0e091 Add generic pool configuration to postgres global config 2018-05-25 18:38:31 +02:00
Daniel García Aubert
4e4399b727 Missing postgis configuration 2018-05-25 18:00:53 +02:00
Daniel García Aubert
bbab9c1a6b Avoid breaking changes 2018-05-25 14:44:58 +02:00
Daniel García Aubert
5938836ff5 Use default postgres port 2018-05-25 14:36:06 +02:00
Daniel García Aubert
d003790e4d Use the right port again 2018-05-25 14:25:43 +02:00
Daniel García Aubert
1d0fa9a5f6 Use the right port 2018-05-25 14:23:19 +02:00
Daniel García Aubert
8fe192267d cosmetic 2018-05-25 13:37:57 +02:00
Daniel García Aubert
92255797d9 Add torque options to renderer factory 2018-05-25 13:37:34 +02:00
Daniel García Aubert
f404285140 Use postgres pool configuration to have the same configuration for all renderers. 2018-05-24 18:36:45 +02:00
Daniel García Aubert
ee38c717a5 Remove geojson renderer config 2018-05-24 18:11:18 +02:00
Daniel García Aubert
1668392296 Move 'postgres' attributes inside Mapnik's renderer configuration and keep params relative to user's connection 2018-05-24 13:42:32 +02:00
Javier Goizueta
c1feaecbcb Merge branch 'master' into refactor-metadata
# Conflicts:
#	lib/cartodb/backends/layer-stats/mapnik-layer-stats.js
2018-05-23 12:12:36 +02:00
Javier Goizueta
dea1c74fcc Merge pull request #964 from CartoDB/metadata-categories
Add metadata parameter for NULL categories
2018-05-23 12:08:26 +02:00
Javier Goizueta
22a0f2c14e Merge branch 'master' into metadata-categories 2018-05-23 11:17:10 +02:00
Javier Goizueta
94c34eeb23 Small change to use const 2018-05-23 11:15:20 +02:00
Daniel G. Aubert
d63d976916 Merge pull request #965 from CartoDB/fix-random-failures-test
Fix random failures test
2018-05-22 20:11:27 +02:00
Daniel García Aubert
54601db44a Remove test runner filter 2018-05-22 19:41:20 +02:00
Daniel García Aubert
4701decfcd No need to drain test-client 2018-05-22 19:37:46 +02:00
Daniel García Aubert
cc81c8ff4c Only test user database timeout against Postgis Renderer if available (Postgis >= 2.4) 2018-05-22 19:22:08 +02:00
Daniel García Aubert
8febc78d0e Do not test against Postgis renderer because it does not implement renderer timeout option 2018-05-22 19:20:37 +02:00
Daniel García Aubert
87838bd4ce Override cache buster in order to not hit any internal cache 2018-05-22 18:28:57 +02:00
Daniel García Aubert
dfc354550c Add method to override cache buster provided by server while fetching tiles and static images 2018-05-22 18:27:50 +02:00
Daniel García Aubert
ef36466b3b Fix test to use the right helper function to set the user render timeout 2018-05-22 18:19:41 +02:00
Javier Goizueta
7041039572 Add tests for category metadata parameters 2018-05-22 13:42:29 +02:00
Javier Goizueta
97b4e19777 Add metadata parameter for NULL categories 2018-05-22 13:00:18 +02:00
Daniel G. Aubert
55bf6e86f7 Merge pull request #962 from CartoDB/upgrade-dependencies
Upgrade dependencies
2018-05-21 18:06:15 +02:00
Daniel García Aubert
683ec662b5 Merge branch 'master' into upgrade-dependencies 2018-05-21 17:51:14 +02:00
Daniel García Aubert
87c5844704 Update NEWS 2018-05-21 17:47:51 +02:00
Daniel García Aubert
094a0ea76d Upgrade windshaft version to 4.8.0 2018-05-21 17:41:19 +02:00
Javier Goizueta
ebc086106f Refactor metadata queryPromise helper
This function to adapt query execution as a Promise was unnecessary complex.
2018-05-21 17:33:03 +02:00
Javier Goizueta
6384f5538c Rename variable for clarity 2018-05-21 17:06:53 +02:00
Javier Goizueta
befedfd80a Use ifError to check for errors 2018-05-21 17:03:16 +02:00
Javier Goizueta
d828a92ea3 Use ifError to check for errors 2018-05-21 16:59:36 +02:00
Javier Goizueta
4711b28c25 Merge pull request #952 from CartoDB/metadata
Add optional layer metadata at instantiation
2018-05-21 16:05:43 +02:00
Javier Goizueta
d7a90e6be4 Remove debugging comment 2018-05-21 15:54:52 +02:00
Javier Goizueta
267e770c63 Update NEWS 2018-05-21 15:43:32 +02:00
Javier Goizueta
b233f18a0f Modernize code copied from SQL API 2018-05-21 15:43:23 +02:00
Javier Goizueta
32092d212e Fix bug 2018-05-21 14:05:27 +02:00
Daniel García Aubert
b7b52eee80 Upgrade istanbul and jshint version to 0.4.5 and 2.9.5 respectively 2018-05-21 14:04:04 +02:00
Daniel García Aubert
a8da7e60c3 Do not define dependencies in package.json with version ranges 2018-05-21 13:58:17 +02:00
Daniel García Aubert
83e944c985 Upgrade lru-cache version to 4.1.3 2018-05-21 13:29:07 +02:00
Daniel García Aubert
7f841d49b2 Upgrade mocha version to 3.5.3 2018-05-21 13:18:44 +02:00
Daniel García Aubert
bdc52204e4 Upgrade nock version to 9.2.6 2018-05-21 13:01:01 +02:00
Daniel García Aubert
c95b080267 Upgrade yargs version to 11.1.0 2018-05-21 12:47:48 +02:00
Javier Goizueta
11cdcc65ad Add safety limit to sample metadata
The sampling probability is now being computed using an estimate of the table row count
This could led to too high probabilities (to large samples) if the estimate is not accurate.
To avoid potential problems with large samples we've added a LIMIT to the sampling queries.
2018-05-21 12:45:16 +02:00
Daniel García Aubert
1e9ec9e053 Upgrade express version to 4.16.3 2018-05-21 12:30:28 +02:00
Daniel García Aubert
7bafc54280 Upgrade moment version to 2.22.1 2018-05-21 12:27:21 +02:00
Daniel García Aubert
69d5aef59b Upgrade node-statsd version to 0.1.1 2018-05-21 12:21:40 +02:00
Daniel García Aubert
985e61b3c6 Upgrade strftime version to 0.10.0 2018-05-21 12:16:30 +02:00
Daniel García Aubert
15f512a3c7 Upgrade step version to 1.0.0 2018-05-21 12:08:29 +02:00
Daniel García Aubert
1f3f7b4560 Upgrade semver version to 5.5.0 2018-05-21 12:04:23 +02:00
Javier Goizueta
fecd63e582 Fix bug 2018-05-21 11:59:49 +02:00
Daniel García Aubert
a32613c854 Upgrade queue-async to version 1.1.0 2018-05-21 11:58:23 +02:00
Javier Goizueta
38e55367b1 Revert error behaviour for estimatedFeatureCount
Keep current production behavior of ignoreing errors when computing this stat and returning -1.
This is done as to no introduce any instability in production at the moment.
2018-05-21 11:44:52 +02:00
Daniel García Aubert
73b837f4d9 Upgrade request to version 2.87.0 2018-05-21 10:53:29 +02:00
Daniel García Aubert
9b971aa124 Upgrade dot version to 1.1.2 2018-05-21 10:46:29 +02:00
Daniel García Aubert
8479198268 Upgrade body-parser version to 1.18.3 2018-05-21 10:25:04 +02:00
Daniel García Aubert
9daaf5bb6a Use development branch of windshaft 2018-05-21 10:15:22 +02:00
Javier Goizueta
4e99ff1c39 Fix token substitution for stat queries 2018-05-18 22:25:32 +02:00
Javier Goizueta
8e8458e557 Merge branch 'master' into metadata 2018-05-18 20:49:26 +02:00
Javier Goizueta
391ac51f0f Implement metadata queries with plain Promises
Remove usage of PhasedExecution
This achives better query execution granularity and
removes questionable usage of shared results object.
It introduces a couple of behavior changes:
* estimatedFeatureCount desn't ignore errors now
* sample always uses estimatedFeatureCount,even if the actual count is also computed.
2018-05-18 15:33:07 +02:00
Javier Goizueta
4bc8fb207a Use sql_raw for query without aggregation 2018-05-18 15:29:46 +02:00
Daniel G. Aubert
42cd36afb7 Merge pull request #960 from CartoDB/upgrade-camshaft-0.61.9
Upgrade camshaft to version 0.61.9
2018-05-17 14:25:08 +02:00
Daniel García Aubert
dd0436e68e Update NEWS 2018-05-17 13:55:57 +02:00
Daniel García Aubert
d76a0d9f22 Upgrade camshaft to version 0.61.9 2018-05-17 13:47:52 +02:00
Simon Martín
d9e047e20e NEWS 2018-05-17 12:44:31 +02:00
Simon Martín
af35ff7419 Merge pull request #953 from CartoDB/fix-dataview-typeof
handling pg_typeof == undefined error in dataviews
2018-05-17 12:39:16 +02:00
Simon Martín
81bace1dca Merge branch 'master' into fix-dataview-typeof 2018-05-17 12:13:11 +02:00
Simon Martín
38c69de01b Merge pull request #955 from CartoDB/fix-image-format-png
Unsupported static image format
2018-05-17 12:07:54 +02:00
Simon Martín
213ca07c38 Merge branch 'master' into fix-image-format-png 2018-05-17 11:41:30 +02:00
Javier Goizueta
012fa91e83 Typo 2018-05-16 14:45:34 +02:00
Javier Goizueta
3af1182206 Rename misleading function argument 2018-05-16 14:45:19 +02:00
Simon Martín
04e00bb834 Merge branch 'master' into fix-image-format-png 2018-05-16 14:40:59 +02:00
Raúl Marín
df089cb0a5 Merge pull request #958 from CartoDB/windshaft473
Windshaft 4.7.3
2018-05-16 12:30:59 +02:00
Simon Martín
56aa1b39f0 removing 'jpeg' as valid format and no checking agains lowercase 2018-05-16 12:00:40 +02:00
Daniel G. Aubert
d940ab36e1 Merge pull request #954 from CartoDB/controllers-goodies
Controllers goodies
2018-05-14 18:31:48 +02:00
Raul Marin
0f48d51062 Update yarn.lock 2018-05-14 15:01:04 +02:00
Raul Marin
1c5344ba6e Update Windshaft to 4.7.3 2018-05-14 14:54:29 +02:00
Daniel G. Aubert
0d4654122c Merge pull request #956 from CartoDB/add-merge-params-options
Express Routers: Activate mergeParams option
2018-05-14 12:59:44 +02:00
Daniel García Aubert
63a9d58c67 Add regression test 2018-05-14 12:28:57 +02:00
Daniel García Aubert
5a397afd06 In order to extract common middlewares between routers and controlles we need to activate mergeParams options to preserve the req.params from the parent router 2018-05-14 11:50:48 +02:00
Javier Goizueta
b8109401d1 Tests for metadata with aggregation 2018-05-13 13:05:39 +02:00
Javier Goizueta
5e09c80b71 Remove comment 2018-05-11 19:57:49 +02:00
Javier Goizueta
b906f88a44 Slight refactor 2018-05-11 19:32:03 +02:00
Javier Goizueta
24b1b53ba0 Merge branch 'master' into metadata 2018-05-11 18:58:38 +02:00
Javier Goizueta
53fae9fbbd Comment 2018-05-11 18:57:14 +02:00
Simon Martín
ad4ed7a06b detail 2018-05-11 17:47:47 +02:00
Simon Martín
7f5e655730 static image format for last route 2018-05-11 17:45:17 +02:00
Simon Martín
e96a9f0b46 static image format controllers 2018-05-11 17:42:28 +02:00
Simon Martín
db7b4fa937 static image format tests 2018-05-11 17:41:26 +02:00
Simon Martín
7112341c51 checkStaticImageFormat middleware 2018-05-11 17:41:00 +02:00
Daniel García Aubert
f4d60f963d Add comment 2018-05-11 16:37:02 +02:00
Daniel García Aubert
c6babc7dc4 Create .middlewares() method to build midlleware stack to perform request 2018-05-11 16:26:05 +02:00
Daniel García Aubert
3905ed796e Use ES6 class syntax 2018-05-11 16:24:28 +02:00
Daniel García Aubert
595d006d5b Rename function 2018-05-11 16:21:57 +02:00
Daniel García Aubert
3bcf6d7ca0 Place comment 2018-05-11 16:20:44 +02:00
Daniel García Aubert
68f5ee7bde Use ES6 class syntax 2018-05-11 16:18:52 +02:00
Daniel García Aubert
9db6e2161b Use ES6 class syntax 2018-05-11 16:15:33 +02:00
Daniel García Aubert
6eeb75a35e Use .middlewares() method to build middleware stack to process the request 2018-05-11 16:13:34 +02:00
Daniel García Aubert
1f717617b0 Add .middlewares() method to build middleware stack to be mounted 2018-05-11 16:07:25 +02:00
Daniel García Aubert
3d7231929c Create .middlewares() method to return the middlewares to process the request 2018-05-11 15:34:10 +02:00
Javier Goizueta
3b4668cc19 Fix simple tabley sampling 2018-05-11 14:45:12 +02:00
Javier Goizueta
34ad3fcfe8 Add aggregated stat for testing
Also change aggregated stats to not filter a single tile
2018-05-11 14:18:31 +02:00
Daniel García Aubert
242224396d Create .middlewares() method to compose controller actions 2018-05-11 14:05:41 +02:00
Javier Goizueta
68b3cb8a34 Fix estimated row count with aggregations
All stats are computed now pre-aggregation
Code to help compute post-aggregation stats remains for testing
2018-05-11 13:44:43 +02:00
Daniel García Aubert
1cfeda8fe5 Create .middlewares() method to mount corresponding set of middlewares 2018-05-11 13:38:51 +02:00
Daniel García Aubert
33af2d37b3 Remove declared variables but not used 2018-05-11 13:30:25 +02:00
Daniel García Aubert
69505974fe Pass object instead of argument list 2018-05-11 13:29:50 +02:00
Daniel García Aubert
a77dd9a11f Create .middlewares() method to be mounted in dataview controller 2018-05-11 13:20:05 +02:00
Daniel García Aubert
1bc017eac9 Rename function 2018-05-11 12:42:17 +02:00
Daniel García Aubert
07dec2e641 Create .middlwares() to return a set of middlewares to be mounted 2018-05-11 12:38:52 +02:00
Daniel García Aubert
d86a839265 Blank line 2018-05-11 12:22:50 +02:00
Daniel García Aubert
72d8a26ede Use ES6 class 2018-05-11 12:14:27 +02:00
Javier Goizueta
cae4dd81c9 WIP: fix problems for aggregations & metadata 2018-05-10 19:12:47 +02:00
Simon Martín
eba68c56ef handling pg_typeof undefined 2018-05-10 18:36:08 +02:00
Simon Martín
99516f5a75 fix tests and jshint happy 2018-05-10 18:35:52 +02:00
Simon Martín
37a2e89c81 testing expected result with pg_typeof undefined 2018-05-10 18:34:18 +02:00
Simon Martín
ed837fbf22 improving tests raising the pg_typeof error 2018-05-10 17:25:02 +02:00
Simon Martín
913b29070f tests raising pg_typeof error 2018-05-10 15:59:38 +02:00
Daniel G. Aubert
e5ddd57d65 Merge pull request #927 from CartoDB/separate-app-and-controllers-creation
Separate app and controllers creation
2018-05-10 13:19:10 +02:00
Daniel García Aubert
db35ec682a Merge branch 'master' into separate-app-and-controllers-creation 2018-05-09 18:12:06 +02:00
Daniel G. Aubert
111889565a Merge pull request #931 from CartoDB/separate-routers
Separate public API (create a router hierarchy) from monitoring endpoints
2018-05-09 17:57:50 +02:00
Daniel G. Aubert
c68ece96cd Merge pull request #934 from CartoDB/improve-folder-structure
Improve folder structure
2018-05-09 17:26:13 +02:00
Daniel García Aubert
0741881959 Do not initialize status code to 404 when method is OPTIONS 2018-05-09 16:24:38 +02:00
Javier Goizueta
f7745928ab Fix tests
eliminate dependency on the order of PostgreSQL results
2018-05-09 15:42:41 +02:00
Daniel García Aubert
976ee35a35 Use new routes configuration schema in ported test 2018-05-09 15:24:33 +02:00
Daniel García Aubert
c51e254287 Set 404 as defautl status code and set the proper status code fir the response at very same time that the response body 2018-05-09 15:00:18 +02:00
Daniel García Aubert
9feea66550 Use routes configuration to create and mount routes and controllers 2018-05-09 14:59:21 +02:00
Javier Goizueta
ee7bd5fb8a Fix tests 2018-05-09 12:42:42 +02:00
Javier Goizueta
fff5b3d85a Revert debugging changes 2018-05-09 11:59:24 +02:00
Javier Goizueta
d706d0eb22 More travis debugging through commits 2018-05-09 11:44:49 +02:00
Javier Goizueta
944ce80c1e Revert debugging change 2018-05-09 11:42:53 +02:00
Javier Goizueta
d8ef8cb12f Debug travis test failures 2018-05-09 11:08:02 +02:00
Javier Goizueta
eea7bed2f3 Slightly more elegant results of queries 2018-05-08 20:41:42 +02:00
Javier Goizueta
741bcd1a80 Metadata fixes 2018-05-08 20:07:20 +02:00
Javier Goizueta
9c9cfd015d Add test for optional layer metadata 2018-05-08 20:06:14 +02:00
Raúl Marín
39a1b0742f Merge pull request #951 from Algunenano/revert_windshaft472
Revert windshaft472
2018-05-08 19:14:59 +02:00
Raul Marin
03bcf573c3 Revert "Update yarn.lock with the Windshaft 4.7.2"
This reverts commit fa647a915c.
2018-05-08 18:58:04 +02:00
Raul Marin
97d1b4fafa Revert "Update Windshaft to 4.7.2"
This reverts commit 2cf0b9d097.
2018-05-08 18:57:32 +02:00
Raúl Marín
d79d3fd4dc Merge pull request #950 from CartoDB/windshaft472
Update to Windshaft 4.7.2
2018-05-08 18:05:24 +02:00
Eneko Lakasta
f60993b042 re-organize no api key token provided tests - explain tests 2018-05-08 14:41:16 +02:00
Eneko Lakasta
1005126a5f re-organize no api key token provided tests 2018-05-08 14:32:44 +02:00
Raul Marin
fa647a915c Update yarn.lock with the Windshaft 4.7.2 2018-05-08 14:08:12 +02:00
Raul Marin
2cf0b9d097 Update Windshaft to 4.7.2 2018-05-08 14:04:19 +02:00
Javier Goizueta
7d68a2967f Fix: callback expected errors in first argument 2018-05-08 13:08:26 +02:00
Javier Goizueta
b96be69a5c Clarify example 2018-05-08 13:08:01 +02:00
Javier Goizueta
636cd8cd50 Fix:phase execution
phase (not only its tasks) must be executed after the tasks of previous phases
2018-05-08 12:56:06 +02:00
Eneko Lakasta
0536d0abcb add test to check that the fallback api key used if none is sent, is the default public 2018-05-08 11:32:45 +02:00
Javier Goizueta
c647f852d6 Refactor metadata queries execution
Also fixed bug where sampling query generation needed results of count queries
2018-05-08 11:09:09 +02:00
Javier Goizueta
ebab879aca Fix bugs & typos 2018-05-08 11:07:47 +02:00
Javier Goizueta
7561635b24 WIP:add layer metadata 2018-05-07 19:03:19 +02:00
Daniel García Aubert
7ed819e84a Merge branch 'separate-routers' into improve-folder-structure 2018-05-07 18:29:53 +02:00
Daniel García Aubert
407a83e81d Merge branch 'separate-app-and-controllers-creation' into separate-routers 2018-05-07 18:29:00 +02:00
Daniel García Aubert
3c3731252d Respect default values with object.assign 2018-05-07 18:24:41 +02:00
Daniel García Aubert
8b64328087 Merge branch 'separate-routers' into improve-folder-structure 2018-05-07 17:28:20 +02:00
Daniel García Aubert
f1bb5b3d1d Merge branch 'separate-app-and-controllers-creation' into separate-routers 2018-05-07 17:27:32 +02:00
Daniel García Aubert
7aeab47df4 Merge branch 'master' into separate-app-and-controllers-creation 2018-05-07 17:27:00 +02:00
Eneko Lakasta
e1b848afd0 use matching cartodb-redis branch [fix] 2018-05-07 16:36:09 +02:00
Eneko Lakasta
f111ddf449 use matching cartodb-redis branch and run all tests 2018-05-07 16:07:28 +02:00
Eneko Lakasta
534c827904 remove auth fallback 2018-05-07 15:44:44 +02:00
Simon Martín
efa765a9fc Merge pull request #945 from CartoDB/docker-tests
Travis tests: xenial+pg10 and precise+pg9.5
2018-05-07 10:56:15 +02:00
Simon Martín
910f5693d8 updating news 2018-05-07 10:43:15 +02:00
Simon Martín
7c5ed7e8f8 using the correct docker account 2018-05-03 17:38:54 +02:00
Simon Martín
066626d928 simplify docker commands 2018-05-03 11:18:09 +02:00
Simon Martín
6bb8c9c271 EOF line 2018-05-03 10:47:20 +02:00
Simon Martín
e76850fce6 updating docker-test with new run_tests_docker.sh 2018-04-27 15:47:40 +02:00
Simon Martín
8d3503e0fc removing unneeded configuration 2018-04-27 15:46:57 +02:00
Simon Martín
9788af4273 adding ENV vars and Postgres configuration. Also, Postgres starts auto 2018-04-27 15:46:10 +02:00
Simon Martín
4442200d6b EOF line 2018-04-27 13:08:46 +02:00
Simon Martín
02b12f370a adding an small docker reference 2018-04-27 13:05:52 +02:00
Simon Martín
dce6349fcb sudo required to right place 2018-04-27 12:01:56 +02:00
Simon Martín
862c79ee16 removing docker image for xenial and pg95 2018-04-27 11:58:05 +02:00
Simon Martín
29c1d74202 adding redis 4 to travis 2018-04-27 11:48:35 +02:00
Simon Martín
d83679c895 ensuring postgres version 2018-04-27 11:39:30 +02:00
Simon Martín
62bdbed1d5 setting yarn version in previous travis version 2018-04-27 11:07:20 +02:00
Simon Martín
390a2b573f test last travis configuration previous to docker 2018-04-27 10:57:47 +02:00
Simon Martín
bbbd7ab41c forcing yarn install with env params 2018-04-27 10:47:39 +02:00
Simon Martín
85eb82a175 gcc in travis 2018-04-27 10:37:32 +02:00
Simon Martín
82189d35e0 removing create publu user 2018-04-27 10:28:04 +02:00
Simon Martín
f880e86cd1 removing configure in travis 2018-04-27 10:20:55 +02:00
Simon Martín
87f30a3633 installing yarn in travis 2 2018-04-26 17:10:40 +02:00
Simon Martín
47b029a871 updating g++ in travis 2018-04-26 17:00:37 +02:00
Simon Martín
b16e2cad19 install yarn in travis 2018-04-26 16:57:14 +02:00
Simon Martín
f8b347a03a postgres 9.5 without docker in travis 2018-04-26 16:31:32 +02:00
Simon Martín
12da3a58fc postgres 10 working with maps and sql 2018-04-26 13:12:25 +02:00
Simon Martín
73d7ee37f8 travis in parallel 4 2018-04-24 18:15:10 +02:00
Simon Martín
7fdee0ebe5 travis in parallel 2018-04-24 18:07:31 +02:00
Simon Martín
9437b35c8c travis in paralell 2 2018-04-24 18:02:59 +02:00
Simon Martín
78326629f3 trying travis tests in parallel 2018-04-24 18:00:17 +02:00
Simon Martín
db4c3b70bb travis build stages 2 2018-04-24 17:49:38 +02:00
Simon Martín
09d937f533 travis build stages 2018-04-24 17:41:29 +02:00
Simon Martín
d3615e8d2b pull images before run tests 2018-04-23 16:18:22 +02:00
Simon Martín
aeaf325e2a travis tests with both images 2018-04-23 16:00:46 +02:00
Simon Martín
d629dc24ae removing default commands from image 2018-04-23 15:40:16 +02:00
Simon Martín
d8c356e350 changing name of docker sh to run_tests_docker 2018-04-23 15:39:01 +02:00
Simon Martín
55d2dcca04 adding first version of docker images 2018-04-23 15:26:42 +02:00
Daniel García Aubert
9bd9503e9b Merge paths to perform the same middleware stack 2018-04-18 19:07:38 +02:00
Daniel García Aubert
56495522b8 Use array of path to avoid collisions and extract scale-factor param properly 2018-04-18 18:52:09 +02:00
Daniel García Aubert
9d1d5c439b Merge branch 'separate-routers' into improve-folder-structure 2018-04-18 17:08:11 +02:00
Daniel García Aubert
be5e419288 Merge branch 'separate-app-and-controllers-creation' into separate-routers 2018-04-18 17:06:53 +02:00
Daniel García Aubert
8afe6c5228 Merge branch 'master' into separate-app-and-controllers-creation 2018-04-18 17:05:27 +02:00
Daniel G. Aubert
20b46a33cf Merge pull request #944 from CartoDB/improve-test-suite
Improve test suite
2018-04-18 16:58:52 +02:00
Daniel García Aubert
1694b4b3a6 Merge branch 'separate-routers' into improve-folder-structure 2018-04-17 16:07:47 +02:00
Daniel García Aubert
5b1b78d386 Merge branch 'separate-app-and-controllers-creation' into separate-routers 2018-04-17 15:56:33 +02:00
Daniel García Aubert
9c249596c0 Merge branch 'master' into separate-app-and-controllers-creation 2018-04-17 15:55:38 +02:00
Raúl Marín
36394520ff Merge pull request #936 from CartoDB/mvt-invalid-props
MVT: Different types/values for same property in same feature in different tiles
2018-04-17 12:33:44 +02:00
Raul Marin
45adaf5dc2 Merge remote-tracking branch 'blessed/master' into mvt-invalid-props 2018-04-17 12:08:57 +02:00
Daniel G. Aubert
be0680675b Merge pull request #943 from CartoDB/validate-coords
Add coordinates validation to specific endpoints
2018-04-17 11:43:28 +02:00
Daniel García Aubert
6c466d13ff Typo 2018-04-17 11:36:33 +02:00
Daniel García Aubert
9905b20448 Improve naming 2018-04-17 11:22:05 +02:00
Daniel García Aubert
2ff410946a Add test to check that the regexp validates just digits 2018-04-17 11:20:15 +02:00
Daniel García Aubert
94be7e5294 Update NEWS 2018-04-17 11:13:33 +02:00
Daniel García Aubert
da3939239f Fix test assertions 2018-04-17 11:10:34 +02:00
Simon Martín
adefbb3365 query with template string 2018-04-17 11:05:37 +02:00
Daniel García Aubert
8000a51918 Improve error messages 2018-04-17 11:04:03 +02:00
Daniel García Aubert
7012c6e77a Test: Let the OS to choose a random free port to make the tiler listen at it 2018-04-17 10:44:44 +02:00
Raul Marin
13843772d4 Update NEWS 2018-04-17 10:42:38 +02:00
Raul Marin
ad4cd2067b JShint fix 2018-04-17 10:39:39 +02:00
Raul Marin
ff3efd7d56 Update Windshaft to 4.7.1 2018-04-17 10:31:32 +02:00
Raul Marin
8d6a406779 Merge remote-tracking branch 'carto/master' into mvt-invalid-props 2018-04-17 10:25:05 +02:00
Daniel García Aubert
98d15e2e34 Typo 2018-04-16 18:57:28 +02:00
Daniel García Aubert
d5c591317b Add coordinates validation to specific endpoints 2018-04-16 18:55:42 +02:00
Daniel García Aubert
817afb13d1 Use mocha hooks to create server just before perform the test 2018-04-16 16:16:23 +02:00
Daniel García Aubert
98f29f945b Create application server inside of describes instead of at module level 2018-04-16 14:09:24 +02:00
Rafa de la Torre
09fdf5b990 Stub next version 2018-04-16 11:53:06 +02:00
Rafa de la Torre
b760aa8c63 Release v6.1.0 2018-04-16 11:51:04 +02:00
Rafa de la Torre
a83e6b6929 Merge pull request #929 from CartoDB/config_markers_symbolizer_caches
Config for markers symbolizer caches
2018-04-16 11:47:13 +02:00
Rafa de la Torre
64fa18220f Update NEWS.md 2018-04-16 10:36:42 +02:00
Rafa de la Torre
b11a766c28 Add config for markers_symbolizer_caches examples 2018-04-16 10:36:42 +02:00
Rafa de la Torre
c2c3993887 Upgrade windshaft to 4.7.0 2018-04-16 10:36:36 +02:00
Eneko Lakasta
35459b7332 Merge pull request #938 from CartoDB/named-map-auth-tests
Named map auth tests
2018-04-13 12:56:49 +02:00
Daniel G. Aubert
1b8e37a62c Merge pull request #940 from CartoDB/regression-test-named-map-dataview-filter
Regression test named map dataview filter
2018-04-13 12:45:34 +02:00
Daniel García Aubert
89cc9ad27d Merge branch 'master' into regression-test-named-map-dataview-filter 2018-04-12 20:53:27 +02:00
Daniel García Aubert
d8a5dc586d Add test to check dataview filters when instantiating a named map 2018-04-12 19:58:33 +02:00
Daniel G. Aubert
837b2f7558 Merge pull request #937 from CartoDB/add-map-intantiation-regression-test
Add test to check regression when instantiating a map
2018-04-12 19:07:48 +02:00
Daniel García Aubert
1df7df21d5 Revert removed line 2018-04-12 18:31:22 +02:00
Daniel García Aubert
53a40de2e7 Use another table 2018-04-12 18:24:00 +02:00
Eneko Lakasta
940cafacac add newline add EOF 2018-04-12 17:51:33 +02:00
Eneko Lakasta
d1ba4a1759 remove unnecessary test 2018-04-12 17:41:07 +02:00
Daniel García Aubert
6437e2ec67 Add test to check regression when cache buster during instantiation is not updated 2018-04-12 16:32:44 +02:00
Eneko Lakasta
31e3b9953f add named maps GET auth tests 2018-04-12 16:22:56 +02:00
Eneko Lakasta
f5bdb8b15b add named maps Update auth tests 2018-04-12 16:03:02 +02:00
Eneko Lakasta
fbcf312071 add named maps Delete auth tests 2018-04-12 14:46:08 +02:00
Eneko Lakasta
9a7a8a3243 add named maps Create auth tests 2018-04-12 12:43:41 +02:00
Simon Martín
fc36950c1d fix errored value in test 2018-04-12 11:27:31 +02:00
Simon Martín
b4ca44f096 Merge branch 'mvt-invalid-props' of github.com:CartoDB/Windshaft-cartodb into mvt-invalid-props 2018-04-12 10:46:45 +02:00
Simon Martín
4978dd86ac simply cases of mvt property type changes 2018-04-12 10:46:21 +02:00
Daniel García Aubert
730b29c9cc Remove defined dependecies but never used 2018-04-11 18:51:44 +02:00
Raul Ochoa
4d631d1b6a Merge branch 'master' into mvt-invalid-props 2018-04-11 14:08:29 +00:00
Raul Ochoa
2d2629c088 improve description for suite and test 2018-04-11 14:08:18 +00:00
Daniel García Aubert
9a52edacb2 Add routes configuration to config example files 2018-04-11 16:00:14 +02:00
Raul Ochoa
67411e32ff Validate property with strict check and use full table
It seems related to using the full table. The geometry column is a
simplified to reduce the file size, it uses the envelope of the original
geometries.
2018-04-11 11:40:55 +00:00
Daniel García Aubert
e06d3200c3 Use a better API to define public routes 2018-04-11 12:33:07 +02:00
Eneko Lakasta
9a3eb3e0fd add named maps Listing auth tests 2018-04-11 12:26:09 +02:00
Raul Ochoa
a7c96acc81 Run all tests 2018-04-11 10:13:24 +00:00
Raul Ochoa
f2ab33b498 Add regression test for invalid MVT properties 2018-04-11 10:03:06 +00:00
Javier Goizueta
6a7c9e34a0 Merge pull request #920 from CartoDB/regression-mvt-postgis-agg-column-name
Error with aggregations and MVT Postgis backend
2018-04-11 11:15:45 +02:00
Raul Ochoa
6c8b3d2f8f Merge branch 'master' into regression-mvt-postgis-agg-column-name 2018-04-11 08:34:28 +00:00
Daniel García Aubert
c0943a7c58 Use config to define base path for express routers 2018-04-10 20:26:36 +02:00
Daniel G. Aubert
5d230c444c Merge pull request #928 from CartoDB/extract-cors-mw
Make cors middleware more generic and link it just to application level
2018-04-10 18:29:59 +02:00
Daniel García Aubert
ac615b4a25 Merge branch 'separate-routers' into improve-folder-structure 2018-04-10 16:15:49 +02:00
Daniel García Aubert
bc45b50290 Merge branch 'separate-app-and-controllers-creation' into separate-routers 2018-04-10 15:59:05 +02:00
Daniel García Aubert
1bbd84b37a Please jshint 2018-04-10 15:56:52 +02:00
Daniel García Aubert
aa3fb4807b Merge branch 'master' into separate-app-and-controllers-creation 2018-04-10 15:25:36 +02:00
Daniel García Aubert
d1a4057a8d Rename user limits api by user limits backend 2018-04-10 10:16:07 +02:00
Daniel García Aubert
8519d2724b Rename tables extent api by tables extent backend 2018-04-10 09:40:09 +02:00
Daniel García Aubert
ba36a47228 Rename overviews metadata api by overviews metadata backend 2018-04-09 19:47:43 +02:00
Daniel García Aubert
e0d8dc0334 Rename filter stats api by filter stats backend 2018-04-09 18:56:01 +02:00
Daniel García Aubert
8dec4814a9 Rename AuthAppi by AuthBackend 2018-04-09 18:08:56 +02:00
Daniel García Aubert
6167562758 Meet application logic and folder struture 2018-04-09 16:18:30 +02:00
Daniel García Aubert
7e68f5270d Move application middlewares to routers folder 2018-04-06 18:20:33 +02:00
Daniel G. Aubert
a24b7d4c8f Merge pull request #933 from CartoDB/4252-fix-named-map-auth
Forbid access to named map admin resources for everyone but master
2018-04-06 15:31:15 +02:00
Daniel García Aubert
25aa967146 Forbid access to named map admin resources for everyone but master 2018-04-06 15:26:11 +02:00
Daniel García Aubert
3bfc7d3d23 Rename controllers folder by routers 2018-04-06 13:53:50 +02:00
Simon Martín
430a3e3fc9 Merge pull request #932 from CartoDB/fix-layergroupid-date
Fix layergroupid date
2018-04-06 13:35:11 +02:00
Simon Martín
c94d782037 calling to new createAffectedTables 2018-04-06 13:00:12 +02:00
Simon Martín
233f9698f3 fix affectedtables cache 2018-04-06 12:59:53 +02:00
Daniel García Aubert
61fc15cec0 Add comment to indicate deprecated paths 2018-04-06 11:13:34 +02:00
Daniel García Aubert
4e754e0d86 Create API router to handle common stuff among child routers 2018-04-05 19:42:20 +02:00
Javier Goizueta
125ab7d95e Merge pull request #930 from CartoDB/cartovl-cartodb_id
Make cartodb_id unique in aggregations
2018-04-05 16:46:23 +02:00
Javier Goizueta
26c5ff1f93 Update news 2018-04-05 16:36:07 +02:00
Javier Goizueta
ffa3a96f1a Use unique cartodb_id in aggregated results
See #889
FOr centroid and point-grid the cartodb_id wasn't unique across tiles.
2018-04-05 14:06:27 +00:00
Javier Goizueta
9818d8bb6c Merge branch 'master' into cartovl-cartodb_id 2018-04-05 16:02:03 +02:00
Javier Goizueta
f7fad736c3 Add test for uniqueness of aggregated cartodb_id 2018-04-05 16:01:29 +02:00
Javier Goizueta
62cc99c1c9 Merge pull request #913 from CartoDB/cartogl
Aggregation filters
2018-04-05 12:23:44 +02:00
Javier Goizueta
44424583f0 Revert "Use unique cartodb_id in aggregated results"
This reverts commit c1da1a8a16.
This is reverted for moving the change out of PR #913 into its own PR for clarity.
2018-04-05 12:12:58 +02:00
Javier Goizueta
98cb0878d9 Update NEWS
Reflect the changes in #913
2018-04-05 12:12:00 +02:00
Daniel García Aubert
5aa63d35ce Add comments 2018-04-04 20:07:53 +02:00
Daniel García Aubert
11099c88dc Improve routing via regular expressions 2018-04-04 20:00:59 +02:00
Daniel García Aubert
ef22c46199 Make send-response middleware generic to the router 2018-04-04 19:15:51 +02:00
Javier Goizueta
e8cd6856b5 Add missing aggregation columns to ST_AsMVT
Aggregation results always should have the cartodb_id and the feature count
2018-04-04 17:18:56 +02:00
Javier Goizueta
3d36802686 Tests for columns present in aggregation MVTs 2018-04-04 16:58:11 +02:00
Javier Goizueta
dc706aeb43 Fix bug with dimension aliases
The point-sample aggregation query failed if dimensions had alias different from the base columns
2018-04-04 16:29:40 +02:00
Javier Goizueta
071b6816e3 Fix docs typos in _cdb_feature_count 2018-04-04 16:28:07 +02:00
Daniel García Aubert
cfdff61d08 Create map & template routers to skip unneeded middlewares in monitor endpoints 2018-04-04 15:52:54 +02:00
Javier Goizueta
2132960d7c Fix non-default aggregation columns
The columns for non-default aggregations were the base columns not the resulting aggregated columns
In particular this could cause invalid wrapped SQL code to be passed to ST_AsMVT when the Windshaft pg-mvt renderer was used.
2018-04-04 15:25:08 +02:00
Javier Goizueta
fefb0b23af Merge branch 'master' into cartogl 2018-04-04 12:21:43 +02:00
Javier Goizueta
c1da1a8a16 Use unique cartodb_id in aggregated results
See #889
FOr centroid and point-grid the cartodb_id wasn't unique across tiles.
2018-04-04 11:05:03 +02:00
Daniel García Aubert
93bdbb1c50 Create monitor monitor router 2018-04-03 19:16:37 +02:00
Daniel García Aubert
f1e421db05 Create an api router isolated from heath checks 2018-04-03 19:08:56 +02:00
Daniel García Aubert
462ba62656 Make cors middleware more generic and link it just to application level 2018-04-03 15:32:29 +02:00
Daniel García Aubert
5cd073c96f Remove old controller from bad merge 2018-04-03 14:50:24 +02:00
Daniel García Aubert
40b8d865a9 Merge branch 'master' into separate-app-and-controllers-creation 2018-04-03 14:45:49 +02:00
Daniel García Aubert
a21d7db390 Extract json-replacer 2018-04-03 13:21:41 +02:00
Daniel García Aubert
cc61a89c68 Remove function 2018-04-03 12:27:45 +02:00
Daniel García Aubert
3316c2ded3 Create logger middleware to encapsulate its configuration from app 2018-04-03 12:26:35 +02:00
Daniel García Aubert
b6989ac82a Create a controllers factory where all collaborators are created and controllers are mounted afterwards 2018-04-02 19:02:31 +02:00
Daniel García Aubert
6bf06116df Build controllers (analysis and server info) in server 2018-03-28 20:12:11 +02:00
Daniel García Aubert
5bb5bc42ee Create Map Controller while building server 2018-03-28 19:58:30 +02:00
Daniel García Aubert
57e10a8d2b Create layergroup controllers in server construction 2018-03-28 19:37:31 +02:00
Daniel García Aubert
51fade6bd3 Ensure each controller only receives one router 2018-03-28 19:11:19 +02:00
Daniel
48ffe5e660 Merge pull request #922 from CartoDB/fix-category-filter
Add query params when instantiating template
2018-03-28 16:06:55 +02:00
Daniel García Aubert
22fdc3d1bf Add query params when instantiating template 2018-03-28 15:53:34 +02:00
Daniel García Aubert
04b65c7c0d Please jshint 2018-03-28 14:21:20 +02:00
Daniel García Aubert
3576eb8081 Do not use template strings 2018-03-28 14:16:13 +02:00
Daniel García Aubert
9377b73aa3 Extract map error middleware 2018-03-28 14:12:21 +02:00
Daniel García Aubert
e5aff3f366 Extract layergroup-metadata middleware 2018-03-28 14:06:23 +02:00
Daniel García Aubert
78356ab298 Extract layergroupIdHeader middleware 2018-03-28 13:41:41 +02:00
Daniel García Aubert
947a367865 Extract layerStats middleware 2018-03-28 13:31:37 +02:00
Daniel García Aubert
e79d9ec2f9 Extract lastUpdatedTimeLayergroup middleware 2018-03-28 13:27:01 +02:00
Daniel García Aubert
16e8451166 Extract augment layergroup data middleware 2018-03-28 13:10:47 +02:00
Daniel García Aubert
1d54a8dccd Extract increment map view count middleware 2018-03-28 13:01:23 +02:00
Daniel García Aubert
b68d2d9115 Extract checkJsonContentType middleware 2018-03-28 12:49:11 +02:00
Daniel García Aubert
64d540f23b Extract initProfiler middlewar 2018-03-28 12:45:03 +02:00
Daniel García Aubert
d8d681e8bc Create separated controllers (named & anonymous) for map controller 2018-03-28 12:39:39 +02:00
Daniel García Aubert
5b9f608667 Use express routers 2018-03-27 18:46:54 +02:00
Daniel García Aubert
7660046720 Merge branch 'master' into extract-common-mw 2018-03-27 17:25:33 +02:00
Daniel
0315b32d2b Merge pull request #917 from CartoDB/903-locals-refactor
Locals middleware refactor
2018-03-27 17:23:41 +02:00
Daniel García Aubert
5f906e54e4 Merge branch 'master' into 903-locals-refactor 2018-03-27 15:44:54 +02:00
Daniel
143f0ea67b Merge pull request #918 from CartoDB/layergroup-split
Layergroup split
2018-03-27 15:44:06 +02:00
Daniel
0aa2cffb5e Merge pull request #914 from CartoDB/unify-middlewares
Unify sendResponse middleware
2018-03-27 15:33:23 +02:00
Daniel
f2a7953d9d Merge pull request #915 from CartoDB/unify-headers-middlewared
Unify headers middlewares
2018-03-27 12:38:23 +02:00
Daniel García Aubert
f231dc13cf Merge branch 'master' into unify-middlewares 2018-03-27 10:41:35 +02:00
Daniel García Aubert
a107ee67fa Use arrow function 2018-03-27 10:32:22 +02:00
Daniel García Aubert
cb488cbde8 Extract middleware served by host header 2018-03-26 19:53:33 +02:00
Raul Ochoa
18d3da66f3 Merge branch 'master' into regression-mvt-postgis-agg-column-name 2018-03-26 16:52:57 +00:00
Raul Ochoa
61dd92129a Merge pull request #919 from CartoDB/rediscell-macosx
Fix test environment in Mac OS X re:redis-cell
2018-03-26 18:48:18 +02:00
Raul Ochoa
489c0f3108 Going red: regression with aggregations and MVT Postgis backend 2018-03-26 16:44:31 +00:00
Raul Ochoa
e327580a2f Remove downloading function
If we are using a fixed release version, it doesn't make sense to keep downloading it.
2018-03-26 17:54:32 +02:00
Raul Ochoa
bd9c28e29c Use specific redis cell SO depending on OS 2018-03-26 17:53:36 +02:00
Raul Ochoa
f4d7148f66 Include shared objects for both, mac and linux, OS 2018-03-26 17:52:32 +02:00
Raul Ochoa
1dd5bc8f14 Do not ignore redis-cell SO 2018-03-26 17:52:00 +02:00
Daniel García Aubert
59db640d0d Typo 2018-03-26 16:05:53 +02:00
Daniel García Aubert
4bb35f5fab Extract cors and user middlewares and set them up at application level 2018-03-26 15:37:44 +02:00
Simon Martín
967a0c31fd Merge pull request #916 from CartoDB/finalDetails
Rate limits final details
2018-03-26 11:51:04 +02:00
Daniel García Aubert
c5c8dd7ad7 Split layergroup controllers into small controllers 2018-03-23 21:20:37 +01:00
Daniel García Aubert
d3e2707fce Tidy middlewares up: put rate limit middleware after authorization 2018-03-23 17:55:41 +01:00
Daniel García Aubert
4cba4c7a1f Tidy middlewares up: cleanUpQeuryParams 2018-03-23 17:37:06 +01:00
Daniel García Aubert
3b1fd05940 Use layergroup token middleware where it's actually needed 2018-03-23 17:24:56 +01:00
Daniel García Aubert
5bc5c0ae86 Remove locals middleware 2018-03-23 16:53:00 +01:00
Daniel García Aubert
5fc801f8a6 Do not use locals middleware in named maps controller 2018-03-23 16:38:55 +01:00
Daniel García Aubert
f7a23c094c Do not use locals middleware in named maps admin controller 2018-03-23 16:16:53 +01:00
Daniel García Aubert
516b1f765e Do not use middleware local in map controller 2018-03-23 16:08:52 +01:00
Simon Martín
d28c915635 jshint happy 2018-03-23 15:57:30 +01:00
Daniel García Aubert
f76606bc26 Do not use locals middleware in layergroup controller 2018-03-23 14:13:27 +01:00
Daniel García Aubert
7ba3394508 Do not merge req.params and req.query into res.locals (don't use locals middleware in analysis controller) 2018-03-23 14:10:27 +01:00
Simon Martín
f19eeff899 returning error mvt on rate limit 2018-03-23 13:30:47 +01:00
Daniel García Aubert
d3c9da6d5f Fix layer filter by query params 2018-03-23 11:57:28 +01:00
Simon Martín
1ce908177e correct error message in rate limit tests 2018-03-23 11:47:28 +01:00
Simon Martín
609bf13765 correct error message in rate limit 2018-03-23 11:42:53 +01:00
Daniel García Aubert
97a49fab2f Remove function defined but nerver used 2018-03-23 11:33:40 +01:00
Daniel García Aubert
10ead27676 Pass only needed properties to named map provider cache (static endpoint) 2018-03-23 11:23:19 +01:00
Daniel García Aubert
8be7ea5cc1 Pass only needed properties to named map provider cache 2018-03-23 11:01:36 +01:00
Daniel García Aubert
ebefba9e32 Revert: move map-config assignment 2018-03-23 10:57:35 +01:00
Simon Martín
fb784d6a91 removing retry after when no necessary 2018-03-23 10:23:57 +01:00
Daniel García Aubert
c31639ebbd Move assignments 2018-03-22 19:38:56 +01:00
Daniel García Aubert
4ff8d6fbc3 Pass only needed params to map backend 2018-03-22 19:37:08 +01:00
Daniel García Aubert
d029f81992 Pass only needed params to create layergroup map config provider 2018-03-22 19:36:42 +01:00
Daniel García Aubert
6b7c2675f1 Use database params module 2018-03-22 19:20:51 +01:00
Daniel García Aubert
4f8c184bc0 Pass only needed params to map config adapter 2018-03-22 19:14:18 +01:00
Daniel García Aubert
afc608fc5d Pass only needed params to named map map config provider 2018-03-22 18:57:26 +01:00
Daniel García Aubert
8523875349 Remove function thet is never used 2018-03-22 18:29:00 +01:00
Daniel García Aubert
79955c7fac Pass only needed params to tile backend 2018-03-22 18:27:40 +01:00
Daniel García Aubert
d3cbd70054 Pass only needed params to attributes backend backend 2018-03-22 18:16:41 +01:00
Daniel García Aubert
81706b8726 Pass only needed params to dataview backend (search) 2018-03-22 18:03:38 +01:00
Daniel García Aubert
2812a54210 Pass only needed params to dataview backend 2018-03-22 17:55:15 +01:00
Daniel García Aubert
258d768887 Use upercase for constants 2018-03-22 17:54:40 +01:00
Daniel García Aubert
1059066c05 Use module to get database parameters 2018-03-22 17:53:24 +01:00
Daniel García Aubert
875f3c07b3 Pass only needed params to MapStoreMapConfigProvider 2018-03-22 17:07:38 +01:00
Daniel García Aubert
8ce72ea842 Do not pass res.locals to collaborators 2018-03-22 12:30:51 +01:00
Daniel García Aubert
e542d38ec7 Reorder middleware 2018-03-22 11:38:33 +01:00
Daniel
771eaf97c8 Merge pull request #909 from CartoDB/spread-prepare-context-middleware
Spread `prepareContext` middleware
2018-03-22 10:48:37 +01:00
Daniel García Aubert
b40ed13f47 Do not use step to deal with asyn code 2018-03-21 19:08:37 +01:00
Javier Goizueta
b9de49d5ab Remove superfluous aggregation filter condition
The default aggregation doesn't admit filters, so this wasn't necessary.
2018-03-21 17:36:26 +01:00
Javier Goizueta
ead6fa5f1f Document aggregation filters
Note that dimension filters remain undocumented
2018-03-21 17:21:05 +01:00
Javier Goizueta
6ada8ba6a2 Implement aggregation filters 2018-03-21 17:01:32 +01:00
Daniel García Aubert
672b19b106 Magic number 2018-03-21 16:48:21 +01:00
Daniel García Aubert
4a2580c9ea Missing semicolon 2018-03-21 16:43:34 +01:00
Daniel García Aubert
52c8c9341a Remove function defined but never used 2018-03-21 16:40:09 +01:00
Daniel García Aubert
72c4a7abd6 Extract cache control header middleware 2018-03-21 16:38:37 +01:00
Daniel García Aubert
d022a1fa5e Extract last-modified header middlleware 2018-03-21 14:43:00 +01:00
Daniel García Aubert
a142620b70 Make generic middlewares to calculate surrogate key and cache channel headers:
- In controllers: all reference to map config are now camelized, for instance: mapconfig -> mapConfig or mapconfigProvider -> mapConfigProvider
 - In controllers: all map config providers created in req/res cycle are saved into `res.locals` and `mapConfigProvider` as key.
 - In map-config-providers: all of them implement `.getAffectedTables()`, in order to calculate the tables involved for a given map-config. For that, `pgConnection` and `affectedTablesCache` are injected as constructor argument.
 - Named Map Provider: rename references from `affectedTablesAndLastUpdate` to `affectedTables`.
 - Named Map Provider Cache: In order to create new named map providers, needs affectedTablesCache.
 - Extract locals middlewares (surrogate-key and cache-channel) from controllers and create an unified version of them.
 - Extract last-modified middleware from named maps controller (draft).
2018-03-21 14:11:54 +01:00
Simon Martín
5603336253 stubs next version 2018-03-20 14:51:37 +01:00
Raúl Marín
838a3464c1 Merge pull request #911 from Algunenano/master_pg11
PG11: Modify regex match to accept pg11 errors
2018-03-20 11:47:14 +01:00
Raul Marin
0906ae3c93 PG11: Modify regex match to accept pg11 errors 2018-03-20 11:34:30 +01:00
Daniel García Aubert
af8ed99ae7 Merge branch 'spread-prepare-context-middleware' into unify-middlewares 2018-03-20 11:09:32 +01:00
Daniel García Aubert
f8d1e159f4 Please jshint 2018-03-20 11:09:05 +01:00
Daniel García Aubert
df999e040c Merge branch 'spread-prepare-context-middleware' into unify-middlewares 2018-03-20 11:06:31 +01:00
Daniel García Aubert
2e13bc42a1 Merge branch 'master' into spread-prepare-context-middleware 2018-03-20 11:01:13 +01:00
Daniel García Aubert
9fd2519c12 Rename middleware 2018-03-20 09:34:50 +01:00
Daniel García Aubert
325bdfe92f Move middleware 2018-03-20 09:34:06 +01:00
Daniel García Aubert
9211fa065b Extract sendResponse middleware 2018-03-19 19:48:14 +01:00
Daniel García Aubert
bb170ee208 Please, jshint 2018-03-19 19:27:38 +01:00
Daniel García Aubert
8333b39928 Use res.body as placeholder of layergroup 2018-03-19 19:16:18 +01:00
Simon Martín
fefff3b788 version 6.0.0 2018-03-19 15:38:31 +01:00
Simon Martín
d5e985fde5 Merge pull request #875 from CartoDB/rateLimits
Rate limit
2018-03-19 15:36:26 +01:00
Simon Martín
ed27f980c2 NEWS 2018-03-19 15:30:47 +01:00
Simon Martín
b6afad1787 upgrading cartodb-redis 2018-03-19 15:19:08 +01:00
Simon Martín
7807ea5f8c updating docker image 2018-03-19 15:15:38 +01:00
Daniel García Aubert
d463d35906 Remove mocha filter 2018-03-19 14:56:20 +01:00
Daniel García Aubert
5cde325d9a Fix ported test related to cache_buster from layergroup token 2018-03-19 13:51:03 +01:00
Daniel García Aubert
b038763b7b Fix skipped test, port is configured via app configuration. It's no longer configurable via query-params 2018-03-19 12:48:52 +01:00
Daniel García Aubert
aa448a8c2e Remove NO PORTED TEST makefile option 2018-03-19 12:43:47 +01:00
Simon Martín
a830eb4ea0 Merge branch 'master' into rateLimits 2018-03-19 11:25:13 +01:00
Daniel García Aubert
73397ab500 Typo 2018-03-16 20:04:39 +01:00
Daniel García Aubert
a7f6eafd5c Remove unused porperties from ported server options 2018-03-16 20:04:29 +01:00
Daniel García Aubert
ed9083de24 Remove req2params middleware from ported server options 2018-03-16 20:02:18 +01:00
Daniel García Aubert
a2fa92abf1 Fix ported test ny adding host header to the request 2018-03-16 19:58:29 +01:00
Daniel García Aubert
fa1e1fd779 Fix ported test by adding host header to the requests 2018-03-16 19:56:34 +01:00
Daniel García Aubert
adde66bc57 Fix more ported test 2018-03-16 18:59:07 +01:00
Daniel García Aubert
db08fc3da2 Remove meaningless test 2018-03-16 18:58:42 +01:00
Daniel García Aubert
97e603b215 Fix ported test 2018-03-16 18:33:47 +01:00
Daniel García Aubert
29936d76b1 Fix ported test by adding host header to request 2018-03-16 18:18:06 +01:00
Daniel García Aubert
3f88aaae64 Fix ported test by adding host header to requests 2018-03-16 18:15:37 +01:00
Daniel García Aubert
fccf46c67d Add host header to test-client (ported) 2018-03-16 18:06:31 +01:00
Daniel García Aubert
970aca1c9d Add host header to test_client (ported) 2018-03-16 17:55:04 +01:00
Daniel García Aubert
5e494f0982 Add host headers to attributes test (ported) 2018-03-16 17:51:48 +01:00
Daniel García Aubert
91a7dc8cf0 Use const 2018-03-16 17:31:40 +01:00
Daniel García Aubert
e52cd28f1e User res.body as placeholder of the response's body 2018-03-16 17:13:48 +01:00
Daniel García Aubert
7bdbd4cb03 Move variable declaration 2018-03-16 16:42:47 +01:00
Daniel García Aubert
95d694f6c5 Fix unit test 2018-03-16 16:29:00 +01:00
Daniel García Aubert
313fc75ec8 Rename middleware 2018-03-16 16:28:50 +01:00
Daniel García Aubert
639a69a639 Reorder middlewares to optimize workflow 2018-03-16 16:12:36 +01:00
Raúl Marín
a4ec3ad6da Merge pull request #908 from CartoDB/master_analyses_fixes
Update camshaft to 0.61.8
2018-03-16 15:44:10 +01:00
Raul Marin
b7c10c95d3 Update camshaft to 0.61.8 2018-03-16 15:39:20 +01:00
Daniel García Aubert
67d2d2fe95 Use spread operator 2018-03-16 14:20:41 +01:00
Daniel García Aubert
0aa8d63a6e Unifiy allowQueryParams and cleanUpQueryParams middlewares 2018-03-16 14:03:59 +01:00
Daniel García Aubert
7b11cdcb74 Use template string 2018-03-16 13:08:00 +01:00
Daniel García Aubert
071a5a4bdf Rename base paths 2018-03-16 13:04:42 +01:00
Daniel García Aubert
5ede6c3021 Add test suite to not run ported test 2018-03-15 19:38:39 +01:00
Daniel García Aubert
7ff7b0c2d1 Extract rest of the middlewares from prepare contex 2018-03-15 19:38:11 +01:00
Daniel García Aubert
30dab7df9f Extract authorize middeware form prepareContext 2018-03-15 18:48:29 +01:00
Daniel García Aubert
afff06c7e6 Extract db-conn-setup middleware for prepare-context 2018-03-15 15:33:20 +01:00
Simon Martín
05ef43c342 download redis cell with curl and change path 2018-03-15 12:19:06 +01:00
Rafa de la Torre
c235754df2 Stub next version 5.4.1 2018-03-15 12:12:37 +01:00
Rafa de la Torre
70dab149ba Update release date in NEWS.md 2018-03-15 12:11:03 +01:00
Rafa de la Torre
ed4b44a78a Merge pull request #905 from CartoDB/mapnik-3.6.2-carto.4
Upgrade @carto/mapnik to 3.6.2-carto.4
2018-03-15 12:08:32 +01:00
Simon Martín
54f113ab5f download redis-cell lib during tests 2018-03-15 11:35:38 +01:00
Simon Martín
60bf81d950 removing redis-cell file 2018-03-15 11:35:00 +01:00
Simon Martín
bdfd58f468 removing basic redis cell tests file 2018-03-15 11:26:29 +01:00
Rafa de la Torre
de1aaf3808 Update image because of new mapnik cache 2018-03-15 09:55:07 +01:00
Rafa de la Torre
769aee1107 Do not use the @github: notation in yarn.lock
The little advantage is that from a clean dir a `yarn install` won't
modify the `yarn.lock` file.
2018-03-15 09:36:35 +01:00
Rafa de la Torre
3663e6d12a More accurate msg in NEWS.md 2018-03-15 09:33:23 +01:00
Rafa de la Torre
a6d9984453 Upgrade @carto/mapnik to 3.6.2-carto.4 2018-03-15 09:29:08 +01:00
Simon Martín
4e8cf136c8 rate limit middleware before auth 2018-03-14 18:06:06 +01:00
Daniel
f49d7478d7 Merge pull request #899 from CartoDB/refactor-named-maps-admin
Named Maps Admin: Extract middlewares form controller's context
2018-03-14 17:55:03 +01:00
Daniel
b4a1c9d648 Merge pull request #898 from CartoDB/refactor-analysis-controller
Refactor analysis controller
2018-03-14 17:50:15 +01:00
Daniel
692246ec44 Merge pull request #901 from CartoDB/refactor-named-maps-controller
Refactor named maps controller
2018-03-14 17:50:06 +01:00
Simon Martín
48a7d28aa6 rate limit analysis catalog endpoint 2018-03-14 17:46:19 +01:00
Daniel García Aubert
04146f897d Use template strings 2018-03-14 17:33:54 +01:00
Daniel García Aubert
a34658c97f Use objects instead of param list 2018-03-14 17:31:37 +01:00
Simon Martín
cbfeb0158e adding type and subtype to rate limit error 2018-03-14 17:27:59 +01:00
Daniel García Aubert
8d37e00869 Use objects instead of parameter list 2018-03-14 17:25:58 +01:00
Raúl Marín
584d6ae9cf Merge pull request #902 from CartoDB/camshaft0614
Update camshaft to 0.61.4
2018-03-14 17:25:25 +01:00
Daniel García Aubert
421e611356 Use objects instead of a list of parameters 2018-03-14 17:22:47 +01:00
Daniel García Aubert
f078713d28 typo 2018-03-14 17:15:50 +01:00
Daniel García Aubert
a8d31d52cf Pass only required params 2018-03-14 17:08:04 +01:00
Raul Marin
d9213b2fe2 Update camshaft to 0.61.4 2018-03-14 15:56:35 +01:00
Daniel García Aubert
091efe52fc Extract sendResponse middleware from context 2018-03-14 13:32:43 +01:00
Daniel García Aubert
a5c508733a Extract setContentTypeHeader middleware from controller's context 2018-03-14 13:31:39 +01:00
Daniel García Aubert
ce944d9a7d Extract setCacheControlHeader from controller's context 2018-03-14 13:30:27 +01:00
Daniel García Aubert
8321b5adba Extract setLastModifiedHeader from controller's context 2018-03-14 13:27:56 +01:00
Daniel García Aubert
667c972308 Extract setCacheChannelHeader and setSurrogateKeyHeader from controller's context 2018-03-14 13:25:42 +01:00
Daniel García Aubert
3dbe05be3a Extarct setCacheChannelHeader middleware from controller's context 2018-03-14 13:19:56 +01:00
Daniel García Aubert
2d4ce19250 Extract incrementMapViews middleware from controllers middleware 2018-03-14 13:18:37 +01:00
Daniel García Aubert
3b3e0c0acd Extract getImage middleware from controller's context 2018-03-14 13:15:38 +01:00
Daniel García Aubert
c3ddb933bb Extract getStaticImageOptions middleware from controller's context 2018-03-14 13:11:17 +01:00
Daniel García Aubert
6aae60ece7 Extract getTile middleware from controller's context 2018-03-14 13:07:40 +01:00
Daniel García Aubert
6b3dc8ece0 Extract prepareLayerFilterFromPreviewLayers middleware from controller's context 2018-03-14 13:05:05 +01:00
Daniel García Aubert
7dd231a8c9 Extract getTemplate middleware form controller's context 2018-03-14 13:02:49 +01:00
Daniel García Aubert
35a3219012 Extract getAffectedTables middleware from controller's context 2018-03-14 13:01:07 +01:00
Daniel García Aubert
7598e6ab4b Extract getNamedMapProvider middleware from controller's context 2018-03-14 12:58:56 +01:00
Simon Martín
bbedc5f41b Updating headers on tests 2018-03-14 12:15:07 +01:00
Simon Martín
e1a2c45b19 Headers following rfc6648 2018-03-14 12:09:20 +01:00
Simon Martín
8fa801e032 updating config parameters 2018-03-14 11:34:59 +01:00
Raúl Marín
75870dc6c1 Merge pull request #900 from Algunenano/windshaft457
Update Windshaft to 4.5.7
2018-03-14 10:39:05 +01:00
Raul Marin
42900b5d0e Update Windshaft to 4.5.7 2018-03-14 10:28:34 +01:00
Simon Martín
c1423d77ff yarn lock after master merge 2018-03-13 18:10:27 +01:00
Simon Martín
0e43c54214 Merge branch 'master' into rateLimits 2018-03-13 18:09:44 +01:00
Daniel García Aubert
2cc4161239 Missing semicolon 2018-03-13 15:02:44 +01:00
Daniel García Aubert
fc8f3fdf27 Create send response middleware 2018-03-13 13:31:49 +01:00
Daniel García Aubert
24b76208ac Improve naming 2018-03-13 13:21:40 +01:00
Daniel García Aubert
0de272b195 Extract middlewares form controller's context 2018-03-13 13:12:18 +01:00
Daniel
7faf40004c Merge pull request #891 from CartoDB/refactor-map-controler
Refactor map controler
2018-03-13 12:44:53 +01:00
Daniel García Aubert
88ae2d473a Typo 2018-03-13 11:43:08 +01:00
Daniel García Aubert
337b47685c Remove controler context from middlewares 2018-03-13 11:42:25 +01:00
Raúl Marín
ed3f9be655 Merge pull request #897 from Algunenano/windshaft456
Update Windshaft to 4.5.6
2018-03-12 19:21:09 +01:00
Raul Marin
248c6d5f22 Update Windshaft to 4.5.6 2018-03-12 19:13:56 +01:00
Raúl Marín
dd4aa09d21 Merge pull request #896 from Algunenano/master_camshaft0613
Update camshaft to 0.61.3
2018-03-12 17:02:25 +01:00
Raul Marin
132f2226ca Update camshaft to 0.61.3 2018-03-12 16:52:43 +01:00
Daniel
2eb6e95fed Merge pull request #895 from CartoDB/fix-performance-regression
Fix performance regression
2018-03-12 15:48:20 +01:00
Daniel García Aubert
db8130be4f Honor jshint 2018-03-12 14:18:07 +01:00
Daniel García Aubert
379b649e95 Don't get mapconfig prematurely 2018-03-12 13:29:40 +01:00
Raúl Marín
e5619492ef Merge pull request #894 from Algunenano/master_windshaft455
Update Windshaft to 4.5.5
2018-03-12 13:23:15 +01:00
Daniel García Aubert
cc76ccc626 Fix undeclared variable 2018-03-12 13:10:20 +01:00
Raul Marin
3d6512dd11 Update Windshaft to 4.5.5 2018-03-12 13:01:17 +01:00
Eneko Lakasta
b19d97e01f Merge pull request #886 from CartoDB/vary-header
Vary header
2018-03-12 12:46:42 +01:00
Daniel García Aubert
25931a618b Do not calculate affected tables when there are no affacted tables 2018-03-12 12:33:29 +01:00
Eneko Lakasta
ffab576399 Merge branch 'master' into vary-header 2018-03-12 12:31:23 +01:00
Raúl Marín
e7067ab9cf Merge pull request #893 from Algunenano/master_request_update
Update request to 2.85.0
2018-03-12 12:25:19 +01:00
Eneko Lakasta
7cfcf6d579 merge master 2018-03-12 12:16:55 +01:00
Raul Marin
a4b586055a Update request to 2.85.0 2018-03-12 12:16:29 +01:00
Eneko Lakasta
5ad1e1b645 merge master 2018-03-12 11:52:38 +01:00
Daniel García Aubert
01ed513a79 Use 'const' 2018-03-09 17:02:13 +01:00
Daniel García Aubert
504f68b8aa Missing semicolon 2018-03-09 16:18:33 +01:00
Daniel García Aubert
cbb08f5642 Extract function 2018-03-09 15:49:03 +01:00
Raúl Marín
a4b5d681ce Merge pull request #890 from Algunenano/windshaft_454
Update Windshaft to 4.5.4
2018-03-09 13:12:37 +01:00
Daniel García Aubert
02f93f3a14 Extract layergroup-metadata class 2018-03-09 12:58:05 +01:00
Raul Marin
ad2f4573f8 Update Windshaft to 4.5.4 2018-03-09 11:44:42 +01:00
Raúl Marín
06604cd738 Merge pull request #888 from Algunenano/support_1368
Aggregation count: Do not return null categories
2018-03-09 11:06:40 +01:00
Raul Marin
089be35b5d Aggregation count: Do not return null categories 2018-03-08 18:13:20 +01:00
Daniel García Aubert
bbcb335d60 Merge branch 'master' into refactor-map-controler 2018-03-08 13:18:43 +01:00
Daniel
6ef2e0bb5f Merge pull request #887 from CartoDB/middlewarify-layergroup-controller
Middlewarify layergroup controller
2018-03-08 13:04:39 +01:00
Daniel García Aubert
d8202d881d Remove legacy test 2018-03-08 12:46:04 +01:00
Daniel García Aubert
aae814a156 Use template strings 2018-03-08 12:35:54 +01:00
Daniel García Aubert
49bcc5368d Use base number as radix to pare intergers 2018-03-08 12:30:27 +01:00
Daniel García Aubert
555e04f9e7 Use ternary operator 2018-03-08 12:27:49 +01:00
Daniel García Aubert
3f6f2e4e23 Use template string 2018-03-08 12:23:43 +01:00
Daniel García Aubert
abffc4b067 Uppercase for actual constants 2018-03-08 12:23:00 +01:00
Daniel García Aubert
363cb0b679 Extract middlewares from map-controller class 2018-03-08 12:16:24 +01:00
Daniel García Aubert
d26910ba9c Extract checkJsonContentType middleware from MapController class 2018-03-07 19:11:03 +01:00
Daniel García Aubert
74b2f305ea Extract initProfiler middleware from map-controller 2018-03-07 19:09:52 +01:00
Daniel García Aubert
6c2f893651 Rename map-store-map-config-provider middleware 2018-03-07 18:53:20 +01:00
Daniel García Aubert
faaf121eb6 Rename center and bbox middlewares 2018-03-07 18:51:43 +01:00
Daniel García Aubert
83ab65163d Rename attributes middleware 2018-03-07 18:43:35 +01:00
Daniel García Aubert
9dcd5ff332 Impreve naming 2018-03-07 15:56:16 +01:00
Daniel García Aubert
c6635f63c1 Unify layer and tile middlewares 2018-03-07 15:39:59 +01:00
Daniel García Aubert
56213219e4 Rename middleware 2018-03-07 15:25:30 +01:00
Daniel García Aubert
7c2dc20dbe Merge branch 'master' into middlewarify-layergroup-controller 2018-03-07 15:24:34 +01:00
Daniel García Aubert
c8e8317ea4 Do not attach middleware to LayergroupController classs 2018-03-07 15:20:47 +01:00
Daniel
8509796743 Merge pull request #882 from CartoDB/middleware-refactor
Middleware refactor
2018-03-07 15:19:04 +01:00
Daniel García Aubert
90aaed0f2c Typo 2018-03-07 15:05:36 +01:00
Daniel García Aubert
48be15b742 Use const in favour of var 2018-03-07 15:01:04 +01:00
Daniel García Aubert
a95b3f2f99 Fix comment 2018-03-07 14:54:09 +01:00
Daniel García Aubert
b2cc7ab84f Move functions to improve readablity 2018-03-07 14:53:13 +01:00
Daniel García Aubert
eb3414f07f Follow middleware pattern 2018-03-07 14:48:21 +01:00
Daniel García Aubert
292dad130d Move middlewares to the right place 2018-03-07 14:42:21 +01:00
Daniel García Aubert
ec41cddb19 Do not pass the whole res.locals to backends 2018-03-07 12:52:44 +01:00
Daniel García Aubert
5871f8290d Use default param values 2018-03-07 12:46:18 +01:00
Daniel García Aubert
33089be2cd Do not attach header middlewares to node status endpoint 2018-03-07 12:30:59 +01:00
Daniel García Aubert
d351c8d14c Define var as const 2018-03-07 12:09:41 +01:00
Daniel García Aubert
82446e5ffa Use template string to define routes 2018-03-07 12:05:53 +01:00
Daniel García Aubert
b786164e8a Middlewarify metrics increment whether success or error 2018-03-07 11:56:57 +01:00
Eneko Lakasta
f9cbb3aac8 use assert.equal instead of assert.ok 2018-03-07 10:51:17 +01:00
Daniel García Aubert
a66c19c6c7 Do not bind context when unneeded 2018-03-06 20:05:55 +01:00
Daniel García Aubert
94d1667d70 Refactor affected tables 2018-03-06 20:01:43 +01:00
Daniel García Aubert
3399db1cff Add comment 2018-03-06 18:58:09 +01:00
Daniel García Aubert
874ea99d19 Remove step 2018-03-06 18:43:23 +01:00
Daniel García Aubert
7022fb87b4 Extract header, affected-tables and response middlewares 2018-03-06 18:28:40 +01:00
Daniel García Aubert
7c1e2a6af0 Avoid nested steps 2018-03-06 17:08:39 +01:00
Daniel García Aubert
2f011c3266 Remove nested steps 2018-03-06 17:01:51 +01:00
Daniel García Aubert
4762aa0897 Remove step from sendResponse function 2018-03-06 16:55:27 +01:00
Daniel García Aubert
f30f83331f Extract tile error middleware 2018-03-06 16:44:37 +01:00
Daniel García Aubert
3695e1e3e5 Place function closer to where is called 2018-03-06 16:21:46 +01:00
Daniel García Aubert
585b5929aa Middlewarify tile and layer endpoints 2018-03-06 16:19:53 +01:00
Eneko Lakasta
0185cdf785 please jshint 2018-03-06 15:34:29 +01:00
Eneko Lakasta
8d22ca66ba fix tests 2018-03-06 15:26:35 +01:00
Eneko Lakasta
b0eacb2a79 add vary header to honor authorization header when caching 2018-03-06 12:46:38 +01:00
Daniel García Aubert
9b40370794 Now that mapConfigProvider is linked to 'res.locals' do not pass the whole 'res.locals' to map-config-provider to avoid converting circular structure to JSON 2018-03-06 12:44:17 +01:00
Daniel García Aubert
95f3d58383 Make jshint happy 2018-03-05 19:33:46 +01:00
Daniel García Aubert
0f0cde1093 Middlewarify static-api (bbox/center) endpoints 2018-03-05 19:26:26 +01:00
Simon Martín
e679366dac upgrading redis-cell to v2.2 2018-03-05 18:44:55 +01:00
Daniel García Aubert
ca56df5cfe Middlewarify attributes endpoint 2018-03-05 18:28:52 +01:00
Daniel García Aubert
d8a4209768 Middlewarify analysis-node-status endpoint 2018-03-05 18:13:19 +01:00
Daniel García Aubert
40712a2e62 Middlewarify search dataview endpoint 2018-03-05 18:05:42 +01:00
Daniel García Aubert
acb9ce33b1 Pass dataview-backend as middleware option 2018-03-05 18:04:50 +01:00
Daniel García Aubert
5e43a7145a Middlewarify dataview endpoint 2018-03-05 17:44:04 +01:00
Daniel
39bd6694f2 Merge pull request #883 from CartoDB/fix-named-map-format
Do not force format for default named tiles
2018-03-05 11:29:43 +01:00
Daniel García Aubert
5de8c4f9c3 Make explicit that forceFormat is optional 2018-03-05 11:19:17 +01:00
Simon Martín
31a554d94f updating tests to use cartodb-redis 2018-03-03 14:46:58 +01:00
Simon Martín
9bc9fc46ff using cartodb-redis for rate limit 2018-03-03 14:46:39 +01:00
Simon Martín
4274e06795 updating cartodb-redis 2018-03-03 14:45:55 +01:00
Simon Martín
a2bf235553 Merge branch 'master' into rateLimits 2018-03-02 21:21:10 +01:00
Simon Martín
1b18b2b188 changing endpointGroup var name 2018-03-02 20:55:50 +01:00
Simon Martín
9c27447b17 refactoring rate limit tests 2018-03-02 20:52:06 +01:00
Daniel García Aubert
f03d98cd0d Going green: fix test 2018-03-02 18:37:13 +01:00
Daniel García Aubert
6331bebb30 Going green: be able to request defaul named tiles in vector format 2018-03-02 18:25:23 +01:00
Daniel García Aubert
fdd4c4aaa0 Going red: get default named map vector tile 2018-03-02 18:22:53 +01:00
Simon Martín
4dd404771e refactoring user_limits_api 2018-03-02 16:07:42 +01:00
Simon Martín
bf267e9c95 fix checking rate limit enabled 2018-03-02 15:48:31 +01:00
Simon Martín
843f70cdba interchange var and middlewware names 2018-03-02 15:35:38 +01:00
Simon Martín
42e0e07c14 refactoring rate limit middleware 2018-03-02 15:33:50 +01:00
Simon Martín
dfdd2b9043 adding middlewares to composer in maps controller 2018-03-02 15:24:18 +01:00
Daniel García Aubert
8656fcd8d1 Use 'const' 2018-03-02 14:04:29 +01:00
Daniel García Aubert
f2f6b9d49c ES6 goodies 2018-03-02 13:29:30 +01:00
Daniel García Aubert
82f1e6753b Remove unreachable code 2018-03-02 13:14:02 +01:00
Daniel García Aubert
7ed717607a Missing space before paramenter list 2018-03-02 13:08:57 +01:00
Daniel García Aubert
0ec9491d21 Fix test: Add stub for profiling 2018-03-02 11:16:46 +01:00
Daniel García Aubert
416970c819 Remove empty line 2018-03-01 19:10:35 +01:00
Daniel García Aubert
ccc28f3617 Add profiler step to lzma 2018-03-01 19:09:11 +01:00
Daniel García Aubert
5bac36b30f Remove bad profiler usage 2018-03-01 18:53:05 +01:00
Daniel García Aubert
ef3ffddec7 Cosmetic changes 2018-03-01 18:49:44 +01:00
Daniel García Aubert
e6ba467d98 ES6 goodies 2018-03-01 18:47:07 +01:00
Daniel García Aubert
314508bcd8 Middleware naming convention 2018-03-01 18:46:04 +01:00
Daniel García Aubert
da18506e41 Follow middleware factory pattern 2018-03-01 18:45:04 +01:00
Daniel García Aubert
5eaee0b71e Follow middleware naming convention 2018-03-01 18:12:07 +01:00
Daniel García Aubert
bd93e7dc7e Follow middleware pattern 2018-03-01 18:09:49 +01:00
Daniel García Aubert
2c762813ba Follow middleware pattern, return a function as the actual middleware 2018-03-01 15:52:48 +01:00
Simon Martín
136c6fa70b new redis keys 2018-03-01 15:50:40 +01:00
Simon Martín
67b2343571 fix existing var name 2018-03-01 15:50:20 +01:00
Daniel García Aubert
3caa1d9c4a ES6 cosmetics 2018-03-01 15:42:46 +01:00
Daniel García Aubert
b0c924ca03 Follow middleware pattern, should return a function as the actual middleware 2018-03-01 15:42:03 +01:00
Daniel García Aubert
f6f59023b4 Ungroup middlewares 2018-02-28 19:46:46 +01:00
Daniel García Aubert
9dc4e7c955 Use the right step name for profiling 2018-02-28 19:29:53 +01:00
Daniel García Aubert
faa44e54ae Cosmetic changes 2018-02-28 19:29:10 +01:00
Daniel García Aubert
bfb743b851 Improve profiling steps 2018-02-28 19:27:49 +01:00
Daniel García Aubert
dad2e92dd3 Follow middleware naming convention 2018-02-28 19:26:47 +01:00
Daniel García Aubert
59c312ea40 Require modules at the beginning of module 2018-02-28 19:25:50 +01:00
Daniel García Aubert
48c5a458f3 Remove bad use of profiling step 2018-02-28 19:22:22 +01:00
Daniel García Aubert
c0830862c8 Follow middleware naming convention 2018-02-28 19:21:44 +01:00
Daniel García Aubert
42deb7abbe Rename middleware 2018-02-28 19:20:51 +01:00
Daniel García Aubert
62deda6470 Improve naming 2018-02-28 19:13:49 +01:00
Daniel García Aubert
3e0981978a Update NEWS 2018-02-28 17:31:03 +01:00
Eneko Lakasta
35e5170907 Merge pull request #877 from CartoDB/project-auth-api
Auth API
2018-02-28 17:20:21 +01:00
Eneko Lakasta
8eba5dcc01 update cartodb-redis to 0.16.0 2018-02-28 17:07:31 +01:00
Daniel García Aubert
5c2248d419 Merge branch 'master' into project-auth-api 2018-02-28 14:43:12 +01:00
Simon Martín
335d91b42d separate rate limit logic from middleware and EVALSHA 2018-02-28 13:11:18 +01:00
Daniel García Aubert
102b11b1b5 Follow middleware naming convention 2018-02-28 13:10:46 +01:00
Eneko Lakasta
26df09b13f require debug at the top of file 2018-02-28 11:42:44 +01:00
Simon Martín
254991c56c changing endpoint groups constant keys 2018-02-27 17:57:25 +01:00
Simon Martín
a492ab0143 improving naming 2018-02-27 17:36:03 +01:00
Simon Martín
b0d63b2ec0 changing module exports and middleware name 2018-02-27 16:52:27 +01:00
Simon Martín
85b0c63eb0 destructuring array assignment 2018-02-27 16:24:30 +01:00
Simon Martín
98a92f51e6 make jshint happy 2018-02-26 17:17:01 +01:00
Simon Martín
ae50dbd47c updating test to own redis-cell 2018-02-26 17:03:56 +01:00
Simon Martín
a97e628520 using own version of redis-cell 2018-02-26 16:27:07 +01:00
Simon Martín
b48dcc1418 moving tests to right folder type 2018-02-26 16:23:42 +01:00
Eneko Lakasta
8867cdbc02 use anonymous function instead of arrow function in middleware export to don't bind this 2018-02-26 15:57:42 +01:00
Simon Martín
f03ee4b836 removing hardcoded user 2018-02-26 11:20:31 +01:00
Simon Martín
90418b204e getLowerRateLimit tests 2018-02-26 10:51:51 +01:00
Simon Martín
918674e01a changing endpoint groups names 2018-02-23 21:09:27 +01:00
Simon Martín
7b44b7d559 removing only 2018-02-23 21:09:03 +01:00
Simon Martín
612b11cbe8 rate limit tests ensuring the smaller limit 2018-02-23 21:07:25 +01:00
Simon Martín
b34f05690c updating rate limit tests supporting multiple limits 2018-02-23 17:42:54 +01:00
Simon Martín
9b01a05727 rate limit supporting multiple limits 2018-02-23 17:42:24 +01:00
Simon Martín
d0024409df removing tests of redis-cell 2018-02-23 16:23:59 +01:00
Simon Martín
91856372f0 adding redis-cell test file. Remove it before PR merge 2018-02-23 16:16:34 +01:00
Simon Martín
2937b6a804 updating redis keys 2018-02-21 17:25:26 +01:00
Simon Martín
b76a8249fa fix overwritten environment params 2018-02-21 11:37:20 +01:00
Simon Martín
db09476137 adding test_helper to have environment params 2018-02-21 11:35:41 +01:00
Simon Martín
08a5e57180 new config params and using them in middleware 2018-02-20 18:18:15 +01:00
Simon Martín
7464d827fe using the new config params 2018-02-20 18:17:25 +01:00
Simon Martín
ae0ec159e1 changing config params to more semantics 2018-02-20 18:16:51 +01:00
Simon Martín
2a1c08da65 ensuring configuration params in tests 2018-02-20 17:58:08 +01:00
Simon Martín
faab174a79 redis keys in easier way 2018-02-20 17:19:50 +01:00
Eneko Lakasta
521b441da5 default apikey is returned by metadata module if no apikey found, remove this code because is never going to be run 2018-02-20 12:53:33 +01:00
Eneko Lakasta
59ca00b33b move apikey credentials getter to middleware file 2018-02-20 12:31:36 +01:00
Simon Martín
6564ed69d8 config params 2018-02-20 11:46:38 +01:00
Simon Martín
fadd9032c6 arrow functions clean 2018-02-20 11:38:44 +01:00
Simon Martín
80918f5b9b adding rate limit middlewware to named maps controller 2018-02-20 11:26:24 +01:00
Simon Martín
e061b3e631 adding rate limit middlewware to maps controller 2018-02-20 11:26:09 +01:00
Simon Martín
06ec3f80b9 adding rate limit middlewware to named maps admin controller 2018-02-20 11:25:56 +01:00
Simon Martín
e6011287f4 adding middleware to layergroup controller 2018-02-20 11:25:16 +01:00
Simon Martín
a0f560ca1a rate limits acceptance tests 2018-02-20 10:57:29 +01:00
Daniel García Aubert
7c7d606aa7 Remove trailing spaces 2018-02-19 19:05:13 +01:00
Daniel
46587e3cf1 Merge pull request #874 from CartoDB/project-auth-api-fallback
add fallback for using metadata fallback
2018-02-19 18:55:24 +01:00
Daniel García Aubert
603ef4044c Reduce cyclomatic complexity 2018-02-19 18:48:02 +01:00
Daniel García Aubert
2e3abfb2cd Catch "name not found" errors from metadata backend and set http code status 404 2018-02-19 18:28:58 +01:00
Daniel García Aubert
47ccb7ded8 Point to main development branch of cartodb-redis 2018-02-19 17:57:46 +01:00
Simon Martín
98907a886c using a real endpoint in test 2018-02-19 17:08:26 +01:00
Eneko Lakasta
7e14247ea9 remove cause of unreachable code/dead code. Not necessary because carto redis assures at least the default api key 2018-02-19 17:06:59 +01:00
Simon Martín
e103427750 removing forgotten .only 2018-02-19 17:06:00 +01:00
Simon Martín
95f55b00b3 using exported const in tests 2018-02-19 17:03:03 +01:00
Simon Martín
e519984790 changing const names and exporting them 2018-02-19 17:01:41 +01:00
Simon Martín
fa3223777f using new docker image with redis4 and redis-cell 2018-02-19 12:43:36 +01:00
Simon Martín
eeb4966294 make jshint happy 2018-02-19 12:42:42 +01:00
Eneko Lakasta
5823859b2a update package.json to use the same branch form carto redis 2018-02-16 12:00:05 +01:00
Eneko Lakasta
7b21bd26d0 Merge branch 'project-auth-api' into project-auth-api-fallback 2018-02-16 11:29:36 +01:00
Eneko Lakasta
4ac224688c in fallback mode, use default api key if api key token doesnt exist 2018-02-16 11:20:04 +01:00
Simon Martín
4742e7f64f skiping some tests 2018-02-16 09:06:14 +01:00
Simon Martín
a66f127828 adding some tests 2018-02-15 20:06:34 +01:00
Eneko Lakasta
e84d88b7a3 remove test 2018-02-15 17:53:15 +01:00
Eneko Lakasta
cda2616a8a get and check api key credentials from api key: username and token 2018-02-15 17:49:47 +01:00
Eneko Lakasta
11aa4d12bd add tests for getting api key token from requests 2018-02-15 15:27:41 +01:00
Eneko Lakasta
18dbeea003 get apikey token from request in named maps admin middleware 2018-02-15 15:20:52 +01:00
Eneko Lakasta
3e916c6054 check if req.query exist before getting req.query.api_key/map_key 2018-02-15 15:20:05 +01:00
Eneko Lakasta
fc420c2c0f use for compatibility res.locals.api_key instead of res.locals.apikeyToken 2018-02-15 15:19:09 +01:00
Simon Martín
7b9d653c46 rate limit tests structure 2018-02-15 13:33:32 +01:00
Eneko Lakasta
140441b777 fix test 2018-02-15 12:53:01 +01:00
Eneko Lakasta
5db0e9c8d8 add middleware for apikeyToken 2018-02-15 12:50:42 +01:00
Simon Martín
63d1c19263 validation in lua script 2018-02-15 12:44:27 +01:00
Simon Martín
018cd25593 adding redis-cell to tests 2018-02-15 12:42:37 +01:00
Eneko Lakasta
963737d3fb create lib for getting api key token from request 2018-02-15 11:36:42 +01:00
Simon Martín
c059f44bf1 lua script and rate limit process 2018-02-14 18:39:57 +01:00
Eneko Lakasta
890f0d1ef6 add fallback for using metadata fallback 2018-02-14 17:31:05 +01:00
Simon Martín
5fca005a3f Merge branch 'master' into rateLimits 2018-02-14 15:40:50 +01:00
Simon Martín
86d4f8e219 lua script v1 2018-02-14 15:40:12 +01:00
Raul Marin
6b0ab45e63 Update Windshaft to 4.5.3 2018-02-13 12:09:59 +01:00
Rafa de la Torre
0b475ab5e2 Stub next version 2018-02-13 09:07:48 +01:00
Rafa de la Torre
97972ac73f Update NEWS.md with v5.3.1 2018-02-13 09:05:06 +01:00
Rafa de la Torre
c3b38b2f60 Merge pull request #865 from CartoDB/perf-boost-agg-dataview
Improve the speed of the aggregation dataview
2018-02-13 09:04:00 +01:00
Rafa de la Torre
b4e06ec1ac Update NEWS.md and stub version 2018-02-12 19:35:34 +01:00
Rafa de la Torre
d0a8bd428f Merge remote-tracking branch 'origin/master' into perf-boost-agg-dataview 2018-02-12 19:33:35 +01:00
Rafa de la Torre
251fe96509 Cosmetic fix, as suggested in PR 2018-02-12 19:24:53 +01:00
Simon Martín
15bf74f770 starting rate limit middleware 2018-02-12 17:19:08 +01:00
Eneko Lakasta
32986e3ebd Merge branch 'master' into project-auth-api 2018-02-12 12:25:46 +01:00
Eneko Lakasta
f106f27df4 Merge branch 'master' into project-auth-api 2018-02-12 12:21:09 +01:00
Raul Marin
3539b658fb Update Windshaft to 4.5.2 2018-02-12 12:19:36 +01:00
Javier Goizueta
abf33a1c68 Release 5.3.0 2018-02-12 12:03:02 +01:00
Javier Goizueta
fc6790ea1e Merge pull request #866 from CartoDB/update-camshaft-to-0.61.2
Update camshaft to 0.61.2
2018-02-12 11:56:31 +01:00
Raul Marin
ac6f0e1c67 Update Windshaft to 4.5.1 2018-02-12 11:35:46 +01:00
Eneko Lakasta
041cd40ec2 please jshint 2018-02-09 19:26:52 +01:00
Eneko Lakasta
8721f56269 add auth test for getting tiles 2018-02-09 19:19:18 +01:00
Eneko Lakasta
1d3045c799 add tests should create/fail creating named maps and regular api key 2018-02-09 12:33:33 +01:00
Daniel García Aubert
52a1ed869c Update to development version of cartodb-redis 2018-02-08 19:28:43 +01:00
Eneko Lakasta
04f60baec5 Set the master role inheritance from regular roles as TBA 2018-02-08 19:01:58 +01:00
Eneko Lakasta
a8de436424 add test should create a layergroup with a buffer analysis using a regular apikey token AND grant privileges to master and regular roles in bootstraping sql 2018-02-08 18:54:14 +01:00
Javier Goizueta
ee7917676b Update camshaft to 0.61.2
This is to fixe a bug in the line-sequential analyses (length was incorrect)
2018-02-08 17:00:13 +01:00
Eneko Lakasta
c7780e9f42 add tests should create a layergroup with a source analysis 2018-02-08 15:20:43 +01:00
Eneko Lakasta
3edd7b8b01 add test should create a layergroup with default apikey token 2018-02-08 14:50:17 +01:00
Eneko Lakasta
455202cd1a organize prepare db api keys 2018-02-08 14:49:42 +01:00
Eneko Lakasta
8bdb82c7be add test should fail creating a layergroup with default apikey token 2018-02-08 14:48:00 +01:00
Eneko Lakasta
fa503ee66a fix test typo 2018-02-08 14:43:12 +01:00
Eneko Lakasta
e1a2ee2381 control API access grants 2018-02-08 13:07:25 +01:00
Eneko Lakasta
b82d26527a remove comment 2018-02-08 12:35:44 +01:00
Eneko Lakasta
1c50dd6b48 add first tests for auth 2018-02-08 12:34:24 +01:00
Eneko Lakasta
b0e9df1400 add pgConnection.getDatabaseParams 2018-02-08 12:04:03 +01:00
Eneko Lakasta
6ebf51ce45 let select apikey type in setDBAuth: regular, default, master 2018-02-08 11:29:17 +01:00
Eneko Lakasta
d9a34f3384 add cartodb250user api keys to redis 2018-02-08 11:13:21 +01:00
Eneko Lakasta
8136a1e136 fix test 2018-02-07 19:12:26 +01:00
Eneko Lakasta
41f3606572 return unauthorized error when api key not found 2018-02-07 19:12:14 +01:00
Eneko Lakasta
ea0542dcb1 remove use of step 2018-02-07 18:48:59 +01:00
Eneko Lakasta
a4dbc1bac2 remove step and check existance of proper api key 2018-02-07 18:20:56 +01:00
Rafa de la Torre
065f56e161 Improve the speed of the aggregation dataview
Improve the performance of the aggregation dataview.

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

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

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

The improvement is about 20-40% in speed on PG10 with 4 cores.
2018-02-07 18:10:13 +01:00
Eneko Lakasta
6b5d6648de fix unit test 2018-02-07 17:14:46 +01:00
Eneko Lakasta
95538707c9 add parameter asMaster to setDBAuth 2018-02-07 17:14:13 +01:00
Eneko Lakasta
4c76a921b1 use res.locals instead of req.params 2018-02-07 16:02:13 +01:00
Eneko Lakasta
85c1c987af refactor setDBConn to not use step 2018-02-07 15:49:32 +01:00
Eneko Lakasta
bde86323fd use master api key in setDBAuth 2018-02-07 15:36:24 +01:00
Eneko Lakasta
880e3f388d remove use of _.extend calls 2018-02-07 12:46:10 +01:00
Eneko Lakasta
c1535b1a12 refactor setDBAuth to not use step 2018-02-07 12:40:36 +01:00
Eneko Lakasta
232ff1ba33 add apikeys keys to be removed after each test 2018-02-07 11:59:00 +01:00
Eneko Lakasta
1b63dcd4e5 add api keys to prepare db for testing 2018-02-07 11:10:50 +01:00
Raúl Marín
b32a0a6547 Merge pull request #864 from Algunenano/master_metrics
Add metrics option to Mapnik renderer (off by default)
2018-02-06 16:48:22 +01:00
Raul Marin
d634be0c30 Update NEWs and Windshaft to 4.5.0 2018-02-06 16:10:44 +01:00
Raul Marin
f9fe3ace37 Add mapnik metrics option 2018-02-06 16:07:22 +01:00
Simon Martín
6cd8131888 updating NEWS 2018-02-06 11:30:34 +01:00
Simon Martín
0ea76f7d15 Merge pull request #849 from CartoDB/redis4
Redis4 support
2018-02-06 11:24:02 +01:00
Simon Martín
51e5b5c255 upgrading cartodb-redis, redis-mpool and windshaft versions 2018-02-05 18:09:14 +01:00
Simon Martín
cedcc094e6 Merge branch 'master' into redis4 2018-02-05 12:42:59 +01:00
Javier Goizueta
bbe8d4e820 Stub next version 2018-02-01 16:39:43 +01:00
Javier Goizueta
5aa98c4ab2 Release 5.2.1 2018-02-01 16:37:05 +01:00
Javier Goizueta
d6a9103779 Merge pull request #861 from CartoDB/860-aggregation-min-res
Limit the minimum size of the the aggregation grid
2018-02-01 16:32:35 +01:00
Javier Goizueta
2e7784ddf2 Add comment to clafify aggregation resolution limit 2018-02-01 10:26:52 +01:00
Raul Marin
086be461b2 Stub next version 2018-02-01 09:33:18 +01:00
Raul Marin
a7157532f1 Release 5.2.0 2018-02-01 09:31:46 +01:00
Raúl Marín
55fd660d69 Merge pull request #859 from Algunenano/master_cache_features
Add Mapnik 'cache-features' option
2018-02-01 09:27:07 +01:00
Javier Goizueta
80604b739a Add test for aggregation with attributes
This reveals #860
2018-01-31 18:56:24 +01:00
Javier Goizueta
d88fbbaa87 Use camelCase 2018-01-31 18:55:28 +01:00
Javier Goizueta
7db0744f67 Simplify expression 2018-01-31 17:54:40 +01:00
Javier Goizueta
d1fcd797a3 Limit the minimum size of the the aggregation grid
Fixes #860
2018-01-31 17:46:13 +01:00
Raul Marin
150c6ee4be Update to Windshaft 5.3.3 2018-01-31 17:24:15 +01:00
Raul Marin
d0df8b1533 Update yarn.lock 2018-01-31 15:40:23 +01:00
Raul Marin
43e1de31fa Add Mapnik 'cache-features' option 2018-01-31 12:18:28 +01:00
Rafa de la Torre
33ed9ab47d Stub next version 2018-01-30 10:16:08 +01:00
Rafa de la Torre
749a08336a Merge pull request #857 from CartoDB/upgrade-to-mapnik-3.6.2-carto.2
Upgrade to node mapnik 3.6.2-carto.2
2018-01-30 10:13:15 +01:00
Rafa de la Torre
467097b3cc Merge remote-tracking branch 'origin/master' into upgrade-to-mapnik-3.6.2-carto.2 2018-01-30 10:07:01 +01:00
Javier Goizueta
487aca52d0 Stub next version 2018-01-29 18:25:14 +01:00
Rafa de la Torre
781d2d3a28 Update NEWS.md 2018-01-29 17:41:46 +01:00
Rafa de la Torre
2a767cdb83 Update yarn.lock 2018-01-29 17:40:49 +01:00
Rafa de la Torre
e3cf69ac1a Bump windshaft version
That version contains our flavor of mapnik 3.0.15 with a bunch of
patches. See
https://github.com/CartoDB/Windshaft/blob/master/NEWS.md#version-431
2018-01-29 17:40:17 +01:00
Simon Martín
2c703e5c16 updating node-redis-mpool dev version 2018-01-19 14:40:27 +01:00
Simon Martín
e68ba95fed adding cartodb-redis with redis4 support 2018-01-18 15:16:40 +01:00
Simon Martín
83d00a8aca Merge branch 'master' into redis4 2018-01-17 10:29:32 +01:00
Simon Martín
479e8970a1 Merge branch 'master' into redis4 2018-01-16 15:10:08 +01:00
Simon Martín
cf5e797f90 supporting redis4 2018-01-16 11:59:23 +01:00
191 changed files with 13548 additions and 5096 deletions

View File

@@ -1,14 +1,81 @@
sudo: required
dist: trusty
jobs:
include:
- sudo: required
services:
- docker
language: generic
before_install: docker pull carto/nodejs6-xenial-pg101
script: npm run docker-test
- dist: precise
addons:
postgresql: "9.5"
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- pkg-config
- libcairo2-dev
- libjpeg8-dev
- libgif-dev
- libpango1.0-dev
- g++-4.9
- wget
services:
- docker
before_install:
# Add custom PPAs from cartodb
- sudo add-apt-repository -y ppa:cartodb/postgresql-9.5
- sudo add-apt-repository -y ppa:cartodb/gis
- sudo add-apt-repository -y ppa:cartodb/gis-testing
before_install:
- docker pull cartoimages/windshaft-testing
- sudo apt-get update
# Force instalation of libgeos-3.5.0 (presumably needed because of existing version of postgis)
- sudo apt-get -y install libgeos-3.5.0=3.5.0-1cdb2
script:
- docker run -e POSTGIS_VERSION=2.4 -v `pwd`:/srv cartoimages/windshaft-testing bash docker-test.sh
# Install postgres db and build deps
- sudo /etc/init.d/postgresql stop # stop travis default instance
- sudo apt-get -y remove --purge postgresql-9.1
- sudo apt-get -y remove --purge postgresql-9.2
- sudo apt-get -y remove --purge postgresql-9.3
- sudo apt-get -y remove --purge postgresql-9.4
- sudo apt-get -y remove --purge postgresql-9.5
- sudo apt-get -y remove --purge postgresql-9.6
- sudo rm -rf /var/lib/postgresql/
- sudo rm -rf /var/log/postgresql/
- sudo rm -rf /etc/postgresql/
- sudo apt-get -y remove --purge postgis-2.2
- sudo apt-get -y autoremove
- sudo apt-get -y install postgresql-9.5=9.5.2-3cdb3
- sudo apt-get -y install postgresql-server-dev-9.5=9.5.2-3cdb3
- sudo apt-get -y install postgresql-plpython-9.5=9.5.2-3cdb3
- sudo apt-get -y install postgresql-9.5-postgis-scripts=2.2.2.0-cdb2
- sudo apt-get -y install postgresql-9.5-postgis-2.2=2.2.2.0-cdb2
language: generic
# configure it to accept local connections from postgres
- echo -e "# TYPE DATABASE USER ADDRESS METHOD \nlocal all postgres trust\nlocal all all trust\nhost all all 127.0.0.1/32 trust" \
| sudo tee /etc/postgresql/9.5/main/pg_hba.conf
- sudo /etc/init.d/postgresql restart 9.5
- createdb template_postgis
- createuser publicuser
- psql -c "CREATE EXTENSION postgis" template_postgis
# install yarn 0.27.5
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 0.27.5
- export PATH="$HOME/.yarn/bin:$PATH"
# instal redis 4
- wget http://download.redis.io/releases/redis-4.0.8.tar.gz
- tar xvzf redis-4.0.8.tar.gz
- cd redis-4.0.8
- make
- sudo make install
- cd ..
- rm redis-4.0.8.tar.gz
env:
- NPROCS=1 JOBS=1 PGUSER=postgres CXX=g++-4.9
language: node_js
node_js:
- "6"

133
NEWS.md
View File

@@ -1,7 +1,138 @@
# Changelog
## 6.2.0
Released 2018-07-20
Notice:
- This release changes the way that authentication works internally. You'll need to run `bundle exec rake carto:api_key:create_default` in your development environment to keep working.
New features:
- CI tests with Ubuntu Xenial + PostgreSQL 10.1 and Ubuntu Precise + PostgreSQL 9.5
- Upgrades Windshaft to [4.8.3](https://github.com/CartoDB/Windshaft/blob/4.8.3/NEWS.md#version-483) which includes:
- Update internal deps.
- A fix in mapnik-vector-tile to avoid grouping together properties with the same value but a different type.
- Performance improvements in the marker symbolizer (local cache, avoid building the collision matrix when possible).
- MVT: Disable simplify_distance to avoid multiple simplifications.
- Fix a bug with zero length lines not being rendered when using the marker symbolizer.
- Reduce size of npm package
- Omit attributes validation in layers with aggregation to avoid potentially long instantiation times
- Upgrades Camshaft to [0.61.11](https://github.com/CartoDB/camshaft/releases/tag/0.61.11):
- Use Dollar-Quoted String Constants to avoid Syntax Error while running moran analyses. [0.61.10](https://github.com/CartoDB/camshaft/releases/tag/0.61.10)
- Quote name columns when performing trade area analysis to avoid Syntax Errors. [0.61.11](https://github.com/CartoDB/camshaft/releases/tag/0.61.11)
- Update other deps:
- body-parser: 1.18.3
- cartodb-psql: 0.11.0
- cartodb-redis: 2.0.1
- dot: 1.1.2
- express: 4.16.3
- lru-cache: 4.1.3
- node-statsd: 0.1.1,
- queue-async: 1.1.0
- request: 2.87.0
- semver: 5.5.0
- step: 1.0.0
- turbo-carto: 0.20.4
- yargs: 11.1.0
- Update devel deps:
- istanbul: 0.4.5
- jshint: 2.9.5
- mocha: 3.5.3
- moment: 2.22.1
- nock: 9.2.6
- strftime: 0.10.0
- Optional instantiation metadata stats (https://github.com/CartoDB/Windshaft-cartodb/pull/952)
- Experimental dates_as_numbers support
- Tiles base urls with api key
Bug Fixes:
- Validates tile coordinates (z/x/y) from request params to be a valid integer value.
- Static maps fails for unsupported formats
- Handling errors extracting the column type on dataviews
- Fix `meta.stats.estimatedFeatureCount` for aggregations and queries with tokens
- Fix numeric histogram bounds when `start` and `end` are specified (#991)
- Static maps filters correctly if `layer` option is passed in the url.
- Aggregation doesn't return out-of-tile, partially aggregated clusters
- Aggregation was not accurate for high zoom, far away from the origin tiles
Announcements:
* Improve error message when the DB query is over the user's limits
## 6.1.0
Released 2018-04-16
New features:
- Aggreation filters
- Upgrades Windshaft to 4.7.0, which includes @carto/mapnik v3.6.2-carto.7 with improvements to metrics and markers caching. It also adds an option to disable the markers symbolizer caches in mapnik.
Bug Fixes:
- Non-default aggregation selected the wrong columns (e.g. for vector tiles)
- Aggregation dimensions with alias where broken
- cartodb_id was not unique accross aggregated vector tiles
## 6.0.0
Released 2018-03-19
Backward incompatible changes:
- Needs Redis v4
New features:
- Upgrades camshaft to 0.61.8
- Upgrades cartodb-redis to 1.0.0
- Rate limit feature (disabled by default)
- Fixes for tests with PG11
## 5.4.0
Released 2018-03-15
- Upgrades Windshaft to 4.5.7 ([Mapnik top metrics](https://github.com/CartoDB/Windshaft/pull/597), [AttributesBackend allows multiple features if all the attributes are the same](https://github.com/CartoDB/Windshaft/pull/602))
- Implemented middleware to authorize users via new Api Key system
- Keep the old authorization system as fallback
- Aggregation widget: Remove NULL categories in 'count' aggregations too
- Update request to 2.85.0
- Update camshaft to 0.61.4 (Fixes for AOI and Merge analyses)
- Update windshaft to 4.6.0, which in turn updates @carto/mapnik to 3.6.2-carto.4 and related dependencies. It brings in a cache for rasterized symbols. See https://github.com/CartoDB/node-mapnik/blob/v3.6.2-carto/CHANGELOG.carto.md#362-carto4
- PostGIS: Variables in postgis SQL queries must now additionally be wrapped in `!` (refs [#29](https://github.com/CartoDB/mapnik/issues/29), [mapnik/#3618](https://github.com/mapnik/mapnik/pull/3618)):
```sql
-- Before
SELECT ... WHERE trait = @variable
-- Now
SELECT ... WHERE trait = !@variable!
```
## 5.3.1
Released 2018-02-13
- Improve the speed of the aggregation dataview #865
## 5.3.0
Released 2018-02-12
- Upgrades redis-mpool to 0.5.0
- Upgrades windshaft to 4.5.2
- Upgrades cartodb-redis to 0.15.0
- Adds metrics option to the Mapnik renderer
- Upgrades camshadft to 0.61.2
## 5.2.1
Released 2018-02-01
Bug Fixes:
- Allow use of aggregation with attributes #861
## 5.2.0
Released 2018-02-01
Announcements:
- Upgrade windshaft to [4.3.3](https://github.com/CartoDB/windshaft/releases/tag/4.3.2) adding support for cache-features' in Mapnik/CartoDB layers.
## 5.1.0
Released 2018-01-30
New features:
- Now mapnik has support for fine-grained metrics.
- Variables can be passed for later substitution in postgis datasource.
Announcements:
- Upgrade windshaft to [4.3.1](https://github.com/CartoDB/windshaft/releases/tag/4.3.1). Underneath it upgrades mapnik and all the related dependencies.
## 5.0.1
Released 2018-01-20
Released 2018-01-29
Bug Fixes:
- Allow aggregation for queries with no the_geom (only the_geom_webmercator) #856

2
app.js
View File

@@ -100,8 +100,6 @@ if ( global.environment.log_filename ) {
global.log4js.configure(log4jsConfig);
global.logger = global.log4js.getLogger();
global.environment.api_hostname = require('os').hostname().split('.')[0];
// Include cartodb_windshaft only _after_ the "global" variable is set
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/28
var cartodbWindshaft = require('./lib/cartodb/server');

View File

@@ -15,26 +15,59 @@ var config = {
// Base URLs for the APIs
//
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
//
// Base url for the Templated Maps API
// "/api/v1/map/named" is the new API,
// "/tiles/template" is for compatibility with versions up to 1.6.x
,base_url_templated: '(?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)'
// Base url for the Detached Maps API
// "maps" is the the new API,
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
,routes: {
v1: {
paths: [
'/api/v1',
'/user/:user/api/v1',
],
// Base url for the Detached Maps API
// "/api/v1/map" is the new API,
map: {
paths: [
'/map',
]
},
// Base url for the Templated Maps API
// "/api/v1/map/named" is the new API,
template: {
paths: [
'/map/named'
]
}
},
// For compatibility with versions up to 1.6.x
v0: {
paths: [
'/tiles'
],
// Base url for the Detached Maps API
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
map: {
paths: [
'/layergroup'
]
},
// Base url for the Templated Maps API
// "/tiles/template" is for compatibility with versions up to 1.6.x
template: {
paths: [
'/template'
]
}
}
}
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
//
// This URLs depend on how `base_url_detached` and `user_from_host` are configured: the application can be
// This URLs depend on how `routes` and `user_from_host` are configured: the application can be
// configured to accept request with the {user} in the header host or in the request path.
// It also might depend on the configured cdn_url via `serverMetadata.cdn_url`.
//
// This template allows to make the endpoints generation more flexible, the template exposes the following params:
// 1. {{=it.cdn_url}}: will be used when `serverMetadata.cdn_url` exists.
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `base_url_detached`.
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `routes`.
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
,resources_url_templates: {
http: 'http://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map',
@@ -54,12 +87,12 @@ var config = {
// idle socket timeout, in milliseconds
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: false
,cache_enabled: true
,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
,log_filename: undefined
,log_filename: 'logs/node-windshaft.log'
// Templated database username for authorized user
// Supported labels: 'user_id' (read from redis)
,postgres_auth_user: 'development_cartodb_user_<%= user_id %>'
@@ -67,39 +100,25 @@ var config = {
// Supported labels: 'user_id', 'user_password' (both read from redis)
,postgres_auth_pass: '<%= user_password %>'
,postgres: {
// Parameters to pass to datasource plugin of mapnik
// See http://github.com/mapnik/mapnik/wiki/PostGIS
type: "postgis",
user: "publicuser",
password: "public",
host: '127.0.0.1',
port: 5432,
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
/* experimental
geometry_field: "the_geom",
extent: "-180,-90,180,90",
srid: 4326,
*/
// max number of rows to return when querying data, 0 means no limit
row_limit: 65535,
simplify_geometries: true,
use_overviews: true, // use overviews to retrieve raster
/*
* Set persist_connection to false if you want
* database connections to be closed on renderer
* expiration (1 minute after last use).
* Setting to true (the default) would never
* close any connection for the server's lifetime
*/
persist_connection: false,
max_size: 500
pool: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
}
}
,mapnik_version: undefined
,mapnik_tile_format: 'png8:m=h'
,statsd: {
host: 'localhost',
port: 8125,
prefix: 'dev.',
prefix: 'dev.', // could be hostname, better not containing dots
cacheDns: true
// support all allowed node-statsd options
}
@@ -111,15 +130,7 @@ var config = {
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
//PostGIS 2.4 is required for this to work
//If disabled it will use Mapnik MVT generation
usePostGIS: false,
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
}
usePostGIS: false
},
mapnik: {
// The size of the pool of internal mapnik backend
@@ -173,6 +184,30 @@ var config = {
// It will only work if snapToGrid is enabled
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
postgis: {
// Parameters to pass to datasource plugin of mapnik
// See http://github.com/mapnik/mapnik/wiki/PostGIS
user: "publicuser",
password: "public",
host: '127.0.0.1',
port: 5432,
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
// max number of rows to return when querying data, 0 means no limit
row_limit: 65535,
/*
* Set persist_connection to false if you want
* database connections to be closed on renderer
* expiration (1 minute after last use).
* Setting to true (the default) would never
* close any connection for the server's lifetime
*/
persist_connection: false,
simplify_geometries: true,
use_overviews: true, // use overviews to retrieve raster
max_size: 500,
twkb_encoding: true
},
limits: {
// Time in milliseconds a render request can take before it fails, some notes:
// - 0 means no render limit
@@ -186,26 +221,17 @@ var config = {
cacheOnTimeout: true
},
geojson: {
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
},
// If enabled Mapnik will reuse the features retrieved from the database
// instead of requesting them once per style inside a layer
'cache-features': true,
// SQL queries will be wrapped with ST_ClipByBox2D
// Returning the portion of a geometry falling within a rectangle
// It will only work if snapToGrid is enabled
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
// geometries will be simplified using ST_RemoveRepeatedPoints
// which cost is no more expensive than snapping and results are
// much closer to the original geometry
removeRepeatedPoints: false // this requires postgis >=2.2
// Require metrics to the renderer
metrics: false,
// Options for markers attributes, ellipses and images caches
markers_symbolizer_caches: {
disabled: false
}
},
http: {
timeout: 2000, // the timeout in ms for a http tile request
@@ -221,16 +247,7 @@ var config = {
src: __dirname + '/../../assets/default-placeholder.png'
}
},
torque: {
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
}
}
torque: {}
}
// anything analyses related
,analysis: {
@@ -251,7 +268,7 @@ var config = {
// If filename is given logs comming from analysis client will be written
// there, in append mode. Otherwise 'log_filename' is used. Otherwise stdout is used (default).
// Log file will be re-opened on receiving the HUP signal
filename: '/tmp/analysis.log'
filename: 'logs/node-windshaft-analysis.log'
},
// Define max execution time in ms for analyses or tags
// If analysis or tag are not found in redis this values will be used as default.
@@ -321,6 +338,12 @@ var config = {
// X-Tiler-Profile header containing elapsed timing for various
// steps taken for producing the response.
,useProfiler:true
,serverMetadata: {
cdn_url: {
http: undefined,
https: undefined
}
}
// Settings for the health check available at /health
,health: {
enabled: false,
@@ -338,7 +361,28 @@ var config = {
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
cdbQueryTablesFromPostgres: true,
// whether in mapconfig is available stats & metadata for each layer
layerStats: true
layerStats: true,
// whether it should rate limit endpoints (global configuration)
rateLimitsEnabled: false,
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
rateLimitsByEndpoint: {
anonymous: false,
static: false,
static_named: false,
dataview: false,
dataview_search: false,
analysis: false,
analysis_catalog: false,
tile: false,
attributes: false,
named_list: false,
named_create: false,
named_get: false,
named: false,
named_update: false,
named_delete: false,
named_tiles: false
}
}
};

View File

@@ -15,26 +15,59 @@ var config = {
// Base URLs for the APIs
//
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
//
// Base url for the Templated Maps API
// "/api/v1/map/named" is the new API,
// "/tiles/template" is for compatibility with versions up to 1.6.x
,base_url_templated: '(?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)'
// Base url for the Detached Maps API
// "maps" is the the new API,
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
,routes: {
v1: {
paths: [
'/api/v1',
'/user/:user/api/v1',
],
// Base url for the Detached Maps API
// "/api/v1/map" is the new API,
map: {
paths: [
'/map',
]
},
// Base url for the Templated Maps API
// "/api/v1/map/named" is the new API,
template: {
paths: [
'/map/named'
]
}
},
// For compatibility with versions up to 1.6.x
v0: {
paths: [
'/tiles'
],
// Base url for the Detached Maps API
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
map: {
paths: [
'/layergroup'
]
},
// Base url for the Templated Maps API
// "/tiles/template" is for compatibility with versions up to 1.6.x
template: {
paths: [
'/template'
]
}
}
}
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
//
// This URLs depend on how `base_url_detached` and `user_from_host` are configured: the application can be
// This URLs depend on how `routes` and `user_from_host` are configured: the application can be
// configured to accept request with the {user} in the header host or in the request path.
// It also might depend on the configured cdn_url via `serverMetadata.cdn_url`.
//
// This template allows to make the endpoints generation more flexible, the template exposes the following params:
// 1. {{=it.cdn_url}}: will be used when `serverMetadata.cdn_url` exists.
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `base_url_detached`.
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `routes`.
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
,resources_url_templates: {
http: 'http://{{=it.cdn_url}}/{{=it.user}}/api/v1/map',
@@ -67,26 +100,18 @@ var config = {
// Supported labels: 'user_id', 'user_password' (both read from redis)
,postgres_auth_pass: '<%= user_password %>'
,postgres: {
// Parameters to pass to datasource plugin of mapnik
// See http://github.com/mapnik/mapnik/wiki/PostGIS
user: "publicuser",
password: "public",
host: '127.0.0.1',
port: 6432,
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
// max number of rows to return when querying data, 0 means no limit
row_limit: 65535,
/*
* Set persist_connection to false if you want
* database connections to be closed on renderer
* expiration (1 minute after last use).
* Setting to true (the default) would never
* close any connection for the server's lifetime
*/
persist_connection: false,
simplify_geometries: true,
use_overviews: true, // use overviews to retrieve raster
max_size: 500
port: 5432,
pool: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
}
}
,mapnik_version: undefined
,mapnik_tile_format: 'png8:m=h'
@@ -105,15 +130,7 @@ var config = {
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
//PostGIS 2.4 is required for this to work
//If disabled it will use Mapnik MVT generation
usePostGIS: false,
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
}
usePostGIS: false
},
mapnik: {
// The size of the pool of internal mapnik backend
@@ -167,6 +184,30 @@ var config = {
// It will only work if snapToGrid is enabled
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
postgis: {
// Parameters to pass to datasource plugin of mapnik
// See http://github.com/mapnik/mapnik/wiki/PostGIS
user: "publicuser",
password: "public",
host: '127.0.0.1',
port: 5432,
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
// max number of rows to return when querying data, 0 means no limit
row_limit: 65535,
/*
* Set persist_connection to false if you want
* database connections to be closed on renderer
* expiration (1 minute after last use).
* Setting to true (the default) would never
* close any connection for the server's lifetime
*/
persist_connection: false,
simplify_geometries: true,
use_overviews: true, // use overviews to retrieve raster
max_size: 500,
twkb_encoding: true
},
limits: {
// Time in milliseconds a render request can take before it fails, some notes:
// - 0 means no render limit
@@ -180,26 +221,17 @@ var config = {
cacheOnTimeout: true
},
geojson: {
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
},
// If enabled Mapnik will reuse the features retrieved from the database
// instead of requesting them once per style inside a layer
'cache-features': true,
// SQL queries will be wrapped with ST_ClipByBox2D
// Returning the portion of a geometry falling within a rectangle
// It will only work if snapToGrid is enabled
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
// geometries will be simplified using ST_RemoveRepeatedPoints
// which cost is no more expensive than snapping and results are
// much closer to the original geometry
removeRepeatedPoints: false // this requires postgis >=2.2
// Require metrics to the renderer
metrics: false,
// Options for markers attributes, ellipses and images caches
markers_symbolizer_caches: {
disabled: false
}
},
http: {
timeout: 2000, // the timeout in ms for a http tile request
@@ -215,16 +247,7 @@ var config = {
src: __dirname + '/../../assets/default-placeholder.png'
}
},
torque: {
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
}
}
torque: {}
}
// anything analyses related
,analysis: {
@@ -245,7 +268,7 @@ var config = {
// If filename is given logs comming from analysis client will be written
// there, in append mode. Otherwise 'log_filename' is used. Otherwise stdout is used (default).
// Log file will be re-opened on receiving the HUP signal
filename: 'logs/analysis.log'
filename: 'logs/node-windshaft-analysis.log'
},
// Define max execution time in ms for analyses or tags
// If analysis or tag are not found in redis this values will be used as default.
@@ -338,7 +361,28 @@ var config = {
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
cdbQueryTablesFromPostgres: true,
// whether in mapconfig is available stats & metadata for each layer
layerStats: false
layerStats: false,
// whether it should rate limit endpoints (global configuration)
rateLimitsEnabled: false,
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
rateLimitsByEndpoint: {
anonymous: false,
static: false,
static_named: false,
dataview: false,
dataview_search: false,
analysis: false,
analysis_catalog: false,
tile: false,
attributes: false,
named_list: false,
named_create: false,
named_get: false,
named: false,
named_update: false,
named_delete: false,
named_tiles: false
}
}
};

View File

@@ -15,29 +15,62 @@ var config = {
// Base URLs for the APIs
//
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
//
// Base url for the Templated Maps API
// "/api/v1/maps/named" is the new API,
// "/tiles/template" is for compatibility with versions up to 1.6.x
,base_url_templated: '(?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)'
// Base url for the Detached Maps API
// "/api/v1/maps" is the the new API,
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
,routes: {
v1: {
paths: [
'/api/v1',
'/user/:user/api/v1',
],
// Base url for the Detached Maps API
// "/api/v1/map" is the new API,
map: {
paths: [
'/map',
]
},
// Base url for the Templated Maps API
// "/api/v1/map/named" is the new API,
template: {
paths: [
'/map/named'
]
}
},
// For compatibility with versions up to 1.6.x
v0: {
paths: [
'/tiles'
],
// Base url for the Detached Maps API
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
map: {
paths: [
'/layergroup'
]
},
// Base url for the Templated Maps API
// "/tiles/template" is for compatibility with versions up to 1.6.x
template: {
paths: [
'/template'
]
}
}
}
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
//
// This URLs depend on how `base_url_detached` and `user_from_host` are configured: the application can be
// This URLs depend on how `routes` and `user_from_host` are configured: the application can be
// configured to accept request with the {user} in the header host or in the request path.
// It also might depend on the configured cdn_url via `serverMetadata.cdn_url`.
//
// This template allows to make the endpoints generation more flexible, the template exposes the following params:
// 1. {{=it.cdn_url}}: will be used when `serverMetadata.cdn_url` exists.
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `base_url_detached`.
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `routes`.
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
,resources_url_templates: {
http: 'http://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map',
http: 'http://{{=it.cdn_url}}/{{=it.user}}/api/v1/map',
https: 'https://{{=it.cdn_url}}/{{=it.user}}/api/v1/map'
}
@@ -55,7 +88,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] (:res[X-Tiler-Errors])'
,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
@@ -67,33 +100,25 @@ var config = {
// Supported labels: 'user_id', 'user_password' (both read from redis)
,postgres_auth_pass: '<%= user_password %>'
,postgres: {
// Parameters to pass to datasource plugin of mapnik
// See http://github.com/mapnik/mapnik/wiki/PostGIS
user: "publicuser",
password: "public",
host: '127.0.0.1',
port: 6432,
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
// max number of rows to return when querying data, 0 means no limit
row_limit: 65535,
simplify_geometries: true,
use_overviews: true, // use overviews to retrieve raster
/*
* Set persist_connection to false if you want
* database connections to be closed on renderer
* expiration (1 minute after last use).
* Setting to true (the default) would never
* close any connection for the server's lifetime
*/
persist_connection: false,
max_size: 500
port: 5432,
pool: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
}
}
,mapnik_version: undefined
,mapnik_tile_format: 'png8:m=h'
,statsd: {
host: 'localhost',
port: 8125,
prefix: 'stage.:host.',
prefix: 'stage.:host.', // could be hostname, better not containing dots
cacheDns: true
// support all allowed node-statsd options
}
@@ -105,15 +130,7 @@ var config = {
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
//PostGIS 2.4 is required for this to work
//If disabled it will use Mapnik MVT generation
usePostGIS: false,
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
}
usePostGIS: false
},
mapnik: {
// The size of the pool of internal mapnik backend
@@ -167,6 +184,30 @@ var config = {
// It will only work if snapToGrid is enabled
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
postgis: {
// Parameters to pass to datasource plugin of mapnik
// See http://github.com/mapnik/mapnik/wiki/PostGIS
user: "publicuser",
password: "public",
host: '127.0.0.1',
port: 5432,
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
// max number of rows to return when querying data, 0 means no limit
row_limit: 65535,
/*
* Set persist_connection to false if you want
* database connections to be closed on renderer
* expiration (1 minute after last use).
* Setting to true (the default) would never
* close any connection for the server's lifetime
*/
persist_connection: false,
simplify_geometries: true,
use_overviews: true, // use overviews to retrieve raster
max_size: 500,
twkb_encoding: true
},
limits: {
// Time in milliseconds a render request can take before it fails, some notes:
// - 0 means no render limit
@@ -180,26 +221,17 @@ var config = {
cacheOnTimeout: true
},
geojson: {
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
},
// If enabled Mapnik will reuse the features retrieved from the database
// instead of requesting them once per style inside a layer
'cache-features': true,
// SQL queries will be wrapped with ST_ClipByBox2D
// Returning the portion of a geometry falling within a rectangle
// It will only work if snapToGrid is enabled
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
// geometries will be simplified using ST_RemoveRepeatedPoints
// which cost is no more expensive than snapping and results are
// much closer to the original geometry
removeRepeatedPoints: false // this requires postgis >=2.2
// Require metrics to the renderer
metrics: false,
// Options for markers attributes, ellipses and images caches
markers_symbolizer_caches: {
disabled: false
}
},
http: {
timeout: 2000, // the timeout in ms for a http tile request
@@ -215,16 +247,7 @@ var config = {
src: __dirname + '/../../assets/default-placeholder.png'
}
},
torque: {
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
}
}
torque: {}
}
// anything analyses related
,analysis: {
@@ -245,7 +268,7 @@ var config = {
// If filename is given logs comming from analysis client will be written
// there, in append mode. Otherwise 'log_filename' is used. Otherwise stdout is used (default).
// Log file will be re-opened on receiving the HUP signal
filename: 'logs/analysis.log'
filename: 'logs/node-windshaft-analysis.log'
},
// Define max execution time in ms for analyses or tags
// If analysis or tag are not found in redis this values will be used as default.
@@ -323,7 +346,7 @@ var config = {
}
// Settings for the health check available at /health
,health: {
enabled: false,
enabled: true,
username: 'localhost',
z: 0,
x: 0,
@@ -338,7 +361,28 @@ var config = {
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
cdbQueryTablesFromPostgres: true,
// whether in mapconfig is available stats & metadata for each layer
layerStats: true
layerStats: true,
// whether it should rate limit endpoints (global configuration)
rateLimitsEnabled: false,
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
rateLimitsByEndpoint: {
anonymous: false,
static: false,
static_named: false,
dataview: false,
dataview_search: false,
analysis: false,
analysis_catalog: false,
tile: false,
attributes: false,
named_list: false,
named_create: false,
named_get: false,
named: false,
named_update: false,
named_delete: false,
named_tiles: false
}
}
};

View File

@@ -16,28 +16,62 @@ var config = {
// Base URLs for the APIs
//
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
//
// Base url for the Templated Maps API
// "/api/v1/map/named" is the new API,
// "/tiles/template" is for compatibility with versions up to 1.6.x
,base_url_templated: '(?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)'
// Base url for the Detached Maps API
// "maps" is the the new API,
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
,routes: {
v1: {
paths: [
'/api/v1',
'/user/:user/api/v1',
],
// Base url for the Detached Maps API
// "/api/v1/map" is the new API,
map: {
paths: [
'/map',
]
},
// Base url for the Templated Maps API
// "/api/v1/map/named" is the new API,
template: {
paths: [
'/map/named'
]
}
},
// For compatibility with versions up to 1.6.x
v0: {
paths: [
'/tiles'
],
// Base url for the Detached Maps API
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
map: {
paths: [
'/layergroup'
]
},
// Base url for the Templated Maps API
// "/tiles/template" is for compatibility with versions up to 1.6.x
template: {
paths: [
'/template'
]
}
}
}
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
//
// This URLs depend on how `base_url_detached` and `user_from_host` are configured: the application can be
// This URLs depend on how `routes` and `user_from_host` are configured: the application can be
// configured to accept request with the {user} in the header host or in the request path.
// It also might depend on the configured cdn_url via `serverMetadata.cdn_url`.
//
// This template allows to make the endpoints generation more flexible, the template exposes the following params:
// 1. {{=it.cdn_url}}: will be used when `serverMetadata.cdn_url` exists.
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `base_url_detached`.
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `routes`.
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
,resources_url_templates: {
http: 'http://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map'
http: 'http://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map',
https: 'https://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map'
}
// Maximum number of connections for one process
@@ -54,11 +88,11 @@ 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]) (:res[X-Tiler-Errors])'
,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
//,log_filename: 'logs/node-windshaft.log'
,log_filename: '/tmp/node-windshaft.log'
// Templated database username for authorized user
// Supported labels: 'user_id' (read from redis)
,postgres_auth_user: 'test_windshaft_cartodb_user_<%= user_id %>'
@@ -66,33 +100,25 @@ var config = {
// Supported labels: 'user_id', 'user_password' (both read from redis)
,postgres_auth_pass: 'test_windshaft_cartodb_user_<%= user_id %>_pass'
,postgres: {
// Parameters to pass to datasource plugin of mapnik
// See http://github.com/mapnik/mapnik/wiki/PostGIS
user: "test_windshaft_publicuser",
password: "public",
host: '127.0.0.1',
port: 5432,
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
// max number of rows to return when querying data, 0 means no limit
row_limit: 65535,
simplify_geometries: true,
use_overviews: true, // use overviews to retrieve raster
/*
* Set persist_connection to false if you want
* database connections to be closed on renderer
* expiration (1 minute after last use).
* Setting to true (the default) would never
* close any connection for the server's lifetime
*/
persist_connection: false,
max_size: 500
pool: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
}
}
,mapnik_version: ''
,mapnik_version: undefined
,mapnik_tile_format: 'png8:m=h'
,statsd: {
host: 'localhost',
port: 8125,
prefix: 'test.:host.',
prefix: 'test.:host.', // could be hostname, better not containing dots
cacheDns: true
// support all allowed node-statsd options
}
@@ -104,15 +130,7 @@ var config = {
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
//PostGIS 2.4 is required for this to work
//If disabled it will use Mapnik MVT generation
usePostGIS: false,
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
}
usePostGIS: false
},
mapnik: {
// The size of the pool of internal mapnik backend
@@ -166,6 +184,30 @@ var config = {
// It will only work if snapToGrid is enabled
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
postgis: {
// Parameters to pass to datasource plugin of mapnik
// See http://github.com/mapnik/mapnik/wiki/PostGIS
user: "publicuser",
password: "public",
host: '127.0.0.1',
port: 5432,
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
// max number of rows to return when querying data, 0 means no limit
row_limit: 65535,
/*
* Set persist_connection to false if you want
* database connections to be closed on renderer
* expiration (1 minute after last use).
* Setting to true (the default) would never
* close any connection for the server's lifetime
*/
persist_connection: false,
simplify_geometries: true,
use_overviews: true, // use overviews to retrieve raster
max_size: 500,
twkb_encoding: false
},
limits: {
// Time in milliseconds a render request can take before it fails, some notes:
// - 0 means no render limit
@@ -179,24 +221,16 @@ var config = {
cacheOnTimeout: true
},
geojson: {
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
},
// If enabled Mapnik will reuse the features retrieved from the database
// instead of requesting them once per style inside a layer
'cache-features': false,
// SQL queries will be wrapped with ST_ClipByBox2D
// Returning the portion of a geometry falling within a rectangle
// It will only work if snapToGrid is enabled
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
// geometries will be simplified using ST_RemoveRepeatedPoints
// which cost is no more expensive than snapping and results are
// much closer to the original geometry
removeRepeatedPoints: false // this requires postgis >=2.2
// Require metrics to the renderer
metrics: false,
// Options for markers attributes, ellipses and images caches
markers_symbolizer_caches: {
disabled: false
}
},
http: {
@@ -215,16 +249,7 @@ var config = {
src: __dirname + '/../../assets/default-placeholder.png'
}
},
torque: {
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
}
}
torque: {}
}
// anything analyses related
,analysis: {
@@ -245,7 +270,7 @@ var config = {
// If filename is given logs comming from analysis client will be written
// there, in append mode. Otherwise 'log_filename' is used. Otherwise stdout is used (default).
// Log file will be re-opened on receiving the HUP signal
filename: 'node-windshaft.log'
filename: '/tmp/node-windshaft-analysis.log'
},
// Define max execution time in ms for analyses or tags
// If analysis or tag are not found in redis this values will be used as default.
@@ -315,6 +340,12 @@ var config = {
// X-Tiler-Profile header containing elapsed timing for various
// steps taken for producing the response.
,useProfiler:true
,serverMetadata: {
cdn_url: {
http: undefined,
https: undefined
}
}
// Settings for the health check available at /health
,health: {
enabled: false,
@@ -332,7 +363,28 @@ var config = {
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
cdbQueryTablesFromPostgres: true,
// whether in mapconfig is available stats & metadata for each layer
layerStats: true
layerStats: true,
// whether it should rate limit endpoints (global configuration)
rateLimitsEnabled: false,
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
rateLimitsByEndpoint: {
anonymous: false,
static: false,
static_named: false,
dataview: false,
dataview_search: false,
analysis: false,
analysis_catalog: false,
tile: false,
attributes: false,
named_list: false,
named_create: false,
named_get: false,
named: false,
named_update: false,
named_delete: false,
named_tiles: false
}
}
};

View File

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

View File

@@ -0,0 +1,89 @@
FROM ubuntu:xenial
# Use UTF8 to avoid encoding problems with pgsql
ENV LANG C.UTF-8
ENV NPROCS 1
ENV JOBS 1
ENV CXX g++-4.9
ENV PGUSER postgres
# Add external repos
RUN set -ex \
&& apt-get update \
&& apt-get install -y \
curl \
software-properties-common \
locales \
&& add-apt-repository -y ppa:ubuntu-toolchain-r/test \
&& add-apt-repository -y ppa:cartodb/postgresql-10 \
&& add-apt-repository -y ppa:cartodb/gis \
&& curl -sL https://deb.nodesource.com/setup_6.x | bash \
&& locale-gen en_US.UTF-8 \
&& update-locale LANG=en_US.UTF-8
# Install dependencies and PostGIS 2.4 from sources
RUN set -ex \
&& apt-get update \
&& apt-get install -y \
g++-4.9 \
gcc-4.9 \
git \
libcairo2-dev \
libgdal-dev \
libgdal1i \
libgdal20 \
libgeos-dev \
libgif-dev \
libjpeg8-dev \
libjson-c-dev \
libpango1.0-dev \
libpixman-1-dev \
libproj-dev \
libprotobuf-c-dev \
libxml2-dev \
gdal-bin \
make \
nodejs \
protobuf-c-compiler \
pkg-config \
wget \
zip \
postgresql-10 \
postgresql-10-plproxy \
postgresql-10-postgis-2.4 \
postgresql-10-postgis-2.4-scripts \
postgresql-10-postgis-scripts \
postgresql-client-10 \
postgresql-client-common \
postgresql-common \
postgresql-contrib \
postgresql-plpython-10 \
postgresql-server-dev-10 \
postgis \
&& wget http://download.redis.io/releases/redis-4.0.8.tar.gz \
&& tar xvzf redis-4.0.8.tar.gz \
&& cd redis-4.0.8 \
&& make \
&& make install \
&& cd .. \
&& rm redis-4.0.8.tar.gz \
&& rm -R redis-4.0.8 \
&& apt-get purge -y wget protobuf-c-compiler \
&& apt-get autoremove -y
# Configure PostgreSQL
RUN set -ex \
&& echo "listen_addresses='*'" >> /etc/postgresql/10/main/postgresql.conf \
&& echo "local all all trust" > /etc/postgresql/10/main/pg_hba.conf \
&& echo "host all all 0.0.0.0/0 trust" >> /etc/postgresql/10/main/pg_hba.conf \
&& echo "host all all ::1/128 trust" >> /etc/postgresql/10/main/pg_hba.conf \
&& /etc/init.d/postgresql start \
&& createdb template_postgis \
&& createuser publicuser \
&& psql -c "CREATE EXTENSION postgis" template_postgis \
&& /etc/init.d/postgresql stop
WORKDIR /srv
EXPOSE 5858
CMD /etc/init.d/postgresql start

23
docker/reference.md Normal file
View File

@@ -0,0 +1,23 @@
After running the tests with docker, you will need Docker installed and the docker image downloaded.
## Install docker
`sudo apt install docker.io && sudo usermod -aG docker $(whoami)`
## Download image
`docker pull carto/IMAGE`
## Carto account
https://hub.docker.com/r/carto/
## Update image
- Edit the docker image file with your desired changes
- Build image:
- `docker build -t carto/IMAGE -f docker/DOCKER_FILE docker/`
- Upload to docker hub:
- Login into docker hub:
- `docker login`
- Create tag:
- `docker tag carto/IMAGE carto/IMAGE`
- Upload:
- `docker push carto/IMAGE`

View File

@@ -29,7 +29,7 @@ The value of this attribute can be `false` to explicitly disable aggregation for
// object, defines the columns of the aggregated datasets. Each property corresponds to a columns name and
// should contain an object with two properties: "aggregate_function" (one of "sum", "max", "min", "avg", "mode" or "count"),
// and "aggregated_column" (the name of a column of the original layer query or "*")
// A column defined as `"_cdb_features_count": {"aggregate_function": "count", aggregated_column: "*"}`
// A column defined as `"_cdb_feature_count": {"aggregate_function": "count", aggregated_column: "*"}`
// is always generated in addition to the defined columns.
// The column names `cartodb_id`, `the_geom`, `the_geom_webmercator` and `_cdb_feature_count` cannot be used
// for aggregated columns, as they correspond to columns always present in the result.

View File

@@ -10,7 +10,7 @@ Aggregation is available only for point geometries. During aggregation the point
When no placement or columns are specified a special default aggregation is performed.
This special mode performs only spatial aggregation (using a grid defined by the requested tile and the resolution, parameter, as all the other cases), and returns a _random_ record from each group (grid cell) with all its columns and an additional `_cdb_features_count` with the number of features in the group.
This special mode performs only spatial aggregation (using a grid defined by the requested tile and the resolution, parameter, as all the other cases), and returns a _random_ record from each group (grid cell) with all its columns and an additional `_cdb_feature_count` with the number of features in the group.
Regarding the randomness of the sample: currently we use the row with the minimum `cartodb_id` value in each group.
@@ -18,7 +18,7 @@ The rationale behind having this special aggregation with all the original colum
### User defined aggregations
When either a explicit placement or columns are requested we no longer use the special, query; we use one determined by the placement (which will default to "centroid"), and it will have as columns only the aggregated columns specified, in addition to `_cdb_features_count`, which is always present.
When either a explicit placement or columns are requested we no longer use the special, query; we use one determined by the placement (which will default to "centroid"), and it will have as columns only the aggregated columns specified, in addition to `_cdb_feature_count`, which is always present.
We might decide in the future to allow sampling column values for any of the different placement modes.
@@ -185,3 +185,80 @@ This is the minimum number of (estimated) rows in the dataset (query results) fo
]
}
```
### `filters`
Aggregated data can be filtered by imposing filtering conditions on the aggregated columns.
Each condition is represented by one or more parameters:
* `{ "equal": V }` selects an specific value of the aggregated column.
* `{ "not_equal": V }` selects values different from the one specified.
* `{ "in": [v1, v2, v3] }` selects any value from a list.
* `{ "not_in": [v1, v2, v3] }` selects any value not in a list.
* `{ "less_than": v }` selects values strictly less than the one given.
* `{ "less_than_or_equal_to": v }` selects values less than or equal to the one given.
* `{ "greater_than": v }` selects values strictly greater than the one given.
* `{ "greater_than_or_equal_to": v }` selects values greater than or equal to the one given.
One of the *less* conditions can be combined with one of the *greater* conditions to select a range of values, for example:
* `{ "greater_than": v1, "less_than": v2 }`
* `{ "greater_than_or_equal_to": v1, "less_than": v2 }`
* `{ "greater_than": v1, "less_than_or_equal_to": v2 }`
* `{ "greater_than_or_equal_to": v1, "less_than_or_equal_to": v2 }`
For a given column, multiple conditions can be passed in an array; the conditions will logically ORed (any of the conditions have to be verifid for the value to be selected):
* `"myvalue": [ { "equal": 10 }, { "less_than": 0 }]` will select values of the column `myvalue` which are equal to 10 **or** less than 0.
In addition, the filters applied to different columns are logically combined with AND (all the conditions have to be satisfied for an element to be selected); for example with the following `filters` parameter we'll select aggregated records which have a `total_value` > 100 **and** a category equal to "a".
```json
{
"total_value": { "greater_than": 100 },
"category": { "equal": "a" }
}
```
Note that the filtered columns have to be defined with the `columns` parameter, except for `_cdb_feature_count`, which is always implicitly defined and can be filtered too.
#### Example
```json
{
"version": "1.7.0",
"extent": [-20037508.5, -20037508.5, 20037508.5, 20037508.5],
"srid": 3857,
"maxzoom": 18,
"minzoom": 3,
"layers": [
{
"type": "mapnik",
"options": {
"sql": "select * from table",
"cartocss": "#table { marker-width: [total]; marker-fill: ramp(value, (red, green, blue), jenks); }",
"cartocss_version": "2.3.0",
"aggregation": {
"placement": "centroid",
"columns": {
"total_value": {
"aggregate_function": "sum",
"aggregated_column": "value"
},
"category": {
"aggregate_function": "mode",
"aggregated_column": "category"
}
},
"filters" : {
"total_value": { "greater_than": 100 },
"category": { "equal": "a" }
},
"resolution": 2,
"threshold": 500000
}
}
}
]
}
```

View File

@@ -11,7 +11,7 @@ Begin by instantiating either a Named or Anonymous Map using the `layergroupid t
#### Definition
```bash
GET /api/v1/map/static/center/{token}/{z}/{lat}/{lng}/{width}/{height}.{format}
GET /api/v1/map/static/center/{token}/{z}/{lat}/{lng}/{width}/{height}.{format}{{?}extra_options}
```
#### Params
@@ -58,6 +58,9 @@ Note: you can see this endpoint as
GET /api/v1/map/static/bbox/{token}/{west},{south},{east},{north}/{width}/{height}.{format}`
```
#### Extra options
* Layer: List of layers to be shown in the image (by default `all`), for example `?layer=0,1`.
### Named Map
#### Definition

View File

@@ -0,0 +1,321 @@
const { Router: router } = require('express');
const RedisPool = require('redis-mpool');
const cartodbRedis = require('cartodb-redis');
const windshaft = require('windshaft');
const PgConnection = require('../backends/pg_connection');
const AnalysisBackend = require('../backends/analysis');
const AnalysisStatusBackend = require('../backends/analysis-status');
const DataviewBackend = require('../backends/dataview');
const TemplateMaps = require('../backends/template_maps.js');
const PgQueryRunner = require('../backends/pg_query_runner');
const StatsBackend = require('../backends/stats');
const AuthBackend = require('../backends/auth');
const UserLimitsBackend = require('../backends/user-limits');
const OverviewsMetadataBackend = require('../backends/overviews-metadata');
const FilterStatsApi = require('../backends/filter-stats');
const TablesExtentBackend = require('../backends/tables-extent');
const LayergroupAffectedTablesCache = require('../cache/layergroup_affected_tables');
const SurrogateKeysCache = require('../cache/surrogate_keys_cache');
const VarnishHttpCacheBackend = require('../cache/backend/varnish_http');
const FastlyCacheBackend = require('../cache/backend/fastly');
const NamedMapProviderCache = require('../cache/named_map_provider_cache');
const NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
const SqlWrapMapConfigAdapter = require('../models/mapconfig/adapter/sql-wrap-mapconfig-adapter');
const MapConfigNamedLayersAdapter = require('../models/mapconfig/adapter/mapconfig-named-layers-adapter');
const MapConfigBufferSizeAdapter = require('../models/mapconfig/adapter/mapconfig-buffer-size-adapter');
const AnalysisMapConfigAdapter = require('../models/mapconfig/adapter/analysis-mapconfig-adapter');
const MapConfigOverviewsAdapter = require('../models/mapconfig/adapter/mapconfig-overviews-adapter');
const TurboCartoAdapter = require('../models/mapconfig/adapter/turbo-carto-adapter');
const DataviewsWidgetsAdapter = require('../models/mapconfig/adapter/dataviews-widgets-adapter');
const AggregationMapConfigAdapter = require('../models/mapconfig/adapter/aggregation-mapconfig-adapter');
const MapConfigAdapter = require('../models/mapconfig/adapter');
const VectorMapConfigAdapter = require('../models/mapconfig/adapter/vector-mapconfig-adapter');
const ResourceLocator = require('../models/resource-locator');
const LayergroupMetadata = require('../utils/layergroup-metadata');
const RendererStatsReporter = require('../stats/reporter/renderer');
const initializeStatusCode = require('./middlewares/initialize-status-code');
const logger = require('./middlewares/logger');
const bodyParser = require('body-parser');
const servedByHostHeader = require('./middlewares/served-by-host-header');
const stats = require('./middlewares/stats');
const lzmaMiddleware = require('./middlewares/lzma');
const cors = require('./middlewares/cors');
const user = require('./middlewares/user');
const sendResponse = require('./middlewares/send-response');
const syntaxError = require('./middlewares/syntax-error');
const errorMiddleware = require('./middlewares/error-middleware');
const MapRouter = require('./map/map-router');
const TemplateRouter = require('./template/template-router');
module.exports = class ApiRouter {
constructor ({ serverOptions, environmentOptions }) {
this.serverOptions = serverOptions;
const redisOptions = Object.assign({
name: 'windshaft-server',
unwatchOnRelease: false,
noReadyCheck: true
}, environmentOptions.redis);
const redisPool = new RedisPool(redisOptions);
redisPool.on('status', function (status) {
var keyPrefix = 'windshaft.redis-pool.' + status.name + '.db' + status.db + '.';
global.statsClient.gauge(keyPrefix + 'count', status.count);
global.statsClient.gauge(keyPrefix + 'unused', status.unused);
global.statsClient.gauge(keyPrefix + 'waiting', status.waiting);
});
const metadataBackend = cartodbRedis({ pool: redisPool });
const pgConnection = new PgConnection(metadataBackend);
const mapStore = new windshaft.storage.MapStore({
pool: redisPool,
expire_time: serverOptions.grainstore.default_layergroup_ttl
});
const rendererFactory = createRendererFactory({ redisPool, serverOptions, environmentOptions });
const rendererCacheOpts = Object.assign({
ttl: 60000, // 60 seconds TTL by default
statsInterval: 60000 // reports stats every milliseconds defined here
}, serverOptions.renderCache || {});
const rendererCache = new windshaft.cache.RendererCache(rendererFactory, rendererCacheOpts);
const rendererStatsReporter = new RendererStatsReporter(rendererCache, rendererCacheOpts.statsInterval);
rendererStatsReporter.start();
const tileBackend = new windshaft.backend.Tile(rendererCache);
const attributesBackend = new windshaft.backend.Attributes();
const previewBackend = new windshaft.backend.Preview(rendererCache);
const mapValidatorBackend = new windshaft.backend.MapValidator(tileBackend, attributesBackend);
const mapBackend = new windshaft.backend.Map(rendererCache, mapStore, mapValidatorBackend);
const surrogateKeysCacheBackends = createSurrogateKeysCacheBackends(serverOptions);
const surrogateKeysCache = new SurrogateKeysCache(surrogateKeysCacheBackends);
const templateMaps = createTemplateMaps({ redisPool, surrogateKeysCache });
const analysisStatusBackend = new AnalysisStatusBackend();
const analysisBackend = new AnalysisBackend(metadataBackend, serverOptions.analysis);
const dataviewBackend = new DataviewBackend(analysisBackend);
const statsBackend = new StatsBackend();
const userLimitsBackend = new UserLimitsBackend(metadataBackend, {
limits: {
cacheOnTimeout: serverOptions.renderer.mapnik.limits.cacheOnTimeout || false,
render: serverOptions.renderer.mapnik.limits.render || 0,
rateLimitsEnabled: global.environment.enabledFeatures.rateLimitsEnabled
}
});
const authBackend = new AuthBackend(pgConnection, metadataBackend, mapStore, templateMaps);
const layergroupAffectedTablesCache = new LayergroupAffectedTablesCache();
if (process.env.NODE_ENV === 'test') {
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
}
const pgQueryRunner = new PgQueryRunner(pgConnection);
const overviewsMetadataBackend = new OverviewsMetadataBackend(pgQueryRunner);
const filterStatsBackend = new FilterStatsApi(pgQueryRunner);
const tablesExtentBackend = new TablesExtentBackend(pgQueryRunner);
const mapConfigAdapter = new MapConfigAdapter(
new MapConfigNamedLayersAdapter(templateMaps, pgConnection),
new MapConfigBufferSizeAdapter(),
new SqlWrapMapConfigAdapter(),
new DataviewsWidgetsAdapter(),
new AnalysisMapConfigAdapter(analysisBackend),
new VectorMapConfigAdapter(pgConnection),
new AggregationMapConfigAdapter(pgConnection),
new MapConfigOverviewsAdapter(overviewsMetadataBackend, filterStatsBackend),
new TurboCartoAdapter()
);
const resourceLocator = new ResourceLocator(global.environment);
const layergroupMetadata = new LayergroupMetadata(resourceLocator);
const namedMapProviderCache = new NamedMapProviderCache(
templateMaps,
pgConnection,
metadataBackend,
userLimitsBackend,
mapConfigAdapter,
layergroupAffectedTablesCache
);
['update', 'delete'].forEach(function(eventType) {
templateMaps.on(eventType, namedMapProviderCache.invalidate.bind(namedMapProviderCache));
});
const collaborators = {
analysisStatusBackend,
attributesBackend,
dataviewBackend,
previewBackend,
tileBackend,
pgConnection,
mapStore,
userLimitsBackend,
layergroupAffectedTablesCache,
authBackend,
surrogateKeysCache,
templateMaps,
mapBackend,
metadataBackend,
mapConfigAdapter,
statsBackend,
layergroupMetadata,
namedMapProviderCache,
tablesExtentBackend
};
this.mapRouter = new MapRouter({ collaborators });
this.templateRouter = new TemplateRouter({ collaborators });
}
register (app) {
// FIXME: we need a better way to reset cache while running tests
if (process.env.NODE_ENV === 'test') {
app.layergroupAffectedTablesCache = this.layergroupAffectedTablesCache;
}
Object.keys(this.serverOptions.routes).forEach(apiVersion => {
const routes = this.serverOptions.routes[apiVersion];
const apiRouter = router({ mergeParams: true });
apiRouter.use(logger(this.serverOptions));
apiRouter.use(initializeStatusCode());
apiRouter.use(bodyParser.json());
apiRouter.use(servedByHostHeader());
apiRouter.use(stats({
enabled: this.serverOptions.useProfiler,
statsClient: global.statsClient
}));
apiRouter.use(lzmaMiddleware());
apiRouter.use(cors());
apiRouter.use(user());
this.templateRouter.register(apiRouter, routes.template.paths);
this.mapRouter.register(apiRouter, routes.map.paths);
apiRouter.use(sendResponse());
apiRouter.use(syntaxError());
apiRouter.use(errorMiddleware());
const apiPaths = routes.paths;
apiPaths.forEach(path => app.use(path, apiRouter));
});
}
};
function createTemplateMaps ({ redisPool, surrogateKeysCache }) {
const templateMaps = new TemplateMaps(redisPool, {
max_user_templates: global.environment.maxUserTemplates
});
function invalidateNamedMap (owner, templateName) {
var startTime = Date.now();
surrogateKeysCache.invalidate(new NamedMapsCacheEntry(owner, templateName), function(err) {
var logMessage = JSON.stringify({
username: owner,
type: 'named_map_invalidation',
elapsed: Date.now() - startTime,
error: !!err ? JSON.stringify(err.message) : undefined
});
if (err) {
global.logger.warn(logMessage);
} else {
global.logger.info(logMessage);
}
});
}
['update', 'delete'].forEach(function(eventType) {
templateMaps.on(eventType, invalidateNamedMap);
});
return templateMaps;
}
function createSurrogateKeysCacheBackends(serverOptions) {
var cacheBackends = [];
if (serverOptions.varnish_purge_enabled) {
cacheBackends.push(
new VarnishHttpCacheBackend(serverOptions.varnish_host, serverOptions.varnish_http_port)
);
}
if (serverOptions.fastly &&
!!serverOptions.fastly.enabled && !!serverOptions.fastly.apiKey && !!serverOptions.fastly.serviceId) {
cacheBackends.push(
new FastlyCacheBackend(serverOptions.fastly.apiKey, serverOptions.fastly.serviceId)
);
}
return cacheBackends;
}
const timeoutErrorTilePath = __dirname + '/../../../assets/render-timeout-fallback.png';
const timeoutErrorTile = require('fs').readFileSync(timeoutErrorTilePath, {encoding: null});
function createRendererFactory ({ redisPool, serverOptions, environmentOptions }) {
var onTileErrorStrategy;
if (environmentOptions.enabledFeatures.onTileErrorStrategy !== false) {
onTileErrorStrategy = function onTileErrorStrategy$TimeoutTile(err, tile, headers, stats, format, callback) {
function isRenderTimeoutError (err) {
return err.message === 'Render timed out';
}
function isDatasourceTimeoutError (err) {
return err.message && err.message.match(/canceling statement due to statement timeout/i);
}
function isTimeoutError (err) {
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
}
function isRasterFormat (format) {
return format === 'png' || format === 'jpg';
}
if (isTimeoutError(err) && isRasterFormat(format)) {
return callback(null, timeoutErrorTile, {
'Content-Type': 'image/png',
}, {});
} else {
return callback(err, tile, headers, stats);
}
};
}
const rendererFactory = new windshaft.renderer.Factory({
onTileErrorStrategy: onTileErrorStrategy,
mapnik: {
redisPool: redisPool,
grainstore: serverOptions.grainstore,
mapnik: serverOptions.renderer.mapnik
},
http: serverOptions.renderer.http,
mvt: serverOptions.renderer.mvt,
torque: serverOptions.renderer.torque
});
return rendererFactory;
}

View File

@@ -1,138 +0,0 @@
var assert = require('assert');
var step = require('step');
/**
*
* @param {PgConnection} pgConnection
* @param metadataBackend
* @param {MapStore} mapStore
* @param {TemplateMaps} templateMaps
* @constructor
* @type {AuthApi}
*/
function AuthApi(pgConnection, metadataBackend, mapStore, templateMaps) {
this.pgConnection = pgConnection;
this.metadataBackend = metadataBackend;
this.mapStore = mapStore;
this.templateMaps = templateMaps;
}
module.exports = AuthApi;
// Check if the user is authorized by a signer
//
// @param res express response object
// @param callback function(err, signed_by) signed_by will be
// null if the request is not signed by anyone
// or will be a string cartodb username otherwise.
//
AuthApi.prototype.authorizedBySigner = function(res, callback) {
if ( ! res.locals.token || ! res.locals.signer ) {
return callback(null, false); // no signer requested
}
var self = this;
var layergroup_id = res.locals.token;
var auth_token = res.locals.auth_token;
this.mapStore.load(layergroup_id, function(err, mapConfig) {
if (err) {
return callback(err);
}
var authorized = self.templateMaps.isAuthorized(mapConfig.obj().template, auth_token);
return callback(null, authorized);
});
};
// Check if a request is authorized by api_key
//
// @param user
// @param req express request object
// @param callback function(err, authorized)
// NOTE: authorized is expected to be 0 or 1 (integer)
//
AuthApi.prototype.authorizedByAPIKey = function(user, req, callback) {
var givenKey = req.query.api_key || req.query.map_key;
if ( ! givenKey && req.body ) {
// check also in request body
givenKey = req.body.api_key || req.body.map_key;
}
if ( ! givenKey ) {
return callback(null, 0); // no api key, no authorization...
}
var self = this;
step(
function () {
self.metadataBackend.getUserMapKey(user, this);
},
function checkApiKey(err, val){
assert.ifError(err);
return val && givenKey === val;
},
function finish(err, authorized) {
callback(err, authorized);
}
);
};
/**
* Check access authorization
*
* @param req - standard req object. Importantly contains table and host information
* @param res - standard res object. Contains the auth parameters in locals
* @param callback function(err, allowed) is access allowed not?
*/
AuthApi.prototype.authorize = function(req, res, callback) {
var self = this;
var user = res.locals.user;
step(
function () {
self.authorizedByAPIKey(user, req, this);
},
function checkApiKey(err, authorized){
req.profiler.done('authorizedByAPIKey');
assert.ifError(err);
// if not authorized by api_key, continue
if (!authorized) {
// not authorized by api_key, check if authorized by signer
return self.authorizedBySigner(res, this);
}
// authorized by api key, login as the given username and stop
self.pgConnection.setDBAuth(user, res.locals, function(err) {
callback(err, true); // authorized (or error)
});
},
function checkSignAuthorized(err, authorized) {
if (err) {
return callback(err);
}
if ( ! authorized ) {
// request not authorized by signer.
// if no signer name was given, let dbparams and
// PostgreSQL do the rest.
//
if ( ! res.locals.signer ) {
return callback(null, true); // authorized so far
}
// if signer name was given, return no authorization
return callback(null, false);
}
self.pgConnection.setDBAuth(user, res.locals, function(err) {
req.profiler.done('setDBAuth');
callback(err, true); // authorized (or error)
});
}
);
};

View File

@@ -1,37 +1,52 @@
var PSQL = require('cartodb-psql');
var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');
const PSQL = require('cartodb-psql');
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
const credentials = require('../middlewares/credentials');
const authorize = require('../middlewares/authorize');
const dbConnSetup = require('../middlewares/db-conn-setup');
const rateLimit = require('../middlewares/rate-limit');
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
const cacheControlHeader = require('../middlewares/cache-control-header');
const dbParamsFromResLocals = require('../../utils/database-params');
function AnalysesController(prepareContext) {
this.prepareContext = prepareContext;
}
module.exports = class AnalysesController {
constructor (pgConnection, authBackend, userLimitsBackend) {
this.pgConnection = pgConnection;
this.authBackend = authBackend;
this.userLimitsBackend = userLimitsBackend;
}
module.exports = AnalysesController;
register (mapRouter) {
mapRouter.get('/analyses/catalog', this.middlewares());
}
AnalysesController.prototype.register = function (app) {
app.get(
`${app.base_url_mapconfig}/analyses/catalog`,
cors(),
userMiddleware,
this.prepareContext,
this.createPGClient(),
this.getDataFromQuery({ queryTemplate: catalogQueryTpl, key: 'catalog' }),
this.getDataFromQuery({ queryTemplate: tablesQueryTpl, key: 'tables' }),
this.prepareResponse(),
this.setCacheControlHeader(),
this.sendResponse(),
this.unathorizedError()
);
middlewares () {
return [
credentials(),
authorize(this.authBackend),
dbConnSetup(this.pgConnection),
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.ANALYSIS_CATALOG),
cleanUpQueryParams(),
createPGClient(),
getDataFromQuery({ queryTemplate: catalogQueryTpl, key: 'catalog' }),
getDataFromQuery({ queryTemplate: tablesQueryTpl, key: 'tables' }),
prepareResponse(),
cacheControlHeader({ ttl: 10, revalidate: true }),
unauthorizedError()
];
}
};
AnalysesController.prototype.createPGClient = function () {
function createPGClient () {
return function createPGClientMiddleware (req, res, next) {
res.locals.pg = new PSQL(dbParamsFromReqParams(res.locals));
const dbParams = dbParamsFromResLocals(res.locals);
res.locals.pg = new PSQL(dbParams);
next();
};
};
}
AnalysesController.prototype.getDataFromQuery = function ({ queryTemplate, key }) {
function getDataFromQuery({ queryTemplate, key }) {
const readOnlyTransactionOn = true;
return function getCatalogMiddleware(req, res, next) {
@@ -48,9 +63,9 @@ AnalysesController.prototype.getDataFromQuery = function ({ queryTemplate, key }
next();
}, readOnlyTransactionOn);
};
};
}
AnalysesController.prototype.prepareResponse = function () {
function prepareResponse () {
return function prepareResponseMiddleware (req, res, next) {
const { catalog, tables } = res.locals;
@@ -87,32 +102,14 @@ AnalysesController.prototype.prepareResponse = function () {
return -1;
});
res.statusCode = 200;
res.body = { catalog: analysisCatalog };
next();
};
};
}
AnalysesController.prototype.setCacheControlHeader = function () {
return function setCacheControlHeaderMiddleware (req, res, next) {
res.set('Cache-Control', 'public,max-age=10,must-revalidate');
next();
};
};
AnalysesController.prototype.sendResponse = function() {
return function sendResponseMiddleware (req, res) {
res.status(200);
if (req.query && req.query.callback) {
res.jsonp(res.body);
} else {
res.json(res.body);
}
};
};
AnalysesController.prototype.unathorizedError = function () {
function unauthorizedError () {
return function unathorizedErrorMiddleware(err, req, res, next) {
if (err.message.match(/permission\sdenied/)) {
err = new Error('Unauthorized');
@@ -121,7 +118,7 @@ AnalysesController.prototype.unathorizedError = function () {
next(err);
};
};
}
const catalogQueryTpl = ctx => `
SELECT analysis_def->>'type' as type, * FROM cdb_analysis_catalog WHERE username = '${ctx._username}'
@@ -145,23 +142,3 @@ var tablesQueryTpl = ctx => `
FROM analysis_tables
ORDER BY size DESC
`;
function dbParamsFromReqParams(params) {
var dbParams = {};
if ( params.dbuser ) {
dbParams.user = params.dbuser;
}
if ( params.dbpassword ) {
dbParams.pass = params.dbpassword;
}
if ( params.dbhost ) {
dbParams.host = params.dbhost;
}
if ( params.dbport ) {
dbParams.port = params.dbport;
}
if ( params.dbname ) {
dbParams.dbname = params.dbname;
}
return dbParams;
}

View File

@@ -0,0 +1,59 @@
const layergroupToken = require('../middlewares/layergroup-token');
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
const credentials = require('../middlewares/credentials');
const dbConnSetup = require('../middlewares/db-conn-setup');
const authorize = require('../middlewares/authorize');
const rateLimit = require('../middlewares/rate-limit');
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
const dbParamsFromResLocals = require('../../utils/database-params');
module.exports = class AnalysisLayergroupController {
constructor (analysisStatusBackend, pgConnection, userLimitsBackend, authBackend) {
this.analysisStatusBackend = analysisStatusBackend;
this.pgConnection = pgConnection;
this.userLimitsBackend = userLimitsBackend;
this.authBackend = authBackend;
}
register (mapRouter) {
mapRouter.get('/:token/analysis/node/:nodeId', this.middlewares());
}
middlewares () {
return [
layergroupToken(),
credentials(),
authorize(this.authBackend),
dbConnSetup(this.pgConnection),
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.ANALYSIS),
cleanUpQueryParams(),
analysisNodeStatus(this.analysisStatusBackend)
];
}
};
function analysisNodeStatus (analysisStatusBackend) {
return function analysisNodeStatusMiddleware(req, res, next) {
const { nodeId } = req.params;
const dbParams = dbParamsFromResLocals(res.locals);
analysisStatusBackend.getNodeStatus(nodeId, dbParams, (err, nodeStatus, stats = {}) => {
req.profiler.add(stats);
if (err) {
err.label = 'GET NODE STATUS';
return next(err);
}
res.set({
'Cache-Control': 'public,max-age=5',
'Last-Modified': new Date().toUTCString()
});
res.statusCode = 200;
res.body = nodeStatus;
next();
});
};
}

View File

@@ -0,0 +1,213 @@
const windshaft = require('windshaft');
const MapConfig = windshaft.model.MapConfig;
const Datasource = windshaft.model.Datasource;
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
const credentials = require('../middlewares/credentials');
const dbConnSetup = require('../middlewares/db-conn-setup');
const authorize = require('../middlewares/authorize');
const initProfiler = require('../middlewares/init-profiler');
const checkJsonContentType = require('../middlewares/check-json-content-type');
const incrementMapViewCount = require('../middlewares/increment-map-view-count');
const augmentLayergroupData = require('../middlewares/augment-layergroup-data');
const cacheControlHeader = require('../middlewares/cache-control-header');
const cacheChannelHeader = require('../middlewares/cache-channel-header');
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
const lastModifiedHeader = require('../middlewares/last-modified-header');
const lastUpdatedTimeLayergroup = require('../middlewares/last-updated-time-layergroup');
const layerStats = require('../middlewares/layer-stats');
const layergroupIdHeader = require('../middlewares/layergroup-id-header');
const layergroupMetadata = require('../middlewares/layergroup-metadata');
const mapError = require('../middlewares/map-error');
const CreateLayergroupMapConfigProvider = require('../../models/mapconfig/provider/create-layergroup-provider');
const rateLimit = require('../middlewares/rate-limit');
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
module.exports = class AnonymousMapController {
/**
* @param {AuthBackend} authBackend
* @param {PgConnection} pgConnection
* @param {TemplateMaps} templateMaps
* @param {MapBackend} mapBackend
* @param metadataBackend
* @param {SurrogateKeysCache} surrogateKeysCache
* @param {UserLimitsBackend} userLimitsBackend
* @param {LayergroupAffectedTables} layergroupAffectedTables
* @param {MapConfigAdapter} mapConfigAdapter
* @param {StatsBackend} statsBackend
* @constructor
*/
constructor (
pgConnection,
templateMaps,
mapBackend,
metadataBackend,
surrogateKeysCache,
userLimitsBackend,
layergroupAffectedTables,
mapConfigAdapter,
statsBackend,
authBackend,
layergroupMetadata
) {
this.pgConnection = pgConnection;
this.templateMaps = templateMaps;
this.mapBackend = mapBackend;
this.metadataBackend = metadataBackend;
this.surrogateKeysCache = surrogateKeysCache;
this.userLimitsBackend = userLimitsBackend;
this.layergroupAffectedTables = layergroupAffectedTables;
this.mapConfigAdapter = mapConfigAdapter;
this.statsBackend = statsBackend;
this.authBackend = authBackend;
this.layergroupMetadata = layergroupMetadata;
}
register (mapRouter) {
mapRouter.options('/');
mapRouter.get('/', this.middlewares());
mapRouter.post('/', this.middlewares());
}
middlewares () {
const isTemplateInstantiation = false;
const useTemplateHash = false;
const includeQuery = true;
const label = 'ANONYMOUS LAYERGROUP';
const addContext = true;
return [
credentials(),
authorize(this.authBackend),
dbConnSetup(this.pgConnection),
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.ANONYMOUS),
cleanUpQueryParams(['aggregation']),
initProfiler(isTemplateInstantiation),
checkJsonContentType(),
checkCreateLayergroup(),
prepareAdapterMapConfig(this.mapConfigAdapter),
createLayergroup (
this.mapBackend,
this.userLimitsBackend,
this.pgConnection,
this.layergroupAffectedTables
),
incrementMapViewCount(this.metadataBackend),
augmentLayergroupData(),
cacheControlHeader({ ttl: global.environment.varnish.layergroupTtl || 86400, revalidate: true }),
cacheChannelHeader(),
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
lastModifiedHeader(),
lastUpdatedTimeLayergroup(),
layerStats(this.pgConnection, this.statsBackend),
layergroupIdHeader(this.templateMaps, useTemplateHash),
layergroupMetadata(this.layergroupMetadata, includeQuery),
mapError({ label, addContext })
];
}
};
function checkCreateLayergroup () {
return function checkCreateLayergroupMiddleware (req, res, next) {
if (req.method === 'GET') {
const { config } = req.query;
if (!config) {
return next(new Error('layergroup GET needs a "config" parameter'));
}
try {
req.body = JSON.parse(config);
} catch (err) {
return next(err);
}
}
req.profiler.done('checkCreateLayergroup');
return next();
};
}
function prepareAdapterMapConfig (mapConfigAdapter) {
return function prepareAdapterMapConfigMiddleware(req, res, next) {
const requestMapConfig = req.body;
const { user, api_key } = res.locals;
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
const params = Object.assign({ dbuser, dbname, dbpassword, dbhost, dbport }, req.query);
const context = {
analysisConfiguration: {
user,
db: {
host: dbhost,
port: dbport,
dbname: dbname,
user: dbuser,
pass: dbpassword
},
batch: {
username: user,
apiKey: api_key
}
}
};
mapConfigAdapter.getMapConfig(user, requestMapConfig, params, context, (err, requestMapConfig) => {
req.profiler.done('anonymous.getMapConfig');
if (err) {
return next(err);
}
req.body = requestMapConfig;
res.locals.context = context;
next();
});
};
}
function createLayergroup (mapBackend, userLimitsBackend, pgConnection, affectedTablesCache) {
return function createLayergroupMiddleware (req, res, next) {
const requestMapConfig = req.body;
const { context } = res.locals;
const { user, cache_buster, api_key } = res.locals;
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
const params = {
cache_buster, api_key,
dbuser, dbname, dbpassword, dbhost, dbport
};
const datasource = context.datasource || Datasource.EmptyDatasource();
const mapConfig = new MapConfig(requestMapConfig, datasource);
const mapConfigProvider = new CreateLayergroupMapConfigProvider(
mapConfig,
user,
userLimitsBackend,
pgConnection,
affectedTablesCache,
params
);
res.locals.mapConfig = mapConfig;
res.locals.analysesResults = context.analysesResults;
const mapParams = { dbuser, dbname, dbpassword, dbhost, dbport };
mapBackend.createLayergroup(mapConfig, mapParams, mapConfigProvider, (err, layergroup, stats = {}) => {
req.profiler.add(stats);
if (err) {
return next(err);
}
res.statusCode = 200;
res.body = layergroup;
res.locals.mapConfigProvider = mapConfigProvider;
next();
});
};
}

View File

@@ -0,0 +1,89 @@
const layergroupToken = require('../middlewares/layergroup-token');
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
const credentials = require('../middlewares/credentials');
const dbConnSetup = require('../middlewares/db-conn-setup');
const authorize = require('../middlewares/authorize');
const rateLimit = require('../middlewares/rate-limit');
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
const createMapStoreMapConfigProvider = require('../middlewares/map-store-map-config-provider');
const cacheControlHeader = require('../middlewares/cache-control-header');
const cacheChannelHeader = require('../middlewares/cache-channel-header');
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
const lastModifiedHeader = require('../middlewares/last-modified-header');
module.exports = class AttributesLayergroupController {
constructor (
attributesBackend,
pgConnection,
mapStore,
userLimitsBackend,
layergroupAffectedTablesCache,
authBackend,
surrogateKeysCache
) {
this.attributesBackend = attributesBackend;
this.pgConnection = pgConnection;
this.mapStore = mapStore;
this.userLimitsBackend = userLimitsBackend;
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
this.authBackend = authBackend;
this.surrogateKeysCache = surrogateKeysCache;
}
register (mapRouter) {
mapRouter.get('/:token/:layer/attributes/:fid', this.middlewares());
}
middlewares () {
return [
layergroupToken(),
credentials(),
authorize(this.authBackend),
dbConnSetup(this.pgConnection),
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.ATTRIBUTES),
cleanUpQueryParams(),
createMapStoreMapConfigProvider(
this.mapStore,
this.userLimitsBackend,
this.pgConnection,
this.layergroupAffectedTablesCache
),
getFeatureAttributes(this.attributesBackend),
cacheControlHeader(),
cacheChannelHeader(),
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
lastModifiedHeader()
];
}
};
function getFeatureAttributes (attributesBackend) {
return function getFeatureAttributesMiddleware (req, res, next) {
req.profiler.start('windshaft.maplayer_attribute');
const { mapConfigProvider } = res.locals;
const { token } = res.locals;
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
const { layer, fid } = req.params;
const params = {
token,
dbuser, dbname, dbpassword, dbhost, dbport,
layer, fid
};
attributesBackend.getFeatureAttributes(mapConfigProvider, params, false, (err, tile, stats = {}) => {
req.profiler.add(stats);
if (err) {
err.label = 'GET ATTRIBUTES';
return next(err);
}
res.statusCode = 200;
res.body = tile;
next();
});
};
}

View File

@@ -0,0 +1,142 @@
const layergroupToken = require('../middlewares/layergroup-token');
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
const credentials = require('../middlewares/credentials');
const dbConnSetup = require('../middlewares/db-conn-setup');
const authorize = require('../middlewares/authorize');
const rateLimit = require('../middlewares/rate-limit');
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
const createMapStoreMapConfigProvider = require('../middlewares/map-store-map-config-provider');
const cacheControlHeader = require('../middlewares/cache-control-header');
const cacheChannelHeader = require('../middlewares/cache-channel-header');
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
const lastModifiedHeader = require('../middlewares/last-modified-header');
const ALLOWED_DATAVIEW_QUERY_PARAMS = [
'filters', // json
'own_filter', // 0, 1
'no_filters', // 0, 1
'bbox', // w,s,e,n
'start', // number
'end', // number
'column_type', // string
'bins', // number
'aggregation', //string
'offset', // number
'q', // widgets search
'categories', // number
];
module.exports = class DataviewLayergroupController {
constructor (
dataviewBackend,
pgConnection,
mapStore,
userLimitsBackend,
layergroupAffectedTablesCache,
authBackend,
surrogateKeysCache
) {
this.dataviewBackend = dataviewBackend;
this.pgConnection = pgConnection;
this.mapStore = mapStore;
this.userLimitsBackend = userLimitsBackend;
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
this.authBackend = authBackend;
this.surrogateKeysCache = surrogateKeysCache;
}
register (mapRouter) {
// Undocumented/non-supported API endpoint methods.
// Use at your own peril.
mapRouter.get('/:token/dataview/:dataviewName', this.middlewares({
action: 'get',
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW
}));
mapRouter.get('/:token/:layer/widget/:dataviewName', this.middlewares({
action: 'get',
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW
}));
mapRouter.get('/:token/dataview/:dataviewName/search', this.middlewares({
action: 'search',
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW_SEARCH
}));
mapRouter.get('/:token/:layer/widget/:dataviewName/search', this.middlewares({
action: 'search',
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW_SEARCH
}));
}
middlewares ({ action, rateLimitGroup }) {
return [
layergroupToken(),
credentials(),
authorize(this.authBackend),
dbConnSetup(this.pgConnection),
rateLimit(this.userLimitsBackend, rateLimitGroup),
cleanUpQueryParams(ALLOWED_DATAVIEW_QUERY_PARAMS),
createMapStoreMapConfigProvider(
this.mapStore,
this.userLimitsBackend,
this.pgConnection,
this.layergroupAffectedTablesCache
),
action === 'search' ? dataviewSearch(this.dataviewBackend) : getDataview(this.dataviewBackend),
cacheControlHeader(),
cacheChannelHeader(),
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
lastModifiedHeader()
];
}
};
function getDataview (dataviewBackend) {
return function getDataviewMiddleware (req, res, next) {
const { user, mapConfigProvider } = res.locals;
const { dataviewName } = req.params;
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
const params = Object.assign({ dataviewName, dbuser, dbname, dbpassword, dbhost, dbport }, req.query);
dataviewBackend.getDataview(mapConfigProvider, user, params, (err, dataview, stats = {}) => {
req.profiler.add(stats);
if (err) {
err.label = 'GET DATAVIEW';
return next(err);
}
res.statusCode = 200;
res.body = dataview;
next();
});
};
}
function dataviewSearch (dataviewBackend) {
return function dataviewSearchMiddleware (req, res, next) {
const { user, mapConfigProvider } = res.locals;
const { dataviewName } = req.params;
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
const params = Object.assign({ dbuser, dbname, dbpassword, dbhost, dbport }, req.query);
dataviewBackend.search(mapConfigProvider, user, dataviewName, params, (err, searchResult, stats = {}) => {
req.profiler.add(stats);
if (err) {
err.label = 'GET DATAVIEW SEARCH';
return next(err);
}
res.statusCode = 200;
res.body = searchResult;
next();
});
};
}

View File

@@ -0,0 +1,129 @@
const { Router: router } = require('express');
const AnalysisLayergroupController = require('./analysis-layergroup-controller');
const AttributesLayergroupController = require('./attributes-layergroup-controller');
const DataviewLayergroupController = require('./dataview-layergroup-controller');
const PreviewLayergroupController = require('./preview-layergroup-controller');
const TileLayergroupController = require('./tile-layergroup-controller');
const AnonymousMapController = require('./anonymous-map-controller');
const PreviewTemplateController = require('./preview-template-controller');
const AnalysesCatalogController = require('./analyses-catalog-controller');
module.exports = class MapRouter {
constructor ({ collaborators }) {
const {
analysisStatusBackend,
attributesBackend,
dataviewBackend,
previewBackend,
tileBackend,
pgConnection,
mapStore,
userLimitsBackend,
layergroupAffectedTablesCache,
authBackend,
surrogateKeysCache,
templateMaps,
mapBackend,
metadataBackend,
mapConfigAdapter,
statsBackend,
layergroupMetadata,
namedMapProviderCache,
tablesExtentBackend
} = collaborators;
this.analysisLayergroupController = new AnalysisLayergroupController(
analysisStatusBackend,
pgConnection,
userLimitsBackend,
authBackend
);
this.attributesLayergroupController = new AttributesLayergroupController(
attributesBackend,
pgConnection,
mapStore,
userLimitsBackend,
layergroupAffectedTablesCache,
authBackend,
surrogateKeysCache
);
this.dataviewLayergroupController = new DataviewLayergroupController(
dataviewBackend,
pgConnection,
mapStore,
userLimitsBackend,
layergroupAffectedTablesCache,
authBackend,
surrogateKeysCache
);
this.previewLayergroupController = new PreviewLayergroupController(
previewBackend,
pgConnection,
mapStore,
userLimitsBackend,
layergroupAffectedTablesCache,
authBackend,
surrogateKeysCache
);
this.tileLayergroupController = new TileLayergroupController(
tileBackend,
pgConnection,
mapStore,
userLimitsBackend,
layergroupAffectedTablesCache,
authBackend,
surrogateKeysCache
);
this.anonymousMapController = new AnonymousMapController(
pgConnection,
templateMaps,
mapBackend,
metadataBackend,
surrogateKeysCache,
userLimitsBackend,
layergroupAffectedTablesCache,
mapConfigAdapter,
statsBackend,
authBackend,
layergroupMetadata
);
this.previewTemplateController = new PreviewTemplateController(
namedMapProviderCache,
previewBackend,
surrogateKeysCache,
tablesExtentBackend,
metadataBackend,
pgConnection,
authBackend,
userLimitsBackend
);
this.analysesController = new AnalysesCatalogController(
pgConnection,
authBackend,
userLimitsBackend
);
}
register (apiRouter, mapPaths) {
const mapRouter = router({ mergeParams: true });
this.analysisLayergroupController.register(mapRouter);
this.attributesLayergroupController.register(mapRouter);
this.dataviewLayergroupController.register(mapRouter);
this.previewLayergroupController.register(mapRouter);
this.tileLayergroupController.register(mapRouter);
this.anonymousMapController.register(mapRouter);
this.previewTemplateController.register(mapRouter);
this.analysesController.register(mapRouter);
mapPaths.forEach(path => apiRouter.use(path, mapRouter));
}
};

View File

@@ -0,0 +1,156 @@
const layergroupToken = require('../middlewares/layergroup-token');
const coordinates = require('../middlewares/coordinates');
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
const credentials = require('../middlewares/credentials');
const noop = require('../middlewares/noop');
const dbConnSetup = require('../middlewares/db-conn-setup');
const authorize = require('../middlewares/authorize');
const rateLimit = require('../middlewares/rate-limit');
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
const createMapStoreMapConfigProvider = require('../middlewares/map-store-map-config-provider');
const cacheControlHeader = require('../middlewares/cache-control-header');
const cacheChannelHeader = require('../middlewares/cache-channel-header');
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
const lastModifiedHeader = require('../middlewares/last-modified-header');
const checkStaticImageFormat = require('../middlewares/check-static-image-format');
module.exports = class PreviewLayergroupController {
constructor (
previewBackend,
pgConnection,
mapStore,
userLimitsBackend,
layergroupAffectedTablesCache,
authBackend,
surrogateKeysCache
) {
this.previewBackend = previewBackend;
this.pgConnection = pgConnection;
this.mapStore = mapStore;
this.userLimitsBackend = userLimitsBackend;
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
this.authBackend = authBackend;
this.surrogateKeysCache = surrogateKeysCache;
}
register (mapRouter) {
mapRouter.get('/static/center/:token/:z/:lat/:lng/:width/:height.:format', this.middlewares({
validateZoom: true,
previewType: 'centered'
}));
mapRouter.get('/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', this.middlewares({
validateZoom: false,
previewType: 'bbox'
}));
}
middlewares ({ validateZoom, previewType }) {
const forcedFormat = 'png';
let getPreviewImage;
if (previewType === 'centered') {
getPreviewImage = getPreviewImageByCenter;
}
if (previewType === 'bbox') {
getPreviewImage = getPreviewImageByBoundingBox;
}
return [
layergroupToken(),
validateZoom ? coordinates({ z: true, x: false, y: false }) : noop(),
credentials(),
authorize(this.authBackend),
dbConnSetup(this.pgConnection),
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.STATIC),
cleanUpQueryParams(['layer']),
checkStaticImageFormat(),
createMapStoreMapConfigProvider(
this.mapStore,
this.userLimitsBackend,
this.pgConnection,
this.layergroupAffectedTablesCache,
forcedFormat
),
getPreviewImage(this.previewBackend),
cacheControlHeader(),
cacheChannelHeader(),
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
lastModifiedHeader()
];
}
};
function getPreviewImageByCenter (previewBackend) {
return function getPreviewImageByCenterMiddleware (req, res, next) {
const width = +req.params.width;
const height = +req.params.height;
const zoom = +req.params.z;
const center = {
lng: +req.params.lng,
lat: +req.params.lat
};
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
const { mapConfigProvider: provider } = res.locals;
previewBackend.getImage(provider, format, width, height, zoom, center, (err, image, headers, stats = {}) => {
req.profiler.done(`render-${format}`);
req.profiler.add(stats);
if (err) {
err.label = 'STATIC_MAP';
return next(err);
}
if (headers) {
res.set(headers);
}
res.set('Content-Type', headers['Content-Type'] || `image/${format}`);
res.statusCode = 200;
res.body = image;
next();
});
};
}
function getPreviewImageByBoundingBox (previewBackend) {
return function getPreviewImageByBoundingBoxMiddleware (req, res, next) {
const width = +req.params.width;
const height = +req.params.height;
const bounds = {
west: +req.params.west,
north: +req.params.north,
east: +req.params.east,
south: +req.params.south
};
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
const { mapConfigProvider: provider } = res.locals;
previewBackend.getImage(provider, format, width, height, bounds, (err, image, headers, stats = {}) => {
req.profiler.done(`render-${format}`);
req.profiler.add(stats);
if (err) {
err.label = 'STATIC_MAP';
return next(err);
}
if (headers) {
res.set(headers);
}
res.set('Content-Type', headers['Content-Type'] || `image/${format}`);
res.statusCode = 200;
res.body = image;
next();
});
};
}

View File

@@ -0,0 +1,367 @@
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
const credentials = require('../middlewares/credentials');
const dbConnSetup = require('../middlewares/db-conn-setup');
const authorize = require('../middlewares/authorize');
const namedMapProvider = require('../middlewares/named-map-provider');
const cacheControlHeader = require('../middlewares/cache-control-header');
const cacheChannelHeader = require('../middlewares/cache-channel-header');
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
const lastModifiedHeader = require('../middlewares/last-modified-header');
const checkStaticImageFormat = require('../middlewares/check-static-image-format');
const rateLimit = require('../middlewares/rate-limit');
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
const DEFAULT_ZOOM_CENTER = {
zoom: 1,
center: {
lng: 0,
lat: 0
}
};
function numMapper(n) {
return +n;
}
module.exports = class PreviewTemplateController {
constructor (
namedMapProviderCache,
previewBackend,
surrogateKeysCache,
tablesExtentBackend,
metadataBackend,
pgConnection,
authBackend,
userLimitsBackend
) {
this.namedMapProviderCache = namedMapProviderCache;
this.previewBackend = previewBackend;
this.surrogateKeysCache = surrogateKeysCache;
this.tablesExtentBackend = tablesExtentBackend;
this.metadataBackend = metadataBackend;
this.pgConnection = pgConnection;
this.authBackend = authBackend;
this.userLimitsBackend = userLimitsBackend;
}
register (mapRouter) {
mapRouter.get('/static/named/:template_id/:width/:height.:format', this.middlewares());
}
middlewares () {
return [
credentials(),
authorize(this.authBackend),
dbConnSetup(this.pgConnection),
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.STATIC_NAMED),
cleanUpQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
checkStaticImageFormat(),
namedMapProvider({
namedMapProviderCache: this.namedMapProviderCache,
label: 'STATIC_VIZ_MAP', forcedFormat: 'png'
}),
getTemplate({ label: 'STATIC_VIZ_MAP' }),
prepareLayerFilterFromPreviewLayers({
namedMapProviderCache: this.namedMapProviderCache,
label: 'STATIC_VIZ_MAP'
}),
getStaticImageOptions({ tablesExtentBackend: this.tablesExtentBackend }),
getImage({ previewBackend: this.previewBackend, label: 'STATIC_VIZ_MAP' }),
setContentTypeHeader(),
incrementMapViews({ metadataBackend: this.metadataBackend }),
cacheControlHeader(),
cacheChannelHeader(),
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
lastModifiedHeader()
];
}
};
function getTemplate ({ label }) {
return function getTemplateMiddleware (req, res, next) {
const { mapConfigProvider } = res.locals;
mapConfigProvider.getTemplate((err, template) => {
if (err) {
err.label = label;
return next(err);
}
res.locals.template = template;
next();
});
};
}
function prepareLayerFilterFromPreviewLayers ({ namedMapProviderCache, label }) {
return function prepareLayerFilterFromPreviewLayersMiddleware (req, res, next) {
const { template } = res.locals;
const { config, auth_token } = req.query;
if (!template || !template.view || !template.view.preview_layers) {
return next();
}
var previewLayers = template.view.preview_layers;
var layerVisibilityFilter = [];
template.layergroup.layers.forEach((layer, index) => {
if (previewLayers[''+index] !== false && previewLayers[layer.id] !== false) {
layerVisibilityFilter.push(''+index);
}
});
if (!layerVisibilityFilter.length) {
return next();
}
const { user, token, cache_buster, api_key } = res.locals;
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
const { template_id, format } = req.params;
const params = {
user, token, cache_buster, api_key,
dbuser, dbname, dbpassword, dbhost, dbport,
template_id, format
};
// overwrites 'all' default filter
params.layer = layerVisibilityFilter.join(',');
// recreates the provider
namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, provider) => {
if (err) {
err.label = label;
return next(err);
}
res.locals.mapConfigProvider = provider;
next();
});
};
}
function getStaticImageOptions ({ tablesExtentBackend }) {
return function getStaticImageOptionsMiddleware(req, res, next) {
const { user, mapConfigProvider, template } = res.locals;
const { zoom, lon, lat, bbox } = req.query;
const params = { zoom, lon, lat, bbox };
const imageOpts = getImageOptions(params, template);
if (imageOpts) {
res.locals.imageOpts = imageOpts;
return next();
}
res.locals.imageOpts = DEFAULT_ZOOM_CENTER;
mapConfigProvider.createAffectedTables((err, affectedTables) => {
if (err) {
return next();
}
var tables = affectedTables.tables || [];
if (tables.length === 0) {
return next();
}
tablesExtentBackend.getBounds(user, tables, (err, bounds) => {
if (err) {
return next();
}
res.locals.imageOpts = bounds;
return next();
});
});
};
}
function getImageOptions (params, template) {
const { zoom, lon, lat, bbox } = params;
let imageOpts = getImageOptionsFromCoordinates(zoom, lon, lat);
if (imageOpts) {
return imageOpts;
}
imageOpts = getImageOptionsFromBoundingBox(bbox);
if (imageOpts) {
return imageOpts;
}
imageOpts = getImageOptionsFromTemplate(template, zoom);
if (imageOpts) {
return imageOpts;
}
}
function getImageOptionsFromCoordinates (zoom, lon, lat) {
if ([zoom, lon, lat].map(numMapper).every(Number.isFinite)) {
return {
zoom: zoom,
center: {
lng: lon,
lat: lat
}
};
}
}
function getImageOptionsFromTemplate (template, zoom) {
if (template.view) {
var zoomCenter = templateZoomCenter(template.view);
if (zoomCenter) {
if (Number.isFinite(+zoom)) {
zoomCenter.zoom = +zoom;
}
return zoomCenter;
}
var bounds = templateBounds(template.view);
if (bounds) {
return bounds;
}
}
}
function getImageOptionsFromBoundingBox (bbox = '') {
var _bbox = bbox.split(',').map(numMapper);
if (_bbox.length === 4 && _bbox.every(Number.isFinite)) {
return {
bounds: {
west: _bbox[0],
south: _bbox[1],
east: _bbox[2],
north: _bbox[3]
}
};
}
}
function getImage({ previewBackend, label }) {
return function getImageMiddleware (req, res, next) {
const { imageOpts, mapConfigProvider } = res.locals;
const { zoom, center, bounds } = imageOpts;
let { width, height } = req.params;
width = +width;
height = +height;
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
if (zoom !== undefined && center) {
return previewBackend.getImage(mapConfigProvider, format, width, height, zoom, center,
(err, image, headers, stats) => {
req.profiler.add(stats);
if (err) {
err.label = label;
return next(err);
}
if (headers) {
res.set(headers);
}
res.statusCode = 200;
res.body = image;
next();
});
}
previewBackend.getImage(mapConfigProvider, format, width, height, bounds, (err, image, headers, stats) => {
req.profiler.add(stats);
req.profiler.done('render-' + format);
if (err) {
err.label = label;
return next(err);
}
if (headers) {
res.set(headers);
}
res.statusCode = 200;
res.body = image;
next();
});
};
}
function setContentTypeHeader () {
return function setContentTypeHeaderMiddleware(req, res, next) {
res.set('Content-Type', res.get('content-type') || res.get('Content-Type') || 'image/png');
next();
};
}
function incrementMapViewsError (ctx) {
return `ERROR: failed to increment mapview count for user '${ctx.user}': ${ctx.err}`;
}
function incrementMapViews ({ metadataBackend }) {
return function incrementMapViewsMiddleware(req, res, next) {
const { user, mapConfigProvider } = res.locals;
mapConfigProvider.getMapConfig((err, mapConfig) => {
if (err) {
global.logger.log(incrementMapViewsError({ user, err }));
return next();
}
const statTag = mapConfig.obj().stat_tag;
metadataBackend.incMapviewCount(user, statTag, (err) => {
if (err) {
global.logger.log(incrementMapViewsError({ user, err }));
}
next();
});
});
};
}
function templateZoomCenter(view) {
if (view.zoom !== undefined && view.center) {
return {
zoom: view.zoom,
center: view.center
};
}
return false;
}
function templateBounds(view) {
if (view.bounds) {
var hasAllBounds = ['west', 'south', 'east', 'north'].every(prop => Number.isFinite(view.bounds[prop]));
if (hasAllBounds) {
return {
bounds: {
west: view.bounds.west,
south: view.bounds.south,
east: view.bounds.east,
north: view.bounds.north
}
};
} else {
return false;
}
}
return false;
}

View File

@@ -0,0 +1,170 @@
const layergroupToken = require('../middlewares/layergroup-token');
const coordinates = require('../middlewares/coordinates');
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
const credentials = require('../middlewares/credentials');
const dbConnSetup = require('../middlewares/db-conn-setup');
const authorize = require('../middlewares/authorize');
const rateLimit = require('../middlewares/rate-limit');
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
const createMapStoreMapConfigProvider = require('../middlewares/map-store-map-config-provider');
const cacheControlHeader = require('../middlewares/cache-control-header');
const cacheChannelHeader = require('../middlewares/cache-channel-header');
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
const lastModifiedHeader = require('../middlewares/last-modified-header');
const vectorError = require('../middlewares/vector-error');
const SUPPORTED_FORMATS = {
grid_json: true,
json_torque: true,
torque_json: true,
png: true,
png32: true,
mvt: true
};
module.exports = class TileLayergroupController {
constructor (
tileBackend,
pgConnection,
mapStore,
userLimitsBackend,
layergroupAffectedTablesCache,
authBackend,
surrogateKeysCache
) {
this.tileBackend = tileBackend;
this.pgConnection = pgConnection;
this.mapStore = mapStore;
this.userLimitsBackend = userLimitsBackend;
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
this.authBackend = authBackend;
this.surrogateKeysCache = surrogateKeysCache;
}
register (mapRouter) {
// REGEXP: doesn't match with `val`
const not = (val) => `(?!${val})([^\/]+?)`;
// Sadly the path that matches 1 also matches with 2 so we need to tell to express
// that performs only the middlewares of the first path that matches
// for that we use one array to group all paths.
mapRouter.get([
`/:token/:z/:x/:y@:scale_factor?x.:format`, // 1
`/:token/:z/:x/:y.:format`, // 2
`/:token${not('static')}/:layer/:z/:x/:y.(:format)`
], this.middlewares());
}
middlewares () {
return [
layergroupToken(),
coordinates(),
credentials(),
authorize(this.authBackend),
dbConnSetup(this.pgConnection),
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.TILE),
cleanUpQueryParams(),
createMapStoreMapConfigProvider(
this.mapStore,
this.userLimitsBackend,
this.pgConnection,
this.layergroupAffectedTablesCache
),
getTile(this.tileBackend),
cacheControlHeader(),
cacheChannelHeader(),
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
lastModifiedHeader(),
incrementSuccessMetrics(global.statsClient),
incrementErrorMetrics(global.statsClient),
tileError(),
vectorError()
];
}
};
function parseFormat (format = '') {
const prettyFormat = format.replace('.', '_');
return SUPPORTED_FORMATS[prettyFormat] ? prettyFormat : 'invalid';
}
function getStatusCode(tile, format){
return tile.length === 0 && format === 'mvt' ? 204 : 200;
}
function getTile (tileBackend) {
return function getTileMiddleware (req, res, next) {
req.profiler.start(`windshaft.${req.params.layer ? 'maplayer_tile' : 'map_tile'}`);
const { mapConfigProvider } = res.locals;
const { token } = res.locals;
const { layer, z, x, y, format } = req.params;
const params = { token, layer, z, x, y, format };
tileBackend.getTile(mapConfigProvider, params, (err, tile, headers, stats = {}) => {
req.profiler.add(stats);
if (err) {
return next(err);
}
if (headers) {
res.set(headers);
}
const formatStat = parseFormat(req.params.format);
res.statusCode = getStatusCode(tile, formatStat);
res.body = tile;
next();
});
};
}
function incrementSuccessMetrics (statsClient) {
return function incrementSuccessMetricsMiddleware (req, res, next) {
const formatStat = parseFormat(req.params.format);
statsClient.increment('windshaft.tiles.success');
statsClient.increment(`windshaft.tiles.${formatStat}.success`);
next();
};
}
function incrementErrorMetrics (statsClient) {
return function incrementErrorMetricsMiddleware (err, req, res, next) {
const formatStat = parseFormat(req.params.format);
statsClient.increment('windshaft.tiles.error');
statsClient.increment(`windshaft.tiles.${formatStat}.error`);
next(err);
};
}
function tileError () {
return function tileErrorMiddleware (err, req, res, next) {
if (err.message === 'Tile does not exist' && req.params.format === 'mvt') {
res.statusCode = 204;
return next();
}
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
let errMsg = err.message ? ( '' + err.message ) : ( '' + err );
// Rewrite mapnik parsing errors to start with layer number
const matches = errMsg.match("(.*) in style 'layer([0-9]+)'");
if (matches) {
errMsg = `style${matches[2]}: ${matches[1]}`;
}
err.message = errMsg;
err.label = 'TILE RENDER';
next(err);
};
}

View File

@@ -0,0 +1,14 @@
const _ = require('underscore');
module.exports = function augmentLayergroupData () {
return function augmentLayergroupDataMiddleware (req, res, next) {
const layergroup = res.body;
// include in layergroup response the variables in serverMedata
// those variables are useful to send to the client information
// about how to reach this server or information about it
_.extend(layergroup, global.environment.serverMetadata);
next();
};
};

View File

@@ -1,9 +1,8 @@
module.exports = function authorizeMiddleware (authApi) {
return function (req, res, next) {
req.profiler.done('req2params.setup');
authApi.authorize(req, res, (err, authorized) => {
module.exports = function authorize (authBackend) {
return function authorizeMiddleware (req, res, next) {
authBackend.authorize(req, res, (err, authorized) => {
req.profiler.done('authorize');
if (err) {
return next(err);
}

View File

@@ -0,0 +1,24 @@
module.exports = function setCacheChannelHeader () {
return function setCacheChannelHeaderMiddleware (req, res, next) {
if (req.method !== 'GET') {
return next();
}
const { mapConfigProvider } = res.locals;
mapConfigProvider.getAffectedTables((err, affectedTables) => {
if (err) {
global.logger.warn('ERROR generating Cache Channel Header:', err);
return next();
}
if (!affectedTables) {
return next();
}
res.set('X-Cache-Channel', affectedTables.getCacheChannel());
next();
});
};
};

View File

@@ -0,0 +1,19 @@
const ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365;
module.exports = function setCacheControlHeader ({ ttl = ONE_YEAR_IN_SECONDS, revalidate = false } = {}) {
return function setCacheControlHeaderMiddleware (req, res, next) {
if (req.method !== 'GET') {
return next();
}
const directives = [ 'public', `max-age=${ttl}` ];
if (revalidate) {
directives.push('must-revalidate');
}
res.set('Cache-Control', directives.join(','));
next();
};
};

View File

@@ -0,0 +1,11 @@
module.exports = function checkJsonContentType () {
return function checkJsonContentTypeMiddleware(req, res, next) {
if (req.method === 'POST' && !req.is('application/json')) {
return next(new Error('POST data must be of type application/json'));
}
req.profiler.done('checkJsonContentTypeMiddleware');
next();
};
};

View File

@@ -0,0 +1,11 @@
const VALID_IMAGE_FORMATS = ['png', 'jpg'];
module.exports = function checkStaticImageFormat () {
return function checkStaticImageFormatMiddleware (req, res, next) {
if(!VALID_IMAGE_FORMATS.includes(req.params.format)) {
return next(new Error(`Unsupported image format "${req.params.format}"`));
}
next();
};
};

View File

@@ -14,19 +14,16 @@ const REQUEST_QUERY_PARAMS_WHITELIST = [
'filters' // json
];
module.exports = function cleanUpQueryParamsMiddleware () {
return function cleanUpQueryParams (req, res, next) {
var allowedQueryParams = REQUEST_QUERY_PARAMS_WHITELIST;
module.exports = function cleanUpQueryParamsMiddleware (customQueryParams = []) {
if (!Array.isArray(customQueryParams)) {
throw new Error('customQueryParams must receive an Array of params');
}
if (Array.isArray(res.locals.allowedQueryParams)) {
allowedQueryParams = allowedQueryParams.concat(res.locals.allowedQueryParams);
}
return function cleanUpQueryParams (req, res, next) {
const allowedQueryParams = [...REQUEST_QUERY_PARAMS_WHITELIST, ...customQueryParams];
req.query = _.pick(req.query, allowedQueryParams);
// bring all query values onto res.locals object
_.extend(res.locals, req.query);
next();
};
};

View File

@@ -0,0 +1,41 @@
const positiveIntegerNumberRegExp = /^\d+$/;
const integerNumberRegExp = /^-?\d+$/;
const invalidZoomMessage = function (zoom) {
return `Invalid zoom value (${zoom}). It should be an integer number greather than or equal to 0`;
};
const invalidCoordXMessage = function (x) {
return `Invalid coodinate 'x' value (${x}). It should be an integer number`;
};
const invalidCoordYMessage = function (y) {
return `Invalid coodinate 'y' value (${y}). It should be an integer number greather than or equal to 0`;
};
module.exports = function coordinates (validate = { z: true, x: true, y: true }) {
return function coordinatesMiddleware (req, res, next) {
const { z, x, y } = req.params;
if (validate.z && !positiveIntegerNumberRegExp.test(z)) {
const err = new Error(invalidZoomMessage(z));
err.http_status = 400;
return next(err);
}
// Negative values for x param are valid. The x param is wrapped
if (validate.x && !integerNumberRegExp.test(x)) {
const err = new Error(invalidCoordXMessage(x));
err.http_status = 400;
return next(err);
}
if (validate.y && !positiveIntegerNumberRegExp.test(y)) {
const err = new Error(invalidCoordYMessage(y));
err.http_status = 400;
return next(err);
}
next();
};
};

View File

@@ -0,0 +1,18 @@
module.exports = function cors () {
return function corsMiddleware (req, res, next) {
const headers = [
'X-Requested-With',
'X-Prototype-Version',
'X-CSRF-Token'
];
if (req.method === 'OPTIONS') {
headers.push('Content-Type');
}
res.set("Access-Control-Allow-Origin", "*");
res.set("Access-Control-Allow-Headers", headers.join(', '));
next();
};
};

View File

@@ -0,0 +1,85 @@
const basicAuth = require('basic-auth');
module.exports = function credentials () {
return function credentialsMiddleware(req, res, next) {
const apikeyCredentials = getApikeyCredentialsFromRequest(req);
res.locals.api_key = apikeyCredentials.token;
res.locals.basicAuthUsername = apikeyCredentials.username;
res.set('vary', 'Authorization'); //Honor Authorization header when caching.
return next();
};
};
function getApikeyCredentialsFromRequest(req) {
let apikeyCredentials = {
token: null,
username: null,
};
for (let getter of apikeyGetters) {
apikeyCredentials = getter(req);
if (apikeyTokenFound(apikeyCredentials)) {
break;
}
}
return apikeyCredentials;
}
const apikeyGetters = [
getApikeyTokenFromHeaderAuthorization,
getApikeyTokenFromRequestQueryString,
getApikeyTokenFromRequestBody,
];
function getApikeyTokenFromHeaderAuthorization(req) {
const credentials = basicAuth(req);
if (credentials) {
return {
username: credentials.username,
token: credentials.pass
};
} else {
return {
username: null,
token: null,
};
}
}
function getApikeyTokenFromRequestQueryString(req) {
let token = null;
if (req.query && req.query.api_key) {
token = req.query.api_key;
} else if (req.query && req.query.map_key) {
token = req.query.map_key;
}
return {
username: null,
token: token,
};
}
function getApikeyTokenFromRequestBody(req) {
let token = null;
if (req.body && req.body.api_key) {
token = req.body.api_key;
} else if (req.body && req.body.map_key) {
token = req.body.map_key;
}
return {
username: null,
token: token,
};
}
function apikeyTokenFound(apikey) {
return !!apikey && !!apikey.token;
}

View File

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

View File

@@ -15,10 +15,6 @@ module.exports = function errorMiddleware (/* options */) {
var statusCode = findStatusCode(err);
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);
@@ -50,26 +46,39 @@ function isDatasourceTimeoutError (err) {
return err.message && err.message.match(/canceling statement due to statement timeout/i);
}
function isTimeoutError (err) {
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
function isTimeoutError (errorTypes) {
return errorTypes.renderTimeoutError || errorTypes.datasourceTimeoutError;
}
function getErrorTypes(error) {
return {
renderTimeoutError: isRenderTimeoutError(error),
datasourceTimeoutError: isDatasourceTimeoutError(error),
};
}
function populateTimeoutErrors (errors) {
return errors.map(function (error) {
if (isRenderTimeoutError(error)) {
error.subtype = 'render';
}
const errorTypes = getErrorTypes(error);
if (isDatasourceTimeoutError(error)) {
error.subtype = 'datasource';
}
if (isTimeoutError(error)) {
if (isTimeoutError(errorTypes)) {
error.message = 'You are over platform\'s limits. Please contact us to know more details';
error.type = 'limit';
error.http_status = 429;
}
if (errorTypes.datasourceTimeoutError) {
error.subtype = 'datasource';
error.message = 'You are over platform\'s limits: SQL query timeout error.' +
' Refactor your query before running again or contact CARTO support for more details.';
}
if (errorTypes.renderTimeoutError) {
error.subtype = 'render';
error.message = 'You are over platform\'s limits: Render timeout error.' +
' Contact CARTO support for more details.';
}
return error;
});
}
@@ -186,15 +195,15 @@ function setErrorHeader(errors, statusCode, res) {
subtype: error.subtype
};
});
res.set('X-Tiler-Errors', stringifyForLogs(errorsLog));
}
/**
* Remove problematic nested characters
* Remove problematic nested characters
* from object for logs RegEx
*
* @param {Object} object
*
* @param {Object} object
*/
function stringifyForLogs(object) {
Object.keys(object).map(key => {

View File

@@ -0,0 +1,16 @@
module.exports = function incrementMapViewCount (metadataBackend) {
return function incrementMapViewCountMiddleware(req, res, next) {
const { mapConfig, user } = res.locals;
// Error won't blow up, just be logged.
metadataBackend.incMapviewCount(user, mapConfig.obj().stat_tag, (err) => {
req.profiler.done('incMapviewCount');
if (err) {
global.logger.log(`ERROR: failed to increment mapview count for user '${user}': ${err.message}`);
}
next();
});
};
};

View File

@@ -0,0 +1,9 @@
module.exports = function initProfiler (isTemplateInstantiation) {
const operation = isTemplateInstantiation ? 'instance_template' : 'createmap';
return function initProfilerMiddleware (req, res, next) {
req.profiler.start(`windshaft-cartodb.${operation}_${req.method.toLowerCase()}`);
req.profiler.done(`${operation}.initProfilerMiddleware`);
next();
};
};

View File

@@ -0,0 +1,9 @@
module.exports = function initializeStatusCode () {
return function initializeStatusCodeMiddleware (req, res, next) {
if (req.method !== 'OPTIONS') {
res.statusCode = 404;
}
next();
};
};

View File

@@ -0,0 +1,38 @@
module.exports = function setLastModifiedHeader () {
return function setLastModifiedHeaderMiddleware(req, res, next) {
if (req.method !== 'GET') {
return next();
}
const { mapConfigProvider, cache_buster } = res.locals;
if (cache_buster) {
const cacheBuster = parseInt(cache_buster, 10);
const lastModifiedDate = Number.isFinite(cacheBuster) ? new Date(cacheBuster) : new Date();
res.set('Last-Modified', lastModifiedDate.toUTCString());
return next();
}
mapConfigProvider.getAffectedTables((err, affectedTables) => {
if (err) {
global.logger.warn('ERROR generating Last Modified Header:', err);
return next();
}
if (!affectedTables) {
res.set('Last-Modified', new Date().toUTCString());
return next();
}
const lastUpdatedAt = affectedTables.getLastUpdatedAt();
const lastModifiedDate = Number.isFinite(lastUpdatedAt) ? new Date(lastUpdatedAt) : new Date();
res.set('Last-Modified', lastModifiedDate.toUTCString());
next();
});
};
};

View File

@@ -0,0 +1,39 @@
module.exports = function setLastUpdatedTimeToLayergroup () {
return function setLastUpdatedTimeToLayergroupMiddleware (req, res, next) {
const { mapConfigProvider, analysesResults } = res.locals;
const layergroup = res.body;
mapConfigProvider.createAffectedTables((err, affectedTables) => {
if (err) {
return next(err);
}
if (!affectedTables) {
return next();
}
var lastUpdateTime = affectedTables.getLastUpdatedAt();
lastUpdateTime = getLastUpdatedTime(analysesResults, lastUpdateTime) || lastUpdateTime;
// last update for layergroup cache buster
layergroup.layergroupid = layergroup.layergroupid + ':' + lastUpdateTime;
layergroup.last_updated = new Date(lastUpdateTime).toISOString();
next();
});
};
};
function getLastUpdatedTime(analysesResults, lastUpdateTime) {
if (!Array.isArray(analysesResults)) {
return lastUpdateTime;
}
return analysesResults.reduce(function(lastUpdateTime, analysis) {
return analysis.getNodes().reduce(function(lastNodeUpdatedAtTime, node) {
var nodeUpdatedAtDate = node.getUpdatedAt();
var nodeUpdatedTimeAt = (nodeUpdatedAtDate && nodeUpdatedAtDate.getTime()) || 0;
return nodeUpdatedTimeAt > lastNodeUpdatedAtTime ? nodeUpdatedTimeAt : lastNodeUpdatedAtTime;
}, lastUpdateTime);
}, lastUpdateTime);
}

View File

@@ -0,0 +1,26 @@
module.exports = function setLayerStats (pgConnection, statsBackend) {
return function setLayerStatsMiddleware(req, res, next) {
const { user, mapConfig } = res.locals;
const layergroup = res.body;
pgConnection.getConnection(user, (err, connection) => {
if (err) {
return next(err);
}
statsBackend.getStats(mapConfig, connection, function(err, layersStats) {
if (err) {
return next(err);
}
if (layersStats.length > 0) {
layergroup.metadata.layers.forEach(function (layer, index) {
layer.meta.stats = layersStats[index];
});
}
next();
});
});
};
};

View File

@@ -0,0 +1,15 @@
module.exports = function setLayergroupIdHeader (templateMaps, useTemplateHash) {
return function setLayergroupIdHeaderMiddleware (req, res, next) {
const { user, template } = res.locals;
const layergroup = res.body;
if (useTemplateHash) {
var templateHash = templateMaps.fingerPrint(template).substring(0, 8);
layergroup.layergroupid = `${user}@${templateHash}@${layergroup.layergroupid}`;
}
res.set('X-Layergroup-Id', layergroup.layergroupid);
next();
};
};

View File

@@ -0,0 +1,15 @@
module.exports = function setMetadataToLayergroup (layergroupMetadata, includeQuery) {
return function setMetadataToLayergroupMiddleware (req, res, next) {
const { user, mapConfig, analysesResults = [], context, api_key: userApiKey } = res.locals;
const layergroup = res.body;
layergroupMetadata.addDataviewsAndWidgetsUrls(user, layergroup, mapConfig.obj());
layergroupMetadata.addAnalysesMetadata(user, layergroup, analysesResults, includeQuery);
layergroupMetadata.addTurboCartoContextMetadata(layergroup, mapConfig.obj(), context);
layergroupMetadata.addAggregationContextMetadata(layergroup, mapConfig.obj(), context);
layergroupMetadata.addDateWrappingMetadata (layergroup, mapConfig.obj());
layergroupMetadata.addTileJsonMetadata(layergroup, user, mapConfig, userApiKey);
next();
};
};

View File

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

View File

@@ -0,0 +1,22 @@
module.exports = function logger (options) {
if (!global.log4js || !options.log_format) {
return function dummyLoggerMiddleware (req, res, next) {
next();
};
}
const opts = {
level: 'info',
// Allowing for unbuffered logging is mainly
// used to avoid hanging during unit testing.
// TODO: provide an explicit teardown function instead,
// releasing any event handler or timer set by
// this component.
buffer: !options.unbuffered_logging,
// optional log format
format: options.log_format
};
const logger = global.log4js.getLogger();
return global.log4js.connectLogger(logger, opts);
};

View File

@@ -0,0 +1,33 @@
const LZMA = require('lzma').LZMA;
module.exports = function lzma () {
const lzmaWorker = new LZMA();
return function lzmaMiddleware (req, res, next) {
if (!req.query.hasOwnProperty('lzma')) {
return next();
}
// Decode (from base64)
var lzma = new Buffer(req.query.lzma, 'base64')
.toString('binary')
.split('')
.map(function(c) {
return c.charCodeAt(0) - 128;
});
// Decompress
lzmaWorker.decompress(lzma, function(result) {
try {
delete req.query.lzma;
Object.assign(req.query, JSON.parse(result));
req.profiler.done('lzma');
next();
} catch (err) {
next(new Error('Error parsing lzma as JSON: ' + err));
}
});
};
};

View File

@@ -0,0 +1,35 @@
module.exports = function mapError (options) {
const { addContext = false, label = 'MAPS CONTROLLER' } = options;
return function mapErrorMiddleware (err, req, res, next) {
req.profiler.done('error');
const { mapConfig } = res.locals;
if (addContext) {
err = Number.isFinite(err.layerIndex) ? populateError(err, mapConfig) : err;
}
err.label = label;
next(err);
};
};
function populateError(err, mapConfig) {
var error = new Error(err.message);
error.http_status = err.http_status;
if (!err.http_status && err.message.indexOf('column "the_geom_webmercator" does not exist') >= 0) {
error.http_status = 400;
}
error.type = 'layer';
error.subtype = err.message.indexOf('Postgis Plugin') >= 0 ? 'query' : undefined;
error.layer = {
id: mapConfig.getLayerId(err.layerIndex),
index: err.layerIndex,
type: mapConfig.layerType(err.layerIndex)
};
return error;
}

View File

@@ -0,0 +1,38 @@
const MapStoreMapConfigProvider = require('../../models/mapconfig/provider/map-store-provider');
module.exports = function createMapStoreMapConfigProvider (
mapStore,
userLimitsBackend,
pgConnection,
affectedTablesCache,
forcedFormat = null
) {
return function createMapStoreMapConfigProviderMiddleware (req, res, next) {
const { user, token, cache_buster, api_key } = res.locals;
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
const { layer: layerFromParams, z, x, y, scale_factor, format } = req.params;
const { layer: layerFromQuery } = req.query;
const params = {
user, token, cache_buster, api_key,
dbuser, dbname, dbpassword, dbhost, dbport,
layer: (layerFromQuery || layerFromParams), z, x, y, scale_factor, format
};
if (forcedFormat) {
params.format = forcedFormat;
params.layer = params.layer || 'all';
}
res.locals.mapConfigProvider = new MapStoreMapConfigProvider(
mapStore,
user,
userLimitsBackend,
pgConnection,
affectedTablesCache,
params
);
next();
};
};

View File

@@ -0,0 +1,32 @@
module.exports = function getNamedMapProvider ({ namedMapProviderCache, label, forcedFormat = null }) {
return function getNamedMapProviderMiddleware (req, res, next) {
const { user, token, cache_buster, api_key } = res.locals;
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
const { template_id, layer: layerFromParams, z, x, y, format } = req.params;
const { layer: layerFromQuery } = req.query;
const params = {
user, token, cache_buster, api_key,
dbuser, dbname, dbpassword, dbhost, dbport,
template_id, layer: (layerFromQuery || layerFromParams), z, x, y, format
};
if (forcedFormat) {
params.format = forcedFormat;
params.layer = params.layer || 'all';
}
const { config, auth_token } = req.query;
namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, namedMapProvider) => {
if (err) {
err.label = label;
return next(err);
}
res.locals.mapConfigProvider = namedMapProvider;
next();
});
};
};

View File

@@ -0,0 +1,5 @@
module.exports = function noop () {
return function noopMiddleware (req, res, next) {
next();
};
};

View File

@@ -0,0 +1,72 @@
'use strict';
const RATE_LIMIT_ENDPOINTS_GROUPS = {
ANONYMOUS: 'anonymous',
STATIC: 'static',
STATIC_NAMED: 'static_named',
DATAVIEW: 'dataview',
DATAVIEW_SEARCH: 'dataview_search',
ANALYSIS: 'analysis',
ANALYSIS_CATALOG: 'analysis_catalog',
TILE: 'tile',
ATTRIBUTES: 'attributes',
NAMED_LIST: 'named_list',
NAMED_CREATE: 'named_create',
NAMED_GET: 'named_get',
NAMED: 'named',
NAMED_UPDATE: 'named_update',
NAMED_DELETE: 'named_delete',
NAMED_TILES: 'named_tiles'
};
function rateLimit(userLimitsBackend, endpointGroup = null) {
if (!isRateLimitEnabled(endpointGroup)) {
return function rateLimitDisabledMiddleware(req, res, next) { next(); };
}
return function rateLimitMiddleware(req, res, next) {
userLimitsBackend.getRateLimit(res.locals.user, endpointGroup, function (err, userRateLimit) {
if (err) {
return next(err);
}
if (!userRateLimit) {
return next();
}
const [isBlocked, limit, remaining, retry, reset] = userRateLimit;
res.set({
'Carto-Rate-Limit-Limit': limit,
'Carto-Rate-Limit-Remaining': remaining,
'Carto-Rate-Limit-Reset': reset
});
if (isBlocked) {
// retry is floor rounded in seconds by redis-cell
res.set('Retry-After', retry + 1);
let rateLimitError = new Error(
'You are over platform\'s limits: too many requests.' +
' Please contact us to know more details'
);
rateLimitError.http_status = 429;
rateLimitError.type = 'limit';
rateLimitError.subtype = 'rate-limit';
return next(rateLimitError);
}
return next();
});
};
}
function isRateLimitEnabled(endpointGroup) {
return global.environment.enabledFeatures.rateLimitsEnabled &&
endpointGroup &&
global.environment.enabledFeatures.rateLimitsByEndpoint[endpointGroup];
}
module.exports = rateLimit;
module.exports.RATE_LIMIT_ENDPOINTS_GROUPS = RATE_LIMIT_ENDPOINTS_GROUPS;

View File

@@ -0,0 +1,17 @@
module.exports = function sendResponse () {
return function sendResponseMiddleware (req, res) {
req.profiler.done('res');
res.status(res.statusCode);
if (Buffer.isBuffer(res.body)) {
return res.send(res.body);
}
if (req.query.callback) {
return res.jsonp(res.body);
}
res.json(res.body);
};
};

View File

@@ -0,0 +1,11 @@
const os = require('os');
module.exports = function servedByHostHeader () {
const hostname = os.hostname().split('.')[0];
return function servedByHostHeaderMiddleware (req, res, next) {
res.set('X-Served-By-Host', hostname);
next();
};
};

View File

@@ -1,11 +1,11 @@
const Profiler = require('../stats/profiler_proxy');
const Profiler = require('../../stats/profiler_proxy');
const debug = require('debug')('windshaft:cartodb:stats');
const onHeaders = require('on-headers');
module.exports = function statsMiddleware(options) {
module.exports = function stats (options) {
const { enabled = true, statsClient } = options;
return function stats(req, res, next) {
return function statsMiddleware (req, res, next) {
req.profiler = new Profiler({
statsd_client: statsClient,
profile: enabled

View File

@@ -0,0 +1,31 @@
const NamedMapsCacheEntry = require('../../cache/model/named_maps_entry');
const NamedMapMapConfigProvider = require('../../models/mapconfig/provider/named-map-provider');
module.exports = function setSurrogateKeyHeader ({ surrogateKeysCache }) {
return function setSurrogateKeyHeaderMiddleware(req, res, next) {
const { user, mapConfigProvider } = res.locals;
if (mapConfigProvider instanceof NamedMapMapConfigProvider) {
surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, mapConfigProvider.getTemplateName()));
}
if (req.method !== 'GET') {
return next();
}
mapConfigProvider.getAffectedTables((err, affectedTables) => {
if (err) {
global.logger.warn('ERROR generating Surrogate Key Header:', err);
return next();
}
if (!affectedTables || !affectedTables.tables || affectedTables.tables.length === 0) {
return next();
}
surrogateKeysCache.tag(res, affectedTables);
next();
});
};
};

View File

@@ -0,0 +1,10 @@
module.exports = function syntaxError () {
return function syntaxErrorMiddleware (err, req, res, next) {
if (err.name === 'SyntaxError') {
err.http_status = 400;
err.message = `${err.name}: ${err.message}`;
}
next(err);
};
};

View File

@@ -0,0 +1,11 @@
const CdbRequest = require('../../models/cdb_request');
module.exports = function user () {
const cdbRequest = new CdbRequest();
return function userMiddleware(req, res, next) {
res.locals.user = cdbRequest.userByReq(req);
next();
};
};

View File

@@ -1,12 +1,11 @@
const fs = require('fs');
const timeoutErrorVectorTile = fs.readFileSync(__dirname + '/../../../assets/render-timeout-fallback.mvt');
const timeoutErrorVectorTile = fs.readFileSync(__dirname + '/../../../../assets/render-timeout-fallback.mvt');
module.exports = function vectorError() {
return function vectorErrorMiddleware(err, req, res, next) {
if(req.params.format === 'mvt') {
if (isTimeoutError(err)) {
if (isTimeoutError(err) || isRateLimitError(err)) {
res.set('Content-Type', 'application/x-protobuf');
return res.status(429).send(timeoutErrorVectorTile);
}
@@ -28,3 +27,7 @@ function isDatasourceTimeoutError (err) {
function isTimeoutError (err) {
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
}
function isRateLimitError (err) {
return err.type === 'limit' && err.subtype === 'rate-limit';
}

View File

@@ -0,0 +1,231 @@
const { templateName } = require('../../backends/template_maps');
const credentials = require('../middlewares/credentials');
const rateLimit = require('../middlewares/rate-limit');
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
module.exports = class AdminTemplateController {
/**
* @param {AuthBackend} authBackend
* @param {PgConnection} pgConnection
* @param {TemplateMaps} templateMaps
* @constructor
*/
constructor (authBackend, templateMaps, userLimitsBackend) {
this.authBackend = authBackend;
this.templateMaps = templateMaps;
this.userLimitsBackend = userLimitsBackend;
}
register (templateRouter) {
templateRouter.options(`/:template_id`);
templateRouter.post('/', this.middlewares({
action: 'create',
label: 'POST TEMPLATE',
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_CREATE
}));
templateRouter.put('/:template_id', this.middlewares({
action: 'update',
label: 'PUT TEMPLATE',
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_UPDATE
}));
templateRouter.get('/:template_id', this.middlewares({
action: 'get',
label: 'GET TEMPLATE',
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_GET
}));
templateRouter.delete('/:template_id', this.middlewares({
action: 'delete',
label: 'DELETE TEMPLATE',
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_DELETE
}));
templateRouter.get('/', this.middlewares({
action: 'list',
label: 'GET TEMPLATE LIST',
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_LIST
}));
}
middlewares ({ action, label, rateLimitGroup }) {
let template;
if (action === 'create') {
template = createTemplate;
}
if (action === 'update') {
template = updateTemplate;
}
if (action === 'get') {
template = retrieveTemplate;
}
if (action === 'delete') {
template = destroyTemplate;
}
if (action === 'list') {
template = listTemplates;
}
return [
credentials(),
authorizedByAPIKey({ authBackend: this.authBackend, action, label }),
rateLimit(this.userLimitsBackend, rateLimitGroup),
checkContentType({ action: 'POST', label: 'POST TEMPLATE' }),
template({ templateMaps: this.templateMaps })
];
}
};
function checkContentType ({ label }) {
return function checkContentTypeMiddleware (req, res, next) {
if ((req.method === 'POST' || req.method === 'PUT') && !req.is('application/json')) {
const error = new Error(`${req.method} template data must be of type application/json`);
error.label = label;
return next(error);
}
next();
};
}
function authorizedByAPIKey ({ authBackend, action, label }) {
return function authorizedByAPIKeyMiddleware (req, res, next) {
const { user } = res.locals;
authBackend.authorizedByAPIKey(user, res, (err, authenticated, apikey) => {
if (err) {
return next(err);
}
if (!authenticated) {
const error = new Error(`Only authenticated users can ${action} templated maps`);
error.http_status = 403;
error.label = label;
return next(error);
}
if (apikey.type !== 'master') {
const error = new Error('Forbidden');
error.type = 'auth';
error.subtype = 'api-key-does-not-grant-access';
error.http_status = 403;
return next(error);
}
next();
});
};
}
function createTemplate ({ templateMaps }) {
return function createTemplateMiddleware (req, res, next) {
const { user } = res.locals;
const template = req.body;
templateMaps.addTemplate(user, template, (err, templateId) => {
if (err) {
return next(err);
}
res.statusCode = 200;
res.body = { template_id: templateId };
next();
});
};
}
function updateTemplate ({ templateMaps }) {
return function updateTemplateMiddleware (req, res, next) {
const { user } = res.locals;
const template = req.body;
const templateId = templateName(req.params.template_id);
templateMaps.updTemplate(user, templateId, template, (err) => {
if (err) {
return next(err);
}
res.statusCode = 200;
res.body = { template_id: templateId };
next();
});
};
}
function retrieveTemplate ({ templateMaps }) {
return function retrieveTemplateMiddleware (req, res, next) {
req.profiler.start('windshaft-cartodb.get_template');
const { user } = res.locals;
const templateId = templateName(req.params.template_id);
templateMaps.getTemplate(user, templateId, (err, template) => {
if (err) {
return next(err);
}
if (!template) {
const error = new Error(`Cannot find template '${templateId}' of user '${user}'`);
error.http_status = 404;
return next(error);
}
// auth_id was added by ourselves,
// so we remove it before returning to the user
delete template.auth_id;
res.statusCode = 200;
res.body = { template };
next();
});
};
}
function destroyTemplate ({ templateMaps }) {
return function destroyTemplateMiddleware (req, res, next) {
req.profiler.start('windshaft-cartodb.delete_template');
const { user } = res.locals;
const templateId = templateName(req.params.template_id);
templateMaps.delTemplate(user, templateId, (err/* , tpl_val */) => {
if (err) {
return next(err);
}
res.statusCode = 204;
res.body = '';
next();
});
};
}
function listTemplates ({ templateMaps }) {
return function listTemplatesMiddleware (req, res, next) {
req.profiler.start('windshaft-cartodb.get_template_list');
const { user } = res.locals;
templateMaps.listTemplates(user, (err, templateIds) => {
if (err) {
return next(err);
}
res.statusCode = 200;
res.body = { template_ids: templateIds };
next();
});
};
}

View File

@@ -0,0 +1,214 @@
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
const credentials = require('../middlewares/credentials');
const dbConnSetup = require('../middlewares/db-conn-setup');
const authorize = require('../middlewares/authorize');
const initProfiler = require('../middlewares/init-profiler');
const checkJsonContentType = require('../middlewares/check-json-content-type');
const incrementMapViewCount = require('../middlewares/increment-map-view-count');
const augmentLayergroupData = require('../middlewares/augment-layergroup-data');
const cacheControlHeader = require('../middlewares/cache-control-header');
const cacheChannelHeader = require('../middlewares/cache-channel-header');
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
const lastModifiedHeader = require('../middlewares/last-modified-header');
const lastUpdatedTimeLayergroup = require('../middlewares/last-updated-time-layergroup');
const layerStats = require('../middlewares/layer-stats');
const layergroupIdHeader = require('../middlewares/layergroup-id-header');
const layergroupMetadata = require('../middlewares/layergroup-metadata');
const mapError = require('../middlewares/map-error');
const NamedMapMapConfigProvider = require('../../models/mapconfig/provider/named-map-provider');
const CreateLayergroupMapConfigProvider = require('../../models/mapconfig/provider/create-layergroup-provider');
const rateLimit = require('../middlewares/rate-limit');
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
module.exports = class NamedMapController {
/**
* @param {PgConnection} pgConnection
* @param {TemplateMaps} templateMaps
* @param {MapBackend} mapBackend
* @param metadataBackend
* @param {SurrogateKeysCache} surrogateKeysCache
* @param {UserLimitsBackend} userLimitsBackend
* @param {LayergroupAffectedTables} layergroupAffectedTables
* @param {MapConfigAdapter} mapConfigAdapter
* @param {StatsBackend} statsBackend
* @param {AuthBackend} authBackend
* @param layergroupMetadata
* @constructor
*/
constructor (
pgConnection,
templateMaps,
mapBackend,
metadataBackend,
surrogateKeysCache,
userLimitsBackend,
layergroupAffectedTables,
mapConfigAdapter,
statsBackend,
authBackend,
layergroupMetadata
) {
this.pgConnection = pgConnection;
this.templateMaps = templateMaps;
this.mapBackend = mapBackend;
this.metadataBackend = metadataBackend;
this.surrogateKeysCache = surrogateKeysCache;
this.userLimitsBackend = userLimitsBackend;
this.layergroupAffectedTables = layergroupAffectedTables;
this.mapConfigAdapter = mapConfigAdapter;
this.statsBackend = statsBackend;
this.authBackend = authBackend;
this.layergroupMetadata = layergroupMetadata;
}
register (templateRouter) {
templateRouter.get('/:template_id/jsonp', this.middlewares());
templateRouter.post('/:template_id', this.middlewares());
}
middlewares () {
const isTemplateInstantiation = true;
const useTemplateHash = true;
const includeQuery = false;
const label = 'NAMED MAP LAYERGROUP';
const addContext = false;
return [
credentials(),
authorize(this.authBackend),
dbConnSetup(this.pgConnection),
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED),
cleanUpQueryParams(['aggregation']),
initProfiler(isTemplateInstantiation),
checkJsonContentType(),
checkInstantiteLayergroup(),
getTemplate(
this.templateMaps,
this.pgConnection,
this.metadataBackend,
this.userLimitsBackend,
this.mapConfigAdapter,
this.layergroupAffectedTables
),
instantiateLayergroup(
this.mapBackend,
this.userLimitsBackend,
this.pgConnection,
this.layergroupAffectedTables
),
incrementMapViewCount(this.metadataBackend),
augmentLayergroupData(),
cacheControlHeader({ ttl: global.environment.varnish.layergroupTtl || 86400, revalidate: true }),
cacheChannelHeader(),
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
lastModifiedHeader(),
lastUpdatedTimeLayergroup(),
layerStats(this.pgConnection, this.statsBackend),
layergroupIdHeader(this.templateMaps ,useTemplateHash),
layergroupMetadata(this.layergroupMetadata, includeQuery),
mapError({ label, addContext })
];
}
};
function checkInstantiteLayergroup () {
return function checkInstantiteLayergroupMiddleware(req, res, next) {
if (req.method === 'GET') {
const { callback, config } = req.query;
if (callback === undefined || callback.length === 0) {
return next(new Error('callback parameter should be present and be a function name'));
}
if (config) {
try {
req.body = JSON.parse(config);
} catch(e) {
return next(new Error('Invalid config parameter, should be a valid JSON'));
}
}
}
req.profiler.done('checkInstantiteLayergroup');
return next();
};
}
function getTemplate (
templateMaps,
pgConnection,
metadataBackend,
userLimitsBackend,
mapConfigAdapter,
affectedTablesCache
) {
return function getTemplateMiddleware (req, res, next) {
const templateParams = req.body;
const { user, dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
const { template_id } = req.params;
const { auth_token } = req.query;
const params = Object.assign({ dbuser, dbname, dbpassword, dbhost, dbport }, req.query);
const mapConfigProvider = new NamedMapMapConfigProvider(
templateMaps,
pgConnection,
metadataBackend,
userLimitsBackend,
mapConfigAdapter,
affectedTablesCache,
user,
template_id,
templateParams,
auth_token,
params
);
mapConfigProvider.getMapConfig((err, mapConfig, rendererParams) => {
req.profiler.done('named.getMapConfig');
if (err) {
return next(err);
}
res.locals.mapConfig = mapConfig;
res.locals.rendererParams = rendererParams;
res.locals.mapConfigProvider = mapConfigProvider;
next();
});
};
}
function instantiateLayergroup (mapBackend, userLimitsBackend, pgConnection, affectedTablesCache) {
return function instantiateLayergroupMiddleware (req, res, next) {
const { user, mapConfig, rendererParams } = res.locals;
const mapConfigProvider = new CreateLayergroupMapConfigProvider(
mapConfig,
user,
userLimitsBackend,
pgConnection,
affectedTablesCache,
rendererParams
);
mapBackend.createLayergroup(mapConfig, rendererParams, mapConfigProvider, (err, layergroup, stats = {}) => {
req.profiler.add(stats);
if (err) {
return next(err);
}
res.statusCode = 200;
res.body = layergroup;
const { mapConfigProvider } = res.locals;
res.locals.analysesResults = mapConfigProvider.analysesResults;
res.locals.template = mapConfigProvider.template;
res.locals.context = mapConfigProvider.context;
next();
});
};
}

View File

@@ -0,0 +1,64 @@
const { Router: router } = require('express');
const NamedMapController = require('./named-template-controller');
const AdminTemplateController = require('./admin-template-controller');
const TileTemplateController = require('./tile-template-controller');
module.exports = class TemplateRouter {
constructor ({ collaborators }) {
const {
pgConnection,
templateMaps,
mapBackend,
metadataBackend,
surrogateKeysCache,
userLimitsBackend,
layergroupAffectedTablesCache,
mapConfigAdapter,
statsBackend,
authBackend,
layergroupMetadata,
namedMapProviderCache,
tileBackend,
} = collaborators;
this.namedMapController = new NamedMapController(
pgConnection,
templateMaps,
mapBackend,
metadataBackend,
surrogateKeysCache,
userLimitsBackend,
layergroupAffectedTablesCache,
mapConfigAdapter,
statsBackend,
authBackend,
layergroupMetadata
);
this.tileTemplateController = new TileTemplateController(
namedMapProviderCache,
tileBackend,
surrogateKeysCache,
pgConnection,
authBackend,
userLimitsBackend
);
this.adminTemplateController = new AdminTemplateController(
authBackend,
templateMaps,
userLimitsBackend
);
}
register (apiRouter, templatePaths) {
const templateRouter = router({ mergeParams: true });
this.namedMapController.register(templateRouter);
this.tileTemplateController.register(templateRouter);
this.adminTemplateController.register(templateRouter);
templatePaths.forEach(path => apiRouter.use(path, templateRouter));
}
};

View File

@@ -0,0 +1,95 @@
const coordinates = require('../middlewares/coordinates');
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
const credentials = require('../middlewares/credentials');
const dbConnSetup = require('../middlewares/db-conn-setup');
const authorize = require('../middlewares/authorize');
const namedMapProvider = require('../middlewares/named-map-provider');
const cacheControlHeader = require('../middlewares/cache-control-header');
const cacheChannelHeader = require('../middlewares/cache-channel-header');
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
const lastModifiedHeader = require('../middlewares/last-modified-header');
const vectorError = require('../middlewares/vector-error');
const rateLimit = require('../middlewares/rate-limit');
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
module.exports = class TileTemplateController {
constructor (
namedMapProviderCache,
tileBackend,
surrogateKeysCache,
pgConnection,
authBackend,
userLimitsBackend
) {
this.namedMapProviderCache = namedMapProviderCache;
this.tileBackend = tileBackend;
this.surrogateKeysCache = surrogateKeysCache;
this.pgConnection = pgConnection;
this.authBackend = authBackend;
this.userLimitsBackend = userLimitsBackend;
}
register (templateRouter) {
templateRouter.get('/:template_id/:layer/:z/:x/:y.(:format)', this.middlewares());
}
middlewares () {
return [
coordinates(),
credentials(),
authorize(this.authBackend),
dbConnSetup(this.pgConnection),
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_TILES),
cleanUpQueryParams(),
namedMapProvider({
namedMapProviderCache: this.namedMapProviderCache,
label: 'NAMED_MAP_TILE'
}),
getTile({
tileBackend: this.tileBackend,
label: 'NAMED_MAP_TILE'
}),
setContentTypeHeader(),
cacheControlHeader(),
cacheChannelHeader(),
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
lastModifiedHeader(),
vectorError()
];
}
};
function getTile ({ tileBackend, label }) {
return function getTileMiddleware (req, res, next) {
const { mapConfigProvider } = res.locals;
const { layer, z, x, y, format } = req.params;
const params = { layer, z, x, y, format };
tileBackend.getTile(mapConfigProvider, params, (err, tile, headers, stats) => {
req.profiler.add(stats);
req.profiler.done('render-' + format);
if (err) {
err.label = label;
return next(err);
}
if (headers) {
res.set(headers);
}
res.statusCode = 200;
res.body = tile;
next();
});
};
}
function setContentTypeHeader () {
return function setContentTypeHeaderMiddleware(req, res, next) {
res.set('Content-Type', res.get('content-type') || res.get('Content-Type') || 'image/png');
next();
};
}

View File

@@ -5,16 +5,14 @@ function AnalysisStatusBackend() {
module.exports = AnalysisStatusBackend;
AnalysisStatusBackend.prototype.getNodeStatus = function (params, callback) {
var nodeId = params.nodeId;
AnalysisStatusBackend.prototype.getNodeStatus = function (nodeId, dbParams, callback) {
var statusQuery = [
'SELECT node_id, status, updated_at, last_error_message as error_message',
'FROM cdb_analysis_catalog where node_id = \'' + nodeId + '\''
].join(' ');
var pg = new PSQL(dbParamsFromReqParams(params));
var pg = new PSQL(dbParams);
pg.query(statusQuery, function(err, result) {
if (err) {
return callback(err, result);
@@ -36,23 +34,3 @@ AnalysisStatusBackend.prototype.getNodeStatus = function (params, callback) {
return callback(null, statusResponse);
}, true); // use read-only transaction
};
function dbParamsFromReqParams(params) {
var dbParams = {};
if ( params.dbuser ) {
dbParams.user = params.dbuser;
}
if ( params.dbpassword ) {
dbParams.pass = params.dbpassword;
}
if ( params.dbhost ) {
dbParams.host = params.dbhost;
}
if ( params.dbport ) {
dbParams.port = params.dbport;
}
if ( params.dbname ) {
dbParams.dbname = params.dbname;
}
return dbParams;
}

View File

@@ -0,0 +1,178 @@
/**
*
* @param {PgConnection} pgConnection
* @param metadataBackend
* @param {MapStore} mapStore
* @param {TemplateMaps} templateMaps
* @constructor
* @type {AuthBackend}
*/
function AuthBackend(pgConnection, metadataBackend, mapStore, templateMaps) {
this.pgConnection = pgConnection;
this.metadataBackend = metadataBackend;
this.mapStore = mapStore;
this.templateMaps = templateMaps;
}
module.exports = AuthBackend;
// Check if the user is authorized by a signer
//
// @param res express response object
// @param callback function(err, signed_by) signed_by will be
// null if the request is not signed by anyone
// or will be a string cartodb username otherwise.
//
AuthBackend.prototype.authorizedBySigner = function(req, res, callback) {
if ( ! res.locals.token || ! res.locals.signer ) {
return callback(null, false); // no signer requested
}
var self = this;
var layergroup_id = res.locals.token;
var auth_token = req.query.auth_token;
this.mapStore.load(layergroup_id, function(err, mapConfig) {
if (err) {
return callback(err);
}
var authorized = self.templateMaps.isAuthorized(mapConfig.obj().template, auth_token);
return callback(null, authorized);
});
};
function isValidApiKey(apikey) {
return apikey.type &&
apikey.user &&
apikey.databasePassword &&
apikey.databaseRole;
}
// Check if a request is authorized by api_key
//
// @param user
// @param res express response object
// @param callback function(err, authorized)
// NOTE: authorized is expected to be 0 or 1 (integer)
//
AuthBackend.prototype.authorizedByAPIKey = function(user, res, callback) {
const apikeyToken = res.locals.api_key;
const basicAuthUsername = res.locals.basicAuthUsername;
if ( ! apikeyToken ) {
return callback(null, false); // no api key, no authorization...
}
this.metadataBackend.getApikey(user, apikeyToken, (err, apikey) => {
if (err) {
if (isNameNotFoundError(err)) {
err.http_status = 404;
}
return callback(err);
}
if ( !isValidApiKey(apikey)) {
const error = new Error('Unauthorized');
error.type = 'auth';
error.subtype = 'api-key-not-found';
error.http_status = 401;
return callback(error);
}
if (!usernameMatches(basicAuthUsername, res.locals.user)) {
const error = new Error('Forbidden');
error.type = 'auth';
error.subtype = 'api-key-username-mismatch';
error.http_status = 403;
return callback(error);
}
if (!apikey.grantsMaps) {
const error = new Error('Forbidden');
error.type = 'auth';
error.subtype = 'api-key-does-not-grant-access';
error.http_status = 403;
return callback(error);
}
return callback(null, true, apikey);
});
};
function isNameNotFoundError (err) {
return err.message && -1 !== err.message.indexOf('name not found');
}
function usernameMatches (basicAuthUsername, requestUsername) {
return !(basicAuthUsername && (basicAuthUsername !== requestUsername));
}
/**
* Check access authorization
*
* @param req - standard req object. Importantly contains table and host information
* @param res - standard res object. Contains the auth parameters in locals
* @param callback function(err, allowed) is access allowed not?
*/
AuthBackend.prototype.authorize = function(req, res, callback) {
var user = res.locals.user;
this.authorizedByAPIKey(user, res, (err, isAuthorizedByApikey) => {
if (err) {
return callback(err);
}
if (isAuthorizedByApikey) {
return this.pgConnection.setDBAuth(user, res.locals, 'regular', function (err) {
req.profiler.done('setDBAuth');
if (err) {
return callback(err);
}
callback(null, true);
});
}
this.authorizedBySigner(req, res, (err, isAuthorizedBySigner) => {
if (err) {
return callback(err);
}
if (isAuthorizedBySigner) {
return this.pgConnection.setDBAuth(user, res.locals, 'master', function (err) {
req.profiler.done('setDBAuth');
if (err) {
return callback(err);
}
callback(null, true);
});
}
// if no signer name was given, use default api key
if (!res.locals.signer) {
return this.pgConnection.setDBAuth(user, res.locals, 'default', function (err) {
req.profiler.done('setDBAuth');
if (err) {
return callback(err);
}
callback(null, true);
});
}
// if signer name was given, return no authorization
return callback(null, false);
});
});
};

View File

@@ -1,13 +1,11 @@
var assert = require('assert');
var _ = require('underscore');
var PSQL = require('cartodb-psql');
var step = require('step');
var BBoxFilter = require('../models/filter/bbox');
var DataviewFactory = require('../models/dataview/factory');
var DataviewFactoryWithOverviews = require('../models/dataview/overviews/factory');
const dbParamsFromReqParams = require('../utils/database-params');
var OverviewsQueryRewriter = require('../utils/overviews_query_rewriter');
var overviewsQueryRewriter = new OverviewsQueryRewriter({
zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)'
@@ -48,7 +46,7 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
}
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});
@@ -170,23 +168,3 @@ function getDataviewDefinition(mapConfig, dataviewName) {
var dataviews = mapConfig.dataviews || {};
return dataviews[dataviewName];
}
function dbParamsFromReqParams(params) {
var dbParams = {};
if ( params.dbuser ) {
dbParams.user = params.dbuser;
}
if ( params.dbpassword ) {
dbParams.pass = params.dbpassword;
}
if ( params.dbhost ) {
dbParams.host = params.dbhost;
}
if ( params.dbport ) {
dbParams.port = params.dbport;
}
if ( params.dbname ) {
dbParams.dbname = params.dbname;
}
return dbParams;
}

View File

@@ -2,11 +2,11 @@ var _ = require('underscore');
var step = require('step');
var AnalysisFilter = require('../models/filter/analysis');
function FilterStatsApi(pgQueryRunner) {
function FilterStatsBackends(pgQueryRunner) {
this.pgQueryRunner = pgQueryRunner;
}
module.exports = FilterStatsApi;
module.exports = FilterStatsBackends;
function getEstimatedRows(pgQueryRunner, username, query, callback) {
pgQueryRunner.run(username, "EXPLAIN (FORMAT JSON)"+query, function(err, result_rows) {
@@ -23,7 +23,7 @@ function getEstimatedRows(pgQueryRunner, username, query, callback) {
});
}
FilterStatsApi.prototype.getFilterStats = function (username, unfiltered_query, filters, callback) {
FilterStatsBackends.prototype.getFilterStats = function (username, unfiltered_query, filters, callback) {
var stats = {};
var self = this;
step(

View File

@@ -1,4 +1,5 @@
var queryUtils = require('../../utils/query-utils');
const queryUtils = require('../../utils/query-utils');
const AggregationMapConfig = require('../../models/aggregation/aggregation-mapconfig');
function MapnikLayerStats () {
this._types = {
@@ -11,17 +12,255 @@ MapnikLayerStats.prototype.is = function (type) {
return this._types[type] ? this._types[type] : false;
};
function columnAggregations(field) {
if (field.type === 'number') {
return ['min', 'max', 'avg', 'sum'];
}
if (field.type === 'date') { // TODO other types too?
return ['min', 'max'];
}
return [];
}
function _getSQL(ctx, query, type='pre', zoom=0) {
let sql;
if (type === 'pre') {
sql = ctx.preQuery;
}
else {
sql = ctx.aggrQuery;
}
sql = queryUtils.subsituteTokensForZoom(sql, zoom || 0);
return query(sql);
}
function _estimatedFeatureCount(ctx) {
return queryUtils.queryPromise(ctx.dbConnection, _getSQL(ctx, queryUtils.getQueryRowEstimation))
.then(res => ({ estimatedFeatureCount: res.rows[0].rows }))
.catch(() => ({ estimatedFeatureCount: -1 }));
}
function _featureCount(ctx) {
if (ctx.metaOptions.featureCount) {
// TODO: if ctx.metaOptions.columnStats we can combine this with column stats query
return queryUtils.queryPromise(ctx.dbConnection, _getSQL(ctx, queryUtils.getQueryActualRowCount))
.then(res => ({ featureCount: res.rows[0].rows }));
}
return Promise.resolve();
}
function _aggrFeatureCount(ctx) {
if (ctx.metaOptions.hasOwnProperty('aggrFeatureCount')) {
// We expect as zoom level as the value of aggrFeatureCount
// TODO: it'd be nice to admit an array of zoom levels to
// return metadata for multiple levels.
return queryUtils.queryPromise(
ctx.dbConnection,
_getSQL(ctx, queryUtils.getQueryActualRowCount, 'post', ctx.metaOptions.aggrFeatureCount)
).then(res => ({ aggrFeatureCount: res.rows[0].rows }));
}
return Promise.resolve();
}
function _geometryType(ctx) {
if (ctx.metaOptions.geometryType) {
const geometryColumn = AggregationMapConfig.getAggregationGeometryColumn();
const sqlQuery = _getSQL(ctx, sql => queryUtils.getQueryGeometryType(sql, geometryColumn));
return queryUtils.queryPromise(ctx.dbConnection, sqlQuery)
.then(res => ({ geometryType: res.rows[0].geom_type }));
}
return Promise.resolve();
}
function _columns(ctx) {
if (ctx.metaOptions.columns || ctx.metaOptions.columnStats) {
// note: post-aggregation columns are in layer.options.columns when aggregation is present
return queryUtils.queryPromise(ctx.dbConnection, _getSQL(ctx, sql => queryUtils.getQueryLimited(sql, 0)))
.then(res => formatResultFields(ctx.dbConnection, res.fields));
}
return Promise.resolve();
}
// combine a list of results merging the properties of all the objects
// undefined results are admitted and ignored
function mergeResults(results) {
if (results) {
if (results.length === 0) {
return {};
}
return results.reduce((a, b) => {
if (a === undefined) {
return b;
}
if (b === undefined) {
return a;
}
return Object.assign({}, a, b);
});
}
}
// deeper (1 level) combination of a list of objects:
// mergeColumns([{ col1: { a: 1 }, col2: { a: 2 } }, { col1: { b: 3 } }]) => { col1: { a: 1, b: 3 }, col2: { a: 2 } }
function mergeColumns(results) {
if (results) {
if (results.length === 0) {
return {};
}
return results.reduce((a, b) => {
let c = Object.assign({}, b || {}, a || {});
Object.keys(c).forEach(key => {
if (b.hasOwnProperty(key)) {
c[key] = Object.assign(c[key], b[key]);
}
});
return c;
});
}
}
const SAMPLE_SEED = 0.5;
const DEFAULT_SAMPLE_ROWS = 100;
function _sample(ctx, numRows) {
if (ctx.metaOptions.sample) {
const sampleProb = Math.min(ctx.metaOptions.sample.num_rows / numRows, 1);
// We'll use a safety limit just in case numRows is a bad estimate
const requestedRows = ctx.metaOptions.sample.num_rows || DEFAULT_SAMPLE_ROWS;
const limit = Math.ceil(requestedRows * 1.5);
let columns = ctx.metaOptions.sample.include_columns;
return queryUtils.queryPromise(ctx.dbConnection, _getSQL(
ctx,
sql => queryUtils.getQuerySample(sql, sampleProb, limit, SAMPLE_SEED, columns)
)).then(res => ({ sample: res.rows }));
}
return Promise.resolve();
}
function _columnStats(ctx, columns) {
if (!columns) {
return Promise.resolve();
}
if (ctx.metaOptions.columnStats) {
let queries = [];
let aggr = [];
queries.push(new Promise(resolve => resolve(columns))); // add columns as first result
Object.keys(columns).forEach(name => {
aggr = aggr.concat(
columnAggregations(columns[name])
.map(fn => `${fn}(${name}) AS ${name}_${fn}`)
);
if (columns[name].type === 'string') {
const topN = ctx.metaOptions.columnStats.topCategories || 1024;
const includeNulls = ctx.metaOptions.columnStats.hasOwnProperty('includeNulls') ?
ctx.metaOptions.columnStats.includeNulls :
true;
// TODO: ctx.metaOptions.columnStats.maxCategories
// => use PG stats to dismiss columns with more distinct values
queries.push(
queryUtils.queryPromise(
ctx.dbConnection,
_getSQL(ctx, sql => queryUtils.getQueryTopCategories(sql, name, topN, includeNulls))
).then(res => ({ [name]: { categories: res.rows } }))
);
}
});
queries.push(
queryUtils.queryPromise(
ctx.dbConnection,
_getSQL(ctx, sql => `SELECT ${aggr.join(',')} FROM (${sql}) AS __cdb_query`)
).then(res => {
let stats = {};
Object.keys(columns).forEach(name => {
stats[name] = {};
columnAggregations(columns[name]).forEach(fn => {
stats[name][fn] = res.rows[0][`${name}_${fn}`];
});
});
return stats;
})
);
return Promise.all(queries).then(results => ({ columns: mergeColumns(results) }));
}
return Promise.resolve({ columns });
}
// This is adapted from SQL API:
function fieldType(cname) {
let tname;
switch (true) {
case /bool/.test(cname):
tname = 'boolean';
break;
case /int|float|numeric/.test(cname):
tname = 'number';
break;
case /text|char|unknown/.test(cname):
tname = 'string';
break;
case /date|time/.test(cname):
tname = 'date';
break;
default:
tname = cname;
}
if ( tname && cname.match(/^_/) ) {
tname += '[]';
}
return tname;
}
// columns are returned as an object { columnName1: { type1: ...}, ..}
// for consistency with SQL API
function formatResultFields(dbConnection, fields = []) {
let nfields = {};
for (let field of fields) {
const cname = dbConnection.typeName(field.dataTypeID);
let tname;
if ( ! cname ) {
tname = 'unknown(' + field.dataTypeID + ')';
} else {
tname = fieldType(cname);
}
nfields[field.name] = { type: tname };
}
return nfields;
}
MapnikLayerStats.prototype.getStats =
function (layer, dbConnection, callback) {
var queryRowCountSql = queryUtils.getQueryRowCount(layer.options.sql);
// This query would gather stats for postgresql table if not exists
dbConnection.query(queryRowCountSql, function (err, res) {
if (err) {
return callback(null, {estimatedFeatureCount: -1});
} else {
// We decided that the relation is 1 row == 1 feature
return callback(null, {estimatedFeatureCount: res.rows[0].rows});
}
let aggrQuery = layer.options.sql;
let preQuery = layer.options.sql_raw || aggrQuery;
let ctx = {
dbConnection,
preQuery,
aggrQuery,
metaOptions: layer.options.metadata || {}
};
// TODO: could save some queries if queryUtils.getAggregationMetadata() has been used and kept somewhere
// we would set queries.results.estimatedFeatureCount and queries.results.geometryType
// (if metaOptions.geometryType) from it.
// TODO: compute _sample with _featureCount when available
// TODO: add support for sample.exclude option by, in that case, forcing the columns query and
// passing the results to the sample query function.
Promise.all([
_estimatedFeatureCount(ctx).then(
({ estimatedFeatureCount }) => _sample(ctx, estimatedFeatureCount)
.then(sampleResults => mergeResults([sampleResults, { estimatedFeatureCount }]))
),
_featureCount(ctx),
_aggrFeatureCount(ctx),
_geometryType(ctx),
_columns(ctx).then(columns => _columnStats(ctx, columns))
]).then(results => {
callback(null, mergeResults(results));
}).catch(error => {
callback(error);
});
};

View File

@@ -1,25 +1,20 @@
var SubstitutionTokens = require('../utils/substitution-tokens');
const queryUtils = require('../utils/query-utils');
function OverviewsMetadataApi(pgQueryRunner) {
function OverviewsMetadataBackend(pgQueryRunner) {
this.pgQueryRunner = pgQueryRunner;
}
module.exports = OverviewsMetadataApi;
module.exports = OverviewsMetadataBackend;
function prepareSql(sql) {
return sql && SubstitutionTokens.replace(sql, {
bbox: 'ST_MakeEnvelope(0,0,0,0)',
scale_denominator: '0',
pixel_width: '1',
pixel_height: '1'
});
}
OverviewsMetadataApi.prototype.getOverviewsMetadata = function (username, sql, callback) {
OverviewsMetadataBackend.prototype.getOverviewsMetadata = function (username, sql, callback) {
// FIXME: Currently using internal function _cdb_schema_name
// CDB_Overviews should provide the schema information directly.
var query = 'SELECT *, _cdb_schema_name(base_table)' +
' FROM CDB_Overviews(CDB_QueryTablesText($windshaft$' + prepareSql(sql) + '$windshaft$))';
const query = `
SELECT *, _cdb_schema_name(base_table)
FROM CDB_Overviews(
CDB_QueryTablesText($windshaft$${queryUtils.substituteDummyTokens(sql)}$windshaft$)
);
`;
this.pgQueryRunner.run(username, query, function handleOverviewsRows(err, rows) {
if (err){
callback(err);

View File

@@ -1,7 +1,6 @@
var assert = require('assert');
var step = require('step');
var PSQL = require('cartodb-psql');
var _ = require('underscore');
const debug = require('debug')('cachechan');
function PgConnection(metadataBackend) {
this.metadataBackend = metadataBackend;
@@ -20,45 +19,59 @@ module.exports = PgConnection;
//
// @param callback function(err)
//
PgConnection.prototype.setDBAuth = function(username, params, callback) {
var self = this;
var user_params = {};
var auth_user = global.environment.postgres_auth_user;
var auth_pass = global.environment.postgres_auth_pass;
step(
function getId() {
self.metadataBackend.getUserId(username, this);
},
function(err, user_id) {
assert.ifError(err);
user_params.user_id = user_id;
var dbuser = _.template(auth_user, user_params);
_.extend(params, {dbuser:dbuser});
// skip looking up user_password if postgres_auth_pass
// doesn't contain the "user_password" label
if (!auth_pass || ! auth_pass.match(/\buser_password\b/) ) {
return null;
PgConnection.prototype.setDBAuth = function(username, params, apikeyType, callback) {
if (apikeyType === 'master') {
this.metadataBackend.getMasterApikey(username, (err, apikey) => {
if (err) {
if (isNameNotFoundError(err)) {
err.http_status = 404;
}
return callback(err);
}
self.metadataBackend.getUserDBPass(username, this);
},
function(err, user_password) {
assert.ifError(err);
user_params.user_password = user_password;
if ( auth_pass ) {
var dbpass = _.template(auth_pass, user_params);
_.extend(params, {dbpassword:dbpass});
params.dbuser = apikey.databaseRole;
params.dbpassword = apikey.databasePassword;
return callback();
});
} else if (apikeyType === 'regular') { //Actually it can be any type of api key
this.metadataBackend.getApikey(username, params.api_key, (err, apikey) => {
if (err) {
if (isNameNotFoundError(err)) {
err.http_status = 404;
}
return callback(err);
}
return true;
},
function finish(err) {
callback(err);
}
);
params.dbuser = apikey.databaseRole;
params.dbpassword = apikey.databasePassword;
return callback();
});
} else if (apikeyType === 'default') {
this.metadataBackend.getApikey(username, 'default_public', (err, apikey) => {
if (err) {
if (isNameNotFoundError(err)) {
err.http_status = 404;
}
return callback(err);
}
params.dbuser = apikey.databaseRole;
params.dbpassword = apikey.databasePassword;
return callback();
});
} else {
return callback(new Error(`Invalid Apikey type: ${apikeyType}, valid ones: master, regular, default`));
}
};
function isNameNotFoundError (err) {
return err.message && -1 !== err.message.indexOf('name not found');
}
// Set db connection parameters to those for the given username
//
// @param dbowner cartodb username of database owner,
@@ -71,36 +84,30 @@ PgConnection.prototype.setDBAuth = function(username, params, callback) {
// @param callback function(err)
//
PgConnection.prototype.setDBConn = function(dbowner, params, callback) {
var self = this;
// Add default database connection parameters
// if none given
_.defaults(params, {
dbuser: global.environment.postgres.user,
dbpassword: global.environment.postgres.password,
// dbuser: global.environment.postgres.user,
// dbpassword: global.environment.postgres.password,
dbhost: global.environment.postgres.host,
dbport: global.environment.postgres.port
});
step(
function getConnectionParams() {
self.metadataBackend.getUserDBConnectionParams(dbowner, this);
},
function extendParams(err, dbParams){
assert.ifError(err);
// we don't want null values or overwrite a non public user
if (params.dbuser !== 'publicuser' || !dbParams.dbuser) {
delete dbParams.dbuser;
}
if ( dbParams ) {
_.extend(params, dbParams);
}
return null;
},
function finish(err) {
callback(err);
}
);
};
this.metadataBackend.getUserDBConnectionParams(dbowner, (err, dbParams) => {
if (err) {
return callback(err);
}
// we dont want null values or overwrite a non public user
if (params.dbuser !== 'publicuser' || !dbParams.dbuser) {
delete dbParams.dbuser;
}
if (dbParams) {
_.extend(params, dbParams);
}
callback();
});
};
/**
* Returns a `cartodb-psql` object for a given username.
@@ -109,28 +116,37 @@ PgConnection.prototype.setDBConn = function(dbowner, params, callback) {
*/
PgConnection.prototype.getConnection = function(username, callback) {
var self = this;
debug("getConn1");
var params = {};
require('debug')('cachechan')("getConn1");
step(
function setAuth() {
self.setDBAuth(username, params, this);
},
function setConn(err) {
assert.ifError(err);
self.setDBConn(username, params, this);
},
function openConnection(err) {
assert.ifError(err);
return callback(err, new PSQL({
user: params.dbuser,
pass: params.dbpass,
host: params.dbhost,
port: params.dbport,
dbname: params.dbname
}));
this.getDatabaseParams(username, (err, databaseParams) => {
if (err) {
return callback(err);
}
);
return callback(err, new PSQL({
user: databaseParams.dbuser,
pass: databaseParams.dbpass,
host: databaseParams.dbhost,
port: databaseParams.dbport,
dbname: databaseParams.dbname
}));
});
};
PgConnection.prototype.getDatabaseParams = function(username, callback) {
const databaseParams = {};
this.setDBAuth(username, databaseParams, 'master', err => {
if (err) {
return callback(err);
}
this.setDBConn(username, databaseParams, err => {
if (err) {
return callback(err);
}
callback(null, databaseParams);
});
});
};

View File

@@ -1,6 +1,4 @@
var assert = require('assert');
var PSQL = require('cartodb-psql');
var step = require('step');
function PgQueryRunner(pgConnection) {
this.pgConnection = pgConnection;
@@ -16,31 +14,23 @@ module.exports = PgQueryRunner;
* @param {Function} callback function({Error}, {Array}) second argument is guaranteed to be an array
*/
PgQueryRunner.prototype.run = function(username, query, callback) {
var self = this;
var params = {};
step(
function setAuth() {
self.pgConnection.setDBAuth(username, params, this);
},
function setConn(err) {
assert.ifError(err);
self.pgConnection.setDBConn(username, params, this);
},
function executeQuery(err) {
assert.ifError(err);
var psql = new PSQL({
user: params.dbuser,
pass: params.dbpass,
host: params.dbhost,
port: params.dbport,
dbname: params.dbname
});
psql.query(query, function(err, resultSet) {
resultSet = resultSet || {};
return callback(err, resultSet.rows || []);
});
this.pgConnection.getDatabaseParams(username, (err, databaseParams) => {
if (err) {
return callback(err);
}
);
const psql = new PSQL({
user: databaseParams.dbuser,
pass: databaseParams.dbpass,
host: databaseParams.dbhost,
port: databaseParams.dbport,
dbname: databaseParams.dbname
});
psql.query(query, function (err, resultSet) {
resultSet = resultSet || {};
return callback(err, resultSet.rows || []);
});
});
};

View File

@@ -1,8 +1,8 @@
function TablesExtentApi(pgQueryRunner) {
function TablesExtentBackend(pgQueryRunner) {
this.pgQueryRunner = pgQueryRunner;
}
module.exports = TablesExtentApi;
module.exports = TablesExtentBackend;
/**
* Given a username and a list of tables it will return the estimated extent in SRID 4326 for all the tables based on
@@ -13,7 +13,7 @@ module.exports = TablesExtentApi;
* `table_name` format as valid input
* @param {Function} callback function(err, result) {Object} result with `west`, `south`, `east`, `north`
*/
TablesExtentApi.prototype.getBounds = function (username, tables, callback) {
TablesExtentBackend.prototype.getBounds = function (username, tables, callback) {
var estimatedExtentSQLs = tables.map(function(table) {
return "ST_EstimatedExtent('" + table.schema_name + "', '" + table.table_name + "', 'the_geom_webmercator')";
});

View File

@@ -5,17 +5,19 @@ var step = require('step');
* @param metadataBackend
* @param options
* @constructor
* @type {UserLimitsApi}
* @type {UserLimitsBackend}
*/
function UserLimitsApi(metadataBackend, options) {
function UserLimitsBackend(metadataBackend, options) {
this.metadataBackend = metadataBackend;
this.options = options || {};
this.options.limits = this.options.limits || {};
this.preprareRateLimit();
}
module.exports = UserLimitsApi;
module.exports = UserLimitsBackend;
UserLimitsApi.prototype.getRenderLimits = function (username, apiKey, callback) {
UserLimitsBackend.prototype.getRenderLimits = function (username, apiKey, callback) {
var self = this;
var limits = {
@@ -38,7 +40,7 @@ UserLimitsApi.prototype.getRenderLimits = function (username, apiKey, callback)
});
};
UserLimitsApi.prototype.getTimeoutRenderLimit = function (username, apiKey, callback) {
UserLimitsBackend.prototype.getTimeoutRenderLimit = function (username, apiKey, callback) {
var self = this;
step(
@@ -77,3 +79,13 @@ UserLimitsApi.prototype.getTimeoutRenderLimit = function (username, apiKey, call
callback
);
};
UserLimitsBackend.prototype.preprareRateLimit = function () {
if (this.options.limits.rateLimitsEnabled) {
this.metadataBackend.loadRateLimitsScript();
}
};
UserLimitsBackend.prototype.getRateLimit = function (user, endpointGroup, callback) {
this.metadataBackend.getRateLimit(user, 'maps', endpointGroup, callback);
};

View File

@@ -6,12 +6,20 @@ var queue = require('queue-async');
var LruCache = require("lru-cache");
function NamedMapProviderCache(templateMaps, pgConnection, metadataBackend, userLimitsApi, mapConfigAdapter) {
function NamedMapProviderCache(
templateMaps,
pgConnection,
metadataBackend,
userLimitsBackend,
mapConfigAdapter,
affectedTablesCache
) {
this.templateMaps = templateMaps;
this.pgConnection = pgConnection;
this.metadataBackend = metadataBackend;
this.userLimitsApi = userLimitsApi;
this.userLimitsBackend = userLimitsBackend;
this.mapConfigAdapter = mapConfigAdapter;
this.affectedTablesCache = affectedTablesCache;
this.providerCache = new LruCache({ max: 2000 });
}
@@ -28,8 +36,9 @@ NamedMapProviderCache.prototype.get = function(user, templateId, config, authTok
this.templateMaps,
this.pgConnection,
this.metadataBackend,
this.userLimitsApi,
this.userLimitsBackend,
this.mapConfigAdapter,
this.affectedTablesCache,
user,
templateId,
config,

View File

@@ -1,8 +0,0 @@
module.exports = {
Analyses: require('./analyses'),
Layergroup: require('./layergroup'),
Map: require('./map'),
NamedMaps: require('./named_maps'),
NamedMapsAdmin: require('./named_maps_admin'),
ServerInfo: require('./server_info')
};

View File

@@ -1,513 +0,0 @@
var assert = require('assert');
var step = require('step');
var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');
var allowQueryParams = require('../middleware/allow-query-params');
var vectorError = require('../middleware/vector-error');
var DataviewBackend = require('../backends/dataview');
var AnalysisStatusBackend = require('../backends/analysis-status');
var MapStoreMapConfigProvider = require('../models/mapconfig/provider/map-store-provider');
var QueryTables = require('cartodb-query-tables');
/**
* @param {AuthApi} authApi
* @param {PgConnection} pgConnection
* @param {MapStore} mapStore
* @param {TileBackend} tileBackend
* @param {PreviewBackend} previewBackend
* @param {AttributesBackend} attributesBackend
* @param {SurrogateKeysCache} surrogateKeysCache
* @param {UserLimitsApi} userLimitsApi
* @param {LayergroupAffectedTables} layergroupAffectedTables
* @param {AnalysisBackend} analysisBackend
* @constructor
*/
function LayergroupController(prepareContext, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, analysisBackend) {
this.pgConnection = pgConnection;
this.mapStore = mapStore;
this.tileBackend = tileBackend;
this.previewBackend = previewBackend;
this.attributesBackend = attributesBackend;
this.surrogateKeysCache = surrogateKeysCache;
this.userLimitsApi = userLimitsApi;
this.layergroupAffectedTables = layergroupAffectedTables;
this.dataviewBackend = new DataviewBackend(analysisBackend);
this.analysisStatusBackend = new AnalysisStatusBackend();
this.prepareContext = prepareContext;
}
module.exports = LayergroupController;
LayergroupController.prototype.register = function(app) {
app.get(
app.base_url_mapconfig + '/:token/:z/:x/:y@:scale_factor?x.:format',
cors(),
userMiddleware,
this.prepareContext,
this.tile.bind(this),
vectorError()
);
app.get(
app.base_url_mapconfig + '/:token/:z/:x/:y.:format',
cors(),
userMiddleware,
this.prepareContext,
this.tile.bind(this),
vectorError()
);
app.get(
app.base_url_mapconfig + '/:token/:layer/:z/:x/:y.(:format)',
cors(),
userMiddleware,
validateLayerRouteMiddleware,
this.prepareContext,
this.layer.bind(this),
vectorError()
);
app.get(
app.base_url_mapconfig + '/:token/:layer/attributes/:fid',
cors(),
userMiddleware,
this.prepareContext,
this.attributes.bind(this)
);
app.get(
app.base_url_mapconfig + '/static/center/:token/:z/:lat/:lng/:width/:height.:format',
cors(),
userMiddleware,
allowQueryParams(['layer']),
this.prepareContext,
this.center.bind(this)
);
app.get(
app.base_url_mapconfig + '/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format',
cors(),
userMiddleware,
allowQueryParams(['layer']),
this.prepareContext,
this.bbox.bind(this)
);
// Undocumented/non-supported API endpoint methods.
// Use at your own peril.
var allowedDataviewQueryParams = [
'filters', // json
'own_filter', // 0, 1
'no_filters', // 0, 1
'bbox', // w,s,e,n
'start', // number
'end', // number
'column_type', // string
'bins', // number
'aggregation', //string
'offset', // number
'q', // widgets search
'categories', // number
];
app.get(
app.base_url_mapconfig + '/:token/dataview/:dataviewName',
cors(),
userMiddleware,
allowQueryParams(allowedDataviewQueryParams),
this.prepareContext,
this.dataview.bind(this)
);
app.get(
app.base_url_mapconfig + '/:token/:layer/widget/:dataviewName',
cors(),
userMiddleware,
allowQueryParams(allowedDataviewQueryParams),
this.prepareContext,
this.dataview.bind(this)
);
app.get(
app.base_url_mapconfig + '/:token/dataview/:dataviewName/search',
cors(),
userMiddleware,
allowQueryParams(allowedDataviewQueryParams),
this.prepareContext,
this.dataviewSearch.bind(this)
);
app.get(
app.base_url_mapconfig + '/:token/:layer/widget/:dataviewName/search',
cors(),
userMiddleware,
allowQueryParams(allowedDataviewQueryParams),
this.prepareContext,
this.dataviewSearch.bind(this)
);
app.get(
app.base_url_mapconfig + '/:token/analysis/node/:nodeId',
cors(),
userMiddleware,
this.prepareContext,
this.analysisNodeStatus.bind(this)
);
};
LayergroupController.prototype.analysisNodeStatus = function(req, res, next) {
var self = this;
step(
function retrieveNodeStatus() {
self.analysisStatusBackend.getNodeStatus(res.locals, this);
},
function finish(err, nodeStatus, stats) {
req.profiler.add(stats || {});
if (err) {
err.label = 'GET NODE STATUS';
next(err);
} else {
self.sendResponse(req, res, nodeStatus, 200, {
'Cache-Control': 'public,max-age=5',
'Last-Modified': new Date().toUTCString()
});
}
}
);
};
LayergroupController.prototype.dataview = function(req, res, next) {
var self = this;
step(
function retrieveDataview() {
var mapConfigProvider = new MapStoreMapConfigProvider(
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
);
self.dataviewBackend.getDataview(
mapConfigProvider,
res.locals.user,
res.locals,
this
);
},
function finish(err, dataview, stats) {
req.profiler.add(stats || {});
if (err) {
err.label = 'GET DATAVIEW';
next(err);
} else {
self.sendResponse(req, res, dataview, 200);
}
}
);
};
LayergroupController.prototype.dataviewSearch = function(req, res, next) {
var self = this;
step(
function searchDataview() {
var mapConfigProvider = new MapStoreMapConfigProvider(
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
);
self.dataviewBackend.search(mapConfigProvider, res.locals.user, req.params.dataviewName, res.locals, this);
},
function finish(err, searchResult, stats) {
req.profiler.add(stats || {});
if (err) {
err.label = 'GET DATAVIEW SEARCH';
next(err);
} else {
self.sendResponse(req, res, searchResult, 200);
}
}
);
};
LayergroupController.prototype.attributes = function(req, res, next) {
var self = this;
req.profiler.start('windshaft.maplayer_attribute');
step(
function retrieveFeatureAttributes() {
var mapConfigProvider = new MapStoreMapConfigProvider(
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
);
self.attributesBackend.getFeatureAttributes(mapConfigProvider, res.locals, false, this);
},
function finish(err, tile, stats) {
req.profiler.add(stats || {});
if (err) {
err.label = 'GET ATTRIBUTES';
next(err);
} else {
self.sendResponse(req, res, tile, 200);
}
}
);
};
// Gets a tile for a given token and set of tile ZXY coords. (OSM style)
LayergroupController.prototype.tile = function(req, res, next) {
req.profiler.start('windshaft.map_tile');
this.tileOrLayer(req, res, next);
};
// Gets a tile for a given token, layer set of tile ZXY coords. (OSM style)
LayergroupController.prototype.layer = function(req, res, next) {
req.profiler.start('windshaft.maplayer_tile');
this.tileOrLayer(req, res, next);
};
LayergroupController.prototype.tileOrLayer = function (req, res, next) {
var self = this;
step(
function mapController$getTileOrGrid() {
self.tileBackend.getTile(
new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals),
req.params, this
);
},
function mapController$finalize(err, tile, headers, stats) {
req.profiler.add(stats);
self.finalizeGetTileOrGrid(err, req, res, tile, headers, next);
}
);
};
function getStatusCode(tile, format){
return tile.length===0 && format==='mvt'? 204:200;
}
// This function is meant for being called as the very last
// step by all endpoints serving tiles or grids
LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers, next) {
var supportedFormats = {
grid_json: true,
json_torque: true,
torque_json: true,
png: true,
png32: true,
mvt: true
};
var formatStat = 'invalid';
if (req.params.format) {
var format = req.params.format.replace('.', '_');
if (supportedFormats[format]) {
formatStat = format;
}
}
if (err) {
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
var errMsg = err.message ? ( '' + err.message ) : ( '' + err );
// Rewrite mapnik parsing errors to start with layer number
var matches = errMsg.match("(.*) in style 'layer([0-9]+)'");
if (matches) {
errMsg = 'style'+matches[2]+': ' + matches[1];
}
err.message = errMsg;
err.label = 'TILE RENDER';
next(err);
global.statsClient.increment('windshaft.tiles.error');
global.statsClient.increment('windshaft.tiles.' + formatStat + '.error');
} else {
this.sendResponse(req, res, tile, getStatusCode(tile, formatStat), headers);
global.statsClient.increment('windshaft.tiles.success');
global.statsClient.increment('windshaft.tiles.' + formatStat + '.success');
}
};
LayergroupController.prototype.bbox = function(req, res, next) {
this.staticMap(req, res, +req.params.width, +req.params.height, {
west: +req.params.west,
north: +req.params.north,
east: +req.params.east,
south: +req.params.south
}, null, next);
};
LayergroupController.prototype.center = function(req, res, next) {
this.staticMap(req, res, +req.params.width, +req.params.height, +req.params.z, {
lng: +req.params.lng,
lat: +req.params.lat
}, next);
};
LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center, next) {
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
// We force always the tile to be generated using PNG because
// is the only format we support by now
res.locals.format = 'png';
res.locals.layer = res.locals.layer || 'all';
var self = this;
step(
function getImage() {
if (center) {
self.previewBackend.getImage(
new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals),
format, width, height, zoom, center, this);
} else {
self.previewBackend.getImage(
new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals),
format, width, height, zoom /* bounds */, this);
}
},
function handleImage(err, image, headers, stats) {
req.profiler.done('render-' + format);
req.profiler.add(stats || {});
if (err) {
err.label = 'STATIC_MAP';
next(err);
} else {
res.set('Content-Type', headers['Content-Type'] || 'image/' + format);
self.sendResponse(req, res, image, 200);
}
}
);
};
LayergroupController.prototype.sendResponse = function(req, res, body, status, headers) {
var self = this;
req.profiler.done('res');
res.set('Cache-Control', 'public,max-age=31536000');
// Set Last-Modified header
var lastUpdated;
if (res.locals.cache_buster) {
// Assuming cache_buster is a timestamp
lastUpdated = new Date(parseInt(res.locals.cache_buster));
} else {
lastUpdated = new Date();
}
res.set('Last-Modified', lastUpdated.toUTCString());
var dbName = res.locals.dbname;
step(
function getAffectedTables() {
self.getAffectedTables(res.locals.user, dbName, res.locals.token, this);
},
function sendResponse(err, affectedTables) {
req.profiler.done('affectedTables');
if (err) {
global.logger.warn('ERROR generating cache channel: ' + err);
}
if (!!affectedTables) {
res.set('X-Cache-Channel', affectedTables.getCacheChannel());
self.surrogateKeysCache.tag(res, affectedTables);
}
if (headers) {
res.set(headers);
}
res.status(status);
if (!Buffer.isBuffer(body) && typeof body === 'object') {
if (req.query && req.query.callback) {
res.jsonp(body);
} else {
res.json(body);
}
} else {
res.send(body);
}
}
);
};
LayergroupController.prototype.getAffectedTables = function(user, dbName, layergroupId, callback) {
if (this.layergroupAffectedTables.hasAffectedTables(dbName, layergroupId)) {
return callback(null, this.layergroupAffectedTables.get(dbName, layergroupId));
}
var self = this;
step(
function extractSQL() {
step(
function loadFromStore() {
self.mapStore.load(layergroupId, this);
},
function getSQL(err, mapConfig) {
assert.ifError(err);
var queries = [];
mapConfig.getLayers().forEach(function(layer) {
queries.push(layer.options.sql);
if (layer.options.affected_tables) {
layer.options.affected_tables.map(function(table) {
queries.push('SELECT * FROM ' + table + ' LIMIT 0');
});
}
});
return queries.length ? queries.join(';') : null;
},
this
);
},
function findAffectedTables(err, sql) {
assert.ifError(err);
if ( ! sql ) {
throw new Error("this request doesn't need an X-Cache-Channel generated");
}
step(
function getConnection() {
self.pgConnection.getConnection(user, this);
},
function getAffectedTables(err, connection) {
assert.ifError(err);
QueryTables.getAffectedTablesFromQuery(connection, sql, this);
},
this
);
},
function buildCacheChannel(err, tables) {
assert.ifError(err);
self.layergroupAffectedTables.set(dbName, layergroupId, tables);
return tables;
},
callback
);
};
function validateLayerRouteMiddleware(req, res, next) {
if (req.params.token === 'static') {
return next('route');
}
next();
}

View File

@@ -1,729 +0,0 @@
var _ = require('underscore');
var windshaft = require('windshaft');
var QueryTables = require('cartodb-query-tables');
var ResourceLocator = require('../models/resource-locator');
var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');
const allowQueryParams = require('../middleware/allow-query-params');
var MapConfig = windshaft.model.MapConfig;
var Datasource = windshaft.model.Datasource;
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
var NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider');
var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/create-layergroup-provider');
/**
* @param {AuthApi} authApi
* @param {PgConnection} pgConnection
* @param {TemplateMaps} templateMaps
* @param {MapBackend} mapBackend
* @param metadataBackend
* @param {SurrogateKeysCache} surrogateKeysCache
* @param {UserLimitsApi} userLimitsApi
* @param {LayergroupAffectedTables} layergroupAffectedTables
* @param {MapConfigAdapter} mapConfigAdapter
* @param {StatsBackend} statsBackend
* @constructor
*/
function MapController(prepareContext, pgConnection, templateMaps, mapBackend, metadataBackend,
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, mapConfigAdapter,
statsBackend) {
this.pgConnection = pgConnection;
this.templateMaps = templateMaps;
this.mapBackend = mapBackend;
this.metadataBackend = metadataBackend;
this.surrogateKeysCache = surrogateKeysCache;
this.userLimitsApi = userLimitsApi;
this.layergroupAffectedTables = layergroupAffectedTables;
this.mapConfigAdapter = mapConfigAdapter;
this.resourceLocator = new ResourceLocator(global.environment);
this.statsBackend = statsBackend;
this.prepareContext = prepareContext;
}
module.exports = MapController;
MapController.prototype.register = function(app) {
const { base_url_mapconfig, base_url_templated } = app;
const useTemplate = true;
app.get(base_url_mapconfig, this.composeCreateMapMiddleware());
app.post(base_url_mapconfig, this.composeCreateMapMiddleware());
app.get(`${base_url_templated}/:template_id/jsonp`, this.composeCreateMapMiddleware(useTemplate));
app.post(`${base_url_templated}/:template_id`, this.composeCreateMapMiddleware(useTemplate));
app.options(app.base_url_mapconfig, cors('Content-Type'));
};
MapController.prototype.composeCreateMapMiddleware = function (useTemplate = false) {
const isTemplateInstantiation = useTemplate;
const useTemplateHash = useTemplate;
const includeQuery = !useTemplate;
const label = useTemplate ? 'NAMED MAP LAYERGROUP' : 'ANONYMOUS LAYERGROUP';
const addContext = !useTemplate;
return [
cors(),
userMiddleware,
allowQueryParams(['aggregation']),
this.prepareContext,
this.initProfiler(isTemplateInstantiation),
this.checkJsonContentType(),
useTemplate ? this.checkInstantiteLayergroup() : this.checkCreateLayergroup(),
useTemplate ? this.getTemplate() : this.prepareAdapterMapConfig(),
useTemplate ? this.instantiateLayergroup() : this.createLayergroup(),
this.incrementMapViewCount(),
this.augmentLayergroupData(),
this.getAffectedTables(),
this.setCacheChannel(),
this.setLastModified(),
this.setLastUpdatedTimeToLayergroup(),
this.setCacheControl(),
this.setLayerStats(),
this.setLayergroupIdHeader(useTemplateHash),
this.setDataviewsAndWidgetsUrlsToLayergroupMetadata(),
this.setAnalysesMetadataToLayergroup(includeQuery),
this.setTurboCartoMetadataToLayergroup(),
this.setAggregationMetadataToLayergroup(),
this.setTilejsonMetadataToLayergroup(),
this.setSurrogateKeyHeader(),
this.sendResponse(),
this.augmentError({ label, addContext })
];
};
MapController.prototype.initProfiler = function (isTemplateInstantiation) {
const operation = isTemplateInstantiation ? 'instance_template' : 'createmap';
return function initProfilerMiddleware (req, res, next) {
req.profiler.start(`windshaft-cartodb.${operation}_${req.method.toLowerCase()}`);
req.profiler.done(`${operation}.initProfilerMiddleware`);
next();
};
};
MapController.prototype.checkJsonContentType = function () {
return function checkJsonContentTypeMiddleware(req, res, next) {
if (req.method === 'POST' && !req.is('application/json')) {
return next(new Error('POST data must be of type application/json'));
}
req.profiler.done('checkJsonContentTypeMiddleware');
next();
};
};
MapController.prototype.checkInstantiteLayergroup = function () {
return function checkInstantiteLayergroupMiddleware(req, res, next) {
if (req.method === 'GET') {
const { callback, config } = req.query;
if (callback === undefined || callback.length === 0) {
return next(new Error('callback parameter should be present and be a function name'));
}
if (config) {
try {
req.body = JSON.parse(config);
} catch(e) {
return next(new Error('Invalid config parameter, should be a valid JSON'));
}
}
}
req.profiler.done('checkInstantiteLayergroup');
return next();
};
};
MapController.prototype.checkCreateLayergroup = function () {
return function checkCreateLayergroupMiddleware (req, res, next) {
if (req.method === 'GET') {
const { config } = res.locals;
if (!config) {
return next(new Error('layergroup GET needs a "config" parameter'));
}
try {
req.body = JSON.parse(config);
} catch (err) {
return next(err);
}
}
req.profiler.done('checkCreateLayergroup');
return next();
};
};
MapController.prototype.getTemplate = function () {
return function getTemplateMiddleware (req, res, next) {
const templateParams = req.body;
const { user } = res.locals;
const mapconfigProvider = new NamedMapMapConfigProvider(
this.templateMaps,
this.pgConnection,
this.metadataBackend,
this.userLimitsApi,
this.mapConfigAdapter,
user,
req.params.template_id,
templateParams,
res.locals.auth_token,
res.locals
);
mapconfigProvider.getMapConfig((err, mapconfig, rendererParams) => {
req.profiler.done('named.getMapConfig');
if (err) {
return next(err);
}
res.locals.mapconfig = mapconfig;
res.locals.rendererParams = rendererParams;
res.locals.mapconfigProvider = mapconfigProvider;
next();
});
}.bind(this);
};
MapController.prototype.prepareAdapterMapConfig = function () {
return function prepareAdapterMapConfigMiddleware(req, res, next) {
const requestMapConfig = req.body;
const { user, dbhost, dbport, dbname, dbuser, dbpassword, api_key } = res.locals;
const context = {
analysisConfiguration: {
user,
db: {
host: dbhost,
port: dbport,
dbname: dbname,
user: dbuser,
pass: dbpassword
},
batch: {
username: user,
apiKey: api_key
}
}
};
this.mapConfigAdapter.getMapConfig(user, requestMapConfig, res.locals, context, (err, requestMapConfig) => {
req.profiler.done('anonymous.getMapConfig');
if (err) {
return next(err);
}
req.body = requestMapConfig;
res.locals.context = context;
next();
});
}.bind(this);
};
MapController.prototype.createLayergroup = function () {
return function createLayergroupMiddleware (req, res, next) {
const requestMapConfig = req.body;
const { context, user } = res.locals;
const datasource = context.datasource || Datasource.EmptyDatasource();
const mapconfig = new MapConfig(requestMapConfig, datasource);
const mapconfigProvider =
new CreateLayergroupMapConfigProvider(mapconfig, user, this.userLimitsApi, res.locals);
res.locals.mapconfig = mapconfig;
res.locals.analysesResults = context.analysesResults;
this.mapBackend.createLayergroup(mapconfig, res.locals, mapconfigProvider, (err, layergroup) => {
req.profiler.done('createLayergroup');
if (err) {
return next(err);
}
res.locals.layergroup = layergroup;
next();
});
}.bind(this);
};
MapController.prototype.instantiateLayergroup = function () {
return function instantiateLayergroupMiddleware (req, res, next) {
const { user, mapconfig, rendererParams } = res.locals;
const mapconfigProvider =
new CreateLayergroupMapConfigProvider(mapconfig, user, this.userLimitsApi, rendererParams);
this.mapBackend.createLayergroup(mapconfig, rendererParams, mapconfigProvider, (err, layergroup) => {
req.profiler.done('createLayergroup');
if (err) {
return next(err);
}
res.locals.layergroup = layergroup;
const { mapconfigProvider } = res.locals;
res.locals.analysesResults = mapconfigProvider.analysesResults;
res.locals.template = mapconfigProvider.template;
res.locals.templateName = mapconfigProvider.getTemplateName();
res.locals.context = mapconfigProvider.context;
next();
});
}.bind(this);
};
MapController.prototype.incrementMapViewCount = function () {
return function incrementMapViewCountMiddleware(req, res, next) {
const { mapconfig, user } = res.locals;
// Error won't blow up, just be logged.
this.metadataBackend.incMapviewCount(user, mapconfig.obj().stat_tag, (err) => {
req.profiler.done('incMapviewCount');
if (err) {
global.logger.log(`ERROR: failed to increment mapview count for user '${user}': ${err.message}`);
}
next();
});
}.bind(this);
};
MapController.prototype.augmentLayergroupData = function () {
return function augmentLayergroupDataMiddleware (req, res, next) {
const { layergroup } = res.locals;
// include in layergroup response the variables in serverMedata
// those variables are useful to send to the client information
// about how to reach this server or information about it
_.extend(layergroup, global.environment.serverMetadata);
next();
};
};
function getTemplateUrl(url) {
return url.https || url.http;
}
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 = {};
const url = {};
if (hasMapnikLayers) {
const vectorResource = `${layergroup.layergroupid}/{z}/{x}/{y}.mvt`;
tilejson.vector = getTilejson(
this.resourceLocator.getTileUrls(user, vectorResource)
);
url.vector = getTemplateUrl(this.resourceLocator.getTemplateUrls(user, vectorResource));
if (!isVectorOnlyMapConfig) {
const rasterResource = `${layergroup.layergroupid}/{z}/{x}/{y}.png`;
tilejson.raster = getTilejson(
this.resourceLocator.getTileUrls(user, rasterResource)
);
url.raster = getTemplateUrl(this.resourceLocator.getTemplateUrls(user, rasterResource));
}
}
layergroup.metadata.tilejson = tilejson;
layergroup.metadata.url = url;
next();
}.bind(this);
};
MapController.prototype.getAffectedTables = function () {
return function getAffectedTablesMiddleware (req, res, next) {
const { dbname, layergroup, user, mapconfig } = res.locals;
this.pgConnection.getConnection(user, (err, connection) => {
if (err) {
return next(err);
}
const sql = [];
mapconfig.getLayers().forEach(function(layer) {
sql.push(layer.options.sql);
if (layer.options.affected_tables) {
layer.options.affected_tables.map(function(table) {
sql.push('SELECT * FROM ' + table + ' LIMIT 0');
});
}
});
QueryTables.getAffectedTablesFromQuery(connection, sql.join(';'), (err, affectedTables) => {
req.profiler.done('getAffectedTablesFromQuery');
if (err) {
return next(err);
}
// feed affected tables cache so it can be reused from, for instance, layergroup controller
this.layergroupAffectedTables.set(dbname, layergroup.layergroupId, affectedTables);
res.locals.affectedTables = affectedTables;
next();
});
});
}.bind(this);
};
MapController.prototype.setCacheChannel = function () {
return function setCacheChannelMiddleware (req, res, next) {
const { affectedTables } = res.locals;
if (req.method === 'GET') {
res.set('X-Cache-Channel', affectedTables.getCacheChannel());
}
next();
};
};
MapController.prototype.setLastModified = function () {
return function setLastModifiedMiddleware (req, res, next) {
if (req.method === 'GET') {
res.set('Last-Modified', (new Date()).toUTCString());
}
next();
};
};
MapController.prototype.setLastUpdatedTimeToLayergroup = function () {
return function setLastUpdatedTimeToLayergroupMiddleware (req, res, next) {
const { affectedTables, layergroup, analysesResults } = res.locals;
var lastUpdateTime = affectedTables.getLastUpdatedAt();
lastUpdateTime = getLastUpdatedTime(analysesResults, lastUpdateTime) || lastUpdateTime;
// last update for layergroup cache buster
layergroup.layergroupid = layergroup.layergroupid + ':' + lastUpdateTime;
layergroup.last_updated = new Date(lastUpdateTime).toISOString();
next();
};
};
function getLastUpdatedTime(analysesResults, lastUpdateTime) {
if (!Array.isArray(analysesResults)) {
return lastUpdateTime;
}
return analysesResults.reduce(function(lastUpdateTime, analysis) {
return analysis.getNodes().reduce(function(lastNodeUpdatedAtTime, node) {
var nodeUpdatedAtDate = node.getUpdatedAt();
var nodeUpdatedTimeAt = (nodeUpdatedAtDate && nodeUpdatedAtDate.getTime()) || 0;
return nodeUpdatedTimeAt > lastNodeUpdatedAtTime ? nodeUpdatedTimeAt : lastNodeUpdatedAtTime;
}, lastUpdateTime);
}, lastUpdateTime);
}
MapController.prototype.setCacheControl = function () {
return function setCacheControlMiddleware (req, res, next) {
if (req.method === 'GET') {
var ttl = global.environment.varnish.layergroupTtl || 86400;
res.set('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
}
next();
};
};
MapController.prototype.setLayerStats = function () {
return function setLayerStatsMiddleware(req, res, next) {
const { user, mapconfig, layergroup } = res.locals;
this.pgConnection.getConnection(user, (err, connection) => {
if (err) {
return next(err);
}
this.statsBackend.getStats(mapconfig, connection, function(err, layersStats) {
if (err) {
return next(err);
}
if (layersStats.length > 0) {
layergroup.metadata.layers.forEach(function (layer, index) {
layer.meta.stats = layersStats[index];
});
}
next();
});
});
}.bind(this);
};
MapController.prototype.setLayergroupIdHeader = function (useTemplateHash) {
return function setLayergroupIdHeaderMiddleware (req, res, next) {
const { layergroup, user, template } = res.locals;
if (useTemplateHash) {
var templateHash = this.templateMaps.fingerPrint(template).substring(0, 8);
layergroup.layergroupid = `${user}@${templateHash}@${layergroup.layergroupid}`;
}
res.set('X-Layergroup-Id', layergroup.layergroupid);
next();
}.bind(this);
};
MapController.prototype.setDataviewsAndWidgetsUrlsToLayergroupMetadata = function () {
return function setDataviewsAndWidgetsUrlsToLayergroupMetadataMiddleware (req, res, next) {
const { layergroup, user, mapconfig } = res.locals;
this.addDataviewsAndWidgetsUrls(user, layergroup, mapconfig.obj());
next();
}.bind(this);
};
// TODO this should take into account several URL patterns
MapController.prototype.addDataviewsAndWidgetsUrls = function(username, layergroup, mapConfig) {
this.addDataviewsUrls(username, layergroup, mapConfig);
this.addWidgetsUrl(username, layergroup, mapConfig);
};
MapController.prototype.addDataviewsUrls = function(username, layergroup, mapConfig) {
layergroup.metadata.dataviews = layergroup.metadata.dataviews || {};
var dataviews = mapConfig.dataviews || {};
Object.keys(dataviews).forEach(function(dataviewName) {
var resource = layergroup.layergroupid + '/dataview/' + dataviewName;
layergroup.metadata.dataviews[dataviewName] = {
url: this.resourceLocator.getUrls(username, resource)
};
}.bind(this));
};
MapController.prototype.addWidgetsUrl = function(username, layergroup, mapConfig) {
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
var mapConfigLayer = mapConfig.layers[layerIndex];
if (mapConfigLayer.options && mapConfigLayer.options.widgets) {
layer.widgets = layer.widgets || {};
Object.keys(mapConfigLayer.options.widgets).forEach(function(widgetName) {
var resource = layergroup.layergroupid + '/' + layerIndex + '/widget/' + widgetName;
layer.widgets[widgetName] = {
type: mapConfigLayer.options.widgets[widgetName].type,
url: this.resourceLocator.getUrls(username, resource)
};
}.bind(this));
}
return layer;
}.bind(this));
}
};
MapController.prototype.setAnalysesMetadataToLayergroup = function (includeQuery) {
return function setAnalysesMetadataToLayergroupMiddleware (req, res, next) {
const { layergroup, user, analysesResults = [] } = res.locals;
this.addAnalysesMetadata(user, layergroup, analysesResults, includeQuery);
next();
}.bind(this);
};
MapController.prototype.addAnalysesMetadata = function(username, layergroup, analysesResults, includeQuery) {
includeQuery = includeQuery || false;
analysesResults = analysesResults || [];
layergroup.metadata.analyses = [];
analysesResults.forEach(function(analysis) {
var nodes = analysis.getNodes();
layergroup.metadata.analyses.push({
nodes: nodes.reduce(function(nodesIdMap, node) {
if (node.params.id) {
var nodeResource = layergroup.layergroupid + '/analysis/node/' + node.id();
var nodeRepr = {
status: node.getStatus(),
url: this.resourceLocator.getUrls(username, nodeResource)
};
if (includeQuery) {
nodeRepr.query = node.getQuery();
}
if (node.getStatus() === 'failed') {
nodeRepr.error_message = node.getErrorMessage();
}
nodesIdMap[node.params.id] = nodeRepr;
}
return nodesIdMap;
}.bind(this), {})
});
}.bind(this));
};
MapController.prototype.setTurboCartoMetadataToLayergroup = function () {
return function setTurboCartoMetadataToLayergroupMiddleware (req, res, next) {
const { layergroup, mapconfig, context } = res.locals;
addTurboCartoContextMetadata(layergroup, mapconfig.obj(), context);
next();
};
};
function addTurboCartoContextMetadata(layergroup, mapConfig, context) {
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
if (context.turboCarto && Array.isArray(context.turboCarto.layers)) {
layer.meta.cartocss_meta = context.turboCarto.layers[layerIndex];
}
return layer;
});
}
}
// TODO: see how evolve this function, it's a good candidate to be refactored
MapController.prototype.setAggregationMetadataToLayergroup = function () {
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;
if (req.method === 'GET' && affectedTables.tables && affectedTables.tables.length > 0) {
this.surrogateKeysCache.tag(res, affectedTables);
}
if (templateName) {
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, templateName));
}
next();
}.bind(this);
};
MapController.prototype.sendResponse = function () {
return function sendResponseMiddleware (req, res) {
req.profiler.done('res');
const { layergroup } = res.locals;
res.status(200);
if (req.query && req.query.callback) {
res.jsonp(layergroup);
} else {
res.json(layergroup);
}
};
};
MapController.prototype.augmentError = function (options) {
const { addContext = false, label = 'MAPS CONTROLLER' } = options;
return function augmentErrorMiddleware (err, req, res, next) {
req.profiler.done('error');
const { mapconfig } = res.locals;
if (addContext) {
err = Number.isFinite(err.layerIndex) ? populateError(err, mapconfig) : err;
}
err.label = label;
next(err);
};
};
function populateError(err, mapConfig) {
var error = new Error(err.message);
error.http_status = err.http_status;
if (!err.http_status && err.message.indexOf('column "the_geom_webmercator" does not exist') >= 0) {
error.http_status = 400;
}
error.type = 'layer';
error.subtype = err.message.indexOf('Postgis Plugin') >= 0 ? 'query' : undefined;
error.layer = {
id: mapConfig.getLayerId(err.layerIndex),
index: err.layerIndex,
type: mapConfig.layerType(err.layerIndex)
};
return error;
}

View File

@@ -1,490 +0,0 @@
const NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
const cors = require('../middleware/cors');
const userMiddleware = require('../middleware/user');
const allowQueryParams = require('../middleware/allow-query-params');
const vectorError = require('../middleware/vector-error');
const DEFAULT_ZOOM_CENTER = {
zoom: 1,
center: {
lng: 0,
lat: 0
}
};
function numMapper(n) {
return +n;
}
function getRequestParams(locals) {
const params = Object.assign({}, locals);
delete params.template;
delete params.affectedTablesAndLastUpdate;
delete params.namedMapProvider;
delete params.allowedQueryParams;
return params;
}
function NamedMapsController(prepareContext, namedMapProviderCache, tileBackend, previewBackend,
surrogateKeysCache, tablesExtentApi, metadataBackend) {
this.namedMapProviderCache = namedMapProviderCache;
this.tileBackend = tileBackend;
this.previewBackend = previewBackend;
this.surrogateKeysCache = surrogateKeysCache;
this.tablesExtentApi = tablesExtentApi;
this.metadataBackend = metadataBackend;
this.prepareContext = prepareContext;
}
module.exports = NamedMapsController;
NamedMapsController.prototype.register = function(app) {
app.get(
app.base_url_templated + '/:template_id/:layer/:z/:x/:y.(:format)',
cors(),
userMiddleware,
this.prepareContext,
this.getNamedMapProvider('NAMED_MAP_TILE'),
this.getAffectedTables(),
this.getTile('NAMED_MAP_TILE'),
this.setSurrogateKey(),
this.setCacheChannelHeader(),
this.setLastModifiedHeader(),
this.setCacheControlHeader(),
this.setContentTypeHeader(),
this.respond(),
vectorError()
);
app.get(
app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format',
cors(),
userMiddleware,
allowQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
this.prepareContext,
this.getNamedMapProvider('STATIC_VIZ_MAP'),
this.getAffectedTables(),
this.getTemplate('STATIC_VIZ_MAP'),
this.prepareLayerFilterFromPreviewLayers('STATIC_VIZ_MAP'),
this.getStaticImageOptions(),
this.getImage('STATIC_VIZ_MAP'),
this.incrementMapViews(),
this.setSurrogateKey(),
this.setCacheChannelHeader(),
this.setLastModifiedHeader(),
this.setCacheControlHeader(),
this.setContentTypeHeader(),
this.respond()
);
};
NamedMapsController.prototype.getNamedMapProvider = function (label) {
return function getNamedMapProviderMiddleware (req, res, next) {
const { user } = res.locals;
const { config, auth_token } = req.query;
const { template_id } = req.params;
// We force always the tile to be generated using PNG because
// is the only format we support by now
res.locals.format = 'png';
res.locals.layer = res.locals.layer || 'all';
const params = getRequestParams(res.locals);
this.namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, namedMapProvider) => {
if (err) {
err.label = label;
return next(err);
}
res.locals.namedMapProvider = namedMapProvider;
next();
});
}.bind(this);
};
NamedMapsController.prototype.getAffectedTables = function () {
return function getAffectedTables (req, res, next) {
const { namedMapProvider } = res.locals;
namedMapProvider.getAffectedTablesAndLastUpdatedTime((err, affectedTablesAndLastUpdate) => {
req.profiler.done('affectedTables');
if (err) {
return next(err);
}
res.locals.affectedTablesAndLastUpdate = affectedTablesAndLastUpdate;
next();
});
}.bind(this);
};
NamedMapsController.prototype.getTemplate = function (label) {
return function getTemplateMiddleware (req, res, next) {
const { namedMapProvider } = res.locals;
namedMapProvider.getTemplate((err, template) => {
if (err) {
err.label = label;
return next(err);
}
res.locals.template = template;
next();
});
};
};
NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (label) {
return function prepareLayerFilterFromPreviewLayersMiddleware (req, res, next) {
const { user, template } = res.locals;
const { template_id } = req.params;
const { config, auth_token } = req.query;
if (!template || !template.view || !template.view.preview_layers) {
return next();
}
var previewLayers = template.view.preview_layers;
var layerVisibilityFilter = [];
template.layergroup.layers.forEach((layer, index) => {
if (previewLayers[''+index] !== false && previewLayers[layer.id] !== false) {
layerVisibilityFilter.push(''+index);
}
});
if (!layerVisibilityFilter.length) {
return next();
}
const params = getRequestParams(res.locals);
// overwrites 'all' default filter
params.layer = layerVisibilityFilter.join(',');
// recreates the provider
this.namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, provider) => {
if (err) {
err.label = label;
return next(err);
}
res.locals.namedMapProvider = provider;
next();
});
}.bind(this);
};
NamedMapsController.prototype.getTile = function (label) {
return function getTileMiddleware (req, res, next) {
const { namedMapProvider } = res.locals;
this.tileBackend.getTile(namedMapProvider, req.params, (err, tile, headers, stats) => {
req.profiler.add(stats);
if (err) {
err.label = label;
return next(err);
}
res.locals.body = tile;
res.locals.headers = headers;
res.locals.stats = stats;
next();
});
}.bind(this);
};
NamedMapsController.prototype.getStaticImageOptions = function () {
return function getStaticImageOptionsMiddleware(req, res, next) {
const { user, namedMapProvider, template } = res.locals;
const imageOpts = getImageOptions(res.locals, template);
if (imageOpts) {
res.locals.imageOpts = imageOpts;
return next();
}
res.locals.imageOpts = DEFAULT_ZOOM_CENTER;
namedMapProvider.getAffectedTablesAndLastUpdatedTime((err, affectedTablesAndLastUpdate) => {
if (err) {
return next();
}
var affectedTables = affectedTablesAndLastUpdate.tables || [];
if (affectedTables.length === 0) {
return next();
}
this.tablesExtentApi.getBounds(user, affectedTables, (err, bounds) => {
if (err) {
return next();
}
res.locals.imageOpts = bounds;
return next();
});
});
}.bind(this);
};
function getImageOptions (params, template) {
const { zoom, lon, lat, bbox } = params;
let imageOpts = getImageOptionsFromCoordinates(zoom, lon, lat);
if (imageOpts) {
return imageOpts;
}
imageOpts = getImageOptionsFromBoundingBox(bbox);
if (imageOpts) {
return imageOpts;
}
imageOpts = getImageOptionsFromTemplate(template, zoom);
if (imageOpts) {
return imageOpts;
}
}
function getImageOptionsFromCoordinates (zoom, lon, lat) {
if ([zoom, lon, lat].map(numMapper).every(Number.isFinite)) {
return {
zoom: zoom,
center: {
lng: lon,
lat: lat
}
};
}
}
function getImageOptionsFromTemplate (template, zoom) {
if (template.view) {
var zoomCenter = templateZoomCenter(template.view);
if (zoomCenter) {
if (Number.isFinite(+zoom)) {
zoomCenter.zoom = +zoom;
}
return zoomCenter;
}
var bounds = templateBounds(template.view);
if (bounds) {
return bounds;
}
}
}
function getImageOptionsFromBoundingBox (bbox = '') {
var _bbox = bbox.split(',').map(numMapper);
if (_bbox.length === 4 && _bbox.every(Number.isFinite)) {
return {
bounds: {
west: _bbox[0],
south: _bbox[1],
east: _bbox[2],
north: _bbox[3]
}
};
}
}
NamedMapsController.prototype.getImage = function (label) {
return function getImageMiddleware (req, res, next) {
const { imageOpts, namedMapProvider } = res.locals;
const { zoom, center, bounds } = imageOpts;
let { width, height } = req.params;
width = +width;
height = +height;
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
if (zoom !== undefined && center) {
return this.previewBackend.getImage(namedMapProvider, format, width, height, zoom, center,
(err, image, headers, stats) => {
if (err) {
err.label = label;
return next(err);
}
res.locals.body = image;
res.locals.headers = headers;
res.locals.stats = stats;
next();
});
}
this.previewBackend.getImage(namedMapProvider, format, width, height, bounds, (err, image, headers, stats) => {
if (err) {
err.label = label;
return next(err);
}
res.locals.body = image;
res.locals.headers = headers;
res.locals.stats = stats;
next();
});
}.bind(this);
};
function incrementMapViewsError (ctx) {
return `ERROR: failed to increment mapview count for user '${ctx.user}': ${ctx.err}`;
}
NamedMapsController.prototype.incrementMapViews = function () {
return function incrementMapViewsMiddleware(req, res, next) {
const { user, namedMapProvider } = res.locals;
namedMapProvider.getMapConfig((err, mapConfig) => {
if (err) {
global.logger.log(incrementMapViewsError({ user, err }));
return next();
}
const statTag = mapConfig.obj().stat_tag;
this.metadataBackend.incMapviewCount(user, statTag, (err) => {
if (err) {
global.logger.log(incrementMapViewsError({ user, err }));
}
next();
});
});
}.bind(this);
};
function templateZoomCenter(view) {
if (view.zoom !== undefined && view.center) {
return {
zoom: view.zoom,
center: view.center
};
}
return false;
}
function templateBounds(view) {
if (view.bounds) {
var hasAllBounds = ['west', 'south', 'east', 'north'].every(prop => Number.isFinite(view.bounds[prop]));
if (hasAllBounds) {
return {
bounds: {
west: view.bounds.west,
south: view.bounds.south,
east: view.bounds.east,
north: view.bounds.north
}
};
} else {
return false;
}
}
return false;
}
NamedMapsController.prototype.setCacheChannelHeader = function () {
return function setCacheChannelHeaderMiddleware (req, res, next) {
const { affectedTablesAndLastUpdate } = res.locals;
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
res.set('X-Cache-Channel', affectedTablesAndLastUpdate.getCacheChannel());
}
next();
};
};
NamedMapsController.prototype.setSurrogateKey = function () {
return function setSurrogateKeyMiddleware(req, res, next) {
const { user, namedMapProvider, affectedTablesAndLastUpdate } = res.locals;
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, namedMapProvider.getTemplateName()));
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
if (affectedTablesAndLastUpdate.tables.length > 0) {
this.surrogateKeysCache.tag(res, affectedTablesAndLastUpdate);
}
}
next();
}.bind(this);
};
NamedMapsController.prototype.setLastModifiedHeader = function () {
return function setLastModifiedHeaderMiddleware(req, res, next) {
const { affectedTablesAndLastUpdate } = res.locals;
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
var lastModifiedDate;
if (Number.isFinite(affectedTablesAndLastUpdate.lastUpdatedTime)) {
lastModifiedDate = new Date(affectedTablesAndLastUpdate.getLastUpdatedAt());
} else {
lastModifiedDate = new Date();
}
res.set('Last-Modified', lastModifiedDate.toUTCString());
}
next();
};
};
NamedMapsController.prototype.setCacheControlHeader = function () {
return function setCacheControlHeaderMiddleware(req, res, next) {
const { affectedTablesAndLastUpdate } = res.locals;
res.set('Cache-Control', 'public,max-age=7200,must-revalidate');
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
// we increase cache control as we can invalidate it
res.set('Cache-Control', 'public,max-age=31536000');
}
next();
};
};
NamedMapsController.prototype.setContentTypeHeader = function () {
return function setContentTypeHeaderMiddleware(req, res, next) {
const { headers = {} } = res.locals;
res.set('Content-Type', headers['content-type'] || headers['Content-Type'] || 'image/png');
next();
};
};
NamedMapsController.prototype.respond = function () {
return function respondMiddleware (req, res) {
const { body, stats = {}, format } = res.locals;
req.profiler.done('render-' + format);
req.profiler.add(stats);
res.status(200);
res.send(body);
};
};

View File

@@ -1,204 +0,0 @@
const { templateName } = require('../backends/template_maps');
const cors = require('../middleware/cors');
const userMiddleware = require('../middleware/user');
/**
* @param {AuthApi} authApi
* @param {PgConnection} pgConnection
* @param {TemplateMaps} templateMaps
* @constructor
*/
function NamedMapsAdminController(authApi, templateMaps) {
this.authApi = authApi;
this.templateMaps = templateMaps;
}
module.exports = NamedMapsAdminController;
NamedMapsAdminController.prototype.register = function (app) {
const { base_url_templated } = app;
app.post(
`${base_url_templated}/`,
cors(),
userMiddleware,
this.checkContentType('POST', 'POST TEMPLATE'),
this.authorizedByAPIKey('create', 'POST TEMPLATE'),
this.create()
);
app.put(
`${base_url_templated}/:template_id`,
cors(),
userMiddleware,
this.checkContentType('PUT', 'PUT TEMPLATE'),
this.authorizedByAPIKey('update', 'PUT TEMPLATE'),
this.update()
);
app.get(
`${base_url_templated}/:template_id`,
cors(),
userMiddleware,
this.authorizedByAPIKey('get', 'GET TEMPLATE'),
this.retrieve()
);
app.delete(
`${base_url_templated}/:template_id`,
cors(),
userMiddleware,
this.authorizedByAPIKey('delete', 'DELETE TEMPLATE'),
this.destroy()
);
app.get(
`${base_url_templated}/`,
cors(),
userMiddleware,
this.authorizedByAPIKey('list', 'GET TEMPLATE LIST'),
this.list()
);
app.options(
`${base_url_templated}/:template_id`,
cors('Content-Type')
);
};
NamedMapsAdminController.prototype.authorizedByAPIKey = function (action, label) {
return function authorizedByAPIKeyMiddleware (req, res, next) {
const { user } = res.locals;
this.authApi.authorizedByAPIKey(user, req, (err, authenticated) => {
if (err) {
return next(err);
}
if (!authenticated) {
const error = new Error(`Only authenticated user can ${action} templated maps`);
error.http_status = 403;
error.label = label;
return next(error);
}
next();
});
}.bind(this);
};
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.create = function () {
return function createTemplateMiddleware (req, res, next) {
const { user } = res.locals;
const template = req.body;
this.templateMaps.addTemplate(user, template, (err, templateId) => {
if (err) {
return next(err);
}
res.status(200);
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 template.auth_id;
res.status(200);
const method = req.query.callback ? 'jsonp' : 'json';
res[method]({ template });
});
}.bind(this);
};
NamedMapsAdminController.prototype.destroy = function () {
return function destroyTemplateMiddleware (req, res, next) {
req.profiler.start('windshaft-cartodb.delete_template');
const { user } = res.locals;
const templateId = templateName(req.params.template_id);
this.templateMaps.delTemplate(user, templateId, (err/* , tpl_val */) => {
if (err) {
return next(err);
}
res.status(204);
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

@@ -1,9 +0,0 @@
module.exports = function allowQueryParams(params) {
if (!Array.isArray(params)) {
throw new Error('allowQueryParams must receive an Array of params');
}
return function allowQueryParamsMiddleware(req, res, next) {
res.locals.allowedQueryParams = params;
next();
};
};

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
module.exports = function localsMiddleware(req, res, next) {
// save req.params in res.locals
res.locals = Object.assign(req.params || {}, res.locals);
next();
};

View File

@@ -1,11 +0,0 @@
module.exports = function cors(extraHeaders) {
return function(req, res, next) {
var baseHeaders = "X-Requested-With, X-Prototype-Version, X-CSRF-Token";
if(extraHeaders) {
baseHeaders += ", " + extraHeaders;
}
res.set("Access-Control-Allow-Origin", "*");
res.set("Access-Control-Allow-Headers", baseHeaders);
next();
};
};

View File

@@ -1,30 +0,0 @@
'use strict';
const LZMA = require('lzma').LZMA;
const lzmaWorker = new LZMA();
module.exports = function lzmaMiddleware(req, res, next) {
if (!req.query.hasOwnProperty('lzma')) {
return next();
}
// Decode (from base64)
var lzma = new Buffer(req.query.lzma, 'base64')
.toString('binary')
.split('')
.map(function(c) {
return c.charCodeAt(0) - 128;
});
// Decompress
lzmaWorker.decompress(lzma, function(result) {
try {
delete req.query.lzma;
Object.assign(req.query, JSON.parse(result));
next();
} catch (err) {
next(new Error('Error parsing lzma as JSON: ' + err));
}
});
};

View File

@@ -1,8 +0,0 @@
var CdbRequest = require('../models/cdb_request');
var cdbRequest = new CdbRequest();
module.exports = function userMiddleware(req, res, next) {
res.locals.user = cdbRequest.userByReq(req);
next();
};

View File

@@ -4,22 +4,14 @@ const aggregationValidator = require('./aggregation-validator');
const {
createPositiveNumberValidator,
createIncludesValueValidator,
createAggregationColumnsValidator
createAggregationColumnsValidator,
createAggregationFiltersValidator
} = aggregationValidator;
const SubstitutionTokens = require('../../utils/substitution-tokens');
const queryUtils = require('../../utils/query-utils');
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;
@@ -43,6 +35,18 @@ module.exports = class AggregationMapConfig extends MapConfig {
];
}
static get FILTER_PARAMETERS () {
return [
// TODO: valid combinations of parameters:
// * Except for less/greater params, only one parameter allowed per filter.
// * Any less parameter can be combined with one of the greater paramters. (to define a range)
'less_than', 'less_than_or_equal_to',
'greater_than', 'greater_than_or_equal_to',
'equal', 'not_equal',
'between', 'in', 'not_in'
];
}
static supportsGeometryType(geometryType) {
return AggregationMapConfig.SUPPORTED_GEOMETRY_TYPES.includes(geometryType);
}
@@ -58,11 +62,15 @@ module.exports = class AggregationMapConfig extends MapConfig {
const positiveNumberValidator = createPositiveNumberValidator(this);
const includesValidPlacementsValidator = createIncludesValueValidator(this, AggregationMapConfig.PLACEMENTS);
const aggregationColumnsValidator = createAggregationColumnsValidator(this, AggregationMapConfig.AGGREGATIONS);
const aggregationFiltersValidator = createAggregationFiltersValidator(
this, AggregationMapConfig.FILTER_PARAMETERS
);
validate('resolution', positiveNumberValidator);
validate('placement', includesValidPlacementsValidator);
validate('threshold', positiveNumberValidator);
validate('columns', aggregationColumnsValidator);
validate('filters', aggregationFiltersValidator);
this.user = user;
this.pgConnection = connection;
@@ -77,7 +85,8 @@ module.exports = class AggregationMapConfig extends MapConfig {
threshold = AggregationMapConfig.THRESHOLD,
placement,
columns = {},
dimensions = {}
dimensions = {},
filters = {}
} = this.getAggregation(index);
return aggregationQuery({
@@ -87,6 +96,7 @@ module.exports = class AggregationMapConfig extends MapConfig {
placement,
columns,
dimensions,
filters,
isDefaultAggregation: this._isDefaultLayerAggregation(index)
});
}
@@ -152,21 +162,19 @@ module.exports = class AggregationMapConfig extends MapConfig {
_getLayerAggregationRequiredColumns (index) {
const { columns, dimensions } = this.getAggregation(index);
let finalColumns = ['cartodb_id', '_cdb_feature_count'];
let aggregatedColumns = [];
if (columns) {
aggregatedColumns = Object.keys(columns)
.map(key => columns[key].aggregated_column)
.filter(aggregatedColumn => typeof aggregatedColumn === 'string');
aggregatedColumns = Object.keys(columns);
}
let dimensionsColumns = [];
if (dimensions) {
dimensionsColumns = Object.keys(dimensions)
.map(key => dimensions[key])
.filter(dimension => typeof dimension === 'string');
dimensionsColumns = Object.keys(dimensions);
}
return removeDuplicates(aggregatedColumns.concat(dimensionsColumns));
return removeDuplicates(finalColumns.concat(aggregatedColumns).concat(dimensionsColumns));
}
doesLayerReachThreshold(index, featureCount) {
@@ -188,7 +196,7 @@ module.exports = class AggregationMapConfig extends MapConfig {
}
const sql = limitedQuery({
query: prepareSql(layer.options.sql)
query: queryUtils.substituteDummyTokens(layer.options.sql)
});
connection.query(sql, (err, result) => {
@@ -220,7 +228,8 @@ module.exports = class AggregationMapConfig extends MapConfig {
_isDefaultAggregation (aggregation) {
return aggregation.placement === undefined &&
aggregation.columns === undefined &&
this._isEmptyParameter(aggregation.dimensions);
this._isEmptyParameter(aggregation.dimensions) &&
this._isEmptyParameter(aggregation.filters);
}
_isEmptyParameter(parameter) {

View File

@@ -42,7 +42,8 @@ const queryForOptions = (options) => templateForOptions(options)({
sourceQuery: options.query,
res: 256/options.resolution,
columns: options.columns,
dimensions: options.dimensions
dimensions: options.dimensions,
filters: options.filters
});
module.exports = queryForOptions;
@@ -93,20 +94,23 @@ const aggregateColumnNames = (ctx, table) => {
return sep(Object.keys(columns));
};
const aggregateExpression = (column_name, column_parameters) => {
const aggregate_function = column_parameters.aggregate_function || 'count';
const aggregate_definition = SUPPORTED_AGGREGATE_FUNCTIONS[aggregate_function];
if (!aggregate_definition) {
throw new Error("Invalid Aggregate function: '" + aggregate_function + "'");
}
return aggregate_definition.sql(column_name, column_parameters);
};
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]);
const aggregate_expression = aggregateExpression(column_name, columns[column_name]);
return `${aggregate_expression} AS ${column_name}`;
}));
};
const aggregateDimensions = ctx => ctx.dimensions || {};
const dimensionNames = (ctx, table) => {
@@ -127,34 +131,181 @@ const dimensionDefs = ctx => {
}));
};
const aggregateFilters = ctx => ctx.filters || {};
const filterConditionSQL = (expr, filter) => {
// TODO: validate filter parameters (e.g. cannot have both greater_than and greater_than or equal to)
if (filter) {
if (!Array.isArray(filter)) {
filter = [filter];
}
if (filter.length > 0) {
return filter.map(f => filterSingleConditionSQL(expr, f)).join(' OR ');
}
}
};
const filterSingleConditionSQL = (expr, filter) => {
let cond;
Object.keys(FILTERS).some(f => {
cond = FILTERS[f](expr, filter);
return cond;
});
return cond;
};
const sqlQ = (value) => {
if (isFinite(value)) {
return String(value);
}
return `'${value}'`; // TODO: escape single quotes! (by doubling them)
};
/* jshint eqeqeq: false */
/* x != null is used to check for both null and undefined; triple !== wouldn't do the trick */
const FILTERS = {
between: (expr, filter) => {
const lo = filter.greater_than_or_equal_to, hi = filter.less_than_or_equal_to;
if (lo != null && hi != null) {
return `(${expr} BETWEEN ${sqlQ(lo)} AND ${sqlQ(hi)})`;
}
},
in: (expr, filter) => {
if (filter.in != null) {
return `(${expr} IN (${filter.in.map(v => sqlQ(v)).join(',')}))`;
}
},
notin: (expr, filter) => {
if (filter.not_in != null) {
return `(${expr} NOT IN (${filter.not_in.map(v => sqlQ(v)).join(',')}))`;
}
},
equal: (expr, filter) => {
if (filter.equal != null) {
return `(${expr} = ${sqlQ(filter.equal)})`;
}
},
not_equal: (expr, filter) => {
if (filter.not_equal != null) {
return `(${expr} <> ${sqlQ(filter.not_equal)})`;
}
},
range: (expr, filter) => {
let conds = [];
if (filter.greater_than_or_equal_to != null) {
conds.push(`(${expr} >= ${sqlQ(filter.greater_than_or_equal_to)})`);
}
if (filter.greater_than != null) {
conds.push(`(${expr} > ${sqlQ(filter.greater_than)})`);
}
if (filter.less_than_or_equal_to != null) {
conds.push(`(${expr} <= ${sqlQ(filter.less_than_or_equal_to)})`);
}
if (filter.less_than != null) {
conds.push(`(${expr} < ${sqlQ(filter.less_than)})`);
}
if (conds.length > 0) {
return conds.join(' AND ');
}
}
};
const filterConditions = ctx => {
let columns = aggregateColumns(ctx);
let dimensions = aggregateDimensions(ctx);
let filters = aggregateFilters(ctx);
return Object.keys(filters).map(filtered_column => {
let filtered_expr;
if (columns[filtered_column]) {
filtered_expr = aggregateExpression(filtered_column, columns[filtered_column]);
}
else if (dimensions[filtered_column]) {
filtered_expr = dimensions[filtered_column];
}
if (!filtered_expr) {
throw new Error("Invalid filtered column: '" + filtered_column + "'");
}
return filterConditionSQL(filtered_expr, filters[filtered_column]);
}).join(' AND ');
};
const havingClause = ctx => {
let cond = filterConditions(ctx);
return cond ? `HAVING ${cond}` : '';
};
// 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`;
// We limit the the minimum resolution to avoid division by zero problems. The limit used is
// the pixel size of zoom level 30 (i.e. 1/2*(30+8) of the full earth web-mercator extent), which is about 0.15 mm.
// Computing this using !scale_denominator!, !pixel_width! or !pixel_height! produces
// inaccurate results due to rounding present in those values.
const gridResolution = ctx => {
const minimumResolution = 2*Math.PI*6378137/Math.pow(2,38);
const pixelSize = 'CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!))';
return `GREATEST(${256/ctx.res}*${pixelSize}, ${minimumResolution})::double precision`;
};
// Each aggregation cell is defined by the cell coordinates Floor(x/res), Floor(y/res),
// i.e. they include the West and South borders but not the East and North ones.
// So, to avoid picking points that don't belong to cells in the tile, given the tile
// limits Xmin, Ymin, Xmax, Ymax (bbox), we should select points that satisfy
// Xmin <= x < Xmax and Ymin <= y < Ymax (with x, y from the_geom_webmercator)
// On the other hand we can efficiently filter spatially (relying on spatial indexing)
// with `the_geom_webmercator && bbox` which is equivalent to
// Xmin <= x <= Xmax and Ymin <= y <= Ymax
// So, in order to be both efficient and accurate we will need to use both
// conditions for spatial filtering.
const spatialFilter = `
(_cdb_query.the_geom_webmercator && _cdb_params.bbox) AND
ST_X(_cdb_query.the_geom_webmercator) >= _cdb_params.xmin AND
ST_X(_cdb_query.the_geom_webmercator) < _cdb_params.xmax AND
ST_Y(_cdb_query.the_geom_webmercator) >= _cdb_params.ymin AND
ST_Y(_cdb_query.the_geom_webmercator) < _cdb_params.ymax
`;
// 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
// * If the MVT extent or tile buffer was 0 or a multiple of the resolution we could use directly
// the bbox for them, but in general we need to find the nearest cell limits inside the bbox.
// * bbox coordinates can have an error in the last digites; we apply a small correction before
// applying CEIL or FLOOR to compensate for this.
const sqlParams = (ctx) => `
_cdb_res AS (
SELECT
${gridResolution(ctx)} AS res,
!bbox! AS bbox,
(2*2.220446049250313e-16::double precision) AS eps
),
_cdb_params AS (
SELECT
res,
bbox,
CEIL((ST_XMIN(bbox) - eps*ABS(ST_XMIN(bbox)))/res)*res AS xmin,
FLOOR((ST_XMAX(bbox) + eps*ABS(ST_XMAX(bbox)))/res)*res AS xmax,
CEIL((ST_YMIN(bbox) - eps*ABS(ST_YMIN(bbox)))/res)*res AS ymin,
FLOOR((ST_YMAX(bbox) + eps*ABS(ST_YMAX(bbox)))/res)*res AS ymax
FROM _cdb_res
)
`;
// 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
),
WITH ${sqlParams(ctx)},
_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
WHERE ${spatialFilter}
GROUP BY
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
@@ -169,14 +320,9 @@ const defaultAggregationQueryTemplate = ctx => `
const aggregationQueryTemplates = {
'centroid': ctx => `
WITH
_cdb_params AS (
SELECT
${gridResolution(ctx)} AS res,
!bbox! AS bbox
)
WITH ${sqlParams(ctx)}
SELECT
row_number() over() AS cartodb_id,
MIN(_cdb_query.cartodb_id) AS cartodb_id,
ST_SetSRID(
ST_MakePoint(
AVG(ST_X(_cdb_query.the_geom_webmercator)),
@@ -186,32 +332,30 @@ const aggregationQueryTemplates = {
${dimensionDefs(ctx)}
${aggregateColumnDefs(ctx)}
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
WHERE ${spatialFilter}
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)}
${havingClause(ctx)}
`,
'point-grid': ctx => `
WITH
_cdb_params AS (
SELECT
${gridResolution(ctx)} AS res,
!bbox! AS bbox
),
WITH ${sqlParams(ctx)},
_cdb_clusters AS (
SELECT
MIN(_cdb_query.cartodb_id) AS cartodb_id,
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
WHERE ${spatialFilter}
GROUP BY _cdb_gx, _cdb_gy ${dimensionNames(ctx)}
${havingClause(ctx)}
)
SELECT
row_number() over() AS cartodb_id,
_cdb_clusters.cartodb_id AS cartodb_id,
ST_SetSRID(ST_MakePoint((_cdb_gx+0.5)*res, (_cdb_gy+0.5)*res), 3857) AS the_geom_webmercator
${dimensionNames(ctx)}
${aggregateColumnNames(ctx)}
@@ -219,28 +363,24 @@ const aggregationQueryTemplates = {
`,
'point-sample': ctx => `
WITH
_cdb_params AS (
SELECT
${gridResolution(ctx)} AS res,
!bbox! AS bbox
),
WITH ${sqlParams(ctx)},
_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
WHERE ${spatialFilter}
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)}
${havingClause(ctx)}
)
SELECT
_cdb_clusters.cartodb_id,
the_geom, the_geom_webmercator
${dimensionNames(ctx, '_cdb_query')}
${dimensionNames(ctx, '_cdb_clusters')}
${aggregateColumnNames(ctx, '_cdb_clusters')}
FROM
_cdb_clusters INNER JOIN (${ctx.sourceQuery}) _cdb_query

View File

@@ -42,6 +42,38 @@ module.exports.createAggregationColumnsValidator = function (mapconfig, validAgg
};
};
module.exports.createAggregationFiltersValidator = function (mapconfig, validParameters) {
return function validateAggregationFilters (value, key, index) {
const dims = mapconfig.getAggregation(index).dimensions || {};
const cols = mapconfig.getAggregation(index).columns || {};
const validKeys = Object.keys(dims).concat(Object.keys(cols));
Object.keys(value).forEach((filteredName) => {
// filteredName must be the name of either an aggregated column or a dimension in the same layer
if (!validKeys.includes(filteredName)) {
const message = `Invalid filtered column: ${filteredName}`;
throw createLayerError(message, mapconfig, index);
}
// The filter parameters must be valid
let filters = value[filteredName];
// a single filter or an array of filters (to be OR-combined) are accepted
if (!Array.isArray(filters)) {
filters = [filters];
}
filters.forEach(params => {
Object.keys(params).forEach(paramName => {
if (!validParameters.includes(paramName)) {
const message = `Invalid filter parameter name: ${paramName}`;
throw createLayerError(message, mapconfig, index);
}
});
// TODO: check parameter value (params[paramName]) to be of the correct type
});
// TODO: if multiple parameters within params check the combination is valid,
// i.e. one of the *less* parameters and one of the *greater* parameters.
});
};
};
function createAggregationColumnNamesValidator(mapconfig) {
return function validateAggregationColumnNames (value, key, index) {
Object.keys(value).forEach((columnName) => {

View File

@@ -2,19 +2,17 @@ const BaseDataview = require('./base');
const debug = require('debug')('windshaft:dataview:aggregation');
const filteredQueryTpl = ctx => `
filtered_source AS (
SELECT *
FROM (${ctx.query}) _cdb_filtered_source
${ctx.aggregationColumn && ctx.isFloatColumn ? `
WHERE
${ctx.aggregationColumn} != 'infinity'::float
AND
${ctx.aggregationColumn} != '-infinity'::float
AND
${ctx.aggregationColumn} != 'NaN'::float` :
''
}
)
SELECT *
FROM (${ctx.query}) _cdb_filtered_source
${ctx.aggregationColumn && ctx.isFloatColumn ? `
WHERE
${ctx.aggregationColumn} != 'infinity'::float
AND
${ctx.aggregationColumn} != '-infinity'::float
AND
${ctx.aggregationColumn} != 'NaN'::float` :
''
}
`;
const summaryQueryTpl = ctx => `
@@ -43,8 +41,8 @@ const rankedCategoriesQueryTpl = ctx => `
${ctx.column} AS category,
${ctx.aggregationFn} AS value,
row_number() OVER (ORDER BY ${ctx.aggregationFn} desc) as rank
FROM filtered_source
${ctx.aggregationColumn !== null ? `WHERE ${ctx.aggregationColumn} IS NOT NULL` : ''}
FROM (${filteredQueryTpl(ctx)}) filtered_source
WHERE ${ctx.aggregation === "count" ? `${ctx.column}` : `${ctx.aggregationColumn}`} IS NOT NULL
GROUP BY ${ctx.column}
ORDER BY 2 DESC
)
@@ -134,7 +132,6 @@ const aggregationFnQueryTpl = ctx => `${ctx.aggregation}(${ctx.aggregationColumn
const aggregationDataviewQueryTpl = ctx => `
WITH
${filteredQueryTpl(ctx)},
${summaryQueryTpl(ctx)},
${rankedCategoriesQueryTpl(ctx)},
${categoriesSummaryMinMaxQueryTpl(ctx)},
@@ -282,7 +279,7 @@ module.exports = class Aggregation extends BaseDataview {
max_val = 0,
categories_count = 0
} = result.rows[0] || {};
return {
aggregation: this.aggregation,
count: count,
@@ -293,10 +290,10 @@ module.exports = class Aggregation extends BaseDataview {
max: max_val,
categoriesCount: categories_count,
categories: result.rows.map(({ category, value, agg }) => {
return {
return {
category: agg ? 'Other' : category,
value,
agg
value,
agg
};
})
};

View File

@@ -52,6 +52,11 @@ module.exports = class BaseDataview {
if (err) {
return callback(err);
}
if (!result || !result.rows || !result.rows.length) {
return callback(new Error('The column type could not be determined'));
}
const pgType = result.rows[0].pg_typeof;
callback(null, getPGTypeName(pgType));
}, readOnlyTransaction);

View File

@@ -2,6 +2,7 @@ const BaseHistogram = require('./base-histogram');
const debug = require('debug')('windshaft:dataview:date-histogram');
const utils = require('../../../utils/query-utils');
/**
* Gets the name of a timezone with the same offset as the required
* using the pg_timezone_names table. We do this because it's simpler to pass

View File

@@ -99,8 +99,14 @@ module.exports = class NumericHistogram extends BaseHistogram {
var extra_tables = ``;
var extra_queries = ``;
var extra_groupby = ``;
var extra_filter = ``;
if (ctx.start >= ctx.end) {
if (ctx.start < ctx.end) {
extra_filter = `
WHERE __ctx_query.${ctx.column} >= ${ctx.start}
AND __ctx_query.${ctx.column} <= ${ctx.end}
`;
} else {
ctx.end = `__cdb_basics.__cdb_max_val`;
ctx.start = `__cdb_basics.__cdb_min_val`;
extra_groupby = `, __cdb_basics.__cdb_max_val, __cdb_basics.__cdb_min_val`;
@@ -135,7 +141,7 @@ SELECT
END AS bin
FROM
(
${ctx.query}
SELECT * FROM (${ctx.query}) __ctx_query${extra_tables} ${extra_filter}
) __cdb_filtered_source_query${extra_tables}
GROUP BY 10${extra_groupby}
ORDER BY 10;`;

View File

@@ -101,6 +101,12 @@ module.exports = class AggregationMapConfigAdapter {
aggregationSql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, aggregationSql);
}
if (!layer.options.sql_raw) {
// if sql_wrap is present, the original query should already be
// in sql_raw (with sql being the wrapped query);
// otherwise we keep the now the original query in sql_raw
layer.options.sql_raw = layer.options.sql;
}
layer.options.sql = aggregationSql;
mapConfig.getLayerAggregationColumns(index, (err, columns) => {
@@ -159,6 +165,7 @@ module.exports = class AggregationMapConfigAdapter {
}
_getAggregationMetadata (mapConfig, layer, adapted) {
// also: pre-aggr query, columns, ...
if (!adapted) {
return { png: false, mvt: false };
}

View File

@@ -108,8 +108,7 @@ MapConfigNamedLayersAdapter.prototype.getMapConfig = function (user, requestMapC
var dbAuth = {};
if (_.some(layers, isNamedTypeLayer)) {
// Lazy load dbAuth
this.pgConnection.setDBAuth(user, dbAuth, function(err) {
this.pgConnection.setDBAuth(user, dbAuth, 'master', function(err) {
if (err) {
return callback(err);
}

View File

@@ -2,9 +2,9 @@ var step = require('step');
var queue = require('queue-async');
var _ = require('underscore');
function MapConfigOverviewsAdapter(overviewsMetadataApi, filterStatsApi) {
this.overviewsMetadataApi = overviewsMetadataApi;
this.filterStatsApi = filterStatsApi;
function MapConfigOverviewsAdapter(overviewsMetadataBackend, filterStatsBackend) {
this.overviewsMetadataBackend = overviewsMetadataBackend;
this.filterStatsBackend = filterStatsBackend;
}
module.exports = MapConfigOverviewsAdapter;
@@ -25,7 +25,7 @@ MapConfigOverviewsAdapter.prototype.getMapConfig = function(user, requestMapConf
if ( layer.type !== 'mapnik' && layer.type !== 'cartodb' ) {
return done(null, layer);
}
self.overviewsMetadataApi.getOverviewsMetadata(user, layer.options.sql, function(err, metadata){
self.overviewsMetadataBackend.getOverviewsMetadata(user, layer.options.sql, function(err, metadata){
if (err) {
done(err, layer);
} else {
@@ -53,7 +53,7 @@ MapConfigOverviewsAdapter.prototype.getMapConfig = function(user, requestMapConf
function collectStatsData(err, filters, unfiltered_query) {
var next_step = this;
if ( filters ) {
self.filterStatsApi.getFilterStats(
self.filterStatsBackend.getFilterStats(
user,
unfiltered_query, filters,
function(err, stats) {

View File

@@ -11,6 +11,8 @@ var PostgresDatasource = require('../../../backends/turbo-carto-postgres-datasou
var MapConfig = require('windshaft').model.MapConfig;
const dbParamsFromReqParams = require('../../../utils/database-params');
function TurboCartoAdapter() {
}
@@ -115,7 +117,9 @@ TurboCartoAdapter.prototype._parseCartoCss = function (username, params, layer,
var layerSql = layer.options.sql;
var layerRawSql = layer.options.sql_raw;
if (SubstitutionTokens.hasTokens(layerSql) && layerRawSql) {
if (SubstitutionTokens.hasTokens(layerSql) && layerRawSql && layer.options.sql_wrap) {
// For wrapped queries we'll derive the tokens from the data extent
// instead of the whole Earth/root tile.
var self = this;
var tokensQuery = tokensQueryTpl({_sql: layerRawSql});
return pg.query(tokensQuery, function(err, resultSet) {
@@ -158,23 +162,3 @@ TurboCartoAdapter.prototype.process = function (psql, cartocss, sql, callback) {
function shouldParseLayerCartocss(layer) {
return layer && layer.options && layer.options.cartocss && layer.options.sql;
}
function dbParamsFromReqParams(params) {
var dbParams = {};
if ( params.dbuser ) {
dbParams.user = params.dbuser;
}
if ( params.dbpassword ) {
dbParams.pass = params.dbpassword;
}
if ( params.dbhost ) {
dbParams.host = params.dbhost;
}
if ( params.dbport ) {
dbParams.port = params.dbport;
}
if ( params.dbname ) {
dbParams.dbname = params.dbname;
}
return dbParams;
}

View File

@@ -0,0 +1,63 @@
const queryUtils = require('../../../utils/query-utils');
const dateWrapper = require('../../../utils/date-wrapper');
/**
* This middleware wraps the layer query transforming the date fields into numbers because mvt tiles
* doesnt support dates as primitive type.
*
* - This middleware is ONLY activated when the `dates_as_numbers` option is enabled for some layer in the mapConfig.
*/
class VectorMapConfigAdapter {
constructor(pgConnection) {
this.pgConnection = pgConnection;
}
getMapConfig(user, requestMapConfig, params, context, callback) {
if (!this._isDatesAsNumbersFlagEnabled(requestMapConfig)) {
return callback(null, requestMapConfig);
}
this._wrapDates(requestMapConfig, user)
.then(updatedRequestMapConfig => callback(null, updatedRequestMapConfig))
.catch(callback);
}
_wrapDates(requestMapConfig, user) {
return Promise.all(requestMapConfig.layers.map(layer => this._wrapLayer(layer, user)))
.then(() => requestMapConfig);
}
_wrapLayer(layer, user) {
if (!layer.options.dates_as_numbers || !layer.options.sql) {
return Promise.resolve(layer);
}
const originalQuery = layer.options.sql;
return this._getColumns(user, originalQuery)
.then(result => {
const newSqlQuery = dateWrapper.wrapDates(originalQuery, result.fields);
layer.options.sql = newSqlQuery;
return layer;
});
}
_getColumns(user, originalQuery) {
return new Promise((resolve, reject) => {
this.pgConnection.getConnection(user, (err, connection) => {
if (err) {
return reject(err);
}
const query = queryUtils.getQueryLimited(queryUtils.substituteDummyTokens(originalQuery), 0);
queryUtils.queryPromise(connection, query)
.then(resolve)
.catch(reject);
});
});
}
_isDatesAsNumbersFlagEnabled(requestMapConfig) {
return requestMapConfig.layers && requestMapConfig.layers.some(layer => layer.options.dates_as_numbers);
}
}
module.exports = VectorMapConfigAdapter;

View File

@@ -0,0 +1,65 @@
const QueryTables = require('cartodb-query-tables');
module.exports = class BaseMapConfigProvider {
createAffectedTables (callback) {
this.getMapConfig((err, mapConfig) => {
if (err) {
return callback(err);
}
const { dbname } = this.params;
const token = mapConfig.id();
const queries = [];
this.mapConfig.getLayers().forEach(layer => {
queries.push(layer.options.sql);
if (layer.options.affected_tables) {
layer.options.affected_tables.map(table => {
queries.push(`SELECT * FROM ${table} LIMIT 0`);
});
}
});
const sql = queries.length ? queries.join(';') : null;
if (!sql) {
return callback();
}
this.pgConnection.getConnection(this.user, (err, connection) => {
if (err) {
return callback(err);
}
QueryTables.getAffectedTablesFromQuery(connection, sql, (err, affectedTables) => {
if (err) {
return callback(err);
}
this.affectedTablesCache.set(dbname, token, affectedTables);
callback(null, affectedTables);
});
});
});
}
getAffectedTables (callback) {
this.getMapConfig((err, mapConfig) => {
if (err) {
return callback(err);
}
const { dbname } = this.params;
const token = mapConfig.id();
if (this.affectedTablesCache.hasAffectedTables(dbname, token)) {
const affectedTables = this.affectedTablesCache.get(dbname, token);
return callback(null, affectedTables);
}
return this.createAffectedTables(callback);
});
}
};

View File

@@ -1,48 +1,27 @@
var assert = require('assert');
var step = require('step');
const MapStoreMapConfigProvider = require('./map-store-provider');
var MapStoreMapConfigProvider = require('./map-store-provider');
module.exports = class CreateLayergroupMapConfigProvider extends MapStoreMapConfigProvider {
constructor (mapConfig, user, userLimitsBackend, pgConnection, affectedTablesCache, params) {
super(null, user, userLimitsBackend, pgConnection, affectedTablesCache, params);
this.mapConfig = mapConfig;
}
/**
* @param {MapConfig} mapConfig
* @param {String} user
* @param {UserLimitsApi} userLimitsApi
* @param {Object} params
* @constructor
* @type {CreateLayergroupMapConfigProvider}
*/
function CreateLayergroupMapConfigProvider(mapConfig, user, userLimitsApi, params) {
this.mapConfig = mapConfig;
this.user = user;
this.userLimitsApi = userLimitsApi;
this.params = params;
this.cacheBuster = params.cache_buster || 0;
}
module.exports = CreateLayergroupMapConfigProvider;
CreateLayergroupMapConfigProvider.prototype.getMapConfig = function(callback) {
var self = this;
var context = {};
step(
function prepareContextLimits() {
self.userLimitsApi.getRenderLimits(self.user, self.params.api_key, this);
},
function handleRenderLimits(err, renderLimits) {
assert.ifError(err);
context.limits = renderLimits;
return null;
},
function finish(err) {
return callback(err, self.mapConfig, self.params, context);
getMapConfig (callback) {
if (this.mapConfig && this.params && this.context) {
return callback(null, this.mapConfig, this.params, this.context);
}
);
const context = {};
this.userLimitsBackend.getRenderLimits(this.user, this.params.api_key, (err, renderLimits) => {
if (err) {
return callback(err);
}
context.limits = renderLimits;
this.context = context;
return callback(null, this.mapConfig, this.params, context);
});
}
};
CreateLayergroupMapConfigProvider.prototype.getKey = MapStoreMapConfigProvider.prototype.getKey;
CreateLayergroupMapConfigProvider.prototype.getCacheBuster = MapStoreMapConfigProvider.prototype.getCacheBuster;
CreateLayergroupMapConfigProvider.prototype.filter = MapStoreMapConfigProvider.prototype.filter;
CreateLayergroupMapConfigProvider.prototype.createKey = MapStoreMapConfigProvider.prototype.createKey;

Some files were not shown because too many files have changed in this diff Show More