Compare commits

...

464 Commits

Author SHA1 Message Date
Raul Ochoa
f136b9f6e7 Release 2.15.1 2015-10-21 13:56:53 +02:00
Raul Ochoa
12175f10a6 Upgrades windshaft to 1.1.1 2015-10-21 13:55:36 +02:00
Raul Ochoa
01bd09040e Stubs next version 2015-10-13 12:44:29 +02:00
Raul Ochoa
ab38e7069a Release 2.15.0 2015-10-13 12:36:25 +02:00
Raul Ochoa
9449642773 Remove soft-purge option when purging fastly 2015-10-09 16:37:17 +02:00
Raul Ochoa
1f489a4537 Fix broken test due fastly-purge upgrade 2015-10-09 16:02:11 +02:00
Raul Ochoa
6effcfb62e Upgrades windshaft to 1.1.0 and fastly-purge to 1.0.1 2015-10-09 15:50:39 +02:00
Raul Ochoa
2cc4cc5deb Stubs next version 2015-09-30 19:26:33 +02:00
Raul Ochoa
4c5630ac43 Release 2.14.1 2015-09-30 18:52:08 +02:00
Raul Ochoa
3181bcc63e Remove app dependency from controllers 2015-09-30 18:00:54 +02:00
Raul Ochoa
bf9cb33d63 Fix tests after upgrading windshaft, which includes a fix for layergroupid 2015-09-30 17:55:17 +02:00
Raul Ochoa
e7aa6bbdf9 Upgrade windshaft 2015-09-30 17:44:40 +02:00
Raul Ochoa
550b73ce23 Update news 2015-09-30 17:41:37 +02:00
Raul Ochoa
ffaa756637 Merge pull request #337 from CartoDB/safe-user-extraction
Safe user extraction
2015-09-30 17:25:12 +02:00
Raul Ochoa
9cd67f06c1 User extraction from request middleware
Used only where potentially a user is required.

It doesn't make sense to extract a user for request that don't need
a user in the context.
2015-09-30 17:17:01 +02:00
Raul Ochoa
79375616d5 Default host to empty string when header is not found
A String object is required to not fail on `.match` interface.
2015-09-30 16:31:56 +02:00
Raul Ochoa
ba6b8fbff1 Stubs next version 2015-09-30 11:28:38 +02:00
Raul Ochoa
6c37901824 Release 2.14.0 2015-09-30 11:26:03 +02:00
Raul Ochoa
0cf8661432 Be more specific about windshaft version upgrade 2015-09-30 11:25:07 +02:00
Raul Ochoa
ff561157ac Use Windshaft from registry 2015-09-30 11:21:25 +02:00
Raul Ochoa
a7cf9fd74f Regenerate npm-shrinkwrap.json 2015-09-30 11:04:40 +02:00
Raul Ochoa
788d24a455 Merge pull request #335 from CartoDB/standalone-server
Standalone server using Windshaft as library
2015-09-30 01:13:16 +02:00
Raul Ochoa
f9e3870941 Update news with all changes related to use Windshaft as library 2015-09-30 00:59:42 +02:00
Raul Ochoa
3a92fdd0f3 Remove sql api related configuration from configure script 2015-09-29 20:00:39 +02:00
Raul Ochoa
6a38defac5 Use Windshaft master branch 2015-09-29 19:42:13 +02:00
Raul Ochoa
3dfa7a8427 Name functions 2015-09-29 19:39:08 +02:00
Raul Ochoa
8dca835a8e Regenerate npm-shrinkwrap.json 2015-09-29 12:58:48 +02:00
Raul Ochoa
f2278d47a5 Increment map views on static preview images 2015-09-29 12:21:11 +02:00
Raul Ochoa
de3f195187 Merge pull request #333 from CartoDB/remove-outdated-docs
Remove outdated docs
2015-09-28 22:15:13 +02:00
Raul Ochoa
f33c3ce21a rmdirRecursiveSync DRY 2015-09-25 19:56:28 +02:00
Raul Ochoa
67ba424a19 Check style 2015-09-25 19:47:09 +02:00
Raul Ochoa
3376a08eb8 Remove unneeded resources server 2015-09-25 19:45:05 +02:00
Raul Ochoa
1ae15fe209 Remove unneeded resources server 2015-09-25 19:44:11 +02:00
Raul Ochoa
3c2820a5e1 Remove unneeded resources server 2015-09-25 19:43:20 +02:00
Raul Ochoa
8da6088f10 Remove outdated benchmark client 2015-09-25 19:39:49 +02:00
Raul Ochoa
8961a266b3 Remove redis key with test helper 2015-09-25 19:37:53 +02:00
Raul Ochoa
db8ab80bef Remove redis client 2015-09-25 19:32:30 +02:00
Raul Ochoa
36efc359f7 Remove redis client 2015-09-25 19:30:01 +02:00
Raul Ochoa
4f4dab143e Make tests to fail if unexpected keys are found in redis
Make test fail if they try to remove unexistent redis key
2015-09-25 19:23:33 +02:00
Raul Ochoa
af1e31fb29 Remove console 2015-09-25 19:22:34 +02:00
Raul Ochoa
1f757a7378 check style 2015-09-25 19:21:20 +02:00
Raul Ochoa
6028172018 Delegates redis keys deletion to test client[A 2015-09-25 19:18:28 +02:00
Raul Ochoa
2d374160a1 Delegates redis keys deletion to test client 2015-09-25 19:18:03 +02:00
Raul Ochoa
587fbfb4ff Delegates redis keys deletion to test client 2015-09-25 19:17:33 +02:00
Raul Ochoa
84820a87c8 Ask for specific redis key removal 2015-09-25 19:16:57 +02:00
Raul Ochoa
937d417e80 Delegates redis keys deletion to test client 2015-09-25 19:14:42 +02:00
Raul Ochoa
5b76cfd4dd Delegates redis keys deletion to test client 2015-09-25 19:13:46 +02:00
Raul Ochoa
957ed22b95 Delegates redis keys deletion to test client 2015-09-25 19:13:13 +02:00
Raul Ochoa
ebbe89cfb8 Ask for specific redis key removal 2015-09-25 19:11:48 +02:00
Raul Ochoa
c8eb6f275f Ask for specific redis key removal 2015-09-25 19:07:57 +02:00
Raul Ochoa
5f1213415b Ask for specific redis key removal 2015-09-25 19:06:04 +02:00
Raul Ochoa
d36ccb2602 Adapts tests to after test client changes 2015-09-25 19:05:37 +02:00
Raul Ochoa
486b803856 Delete redis keys using test helper 2015-09-25 19:04:59 +02:00
Raul Ochoa
d35630329f Ask for specific redis key removal 2015-09-25 18:41:44 +02:00
Raul Ochoa
aa9f742852 Ask for specific redis key removal 2015-09-25 18:37:21 +02:00
Raul Ochoa
960906e00c Ask for specific redis key removal 2015-09-25 18:22:45 +02:00
Raul Ochoa
432acd2b0e Ask for specific redis key removal 2015-09-25 18:20:30 +02:00
Raul Ochoa
84a03a81a0 Ask for specific redis key removal 2015-09-25 18:17:58 +02:00
Raul Ochoa
f64791eadd Ask for specific redis key removal 2015-09-25 18:04:45 +02:00
Raul Ochoa
51db76ac41 Ask for specific redis key removal 2015-09-25 17:59:00 +02:00
Raul Ochoa
1ec7f71b6c Ask for specific redis key removal 2015-09-25 17:51:19 +02:00
Raul Ochoa
bb3abdcc48 Remove redis keys after each test 2015-09-25 17:08:38 +02:00
Raul Ochoa
4d3ce038bc Remove redis keys after each test 2015-09-25 17:04:08 +02:00
Raul Ochoa
d66db547cb Remove redis keys after each test 2015-09-25 16:25:26 +02:00
Raul Ochoa
ebf1627753 Remove redis keys after each test 2015-09-25 16:23:14 +02:00
Raul Ochoa
bc818ca9cf Remove redis keys after each test 2015-09-25 16:23:08 +02:00
Raul Ochoa
0827124492 Remove redis keys after each test 2015-09-25 16:16:51 +02:00
Raul Ochoa
fea970f434 Remove redis keys after each test 2015-09-25 16:16:37 +02:00
Raul Ochoa
61cc14939b Remove redis keys after each test 2015-09-25 16:10:50 +02:00
Raul Ochoa
835d8c867e Remove redis keys after each test 2015-09-25 16:09:41 +02:00
Raul Ochoa
62601e4252 Remove redis keys after each test 2015-09-25 16:06:19 +02:00
Raul Ochoa
cc0b482c7a Remove redis keys after each test 2015-09-25 16:05:01 +02:00
Raul Ochoa
ff94f9ca0c Remove redis keys after each test 2015-09-25 16:03:20 +02:00
Raul Ochoa
8cd25dbd4f Remove redis keys after each test 2015-09-25 14:51:21 +02:00
Raul Ochoa
8ecf2e10c8 style 2015-09-25 14:50:09 +02:00
Raul Ochoa
5fb13bb545 Remove redis keys after each test 2015-09-25 14:49:59 +02:00
Raul Ochoa
a24b745f5c Remove redis keys after each test 2015-09-25 14:46:46 +02:00
Raul Ochoa
a127ff8d89 Remove redis keys after each test 2015-09-25 14:44:17 +02:00
Raul Ochoa
d3aa27533a Remove redis keys after each test 2015-09-25 14:42:40 +02:00
Raul Ochoa
35ec9e0063 Remove redis keys after each test 2015-09-25 14:42:30 +02:00
Raul Ochoa
1bac98d086 Remove redis keys after each test 2015-09-25 14:40:12 +02:00
Raul Ochoa
fc2f759dd3 Remove redis keys after each test 2015-09-25 14:39:39 +02:00
Raul Ochoa
f337a13577 Remove redis keys after each test 2015-09-25 14:39:15 +02:00
Raul Ochoa
eebd89aedb Remove redis keys after each test 2015-09-25 14:35:58 +02:00
Raul Ochoa
a9573987ec Remove redis keys after each test 2015-09-25 14:23:13 +02:00
Raul Ochoa
be8c82870f Add database id for found keys 2015-09-25 14:21:04 +02:00
Raul Ochoa
e667da8453 Remove redis keys being used in tests 2015-09-25 14:20:21 +02:00
Raul Ochoa
1258466529 Remove redis keys used in tests after each test 2015-09-25 14:09:35 +02:00
Raul Ochoa
8495b223c6 Early return when no keys to delete 2015-09-25 14:09:14 +02:00
Raul Ochoa
8339e4a4cb Remove redis keys used in tests after each test 2015-09-25 14:08:59 +02:00
Raul Ochoa
fa7288e03e Remove redis keys used in tests after each test 2015-09-25 14:08:35 +02:00
Raul Ochoa
763b6fce6e Remove redis keys used in each test 2015-09-25 14:07:12 +02:00
Raul Ochoa
7224acca84 Clean all redis keys after each test 2015-09-25 14:06:53 +02:00
Raul Ochoa
e12e7c4170 Don't allow to use suite related functions anymore 2015-09-25 13:37:32 +02:00
Raul Ochoa
0060d751b6 Use describe instead of suite 2015-09-25 13:37:10 +02:00
Raul Ochoa
b368463670 Use describe instead of suite 2015-09-25 13:31:51 +02:00
Raul Ochoa
21eb931701 Remove specific template maps doc as it's redundant info from Map-API.md 2015-09-25 11:21:03 +02:00
Raul Ochoa
e12b44ab2b Removes internal doc as it's mostly a duplication of Map-API.md
It also contains outdated terms and features.
2015-09-25 11:19:07 +02:00
Raul Ochoa
723dc59490 Show some redis stats after running tests
By commenting out you can monitor to a file
2015-09-25 09:52:14 +02:00
Raul Ochoa
5e1bc3e199 Named map updates does not emit update event if template didn't change
closes #323
2015-09-23 19:59:39 +02:00
Raul Ochoa
857548bbe4 Adds support for named layers in named tiles/static maps 2015-09-23 18:44:11 +02:00
Raul Ochoa
56070f3017 Show some redis stats after running tests
By commenting out you can monitor to a file
2015-09-23 17:18:58 +02:00
Raul Ochoa
f553efa69e Named map mapconfig provider takes care of template modifications
If a template changes it will flush the provider so it recreates the mapconfig
2015-09-23 16:45:20 +02:00
Raul Ochoa
84bf375f72 Makes cache async 2015-09-23 14:32:26 +02:00
Raul Ochoa
807455eb3d Force VACUUM ANALYZE in test table to stabilize test 2015-09-23 14:07:51 +02:00
Raul Ochoa
57284a9398 style 2015-09-23 13:05:35 +02:00
Raul Ochoa
c8705a8022 Use provider to get affected tables in static maps 2015-09-23 13:04:46 +02:00
Raul Ochoa
bbdc4591df Adds methods to assert mapnik images 2015-09-23 13:04:08 +02:00
Raul Ochoa
0a13e7943b Split tiles/static 2015-09-22 19:59:27 +02:00
Raul Ochoa
1e0bc57d32 Rename test file 2015-09-22 19:49:01 +02:00
Raul Ochoa
14e6cb05b3 Adds tests for named maps authentication for tiles 2015-09-22 19:48:08 +02:00
Raul Ochoa
b617bb0277 Health check will always return error if file is found even if empty 2015-09-22 15:15:57 +02:00
Raul Ochoa
ac7b02a434 Adds test for corner case in health check 2015-09-22 14:55:50 +02:00
Raul Ochoa
17458f3f4e Use windshaft's backend-foundations branch as it already has mvt support 2015-09-22 14:13:09 +02:00
Raul Ochoa
e87fc8a839 Merge branch 'standalone-server' into standalone-server-mvt 2015-09-22 12:28:16 +02:00
Raul Ochoa
6cf4d53b7c Merge branch 'master' into standalone-server
Conflicts:
	lib/cartodb/controllers/named_static_maps.js
2015-09-21 23:55:17 +02:00
Raul Ochoa
619545b993 Stubs next version 2015-09-21 22:49:06 +02:00
Raul Ochoa
e75d17f0e1 Release 2.13.0 2015-09-21 22:45:50 +02:00
Raul Ochoa
77a5b70576 Update news and bump version 2015-09-21 22:43:33 +02:00
Raul Ochoa
59b0a00c6f Merge pull request #332 from CartoDB/static-namedmaps-x-cache
Keep x-cache-channel in named map static maps
2015-09-21 22:40:42 +02:00
Raul Ochoa
5d24da4f2b Keep x-cache-channel in named map static maps 2015-09-21 22:36:13 +02:00
Raul Ochoa
0d3e96ee9f Merge branch 'standalone-server' into standalone-server-mvt 2015-09-18 17:23:58 +02:00
Raul Ochoa
16480a6c44 log as warn 2015-09-18 17:23:37 +02:00
Raul Ochoa
813a59a36e Removes function from control flow 2015-09-18 17:16:30 +02:00
Raul Ochoa
23fd33030d remove console.log 2015-09-18 17:13:37 +02:00
Raul Ochoa
2bdce4baa7 Replaces console.log with global logger 2015-09-18 17:13:22 +02:00
Raul Ochoa
d2df0b7c84 Configure log4js in test environment so it doesn't output by default 2015-09-18 16:59:45 +02:00
Raul Ochoa
16468b1216 remove console.log 2015-09-18 16:45:35 +02:00
Raul Ochoa
3abdee5e87 use debug for corner case 2015-09-18 16:40:55 +02:00
Raul Ochoa
d69a69da94 remove console.log 2015-09-18 16:39:52 +02:00
Raul Ochoa
bc806bba34 remove console.log 2015-09-18 16:39:30 +02:00
Raul Ochoa
81081ba2d4 Remove console.log from test 2015-09-18 16:26:54 +02:00
Raul Ochoa
5ad567803d Set backlog on server listen 2015-09-18 16:25:10 +02:00
Raul Ochoa
486cafba9d re-generate npm-shrinkwrap.json 2015-09-18 03:06:26 +02:00
Raul Ochoa
c0a1119d1d Merge branch 'standalone-server' into standalone-server-mvt 2015-09-18 02:58:39 +02:00
Raul Ochoa
bd4894ebd3 Starts modifying readme 2015-09-18 02:58:16 +02:00
Raul Ochoa
e9e95eda0a Merge branch 'standalone-server' into standalone-server-mvt 2015-09-18 02:31:38 +02:00
Raul Ochoa
1b3f1ba2e8 Merge pull request #331 from CartoDB/standalone-server-express-4
Upgrade express to 4.x
2015-09-18 02:09:23 +02:00
Raul Ochoa
5e27f981de Regenerate npm-shrinkwrap.json 2015-09-18 01:58:47 +02:00
Raul Ochoa
c97f78bb39 Merge branch 'standalone-server' into standalone-server-mvt
Conflicts:
	npm-shrinkwrap.json
	package.json
2015-09-18 01:34:14 +02:00
Raul Ochoa
f7b1032b7a Do not fail for now if there are pending keys in redis 2015-09-17 18:24:12 +02:00
Raul Ochoa
7ee2649feb Remove redis keys having in mind last updated time 2015-09-17 18:12:45 +02:00
Raul Ochoa
281320f2c4 use 127.0.0.1 instead of localhost 2015-09-17 17:51:34 +02:00
Raul Ochoa
30f7c74aee Reenable external resources tests using 127.0.0.1 2015-09-17 17:36:23 +02:00
Raul Ochoa
2dfd7257dd Try to run in travis with external resources disabled 2015-09-17 17:19:31 +02:00
Raul Ochoa
1f9dd5fd8c re-enable nock after every suite 2015-09-17 17:14:32 +02:00
Raul Ochoa
dd83c05a89 restore nock globally after each suite 2015-09-17 15:10:23 +02:00
Raul Ochoa
30cba053da Check there is no unexepcted keys on redis after tests 2015-09-17 15:07:54 +02:00
Raul Ochoa
5428d3f0b0 Remove __dirname 2015-09-17 13:58:45 +02:00
Raul Ochoa
95398354e3 require test helper 2015-09-17 13:58:22 +02:00
Raul Ochoa
7e73216539 Remove unused variables 2015-09-17 13:44:37 +02:00
Raul Ochoa
bbac1df463 Moves acceptance test about statsd uncaugth exception to integration 2015-09-17 13:40:01 +02:00
Raul Ochoa
208dd209a4 Merge branch 'standalone-server' into standalone-server-express-4
Conflicts:
	lib/cartodb/controllers/base.js
2015-09-17 12:57:33 +02:00
Raul Ochoa
9139feaa30 Move error message handling test to unit 2015-09-17 12:48:29 +02:00
Raul Ochoa
f9f6c8b700 Use debug instead of console 2015-09-17 12:03:58 +02:00
Raul Ochoa
db8457af60 status + send on syntax error handler 2015-09-17 11:07:02 +02:00
Raul Ochoa
361dd00e9d Use debug instead of console 2015-09-17 11:06:46 +02:00
Raul Ochoa
7fd870cfd2 Rewrite assert.response using request module 2015-09-17 02:06:46 +02:00
Raul Ochoa
967ef99277 Fix jsonp tests 2015-09-17 02:06:32 +02:00
Raul Ochoa
d93abe8e7d Change to delete 2015-09-17 02:05:47 +02:00
Raul Ochoa
a4ba21f9db Call send with correct params 2015-09-17 02:05:25 +02:00
Raul Ochoa
feabb20748 Send depending on body type 2015-09-17 02:04:30 +02:00
Raul Ochoa
31fe06e3ce Use listener and remove etag 2015-09-17 02:04:10 +02:00
Raul Ochoa
ef86bacf7f Set headers with set method 2015-09-17 02:03:09 +02:00
Raul Ochoa
beabe48aec Upgrade express, adds body-parser
- basic changes in server
- basic changes in unit tests
2015-09-17 00:19:00 +02:00
Raul Ochoa
e335f51dbc Makefile getting tests with find command 2015-09-16 23:18:38 +02:00
Raul Ochoa
38e422e84c Moves sendError and sendResponse to Base controller
Test for findStatusCode moved to controller
2015-09-16 21:54:56 +02:00
Raul Ochoa
1d6d11171d Fix test to not rely on network 2015-09-16 19:53:14 +02:00
Raul Ochoa
e32ced107e Fix all ported tests related to req2params 2015-09-16 18:09:39 +02:00
Raul Ochoa
99d78ce9b8 Remove unused variables 2015-09-16 17:02:35 +02:00
Raul Ochoa
066aff16f1 Use global for req2params number of request assert 2015-09-16 16:58:08 +02:00
Raul Ochoa
352dc6b311 BaseController to encapsulate req2params method
All controllers now extending BaseController
- Most of the acceptance ported tests will be broken
2015-09-16 16:18:26 +02:00
Raul Ochoa
a176ddbf3f Regenerate npm-shrinkwrap.json 2015-09-16 13:25:57 +02:00
Raul Ochoa
1c6571d1db Upgrade dependencies and regenerate npm-shrinkwrap.json
Surrogate keys tests sleeping :-(
2015-09-16 13:17:03 +02:00
Raul Ochoa
bf7b35230f Regenerate npm-shrinkwrap.json 2015-09-16 11:44:12 +02:00
Raul Ochoa
46d901ada7 Merge branch 'standalone-server' into standalone-server-mvt 2015-09-16 11:09:27 +02:00
Raul Ochoa
66f94d9452 Fix test 2015-09-16 02:49:18 +02:00
Raul Ochoa
62f428f434 Remove app dependency 2015-09-16 01:48:54 +02:00
Raul Ochoa
713ad03c3b No need to expose findStatusCode at app level 2015-09-16 01:44:30 +02:00
Raul Ochoa
ad2ebc11dd Remove unused require 2015-09-16 01:39:15 +02:00
Raul Ochoa
72a0c4a487 New sendResponse and sendError methods
- fixes response for static named map error cases
2015-09-16 01:36:51 +02:00
Raul Ochoa
fba5a35514 Move sendResponse and sendError to response object 2015-09-15 19:28:02 +02:00
Raul Ochoa
e2e5e40ea9 Fix maxcomplexity 2015-09-15 19:01:34 +02:00
Raul Ochoa
f5660667c8 Move req.profiler call to req2params itself 2015-09-15 18:16:50 +02:00
Raul Ochoa
64e225c4aa Merge branch 'master' into standalone-server 2015-09-15 11:48:33 +02:00
Raul Ochoa
126491fc93 Remove unused xml file that was used in health check 2015-09-15 11:46:19 +02:00
Raul Ochoa
ea872d96f8 Listen to renderer cache events and log stats 2015-09-14 19:24:24 +02:00
Raul Ochoa
3f7202d89c Port tests for stats 2015-09-14 19:07:53 +02:00
Raul Ochoa
2d3088ba27 Port everything related to stats from windshaft 2015-09-14 18:47:01 +02:00
Raul Ochoa
469b602484 Attributes backend using mapconfig provider to access attributes 2015-09-14 18:46:22 +02:00
Raul Ochoa
aa6f00f927 Merge branch 'standalone-server' into standalone-server-mvt 2015-09-10 12:06:21 +02:00
Raul Ochoa
67afd8738b Remove renderer cache param from attributes backend 2015-09-10 00:47:32 +02:00
Raul Ochoa
98d6170870 Adds notes about contributing 2015-09-08 16:44:44 +02:00
Raul Ochoa
b2e1e5361f Merge branch 'standalone-server' into standalone-server-mvt
Conflicts:
	npm-shrinkwrap.json
2015-09-08 15:46:23 +02:00
Raul Ochoa
f96c80d7a1 Merge branch 'master' into standalone-server
Conflicts:
	lib/cartodb/cartodb_windshaft.js
2015-09-08 15:42:30 +02:00
Raul Ochoa
7ae034d746 Remove no longer needed health check params 2015-09-07 18:40:20 +02:00
Raul Ochoa
c409c146bf Upgrade CDB_QueryTables to use latest version 2015-09-07 17:17:40 +02:00
Raul Ochoa
e0a7eb01cc Use torque renderer config
Adds some notes about db pool params in torque
2015-09-04 16:33:40 +02:00
Raul Ochoa
3af2136770 Merge branch 'master' into standalone-server
Conflicts:
	lib/cartodb/cartodb_windshaft.js
	lib/cartodb/monitoring/health_check.js
2015-09-04 13:21:54 +02:00
Raul Ochoa
13a2001a2b Merge pull request #329 from CartoDB/health-check-no-results
Do not return results from health check
2015-08-28 18:50:00 +02:00
Raul Ochoa
d6102284a4 Do not return results from health check
It also removed old dependencies and takes disabled file path in ctor.
2015-08-28 17:41:40 +02:00
Raul Ochoa
c56ef8e0da Merge branch 'master' into standalone-server
Conflicts:
	app.js
	npm-shrinkwrap.json
	package.json
2015-08-28 13:54:56 +02:00
Raul Ochoa
bb17609ea3 Stubs next version 2015-08-27 17:48:06 +02:00
Raul Ochoa
f2f342e14c Release 2.12.0 2015-08-27 17:46:29 +02:00
Raul Ochoa
11bd07ee6b Merge pull request #328 from CartoDB/upgrade-windshaft
Upgrades windshaft to 0.51.0
2015-08-27 17:39:14 +02:00
Raul Ochoa
fa85dcbc4c Upgrades windshaft to 0.51.0 2015-08-27 17:24:38 +02:00
Raul Ochoa
473ae13a03 Merge pull request #327 from CartoDB/http-agent-configuration
Make http and https globalAgent options configurable
2015-08-27 16:44:55 +02:00
Raul Ochoa
0561a28a4d Make http and https globalAgent options configurable 2015-08-27 16:28:16 +02:00
Raul Ochoa
b3467116fe Stubs next version 2015-08-26 09:55:15 +02:00
Raul Ochoa
e667d10eb8 Release 2.11.0 2015-08-26 09:52:53 +02:00
Raul Ochoa
a5bda00cdd Merge pull request #326 from CartoDB/upgrade-windshaft
Upgrades windshaft to 0.50.0
2015-08-25 19:32:28 +02:00
Raul Ochoa
627bc672bc Upgrades windshaft to 0.50.0 2015-08-25 19:27:00 +02:00
Raul Ochoa
9ef96080a6 Log PID on start 2015-08-24 12:45:21 +02:00
Raul Ochoa
bf4844e664 Stubs next version 2015-08-18 16:35:02 +02:00
Raul Ochoa
06f454abcf Release 2.10.0 2015-08-18 16:33:44 +02:00
Raul Ochoa
9d4e4a99bc Merge pull request #325 from CartoDB/upgrade-windshaft-tilelive-cache
Upgrades windshaft
2015-08-18 16:29:22 +02:00
Raul Ochoa
b77be76f51 Bump version and add news 2015-08-18 15:22:27 +02:00
Raul Ochoa
3121ed9a95 Config samples for metatileCache with default behaviour 2015-08-18 15:18:58 +02:00
Raul Ochoa
37dfd8fc12 Upgrade windshaft 2015-08-18 15:18:44 +02:00
Raul Ochoa
de2719b0c5 Stubs next version 2015-08-06 18:03:00 +02:00
Raul Ochoa
e3d5abc9a2 Release 2.9.0 2015-08-06 18:02:09 +02:00
Raul Ochoa
ea7a5da1c1 Merge pull request #324 from CartoDB/memory-stats
Send memory usage stats
2015-08-06 16:12:18 +02:00
Raul Ochoa
2e063cc2d2 Send memory usage stats 2015-08-06 16:06:42 +02:00
Raul Ochoa
943509864d Improve uniqueness of named map map config provider 2015-07-31 12:24:34 +02:00
Raul Ochoa
2ac228359f Fallback to image/png header 2015-07-31 12:23:36 +02:00
Raul Ochoa
909f8da2ff Adds lru cache for layergroups and named maps mapconfig provider 2015-07-15 16:51:26 +02:00
Raul Ochoa
5a5832394a Remove console.log 2015-07-15 16:10:59 +02:00
Raul Ochoa
52b60a22fd Makes all tests to run together 2015-07-15 16:09:43 +02:00
Raul Ochoa
116da64e5c More strict cyclomatic complexity check 2015-07-15 15:10:59 +02:00
Raul Ochoa
9c6c63c167 More strict jshint 2015-07-15 15:03:28 +02:00
Raul Ochoa
e7284262e4 Merge branch 'master' into standalone-server
Conflicts:
	npm-shrinkwrap.json
	package.json
2015-07-15 12:20:12 +02:00
Raul Ochoa
decd9077e4 Stubs next version 2015-07-15 11:44:59 +02:00
Raul Ochoa
f6c47bf85e Release 2.8.0 2015-07-15 11:43:54 +02:00
Raul Ochoa
423191c13b Merge pull request #319 from CartoDB/upgrade-windshaft
Upgrades windshaft to 0.48.0
2015-07-15 11:43:09 +02:00
Raul Ochoa
7a5928d957 Upgrades windshaft to 0.48.0 2015-07-15 11:31:26 +02:00
Raul Ochoa
bbec3ae7da Subscribes to named map changes to invalidate cache 2015-07-14 21:18:10 +02:00
Raul Ochoa
1f7daab677 Caching named map providers by template name and config/auth token
Named Map provider cache buster changes on creation
2015-07-14 21:17:58 +02:00
Raul Ochoa
91ab64dda9 Fix cached result in getAffectedTablesAndLastUpdatedTime 2015-07-14 21:00:27 +02:00
Raul Ochoa
722705468f Not adding surrogate keys for empty affected tables 2015-07-14 20:53:26 +02:00
Raul Ochoa
07c920bad5 Use named map provider cache to retrieve providers 2015-07-14 20:53:06 +02:00
Raul Ochoa
6d3ef11a7c Fix cache usage in layergroup affected tables 2015-07-14 20:11:49 +02:00
Raul Ochoa
4aabe9d946 Named maps controller adding cache headers
This requires a cache for affected tables as it is hitting db for
every request right now
2015-07-14 20:10:55 +02:00
Raul Ochoa
7247b20686 Unify sendResponse in named maps controller 2015-07-14 17:34:05 +02:00
Raul Ochoa
8e8f618a22 assert instead of ifs 2015-07-14 17:33:42 +02:00
Raul Ochoa
a50af0ee64 Merge branch 'master' into standalone-server 2015-07-14 16:36:26 +02:00
Raul Ochoa
436c334f5a Stubs next version 2015-07-14 16:33:12 +02:00
Raul Ochoa
4f84138ade Release 2.7.2 2015-07-14 16:32:04 +02:00
Raul Ochoa
a0d86ac5dc Update news 2015-07-14 16:28:51 +02:00
Raul Ochoa
d426702213 Merge branch 'master' into standalone-server 2015-07-14 16:24:39 +02:00
Raul Ochoa
e2be4f1275 Merge pull request #314 from CartoDB/use-cdb-query-tables-text
Use CDB_QueryTablesText instead of CDB_QueryTables
2015-07-14 16:20:23 +02:00
Raul Ochoa
c97610ad59 style 2015-07-14 14:30:37 +02:00
Raul Ochoa
e8b5845174 Shared cache for affected tables in layergroup and map controllers 2015-07-14 13:40:41 +02:00
Raul Ochoa
c295584864 Cache channel now in layergroup controller
Internal cache channel dbname+layergroupid cache must be unified in layergroup
and map controllers
Removes sendWithHeaders
2015-07-14 11:55:49 +02:00
Raul Ochoa
36257f73b9 Better format 2015-07-13 17:18:50 +02:00
Raul Ochoa
9355a5ca24 Tests for surrogate keys in layergroup anonymous instantiation 2015-07-13 16:54:08 +02:00
Raul Ochoa
d6447ef311 Fix tests related to surrogate keys, includes tables 2015-07-13 16:36:41 +02:00
Raul Ochoa
5e2a20fbe0 Tags layergroup instantiation with surrogate keys per affected tables 2015-07-13 16:15:34 +02:00
Raul Ochoa
76823f7529 No need to pass a reference to itself 2015-07-13 15:06:22 +02:00
Raul Ochoa
96a6a0d980 Using MapStore, no need to attach it to app 2015-07-13 15:05:45 +02:00
Raul Ochoa
b05701be61 Authentication/Authorization moves to its own entity 2015-07-13 15:05:03 +02:00
Raul Ochoa
962ac97433 regenerate npm-shrinkwrap.json 2015-07-13 12:52:12 +02:00
Raul Ochoa
9491b81d0c Standalone server with MVT support 2015-07-13 12:34:52 +02:00
Raul Ochoa
316f08df08 named maps tiles sending tile headers 2015-07-13 11:53:23 +02:00
Raul Ochoa
f9554ec761 Re-enable render limits 2015-07-10 19:10:55 +02:00
Raul Ochoa
e128b1d750 remove unused method 2015-07-10 12:33:01 +02:00
Raul Ochoa
e45efbcfb0 New map config provider to allow injecting limits in context 2015-07-10 12:31:56 +02:00
Raul Ochoa
847ab96a48 RendererFactory changes for new signature 2015-07-10 12:30:52 +02:00
Raul Ochoa
1e52f790ad One pass for prepare request and response objects 2015-07-10 11:25:20 +02:00
Raul Ochoa
9bece712a9 Splits controllers and supports after layergroup creation actions 2015-07-10 11:24:32 +02:00
Raul Ochoa
6e0678e084 better style 2015-07-10 01:31:06 +02:00
Raul Ochoa
579cabdc1a Initial refactor of layergroup creation 2015-07-10 01:30:38 +02:00
Raul Ochoa
9f252dfac4 Improve named map config format for cache key 2015-07-09 19:49:11 +02:00
Raul Ochoa
5aad624346 NamedMaps controller using NamedMapMapConfigProvider
PreviewBackend with format param
2015-07-09 18:47:21 +02:00
Raul Ochoa
23d1109910 Adds named maps mapconfig provider
starts using it in named map instantiation
2015-07-09 14:39:25 +02:00
Raul Ochoa
ae2a72a810 Fix named maps controller using MapStoreMapConfig 2015-07-09 13:37:00 +02:00
Raul Ochoa
ed096c3a1a disable tests, time to work on a named map provider before fixing 'em 2015-07-08 20:51:55 +02:00
Raul Ochoa
123346ebdb Refactor controllers to use map store map config provider 2015-07-08 20:51:36 +02:00
Raul Ochoa
8540965696 fix health check tests 2015-07-08 20:50:34 +02:00
Raul Ochoa
c8568b175b Move server info to its own controller 2015-07-08 16:08:38 +02:00
Raul Ochoa
1737cbe1a5 Unifies named map instantiation so it's easy to work on it 2015-07-08 15:50:59 +02:00
Raul Ochoa
c81048312d Context with user 2015-07-08 15:34:46 +02:00
Raul Ochoa
ac3afd5695 Fix jshint 2015-07-08 13:28:07 +02:00
Raul Ochoa
fa84813a37 Manage cors with a middleware 2015-07-08 13:27:56 +02:00
Raul Ochoa
8cd3807100 Split named maps administration from instantiation/usage 2015-07-08 13:11:57 +02:00
Raul Ochoa
7aeb54d53d Enables multilayer ported tests 2015-07-08 12:59:49 +02:00
Raul Ochoa
725ff41fb1 Ports tile stats tests from windshaft 2015-07-08 00:19:11 +02:00
Raul Ochoa
d071fe6d0c Ports windshaft server unit tests 2015-07-08 00:12:32 +02:00
Raul Ochoa
2234a763cb Uses model through model namespace 2015-07-07 23:52:39 +02:00
Raul Ochoa
d52b65470e Ports acceptance tests from windshaft 2015-07-07 23:46:58 +02:00
Raul Ochoa
b63e697934 Handle no layers case 2015-07-07 23:45:56 +02:00
Raul Ochoa
8a036c79c7 Merge branch 'master' into standalone-server
Conflicts:
	app.js
2015-07-07 12:36:39 +02:00
Raul Ochoa
9a393fa793 Adds some notes about uv_threadpool_size and mapnik renderer pool size 2015-07-07 12:27:09 +02:00
Raul Ochoa
7614f72df6 Stubs next version 2015-07-06 11:58:08 +02:00
Raul Ochoa
ef2db78567 Release 2.7.1 2015-07-06 11:56:30 +02:00
Raul Ochoa
8708468444 fix 2.7.0 release date 2015-07-06 11:56:18 +02:00
Raul Ochoa
cd28a4fbcc redis-mpool noReadyCheck and unwatchOnRelease options from config
do not extend them as it disallows to pick from config
2015-07-06 11:52:34 +02:00
Raul Ochoa
e49881d1ed Improve authorizedBySigner 2015-07-06 03:23:51 +02:00
Raul Ochoa
aa266f9b61 Improve authorizedByAPIKey 2015-07-06 03:19:56 +02:00
Raul Ochoa
ccd3d0a3bf Merge branch 'named-maps-tiles' into standalone-server
Conflicts:
	app.js
2015-07-06 02:55:13 +02:00
Raul Ochoa
69fc367f69 Empty results/png directory 2015-07-06 02:32:13 +02:00
Raul Ochoa
a9f24542d5 regenerate npm-shrinkwrap.json 2015-07-06 02:09:06 +02:00
Raul Ochoa
8e4e458a2a fix jshint 2015-07-06 02:08:56 +02:00
Raul Ochoa
feec7805af Merge branch 'master' into named-maps-tiles
Conflicts:
	npm-shrinkwrap.json
	package.json
2015-07-06 01:32:33 +02:00
Raul Ochoa
19f488095b Stubs next version 2015-07-06 01:07:30 +02:00
Raul Ochoa
cd65c6dd0e Release 2.7.0 2015-07-06 01:05:28 +02:00
Raul Ochoa
0c670cfdfd Merge pull request #318 from CartoDB/upgrade-windshaft
Upgrades windshaft to 0.47.0
2015-07-05 21:51:34 +02:00
Raul Ochoa
27ff1ac4f6 Upgrades windshaft to 0.47.0 2015-07-05 21:16:12 +02:00
Raul Ochoa
3f06de93f7 Merge pull request #317 from CartoDB/improve-redis
Improve redis
2015-07-05 21:11:34 +02:00
Raul Ochoa
0da6495330 Fixes unwatchOnRelease redis config 2015-07-05 21:00:25 +02:00
Raul Ochoa
bf24347328 Exposes redis noReadyCheck config 2015-07-05 20:59:52 +02:00
Raul Ochoa
7a5d73f9df Upgrades redis-mpool 2015-07-05 20:58:39 +02:00
Raul Ochoa
7fc403425d metadata backend 2015-07-04 23:44:39 +02:00
Raul Ochoa
ef171bf2af reverts map store changes 2015-07-04 23:38:15 +02:00
Raul Ochoa
b74a6624e3 remove redundant code 2015-07-04 23:32:26 +02:00
Raul Ochoa
19bf1fe56b note about token format 2015-07-04 23:32:19 +02:00
Raul Ochoa
ea6bb8dca3 fix jsdoc 2015-07-04 23:20:12 +02:00
Raul Ochoa
9d6d3f96b2 Unify mapstore 2015-07-04 23:18:09 +02:00
Raul Ochoa
5967c5d1d5 Reorg app.js 2015-07-04 23:09:00 +02:00
Raul Ochoa
a6017c6ade Reorg requires 2015-07-04 21:33:31 +02:00
Raul Ochoa
2d3f2667ca Standalone server initial implementation
- no dependency over Windshaft.Server
2015-07-04 20:41:22 +02:00
Raul Ochoa
ed90cadd75 fix jshint 2015-07-02 16:35:13 +02:00
Raul Ochoa
0d9f34fd48 Stubs next version 2015-07-02 15:48:58 +02:00
Raul Ochoa
da55a3bdd2 Release 2.6.1 2015-07-02 15:47:40 +02:00
Raul Ochoa
333334e598 Upgrades windshaft 2015-07-02 15:41:48 +02:00
Raul Ochoa
7168e4410c Stubs next version 2015-07-02 14:12:41 +02:00
Raul Ochoa
3ff8571f4a Release 2.6.0 2015-07-02 14:11:39 +02:00
Raul Ochoa
75ddcbbd01 Updates windshaft to 0.46.0 and documents new config formatMetatile 2015-07-02 13:28:37 +02:00
Raul Ochoa
91a44980f3 Skips limits tests until beforeRendererCreate is available 2015-07-02 02:03:03 +02:00
Raul Ochoa
034f3c77ce modifies controllers to use new mapbackend signatures 2015-07-02 02:02:22 +02:00
Raul Ochoa
9b3e18f333 Merge pull request #315 from CartoDB/readme_update
Update instructions
2015-07-01 09:37:57 +02:00
Juan Ignacio Sánchez Lara
fcb0a4a7e6 Less specific upgrade help 2015-07-01 07:22:37 +02:00
Raul Ochoa
5a003a7cbe Initial/dummy implementation for named maps tiles
Issues:
 - creates a layergroup per tile:
   - trigges a mapview
   - extracts each time affected tables and last update
 - duplicates a lot of code from NamedStaticMapsController
 - keeps relying on fake request concept
2015-06-30 15:41:57 +02:00
Juan Ignacio Sánchez Lara
94e38cef9d Update instructions 2015-06-30 11:00:32 +02:00
Raul Ochoa
d13d107aea Adds names to functions 2015-06-29 19:18:52 +02:00
Raul Ochoa
4f87796e9c Uses backend-foundations branch to use createLayergroup from backend 2015-06-29 18:58:58 +02:00
Raul Ochoa
837da45f4f Merge branch 'master' into named-maps-tiles 2015-06-29 18:37:11 +02:00
Raul Ochoa
9e30f05e7d Reverts to use cdb branch as is already published 2015-06-29 16:46:07 +02:00
Raul Ochoa
0df725112b Update CDB_QueryTables function 2015-06-29 16:42:55 +02:00
Raul Ochoa
098ed6b203 New endpoint for named maps tiles 2015-06-29 16:39:35 +02:00
Raul Ochoa
c6f9152efe Moves template maps to backends directory 2015-06-29 16:38:13 +02:00
Raul Ochoa
9ea2029f81 Deprecating scripts from tools directory 2015-06-25 18:07:48 +02:00
Raul Ochoa
2715f47a22 Points CDB_QueryTables script to the branch with CDB_QueryTablesText 2015-06-24 19:07:41 +02:00
Rafa de la Torre
90d0b23441 Use CDB_QueryTablesText instead of CDB_QueryTables
This avoids trouble with len(schema.table_name) > 63
See https://github.com/CartoDB/cartodb-postgresql/issues/86
2015-06-24 15:43:04 +02:00
Raul Ochoa
b59e0a00a0 Merge pull request #313 from CartoDB/cartodb-layer-docs
Update Map-API docs
2015-06-23 15:04:16 +02:00
Andrew Thompson
790571fd2c Update Map-API docs
It was not clear to me what the difference was between a mapnik layer and a cartodb layer until I read the comments in the Mapconfig spec! Let's save some users the extra step :)
2015-06-22 11:14:57 -04:00
Raul Ochoa
6ecebae110 Adds test to validate (once it is fixed) long table names do not fail 2015-06-18 16:29:59 +02:00
Raul Ochoa
849470a3c0 Stubs next version 2015-06-18 12:55:38 +02:00
Raul Ochoa
74d0a6f183 Release 2.5.0 2015-06-18 12:53:45 +02:00
Raul Ochoa
61c134215a Bumps version and regenerates npm-shrinkwrap.json 2015-06-18 12:29:02 +02:00
Raul Ochoa
b7218d8832 Fix list style 2015-06-18 12:19:03 +02:00
Raul Ochoa
63e19427af Fix documentation error examples to match 'General Concepts' guidelines 2015-06-18 12:17:17 +02:00
Raul Ochoa
d4f8578fd6 Merge pull request #312 from CartoDB/issue-311
Adds layergroupid header
2015-06-18 11:21:03 +02:00
Raul Ochoa
eaccd062d3 Adds layergroupid header
Closes #311
2015-06-18 01:13:33 +02:00
Raul Ochoa
5f2d5931f4 Regenerate npm-shrinkwrap.json using master branch 2015-06-12 15:48:03 +02:00
Raul Ochoa
ce40fa608e Clarify bounds meaning in template view 2015-06-08 14:31:04 -04:00
Raul Ochoa
0979c75852 Merge pull request #308 from CartoDB/issue-305
Named maps error responses with valid format
2015-06-08 10:49:22 -04:00
Raul Ochoa
f01e8e0866 Merge pull request #304 from CartoDB/docs-revamp
Documentation for named maps static images + view option in templates
2015-06-08 10:49:08 -04:00
Raul Ochoa
a4e303ab63 Remove console.log from tests 2015-06-08 10:37:56 -04:00
Raul Ochoa
c5137c9c29 Update news 2015-06-05 13:41:46 -04:00
Raul Ochoa
9bce88f9b1 Fix tests 2015-06-05 13:39:25 -04:00
Raul Ochoa
68c70effec Named maps returning errors=>Array instead of error=>String 2015-06-05 13:38:38 -04:00
Raul Ochoa
ebae218219 Use windshaft branch with unified error reponses format 2015-06-05 13:37:49 -04:00
Raul Ochoa
6685b759b2 Remove duplicated module.exports 2015-06-04 20:14:36 -04:00
Raul Ochoa
539b0496bf Update news 2015-06-04 12:53:59 -04:00
Raul Ochoa
3c33fac8f4 Merge pull request #307 from CartoDB/template-names
Changes rules for names in templates
2015-06-04 12:52:30 -04:00
Raul Ochoa
9613f76ef5 Keep placeholder key validation independent from name validation 2015-06-04 11:58:24 -04:00
Raul Ochoa
3f0d344313 Changes rules for names in templates
Now valid names can start with numbers and can contain dashes (-).
Closes #306
2015-06-04 10:41:40 -04:00
Raul Ochoa
823ee63c25 Merge branch 'master' into docs-revamp
Conflicts:
	docs/Map-API.md
2015-06-03 17:11:01 -04:00
Raul Ochoa
d5d76f9c63 Adds information about named map static images endpoint 2015-06-03 17:09:10 -04:00
Raul Ochoa
dfc9a6fbb4 Documentation about view in named maps' templates
Closes #287
2015-06-03 17:00:42 -04:00
Raul Ochoa
2bd9aece35 Add header for zoom + center in static maps 2015-06-03 17:00:05 -04:00
Raul Ochoa
21870c9fa2 Merge branch 'master' into docs-revamp 2015-06-03 16:57:24 -04:00
Raul Ochoa
7c315b3afd Merge pull request #303 from CartoDB/links-to-map-config
Updated links to MapConfig 1.3.0
2015-06-03 16:37:24 -04:00
Pablo Alonso Garcia
da0cdb081d Updated links to MapConfig 1.3.0 2015-06-03 10:48:42 +02:00
Carlos Matallín
9bd0a3f1c9 Update Map-API.md 2015-06-01 17:00:53 +02:00
Raul Ochoa
83992895e4 Merge pull request #301 from CartoDB/layers-documentation
Documentation about layers metadata and layer selection/filtering
2015-06-01 16:48:14 +02:00
Raul Ochoa
aa3b336e46 Stubs next version 2015-06-01 15:50:53 +02:00
Raul Ochoa
e17b374fde Release 2.4.1 2015-06-01 15:49:30 +02:00
Raul Ochoa
61158b62f1 Update news 2015-06-01 15:46:32 +02:00
Raul Ochoa
88ed43a92e Merge pull request #302 from CartoDB/upgrade-windshaft
Upgrade windshaft
2015-06-01 14:28:28 +02:00
Raul Ochoa
e5fff6b452 Merge branch 'master' into upgrade-windshaft 2015-06-01 14:19:49 +02:00
Raul Ochoa
044d49c53a Uses windshaft 0.44.1 from registry 2015-06-01 12:20:44 +02:00
Raul Ochoa
69abf8d9b1 uses windshaft's master branch 2015-06-01 11:35:57 +02:00
Andy Eschbacher
14e13899a6 made minor changes 2015-05-28 11:02:34 -04:00
Raul Ochoa
488c246222 Documentation about layers metadata and layer selection 2015-05-28 15:10:55 +02:00
Raul Ochoa
654905a79c Stubs next version 2015-05-26 15:53:07 +02:00
Raul Ochoa
12cb199803 Release 2.4.0 2015-05-26 15:52:28 +02:00
Raul Ochoa
8759cf726b Merge pull request #300 from CartoDB/upgrade-windshaft
Bumps windshaft version to 0.44.0
2015-05-26 15:50:19 +02:00
Raul Ochoa
7a45c9e434 Bumps windshaft version to 0.44.0
- adds a test to validate metadata is returned for unrolled named layers
2015-05-26 15:39:21 +02:00
Raul Ochoa
9ee69dea55 Stubs next version 2015-05-18 18:11:48 +02:00
Raul Ochoa
ebe38e977f Release 2.3.0 2015-05-18 18:10:47 +02:00
Raul Ochoa
40ad143c3e Merge pull request #299 from CartoDB/upgrade-cartodb-redis
Upgrades cartodb-redis for `global` map stats
2015-05-18 18:08:06 +02:00
Raul Ochoa
875159fa5f Upgrades cartodb-redis for global map stats 2015-05-18 17:40:41 +02:00
Raul Ochoa
c97c65de34 Fixes multilayer link 2015-05-18 11:24:06 +02:00
Raul Ochoa
25ae09b2c5 Update patch to generate routes document 2015-04-29 17:18:36 +02:00
Raul Ochoa
853c2b4b85 Adds new route for named maps static previews 2015-04-29 17:17:46 +02:00
Raul Ochoa
682db1ca75 Stubs next version 2015-04-29 15:26:56 +02:00
Raul Ochoa
d56bd8de72 Release 2.2.0 2015-04-29 15:18:47 +02:00
Raul Ochoa
1df91aee6f Merge pull request #293 from CartoDB/named-static-maps
Named static maps
2015-04-29 12:48:02 +02:00
Raul Ochoa
b64eaed7ed Enables any URL in HTTP renderer whitelist in example configurations 2015-04-29 12:15:26 +02:00
Raul Ochoa
03dae8a93a changes about tests 2015-04-29 10:35:11 +02:00
Raul Ochoa
6b93fa0575 Update news 2015-04-29 10:31:47 +02:00
Raul Ochoa
5a9a2d7449 Use windshaft's registry version 2015-04-29 10:21:23 +02:00
Raul Ochoa
8d200686fd regenerate npm-shrinkwrap.json 2015-04-29 09:59:36 +02:00
Raul Ochoa
1c2f84b0cb Rely on windshaft master branch 2015-04-29 00:20:41 +02:00
Raul Ochoa
513fa2af01 Log all named map invalidation with context 2015-04-28 17:25:07 +02:00
Raul Ochoa
7580081a64 Append stats to profiler 2015-04-28 16:14:30 +02:00
Raul Ochoa
1a66f96379 Adds custom cache control header for named map static images 2015-04-28 16:14:19 +02:00
Raul Ochoa
fde680450f Do not use headers from abaculus in combination with sendWithHeaders 2015-04-28 16:14:03 +02:00
Raul Ochoa
6843692f01 Pick format from user params 2015-04-28 16:10:50 +02:00
Raul Ochoa
1f3a073f21 Use headers from fake request 2015-04-28 16:10:30 +02:00
Raul Ochoa
7b4d41464f tests for static named maps 2015-04-27 19:15:06 +02:00
Raul Ochoa
7ae2910061 adds tests as part of the jshint target 2015-04-27 18:08:55 +02:00
Raul Ochoa
ed3517e733 fix jshint 2015-04-27 18:08:40 +02:00
Raul Ochoa
6ac3b4c005 fix jshint 2015-04-27 18:05:39 +02:00
Raul Ochoa
26545af9ae fix jshint 2015-04-27 18:03:15 +02:00
Raul Ochoa
1ee96f14ce fix jshint 2015-04-27 18:02:15 +02:00
Raul Ochoa
2250e6d608 fix jshint 2015-04-27 18:00:47 +02:00
Raul Ochoa
5ad27e4bf5 fix jshint 2015-04-27 17:58:56 +02:00
Raul Ochoa
5f765712b4 fix jshint 2015-04-27 17:54:07 +02:00
Raul Ochoa
cb2e330e0b Uses describe/it instead of suite/test 2015-04-27 17:49:15 +02:00
Raul Ochoa
6de911e5bb Adds fastly invalidation expectations in surrogate key invalidation tests 2015-04-27 17:43:46 +02:00
Raul Ochoa
9edec8ef3f Adds Fastly cache backend 2015-04-27 16:31:47 +02:00
Raul Ochoa
8e8ab09bec Clarify fastly configuration is optional 2015-04-27 16:28:28 +02:00
Raul Ochoa
c06cba81f4 SurrogateKeysCache now accepts several cache backends
- uses queue-async to parallelize the call to invalidate
2015-04-27 16:22:37 +02:00
Raul Ochoa
ad5514dd02 Pick fastly config for server options 2015-04-27 16:20:55 +02:00
Raul Ochoa
a5b9ca706c Adds new fastly cache backend 2015-04-27 16:18:50 +02:00
Raul Ochoa
5a476f9354 Adds fastly-purge dependency 2015-04-27 16:18:03 +02:00
Raul Ochoa
403039b695 Adds new configuration examples for fastly surrogate keys invalidation 2015-04-27 16:17:13 +02:00
Raul Ochoa
5ee19cc2ed Rename template maps controller to named maps to be more clear 2015-04-27 15:01:49 +02:00
Raul Ochoa
8c3f9c7ba0 Inject server options to use setDBParams 2015-04-27 14:59:41 +02:00
Raul Ochoa
b95a001e0b New static maps controller/endpoint for named maps
- loads a template
 - creates a layergroup on the fly
 - checks for view center+zoom or bounds
 - if not found it tries to estimate them
 - if fails it falls to default bounds value
 - returns an static image tagged with a surrogate key
2015-04-27 14:56:38 +02:00
Raul Ochoa
d180305e8b Exposes pgQueryRunner in server options 2015-04-27 14:54:14 +02:00
Raul Ochoa
ef8fcf7e93 Do not inject NamedMapsCacheEntry as template controller knows about them
Also do not inject pgConnection
2015-04-27 14:52:36 +02:00
Raul Ochoa
e7bd5dd644 Moves setDBParams to serverOptions so it can be reused 2015-04-27 14:47:58 +02:00
Raul Ochoa
8503a5c7c9 Tables extent API: returns estimated bounds for a list of tables 2015-04-27 12:55:20 +02:00
Raul Ochoa
2de0e5d52b Extracts psql query run to its own class to be reusable 2015-04-27 12:48:34 +02:00
Raul Ochoa
b9e4b0a90c Removes alpha version from example 2015-04-27 11:56:54 +02:00
Raul Ochoa
8fb3dc7529 Move templateName function to template maps model 2015-04-27 11:55:05 +02:00
Raul Ochoa
a897e36b91 Merge pull request #290 from CartoDB/simplify-template-names
Simplify template names
2015-04-23 12:19:21 +02:00
Raul Ochoa
446c432484 dry content type validation 2015-04-23 12:05:52 +02:00
Raul Ochoa
c49f3aaba5 DRY ifUnauthenticated method 2015-04-23 12:01:53 +02:00
Raul Ochoa
fed29b3b50 Extract finish function 2015-04-23 11:47:01 +02:00
Raul Ochoa
e7d134d70c No more {username}@{template_name} template id
It's still backwards compatible
2015-04-23 11:29:55 +02:00
Raul Ochoa
62dbce4311 Merge pull request #289 from CartoDB/issue-267
Call callback on invalid map store token for named maps
2015-04-22 18:38:58 +02:00
Raul Ochoa
5b5f7fc700 Merge pull request #288 from CartoDB/clean-templates-suite
cleans templates suite: uses describe+it instead of suite+test
2015-04-21 23:58:13 +02:00
Raul Ochoa
026a0750e3 Call callback on invalid map store token for named maps
fixes #267
2015-04-21 18:59:52 +02:00
Raul Ochoa
7045f41252 cleans templates suite: uses describe+it instead of suite+test 2015-04-21 18:28:31 +02:00
Raul Ochoa
eaf6775d9d Stubs next version 2015-04-16 18:06:55 +02:00
Raul Ochoa
ba2a9b81e9 Release 2.1.3 2015-04-16 18:02:36 +02:00
Raul Ochoa
5577600903 Upgrade windshaft and regenerate npm-shrinkwrap.json 2015-04-16 17:48:59 +02:00
Raul Ochoa
a0a455b225 Stubs next version 2015-04-15 16:13:40 +02:00
Raul Ochoa
0019ab495b Release 2.1.2 2015-04-15 16:12:42 +02:00
Raul Ochoa
cbebac1cb1 Merge pull request #286 from CartoDB/profiler-metrics
Profiler metrics improvements
2015-04-15 15:41:12 +02:00
Raul Ochoa
e2fd4aca60 Upgrade windshaft 2015-04-15 15:32:10 +02:00
Raul Ochoa
0c578a193c Remove stack for debug environment option 2015-04-14 16:44:03 +02:00
Raul Ochoa
84f579f0ec Do not add x-profiler header as it's already added by sendResponse 2015-04-14 16:41:04 +02:00
Raul Ochoa
1bf2809355 Do not check statsd_client in profiler 2015-04-14 16:40:15 +02:00
Raul Ochoa
e91bc91057 Adds test suite for x-cache-channel 2015-04-10 13:39:20 +02:00
Raul Ochoa
4f9b6be45b Update routes documentation 2015-04-10 12:00:25 +02:00
Raul Ochoa
95aa74ee34 Stubs next version 2015-04-10 10:57:31 +02:00
Raul Ochoa
e516300825 Release 2.1.1 2015-04-10 10:56:10 +02:00
Raul Ochoa
2d84d38b90 Do not add x-cache-channel header for GET template routes 2015-04-10 10:55:46 +02:00
Raul Ochoa
7cd78be094 Stubs next version 2015-04-09 15:26:21 +02:00
173 changed files with 22221 additions and 4433 deletions

4
.jshintignore Normal file
View File

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

View File

@@ -7,8 +7,8 @@
// // Enforcing
// "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
// "camelcase" : false, // true: Identifiers must be in camelCase
// "curly" : true, // true: Require {} for every new block or scope
// "eqeqeq" : true, // true: Require triple equals (===) for comparison
"curly" : true, // true: Require {} for every new block or scope
"eqeqeq" : true, // true: Require triple equals (===) for comparison
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
"freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc.
"immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
@@ -31,7 +31,7 @@
// "maxparams" : false, // {int} Max number of formal params allowed per function
// "maxdepth" : false, // {int} Max depth of nested blocks (within functions)
// "maxstatements" : false, // {int} Max number statements per function
"maxcomplexity" : 8, // {int} Max cyclomatic complexity per function
"maxcomplexity" : 6, // {int} Max cyclomatic complexity per function
"maxlen" : 120, // {int} Max number of characters per line
//
// // Relaxing
@@ -89,10 +89,6 @@
"after": true,
"beforeEach": true,
"afterEach": true,
"it": true,
"suite": true,
"suiteSetup": true,
"test": true,
"suiteTeardown": true
"it": true
}
}

11
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,11 @@
Contributing
---
The issue tracker is at [github.com/CartoDB/Windshaft-cartodb](https://github.com/CartoDB/Windshaft-cartodb).
We love pull requests from everyone, see [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/#contributing).
## Submitting Contributions
* You will need to sign a Contributor License Agreement (CLA) before making a submission. [Learn more here](https://cartodb.com/contributing).

View File

@@ -1,4 +1,4 @@
Copyright (c) 2014, Vizzuality
Copyright (c) 2015, CartoDB
All rights reserved.
Redistribution and use in source and binary forms, with or without

View File

@@ -18,18 +18,30 @@ config.status--test:
config/environments/test.js: config.status--test
./config.status--test
TEST_SUITE := $(shell find test/{acceptance,integration,unit} -name "*.js")
TEST_SUITE_UNIT := $(shell find test/unit -name "*.js")
TEST_SUITE_INTEGRATION := $(shell find test/integration -name "*.js")
TEST_SUITE_ACCEPTANCE := $(shell find test/acceptance -name "*.js")
test: config/environments/test.js
@echo "***tests***"
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} \
test/unit/cartodb/*.js \
test/unit/cartodb/cache/model/*.js \
test/integration/*.js \
test/acceptance/*.js \
test/acceptance/cache/*.js
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} $(TEST_SUITE)
test-unit: config/environments/test.js
@echo "***tests***"
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} $(TEST_SUITE_UNIT)
test-integration: config/environments/test.js
@echo "***tests***"
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} $(TEST_SUITE_INTEGRATION)
test-acceptance: config/environments/test.js
@echo "***tests***"
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} $(TEST_SUITE_ACCEPTANCE)
jshint:
@echo "***jshint***"
@./node_modules/.bin/jshint lib/ app.js
@./node_modules/.bin/jshint lib/ test/ app.js
test-all: jshint test

262
NEWS.md
View File

@@ -1,5 +1,267 @@
# Changelog
## 2.15.1
Released 2015-10-21
Announcements:
- Upgrades windshaft to [1.1.1](https://github.com/CartoDB/Windshaft/releases/tag/1.1.1)
## 2.15.0
Released 2015-10-13
Announcements:
- Fastly purging no longer uses soft-purge option
- Upgrades windshaft to [1.1.0](https://github.com/CartoDB/Windshaft/releases/tag/1.1.0)
- Upgrades fastly-purge to [1.0.1](https://github.com/CartoDB/node-fastly-purge/releases/tag/1.0.1)
## 2.14.1
Released 2015-09-30
Enhancements:
- Remove app dependency from controllers
Announcements:
- Upgrades windshaft to [1.0.1](https://github.com/CartoDB/Windshaft/releases/tag/1.0.1)
Improvements:
- Safer user extraction from request Host header
## 2.14.0
Released 2015-09-30
Summary: this starts using Windshaft as library (aka version 1.0.0), it no longer extends old Windshaft server.
Announcements:
- Upgrades windshaft to [1.0.0](https://github.com/CartoDB/Windshaft/releases/tag/1.0.0)
New features:
- Named tiles: /api/v1/map/named/:name/:layer/:z/:x/:y.:format
Ported from Windshaft pre-library:
- Almost all acceptance tests, some unit and some integration tests
- Stats + profiler
New features:
- Named maps MapConfig provider
- Base controller with: req2params, send response/error mechanisms
- Authentication/Authorization moves to its own API so it can be reused
- Surrogate-Key headers for named maps and affected tables
Improvements:
- No more fake requests to emulate map config instantiations
- As named maps previews are using named map MapConfigProvider it doesn't need to load the MapConfig
- Controllers using Windshaft's backends to request resources through MapConfig providers
- Express 4.x, as Windshaft no longer provides an HTTP server, here we start using latest major version of Express.
- assert.response implemented using request
- All tests validate there are no unexpected keys in Redis and keys requested to be deleted after a test are present
- Test suite in Makefile generated with `find`
- Image comparison with `mapnik.Image.compare`
- Doesn't emit Named map update event on unmodified templates
TODO:
- Named map provider checks on every request if named map has changed to reload it (actually reset it so MapConfig has to be regenerated). See https://github.com/CartoDB/Windshaft-cartodb/commit/f553efa69e83fdf296154ab1b7b49aa08957c04e. This is done this way because when running the Service in a cluster there is no communication between different instances so when a named map gets updated in one of the them the rest is not aware/notified of the change. In the future there should be a mechanism to synch this changes between instance:
* endpoint
* redis pub/sub
* backdoor
## 2.13.0
Released 2015-09-21
New features:
- Keep x-cache-channel in named map static maps
## 2.12.0
Released 2015-08-27
Announcements:
- Upgrades windshaft to [0.51.0](https://github.com/CartoDB/Windshaft/releases/tag/0.51.0)
New features:
- Make http and https globalAgent options configurable
* If config is not provided it configures them with default values
## 2.11.0
Released 2015-08-26
Announcements:
- Upgrades windshaft to [0.50.0](https://github.com/CartoDB/Windshaft/releases/tag/0.50.0)
## 2.10.0
Released 2015-08-18
New features:
- Exposes metatile cache configuration for tilelive-mapnik, see configuration sample files for more information.
Announcements:
- Upgrades windshaft to [0.49.0](https://github.com/CartoDB/Windshaft/releases/tag/0.49.0)
## 2.9.0
Released 2015-08-06
New features:
- Send memory usage stats
## 2.8.0
Released 2015-07-15
Announcements:
- Upgrades windshaft to [0.48.0](https://github.com/CartoDB/Windshaft/releases/tag/0.48.0)
## 2.7.2
Released 2015-07-14
Enhancements:
- Replaces `CDB_QueryTables` with `CDB_QueryTablesText` to avoid issues with long schema+table names
## 2.7.1
Released 2015-07-06
Bug fixes:
- redis-mpool `noReadyCheck` and `unwatchOnRelease` options from config and defaulted
## 2.7.0
Released 2015-07-06
Announcements:
- Upgrades windshaft to [0.47.0](https://github.com/CartoDB/Windshaft/releases/tag/0.47.0)
- Upgrades redis-mpool to [0.4.0](https://github.com/CartoDB/node-redis-mpool/releases/tag/0.4.0)
New features:
- Exposes redis `noReadyCheck` config
Bug fixes:
- Fixes `unwatchOnRelease` redis config
## 2.6.1
Released 2015-07-02
Announcements:
- Upgrades windshaft to [0.46.1](https://github.com/CartoDB/Windshaft/releases/tag/0.46.1)
## 2.6.0
Released 2015-07-02
Announcements:
- Upgrades windshaft to [0.46.0](https://github.com/CartoDB/Windshaft/releases/tag/0.46.0)
- New config to set metatile by format
## 2.5.0
Released 2015-06-18
New features:
- Named maps names can start with numbers and can contain dashes (-).
- Adds layergroupid header in map instantiations
Bug fixes:
- Named maps error responses with `{ "errors": ["message"] }` format (#305)
Announcements:
- Upgrades windshaft to [0.45.0](https://github.com/CartoDB/Windshaft/releases/tag/0.45.0)
Enhancements:
- Fix documentation style and error examples
## 2.4.1
Released 2015-06-01
Announcements:
- Upgrades windshaft to [0.44.1](https://github.com/CartoDB/Windshaft/releases/tag/0.44.1)
## 2.4.0
Released 2015-05-26
Announcements:
- Upgrades windshaft to [0.44.0](https://github.com/CartoDB/Windshaft/releases/tag/0.44.0)
## 2.3.0
Released 2015-05-18
Announcements:
- Upgrades cartodb-redis for `global` map stats
## 2.2.0
Released 2015-04-29
Enhancements:
- jshint is run against tests
- tests moved to mocha's `describe`
New features:
- Fastly surrogate keys invalidation for named maps
* **New configuration entry**: `fastly`. Check example configurations for more information.
- `PgQueryRunner` extracted from `QueryTablesApi` so it can be reused in new `TablesExtentApi`
- New top level element, `view`, in templates that holds attributes to identify the map scene.
- Named maps static preview in /api/v1/map/static/named/:name/:width/:height.:format endpoint
* It will be invalidated if the named map changes
* But have a Cache-Control header with a 2 hours max-age, won't be invalidated on data changes
## 2.1.3
Released 2015-04-16
Announcements:
- Upgrades windshaft to [0.42.2](https://github.com/CartoDB/Windshaft/releases/tag/0.42.2)
## 2.1.2
Released 2015-04-15
Bug fixes:
- Do not check statsd_client in profiler
Announcements:
- Upgrades windshaft to [0.42.1](https://github.com/CartoDB/Windshaft/releases/tag/0.42.1)
## 2.1.1
Released 2015-04-10
Bug fixes:
- Do not add x-cache-channel header for GET template routes
## 2.1.0
Released 2015-04-09

View File

@@ -11,8 +11,6 @@ This is the [CartoDB Maps API](http://docs.cartodb.com/cartodb-platform/maps-api
* gets the default geometry type from the cartodb redis store
* allows tiles to be styled individually
* provides a link to varnish high speed cache
* provides a ``infowindow`` endpoint for windshaft (DEPRECATED)
* provides a ``map_metadata`` endpoint for windshaft (DEPRECATED)
* provides a [template maps API](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Template-maps.md)
Requirements
@@ -27,7 +25,6 @@ Requirements
- libcairo2-dev, libpango1.0-dev, libjpeg8-dev and libgif-dev for server side canvas support
- For cache control (optional)
- CartoDB-SQL-API 1.0.0+
- CartoDB 0.9.5+ (for `CDB_QueryTables`)
- Varnish (http://www.varnish-cache.org)
@@ -59,6 +56,14 @@ happen to have startup errors you may need to force rebuilding those
modules. At any time just wipe out the node_modules/ directory and run
```npm install``` again.
Upgrading
---------
Checkout your commit/branch. If you need to reinstall dependencies (you can check [NEWS](NEWS.md)) do the following:
```
rm -rf node_modules; npm install
```
Run
---
@@ -87,3 +92,9 @@ Examples
--------
[CartoDB's Map Gallery](http://cartodb.com/gallery/) showcases several examples of visualisations built on top of this.
Contributing
---
See [CONTRIBUTING.md](CONTRIBUTING.md).

90
app.js
View File

@@ -1,49 +1,58 @@
/*
* Windshaft-CartoDB
* ===============
*
* ./app.js [environment]
*
* environments: [development, production]
*/
var http = require('http');
var https = require('https');
var path = require('path');
var fs = require('fs');
var RedisPool = require('redis-mpool');
var _ = require('underscore');
var ENV;
var ENVIRONMENT;
if ( process.argv[2] ) {
ENV = process.argv[2];
ENVIRONMENT = process.argv[2];
} else if ( process.env.NODE_ENV ) {
ENV = process.env.NODE_ENV;
ENVIRONMENT = process.env.NODE_ENV;
} else {
ENV = 'development';
ENVIRONMENT = 'development';
}
process.env.NODE_ENV = ENV;
var availableEnvironments = {
production: true,
staging: true,
development: true
};
// sanity check
if (ENV != 'development' && ENV != 'production' && ENV != 'staging' ){
console.error("\nnode app.js [environment]");
console.error("environments: development, production, staging\n");
if (!availableEnvironments[ENVIRONMENT]){
console.error('node app.js [environment]');
console.error('environments: %s', Object.keys(availableEnvironments).join(', '));
process.exit(1);
}
process.env.NODE_ENV = ENVIRONMENT;
// set environment specific variables
global.environment = require(__dirname + '/config/environments/' + ENV);
global.environment.api_hostname = require('os').hostname().split('.')[0];
global.environment = require('./config/environments/' + ENVIRONMENT);
global.log4js = require('log4js');
var log4js_config = {
appenders: [],
replaceConsole:true
replaceConsole: true
};
if (global.environment.uv_threadpool_size) {
process.env.UV_THREADPOOL_SIZE = global.environment.uv_threadpool_size;
}
// set global HTTP and HTTPS agent default configurations
// ref https://nodejs.org/api/http.html#http_new_agent_options
var agentOptions = _.defaults(global.environment.httpAgent || {}, {
keepAlive: false,
keepAliveMsecs: 1000,
maxSockets: Infinity,
maxFreeSockets: 256
});
http.globalAgent = new http.Agent(agentOptions);
https.globalAgent = new https.Agent(agentOptions);
if ( global.environment.log_filename ) {
var logdir = path.dirname(global.environment.log_filename);
// See cwd inlog4js.configure call below
@@ -65,42 +74,39 @@ if ( global.environment.log_filename ) {
global.log4js.configure(log4js_config, { cwd: __dirname });
global.logger = global.log4js.getLogger();
var redisOpts = _.extend(global.environment.redis, { name: 'windshaft' }),
redisPool = new RedisPool(redisOpts);
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/cartodb_windshaft'),
serverOptions = require('./lib/cartodb/server_options')(redisPool);
var cartodbWindshaft = require('./lib/cartodb/server');
var serverOptions = require('./lib/cartodb/server_options');
var ws = cartodbWindshaft(serverOptions);
if (global.statsClient) {
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);
});
}
var server = cartodbWindshaft(serverOptions);
// Maximum number of connections for one process
// 128 is a good number if you have up to 1024 filedescriptors
// 4 is good if you have max 32 filedescriptors
// 1 is good if you have max 16 filedescriptors
ws.maxConnections = global.environment.maxConnections || 128;
var backlog = global.environment.maxConnections || 128;
ws.listen(global.environment.port, global.environment.host);
var listener = server.listen(serverOptions.bind.port, serverOptions.bind.host, backlog);
var version = require("./package").version;
ws.on('listening', function() {
console.log(
"Windshaft tileserver %s started on %s:%s (%s)",
version, global.environment.host, global.environment.port, ENV
);
listener.on('listening', function() {
console.log(
"Windshaft tileserver %s started on %s:%s PID=%d (%s)",
version, serverOptions.bind.host, serverOptions.bind.port, process.pid, ENVIRONMENT
);
});
setInterval(function() {
var memoryUsage = process.memoryUsage();
Object.keys(memoryUsage).forEach(function(k) {
global.statsClient.gauge('windshaft.memory.' + k, memoryUsage[k]);
});
}, 5000);
process.on('SIGHUP', function() {
global.log4js.clearAndShutdownAppenders(function() {
global.log4js.configure(log4js_config);

View File

@@ -2,6 +2,9 @@ var config = {
environment: 'development'
,port: 8181
,host: '127.0.0.1'
// Size of the threadpool which can be used to run user code and get notified in the loop thread
// Its default size is 4, but it can be changed at startup time (the absolute maximum is 128).
// See http://docs.libuv.org/en/latest/threadpool.html
,uv_threadpool_size: undefined
// Regular expression pattern to extract username
// from hostname. Must have a single grabbing block.
@@ -86,8 +89,10 @@ var config = {
cache_ttl: 60000,
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
mapnik: {
// The size of the pool of internal mapnik renderers
// Check the configuration of uv_threadpool_size to use suitable value
// The size of the pool of internal mapnik backend
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
// See https://github.com/CartoDB/Windshaft/blob/master/lib/windshaft/renderers/renderer_factory.js
// Important: check the configuration of uv_threadpool_size to use suitable value
poolSize: 8,
// Metatile is the number of tiles-per-side that are going
@@ -96,6 +101,23 @@ var config = {
// wasted time.
metatile: 2,
// tilelive-mapnik uses an internal cache to store tiles/grids
// generated when using metatile. This options allow to tune
// the behaviour for that internal cache.
metatileCache: {
// Time an object must stay in the cache until is removed
ttl: 0,
// Whether an object must be removed after the first hit
// Usually you want to use `true` here when ttl>0.
deleteOnHit: false
},
// Override metatile behaviour depending on the format
formatMetatile: {
png: 2,
'grid.json': 1
},
// Buffer size is the tickness in pixel of a buffer
// around the rendered (meta?)tile.
//
@@ -131,6 +153,7 @@ var config = {
timeout: 2000, // the timeout in ms for a http tile request
proxy: undefined, // the url for a proxy server
whitelist: [ // the whitelist of urlTemplates that can be used
'.*', // will enable any URL
'http://{s}.example.com/{z}/{x}/{y}.png'
],
// image to use as placeholder when urlTemplate is not in the whitelist
@@ -139,6 +162,16 @@ var config = {
type: 'fs', // 'fs' and 'url' supported
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
}
}
}
,millstone: {
@@ -169,7 +202,16 @@ var config = {
},
emitter: {
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
}
},
unwatchOnRelease: false, // Send unwatch on release, see http://github.com/CartoDB/Windshaft-cartodb/issues/161
noReadyCheck: true // Check `no_ready_check` at https://github.com/mranney/node_redis/tree/v0.12.1#overloading
}
// For more details about this options check https://nodejs.org/api/http.html#http_new_agent_options
,httpAgent: {
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 25,
maxFreeSockets: 256
}
,varnish: {
host: 'localhost',
@@ -180,6 +222,15 @@ var config = {
ttl: 86400,
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
}
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
,fastly: {
// whether the invalidation is enabled or not
enabled: false,
// the fastly api key
apiKey: 'wadus_api_key',
// the service that will get surrogate key invalidation
serviceId: 'wadus_service_id'
}
// If useProfiler is true every response will be served with an
// X-Tiler-Profile header containing elapsed timing for various
// steps taken for producing the response.

View File

@@ -2,6 +2,9 @@ var config = {
environment: 'production'
,port: 8181
,host: '127.0.0.1'
// Size of the threadpool which can be used to run user code and get notified in the loop thread
// Its default size is 4, but it can be changed at startup time (the absolute maximum is 128).
// See http://docs.libuv.org/en/latest/threadpool.html
,uv_threadpool_size: undefined
// Regular expression pattern to extract username
// from hostname. Must have a single grabbing block.
@@ -80,8 +83,10 @@ var config = {
cache_ttl: 60000,
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
mapnik: {
// The size of the pool of internal mapnik renderers
// Check the configuration of uv_threadpool_size to use suitable value
// The size of the pool of internal mapnik backend
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
// See https://github.com/CartoDB/Windshaft/blob/master/lib/windshaft/renderers/renderer_factory.js
// Important: check the configuration of uv_threadpool_size to use suitable value
poolSize: 8,
// Metatile is the number of tiles-per-side that are going
@@ -90,6 +95,23 @@ var config = {
// wasted time.
metatile: 2,
// tilelive-mapnik uses an internal cache to store tiles/grids
// generated when using metatile. This options allow to tune
// the behaviour for that internal cache.
metatileCache: {
// Time an object must stay in the cache until is removed
ttl: 0,
// Whether an object must be removed after the first hit
// Usually you want to use `true` here when ttl>0.
deleteOnHit: false
},
// Override metatile behaviour depending on the format
formatMetatile: {
png: 2,
'grid.json': 1
},
// Buffer size is the tickness in pixel of a buffer
// around the rendered (meta?)tile.
//
@@ -125,6 +147,7 @@ var config = {
timeout: 2000, // the timeout in ms for a http tile request
proxy: undefined, // the url for a proxy server
whitelist: [ // the whitelist of urlTemplates that can be used
'.*', // will enable any URL
'http://{s}.example.com/{z}/{x}/{y}.png'
],
// image to use as placeholder when urlTemplate is not in the whitelist
@@ -133,6 +156,16 @@ var config = {
type: 'fs', // 'fs' and 'url' supported
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
}
}
}
,millstone: {
@@ -163,7 +196,16 @@ var config = {
},
emitter: {
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
}
},
unwatchOnRelease: false, // Send unwatch on release, see http://github.com/CartoDB/Windshaft-cartodb/issues/161
noReadyCheck: true // Check `no_ready_check` at https://github.com/mranney/node_redis/tree/v0.12.1#overloading
}
// For more details about this options check https://nodejs.org/api/http.html#http_new_agent_options
,httpAgent: {
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 25,
maxFreeSockets: 256
}
,varnish: {
host: 'localhost',
@@ -174,6 +216,15 @@ var config = {
ttl: 86400,
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
}
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
,fastly: {
// whether the invalidation is enabled or not
enabled: false,
// the fastly api key
apiKey: 'wadus_api_key',
// the service that will get surrogate key invalidation
serviceId: 'wadus_service_id'
}
// If useProfiler is true every response will be served with an
// X-Tiler-Profile header containing elapsed timing for various
// steps taken for producing the response.

View File

@@ -2,6 +2,9 @@ var config = {
environment: 'production'
,port: 8181
,host: '127.0.0.1'
// Size of the threadpool which can be used to run user code and get notified in the loop thread
// Its default size is 4, but it can be changed at startup time (the absolute maximum is 128).
// See http://docs.libuv.org/en/latest/threadpool.html
,uv_threadpool_size: undefined
// Regular expression pattern to extract username
// from hostname. Must have a single grabbing block.
@@ -80,8 +83,10 @@ var config = {
cache_ttl: 60000,
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
mapnik: {
// The size of the pool of internal mapnik renderers
// Check the configuration of uv_threadpool_size to use suitable value
// The size of the pool of internal mapnik backend
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
// See https://github.com/CartoDB/Windshaft/blob/master/lib/windshaft/renderers/renderer_factory.js
// Important: check the configuration of uv_threadpool_size to use suitable value
poolSize: 8,
// Metatile is the number of tiles-per-side that are going
@@ -90,6 +95,23 @@ var config = {
// wasted time.
metatile: 2,
// tilelive-mapnik uses an internal cache to store tiles/grids
// generated when using metatile. This options allow to tune
// the behaviour for that internal cache.
metatileCache: {
// Time an object must stay in the cache until is removed
ttl: 0,
// Whether an object must be removed after the first hit
// Usually you want to use `true` here when ttl>0.
deleteOnHit: false
},
// Override metatile behaviour depending on the format
formatMetatile: {
png: 2,
'grid.json': 1
},
// Buffer size is the tickness in pixel of a buffer
// around the rendered (meta?)tile.
//
@@ -125,6 +147,7 @@ var config = {
timeout: 2000, // the timeout in ms for a http tile request
proxy: undefined, // the url for a proxy server
whitelist: [ // the whitelist of urlTemplates that can be used
'.*', // will enable any URL
'http://{s}.example.com/{z}/{x}/{y}.png'
],
// image to use as placeholder when urlTemplate is not in the whitelist
@@ -133,6 +156,16 @@ var config = {
type: 'fs', // 'fs' and 'url' supported
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
}
}
}
,millstone: {
@@ -163,7 +196,16 @@ var config = {
},
emitter: {
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
}
},
unwatchOnRelease: false, // Send unwatch on release, see http://github.com/CartoDB/Windshaft-cartodb/issues/161
noReadyCheck: true // Check `no_ready_check` at https://github.com/mranney/node_redis/tree/v0.12.1#overloading
}
// For more details about this options check https://nodejs.org/api/http.html#http_new_agent_options
,httpAgent: {
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 25,
maxFreeSockets: 256
}
,varnish: {
host: 'localhost',
@@ -174,6 +216,15 @@ var config = {
ttl: 86400,
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
}
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
,fastly: {
// whether the invalidation is enabled or not
enabled: false,
// the fastly api key
apiKey: 'wadus_api_key',
// the service that will get surrogate key invalidation
serviceId: 'wadus_service_id'
}
// If useProfiler is true every response will be served with an
// X-Tiler-Profile header containing elapsed timing for various
// steps taken for producing the response.

View File

@@ -2,6 +2,9 @@ var config = {
environment: 'test'
,port: 8888
,host: '127.0.0.1'
// Size of the threadpool which can be used to run user code and get notified in the loop thread
// Its default size is 4, but it can be changed at startup time (the absolute maximum is 128).
// See http://docs.libuv.org/en/latest/threadpool.html
,uv_threadpool_size: undefined
// Regular expression pattern to extract username
// from hostname. Must have a single grabbing block.
@@ -80,8 +83,10 @@ var config = {
cache_ttl: 60000,
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
mapnik: {
// The size of the pool of internal mapnik renderers
// Check the configuration of uv_threadpool_size to use suitable value
// The size of the pool of internal mapnik backend
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
// See https://github.com/CartoDB/Windshaft/blob/master/lib/windshaft/renderers/renderer_factory.js
// Important: check the configuration of uv_threadpool_size to use suitable value
poolSize: 8,
// Metatile is the number of tiles-per-side that are going
@@ -90,6 +95,23 @@ var config = {
// wasted time.
metatile: 2,
// tilelive-mapnik uses an internal cache to store tiles/grids
// generated when using metatile. This options allow to tune
// the behaviour for that internal cache.
metatileCache: {
// Time an object must stay in the cache until is removed
ttl: 0,
// Whether an object must be removed after the first hit
// Usually you want to use `true` here when ttl>0.
deleteOnHit: false
},
// Override metatile behaviour depending on the format
formatMetatile: {
png: 2,
'grid.json': 1
},
// Buffer size is the tickness in pixel of a buffer
// around the rendered (meta?)tile.
//
@@ -125,6 +147,7 @@ var config = {
timeout: 2000, // the timeout in ms for a http tile request
proxy: undefined, // the url for a proxy server
whitelist: [ // the whitelist of urlTemplates that can be used
'.*', // will enable any URL
'http://{s}.example.com/{z}/{x}/{y}.png',
// for testing purposes
'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png'
@@ -135,6 +158,16 @@ var config = {
type: 'fs', // 'fs' and 'url' supported
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
}
}
}
,millstone: {
@@ -165,7 +198,16 @@ var config = {
},
emitter: {
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
}
},
unwatchOnRelease: false, // Send unwatch on release, see http://github.com/CartoDB/Windshaft-cartodb/issues/161
noReadyCheck: true // Check `no_ready_check` at https://github.com/mranney/node_redis/tree/v0.12.1#overloading
}
// For more details about this options check https://nodejs.org/api/http.html#http_new_agent_options
,httpAgent: {
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 25,
maxFreeSockets: 256
}
,varnish: {
host: '',
@@ -176,6 +218,15 @@ var config = {
ttl: 86400,
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
}
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
,fastly: {
// whether the invalidation is enabled or not
enabled: false,
// the fastly api key
apiKey: 'wadus_api_key',
// the service that will get surrogate key invalidation
serviceId: 'wadus_service_id'
}
// If useProfiler is true every response will be served with an
// X-Tiler-Profile header containing elapsed timing for various
// steps taken for producing the response.

10
configure vendored
View File

@@ -20,7 +20,6 @@
ENVDIR=config/environments
PGPORT=
SQLAPI_PORT=
MAPNIK_VERSION=
ENVIRONMENT=development
@@ -32,7 +31,6 @@ usage() {
echo "Configuration:"
echo " --help display this help and exit"
echo " --with-pgport=NUM access PostgreSQL server on TCP port NUM [$PGPORT]"
echo " --with-sqlapi-port=NUM access SQL-API server on TCP port NUM [$SQLAPI_PORT]"
echo " --with-mapnik-version=STRING set mapnik version string [$MAPNIK_VERSION]"
echo " --environment=STRING set output environment name [$ENVIRONMENT]"
}
@@ -46,9 +44,6 @@ while test -n "$1"; do
--with-pgport=*)
PGPORT=`echo "$1" | cut -d= -f2`
;;
--with-sqlapi-port=*)
SQLAPI_PORT=`echo "$1" | cut -d= -f2`
;;
--with-mapnik-version=*)
MAPNIK_VERSION=`echo "$1" | cut -d= -f2`
;;
@@ -67,12 +62,8 @@ ENVEX=./${ENVDIR}/${ENVIRONMENT}.js.example
if [ -z "$PGPORT" ]; then
PGPORT=`node -e "console.log(require('${ENVEX}').postgres.port)"`
fi
if [ -z "$SQLAPI_PORT" ]; then
SQLAPI_PORT=`node -e "console.log(require('${ENVEX}').sqlapi.port)"`
fi
echo "PGPORT: $PGPORT"
echo "SQLAPI_PORT: $SQLAPI_PORT"
echo "MAPNIK_VERSION: $MAPNIK_VERSION"
echo "ENVIRONMENT: $ENVIRONMENT"
@@ -82,7 +73,6 @@ echo "Writing $o"
# See http://austinmatzko.com/2008/04/26/sed-multi-line-search-and-replace/
sed -n "1h;1!H;\${;g;s/\(,postgres: {[^}]*port: *'\?\)[^',]*\('\?,\)/\1$PGPORT\2/;p;}" < "${ENVEX}" \
| sed "s/mapnik_version:.*/mapnik_version: '$MAPNIK_VERSION'/" \
| sed -n "1h;1!H;\${;g;s/\(,sqlapi: {[^}]*port: *'\?\)[^',]*\('\?,\)/\1$SQLAPI_PORT\2/;p;}" \
> "$o"
STATUSFILE=config.status--${ENVIRONMENT}

View File

@@ -1,111 +0,0 @@
# Kind of maps
Windshaft-CartoDB supports the following types of maps:
- [Temporary maps](#temporary-maps) (created by anyone)
- [Detached maps](#detached-maps)
- [Inline maps](#inline-maps) (legacy)
- [Persistent maps](#peristent-maps) (created by CartDB user)
- [Template maps](#template-maps)
- [Table maps](#table-maps) (legacy, deprecated)
## Temporary maps
Temporary maps have no owners and are anonymous in nature.
There are two kinds of temporary maps:
- Detached maps (aka MultiLayer-API)
- Inline maps
### Detached maps
Detached maps are maps that are configured with a request
obtaining a temporary token and then used by referencing
the obtained token. The token expires automatically when unused.
Anyone can create detached maps, but users will need read access
to the data source of the map layers.
The configuration format is a [MapConfig]
(http://github.com/CartoDB/Windshaft/wiki/MapConfig-specification) document.
The HTTP endpoints for creating the map and using it are described [here]
(http://github.com/CartoDB/Windshaft-cartodb/wiki/MultiLayer-API)
*TODO* cleanup the referenced document
### Inline maps
Inline maps are maps that only exist for a single request,
being the request for a specific map resource (tile).
Inline maps are always bound to a table, and can only be
obtained by those having read access to the that table.
Additionally, users need to have access to any datasource
specified as part of the configuration.
Inline maps only support PNG and UTF8GRID tiles.
The configuration consist in a set of parameters, to be
specified in the query string of the tile request:
* sql - the query to run as datasource, can be an array
* style - the CartoCSS style for the datasource, can be an array
* style_version - version of the CartoCSS style, can be an array
* interactivity - only for fetching UTF8GRID,
If the style is not provided, style of the associated table is
used; if the sql is not provided, all records of the associated
table are used as the datasource; the two possibilities result
in a mix between _inline_ maps and [Table maps][].
*TODO* specify (or link) api endpoints
## Persistent maps
Persistent maps can only be created by a CartoDB user who has full
responsibility over editing and deleting them. There are two
kind of persistent maps:
- Template maps
- Table maps (legacy, deprecated)
### Templated maps
Templated maps are templated [MapConfig]
(http://github.com/CartoDB/Windshaft/wiki/MapConfig-specification) documents
associated with an authorization certificate.
The authorization certificate determines who can instanciate the
template and use the resulting map. Authorized users of the instanciated
maps will have the same database access privilege of the template owner.
The HTTP endpoints for creating and using templated maps are described [here]
(http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps).
*TODO* cleanup the referenced document
### Table maps
Table maps are maps associated with a table.
Configuration of such maps is limited to the CartoCSS style.
* style - the CartoCSS style for the datasource, can be an array
* style_version - version of the CartoCSS style, can be an array
You can only fetch PNG or UTF8GRID tiles from these maps.
Access method is the same as the one for [Inline maps](#inline-maps)
# Endpoints description
- **/api/maps/** (same interface than https://github.com/CartoDB/Windshaft/wiki/Multilayer-API)
- **/api/maps/named** (same interface than https://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps)
NOTE: in case Multilayer-API does not contain this info yet, the
endpoint for fetching attributes is this:
- **/api/maps/:map_id/:layer_index/attributes/:feature_id**
- would return { c: 1, d: 2 }

View File

@@ -18,7 +18,7 @@ Here is an example of how to create an anonymous map with JavaScript:
```javascript
var mapconfig = {
"version": "1.0.1",
"version": "1.3.1",
"layers": [{
"type": "cartodb",
"options": {
@@ -57,7 +57,7 @@ The following map config sets up a map of European countries that have a white f
},
"layergroup": {
"layers": [{
"type": "cartodb",
"type": "mapnik",
"options": {
"cartocss_version": "2.1.1",
"cartocss": "#layer { polygon-fill: #FFF; }",
@@ -82,14 +82,23 @@ To get the `URL` to fetch the tiles you need to instantiate the map, where `temp
curl -X POST 'https://{account}.cartodb.com/api/v1/map/named/:template_id' -H 'Content-Type: application/json'
```
The response will return JSON with properties for the `layergroupid` and the timestamp (`last_updated`) of the last data modification.
The response will return JSON with properties for the `layergroupid`, the timestamp (`last_updated`) of the last data modification and some key/value pairs with `metadata` for the `layers`.
Note: all `layers` in `metadata` will always have a `type` string and a `meta` dictionary with the key/value pairs.
Here is an example response:
```javascript
{
"layergroupid": "c01a54877c62831bb51720263f91fb33:0",
"last_updated": "1970-01-01T00:00:00.000Z"
"last_updated": "1970-01-01T00:00:00.000Z",
"metadata": {
"layers": [
{
"type": "mapnik",
"meta": {}
}
]
}
}
```
@@ -144,9 +153,9 @@ POST /api/v1/map
```javascript
{
"version": "1.0.1",
"version": "1.3.0",
"layers": [{
"type": "cartodb",
"type": "mapnik",
"options": {
"cartocss_version": "2.1.1",
"cartocss": "#layer { polygon-fill: #FFF; }",
@@ -157,7 +166,7 @@ POST /api/v1/map
}
```
Should be a [Mapconfig](https://github.com/CartoDB/Windshaft/blob/0.19.1/doc/MapConfig-1.1.0.md).
Should be a [Mapconfig](https://github.com/CartoDB/Windshaft/blob/0.44.1/doc/MapConfig-1.3.0.md).
#### Response
@@ -173,8 +182,9 @@ The response includes:
- **updated_at**
The ISO date of the last time the data involved in the query was updated.
- **metadata** *(optional)*
Includes information about the layers. Some layers may not have metadata.
- **metadata**
Includes information about the layers.
-
- **cdn_url**
URLs to fetch the data using the best CDN for your zone.
@@ -189,8 +199,16 @@ curl 'https://documentation.cartodb.com/api/v1/map' -H 'Content-Type: applicatio
<div class="code-title">RESPONSE</div>
```javascript
{
"layergroupid":"c01a54877c62831bb51720263f91fb33:0",
"last_updated":"1970-01-01T00:00:00.000Z"
"layergroupid": "c01a54877c62831bb51720263f91fb33:0",
"last_updated": "1970-01-01T00:00:00.000Z",
"metadata": {
"layers": [
{
"type": "mapnik",
"meta": {}
}
]
},
"cdn_url": {
"http": "http://cdb.com",
"https": "https://cdb.com"
@@ -198,19 +216,35 @@ curl 'https://documentation.cartodb.com/api/v1/map' -H 'Content-Type: applicatio
}
```
The tiles can be accessed using:
##### Retrieve resources from the layergroup
###### Mapnik tiles can be accessed using:
These tiles will get just the mapnik layers. To get individual layers see next section.
```bash
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
```
For UTF grid tiles:
###### Individual layers
The MapConfig specification holds the layers definition in a 0-based index. Layers can be requested individually in different formats depending on the layer type.
Individual layers can be accessed using that 0-based index. For UTF grid tiles:
```bash
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/{z}/{x}/{y}.grid.json
```
For attributes defined in `attributes` section:
In this case, `:layer` as 0 returns the UTF grid tiles/attributes for layer 0, the only layer in the example MapConfig.
If the MapConfig had a Torque layer at index 1 it could be possible to request it with:
```bash
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/1/{z}/{x}/{y}.torque.json
```
###### Attributes defined in `attributes` section:
```bash
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/attributes/:feature_id
@@ -219,10 +253,44 @@ https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/
Which returns JSON with the attributes defined, like:
```javascript
{ c: 1, d: 2 }
{ "c": 1, "d": 2 }
```
Notice UTF Grid and attributes endpoints need an integer parameter, ``layer``. That number is the 0-based index of the layer inside the mapconfig. In this case, 0 returns the UTF grid tiles/attributes for layer 0, the only layer in the example mapconfig. If a second layer was available it could be returned with 1, a third layer with 2, etc.
###### Blending and layer selection
```bash
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer_filter/{z}/{x}/{y}.png
```
Note: currently format is limited to `png`.
`:layer_filter` can be used to select some layers to be rendered together. `:layer_filter` supports two formats:
- `all` alias
Using `all` as `:layer_filter` will blend all layers in the layergroup
```bash
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/all/{z}/{x}/{y}.png
```
- Filter by layer index
A list of comma separated layer indexes can be used to just render a subset of layers. For example `0,3,4` will filter and blend layers with indexes 0, 3, and 4.
```bash
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/0,3,4/{z}/{x}/{y}.png
```
Some notes about filtering:
- Invalid index values or out of bounds indexes will end in `Invalid layer filtering` errors.
- Once a mapnik layer is selected, all mapnik layers will get blended. As this may change in the future **it is
recommended** to always select all mapnik layers if you want to select at least one so you will get a consistent
behavior in the future.
- Ordering is not considered. So right now filtering layers 0,3,4 is the very same thing as filtering 3,4,0. As this
may change in the future **it is recommended** to always select the layers in ascending order so you will get a
consistent behavior in the future.
### Create JSONP
@@ -272,7 +340,7 @@ Anonymous maps cannot be removed by an API call. They will expire after about fi
## Named Maps
Named maps are essentially the same as anonymous maps except the mapconfig is stored on the server and the map is given a unique name. Two other big differences are: you can create named maps from private data and that users without an API Key can see them even though they are from that private data.
Named maps are essentially the same as anonymous maps except the MapConfig is stored on the server and the map is given a unique name. Two other big differences are: you can create named maps from private data and that users without an API Key can see them even though they are from that private data.
The main two differences compared to anonymous maps are:
@@ -280,7 +348,7 @@ The main two differences compared to anonymous maps are:
This allows you to control who is able to see the map based on a token auth
- **templates**
Since the mapconfig is static it can contain some variables so the client can modify the map's appearance using those variables.
Since the MapConfig is static it can contain some variables so the client can modify the map's appearance using those variables.
Template maps are persistent with no preset expiration. They can only be created or deleted by a CartoDB user with a valid API_KEY (see auth section).
@@ -331,18 +399,41 @@ POST /api/v1/map/named
}
}
]
},
"view": {
"zoom": 4,
"center": {
"lng": 0,
"lat": 0
},
"bounds": {
"west": -45,
"south": -45,
"east": 45,
"north": 45
}
}
}
```
##### Arguments
- **name**: There can be at most _one_ template with the same name for any user. Valid names start with a letter, and only contain letters, numbers, or underscores (_).
- **name**: There can be at most _one_ template with the same name for any user. Valid names start with a letter or a number, and only contain letters, numbers, dashes (-) or underscores (_).
- **auth**:
- **method** `"token"` or `"open"` (the default if no `"method"` is given).
- **valid_tokens** when `"method"` is set to `"token"`, the values listed here allow you to instantiate the named map.
- **placeholders**: Variables not listed here are not substituted. Variables not provided at instantiation time trigger an error. A default is required for optional variables. Type specification is used for quoting, to avoid injections see template format section below.
- **layergroup**: the layer list definition. This is the MapConfig explained in anonymous maps. See [MapConfig documentation](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.1.0.md) for more info.
- **layergroup**: the layer list definition. This is the MapConfig explained in anonymous maps. See [MapConfig documentation](https://github.com/CartoDB/Windshaft/blob/0.44.1/doc/MapConfig-1.3.0.md) for more info.
- **view** (optional): extra keys to specify the compelling area for the map. It can be used to have a static preview of a named map without having to instantiate it. It is possible to specify it with `center` + `zoom` or with a bounding box `bbox`. Center+zoom takes precedence over bounding box.
- **zoom** The zoom level to use
- **center**
- **lng** The longitude to use for the center
- **lat** The latitude to use for the center
- **bounds**
- **west**: LowerCorner longitude for the bounding box, in decimal degrees (aka most western)
- **south**: LowerCorner latitude for the bounding box, in decimal degrees (aka most southern)
- **east**: UpperCorner longitude for the bounding box, in decimal degrees (aka most eastern)
- **north**: UpperCorner latitude for the bounding box, in decimal degrees (aka most northern)
#### Template Format
@@ -438,7 +529,7 @@ curl -X POST \
<div class="code-title">Error</div>
```javascript
{
"error": "Some error string here"
"errors" : ["Some error string here"]
}
```
@@ -539,7 +630,7 @@ If a template with the same name does NOT exist, a 400 HTTP response is generate
```javascript
{
"error": "error string here"
"errors" : ["error string here"]
}
```
@@ -568,7 +659,7 @@ curl -X DELETE 'https://documentation.cartodb.com/api/v1/map/named/:template_nam
<div class="code-title">RESPONSE</div>
```javascript
{
"error": "Some error string here"
"errors" : ["Some error string here"]
}
```
@@ -606,7 +697,7 @@ curl -X GET 'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
<div class="code-title">ERROR</div>
```javascript
{
"error": "Some error string here"
"errors" : ["Some error string here"]
}
```
@@ -642,7 +733,7 @@ curl -X GET 'https://documentation.cartodb.com/api/v1/map/named/:template_name?a
<div class="code-title">ERROR</div>
```javascript
{
"error": "Some error string here"
"errors" : ["Some error string here"]
}
```
@@ -672,13 +763,15 @@ cartodb.createLayer('map_dom_id',layerSource)
1. [layer.setParams()](http://docs.cartodb.com/cartodb-platform/cartodb-js.html#layersetparamskey-value) allows you to change the template variables (in the placeholders object) via JavaScript
2. [layer.setAuthToken()](http://docs.cartodb.com/cartodb-platform/cartodb-js.html#layersetauthtokenauthtoken) allows you to set the auth tokens to create the layer
##Static Maps API
## Static Maps API
The Static Maps API can be initiated using both named and anonymous maps using the 'layergroupid' token. The API can be used to create static images of parts of maps and thumbnails for use in web design, graphic design, print, field work, and many other applications that require standard image formats.
### Maps API endpoints
Begin by instantiating either a named or anonymous map using the `layergroupid token` as demonstrated in the Maps API documentation above. The `layergroupsid token` calls to the map and allows for parameters in the definition to generate static images.
Begin by instantiating either a named or anonymous map using the `layergroupid token` as demonstrated in the Maps API documentation above. The `layergroupid` token calls to the map and allows for parameters in the definition to generate static images.
#### Zoom + center
##### Definition
@@ -726,6 +819,25 @@ Note: you can see this endpoint as:
GET /api/v1/map/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format`
```
#### Named map
##### Definition
<div class="code-title notitle code-request"></div>
```bash
GET /api/v1/map/static/named/:name/:width/:height.:format
```
##### Params
* **:name**: the name of the named map
* **:width**: the width in pixels for the output image
* **:height**: the height in pixels for the output image
* **:format**: the format for the image, supported types: `png`, `jpg`
* **jpg** will have a default quality of 85.
A named maps static image will get its constraints from the [view in the template](#Arguments), if `view` is not present it will estimate the extent based on the involved tables otherwise it fallback to `"zoom": 1`, `"lng": 0` and `"lat": 0`.
####Layers
The Static Maps API allows for multiple layers of incorporation into the `MapConfig` to allow for maximum versatility in creating a static map. The examples below were used to generate the static image example in the next section, and appear in the specific order designated.
@@ -768,6 +880,8 @@ By manipulating the `"urlTemplate"` custom basemaps can be used in generating st
**CartoDB**
As described in the [Mapconfig documentation](https://github.com/CartoDB/Windshaft/blob/0.44.1/doc/MapConfig-1.3.0.md), a "cartodb" type layer is now just an alias to a "mapnik" type layer as above, intended for backwards compatibility.
```javascript
{
"type": "cartodb",
@@ -779,14 +893,14 @@ By manipulating the `"urlTemplate"` custom basemaps can be used in generating st
},
```
Additoinally, static images from Torque maps and other map layers can be used together to generate highly customizable and versatile static maps.
Additionally, static images from Torque maps and other map layers can be used together to generate highly customizable and versatile static maps.
####Caching
#### Caching
It is important to note that generated images are cached from the live data referenced with the `layergroupid token` on the specified CartoDB account. This means that if the data changes, the cached image will also change. When linking dynamically, it is important to take into consideration the state of the data and longevity of the static image to avoid broken images or changes in how the image is displayed. To obtain a static snapshot of the map as it is today and preserve the image long-term regardless of changes in data, the image must be saved and stored locally.
####Limits
#### Limits
* While images can encompass an entirety of a map, the default limit for pixel range is 8192 x 8192.
* Image resolution by default is set to 72 DPI
@@ -803,16 +917,17 @@ After instantiating a map from a CartoDB account:
GET /api/v1/map/static/center/4b615ff367e498e770e7d05e99181873:1420231989550.8699/14/40.71502926732618/-73.96039009094238/600/400.png
```
####Response
<div clas="wrap"><p class="wrap-border"><img src="https://raw.githubusercontent.com/namessanti/Pictures/master/static_api.png" alt="static-api"/></p>,</div>
#### Response
####MapConfig
<p class="wrap-border"><img src="https://raw.githubusercontent.com/namessanti/Pictures/master/static_api.png" alt="static-api"/></p>
#### MapConfig
For this map, the multiple layers, order, and stylings are defined by the MapConfig.
```javascript
{
"version": "1.3.0-alpha",
"version": "1.3.0",
"layers": [
{
"type": "http",

View File

@@ -1,4 +1,4 @@
The Windshaft-CartoDB MultiLayer API extends the [Windshaft MultiLayer API](https://github.com/Vizzuality/Windshaft/wiki/Multilayer-API) in a few ways.
The Windshaft-CartoDB MultiLayer API extends the [Windshaft MultiLayer API](https://github.com/CartoDB/Windshaft/blob/master/doc/Multilayer-API.md) in a few ways.
## Last modification timestamp embedded in the token

View File

@@ -2,25 +2,25 @@ This document list all routes available in Windshaft-cartodb Maps API server.
## Routes list
1. `GET (?:/api/v1/map|/tiles/layergroup)/:token/:z/:x/:y@:scale_factor?x.:format {:token(f),:z(f),:x(f),:y(f),:scale_factor(t),:format(f)} (1)`
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:z/:x/:y@:scale_factor?x.:format {:user(f),:token(f),:z(f),:x(f),:y(f),:scale_factor(t),:format(f)} (1)`
<br/>Notes: Mapnik retina tiles [0]
1. `GET (?:/api/v1/map|/tiles/layergroup)/:token/:z/:x/:y.:format {:token(f),:z(f),:x(f),:y(f),:format(f)} (1)`
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:z/:x/:y.:format {:user(f),:token(f),:z(f),:x(f),:y(f),:format(f)} (1)`
<br/>Notes: Mapnik tiles [0]
1. `GET (?:/api/v1/map|/tiles/layergroup)/:token/:layer/:z/:x/:y.(:format) {:token(f),:layer(f),:z(f),:x(f),:y(f),:format(f)} (1)`
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:layer/:z/:x/:y.(:format) {:user(f),:token(f),:layer(f),:z(f),:x(f),:y(f),:format(f)} (1)`
<br/>Notes: Per :layer rendering based on :format [0]
1. `GET (?:/api/v1/map|/tiles/layergroup) {} (1)`
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup) {:user(f)} (1)`
<br/>Notes: Map instantiation [0]
1. `GET (?:/api/v1/map|/tiles/layergroup)/:token/:layer/attributes/:fid {:token(f),:layer(f),:fid(f)} (1)`
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:layer/attributes/:fid {:user(f),:token(f),:layer(f),:fid(f)} (1)`
<br/>Notes: Endpoint for info windows data, alternative for sql api when tables are private [0]
1. `GET (?:/api/v1/map|/tiles/layergroup)/static/center/:token/:z/:lat/:lng/:width/:height.:format {:token(f),:z(f),:lat(f),:lng(f),:width(f),:height(f),:format(f)} (1)`
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/static/center/:token/:z/:lat/:lng/:width/:height.:format {:user(f),:token(f),:z(f),:lat(f),:lng(f),:width(f),:height(f),:format(f)} (1)`
<br/>Notes: Static Maps API [0]
1. `GET (?:/api/v1/map|/tiles/layergroup)/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format {:token(f),:west(f),:south(f),:east(f),:north(f),:width(f),:height(f),:format(f)} (1)`
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format {:user(f),:token(f),:west(f),:south(f),:east(f),:north(f),:width(f),:height(f),:format(f)} (1)`
<br/>Notes: Static Maps API [0]
1. `GET / {} (1)`
@@ -29,66 +29,42 @@ This document list all routes available in Windshaft-cartodb Maps API server.
1. `GET /version {} (1)`
<br/>Notes: Return relevant module versions: mapnik, grainstore, etc
1. `GET /tiles/:table/:z/:x/:y.* {:table(f),:z(f),:x(f),:y(f)} (1)`
<br/>Notes: **[DEPRECATED]** Per :table tiles rendering
1. `GET /tiles/:table/style {:table(f)} (1)`
<br/>Notes: **[DEPRECATED]** Style for :table
1. `GET (?:/api/v1/map/named|/tiles/template)/:template_id/jsonp {:template_id(f)} (1)`
1. `GET (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id/jsonp {:user(f),:template_id(f)} (1)`
<br/>Notes: Named maps JSONP instantiation [1]
1. `GET (?:/api/v1/map/named|/tiles/template)/:template_id {:template_id(f)} (1)`
1. `GET (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
<br/>Notes: Named map retrieval (w/ API KEY) [1]
1. `GET (?:/api/v1/map/named|/tiles/template) {} (1)`
1. `GET (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template) {:user(f)} (1)`
<br/>Notes: List named maps (w/ API KEY) [1]
1. `GET /tiles/:table/infowindow {:table(f)} (1)`
<br/>Notes: **[DEPRECATED]** retrieve info window template for :table
1. `GET /tiles/:table/map_metadata {:table(f)} (1)`
<br/>Notes: **[DEPRECATED]** retrieve map metadata for :table
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/static/named/:template_id/:width/:height.:format {:user(f),:template_id(f),:width(f),:height(f),:format(f)} (1)`
<br/>Notes: Static map for named maps
1. `GET /health {} (1)`
<br/>Notes: Healt check
1. `OPTIONS (?:/api/v1/map|/tiles/layergroup) {} (1)`
1. `OPTIONS (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup) {:user(f)} (1)`
<br/>Notes: CORS [0]
1. `OPTIONS /tiles/:table/:z/:x/:y.* {:table(f),:z(f),:x(f),:y(f)} (1)`
<br/>Notes: **[DEPRECATED]** CORS
1. `OPTIONS /tiles/:table/style {:table(f)} (1)`
<br/>Notes: **[DEPRECATED]** CORS
1. `OPTIONS (?:/api/v1/map/named|/tiles/template)/:template_id {:template_id(f)} (1)`
1. `OPTIONS (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
<br/>Notes: CORS [1]
1. `POST (?:/api/v1/map|/tiles/layergroup) {} (1)`
1. `POST (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup) {:user(f)} (1)`
<br/>Notes: Map instantiation [0]
1. `POST /tiles/:table/style {:table(f)} (1)`
<br/>Notes: **[DEPRECATED]** Create style for :table
1. `POST (?:/api/v1/map/named|/tiles/template) {} (1)`
1. `POST (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template) {:user(f)} (1)`
<br/>Notes: Create named map (w/ API KEY) [1]
1. `POST (?:/api/v1/map/named|/tiles/template)/:template_id {:template_id(f)} (1)`
1. `POST (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
<br/>Notes: Instantiate named map [1]
1. `DELETE /tiles/:table/style {:table(f)} (1)`
<br/>Notes: **[DEPRECATED]** Delete :table style
1. `DELETE (?:/api/v1/map/named|/tiles/template)/:template_id {:template_id(f)} (1)`
<br/>Notes: Delete named map (w/ API KEY) [1]
1. `DELETE /tiles/:table/flush_cache {:table(f)} (1)`
<br/>Notes: **[DEPRECATED]** Flush internal caches for :table
1. `PUT (?:/api/v1/map/named|/tiles/template)/:template_id {:template_id(f)} (1)`
1. `PUT (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
<br/>Notes: Update a named map (w/ API KEY) [1]
1. `DELETE (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
<br/>Notes: Delete named map (w/ API KEY) [1]
## Optional deprecated routes
- [0] `/tiles/layergroup` is deprecated and `/api/v1/map` should be used but we keep it for now.
@@ -100,10 +76,10 @@ Something like the following patch should do the trick
```javascript
diff --git a/lib/cartodb/cartodb_windshaft.js b/lib/cartodb/cartodb_windshaft.js
index 477a4c2..f69eebb 100644
index b9429a2..e6cc5f9 100644
--- a/lib/cartodb/cartodb_windshaft.js
+++ b/lib/cartodb/cartodb_windshaft.js
@@ -242,6 +242,20 @@ var CartodbWindshaft = function(serverOptions) {
@@ -212,6 +212,20 @@ var CartodbWindshaft = function(serverOptions) {
}
});

View File

@@ -1,272 +0,0 @@
Template maps are layergroup configurations that rather than being
fully defined contain variables that can be set to produce a different
layergroup configurations (instantiation).
Template maps are persistent, can only be created and deleted by the
CartoDB user showing a valid API_KEY.
# Template format
A templated layergroup would allow using placeholders
in the "cartocss" and "sql" elements in the "option"
field of any "layer" of a layergroup configuration
(see https://github.com/CartoDB/Windshaft/wiki/MapConfig-specification).
Valid placeholder names start with a letter and can only
contain letters, numbers or underscores. They have to be
written between ``<%= `` and `` %>`` strings in order to be
replaced. Example: ``<%= my_color %>``.
The set of supported placeholders for a template will need to be
explicitly defined specifying type and default value for each.
**placeholder types**
Placeholder type will determine the kind of escaping for the
associated value. Supported types are:
* sql_literal (internal single-quotes will be sql-escaped)
* sql_ident (internal double-quotes will be sql-escaped)
* number (can only contain numerical representation)
* css_color (can only contain color names or hex-values)
* ... (add more as need arises)
Placeholder default value will be used when not provided at
instantiation time and could be used to test validity of the
template by creating a default instance.
```js
// template.json
{
version: "0.0.1",
// there can be at most 1 template with the same name for any user
// valid names start with a letter and only contains letter, numbers
// or underscores
name: "template_name",
// embedded authorization certificate
auth: {
method: "token", // or "open" (the default if no "method" is given)
// only (required and non empty) for "token" method
valid_tokens: ["auth_token1","auth_token2"]
},
// Variables not listed here are not substituted
// Variable not provided at instantiation time trigger an error
// A default is required for optional variables
// Type specification is used for quoting, to avoid injections
placeholders: {
color: {
type:"css_color",
default:"red"
},
cartodb_id: {
type:"number",
default: 1
}
},
layergroup: {
// see https://github.com/CartoDB/Windshaft/wiki/MapConfig-specification
"version": "1.0.1",
"layers": [{
"type": "cartodb",
"options": {
"cartocss_version": "2.1.1",
"cartocss": "#layer { polygon-fill: <%= color %>; }",
"sql": "select * from european_countries_e WHERE cartodb_id = <%= cartodb_id %>"
}
}]
}
}
```
# Creating a templated map
You can create a template map with a single call (for simplicity).
You'd use a POST sending JSON data:
```sh
curl -X POST \
-H 'Content-Type: application/json' \
-d @template.json \
'https://docs.cartodb.com/tiles/template?api_key=APIKEY'
```
The response would be like this:
```js
{
"template_id":"@template_name"
}
```
If a template with the same name exists in the user storage,
a 400 response is generated.
Errors are in this form:
```js
{
"error":"Some error string here"
}
```
# Updating an existing template
You can update a template map with a PUT:
```sh
curl -X PUT \
-H 'Content-Type: application/json' \
-d @template.json \
'https://docs.cartodb.com/tiles/template/:template_name?api_key=APIKEY'
```
A template with the same name will be updated, if any.
The response would be like this:
```js
{
"template_id":"@template_name"
}
```
If a template with the same name does NOT exist,
a 400 HTTP response is generated with an error in this format:
```js
{
"error":"Some error string here"
}
```
# Listing available templates
You can get a list of available templates with a GET to ``/template``.
A valid api_key is required.
```sh
curl -X GET 'https://docs.cartodb.com/tiles/template?api_key=APIKEY'
```
The response would be like this:
```js
{
"template_ids": ["@template_name1","@template_name2"]
}
```
Or, on error:
```js
{
"error":"Some error string here"
}
```
# Getting a specific template
You can get the definition of a template with a
GET to ``/template/:template_name``.
A valid api_key is required.
Example:
```sh
curl -X GET 'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
```
The response would be like this:
```js
{
"template": {...} // see template.json above
}
```
Or, on error:
```js
{
"error":"Some error string here"
}
```
# Instantiating a template map
You can instantiate a template map passing all required parameters with
a POST to ``/template/:template_name``.
Valid credentials will be needed, if required by the template.
```js
// params.js
{
color: '#ff0000',
cartodb_id: 3
}
```
```sh
curl -X POST \
-H 'Content-Type: application/json' \
-d @params.js \
'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
```
The response would be like this:
```js
{
"layergroupid":"docs@fd2861af@c01a54877c62831bb51720263f91fb33:123456788",
"last_updated":"2013-11-14T11:20:15.000Z"
}
```
or, on error:
```js
{
"error":"Some error string here"
}
```
You can then use the ``layergroupid`` for fetching tiles and grids as you do
normally ( see https://github.com/CartoDB/Windshaft/wiki/Multilayer-API).
But you'll still have to show the ``auth_token``, if required by the template.
### using JSONP
There is also a special endpoint to be able to instanciate using JSONP (for old browsers)
```
curl 'https://docs.cartodb.com/tiles/template/@template_name/jsonp?auth_token=AUTH_TOKEN&callback=function_name&config=template_params_json'
```
it takes the ``callback`` function (required), ``auth_token`` in case the template needs auth and ``config`` which is the variabñes for the template (in case it has variables). For example config may be created (using javascript)
```
url += "config=" + encodeURIComponent(
JSON.stringify({ color: 'red' });
```
the response it's in this format:
```
jQuery17205720721024554223_1390996319118(
{
layergroupid: "dev@744bd0ed9b047f953fae673d56a47b4d:1390844463021.1401",
last_updated: "2014-01-27T17:41:03.021Z"
}
)
```
# Deleting a template map
You can delete a templated map with a DELETE to ``/template/:template_name``:
```sh
curl -X DELETE 'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
```
On success, a 204 (No Content) response would be issued.
Otherwise a 4xx response with this format:
```js
{
"error":"Some error string here"
}
```

141
lib/cartodb/api/auth_api.js Normal file
View File

@@ -0,0 +1,141 @@
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 a request is authorized by a signer
//
// @param req express request object
// @param callback function(err, signed_by) signed_by will be
// null if the request is not signed by anyone
// or will be a string cartodb username otherwise.
//
AuthApi.prototype.authorizedBySigner = function(req, callback) {
if ( ! req.params.token || ! req.params.signer ) {
return callback(null, false); // no signer requested
}
var self = this;
var layergroup_id = req.params.token;
var auth_token = req.params.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 callback function(err, allowed) is access allowed not?
*/
AuthApi.prototype.authorize = function(req, callback) {
var self = this;
var user = req.context.user;
step(
function () {
self.authorizedByAPIKey(user, req, this);
},
function checkApiKey(err, authorized){
if (req.profiler) {
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(req, this);
}
// authorized by api key, login as the given username and stop
self.pgConnection.setDBAuth(user, req.params, 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 ( ! req.params.signer ) {
return callback(null, true); // authorized so far
}
// if signer name was given, return no authorization
return callback(null, false);
}
self.pgConnection.setDBAuth(user, req.params, function(err) {
if (req.profiler) {
req.profiler.done('setDBAuth');
}
callback(err, true); // authorized (or error)
});
}
);
};

View File

@@ -1,9 +1,5 @@
var PSQL = require('cartodb-psql');
var step = require('step');
function QueryTablesApi(pgConnection, metadataBackend) {
this.pgConnection = pgConnection;
this.metadataBackend = metadataBackend;
function QueryTablesApi(pgQueryRunner) {
this.pgQueryRunner = pgQueryRunner;
}
var affectedTableRegexCache = {
@@ -18,9 +14,9 @@ module.exports = QueryTablesApi;
QueryTablesApi.prototype.getAffectedTablesInQuery = function (username, sql, callback) {
var query = 'SELECT CDB_QueryTables($windshaft$' + prepareSql(sql) + '$windshaft$)';
var query = 'SELECT CDB_QueryTablesText($windshaft$' + prepareSql(sql) + '$windshaft$)';
this.runQuery(username, query, handleAffectedTablesInQueryRows, callback);
this.pgQueryRunner.run(username, query, handleAffectedTablesInQueryRows, callback);
};
function handleAffectedTablesInQueryRows(err, rows, callback) {
@@ -29,9 +25,9 @@ function handleAffectedTablesInQueryRows(err, rows, callback) {
callback(new Error('could not fetch source tables: ' + msg));
return;
}
var qtables = rows[0].cdb_querytables;
var tableNames = qtables.split(/^\{(.*)\}$/)[1];
tableNames = tableNames ? tableNames.split(',') : [];
// This is an Array, so no need to split into parts
var tableNames = rows[0].cdb_querytablestext;
callback(null, tableNames);
}
@@ -39,27 +35,27 @@ QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (usernam
var query = [
'WITH querytables AS (',
'SELECT * FROM CDB_QueryTables($windshaft$' + prepareSql(sql) + '$windshaft$) as tablenames',
'SELECT * FROM CDB_QueryTablesText($windshaft$' + prepareSql(sql) + '$windshaft$) as tablenames',
')',
'SELECT (SELECT tablenames FROM querytables), EXTRACT(EPOCH FROM max(updated_at)) as max',
'FROM CDB_TableMetadata m',
'WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])'
].join(' ');
this.runQuery(username, query, handleAffectedTablesAndLastUpdatedTimeRows, callback);
this.pgQueryRunner.run(username, query, handleAffectedTablesAndLastUpdatedTimeRows, callback);
};
function handleAffectedTablesAndLastUpdatedTimeRows(err, rows, callback) {
if (err || rows.length === 0) {
var msg = err.message ? err.message : err;
callback(new Error('could not fetch affected tables and last updated time: ' + msg));
callback(new Error('could not fetch affected tables or last updated time: ' + msg));
return;
}
var result = rows[0];
var tableNames = result.tablenames.split(/^\{(.*)\}$/)[1];
tableNames = tableNames ? tableNames.split(',') : [];
// This is an Array, so no need to split into parts
var tableNames = result.tablenames;
var lastUpdatedTime = result.max || 0;
@@ -69,42 +65,34 @@ function handleAffectedTablesAndLastUpdatedTimeRows(err, rows, callback) {
});
}
QueryTablesApi.prototype.getLastUpdatedTime = function (username, tableNames, callback) {
if (!Array.isArray(tableNames) || tableNames.length === 0) {
return callback(null, 0);
}
QueryTablesApi.prototype.runQuery = function(username, query, queryHandler, callback) {
var self = this;
var query = [
'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max',
'FROM CDB_TableMetadata m WHERE m.tabname = any (ARRAY[',
tableNames.map(function(t) { return "'" + t + "'::regclass"; }).join(','),
'])'
].join(' ');
var params = {};
step(
function setAuth() {
self.pgConnection.setDBAuth(username, params, this);
},
function setConn(err) {
if (err) {
throw err;
}
self.pgConnection.setDBConn(username, params, this);
},
function executeQuery(err) {
if (err) {
throw 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 || {};
var rows = resultSet.rows || [];
queryHandler(err, rows, callback);
});
}
);
this.pgQueryRunner.run(username, query, handleLastUpdatedTimeRows, callback);
};
function handleLastUpdatedTimeRows(err, rows, callback) {
if (err) {
var msg = err.message ? err.message : err;
return callback(new Error('could not fetch affected tables or last updated time: ' + msg));
}
// when the table has not updated_at means it hasn't been changed so a default last_updated is set
var lastUpdated = 0;
if (rows.length !== 0) {
lastUpdated = rows[0].max || 0;
}
return callback(null, lastUpdated*1000);
}
function prepareSql(sql) {
return sql

View File

@@ -0,0 +1,53 @@
function TablesExtentApi(pgQueryRunner) {
this.pgQueryRunner = pgQueryRunner;
}
module.exports = TablesExtentApi;
/**
* Given a username and a list of tables it will return the estimated extent in SRID 4326 for all the tables based on
* the_geom_webmercator (SRID 3857) column.
*
* @param {String} username
* @param {Array} tableNames The named can be schema qualified, so this accepts both `schema_name.table_name` and
* `table_name` format as valid input
* @param {Function} callback function(err, result) {Object} result with `west`, `south`, `east`, `north`
*/
TablesExtentApi.prototype.getBounds = function (username, tableNames, callback) {
var estimatedExtentSQLs = tableNames.map(function(tableName) {
var schemaTable = tableName.split('.');
if (schemaTable.length > 1) {
return "ST_EstimatedExtent('" + schemaTable[0] + "', '" + schemaTable[1] + "', 'the_geom_webmercator')";
}
return "ST_EstimatedExtent('" + schemaTable[0] + "', 'the_geom_webmercator')";
});
var query = [
"WITH ext as (" +
"SELECT ST_Transform(ST_SetSRID(ST_Extent(ST_Union(ARRAY[",
estimatedExtentSQLs.join(','),
"])), 3857), 4326) geom)",
"SELECT",
"ST_XMin(geom) west,",
"ST_YMin(geom) south,",
"ST_XMax(geom) east,",
"ST_YMax(geom) north",
"FROM ext"
].join(' ');
this.pgQueryRunner.run(username, query, handleBoundsResult, callback);
};
function handleBoundsResult(err, rows, callback) {
if (err) {
var msg = err.message ? err.message : err;
return callback(new Error('could not fetch source tables: ' + msg));
}
var result = null;
if (rows.length > 0) {
result = {
bounds: rows[0]
};
}
callback(null, result);
}

View File

@@ -0,0 +1,28 @@
/**
*
* @param metadataBackend
* @param options
* @constructor
* @type {UserLimitsApi}
*/
function UserLimitsApi(metadataBackend, options) {
this.metadataBackend = metadataBackend;
this.options = options || {};
this.options.limits = this.options.limits || {};
}
module.exports = UserLimitsApi;
UserLimitsApi.prototype.getRenderLimits = function (username, callback) {
var self = this;
this.metadataBackend.getTilerRenderLimit(username, function handleTilerLimits(err, renderLimit) {
if (err) {
return callback(err);
}
return callback(null, {
cacheOnTimeout: self.options.limits.cacheOnTimeout || false,
render: renderLimit || self.options.limits.render || 0
});
});
};

View File

@@ -1,3 +1,4 @@
var assert = require('assert');
var step = require('step');
var _ = require('underscore');
@@ -29,19 +30,21 @@ PgConnection.prototype.setDBAuth = function(username, params, callback) {
self.metadataBackend.getUserId(username, this);
},
function(err, user_id) {
if (err) throw err;
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;
if (!auth_pass || ! auth_pass.match(/\buser_password\b/) ) {
return null;
}
self.metadataBackend.getUserDBPass(username, this);
},
function(err, user_password) {
if (err) throw err;
assert.ifError(err);
user_params.user_password = user_password;
if ( auth_pass ) {
var dbpass = _.template(auth_pass, user_params);
@@ -81,12 +84,14 @@ PgConnection.prototype.setDBConn = function(dbowner, params, callback) {
self.metadataBackend.getUserDBConnectionParams(dbowner, this);
},
function extendParams(err, dbParams){
if (err) throw err;
assert.ifError(err);
// we don't want null values or overwrite a non public user
if (params.dbuser != 'publicuser' || !dbParams.dbuser) {
if (params.dbuser !== 'publicuser' || !dbParams.dbuser) {
delete dbParams.dbuser;
}
if ( dbParams ) _.extend(params, dbParams);
if ( dbParams ) {
_.extend(params, dbParams);
}
return null;
},
function finish(err) {

View File

@@ -0,0 +1,41 @@
var assert = require('assert');
var PSQL = require('cartodb-psql');
var step = require('step');
function PgQueryRunner(pgConnection) {
this.pgConnection = pgConnection;
}
module.exports = PgQueryRunner;
PgQueryRunner.prototype.run = function(username, query, queryHandler, 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 || {};
var rows = resultSet.rows || [];
queryHandler(err, rows, callback);
});
}
);
};

View File

@@ -1,4 +1,6 @@
var assert = require('assert');
var crypto = require('crypto');
var debug = require('debug')('windshaft:templates');
var step = require('step');
var _ = require('underscore');
var dot = require('dot');
@@ -21,7 +23,9 @@ var util = require('util');
//
//
function TemplateMaps(redis_pool, opts) {
if (!(this instanceof TemplateMaps)) return new TemplateMaps();
if (!(this instanceof TemplateMaps)) {
return new TemplateMaps();
}
EventEmitter.call(this);
@@ -76,29 +80,32 @@ o._redisCmd = function(redisFunc, redisArgs, callback) {
that.redis_pool.acquire(db, this);
},
function executeQuery(err, data) {
if ( err ) throw err;
assert.ifError(err);
redisClient = data;
redisArgs.push(this);
redisClient[redisFunc.toUpperCase()].apply(redisClient, redisArgs);
},
function releaseRedisClient(err, data) {
if ( ! _.isUndefined(redisClient) ) that.redis_pool.release(db, redisClient);
if ( ! _.isUndefined(redisClient) ) {
that.redis_pool.release(db, redisClient);
}
callback(err, data);
}
);
};
var _reValidIdentifier = /^[a-zA-Z][0-9a-zA-Z_]*$/;
var _reValidNameIdentifier = /^[a-z0-9][0-9a-z_\-]*$/i;
var _reValidPlaceholderIdentifier = /^[a-z][0-9a-z_]*$/i;
// jshint maxcomplexity:15
o._checkInvalidTemplate = function(template) {
if ( template.version != '0.0.1' ) {
if ( template.version !== '0.0.1' ) {
return new Error("Unsupported template version " + template.version);
}
var tplname = template.name;
if ( ! tplname ) {
return new Error("Missing template name");
}
if ( ! tplname.match(_reValidIdentifier) ) {
if ( ! tplname.match(_reValidNameIdentifier) ) {
return new Error("Invalid characters in template name '" + tplname + "'");
}
@@ -113,7 +120,7 @@ o._checkInvalidTemplate = function(template) {
for (var i = 0, len = placeholderKeys.length; i < len; i++) {
var placeholderKey = placeholderKeys[i];
if (!placeholderKey.match(_reValidIdentifier)) {
if (!placeholderKey.match(_reValidPlaceholderIdentifier)) {
return new Error("Invalid characters in placeholder name '" + placeholderKey + "'");
}
if ( ! placeholders[placeholderKey].hasOwnProperty('default') ) {
@@ -130,10 +137,12 @@ o._checkInvalidTemplate = function(template) {
case 'open':
break;
case 'token':
if ( ! _.isArray(auth.valid_tokens) )
if ( ! _.isArray(auth.valid_tokens) ) {
return new Error("Invalid 'token' authentication: missing valid_tokens");
if ( ! auth.valid_tokens.length )
}
if ( ! auth.valid_tokens.length ) {
return new Error("Invalid 'token' authentication: no valid_tokens");
}
break;
default:
return new Error("Unsupported authentication method: " + auth.method);
@@ -213,9 +222,7 @@ o.addTemplate = function(owner, template, callback) {
self._redisCmd('HLEN', [ userTemplatesKey ], this);
},
function installTemplateIfDoesNotExist(err, numberOfTemplates) {
if ( err ) {
throw err;
}
assert.ifError(err);
if ( limit && numberOfTemplates >= limit ) {
throw new Error("User '" + owner + "' reached limit on number of templates " +
"("+ numberOfTemplates + "/" + limit + ")");
@@ -223,9 +230,7 @@ o.addTemplate = function(owner, template, callback) {
self._redisCmd('HSETNX', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
},
function validateInstallation(err, wasSet) {
if ( err ) {
throw err;
}
assert.ifError(err);
if ( ! wasSet ) {
throw new Error("Template '" + templateName + "' of user '" + owner + "' already exists");
}
@@ -258,9 +263,7 @@ o.delTemplate = function(owner, tpl_id, callback) {
self._redisCmd('HDEL', [ self.key_usr_tpl({ owner:owner }), tpl_id ], this);
},
function handleDeletion(err, deleted) {
if (err) {
throw err;
}
assert.ifError(err);
if (!deleted) {
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
}
@@ -305,37 +308,38 @@ o.updTemplate = function(owner, tpl_id, template, callback) {
var templateName = template.name;
if ( tpl_id != templateName ) {
if ( tpl_id !== templateName ) {
return callback(new Error("Cannot update name of a map template ('" + tpl_id + "' != '" + templateName + "')"));
}
var userTemplatesKey = this.key_usr_tpl({ owner:owner });
var previousTemplate = null;
step(
function getExistingTemplate() {
self._redisCmd('HGET', [ userTemplatesKey, tpl_id ], this);
},
function updateTemplate(err, currentTemplate) {
if (err) {
throw err;
}
if (!currentTemplate) {
function updateTemplate(err, _currentTemplate) {
assert.ifError(err);
if (!_currentTemplate) {
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
}
previousTemplate = _currentTemplate;
self._redisCmd('HSET', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
},
function handleTemplateUpdate(err, didSetNewField) {
if (err) {
throw err;
}
assert.ifError(err);
if (didSetNewField) {
console.warn('New template created on update operation');
debug('New template created on update operation');
}
return true;
},
function finish(err) {
if (!err) {
self.emit('update', owner, templateName, template);
if (self.fingerPrint(JSON.parse(previousTemplate)) !== self.fingerPrint(template)) {
self.emit('update', owner, templateName, template);
}
}
callback(err, template);
@@ -371,7 +375,7 @@ o.getTemplate = function(owner, tpl_id, callback) {
self._redisCmd('HGET', [ self.key_usr_tpl({owner:owner}), tpl_id ], this);
},
function parseTemplate(err, tpl_val) {
if ( err ) throw err;
assert.ifError(err);
return JSON.parse(tpl_val);
},
function finish(err, tpl) {
@@ -471,8 +475,12 @@ o.instance = function(template, params) {
var layergroup = JSON.parse(JSON.stringify(template.layergroup));
for (var i=0; i<layergroup.layers.length; ++i) {
var lyropt = layergroup.layers[i].options;
if ( lyropt.cartocss ) lyropt.cartocss = _replaceVars(lyropt.cartocss, all_params);
if ( lyropt.sql) lyropt.sql = _replaceVars(lyropt.sql, all_params);
if ( lyropt.cartocss ) {
lyropt.cartocss = _replaceVars(lyropt.cartocss, all_params);
}
if ( lyropt.sql) {
lyropt.sql = _replaceVars(lyropt.sql, all_params);
}
// Anything else ?
}
@@ -492,3 +500,14 @@ o.fingerPrint = function(template) {
.digest('hex')
;
};
module.exports.templateName = function templateName(templateId) {
var templateIdTokens = templateId.split('@');
var name = templateIdTokens[0];
if (templateIdTokens.length > 1) {
name = templateIdTokens[1];
}
return name;
};

16
lib/cartodb/cache/backend/fastly.js vendored Normal file
View File

@@ -0,0 +1,16 @@
var FastlyPurge = require('fastly-purge');
function FastlyCacheBackend(apiKey, serviceId) {
this.serviceId = serviceId;
this.fastlyPurge = new FastlyPurge(apiKey, { softPurge: false });
}
module.exports = FastlyCacheBackend;
/**
* @param cacheObject should respond to `key() -> String` method
* @param {Function} callback
*/
FastlyCacheBackend.prototype.invalidate = function(cacheObject, callback) {
this.fastlyPurge.key(this.serviceId, cacheObject.key(), callback);
};

View File

@@ -28,5 +28,3 @@ VarnishHttpCacheBackend.prototype.invalidate = function(cacheObject, callback) {
}
);
};
module.exports = VarnishHttpCacheBackend;

View File

@@ -0,0 +1,24 @@
var LruCache = require('lru-cache');
function LayergroupAffectedTables() {
// dbname + layergroupId -> affected tables cache
this.cache = new LruCache({ max: 2000 });
}
module.exports = LayergroupAffectedTables;
LayergroupAffectedTables.prototype.hasAffectedTables = function(dbName, layergroupId) {
return this.cache.has(createKey(dbName, layergroupId));
};
LayergroupAffectedTables.prototype.set = function(dbName, layergroupId, affectedTables) {
this.cache.set(createKey(dbName, layergroupId), affectedTables);
};
LayergroupAffectedTables.prototype.get = function(dbName, layergroupId) {
return this.cache.get(createKey(dbName, layergroupId));
};
function createKey(dbName, layergroupId) {
return dbName + ':' + layergroupId;
}

View File

@@ -0,0 +1,24 @@
var crypto = require('crypto');
function DatabaseTables(dbName, tableNames) {
this.namespace = 't';
this.dbName = dbName;
this.tableNames = tableNames;
}
module.exports = DatabaseTables;
DatabaseTables.prototype.key = function() {
return this.tableNames.map(function(tableName) {
return this.namespace + ':' + shortHashKey(this.dbName + ':' + tableName);
}.bind(this));
};
DatabaseTables.prototype.getCacheChannel = function() {
return this.dbName + ':' + this.tableNames.join(',');
};
function shortHashKey(target) {
return crypto.createHash('sha256').update(target).digest('base64').substring(0,6);
}

View File

@@ -0,0 +1,88 @@
var _ = require('underscore');
var dot = require('dot');
var NamedMapMapConfigProvider = require('../models/mapconfig/named_map_provider');
var MapConfigNamedLayersAdapter = require('../models/mapconfig_named_layers_adapter');
var templateName = require('../backends/template_maps').templateName;
var queue = require('queue-async');
var LruCache = require("lru-cache");
function NamedMapProviderCache(templateMaps, pgConnection, userLimitsApi, queryTablesApi) {
this.templateMaps = templateMaps;
this.pgConnection = pgConnection;
this.userLimitsApi = userLimitsApi;
this.queryTablesApi = queryTablesApi;
this.namedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
this.providerCache = new LruCache({ max: 2000 });
}
module.exports = NamedMapProviderCache;
NamedMapProviderCache.prototype.get = function(user, templateId, config, authToken, params, callback) {
var namedMapKey = createNamedMapKey(user, templateId);
var namedMapProviders = this.providerCache.get(namedMapKey) || {};
var providerKey = createProviderKey(config, authToken, params);
if (!namedMapProviders.hasOwnProperty(providerKey)) {
namedMapProviders[providerKey] = new NamedMapMapConfigProvider(
this.templateMaps,
this.pgConnection,
this.userLimitsApi,
this.queryTablesApi,
this.namedLayersAdapter,
user,
templateId,
config,
authToken,
params
);
this.providerCache.set(namedMapKey, namedMapProviders);
// early exit, if provider did not exist we just return it
return callback(null, namedMapProviders[providerKey]);
}
var namedMapProvider = namedMapProviders[providerKey];
var self = this;
queue(2)
.defer(namedMapProvider.getTemplate.bind(namedMapProvider))
.defer(this.templateMaps.getTemplate.bind(this.templateMaps), user, templateId)
.awaitAll(function templatesQueueDone(err, results) {
if (err) {
return callback(err);
}
// We want to reset provider its template has changed
// Ideally this should be done in a passive mode where this cache gets notified of template changes
var uniqueFingerprints = _.uniq(results.map(self.templateMaps.fingerPrint)).length;
if (uniqueFingerprints > 1) {
namedMapProvider.reset();
}
return callback(null, namedMapProvider);
});
};
NamedMapProviderCache.prototype.invalidate = function(user, templateId) {
this.providerCache.del(createNamedMapKey(user, templateId));
};
function createNamedMapKey(user, templateId) {
return user + ':' + templateName(templateId);
}
var providerKey = '{{=it.authToken}}:{{=it.configHash}}:{{=it.format}}:{{=it.layer}}:{{=it.scale_factor}}';
var providerKeyTpl = dot.template(providerKey);
function createProviderKey(config, authToken, params) {
var tplValues = _.defaults({}, params, {
authToken: authToken || '',
configHash: NamedMapMapConfigProvider.configHash(config),
layer: '',
format: '',
scale_factor: 1
});
return providerKeyTpl(tplValues);
}

View File

@@ -1,9 +1,11 @@
var queue = require('queue-async');
/**
* @param cacheBackend should respond to `invalidate(cacheObject, callback)` method
* @param {Array|Object} cacheBackends each backend backend should respond to `invalidate(cacheObject, callback)` method
* @constructor
*/
function SurrogateKeysCache(cacheBackend) {
this.cacheBackend = cacheBackend;
function SurrogateKeysCache(cacheBackends) {
this.cacheBackends = Array.isArray(cacheBackends) ? cacheBackends : [cacheBackends];
}
module.exports = SurrogateKeysCache;
@@ -14,13 +16,38 @@ module.exports = SurrogateKeysCache;
* @param cacheObject should respond to `key() -> String` method
*/
SurrogateKeysCache.prototype.tag = function(response, cacheObject) {
response.header('Surrogate-Key', cacheObject.key());
var newKey = cacheObject.key();
response.set('Surrogate-Key', appendSurrogateKey(
response.get('Surrogate-Key'),
Array.isArray(newKey) ? cacheObject.key().join(' ') : newKey
));
};
function appendSurrogateKey(currentKey, newKey) {
if (!!currentKey) {
newKey = currentKey + ' ' + newKey;
}
return newKey;
}
/**
* @param cacheObject should respond to `key() -> String` method
* @param {Function} callback
*/
SurrogateKeysCache.prototype.invalidate = function(cacheObject, callback) {
this.cacheBackend.invalidate(cacheObject, callback);
var invalidationQueue = queue(this.cacheBackends.length);
this.cacheBackends.forEach(function(cacheBackend) {
invalidationQueue.defer(function(cacheBackend, done) {
cacheBackend.invalidate(cacheObject, done);
}, cacheBackend);
});
invalidationQueue.awaitAll(function(err, result) {
if (err) {
return callback(err);
}
callback(null, result);
});
};

View File

@@ -1,184 +0,0 @@
var _ = require('underscore');
var step = require('step');
var Windshaft = require('windshaft');
var os = require('os');
var HealthCheck = require('./monitoring/health_check');
if ( ! process.env.PGAPPNAME )
process.env.PGAPPNAME='cartodb_tiler';
var CartodbWindshaft = function(serverOptions) {
// Perform keyword substitution in statsd
// See https://github.com/CartoDB/Windshaft-cartodb/issues/153
if ( global.environment.statsd ) {
if ( global.environment.statsd.prefix ) {
var host_token = os.hostname().split('.').reverse().join('.');
global.environment.statsd.prefix = global.environment.statsd.prefix.replace(/:host/, host_token);
}
}
var redisPool = serverOptions.redis.pool ||
require('redis-mpool')(_.extend(global.environment.redis, {name: 'windshaft:cartodb'}));
var cartoData = require('cartodb-redis')({pool: redisPool});
var templateMaps = serverOptions.templateMaps;
// This is for Templated maps
//
// "named" is the official, "template" is for backward compatibility up to 1.6.x
//
var template_baseurl = global.environment.base_url_templated || '(?:/maps/named|/tiles/template)';
var SurrogateKeysCache = require('./cache/surrogate_keys_cache'),
NamedMapsCacheEntry = require('./cache/model/named_maps_entry'),
VarnishHttpCacheBackend = require('./cache/backend/varnish_http'),
varnishHttpCacheBackend = new VarnishHttpCacheBackend(
serverOptions.varnish_host,
serverOptions.varnish_http_port
),
surrogateKeysCache = new SurrogateKeysCache(varnishHttpCacheBackend);
function invalidateNamedMap (owner, templateName) {
surrogateKeysCache.invalidate(new NamedMapsCacheEntry(owner, templateName), function(err) {
if (err) {
console.warn('Cache: surrogate key invalidation failed');
}
});
}
if (serverOptions.varnish_purge_enabled) {
['update', 'delete'].forEach(function(eventType) {
templateMaps.on(eventType, invalidateNamedMap);
});
}
// boot
var ws = new Windshaft.Server(serverOptions);
// Override getVersion to include cartodb-specific versions
var wsversion = ws.getVersion;
ws.getVersion = function() {
var version = wsversion();
version.windshaft_cartodb = require('../../package.json').version;
return version;
};
var ws_sendResponse = ws.sendResponse;
// GET routes for which we don't want to request any caching.
// POST/PUT/DELETE requests are never cached anyway.
var noCacheGETRoutes = [
'/',
// See https://github.com/CartoDB/Windshaft-cartodb/issues/176
serverOptions.base_url_mapconfig,
template_baseurl + '/:template_id/jsonp'
];
ws.sendResponse = function(res, args) {
var that = this;
var thatArgs = arguments;
var statusCode;
if ( res._windshaftStatusCode ) {
// Added by our override of sendError
statusCode = res._windshaftStatusCode;
} else {
if ( args.length > 2 ) statusCode = args[2];
else {
statusCode = args[1] || 200;
}
}
var req = res.req;
step (
function addCacheChannel() {
if ( ! req ) {
// having no associated request can happen when
// using fake response objects for testing layergroup
// creation
return false;
}
if ( ! req.params ) {
// service requests (/version, /)
// have no need for an X-Cache-Channel
return false;
}
if ( statusCode != 200 ) {
// We do not want to cache
// unsuccessful responses
return false;
}
if ( _.contains(noCacheGETRoutes, req.route.path) ) {
//console.log("Skipping cache channel in route:\n" + req.route.path);
return false;
}
//console.log("Adding cache channel to route\n" + req.route.path + " not matching any in:\n" +
// mapCreateRoutes.join("\n"));
serverOptions.addCacheChannel(that, req, this);
},
function sendResponse(err/*, added*/) {
if ( err ) console.log(err + err.stack);
ws_sendResponse.apply(that, thatArgs);
return null;
},
function finish(err) {
if ( err ) console.log(err + err.stack);
}
);
};
var ws_sendError = ws.sendError;
ws.sendError = function() {
var res = arguments[0];
var statusCode = arguments[2];
res._windshaftStatusCode = statusCode;
ws_sendError.apply(this, arguments);
};
/*******************************************************************************************************************
* Routing
******************************************************************************************************************/
var TemplateMapsController = require('./controllers/template_maps'),
templateMapsController = new TemplateMapsController(
ws,
serverOptions,
templateMaps,
cartoData,
template_baseurl,
surrogateKeysCache,
NamedMapsCacheEntry,
serverOptions.pgConnection
);
templateMapsController.register(ws);
/*******************************************************************************************************************
* END Routing
******************************************************************************************************************/
var healthCheck = new HealthCheck(cartoData, Windshaft.tilelive);
ws.get('/health', function(req, res) {
var healthConfig = global.environment.health || {};
if (!!healthConfig.enabled) {
var startTime = Date.now();
healthCheck.check(healthConfig, function(err, result) {
var ok = !err;
var response = {
enabled: true,
ok: ok,
elapsed: Date.now() - startTime,
result: result
};
if (err) {
response.err = err.message;
}
res.send(response, ok ? 200 : 503);
});
} else {
res.send({enabled: false, ok: true}, 200);
}
});
return ws;
};
module.exports = CartodbWindshaft;

View File

@@ -0,0 +1,250 @@
var assert = require('assert');
var _ = require('underscore');
var step = require('step');
var debug = require('debug')('windshaft:cartodb');
var LZMA = require('lzma').LZMA;
var lzmaWorker = new LZMA();
// Whitelist query parameters and attach format
var REQUEST_QUERY_PARAMS_WHITELIST = [
'config',
'map_key',
'api_key',
'auth_token',
'callback'
];
function BaseController(authApi, pgConnection) {
this.authApi = authApi;
this.pgConnection = pgConnection;
}
module.exports = BaseController;
// jshint maxcomplexity:9
/**
* Whitelist input and get database name & default geometry type from
* subdomain/user metadata held in CartoDB Redis
* @param req - standard express request obj. Should have host & table
* @param callback
*/
BaseController.prototype.req2params = function(req, callback){
var self = this;
if ( req.query.lzma ) {
// Decode (from base64)
var lzma = new Buffer(req.query.lzma, 'base64')
.toString('binary')
.split('')
.map(function(c) {
return c.charCodeAt(0) - 128;
});
// Decompress
lzmaWorker.decompress(
lzma,
function(result) {
if (req.profiler) {
req.profiler.done('lzma');
}
try {
delete req.query.lzma;
_.extend(req.query, JSON.parse(result));
self.req2params(req, callback);
} catch (err) {
req.profiler.done('req2params');
callback(new Error('Error parsing lzma as JSON: ' + err));
}
}
);
return;
}
req.query = _.pick(req.query, REQUEST_QUERY_PARAMS_WHITELIST);
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
var user = req.context.user;
if ( req.params.token ) {
// Token might match the following patterns:
// - {user}@{tpl_id}@{token}:{cache_buster}
var tksplit = req.params.token.split(':');
req.params.token = tksplit[0];
if ( tksplit.length > 1 ) {
req.params.cache_buster= tksplit[1];
}
tksplit = req.params.token.split('@');
if ( tksplit.length > 1 ) {
req.params.signer = tksplit.shift();
if ( ! req.params.signer ) {
req.params.signer = user;
}
else if ( req.params.signer !== user ) {
var err = new Error(
'Cannot use map signature of user "' + req.params.signer + '" on db of user "' + user + '"'
);
err.http_status = 403;
req.profiler.done('req2params');
callback(err);
return;
}
if ( tksplit.length > 1 ) {
/*var template_hash = */tksplit.shift(); // unused
}
req.params.token = tksplit.shift();
}
}
// bring all query values onto req.params object
_.extend(req.params, req.query);
if (req.profiler) {
req.profiler.done('req2params.setup');
}
step(
function getPrivacy(){
self.authApi.authorize(req, this);
},
function validateAuthorization(err, authorized) {
if (req.profiler) {
req.profiler.done('authorize');
}
assert.ifError(err);
if(!authorized) {
err = new Error("Sorry, you are unauthorized (permission denied)");
err.http_status = 403;
throw err;
}
return null;
},
function getDatabase(err){
assert.ifError(err);
self.pgConnection.setDBConn(user, req.params, this);
},
function finishSetup(err) {
if ( err ) {
req.profiler.done('req2params');
return callback(err, req);
}
// Add default database connection parameters
// if none given
_.defaults(req.params, {
dbuser: global.environment.postgres.user,
dbpassword: global.environment.postgres.password,
dbhost: global.environment.postgres.host,
dbport: global.environment.postgres.port
});
req.profiler.done('req2params');
callback(null, req);
}
);
};
// jshint maxcomplexity:6
// jshint maxcomplexity:9
BaseController.prototype.send = function(req, res, body, status, headers) {
if (req.params.dbhost) {
res.set('X-Served-By-DB-Host', req.params.dbhost);
}
if (req.profiler) {
res.set('X-Tiler-Profiler', req.profiler.toJSONString());
}
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);
}
if (req.profiler) {
try {
// May throw due to dns, see
// See http://github.com/CartoDB/Windshaft/issues/166
req.profiler.sendStats();
} catch (err) {
debug("error sending profiling stats: " + err);
}
}
};
// jshint maxcomplexity:6
BaseController.prototype.sendError = function(req, res, err, label) {
label = label || 'UNKNOWN';
var statusCode = findStatusCode(err);
debug('[%s ERROR] -- %d: %s', label, statusCode, err);
// If a callback was requested, force status to 200
if (req.query && req.query.callback) {
statusCode = 200;
}
var errorResponseBody = { errors: [errorMessage(err)] };
this.send(req, res, errorResponseBody, statusCode);
};
function errorMessage(err) {
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
// Strip connection info, if any
return message
// See https://github.com/CartoDB/Windshaft/issues/173
.replace(/Connection string: '[^']*'\n\s/im, '')
// See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644
.replace(/is the server.*encountered/im, 'encountered');
}
module.exports.errorMessage = errorMessage;
function findStatusCode(err) {
var statusCode;
if ( err.http_status ) {
statusCode = err.http_status;
} else {
statusCode = statusFromErrorMessage('' + err);
}
return statusCode;
}
module.exports.findStatusCode = findStatusCode;
function statusFromErrorMessage(errMsg) {
// Find an appropriate statusCode based on message
var statusCode = 400;
if ( -1 !== errMsg.indexOf('permission denied') ) {
statusCode = 403;
}
else if ( -1 !== errMsg.indexOf('authentication failed') ) {
statusCode = 403;
}
else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) {
statusCode = 400;
}
else if ( -1 !== errMsg.indexOf('does not exist') ) {
if ( -1 !== errMsg.indexOf(' role ') ) {
statusCode = 403; // role 'xxx' does not exist
} else {
statusCode = 404;
}
}
return statusCode;
}

View File

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

View File

@@ -0,0 +1,314 @@
var assert = require('assert');
var step = require('step');
var util = require('util');
var BaseController = require('./base');
var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');
var MapStoreMapConfigProvider = require('../models/mapconfig/map_store_provider');
var TablesCacheEntry = require('../cache/model/database_tables_entry');
/**
* @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 {QueryTablesApi} queryTablesApi
* @param {LayergroupAffectedTables} layergroupAffectedTables
* @constructor
*/
function LayergroupController(authApi, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
surrogateKeysCache, userLimitsApi, queryTablesApi, layergroupAffectedTables) {
BaseController.call(this, authApi, pgConnection);
this.mapStore = mapStore;
this.tileBackend = tileBackend;
this.previewBackend = previewBackend;
this.attributesBackend = attributesBackend;
this.surrogateKeysCache = surrogateKeysCache;
this.userLimitsApi = userLimitsApi;
this.queryTablesApi = queryTablesApi;
this.layergroupAffectedTables = layergroupAffectedTables;
}
util.inherits(LayergroupController, BaseController);
module.exports = LayergroupController;
LayergroupController.prototype.register = function(app) {
app.get(app.base_url_mapconfig +
'/:token/:z/:x/:y@:scale_factor?x.:format', cors(), userMiddleware,
this.tile.bind(this));
app.get(app.base_url_mapconfig +
'/:token/:z/:x/:y.:format', cors(), userMiddleware,
this.tile.bind(this));
app.get(app.base_url_mapconfig +
'/:token/:layer/:z/:x/:y.(:format)', cors(), userMiddleware,
this.layer.bind(this));
app.get(app.base_url_mapconfig +
'/:token/:layer/attributes/:fid', cors(), userMiddleware,
this.attributes.bind(this));
app.get(app.base_url_mapconfig +
'/static/center/:token/:z/:lat/:lng/:width/:height.:format', cors(), userMiddleware,
this.center.bind(this));
app.get(app.base_url_mapconfig +
'/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', cors(), userMiddleware,
this.bbox.bind(this));
};
LayergroupController.prototype.attributes = function(req, res) {
var self = this;
req.profiler.start('windshaft.maplayer_attribute');
step(
function setupParams() {
self.req2params(req, this);
},
function retrieveFeatureAttributes(err) {
assert.ifError(err);
var mapConfigProvider = new MapStoreMapConfigProvider(
self.mapStore, req.context.user, self.userLimitsApi, req.params
);
self.attributesBackend.getFeatureAttributes(mapConfigProvider, req.params, false, this);
},
function finish(err, tile, stats) {
req.profiler.add(stats || {});
if (err) {
self.sendError(req, res, err, 'GET ATTRIBUTES');
} 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) {
req.profiler.start('windshaft.map_tile');
this.tileOrLayer(req, res);
};
// Gets a tile for a given token, layer set of tile ZXY coords. (OSM style)
LayergroupController.prototype.layer = function(req, res, next) {
if (req.params.token === 'static') {
return next();
}
req.profiler.start('windshaft.maplayer_tile');
this.tileOrLayer(req, res);
};
LayergroupController.prototype.tileOrLayer = function (req, res) {
var self = this;
step(
function mapController$prepareParams() {
self.req2params(req, this);
},
function mapController$getTileOrGrid(err) {
assert.ifError(err);
self.tileBackend.getTile(
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
req.params, this
);
},
function mapController$finalize(err, tile, headers, stats) {
req.profiler.add(stats);
self.finalizeGetTileOrGrid(err, req, res, tile, headers);
}
);
};
// 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) {
var supportedFormats = {
grid_json: true,
json_torque: true,
torque_json: true,
png: 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;
this.sendError(req, res, err, 'TILE RENDER');
global.statsClient.increment('windshaft.tiles.error');
global.statsClient.increment('windshaft.tiles.' + formatStat + '.error');
} else {
this.sendResponse(req, res, tile, 200, headers);
global.statsClient.increment('windshaft.tiles.success');
global.statsClient.increment('windshaft.tiles.' + formatStat + '.success');
}
};
LayergroupController.prototype.bbox = function(req, res) {
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
});
};
LayergroupController.prototype.center = function(req, res) {
this.staticMap(req, res, +req.params.width, +req.params.height, +req.params.z, {
lng: +req.params.lng,
lat: +req.params.lat
});
};
LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center) {
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
req.params.layer = 'all';
req.params.format = 'png';
var self = this;
step(
function reqParams() {
self.req2params(req, this);
},
function getImage(err) {
assert.ifError(err);
if (center) {
self.previewBackend.getImage(
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
format, width, height, zoom, center, this);
} else {
self.previewBackend.getImage(
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
format, width, height, zoom /* bounds */, this);
}
},
function handleImage(err, image, headers, stats) {
req.profiler.done('render-' + format);
req.profiler.add(stats || {});
if (err) {
self.sendError(req, res, err, 'STATIC_MAP');
} 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;
res.set('Cache-Control', 'public,max-age=31536000');
// Set Last-Modified header
var lastUpdated;
if (req.params.cache_buster) {
// Assuming cache_buster is a timestamp
lastUpdated = new Date(parseInt(req.params.cache_buster));
} else {
lastUpdated = new Date();
}
res.set('Last-Modified', lastUpdated.toUTCString());
var dbName = req.params.dbname;
step(
function getAffectedTables() {
self.getAffectedTables(req.context.user, dbName, req.params.token, this);
},
function sendResponse(err, affectedTables) {
req.profiler.done('affectedTables');
if (err) {
global.logger.warn('ERROR generating cache channel: ' + err);
}
if (!!affectedTables) {
var tablesCacheEntry = new TablesCacheEntry(dbName, affectedTables);
res.set('X-Cache-Channel', tablesCacheEntry.getCacheChannel());
self.surrogateKeysCache.tag(res, tablesCacheEntry);
}
self.send(req, res, body, status, headers);
}
);
};
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()
.map(function(lyr) {
return lyr.options.sql;
})
.filter(function(sql) {
return !!sql;
});
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");
}
self.queryTablesApi.getAffectedTablesInQuery(user, sql, this); // in addCacheChannel
},
function buildCacheChannel(err, tableNames) {
assert.ifError(err);
self.layergroupAffectedTables.set(dbName, layergroupId, tableNames);
return tableNames;
},
function finish(err, affectedTables) {
callback(err, affectedTables);
}
);
};

View File

@@ -0,0 +1,326 @@
var _ = require('underscore');
var assert = require('assert');
var step = require('step');
var windshaft = require('windshaft');
var util = require('util');
var BaseController = require('./base');
var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');
var MapConfig = windshaft.model.MapConfig;
var Datasource = windshaft.model.Datasource;
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
var TablesCacheEntry = require('../cache/model/database_tables_entry');
var MapConfigNamedLayersAdapter = require('../models/mapconfig_named_layers_adapter');
var NamedMapMapConfigProvider = require('../models/mapconfig/named_map_provider');
var CreateLayergroupMapConfigProvider = require('../models/mapconfig/create_layergroup_provider');
/**
* @param {AuthApi} authApi
* @param {PgConnection} pgConnection
* @param {TemplateMaps} templateMaps
* @param {MapBackend} mapBackend
* @param metadataBackend
* @param {QueryTablesApi} queryTablesApi
* @param {SurrogateKeysCache} surrogateKeysCache
* @param {UserLimitsApi} userLimitsApi
* @param {LayergroupAffectedTables} layergroupAffectedTables
* @constructor
*/
function MapController(authApi, pgConnection, templateMaps, mapBackend, metadataBackend, queryTablesApi,
surrogateKeysCache, userLimitsApi, layergroupAffectedTables) {
BaseController.call(this, authApi, pgConnection);
this.pgConnection = pgConnection;
this.templateMaps = templateMaps;
this.mapBackend = mapBackend;
this.metadataBackend = metadataBackend;
this.queryTablesApi = queryTablesApi;
this.surrogateKeysCache = surrogateKeysCache;
this.userLimitsApi = userLimitsApi;
this.layergroupAffectedTables = layergroupAffectedTables;
this.namedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
}
util.inherits(MapController, BaseController);
module.exports = MapController;
MapController.prototype.register = function(app) {
app.get(app.base_url_mapconfig, cors(), userMiddleware, this.createGet.bind(this));
app.post(app.base_url_mapconfig, cors(), userMiddleware, this.createPost.bind(this));
app.get(app.base_url_templated + '/:template_id/jsonp', cors(), userMiddleware, this.jsonp.bind(this));
app.post(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.instantiate.bind(this));
app.options(app.base_url_mapconfig, cors('Content-Type'));
};
MapController.prototype.createGet = function(req, res){
req.profiler.start('windshaft.createmap_get');
this.create(req, res, function createGet$prepareConfig(err, req) {
assert.ifError(err);
if ( ! req.params.config ) {
throw new Error('layergroup GET needs a "config" parameter');
}
return JSON.parse(req.params.config);
});
};
MapController.prototype.createPost = function(req, res) {
req.profiler.start('windshaft.createmap_post');
this.create(req, res, function createPost$prepareConfig(err, req) {
assert.ifError(err);
if (!req.is('application/json')) {
throw new Error('layergroup POST data must be of type application/json');
}
return req.body;
});
};
MapController.prototype.instantiate = function(req, res) {
if (req.profiler) {
req.profiler.start('windshaft-cartodb.instance_template_post');
}
this.instantiateTemplate(req, res, function prepareTemplateParams(callback) {
if (!req.is('application/json')) {
return callback(new Error('Template POST data must be of type application/json'));
}
return callback(null, req.body);
});
};
MapController.prototype.jsonp = function(req, res) {
if (req.profiler) {
req.profiler.start('windshaft-cartodb.instance_template_get');
}
this.instantiateTemplate(req, res, function prepareJsonTemplateParams(callback) {
var err = null;
if ( req.query.callback === undefined || req.query.callback.length === 0) {
err = new Error('callback parameter should be present and be a function name');
}
var templateParams = {};
if (req.query.config) {
try {
templateParams = JSON.parse(req.query.config);
} catch(e) {
err = new Error('Invalid config parameter, should be a valid JSON');
}
}
return callback(err, templateParams);
});
};
MapController.prototype.create = function(req, res, prepareConfigFn) {
var self = this;
var mapConfig;
step(
function setupParams(){
self.req2params(req, this);
},
prepareConfigFn,
function beforeLayergroupCreate(err, requestMapConfig) {
assert.ifError(err);
var next = this;
self.namedLayersAdapter.getLayers(req.context.user, requestMapConfig.layers, self.pgConnection,
function(err, layers, datasource) {
if (err) {
return next(err);
}
if (layers) {
requestMapConfig.layers = layers;
}
return next(null, requestMapConfig, datasource);
}
);
},
function createLayergroup(err, requestMapConfig, datasource) {
assert.ifError(err);
mapConfig = new MapConfig(requestMapConfig, datasource || Datasource.EmptyDatasource());
self.mapBackend.createLayergroup(
mapConfig, req.params,
new CreateLayergroupMapConfigProvider(mapConfig, req.context.user, self.userLimitsApi, req.params),
this
);
},
function afterLayergroupCreate(err, layergroup) {
assert.ifError(err);
self.afterLayergroupCreate(req, res, mapConfig, layergroup, this);
},
function finish(err, layergroup) {
if (err) {
self.sendError(req, res, err, 'ANONYMOUS LAYERGROUP');
} else {
res.set('X-Layergroup-Id', layergroup.layergroupid);
self.send(req, res, layergroup, 200);
}
}
);
};
MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn) {
var self = this;
var cdbuser = req.context.user;
var mapConfigProvider;
var mapConfig;
step(
function setupParams(){
self.req2params(req, this);
},
function getTemplateParams() {
prepareParamsFn(this);
},
function getTemplate(err, templateParams) {
assert.ifError(err);
mapConfigProvider = new NamedMapMapConfigProvider(
self.templateMaps,
self.pgConnection,
self.userLimitsApi,
self.queryTablesApi,
self.namedLayersAdapter,
cdbuser,
req.params.template_id,
templateParams,
req.query.auth_token,
req.params
);
mapConfigProvider.getMapConfig(this);
},
function createLayergroup(err, mapConfig_, rendererParams/*, context*/) {
assert.ifError(err);
mapConfig = mapConfig_;
self.mapBackend.createLayergroup(
mapConfig, rendererParams,
new CreateLayergroupMapConfigProvider(mapConfig, cdbuser, self.userLimitsApi, rendererParams),
this
);
},
function afterLayergroupCreate(err, layergroup) {
assert.ifError(err);
self.afterLayergroupCreate(req, res, mapConfig, layergroup, this);
},
function finishTemplateInstantiation(err, layergroup) {
if (err) {
self.sendError(req, res, err, 'NAMED MAP LAYERGROUP');
} else {
var templateHash = self.templateMaps.fingerPrint(mapConfigProvider.template).substring(0, 8);
layergroup.layergroupid = cdbuser + '@' + templateHash + '@' + layergroup.layergroupid;
res.set('X-Layergroup-Id', layergroup.layergroupid);
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, mapConfigProvider.getTemplateName()));
self.send(req, res, layergroup, 200);
}
}
);
};
MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, layergroup, callback) {
var self = this;
var username = req.context.user;
var tasksleft = 2; // redis key and affectedTables
var errors = [];
var done = function(err) {
if ( err ) {
errors.push('' + err);
}
if ( ! --tasksleft ) {
err = errors.length ? new Error(errors.join('\n')) : null;
callback(err, layergroup);
}
};
// 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);
// Don't wait for the mapview count increment to
// take place before proceeding. Error will be logged
// asynchronously
this.metadataBackend.incMapviewCount(username, mapconfig.obj().stat_tag, function(err) {
if (req.profiler) {
req.profiler.done('incMapviewCount');
}
if ( err ) {
global.logger.log("ERROR: failed to increment mapview count for user '" + username + "': " + err);
}
done();
});
var sql = mapconfig.getLayers().map(function(layer) {
return layer.options.sql;
}).join(';');
var dbName = req.params.dbname;
var layergroupId = layergroup.layergroupid;
step(
function checkCachedAffectedTables() {
return self.layergroupAffectedTables.hasAffectedTables(dbName, layergroupId);
},
function getAffectedTablesAndLastUpdatedTime(err, hasCache) {
assert.ifError(err);
if (hasCache) {
var next = this;
var affectedTables = self.layergroupAffectedTables.get(dbName, layergroupId);
self.queryTablesApi.getLastUpdatedTime(username, affectedTables, function(err, lastUpdatedTime) {
if (err) {
return next(err);
}
return next(null, { affectedTables: affectedTables, lastUpdatedTime: lastUpdatedTime });
});
} else {
self.queryTablesApi.getAffectedTablesAndLastUpdatedTime(username, sql, this);
}
},
function handleAffectedTablesAndLastUpdatedTime(err, result) {
if (req.profiler) {
req.profiler.done('queryTablesAndLastUpdated');
}
assert.ifError(err);
self.layergroupAffectedTables.set(dbName, layergroupId, result.affectedTables);
// last update for layergroup cache buster
layergroup.layergroupid = layergroup.layergroupid + ':' + result.lastUpdatedTime;
layergroup.last_updated = new Date(result.lastUpdatedTime).toISOString();
if (req.method === 'GET') {
var tableCacheEntry = new TablesCacheEntry(dbName, result.affectedTables);
var ttl = global.environment.varnish.layergroupTtl || 86400;
res.set('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
res.set('Last-Modified', (new Date()).toUTCString());
res.set('X-Cache-Channel', tableCacheEntry.getCacheChannel());
if (result.affectedTables && result.affectedTables.length > 0) {
self.surrogateKeysCache.tag(res, tableCacheEntry);
}
}
return null;
},
function finish(err) {
done(err);
}
);
};

View File

@@ -0,0 +1,281 @@
var step = require('step');
var assert = require('assert');
var _ = require('underscore');
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
var util = require('util');
var BaseController = require('./base');
var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');
var TablesCacheEntry = require('../cache/model/database_tables_entry');
function NamedMapsController(authApi, pgConnection, namedMapProviderCache, tileBackend, previewBackend,
surrogateKeysCache, tablesExtentApi, metadataBackend) {
BaseController.call(this, authApi, pgConnection);
this.namedMapProviderCache = namedMapProviderCache;
this.tileBackend = tileBackend;
this.previewBackend = previewBackend;
this.surrogateKeysCache = surrogateKeysCache;
this.tablesExtentApi = tablesExtentApi;
this.metadataBackend = metadataBackend;
}
util.inherits(NamedMapsController, BaseController);
module.exports = NamedMapsController;
NamedMapsController.prototype.register = function(app) {
app.get(app.base_url_templated +
'/:template_id/:layer/:z/:x/:y.(:format)', cors(), userMiddleware,
this.tile.bind(this));
app.get(app.base_url_mapconfig +
'/static/named/:template_id/:width/:height.:format', cors(), userMiddleware,
this.staticMap.bind(this));
};
NamedMapsController.prototype.sendResponse = function(req, res, resource, headers, namedMapProvider) {
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(req.context.user, namedMapProvider.getTemplateName()));
res.set('Content-Type', headers['content-type'] || headers['Content-Type'] || 'image/png');
res.set('Cache-Control', 'public,max-age=7200,must-revalidate');
var self = this;
var dbName = req.params.dbname;
step(
function getAffectedTablesAndLastUpdatedTime() {
namedMapProvider.getAffectedTablesAndLastUpdatedTime(this);
},
function sendResponse(err, result) {
req.profiler.done('affectedTables');
if (err) {
global.logger.log('ERROR generating cache channel: ' + err);
}
if (!result || !!result.affectedTables) {
// we increase cache control as we can invalidate it
res.set('Cache-Control', 'public,max-age=31536000');
var lastModifiedDate;
if (Number.isFinite(result.lastUpdatedTime)) {
lastModifiedDate = new Date(result.lastUpdatedTime);
} else {
lastModifiedDate = new Date();
}
res.set('Last-Modified', lastModifiedDate.toUTCString());
var tablesCacheEntry = new TablesCacheEntry(dbName, result.affectedTables);
res.set('X-Cache-Channel', tablesCacheEntry.getCacheChannel());
if (result.affectedTables.length > 0) {
self.surrogateKeysCache.tag(res, tablesCacheEntry);
}
}
self.send(req, res, resource, 200);
}
);
};
NamedMapsController.prototype.tile = function(req, res) {
var self = this;
var cdbUser = req.context.user;
var namedMapProvider;
step(
function reqParams() {
self.req2params(req, this);
},
function getNamedMapProvider(err) {
assert.ifError(err);
self.namedMapProviderCache.get(
cdbUser,
req.params.template_id,
req.query.config,
req.query.auth_token,
req.params,
this
);
},
function getTile(err, _namedMapProvider) {
assert.ifError(err);
namedMapProvider = _namedMapProvider;
self.tileBackend.getTile(namedMapProvider, req.params, this);
},
function handleImage(err, tile, headers, stats) {
if (req.profiler) {
req.profiler.add(stats);
}
if (err) {
self.sendError(req, res, err, 'NAMED_MAP_TILE');
} else {
self.sendResponse(req, res, tile, headers, namedMapProvider);
}
}
);
};
NamedMapsController.prototype.staticMap = function(req, res) {
var self = this;
var cdbUser = req.context.user;
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
req.params.format = 'png';
req.params.layer = 'all';
var namedMapProvider;
step(
function reqParams() {
self.req2params(req, this);
},
function getNamedMapProvider(err) {
assert.ifError(err);
self.namedMapProviderCache.get(
cdbUser,
req.params.template_id,
req.query.config,
req.query.auth_token,
req.params,
this
);
},
function prepareImageOptions(err, _namedMapProvider) {
assert.ifError(err);
namedMapProvider = _namedMapProvider;
self.getStaticImageOptions(cdbUser, namedMapProvider, this);
},
function getImage(err, imageOpts) {
assert.ifError(err);
var width = +req.params.width;
var height = +req.params.height;
if (!_.isUndefined(imageOpts.zoom) && imageOpts.center) {
self.previewBackend.getImage(
namedMapProvider, format, width, height, imageOpts.zoom, imageOpts.center, this);
} else {
self.previewBackend.getImage(
namedMapProvider, format, width, height, imageOpts.bounds, this);
}
},
function incrementMapViews(err, image, headers, stats) {
assert.ifError(err);
var next = this;
namedMapProvider.getMapConfig(function(mapConfigErr, mapConfig) {
self.metadataBackend.incMapviewCount(cdbUser, mapConfig.obj().stat_tag, function(sErr) {
if (err) {
global.logger.log("ERROR: failed to increment mapview count for user '%s': %s", cdbUser, sErr);
}
next(err, image, headers, stats);
});
});
},
function handleImage(err, image, headers, stats) {
if (req.profiler) {
req.profiler.done('render-' + format);
req.profiler.add(stats || {});
}
if (err) {
self.sendError(req, res, err, 'STATIC_VIZ_MAP');
} else {
self.sendResponse(req, res, image, headers, namedMapProvider);
}
}
);
};
var DEFAULT_ZOOM_CENTER = {
zoom: 1,
center: {
lng: 0,
lat: 0
}
};
NamedMapsController.prototype.getStaticImageOptions = function(cdbUser, namedMapProvider, callback) {
var self = this;
step(
function getTemplate() {
namedMapProvider.getTemplate(this);
},
function handleTemplateView(err, template) {
assert.ifError(err);
if (template.view) {
var zoomCenter = templateZoomCenter(template.view);
if (zoomCenter) {
return zoomCenter;
}
var bounds = templateBounds(template.view);
if (bounds) {
return bounds;
}
}
return false;
},
function estimateBoundsIfNoImageOpts(err, imageOpts) {
if (imageOpts) {
return imageOpts;
}
var next = this;
namedMapProvider.getAffectedTablesAndLastUpdatedTime(function(err, affectedTablesAndLastUpdate) {
if (err) {
return next(null);
}
var affectedTables = affectedTablesAndLastUpdate.affectedTables || [];
if (affectedTables.length === 0) {
return next(null);
}
self.tablesExtentApi.getBounds(cdbUser, affectedTables, function(err, result) {
return next(null, result);
});
});
},
function returnCallback(err, imageOpts) {
return callback(err, imageOpts || DEFAULT_ZOOM_CENTER);
}
);
};
function templateZoomCenter(view) {
if (!_.isUndefined(view.zoom) && view.center) {
return {
zoom: view.zoom,
center: view.center
};
}
return false;
}
function templateBounds(view) {
if (view.bounds) {
var hasAllBounds = _.every(['west', 'south', 'east', 'north'], function(prop) {
return 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,202 @@
var step = require('step');
var assert = require('assert');
var templateName = require('../backends/template_maps').templateName;
var util = require('util');
var BaseController = require('./base');
var cors = require('../middleware/cors');
var userMiddleware = require('../middleware/user');
/**
* @param {AuthApi} authApi
* @param {PgConnection} pgConnection
* @param {TemplateMaps} templateMaps
* @constructor
*/
function NamedMapsAdminController(authApi, pgConnection, templateMaps) {
BaseController.call(this, authApi, pgConnection);
this.authApi = authApi;
this.templateMaps = templateMaps;
}
util.inherits(NamedMapsAdminController, BaseController);
module.exports = NamedMapsAdminController;
NamedMapsAdminController.prototype.register = function(app) {
app.post(app.base_url_templated, cors(), userMiddleware, this.create.bind(this));
app.put(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.update.bind(this));
app.get(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.retrieve.bind(this));
app.delete(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.destroy.bind(this));
app.get(app.base_url_templated, cors(), userMiddleware, this.list.bind(this));
app.options(app.base_url_templated + '/:template_id', cors('Content-Type'));
};
NamedMapsAdminController.prototype.create = function(req, res) {
var self = this;
var cdbuser = req.context.user;
step(
function checkPerms(){
self.authApi.authorizedByAPIKey(cdbuser, req, this);
},
function addTemplate(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
ifInvalidContentType(req, 'template POST data must be of type application/json');
var cfg = req.body;
self.templateMaps.addTemplate(cdbuser, cfg, this);
},
function prepareResponse(err, tpl_id){
assert.ifError(err);
return { template_id: tpl_id };
},
finishFn(self, req, res, 'POST TEMPLATE')
);
};
NamedMapsAdminController.prototype.update = function(req, res) {
var self = this;
var cdbuser = req.context.user;
var template;
var tpl_id;
step(
function checkPerms(){
self.authApi.authorizedByAPIKey(cdbuser, req, this);
},
function updateTemplate(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated user can update templated maps');
ifInvalidContentType(req, 'template PUT data must be of type application/json');
template = req.body;
tpl_id = templateName(req.params.template_id);
self.templateMaps.updTemplate(cdbuser, tpl_id, template, this);
},
function prepareResponse(err){
assert.ifError(err);
return { template_id: tpl_id };
},
finishFn(self, req, res, 'PUT TEMPLATE')
);
};
NamedMapsAdminController.prototype.retrieve = function(req, res) {
var self = this;
if (req.profiler) {
req.profiler.start('windshaft-cartodb.get_template');
}
var cdbuser = req.context.user;
var tpl_id;
step(
function checkPerms(){
self.authApi.authorizedByAPIKey(cdbuser, req, this);
},
function getTemplate(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
tpl_id = templateName(req.params.template_id);
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
},
function prepareResponse(err, tpl_val) {
assert.ifError(err);
if ( ! tpl_val ) {
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
err.http_status = 404;
throw err;
}
// auth_id was added by ourselves,
// so we remove it before returning to the user
delete tpl_val.auth_id;
return { template: tpl_val };
},
finishFn(self, req, res, 'GET TEMPLATE')
);
};
NamedMapsAdminController.prototype.destroy = function(req, res) {
var self = this;
if (req.profiler) {
req.profiler.start('windshaft-cartodb.delete_template');
}
var cdbuser = req.context.user;
var tpl_id;
step(
function checkPerms(){
self.authApi.authorizedByAPIKey(cdbuser, req, this);
},
function deleteTemplate(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated users can delete template maps');
tpl_id = templateName(req.params.template_id);
self.templateMaps.delTemplate(cdbuser, tpl_id, this);
},
function prepareResponse(err/*, tpl_val*/){
assert.ifError(err);
return '';
},
finishFn(self, req, res, 'DELETE TEMPLATE', 204)
);
};
NamedMapsAdminController.prototype.list = function(req, res) {
var self = this;
if ( req.profiler ) {
req.profiler.start('windshaft-cartodb.get_template_list');
}
var cdbuser = req.context.user;
step(
function checkPerms(){
self.authApi.authorizedByAPIKey(cdbuser, req, this);
},
function listTemplates(err, authenticated) {
assert.ifError(err);
ifUnauthenticated(authenticated, 'Only authenticated user can list templated maps');
self.templateMaps.listTemplates(cdbuser, this);
},
function prepareResponse(err, tpl_ids){
assert.ifError(err);
return { template_ids: tpl_ids };
},
finishFn(self, req, res, 'GET TEMPLATE LIST')
);
};
function finishFn(controller, req, res, description, status) {
return function finish(err, response){
if (err) {
controller.sendError(req, res, err, description);
} else {
controller.send(req, res, response, status || 200);
}
};
}
function ifUnauthenticated(authenticated, description) {
if (!authenticated) {
var err = new Error(description);
err.http_status = 403;
throw err;
}
}
function ifInvalidContentType(req, description) {
if (!req.is('application/json')) {
throw new Error(description);
}
}

View File

@@ -0,0 +1,56 @@
var windshaft = require('windshaft');
var HealthCheck = require('../monitoring/health_check');
var WELCOME_MSG = "This is the CartoDB Maps API, " +
"see the documentation at http://docs.cartodb.com/cartodb-platform/maps-api.html";
var versions = {
windshaft: windshaft.version,
grainstore: windshaft.grainstore.version(),
node_mapnik: windshaft.mapnik.version,
mapnik: windshaft.mapnik.versions.mapnik,
windshaft_cartodb: require('../../../package.json').version
};
function ServerInfoController() {
this.healthConfig = global.environment.health || {};
this.healthCheck = new HealthCheck(global.environment.disabled_file);
}
module.exports = ServerInfoController;
ServerInfoController.prototype.register = function(app) {
app.get('/health', this.health.bind(this));
app.get('/', this.welcome.bind(this));
app.get('/version', this.version.bind(this));
};
ServerInfoController.prototype.welcome = function(req, res) {
res.status(200).send(WELCOME_MSG);
};
ServerInfoController.prototype.version = function(req, res) {
res.status(200).send(versions);
};
ServerInfoController.prototype.health = function(req, res) {
if (!!this.healthConfig.enabled) {
var startTime = Date.now();
this.healthCheck.check(function(err) {
var ok = !err;
var response = {
enabled: true,
ok: ok,
elapsed: Date.now() - startTime
};
if (err) {
response.err = err.message;
}
res.status(ok ? 200 : 503).send(response);
});
} else {
res.status(200).send({enabled: false, ok: true});
}
};

View File

@@ -1,483 +0,0 @@
var step = require('step');
var _ = require('underscore');
var CdbRequest = require('../models/cdb_request');
function TemplateMapsController(app, serverOptions, templateMaps, metadataBackend, templateBaseUrl, surrogateKeysCache,
NamedMapsCacheEntry, pgConnection) {
this.app = app;
this.serverOptions = serverOptions;
this.templateMaps = templateMaps;
this.metadataBackend = metadataBackend;
this.templateBaseUrl = templateBaseUrl;
this.surrogateKeysCache = surrogateKeysCache;
this.NamedMapsCacheEntry = NamedMapsCacheEntry;
this.pgConnection = pgConnection;
}
module.exports = TemplateMapsController;
var cdbRequest = new CdbRequest();
TemplateMapsController.prototype.register = function(app) {
app.get(this.templateBaseUrl + '/:template_id/jsonp', this.jsonp.bind(this));
app.post(this.templateBaseUrl, this.create.bind(this));
app.put(this.templateBaseUrl + '/:template_id', this.update.bind(this));
app.get(this.templateBaseUrl + '/:template_id', this.retrieve.bind(this));
app.del(this.templateBaseUrl + '/:template_id', this.destroy.bind(this));
app.get(this.templateBaseUrl, this.list.bind(this));
app.options(this.templateBaseUrl + '/:template_id', this.options.bind(this));
app.post(this.templateBaseUrl + '/:template_id', this.instantiate.bind(this));
};
// Add a template
TemplateMapsController.prototype.create = function(req, res) {
var self = this;
this.app.doCORS(res);
var cdbuser = cdbRequest.userByReq(req);
step(
function checkPerms(){
self.serverOptions.authorizedByAPIKey(req, this);
},
function addTemplate(err, authenticated) {
if ( err ) throw err;
if (authenticated !== 1) {
err = new Error("Only authenticated user can create templated maps");
err.http_status = 403;
throw err;
}
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' )
throw new Error('template POST data must be of type application/json');
var cfg = req.body;
self.templateMaps.addTemplate(cdbuser, cfg, this);
},
function prepareResponse(err, tpl_id){
if ( err ) throw err;
// NOTE: might omit "cdbuser" if == dbowner ...
return { template_id: cdbuser + '@' + tpl_id };
},
function finish(err, response){
if ( req.profiler ) {
res.header('X-Tiler-Profiler', req.profiler.toJSONString());
}
if (err){
response = { error: ''+err };
var statusCode = 400;
if ( ! _.isUndefined(err.http_status) ) {
statusCode = err.http_status;
}
self.app.sendError(res, response, statusCode, 'POST TEMPLATE', err);
} else {
self.app.sendResponse(res, [response, 200]);
}
}
);
};
// Update a template
TemplateMapsController.prototype.update = function(req, res) {
var self = this;
this.app.doCORS(res);
var cdbuser = cdbRequest.userByReq(req);
var template;
var tpl_id;
step(
function checkPerms(){
self.serverOptions.authorizedByAPIKey(req, this);
},
function updateTemplate(err, authenticated) {
if ( err ) throw err;
if (authenticated !== 1) {
err = new Error("Only authenticated user can list templated maps");
err.http_status = 403;
throw err;
}
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' )
throw new Error('template PUT data must be of type application/json');
template = req.body;
tpl_id = req.params.template_id.split('@');
if ( tpl_id.length > 1 ) {
if ( tpl_id[0] != cdbuser ) {
err = new Error("Invalid template id '" + req.params.template_id + "' for user '" + cdbuser + "'");
err.http_status = 404;
throw err;
}
tpl_id = tpl_id[1];
}
self.templateMaps.updTemplate(cdbuser, tpl_id, template, this);
},
function prepareResponse(err){
if ( err ) throw err;
return { template_id: cdbuser + '@' + tpl_id };
},
function finish(err, response){
if ( req.profiler ) {
res.header('X-Tiler-Profiler', req.profiler.toJSONString());
}
if (err){
var statusCode = 400;
response = { error: ''+err };
if ( ! _.isUndefined(err.http_status) ) {
statusCode = err.http_status;
}
self.app.sendError(res, response, statusCode, 'PUT TEMPLATE', err);
} else {
self.app.sendResponse(res, [response, 200]);
}
}
);
};
// Get a specific template
TemplateMapsController.prototype.retrieve = function(req, res) {
var self = this;
if ( req.profiler && req.profiler.statsd_client ) {
req.profiler.start('windshaft-cartodb.get_template');
}
this.app.doCORS(res);
var cdbuser = cdbRequest.userByReq(req);
var tpl_id;
step(
function checkPerms(){
self.serverOptions.authorizedByAPIKey(req, this);
},
function updateTemplate(err, authenticated) {
if ( err ) throw err;
if (authenticated !== 1) {
err = new Error("Only authenticated users can get template maps");
err.http_status = 403;
throw err;
}
tpl_id = req.params.template_id.split('@');
if ( tpl_id.length > 1 ) {
if ( tpl_id[0] != cdbuser ) {
var templateNotFoundErr = new Error("Cannot get template id '" + req.params.template_id +
"' for user '" + cdbuser + "'");
templateNotFoundErr.http_status = 404;
throw templateNotFoundErr;
}
tpl_id = tpl_id[1];
}
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
},
function prepareResponse(err, tpl_val){
if ( err ) throw err;
if ( ! tpl_val ) {
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
err.http_status = 404;
throw err;
}
// auth_id was added by ourselves,
// so we remove it before returning to the user
delete tpl_val.auth_id;
return { template: tpl_val };
},
function finish(err, response){
if (err){
var statusCode = 400;
response = { error: ''+err };
if ( ! _.isUndefined(err.http_status) ) {
statusCode = err.http_status;
}
self.app.sendError(res, response, statusCode, 'GET TEMPLATE', err);
} else {
self.app.sendResponse(res, [response, 200]);
}
}
);
};
// Delete a specific template
TemplateMapsController.prototype.destroy = function(req, res) {
var self = this;
if ( req.profiler && req.profiler.statsd_client ) {
req.profiler.start('windshaft-cartodb.delete_template');
}
this.app.doCORS(res);
var cdbuser = cdbRequest.userByReq(req);
var tpl_id;
step(
function checkPerms(){
self.serverOptions.authorizedByAPIKey(req, this);
},
function updateTemplate(err, authenticated) {
if ( err ) throw err;
if (authenticated !== 1) {
err = new Error("Only authenticated users can delete template maps");
err.http_status = 403;
throw err;
}
tpl_id = req.params.template_id.split('@');
if ( tpl_id.length > 1 ) {
if ( tpl_id[0] != cdbuser ) {
var templateNotFoundErr = new Error("Cannot find template id '" + req.params.template_id +
"' for user '" + cdbuser + "'");
templateNotFoundErr.http_status = 404;
throw templateNotFoundErr;
}
tpl_id = tpl_id[1];
}
self.templateMaps.delTemplate(cdbuser, tpl_id, this);
},
function prepareResponse(err/*, tpl_val*/){
if ( err ) throw err;
return { status: 'ok' };
},
function finish(err, response){
if (err){
var statusCode = 400;
response = { error: ''+err };
if ( ! _.isUndefined(err.http_status) ) {
statusCode = err.http_status;
}
self.app.sendError(res, response, statusCode, 'DELETE TEMPLATE', err);
} else {
self.app.sendResponse(res, ['', 204]);
}
}
);
};
// Get a list of owned templates
TemplateMapsController.prototype.list = function(req, res) {
var self = this;
if ( req.profiler && req.profiler.statsd_client ) {
req.profiler.start('windshaft-cartodb.get_template_list');
}
this.app.doCORS(res);
var cdbuser = cdbRequest.userByReq(req);
step(
function checkPerms(){
self.serverOptions.authorizedByAPIKey(req, this);
},
function listTemplates(err, authenticated) {
if ( err ) throw err;
if (authenticated !== 1) {
err = new Error("Only authenticated user can list templated maps");
err.http_status = 403;
throw err;
}
self.templateMaps.listTemplates(cdbuser, this);
},
function prepareResponse(err, tpl_ids){
if ( err ) throw err;
// NOTE: might omit "cbduser" if == dbowner ...
var ids = _.map(tpl_ids, function(id) { return cdbuser + '@' + id; });
return { template_ids: ids };
},
function finish(err, response){
var statusCode = 200;
if (err){
response = { error: ''+err };
if ( ! _.isUndefined(err.http_status) ) {
statusCode = err.http_status;
}
self.app.sendError(res, response, statusCode, 'GET TEMPLATE LIST', err);
} else {
self.app.sendResponse(res, [response, statusCode]);
}
}
);
};
TemplateMapsController.prototype.instantiate = function(req, res) {
var self = this;
if ( req.profiler && req.profiler.statsd_client) {
req.profiler.start('windshaft-cartodb.instance_template_post');
}
step(
function() {
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json') {
throw new Error('template POST data must be of type application/json, it is instead ');
}
self.instantiateTemplate(req, res, req.body, this);
}, function(err, response) {
self.finish_instantiation(err, response, res, req);
}
);
};
TemplateMapsController.prototype.options = function(req, res, next) {
this.app.doCORS(res, "Content-Type");
return next();
};
/**
* jsonp endpoint, allows to instantiate a template with a json call.
* callback query argument is mandatory
*/
TemplateMapsController.prototype.jsonp = function(req, res) {
var self = this;
if ( req.profiler && req.profiler.statsd_client) {
req.profiler.start('windshaft-cartodb.instance_template_get');
}
step(
function() {
if ( req.query.callback === undefined || req.query.callback.length === 0) {
throw new Error('callback parameter should be present and be a function name');
}
var config = {};
if(req.query.config) {
try {
config = JSON.parse(req.query.config);
} catch(e) {
throw new Error('badformed config parameter, should be a valid JSON');
}
}
self.instantiateTemplate(req, res, config, this);
}, function(err, response) {
self.finish_instantiation(err, response, res, req);
}
);
};
// Instantiate a template
TemplateMapsController.prototype.instantiateTemplate = function(req, res, template_params, callback) {
var self = this;
this.app.doCORS(res);
var template;
var layergroup;
var fakereq; // used for call to createLayergroup
var cdbuser = cdbRequest.userByReq(req);
// Format of template_id: [<template_owner>]@<template_id>
var tpl_id = req.params.template_id.split('@');
if ( tpl_id.length > 1 ) {
if ( tpl_id[0] && tpl_id[0] != cdbuser ) {
var err = new Error('Cannot instanciate map of user "' + tpl_id[0] + '" on database of user "' + cdbuser +
'"');
err.http_status = 403;
callback(err);
return;
}
tpl_id = tpl_id[1];
}
var auth_token = req.query.auth_token;
step(
function getTemplate(){
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
},
function checkAuthorized(err, templateValue) {
if ( req.profiler ) req.profiler.done('getTemplate');
if ( err ) throw err;
if ( ! templateValue ) {
err = new Error("Template '" + tpl_id + "' of user '" + cdbuser + "' not found");
err.http_status = 404;
throw err;
}
template = templateValue;
var authorized = false;
try {
authorized = self.templateMaps.isAuthorized(template, auth_token);
} catch (err) {
// we catch to add http_status
err.http_status = 403;
throw err;
}
if ( ! authorized ) {
err = new Error('Unauthorized template instanciation');
err.http_status = 403;
throw err;
}
if (req.profiler) {
req.profiler.done('authorizedByCert');
}
return self.templateMaps.instance(template, template_params);
},
function prepareParams(err, instance){
if ( req.profiler ) req.profiler.done('TemplateMaps_instance');
if ( err ) throw err;
layergroup = instance;
fakereq = {
query: {},
params: {
user: req.params.user
},
headers: _.clone(req.headers),
context: _.clone(req.context),
method: req.method,
res: res,
profiler: req.profiler
};
self.setDBParams(cdbuser, fakereq.params, this);
},
function setApiKey(err){
if ( req.profiler ) req.profiler.done('setDBParams');
if ( err ) throw err;
self.metadataBackend.getUserMapKey(cdbuser, this);
},
function createLayergroup(err, val) {
if ( req.profiler ) req.profiler.done('getUserMapKey');
if ( err ) throw err;
fakereq.params.api_key = val;
self.app.createLayergroup(layergroup, fakereq, this);
},
function prepareResponse(err, layergroup) {
if ( err ) {
throw err;
}
var tplhash = self.templateMaps.fingerPrint(template).substring(0,8);
layergroup.layergroupid = cdbuser + '@' + tplhash + '@' + layergroup.layergroupid;
self.surrogateKeysCache.tag(res, new self.NamedMapsCacheEntry(cdbuser, template.name));
return layergroup;
},
callback
);
};
TemplateMapsController.prototype.finish_instantiation = function(err, response, res, req) {
if ( req.profiler ) {
res.header('X-Tiler-Profiler', req.profiler.toJSONString());
}
if (err) {
var statusCode = 400;
response = { error: ''+err };
if ( ! _.isUndefined(err.http_status) ) {
statusCode = err.http_status;
}
if(global.environment.debug) {
response.stack = err.stack;
}
this.app.sendError(res, response, statusCode, 'POST INSTANCE TEMPLATE', err);
} else {
this.app.sendResponse(res, [response, 200]);
}
};
TemplateMapsController.prototype.setDBParams = function(cdbuser, params, callback) {
var self = this;
step(
function setAuth() {
self.pgConnection.setDBAuth(cdbuser, params, this);
},
function setConn(err) {
if ( err ) throw err;
self.pgConnection.setDBConn(cdbuser, params, this);
},
function finish(err) {
callback(err);
}
);
};

View File

@@ -0,0 +1,11 @@
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

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

View File

@@ -8,18 +8,17 @@ module.exports = CdbRequest;
CdbRequest.prototype.userByReq = function(req) {
var host = req.headers.host;
var host = req.headers.host || '';
if (req.params.user) {
return req.params.user;
}
var mat = host.match(this.RE_USER_FROM_HOST);
if ( ! mat ) {
console.error("Pattern '" + this.RE_USER_FROM_HOST + "' does not match hostname '" + host + "'");
global.logger.error("Pattern '%s' does not match hostname '%s'", this.RE_USER_FROM_HOST, host);
return;
}
// console.log("Matches: "); console.dir(mat);
if ( mat.length !== 2 ) {
console.error("Pattern '" + this.RE_USER_FROM_HOST + "' gave unexpected matches against '" + host + "': ", mat);
global.logger.error("Pattern '%s' gave unexpected matches against '%s': %s", this.RE_USER_FROM_HOST, host, mat);
return;
}
return mat[1];

View File

@@ -0,0 +1,29 @@
/**
* @param {String} token might match the following pattern: {user}@{tpl_id}@{token}:{cache_buster}
*/
function parse(token) {
var signer, cacheBuster;
var tokenSplit = token.split(':');
token = tokenSplit[0];
if (tokenSplit.length > 1) {
cacheBuster = tokenSplit[1];
}
tokenSplit = token.split('@');
if ( tokenSplit.length > 1 ) {
signer = tokenSplit.shift();
if ( tokenSplit.length > 1 ) {
/*var template_hash = */tokenSplit.shift(); // unused
}
token = tokenSplit.shift();
}
return {
token: token,
signer: signer,
cacheBuster: cacheBuster
};
}
module.exports.parse = parse;

View File

@@ -0,0 +1,48 @@
var assert = require('assert');
var step = require('step');
var MapStoreMapConfigProvider = require('./map_store_provider');
/**
* @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, this);
},
function handleRenderLimits(err, renderLimits) {
assert.ifError(err);
context.limits = renderLimits;
return null;
},
function finish(err) {
return callback(err, self.mapConfig, self.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;

View File

@@ -0,0 +1,77 @@
var _ = require('underscore');
var assert = require('assert');
var dot = require('dot');
var step = require('step');
/**
* @param {MapStore} mapStore
* @param {String} user
* @param {UserLimitsApi} userLimitsApi
* @param {Object} params
* @constructor
* @type {MapStoreMapConfigProvider}
*/
function MapStoreMapConfigProvider(mapStore, user, userLimitsApi, params) {
this.mapStore = mapStore;
this.user = user;
this.userLimitsApi = userLimitsApi;
this.params = params;
this.token = params.token;
this.cacheBuster = params.cache_buster || 0;
}
module.exports = MapStoreMapConfigProvider;
MapStoreMapConfigProvider.prototype.getMapConfig = function(callback) {
var self = this;
var context = {};
step(
function prepareContextLimits() {
self.userLimitsApi.getRenderLimits(self.user, this);
},
function handleRenderLimits(err, renderLimits) {
assert.ifError(err);
context.limits = renderLimits;
return null;
},
function loadMapConfig(err) {
assert.ifError(err);
self.mapStore.load(self.token, this);
},
function finish(err, mapConfig) {
return callback(err, mapConfig, self.params, context);
}
);
};
MapStoreMapConfigProvider.prototype.getKey = function() {
return this.createKey(false);
};
MapStoreMapConfigProvider.prototype.getCacheBuster = function() {
return this.cacheBuster;
};
MapStoreMapConfigProvider.prototype.filter = function(key) {
var regex = new RegExp('^' + this.createKey(true) + '.*');
return key && key.match(regex);
};
// Configure bases for cache keys suitable for string interpolation
var baseKey = '{{=it.dbname}}:{{=it.token}}';
var rendererKey = baseKey + ':{{=it.dbuser}}:{{=it.format}}:{{=it.layer}}:{{=it.scale_factor}}';
var baseKeyTpl = dot.template(baseKey);
var rendererKeyTpl = dot.template(rendererKey);
MapStoreMapConfigProvider.prototype.createKey = function(base) {
var tplValues = _.defaults({}, this.params, {
dbname: '',
token: '',
dbuser: '',
format: '',
layer: '',
scale_factor: 1
});
return (base) ? baseKeyTpl(tplValues) : rendererKeyTpl(tplValues);
};

View File

@@ -0,0 +1,266 @@
var _ = require('underscore');
var assert = require('assert');
var crypto = require('crypto');
var dot = require('dot');
var step = require('step');
var MapConfig = require('windshaft').model.MapConfig;
var templateName = require('../../backends/template_maps').templateName;
/**
* @constructor
* @type {NamedMapMapConfigProvider}
*/
function NamedMapMapConfigProvider(templateMaps, pgConnection, userLimitsApi, queryTablesApi, namedLayersAdapter,
owner, templateId, config, authToken, params) {
this.templateMaps = templateMaps;
this.pgConnection = pgConnection;
this.userLimitsApi = userLimitsApi;
this.queryTablesApi = queryTablesApi;
this.namedLayersAdapter = namedLayersAdapter;
this.owner = owner;
this.templateName = templateName(templateId);
this.config = config;
this.authToken = authToken;
this.params = params;
this.cacheBuster = Date.now();
// use template after call to mapConfig
this.template = null;
this.affectedTablesAndLastUpdate = null;
// providing
this.err = null;
this.mapConfig = null;
this.rendererParams = null;
this.context = {};
}
module.exports = NamedMapMapConfigProvider;
NamedMapMapConfigProvider.prototype.getMapConfig = function(callback) {
if (!!this.err || this.mapConfig !== null) {
return callback(this.err, this.mapConfig, this.rendererParams, this.context);
}
var self = this;
var mapConfig = null;
var datasource = null;
var rendererParams;
step(
function getTemplate() {
self.getTemplate(this);
},
function prepareParams(err, tpl) {
assert.ifError(err);
self.template = tpl;
var templateParams = {};
if (self.config) {
try {
templateParams = _.isString(self.config) ? JSON.parse(self.config) : self.config;
} catch (e) {
throw new Error('malformed config parameter, should be a valid JSON');
}
}
return templateParams;
},
function instantiateTemplate(err, templateParams) {
assert.ifError(err);
return self.templateMaps.instance(self.template, templateParams);
},
function prepareLayergroup(err, _mapConfig) {
assert.ifError(err);
var next = this;
self.namedLayersAdapter.getLayers(self.owner, _mapConfig.layers, self.pgConnection,
function(err, layers, datasource) {
if (err) {
return next(err);
}
if (layers) {
_mapConfig.layers = layers;
}
return next(null, _mapConfig, datasource);
}
);
},
function beforeLayergroupCreate(err, _mapConfig, _datasource) {
assert.ifError(err);
mapConfig = _mapConfig;
datasource = _datasource;
rendererParams = _.extend({}, self.params, {
user: self.owner
});
self.setDBParams(self.owner, rendererParams, this);
},
function prepareContextLimits(err) {
assert.ifError(err);
self.userLimitsApi.getRenderLimits(self.owner, this);
},
function cacheAndReturnMapConfig(err, renderLimits) {
self.err = err;
self.mapConfig = (mapConfig === null) ? null : new MapConfig(mapConfig, datasource);
self.rendererParams = rendererParams;
self.context.limits = renderLimits || {};
return callback(self.err, self.mapConfig, self.rendererParams, self.context);
}
);
};
NamedMapMapConfigProvider.prototype.getTemplate = function(callback) {
var self = this;
if (!!this.err || this.template !== null) {
return callback(this.err, this.template);
}
step(
function getTemplate() {
self.templateMaps.getTemplate(self.owner, self.templateName, this);
},
function checkExists(err, tpl) {
assert.ifError(err);
if (!tpl) {
var notFoundErr = new Error(
"Template '" + self.templateName + "' of user '" + self.owner + "' not found"
);
notFoundErr.http_status = 404;
throw notFoundErr;
}
return tpl;
},
function checkAuthorized(err, tpl) {
assert.ifError(err);
var authorized = false;
try {
authorized = self.templateMaps.isAuthorized(tpl, self.authToken);
} catch (err) {
// we catch to add http_status
var authorizationFailedErr = new Error('Failed to authorize template');
authorizationFailedErr.http_status = 403;
throw authorizationFailedErr;
}
if ( ! authorized ) {
var unauthorizedErr = new Error('Unauthorized template instantiation');
unauthorizedErr.http_status = 403;
throw unauthorizedErr;
}
return tpl;
},
function cacheAndReturnTemplate(err, template) {
self.err = err;
self.template = template;
return callback(self.err, self.template);
}
);
};
NamedMapMapConfigProvider.prototype.getKey = function() {
return this.createKey(false);
};
NamedMapMapConfigProvider.prototype.getCacheBuster = function() {
return this.cacheBuster;
};
NamedMapMapConfigProvider.prototype.reset = function() {
this.template = null;
this.affectedTablesAndLastUpdate = null;
this.err = null;
this.mapConfig = null;
this.cacheBuster = Date.now();
};
NamedMapMapConfigProvider.prototype.filter = function(key) {
var regex = new RegExp('^' + this.createKey(true) + '.*');
return key && key.match(regex);
};
// Configure bases for cache keys suitable for string interpolation
var baseKey = '{{=it.dbname}}:{{=it.owner}}:{{=it.templateName}}';
var rendererKey = baseKey + ':{{=it.authToken}}:{{=it.configHash}}:{{=it.format}}:{{=it.layer}}:{{=it.scale_factor}}';
var baseKeyTpl = dot.template(baseKey);
var rendererKeyTpl = dot.template(rendererKey);
NamedMapMapConfigProvider.prototype.createKey = function(base) {
var tplValues = _.defaults({}, this.params, {
dbname: '',
owner: this.owner,
templateName: this.templateName,
authToken: this.authToken || '',
configHash: configHash(this.config),
layer: '',
scale_factor: 1
});
return (base) ? baseKeyTpl(tplValues) : rendererKeyTpl(tplValues);
};
function configHash(config) {
if (!config) {
return '';
}
return crypto.createHash('md5').update(JSON.stringify(config)).digest('hex').substring(0,8);
}
module.exports.configHash = configHash;
NamedMapMapConfigProvider.prototype.setDBParams = function(cdbuser, params, callback) {
var self = this;
step(
function setAuth() {
self.pgConnection.setDBAuth(cdbuser, params, this);
},
function setConn(err) {
assert.ifError(err);
self.pgConnection.setDBConn(cdbuser, params, this);
},
function finish(err) {
callback(err);
}
);
};
NamedMapMapConfigProvider.prototype.getTemplateName = function() {
return this.templateName;
};
NamedMapMapConfigProvider.prototype.getAffectedTablesAndLastUpdatedTime = function(callback) {
var self = this;
if (this.affectedTablesAndLastUpdate !== null) {
return callback(null, this.affectedTablesAndLastUpdate);
}
step(
function getMapConfig() {
self.getMapConfig(this);
},
function getSql(err, mapConfig) {
assert.ifError(err);
return mapConfig.getLayers().map(function(layer) {
return layer.options.sql;
}).join(';');
},
function getAffectedTables(err, sql) {
assert.ifError(err);
self.queryTablesApi.getAffectedTablesAndLastUpdatedTime(self.owner, sql, this);
},
function finish(err, result) {
self.affectedTablesAndLastUpdate = result;
return callback(err, result);
}
);
};

View File

@@ -1,6 +1,6 @@
var queue = require('queue-async');
var _ = require('underscore');
var Datasource = require('windshaft').Datasource;
var Datasource = require('windshaft').model.Datasource;
function MapConfigNamedLayersAdapter(templateMaps) {
this.templateMaps = templateMaps;
@@ -11,6 +11,10 @@ module.exports = MapConfigNamedLayersAdapter;
MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbMetadata, callback) {
var self = this;
if (!layers) {
return callback(null);
}
var adaptLayersQueue = queue(layers.length);
function adaptLayer(layer, done) {

View File

@@ -1,45 +1,19 @@
var fs = require('fs');
var step = require('step');
function HealthCheck(metadataBackend, tilelive) {
this.metadataBackend = metadataBackend;
this.tilelive = tilelive;
function HealthCheck(disableFile) {
this.disableFile = disableFile;
}
module.exports = HealthCheck;
HealthCheck.prototype.check = function(config, callback) {
var result = {
redis: {
ok: false
},
mapnik: {
ok: false
},
tile: {
ok: false
}
};
step(
function getManualDisable() {
fs.readFile(global.environment.disabled_file, this);
},
function handleDisabledFile(err, data) {
var next = this;
if (err) {
return next();
}
if (!!data) {
err = new Error(data);
err.http_status = 503;
throw err;
}
},
function handleResult(err) {
callback(err, result);
HealthCheck.prototype.check = function(callback) {
fs.readFile(this.disableFile, function handleDisabledFile(err, data) {
var disabledError = null;
if (!err) {
disabledError = new Error(data || 'Unknown error');
disabledError.http_status = 503;
}
);
return callback(disabledError);
});
};

View File

@@ -1,4 +0,0 @@
<Map
background-color="#c33"
srs="+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs">
</Map>

311
lib/cartodb/server.js Normal file
View File

@@ -0,0 +1,311 @@
var express = require('express');
var bodyParser = require('body-parser');
var RedisPool = require('redis-mpool');
var cartodbRedis = require('cartodb-redis');
var _ = require('underscore');
var controller = require('./controllers');
var SurrogateKeysCache = require('./cache/surrogate_keys_cache');
var NamedMapsCacheEntry = require('./cache/model/named_maps_entry');
var VarnishHttpCacheBackend = require('./cache/backend/varnish_http');
var FastlyCacheBackend = require('./cache/backend/fastly');
var StatsClient = require('./stats/client');
var Profiler = require('./stats/profiler_proxy');
var RendererStatsReporter = require('./stats/reporter/renderer');
var windshaft = require('windshaft');
var mapnik = windshaft.mapnik;
var TemplateMaps = require('./backends/template_maps.js');
var QueryTablesApi = require('./api/query_tables_api');
var UserLimitsApi = require('./api/user_limits_api');
var AuthApi = require('./api/auth_api');
var LayergroupAffectedTablesCache = require('./cache/layergroup_affected_tables');
var NamedMapProviderCache = require('./cache/named_map_provider_cache');
var PgQueryRunner = require('./backends/pg_query_runner');
var PgConnection = require('./backends/pg_connection');
var timeoutErrorTilePath = __dirname + '/../../assets/render-timeout-fallback.png';
var timeoutErrorTile = require('fs').readFileSync(timeoutErrorTilePath, {encoding: null});
module.exports = function(serverOptions) {
// Make stats client globally accessible
global.statsClient = StatsClient.getInstance(serverOptions.statsd);
var redisPool = new RedisPool(_.defaults(global.environment.redis, {
name: 'windshaft:server',
unwatchOnRelease: false,
noReadyCheck: true
}));
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);
});
var metadataBackend = cartodbRedis({pool: redisPool});
var pgConnection = new PgConnection(metadataBackend);
var pgQueryRunner = new PgQueryRunner(pgConnection);
var queryTablesApi = new QueryTablesApi(pgQueryRunner);
var userLimitsApi = new UserLimitsApi(metadataBackend, {
limits: {
cacheOnTimeout: serverOptions.renderer.mapnik.limits.cacheOnTimeout || false,
render: serverOptions.renderer.mapnik.limits.render || 0
}
});
var templateMaps = new TemplateMaps(redisPool, {
max_user_templates: global.environment.maxUserTemplates
});
var surrogateKeysCache = new SurrogateKeysCache(surrogateKeysCacheBackends(serverOptions));
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);
});
serverOptions.grainstore.mapnik_version = mapnikVersion(serverOptions);
validateOptions(serverOptions);
bootstrapFonts(serverOptions);
// initialize express server
var app = bootstrap(serverOptions);
// Extend windshaft with all the elements of the options object
_.extend(app, serverOptions);
var mapStore = new windshaft.storage.MapStore({
pool: redisPool,
expire_time: serverOptions.grainstore.default_layergroup_ttl
});
var onTileErrorStrategy;
if (global.environment.enabledFeatures.onTileErrorStrategy !== false) {
onTileErrorStrategy = function onTileErrorStrategy$TimeoutTile(err, tile, headers, stats, format, callback) {
if (err && err.message === 'Render timed out' && format === 'png') {
return callback(null, timeoutErrorTile, { 'Content-Type': 'image/png' }, {});
} else {
return callback(err, tile, headers, stats);
}
};
}
var rendererFactory = new windshaft.renderer.Factory({
onTileErrorStrategy: onTileErrorStrategy,
mapnik: {
redisPool: redisPool,
grainstore: serverOptions.grainstore,
mapnik: serverOptions.renderer.mapnik
},
http: serverOptions.renderer.http
});
// initialize render cache
var rendererCacheOpts = _.defaults(serverOptions.renderCache || {}, {
ttl: 60000, // 60 seconds TTL by default
statsInterval: 60000 // reports stats every milliseconds defined here
});
var rendererCache = new windshaft.cache.RendererCache(rendererFactory, rendererCacheOpts);
var rendererStatsReporter = new RendererStatsReporter(rendererCache, rendererCacheOpts.statsInterval);
rendererStatsReporter.start();
var attributesBackend = new windshaft.backend.Attributes(mapStore);
var previewBackend = new windshaft.backend.Preview(rendererCache);
var tileBackend = new windshaft.backend.Tile(rendererCache);
var mapValidatorBackend = new windshaft.backend.MapValidator(tileBackend, attributesBackend);
var mapBackend = new windshaft.backend.Map(rendererCache, mapStore, mapValidatorBackend);
var layergroupAffectedTablesCache = new LayergroupAffectedTablesCache();
app.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
var namedMapProviderCache = new NamedMapProviderCache(templateMaps, pgConnection, userLimitsApi, queryTablesApi);
['update', 'delete'].forEach(function(eventType) {
templateMaps.on(eventType, namedMapProviderCache.invalidate.bind(namedMapProviderCache));
});
var authApi = new AuthApi(pgConnection, metadataBackend, mapStore, templateMaps);
var TablesExtentApi = require('./api/tables_extent_api');
var tablesExtentApi = new TablesExtentApi(pgQueryRunner);
/*******************************************************************************************************************
* Routing
******************************************************************************************************************/
new controller.Layergroup(
authApi,
pgConnection,
mapStore,
tileBackend,
previewBackend,
attributesBackend,
surrogateKeysCache,
userLimitsApi,
queryTablesApi,
layergroupAffectedTablesCache
).register(app);
new controller.Map(
authApi,
pgConnection,
templateMaps,
mapBackend,
metadataBackend,
queryTablesApi,
surrogateKeysCache,
userLimitsApi,
layergroupAffectedTablesCache
).register(app);
new controller.NamedMaps(
authApi,
pgConnection,
namedMapProviderCache,
tileBackend,
previewBackend,
surrogateKeysCache,
tablesExtentApi,
metadataBackend
).register(app);
new controller.NamedMapsAdmin(authApi, pgConnection, templateMaps).register(app);
new controller.ServerInfo().register(app);
/*******************************************************************************************************************
* END Routing
******************************************************************************************************************/
return app;
};
function validateOptions(opts) {
if (!_.isString(opts.base_url) || !_.isString(opts.base_url_mapconfig) || !_.isString(opts.base_url_templated)) {
throw new Error("Must initialise server with: 'base_url'/'base_url_mapconfig'/'base_url_templated' URLs");
}
// Be nice and warn if configured mapnik version is != instaled mapnik version
if (mapnik.versions.mapnik !== opts.grainstore.mapnik_version) {
console.warn('WARNING: detected mapnik version (' + mapnik.versions.mapnik + ')' +
' != configured mapnik version (' + opts.grainstore.mapnik_version + ')');
}
}
function bootstrapFonts(opts) {
// Set carto renderer configuration for MMLStore
opts.grainstore.carto_env = opts.grainstore.carto_env || {};
var cenv = opts.grainstore.carto_env;
cenv.validation_data = cenv.validation_data || {};
if ( ! cenv.validation_data.fonts ) {
mapnik.register_system_fonts();
mapnik.register_default_fonts();
cenv.validation_data.fonts = _.keys(mapnik.fontFiles());
}
}
function bootstrap(opts) {
var app;
if (_.isObject(opts.https)) {
// use https if possible
app = express.createServer(opts.https);
} else {
// fall back to http by default
app = express();
}
app.enable('jsonp callback');
app.disable('x-powered-by');
app.disable('etag');
app.use(bodyParser.json());
app.use(function bootstrap$prepareRequestResponse(req, res, next) {
req.context = req.context || {};
req.profiler = new Profiler({
statsd_client: global.statsClient,
profile: opts.useProfiler
});
if (global.environment && global.environment.api_hostname) {
res.set('X-Served-By-Host', global.environment.api_hostname);
}
next();
});
// temporary measure until we upgrade to newer version expressjs so we can check err.status
app.use(function(err, req, res, next) {
if (err) {
if (err.name === 'SyntaxError') {
res.status(400).json({ errors: [err.name + ': ' + err.message] });
} else {
next(err);
}
} else {
next();
}
});
setupLogger(app, opts);
return app;
}
function setupLogger(app, opts) {
if (global.log4js && opts.log_format) {
var loggerOpts = {
// 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: !opts.unbuffered_logging,
// optional log format
format: opts.log_format
};
app.use(global.log4js.connectLogger(global.log4js.getLogger(), _.defaults(loggerOpts, {level: 'info'})));
}
}
function surrogateKeysCacheBackends(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;
}
function mapnikVersion(opts) {
return opts.grainstore.mapnik_version || mapnik.versions.mapnik;
}

View File

@@ -1,51 +1,34 @@
var os = require('os');
var _ = require('underscore');
var step = require('step');
var LZMA = require('lzma').LZMA;
var assert = require('assert');
var RedisPool = require('redis-mpool');
var QueryTablesApi = require('./api/query_tables_api');
var PgConnection = require('./backends/pg_connection');
var TemplateMaps = require('./template_maps.js');
var MapConfigNamedLayersAdapter = require('./models/mapconfig_named_layers_adapter');
var CdbRequest = require('./models/cdb_request');
var rendererConfig = _.defaults(global.environment.renderer || {}, {
cache_ttl: 60000, // milliseconds
statsInterval: 60000,
mapnik: {
poolSize: 8,
metatile: 2,
bufferSize: 64,
snapToGrid: false,
clipByBox2d: false,
limits: {}
},
http: {}
});
var timeoutErrorTilePath = __dirname + '/../../assets/render-timeout-fallback.png';
var timeoutErrorTile = require('fs').readFileSync(timeoutErrorTilePath, {encoding: null});
// Perform keyword substitution in statsd
// See https://github.com/CartoDB/Windshaft-cartodb/issues/153
if ( global.environment.statsd ) {
if ( global.environment.statsd.prefix ) {
var host_token = os.hostname().split('.').reverse().join('.');
global.environment.statsd.prefix = global.environment.statsd.prefix.replace(/:host/, host_token);
}
}
// Whitelist query parameters and attach format
var REQUEST_QUERY_PARAMS_WHITELIST = [
'config',
'map_key',
'api_key',
'auth_token',
'callback'
];
module.exports = function(redisPool) {
redisPool = redisPool || new RedisPool(_.extend(global.environment.redis, {name: 'windshaft:server_options'}));
var cartoData = require('cartodb-redis')({ pool: redisPool }),
lzmaWorker = new LZMA(),
pgConnection = new PgConnection(cartoData),
queryTablesApi = new QueryTablesApi(pgConnection, cartoData),
cdbRequest = new CdbRequest();
var rendererConfig = _.defaults(global.environment.renderer || {}, {
cache_ttl: 60000, // milliseconds
statsInterval: 60000,
mapnik: {
poolSize: 8,
metatile: 2,
bufferSize: 64,
snapToGrid: false,
clipByBox2d: false,
limits: {}
},
http: {}
});
var me = {
module.exports = {
bind: {
port: global.environment.port,
host: global.environment.host
},
// This is for inline maps and table maps
base_url: global.environment.base_url_legacy || '/tiles/:table',
@@ -59,6 +42,8 @@ module.exports = function(redisPool) {
//
base_url_mapconfig: global.environment.base_url_detached || '(?:/maps|/tiles/layergroup)',
base_url_templated: global.environment.base_url_templated || '(?:/maps/named|/tiles/template)',
grainstore: {
map: {
// TODO: allow to specify in configuration
@@ -77,546 +62,19 @@ module.exports = function(redisPool) {
},
renderer: {
mapnik: rendererConfig.mapnik,
torque: rendererConfig.torque,
http: rendererConfig.http
},
redis: global.environment.redis,
// Do not send unwatch on release. See http://github.com/CartoDB/Windshaft-cartodb/issues/161
redis: _.extend(global.environment.redis, {unwatchOnRelease: false}),
enable_cors: global.environment.enable_cors,
varnish_host: global.environment.varnish.host,
varnish_port: global.environment.varnish.port,
varnish_http_port: global.environment.varnish.http_port,
varnish_secret: global.environment.varnish.secret,
varnish_purge_enabled: global.environment.varnish.purge_enabled,
fastly: global.environment.fastly || {},
cache_enabled: global.environment.cache_enabled,
log_format: global.environment.log_format,
useProfiler: global.environment.useProfiler
};
// Do not send unwatch on release
// See http://github.com/CartoDB/Windshaft-cartodb/issues/161
me.redis.unwatchOnRelease = false;
// Re-use redisPool
me.redis.pool = redisPool;
// Re-use pgConnection
me.pgConnection = pgConnection;
var templateMaps = new TemplateMaps(redisPool, {
max_user_templates: global.environment.maxUserTemplates
});
me.templateMaps = templateMaps;
var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
/* This whole block is about generating X-Cache-Channel { */
// TODO: review lifetime of elements of this cache
// NOTE: by-token indices should only be dropped when
// the corresponding layegroup is dropped, because
// we have no SQL after layer creation.
me.channelCache = {};
me.buildCacheChannel = function (dbName, tableNames){
return dbName + ':' + tableNames.join(',');
};
me.generateCacheChannel = function(app, req, callback){
// Build channelCache key
var dbName = req.params.dbname;
var cacheKey = [ dbName, req.params.token ].join(':');
// no token means no tables associated
if (!req.params.token) {
return callback(null, this.buildCacheChannel(dbName, []));
}
step(
function checkCached() {
if ( me.channelCache.hasOwnProperty(cacheKey) ) {
return callback(null, me.channelCache[cacheKey]);
}
return null;
},
function extractSQL(err) {
assert.ifError(err);
// TODO: cached cache channel for token-based access should
// be constructed at renderer cache creation time
// See http://github.com/CartoDB/Windshaft-cartodb/issues/152
if ( ! app.mapStore ) {
throw new Error('missing channel cache for token ' + req.params.token);
}
var mapStore = app.mapStore;
step(
function loadFromStore() {
mapStore.load(req.params.token, this);
},
function getSQL(err, mapConfig) {
if (req.profiler) {
req.profiler.done('mapStore_load');
}
assert.ifError(err);
var queries = mapConfig.getLayers()
.map(function(lyr) {
return lyr.options.sql;
})
.filter(function(sql) {
return !!sql;
});
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");
}
queryTablesApi.getAffectedTablesInQuery(cdbRequest.userByReq(req), sql, this); // in addCacheChannel
},
function buildCacheChannel(err, tableNames) {
assert.ifError(err);
if (req.profiler) {
req.profiler.done('affectedTables');
}
var cacheChannel = me.buildCacheChannel(dbName,tableNames);
me.channelCache[cacheKey] = cacheChannel;
return cacheChannel;
},
function finish(err, cacheChannel) {
callback(err, cacheChannel);
}
);
};
// Set the cache chanel info to invalidate the cache on the frontend server
//
// @param req The request object.
// The function will have no effect unless req.res exists.
// It is expected that req.params contains 'table' and 'dbname'
//
// @param cb function(err, channel) will be called when ready.
// the channel parameter will be null if nothing was added
//
me.addCacheChannel = function(app, req, cb) {
// skip non-GET requests, or requests for which there's no response
if ( req.method != 'GET' || ! req.res ) { cb(null, null); return; }
if (req.profiler) {
req.profiler.start('addCacheChannel');
}
var res = req.res;
if ( req.params.token ) {
res.header('Cache-Control', 'public,max-age=31536000'); // 1 year
} else {
var ttl = global.environment.varnish.ttl || 86400;
res.header('Cache-Control', 'no-cache,max-age='+ttl+',must-revalidate, public');
}
// Set Last-Modified header
var lastUpdated;
if ( req.params.cache_buster ) {
// Assuming cache_buster is a timestamp
// FIXME: store lastModified in the cache channel instead
lastUpdated = new Date(parseInt(req.params.cache_buster));
} else {
lastUpdated = new Date();
}
res.header('Last-Modified', lastUpdated.toUTCString());
me.generateCacheChannel(app, req, function(err, channel){
if (req.profiler) {
req.profiler.done('generateCacheChannel');
req.profiler.end();
}
if ( ! err ) {
res.header('X-Cache-Channel', channel);
cb(null, channel);
} else {
console.log('ERROR generating cache channel: ' + ( err.message ? err.message : err ));
// TODO: evaluate if we should bubble up the error instead
cb(null, 'ERROR');
}
});
};
if (global.environment.enabledFeatures.onTileErrorStrategy !== false) {
me.renderer.onTileErrorStrategy = function(err, tile, headers, stats, format, callback) {
if (err && err.message === 'Render timed out' && format === 'png') {
return callback(null, timeoutErrorTile, { 'Content-Type': 'image/png' }, {});
} else {
return callback(err, tile, headers, stats);
}
};
}
me.renderCache.beforeRendererCreate = function(req, callback) {
var user = cdbRequest.userByReq(req);
var rendererOptions = {};
step(
function getLimits(err) {
assert.ifError(err);
cartoData.getTilerRenderLimit(user, this);
},
function handleTilerLimits(err, renderLimit) {
assert.ifError(err);
rendererOptions.limits = {
cacheOnTimeout: rendererConfig.mapnik.limits.cacheOnTimeout || false,
render: renderLimit || rendererConfig.mapnik.limits.render || 0
};
return null;
},
function finish(err) {
if (err) {
return callback(err);
}
return callback(null, rendererOptions);
}
);
};
me.beforeLayergroupCreate = function(req, requestMapConfig, callback) {
mapConfigNamedLayersAdapter.getLayers(cdbRequest.userByReq(req), requestMapConfig.layers, pgConnection,
function(err, layers, datasource) {
if (err) {
return callback(err);
}
requestMapConfig.layers = layers;
return callback(null, requestMapConfig, datasource);
}
);
};
me.afterLayergroupCreate = function(req, mapconfig, response, callback) {
var token = response.layergroupid;
var username = cdbRequest.userByReq(req);
var tasksleft = 2; // redis key and affectedTables
var errors = [];
var done = function(err) {
if ( err ) {
errors.push('' + err);
}
if ( ! --tasksleft ) {
err = errors.length ? new Error(errors.join('\n')) : null;
callback(err);
}
};
// 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
var serverMetadata = global.environment.serverMetadata;
if (serverMetadata) {
_.extend(response, serverMetadata);
}
// Don't wait for the mapview count increment to
// take place before proceeding. Error will be logged
// asyncronously
cartoData.incMapviewCount(username, mapconfig.stat_tag, function(err) {
if (req.profiler) {
req.profiler.done('incMapviewCount');
}
if ( err ) {
console.log("ERROR: failed to increment mapview count for user '" + username + "': " + err);
}
done();
});
var sql = mapconfig.layers.map(function(layer) {
return layer.options.sql;
}).join(';');
var dbName = req.params.dbname;
var cacheKey = dbName + ':' + token;
step(
function getAffectedTablesAndLastUpdatedTime() {
queryTablesApi.getAffectedTablesAndLastUpdatedTime(username, sql, this);
},
function handleAffectedTablesAndLastUpdatedTime(err, result) {
if (req.profiler) {
req.profiler.done('queryTablesAndLastUpdated');
}
assert.ifError(err);
var cacheChannel = me.buildCacheChannel(dbName, result.affectedTables);
me.channelCache[cacheKey] = cacheChannel;
if (req.res && req.method == 'GET') {
var res = req.res;
var ttl = global.environment.varnish.layergroupTtl || 86400;
res.header('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
res.header('Last-Modified', (new Date()).toUTCString());
res.header('X-Cache-Channel', cacheChannel);
}
// last update for layergroup cache buster
response.layergroupid = response.layergroupid + ':' + result.lastUpdatedTime;
response.last_updated = new Date(result.lastUpdatedTime).toISOString();
return null;
},
function finish(err) {
done(err);
}
);
};
/* X-Cache-Channel generation } */
// Check if a request is authorized by a signer
//
// @param req express request 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.
//
me.authorizedBySigner = function(req, callback) {
if ( ! req.params.token || ! req.params.signer ) {
return callback(null, null); // no signer requested
}
var signer = req.params.signer;
var layergroup_id = req.params.token;
var auth_token = req.params.auth_token;
var mapStore = req.app.mapStore;
if (!mapStore) {
throw new Error('Unable to retrieve map configuration token');
}
mapStore.load(layergroup_id, function(err, mapConfig) {
assert.ifError(err);
var authorized = me.templateMaps.isAuthorized(mapConfig.obj().template, auth_token);
return callback(null, authorized ? signer : null);
});
};
// Check if a request is authorized by api_key
//
// @param req express request object
// @param callback function(err, authorized)
// NOTE: authorized is expected to be 0 or 1 (integer)
//
me.authorizedByAPIKey = function(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 ) {
callback(null, 0); // no api key, no authorization...
return;
}
//console.log("given ApiKey: " + givenKey);
var user = cdbRequest.userByReq(req);
step(
function (){
cartoData.getUserMapKey(user, this);
},
function checkApiKey(err, val){
assert.ifError(err);
return ( val && givenKey == val ) ? 1 : 0;
},
function finish(err, authorized) {
callback(err, authorized);
}
);
};
/**
* Check access authorization
*
* @param req - standard req object. Importantly contains table and host information
* @param callback function(err, allowed) is access allowed not?
*/
me.authorize = function(req, callback) {
var that = this;
var user = cdbRequest.userByReq(req);
step(
function (){
that.authorizedByAPIKey(req, this);
},
function checkApiKey(err, authorized){
if (req.profiler) {
req.profiler.done('authorizedByAPIKey');
}
assert.ifError(err);
// if not authorized by api_key, continue
if (authorized !== 1) {
// not authorized by api_key,
// check if authorized by signer
that.authorizedBySigner(req, this);
return;
}
// authorized by api key, login as the given username and stop
pgConnection.setDBAuth(user, req.params, function(err) {
callback(err, true); // authorized (or error)
});
},
function checkSignAuthorized(err, signed_by){
assert.ifError(err);
if ( ! signed_by ) {
// request not authorized by signer.
// if no signer name was given, let dbparams and
// PostgreSQL do the rest.
//
if ( ! req.params.signer ) {
callback(null, true); // authorized so far
return;
}
// if signer name was given, return no authorization
callback(null, false);
return;
}
pgConnection.setDBAuth(signed_by, req.params, function(err) {
if (req.profiler) {
req.profiler.done('setDBAuth');
}
callback(err, true); // authorized (or error)
});
}
);
};
// jshint maxcomplexity:10
/**
* Whitelist input and get database name & default geometry type from
* subdomain/user metadata held in CartoDB Redis
* @param req - standard express request obj. Should have host & table
* @param callback
*/
me.req2params = function(req, callback){
if ( req.query.lzma ) {
// Decode (from base64)
var lzma = new Buffer(req.query.lzma, 'base64')
.toString('binary')
.split('')
.map(function(c) {
return c.charCodeAt(0) - 128;
});
// Decompress
lzmaWorker.decompress(
lzma,
function(result) {
if (req.profiler) {
req.profiler.done('lzma');
}
try {
delete req.query.lzma;
_.extend(req.query, JSON.parse(result));
me.req2params(req, callback);
} catch (err) {
callback(new Error('Error parsing lzma as JSON: ' + err));
}
}
);
return;
}
req.query = _.pick(req.query, REQUEST_QUERY_PARAMS_WHITELIST);
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
var user = cdbRequest.userByReq(req);
if ( req.params.token ) {
//console.log("Request parameters include token " + req.params.token);
var tksplit = req.params.token.split(':');
req.params.token = tksplit[0];
if ( tksplit.length > 1 ) {
req.params.cache_buster= tksplit[1];
}
tksplit = req.params.token.split('@');
if ( tksplit.length > 1 ) {
req.params.signer = tksplit.shift();
if ( ! req.params.signer ) {
req.params.signer = user;
}
else if ( req.params.signer !== user ) {
var err = new Error('Cannot use map signature of user "' + req.params.signer + '" on database of user "' +
user + '"');
err.http_status = 403;
callback(err);
return;
}
if ( tksplit.length > 1 ) {
/*var template_hash = */tksplit.shift(); // unused
}
req.params.token = tksplit.shift();
//console.log("Request for token " + req.params.token + " with signature from " + req.params.signer);
}
}
// bring all query values onto req.params object
_.extend(req.params, req.query);
if (req.profiler) {
req.profiler.done('req2params.setup');
}
step(
function getPrivacy(){
me.authorize(req, this);
},
function gatekeep(err, authorized){
if (req.profiler) {
req.profiler.done('authorize');
}
assert.ifError(err);
if(!authorized) {
err = new Error("Sorry, you are unauthorized (permission denied)");
err.http_status = 403;
throw err;
}
return null;
},
function getDatabase(err){
assert.ifError(err);
pgConnection.setDBConn(user, req.params, this);
},
function finishSetup(err) {
if ( err ) { callback(err, req); return; }
// Add default database connection parameters
// if none given
_.defaults(req.params, {
dbuser: global.environment.postgres.user,
dbpassword: global.environment.postgres.password,
dbhost: global.environment.postgres.host,
dbport: global.environment.postgres.port
});
callback(null, req);
}
);
};
return me;
};

View File

@@ -0,0 +1,73 @@
var _ = require('underscore');
var debug = require('debug')('windshaft:stats_client');
var StatsD = require('node-statsd').StatsD;
module.exports = {
/**
* Returns an StatsD instance or an stub object that replicates the StatsD public interface so there is no need to
* keep checking if the stats_client is instantiated or not.
*
* The first call to this method implies all future calls will use the config specified in the very first call.
*
* TODO: It's far from ideal to use make this a singleton, improvement desired.
* We proceed this way to be able to use StatsD from several places sharing one single StatsD instance.
*
* @param config Configuration for StatsD, if undefined it will return an stub
* @returns {StatsD|Object}
*/
getInstance: function(config) {
if (!this.instance) {
var instance;
if (config) {
instance = new StatsD(config);
instance.last_error = { msg: '', count: 0 };
instance.socket.on('error', function (err) {
var last_err = instance.last_error;
var last_msg = last_err.msg;
var this_msg = '' + err;
if (this_msg !== last_msg) {
debug("statsd client socket error: " + err);
instance.last_error.count = 1;
instance.last_error.msg = this_msg;
} else {
++last_err.count;
if (!last_err.interval) {
instance.last_error.interval = setInterval(function () {
var count = instance.last_error.count;
if (count > 1) {
debug("last statsd client socket error repeated " + count + " times");
instance.last_error.count = 1;
clearInterval(instance.last_error.interval);
instance.last_error.interval = null;
}
}, 1000);
}
}
});
} else {
var stubFunc = function (stat, value, sampleRate, callback) {
if (_.isFunction(callback)) {
callback(null, 0);
}
};
instance = {
timing: stubFunc,
increment: stubFunc,
decrement: stubFunc,
gauge: stubFunc,
unique: stubFunc,
set: stubFunc,
sendAll: stubFunc,
send: stubFunc
};
}
this.instance = instance;
}
return this.instance;
}
};

View File

@@ -0,0 +1,53 @@
var Profiler = require('step-profiler');
/**
* Proxy to encapsulate node-step-profiler module so there is no need to check if there is an instance
*/
function ProfilerProxy(opts) {
this.profile = !!opts.profile;
this.profiler = null;
if (!!opts.profile) {
this.profiler = new Profiler({statsd_client: opts.statsd_client});
}
}
ProfilerProxy.prototype.done = function(what) {
if (this.profile) {
this.profiler.done(what);
}
};
ProfilerProxy.prototype.end = function() {
if (this.profile) {
this.profiler.end();
}
};
ProfilerProxy.prototype.start = function(what) {
if (this.profile) {
this.profiler.start(what);
}
};
ProfilerProxy.prototype.add = function(what) {
if (this.profile) {
this.profiler.add(what || {});
}
};
ProfilerProxy.prototype.sendStats = function() {
if (this.profile) {
this.profiler.sendStats();
}
};
ProfilerProxy.prototype.toString = function() {
return this.profile ? this.profiler.toString() : "";
};
ProfilerProxy.prototype.toJSONString = function() {
return this.profile ? this.profiler.toJSONString() : "{}";
};
module.exports = ProfilerProxy;

View File

@@ -0,0 +1,82 @@
// - Reports stats about:
// * Total number of renderers
// * For mapnik renderers:
// - the mapnik-pool status: count, unused and waiting
// - the internally cached objects: png and grid
var _ = require('underscore');
function RendererStatsReporter(rendererCache, statsInterval) {
this.rendererCache = rendererCache;
this.statsInterval = statsInterval || 6e4;
this.renderersStatsIntervalId = null;
}
module.exports = RendererStatsReporter;
RendererStatsReporter.prototype.start = function() {
var self = this;
this.renderersStatsIntervalId = setInterval(function() {
var rendererCacheEntries = self.rendererCache.renderers;
if (!rendererCacheEntries) {
return null;
}
global.statsClient.gauge('windshaft.rendercache.count', _.keys(rendererCacheEntries).length);
var renderersStats = _.reduce(rendererCacheEntries, function(_rendererStats, cacheEntry) {
var stats = cacheEntry.renderer && cacheEntry.renderer.getStats && cacheEntry.renderer.getStats();
if (!stats) {
return _rendererStats;
}
_rendererStats.pool.count += stats.pool.count;
_rendererStats.pool.unused += stats.pool.unused;
_rendererStats.pool.waiting += stats.pool.waiting;
_rendererStats.cache.grid += stats.cache.grid;
_rendererStats.cache.png += stats.cache.png;
return _rendererStats;
},
{
pool: {
count: 0,
unused: 0,
waiting: 0
},
cache: {
png: 0,
grid: 0
}
}
);
global.statsClient.gauge('windshaft.mapnik-cache.png', renderersStats.cache.png);
global.statsClient.gauge('windshaft.mapnik-cache.grid', renderersStats.cache.grid);
global.statsClient.gauge('windshaft.mapnik-pool.count', renderersStats.pool.count);
global.statsClient.gauge('windshaft.mapnik-pool.unused', renderersStats.pool.unused);
global.statsClient.gauge('windshaft.mapnik-pool.waiting', renderersStats.pool.waiting);
}, this.statsInterval);
this.rendererCache.on('err', rendererCacheErrorListener);
this.rendererCache.on('gc', gcTimingListener);
};
function rendererCacheErrorListener() {
global.statsClient.increment('windshaft.rendercache.error');
}
function gcTimingListener(gcTime) {
global.statsClient.timing('windshaft.rendercache.gc', gcTime);
}
RendererStatsReporter.prototype.stop = function() {
this.rendererCache.removeListener('err', rendererCacheErrorListener);
this.rendererCache.removeListener('gc', gcTimingListener);
clearInterval(this.renderersStatsIntervalId);
this.renderersStatsIntervalId = null;
};

2406
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "windshaft-cartodb",
"version": "2.1.0",
"version": "2.15.1",
"description": "A map tile server for CartoDB",
"keywords": [
"cartodb"
@@ -22,22 +22,29 @@
"Sandro Santilli <strk@vizzuality.com>"
],
"dependencies": {
"express": "~4.13.3",
"body-parser": "~1.14.0",
"debug": "~2.2.0",
"step-profiler": "~0.2.1",
"node-statsd": "~0.0.7",
"underscore" : "~1.6.0",
"dot": "~1.0.2",
"windshaft": "0.42.0",
"step": "~0.0.5",
"windshaft": "~1.1.1",
"step": "~0.0.6",
"queue-async": "~1.0.7",
"request": "~2.9.203",
"cartodb-redis": "~0.12.1",
"request": "~2.62.0",
"cartodb-redis": "~0.13.0",
"cartodb-psql": "~0.4.0",
"redis-mpool": "~0.3.0",
"fastly-purge": "~1.0.1",
"redis-mpool": "~0.4.0",
"lru-cache": "2.6.5",
"lzma": "~1.3.7",
"log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb"
},
"devDependencies": {
"istanbul": "~0.3.6",
"mocha": "~1.21.4",
"nock": "~1.3.0",
"nock": "~2.11.0",
"jshint": "~2.6.0",
"redis": "~0.8.6",
"strftime": "~0.8.2",

View File

@@ -24,7 +24,10 @@ cleanup() {
return;
fi
fi
redis-cli -p ${REDIS_PORT} info stats
redis-cli -p ${REDIS_PORT} info keyspace
echo "Killing test redis pid ${PID_REDIS}"
#kill ${PID_REDIS_MONITOR}
kill ${PID_REDIS}
fi
if test x"$OPT_DROP_PGSQL" = xyes; then
@@ -118,6 +121,9 @@ cd -
PATH=node_modules/.bin/:$PATH
#redis-cli -p ${REDIS_PORT} monitor > /tmp/windshaft-cartodb.redis.log &
#PID_REDIS_MONITOR=$!
if test x"$OPT_COVERAGE" = xyes; then
echo "Running tests with coverage"
./node_modules/.bin/istanbul cover node_modules/.bin/_mocha -- -u tdd -t 5000 ${TESTS}

View File

@@ -1,64 +1,84 @@
require(__dirname + '/../../support/test_helper');
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var redis = require('redis');
var step = require('step');
var FastlyPurge = require('fastly-purge');
var _ = require('underscore');
var NamedMapsCacheEntry = require(__dirname + '/../../../lib/cartodb/cache/model/named_maps_entry');
var CartodbWindshaft = require(__dirname + '/../../../lib/cartodb/cartodb_windshaft');
var CartodbWindshaft = require(__dirname + '/../../../lib/cartodb/server');
describe('templates surrogate keys', function() {
var redisClient = redis.createClient(global.environment.redis.port);
var serverOptions = require('../../../lib/cartodb/server_options');
// Enable Varnish purge for tests
var varnishHost = global.environment.varnish.host;
global.environment.varnish.host = '127.0.0.1';
var varnishPurgeEnabled = global.environment.varnish.purge_enabled;
global.environment.varnish.purge_enabled = true;
var varnishHost = serverOptions.varnish_host;
serverOptions.varnish_host = '127.0.0.1';
var varnishPurgeEnabled = serverOptions.varnish_purge_enabled;
serverOptions.varnish_purge_enabled = true;
var fastlyConfig = serverOptions.fastly;
var FAKE_FASTLY_API_KEY = 'fastly-api-key';
var FAKE_FASTLY_SERVICE_ID = 'fake-service-id';
serverOptions.fastly = {
enabled: true,
// the fastly api key
apiKey: FAKE_FASTLY_API_KEY,
// the service that will get surrogate key invalidation
serviceId: FAKE_FASTLY_SERVICE_ID
};
var serverOptions = require('../../../lib/cartodb/server_options')();
var server = new CartodbWindshaft(serverOptions);
var templateOwner = 'localhost',
templateName = 'acceptance',
expectedTemplateId = templateOwner + '@' + templateName,
template = {
version: '0.0.1',
name: templateName,
auth: {
method: 'open'
},
layergroup: {
version: '1.2.0',
layers: [
{
options: {
sql: 'select 1 cartodb_id, null::geometry as the_geom_webmercator',
cartocss: '#layer { marker-fill:blue; }',
cartocss_version: '2.3.0'
}
}
]
}
var templateOwner = 'localhost';
var templateName = 'acceptance';
var expectedTemplateId = templateName;
var template = {
version: '0.0.1',
name: templateName,
auth: {
method: 'open'
},
expectedBody = { template_id: expectedTemplateId };
layergroup: {
version: '1.2.0',
layers: [
{
options: {
sql: 'select 1 cartodb_id, null::geometry as the_geom_webmercator',
cartocss: '#layer { marker-fill:blue; }',
cartocss_version: '2.3.0'
}
}
]
}
};
var templateUpdated = _.extend({}, template, {layergroup: {layers: [{
type: 'plain',
options: {
color: 'red'
}
}]} });
var expectedBody = { template_id: expectedTemplateId };
var varnishHttpUrl = [
'http://', global.environment.varnish.host, ':', global.environment.varnish.http_port
'http://', serverOptions.varnish_host, ':', serverOptions.varnish_http_port
].join('');
var cacheEntryKey = new NamedMapsCacheEntry(templateOwner, templateName).key();
var invalidationMatchHeader = '\\b' + cacheEntryKey + '\\b';
var fastlyPurgePath = '/service/' + FAKE_FASTLY_SERVICE_ID + '/purge/' + cacheEntryKey;
var nock = require('nock');
nock.enableNetConnect(/(127.0.0.1:5555|cartocdn.com)/);
after(function(done) {
serverOptions.varnish_purge_enabled = false;
global.environment.varnish.host = varnishHost;
global.environment.varnish.purge_enabled = varnishPurgeEnabled;
serverOptions.varnish_host = varnishHost;
serverOptions.varnish_purge_enabled = varnishPurgeEnabled;
serverOptions.fastly = fastlyConfig;
nock.restore();
done();
@@ -109,6 +129,14 @@ describe('templates surrogate keys', function() {
.matchHeader('Invalidation-Match', invalidationMatchHeader)
.reply(204, '');
var fastlyScope = nock(FastlyPurge.FASTLY_API_ENDPOINT)
.post(fastlyPurgePath)
.matchHeader('Fastly-Key', FAKE_FASTLY_API_KEY)
.matchHeader('Accept', 'application/json')
.reply(200, {
status:'ok'
});
step(
function createTemplateToUpdate() {
createTemplate(this);
@@ -124,7 +152,7 @@ describe('templates surrogate keys', function() {
host: templateOwner,
'Content-Type': 'application/json'
},
data: JSON.stringify(template)
data: JSON.stringify(templateUpdated)
};
var next = this;
assert.response(server,
@@ -133,7 +161,9 @@ describe('templates surrogate keys', function() {
status: 200
},
function(res) {
next(null, res);
setTimeout(function() {
next(null, res);
}, 50);
}
);
},
@@ -145,6 +175,7 @@ describe('templates surrogate keys', function() {
assert.deepEqual(parsedBody, expectedBody);
assert.equal(scope.pendingMocks().length, 0);
assert.equal(fastlyScope.pendingMocks().length, 0);
return null;
},
@@ -152,14 +183,7 @@ describe('templates surrogate keys', function() {
if ( err ) {
return done(err);
}
redisClient.keys("map_*|localhost", function(err, keys) {
if ( err ) {
return done(err);
}
redisClient.del(keys, function(err) {
return done(err);
});
});
testHelper.deleteRedisKeys({'map_tpl|localhost': 0}, done);
}
);
});
@@ -171,6 +195,14 @@ describe('templates surrogate keys', function() {
.matchHeader('Invalidation-Match', invalidationMatchHeader)
.reply(204, '');
var fastlyScope = nock(FastlyPurge.FASTLY_API_ENDPOINT)
.post(fastlyPurgePath)
.matchHeader('Fastly-Key', FAKE_FASTLY_API_KEY)
.matchHeader('Accept', 'application/json')
.reply(200, {
status:'ok'
});
step(
function createTemplateToDelete() {
createTemplate(this);
@@ -194,7 +226,9 @@ describe('templates surrogate keys', function() {
status: 204
},
function(res) {
next(null, res);
setTimeout(function() {
next(null, res);
}, 50);
}
);
},
@@ -204,6 +238,7 @@ describe('templates surrogate keys', function() {
}
assert.equal(scope.pendingMocks().length, 0);
assert.equal(fastlyScope.pendingMocks().length, 0);
return null;
},
@@ -220,6 +255,14 @@ describe('templates surrogate keys', function() {
.matchHeader('Invalidation-Match', invalidationMatchHeader)
.reply(503, '');
var fastlyScope = nock(FastlyPurge.FASTLY_API_ENDPOINT)
.post(fastlyPurgePath)
.matchHeader('Fastly-Key', FAKE_FASTLY_API_KEY)
.matchHeader('Accept', 'application/json')
.reply(200, {
status:'ok'
});
step(
function createTemplateToUpdate() {
createTemplate(this);
@@ -235,7 +278,7 @@ describe('templates surrogate keys', function() {
host: templateOwner,
'Content-Type': 'application/json'
},
data: JSON.stringify(template)
data: JSON.stringify(templateUpdated)
};
var next = this;
assert.response(server,
@@ -244,7 +287,9 @@ describe('templates surrogate keys', function() {
status: 200
},
function(res) {
next(null, res);
setTimeout(function() {
next(null, res);
}, 50);
}
);
},
@@ -256,6 +301,7 @@ describe('templates surrogate keys', function() {
assert.deepEqual(parsedBody, expectedBody);
assert.equal(scope.pendingMocks().length, 0);
assert.equal(fastlyScope.pendingMocks().length, 0);
return null;
},
@@ -263,14 +309,7 @@ describe('templates surrogate keys', function() {
if ( err ) {
return done(err);
}
redisClient.keys("map_*|localhost", function(err, keys) {
if ( err ) {
return done(err);
}
redisClient.del(keys, function(err) {
return done(err);
});
});
testHelper.deleteRedisKeys({'map_tpl|localhost': 0}, done);
}
);
});

View File

@@ -1,24 +1,22 @@
var helper = require(__dirname + '/../support/test_helper');
require(__dirname + '/../support/test_helper');
var assert = require('../support/assert');
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
var server = new CartodbWindshaft(serverOptions);
var fs = require('fs');
var metadataBackend = {};
var tilelive = {};
var HealthCheck = require('../../lib/cartodb/monitoring/health_check');
var healthCheck = new HealthCheck(metadataBackend, tilelive);
var assert = require('../support/assert');
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
suite('health checks', function () {
describe('health checks', function () {
function resetHealthConfig() {
function enableHealthConfig() {
global.environment.health = {
enabled: true,
username: 'localhost',
z: 0,
x: 0,
y: 0
enabled: true
};
}
function disableHealthConfig() {
global.environment.health = {
enabled: false
};
}
@@ -30,65 +28,89 @@ suite('health checks', function () {
}
};
test('returns 200 and ok=true with enabled configuration', function (done) {
resetHealthConfig();
beforeEach(enableHealthConfig);
afterEach(disableHealthConfig);
assert.response(server,
healthCheckRequest,
{
status: 200
},
function (res, err) {
assert.ok(!err);
var RESPONSE_OK = {
status: 200
};
var parsed = JSON.parse(res.body);
var RESPONSE_FAIL = {
status: 503
};
assert.ok(parsed.enabled);
assert.ok(parsed.ok);
it('returns 200 and ok=true with enabled configuration', function (done) {
var server = new CartodbWindshaft(serverOptions);
done();
}
);
assert.response(server, healthCheckRequest, RESPONSE_OK, function (res, err) {
assert.ok(!err);
var parsed = JSON.parse(res.body);
assert.ok(parsed.enabled);
assert.ok(parsed.ok);
done();
});
});
test('error if disabled file exists', function(done) {
var fs = require('fs');
it('error if disabled file exists', function(done) {
var errorMessage = "Maintenance";
var readFileFn = fs.readFile
fs.readFile = function(filename, callback) {
callback(null, "Maintenance");
}
healthCheck.check(null, function(err, result) {
assert.equal(err.message, "Maintenance");
assert.equal(err.http_status, 503);
done();
fs.readFile = readFileFn;
});
});
var readFileFn = fs.readFile;
fs.readFile = function(filename, callback) {
callback(null, errorMessage);
};
var server = new CartodbWindshaft(serverOptions);
test('not err if disabled file does not exists', function(done) {
resetHealthConfig();
assert.response(server, healthCheckRequest, RESPONSE_FAIL, function(res, err) {
fs.readFile = readFileFn;
global.environment.disabled_file = '/tmp/ftreftrgtrccre';
assert.ok(!err);
var parsed = JSON.parse(res.body);
assert.ok(parsed.enabled);
assert.ok(!parsed.ok);
assert.equal(parsed.err, errorMessage);
assert.response(server,
healthCheckRequest,
{
status: 200
},
function (res, err) {
assert.ok(!err);
done();
});
});
var parsed = JSON.parse(res.body);
it('error if disabled file exists but has no content', function(done) {
var readFileFn = fs.readFile;
fs.readFile = function(filename, callback) {
callback(null, '');
};
var server = new CartodbWindshaft(serverOptions);
assert.equal(parsed.enabled, true);
assert.equal(parsed.ok, true);
assert.response(server, healthCheckRequest, RESPONSE_FAIL, function(res, err) {
fs.readFile = readFileFn;
done();
}
);
assert.ok(!err);
var parsed = JSON.parse(res.body);
assert.ok(parsed.enabled);
assert.ok(!parsed.ok);
assert.equal(parsed.err, 'Unknown error');
done();
});
});
it('not err if disabled file does not exists', function(done) {
global.environment.disabled_file = '/tmp/ftreftrgtrccre';
var server = new CartodbWindshaft(serverOptions);
assert.response(server, healthCheckRequest, RESPONSE_OK, function (res, err) {
assert.ok(!err);
var parsed = JSON.parse(res.body);
assert.equal(parsed.enabled, true);
assert.equal(parsed.ok, true);
done();
});
});
});

View File

@@ -1,37 +1,30 @@
require('../support/test_helper');
var testHelper = require('../support/test_helper');
var assert = require('../support/assert');
var _ = require('underscore');
var redis = require('redis');
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
describe('render limits', function() {
var layergroupUrl = '/api/v1/map';
var redisClient = redis.createClient(global.environment.redis.port);
after(function(done) {
redisClient.keys("map_style|*", function(err, matches) {
redisClient.del(matches, function() {
done();
});
});
});
var server;
var keysToDelete;
beforeEach(function() {
server = new CartodbWindshaft(serverOptions());
keysToDelete = {};
server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
});
var keysToDelete = [];
afterEach(function(done) {
redisClient.DEL(keysToDelete, function() {
keysToDelete = [];
done();
});
testHelper.deleteRedisKeys(keysToDelete, done);
});
var user = 'localhost';
@@ -81,7 +74,7 @@ describe('render limits', function() {
if (err) {
return callback(err);
}
keysToDelete.push(userLimitsKey);
keysToDelete[userLimitsKey] = 5;
return callback();
});
});
@@ -135,6 +128,8 @@ describe('render limits', function() {
function(res) {
var parsed = JSON.parse(res.body);
assert.ok(parsed.layergroupid);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
}
);
@@ -155,6 +150,8 @@ describe('render limits', function() {
status: 200
},
function(res) {
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
assert.response(server,
{
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
@@ -174,7 +171,7 @@ describe('render limits', function() {
},
function(res) {
var parsed = JSON.parse(res.body);
assert.deepEqual(parsed, { error: 'Render timed out' });
assert.deepEqual(parsed, { errors: ['Render timed out'] });
done();
}
);
@@ -197,6 +194,8 @@ describe('render limits', function() {
status: 200
},
function(res) {
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
assert.response(server,
{
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
@@ -246,6 +245,8 @@ describe('render limits', function() {
function(res) {
var parsed = JSON.parse(res.body);
assert.ok(parsed.layergroupid);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
}
);
@@ -265,6 +266,8 @@ describe('render limits', function() {
status: 200
},
function(res) {
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
assert.response(server,
{
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {

View File

@@ -6,32 +6,45 @@ var strftime = require('strftime');
var redis_stats_db = 5;
var helper = require(__dirname + '/../support/test_helper');
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
var windshaft_fixtures = __dirname + '/../../node_modules/windshaft/test/fixtures';
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 20;
var IMAGE_EQUALS_HIGHER_TOLERANCE_PER_MIL = 25;
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions());
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
var TablesCacheEntry = require('../../lib/cartodb/cache/model/database_tables_entry');
['/api/v1/map', '/user/localhost/api/v1/map'].forEach(function(layergroup_url) {
var suiteName = 'multilayer:postgres=layergroup_url=' + layergroup_url;
suite(suiteName, function() {
describe(suiteName, function() {
var keysToDelete;
beforeEach(function() {
keysToDelete = {};
});
afterEach(function(done) {
helper.deleteRedisKeys(keysToDelete, done);
});
var cdbQueryTablesFromPostgresEnabledValue = true;
var redis_client = redis.createClient(global.environment.redis.port);
var expected_last_updated_epoch = 1234567890123; // this is hard-coded into SQLAPIEmu
var expected_last_updated = new Date(expected_last_updated_epoch).toISOString();
var test_user = _.template(global.environment.postgres_auth_user, {user_id:1});
var test_database = test_user + '_db';
test("layergroup with 2 layers, each with its style", function(done) {
it("layergroup with 2 layers, each with its style", function(done) {
var layergroup = {
version: '1.0.0',
@@ -67,16 +80,14 @@ suite(suiteName, function() {
assert.equal(res.statusCode, 200, res.body);
var parsedBody = JSON.parse(res.body);
assert.equal(parsedBody.last_updated, expected_last_updated);
if ( expected_token ) {
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
}
else expected_token = parsedBody.layergroupid.split(':')[0];
assert.equal(res.headers['x-layergroup-id'], parsedBody.layergroupid);
expected_token = parsedBody.layergroupid;
next(null, res);
});
},
function do_get_tile(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png',
@@ -121,7 +132,7 @@ suite(suiteName, function() {
// See https://github.com/CartoDB/Windshaft-cartodb/issues/170
function do_get_tile_nosignature(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + '/localhost@' + expected_token + ':cb0/0/0/0.png',
@@ -131,14 +142,14 @@ suite(suiteName, function() {
}, {}, function(res) {
assert.equal(res.statusCode, 403, res.statusCode + ':' + res.body);
var parsed = JSON.parse(res.body);
var msg = parsed.error; // TODO: should it be "errors" ?
var msg = parsed.errors[0];
assert.ok(msg.match(/permission denied/i), msg);
next(err);
});
},
function do_get_grid_layer0(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + '/0/0/0/0.grid.json',
@@ -155,7 +166,7 @@ suite(suiteName, function() {
},
function do_get_grid_layer1(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + '/1/0/0/0.grid.json',
@@ -171,26 +182,15 @@ suite(suiteName, function() {
});
},
function finish(err) {
var errors = [];
if ( err ) {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
if ( errors.length ) done(new Error(errors));
else done(null);
});
});
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done(err);
}
);
});
test("should include serverMedata in the response", function(done) {
it("should include serverMedata in the response", function(done) {
global.environment.serverMetadata = { cdn_url : { http:'test', https: 'tests' } };
var layergroup = {
version: '1.0.0',
@@ -216,6 +216,8 @@ suite(suiteName, function() {
},
function do_check_create(err, res) {
var parsed = JSON.parse(res.body);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
assert.ok(_.isEqual(parsed.cdn_url, global.environment.serverMetadata.cdn_url));
done();
}
@@ -223,19 +225,25 @@ suite(suiteName, function() {
});
test("get creation requests has cache", function(done) {
it("get creation requests has cache", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
{ options: {
sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, 5e6, 0) as the_geom_webmercator' +
' from test_table limit 2',
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
cartocss_version: '2.0.1'
} }
]
};
var layergroup = {
version: '1.0.0',
layers: [
{ options: {
sql: 'select cartodb_id, the_geom_webmercator from test_table',
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
cartocss_version: '2.0.1',
interactivity: 'cartodb_id'
} },
{ options: {
sql: 'select cartodb_id, the_geom_webmercator from test_table_2',
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
cartocss_version: '2.0.2',
interactivity: 'cartodb_id'
} }
]
};
var expected_token;
step(
@@ -249,33 +257,26 @@ suite(suiteName, function() {
}, {}, function(res, err) { next(err, res); });
},
function do_check_create(err, res) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(res.statusCode, 200, res.body);
var parsedBody = JSON.parse(res.body);
expected_token = parsedBody.layergroupid.split(':')[0];
helper.checkCache(res);
return null;
},
function finish(err) {
var errors = [];
if ( err ) {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
if ( errors.length ) done(new Error(errors));
else done(null);
});
});
helper.checkSurrogateKey(res, new TablesCacheEntry('test_windshaft_cartodb_user_1_db', [
'public.test_table',
'public.test_table_2'
]).key().join(' '));
keysToDelete['map_cfg|' + expected_token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
}
);
});
test("get creation has no cache if sql is bogus", function(done) {
it("get creation has no cache if sql is bogus", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
@@ -297,7 +298,7 @@ suite(suiteName, function() {
});
});
test("get creation has no cache if cartocss is not valid", function(done) {
it("get creation has no cache if cartocss is not valid", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
@@ -320,7 +321,7 @@ suite(suiteName, function() {
});
});
test("layergroup can hold substitution tokens", function(done) {
it("layergroup can hold substitution tokens", function(done) {
var layergroup = {
version: '1.0.0',
@@ -352,13 +353,15 @@ suite(suiteName, function() {
if ( expected_token ) {
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
}
else expected_token = parsedBody.layergroupid.split(':')[0];
else {
expected_token = parsedBody.layergroupid.split(':')[0];
}
next(null, res);
});
},
function do_get_tile1(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb10/1/0/0.png',
@@ -397,7 +400,7 @@ suite(suiteName, function() {
},
function do_get_tile4(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb11/4/0/0.png',
@@ -436,7 +439,7 @@ suite(suiteName, function() {
},
function do_get_grid1(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + '/0/1/0/0.grid.json',
@@ -453,7 +456,7 @@ suite(suiteName, function() {
},
function do_get_grid4(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + '/0/4/0/0.grid.json',
@@ -462,32 +465,19 @@ suite(suiteName, function() {
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
assert.utfgridEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.grid.json', 2,
function(err/*, similarity*/) {
next(err);
});
assert.utfgridEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.grid.json', 2, next);
});
},
function finish(err) {
var errors = [];
if ( err ) {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
if ( errors.length ) done(new Error(errors));
else done(null);
});
});
keysToDelete['map_cfg|' + expected_token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done(err);
}
);
});
test("layergroup creation raises mapviews counter", function(done) {
it("layergroup creation raises mapviews counter", function(done) {
var layergroup = {
stat_tag: 'random_tag',
version: '1.0.0',
@@ -504,19 +494,22 @@ suite(suiteName, function() {
var redis_stats_client = redis.createClient(global.environment.redis.port);
var expected_token; // will be set on first post and checked on second
var now = strftime("%Y%m%d", new Date());
var errors = [];
step(
function clean_stats()
{
var next = this;
redis_stats_client.select(redis_stats_db, function(err) {
if ( err ) next(err);
else redis_stats_client.del(statskey+':global', next);
if ( err ) {
next(err);
}
else {
redis_stats_client.del(statskey+':global', next);
}
});
},
function do_post_1(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url,
@@ -530,12 +523,12 @@ suite(suiteName, function() {
});
},
function check_global_stats_1(err, val) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(val, 1, "Expected score of " + now + " in " + statskey + ":global to be 1, got " + val);
redis_stats_client.zscore(statskey+':stat_tag:random_tag', now, this);
},
function check_tag_stats_1_do_post_2(err, val) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(val, 1, "Expected score of " + now + " in " + statskey + ":stat_tag:" + layergroup.stat_tag +
" to be 1, got " + val);
var next = this;
@@ -552,39 +545,32 @@ suite(suiteName, function() {
},
function check_global_stats_2(err, val)
{
if ( err ) throw err;
assert.ifError(err);
assert.equal(val, 2, "Expected score of " + now + " in " + statskey + ":global to be 2, got " + val);
redis_stats_client.zscore(statskey+':stat_tag:' + layergroup.stat_tag, now, this);
},
function check_tag_stats_2(err, val)
{
if ( err ) throw err;
assert.ifError(err);
assert.equal(val, 2, "Expected score of " + now + " in " + statskey + ":stat_tag:" + layergroup.stat_tag +
" to be 2, got " + val);
return 1;
},
function cleanup_map_style(err) {
if ( err ) errors.push('' + err);
var next = this;
// trip epoch
expected_token = expected_token.split(':')[0];
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
redis_client.del(matches, next);
});
},
function cleanup_stats(err) {
if ( err ) errors.push('' + err);
redis_client.del([statskey+':global', statskey+':stat_tag:'+layergroup.stat_tag], this);
},
function finish(err) {
if ( err ) errors.push('' + err);
if ( errors.length ) done(new Error(errors.join(',')));
else done(null);
if ( err ) {
return done(err);
}
// trip epoch
expected_token = expected_token.split(':')[0];
keysToDelete['map_cfg|' + expected_token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
keysToDelete[statskey+':stat_tag:'+layergroup.stat_tag] = 5;
done();
}
);
});
test("layergroup creation fails if CartoCSS is bogus", function(done) {
it("layergroup creation fails if CartoCSS is bogus", function(done) {
var layergroup = {
stat_tag: 'random_tag',
version: '1.0.0',
@@ -613,7 +599,7 @@ suite(suiteName, function() {
// Also tests that server doesn't crash:
// see http://github.com/CartoDB/Windshaft-cartodb/issues/109
test("layergroup creation fails if sql is bogus", function(done) {
it("layergroup creation fails if sql is bogus", function(done) {
var layergroup = {
stat_tag: 'random_tag',
version: '1.0.0',
@@ -640,7 +626,7 @@ suite(suiteName, function() {
});
});
test("layergroup with 2 private-table layers", function(done) {
it("layergroup with 2 private-table layers", function(done) {
var layergroup = {
version: '1.0.0',
@@ -677,13 +663,15 @@ suite(suiteName, function() {
if ( expected_token ) {
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
}
else expected_token = parsedBody.layergroupid.split(':')[0];
else {
expected_token = parsedBody.layergroupid.split(':')[0];
}
next(null, res);
});
},
function do_get_tile(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png?map_key=1234',
@@ -704,7 +692,7 @@ suite(suiteName, function() {
},
function do_get_grid_layer0(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + '/0/0/0/0.grid.json?map_key=1234',
@@ -717,7 +705,7 @@ suite(suiteName, function() {
},
function do_get_grid_layer1(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + '/1/0/0/0.grid.json?map_key=1234',
@@ -731,7 +719,7 @@ suite(suiteName, function() {
},
function do_get_tile_unauth(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png',
@@ -747,7 +735,7 @@ suite(suiteName, function() {
},
function do_get_grid_layer0_unauth(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + '/0/0/0/0.grid.json',
@@ -762,7 +750,7 @@ suite(suiteName, function() {
},
function do_get_grid_layer1_unauth(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + '/1/0/0/0.grid.json',
@@ -776,26 +764,16 @@ suite(suiteName, function() {
});
},
function finish(err) {
var errors = [];
if ( err ) {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
if ( errors.length ) done(new Error(errors));
else done(null);
});
});
keysToDelete['map_cfg|' + expected_token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done(err);
}
);
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/152
test("x-cache-channel still works for GETs after tiler restart", function(done) {
it("x-cache-channel still works for GETs after tiler restart", function(done) {
var layergroup = {
version: '1.0.0',
@@ -822,19 +800,21 @@ suite(suiteName, function() {
}, {}, function(res, err) { next(err, res); });
},
function check_post(err, res) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(res.statusCode, 200, res.body);
var parsedBody = JSON.parse(res.body);
assert.equal(parsedBody.last_updated, expected_last_updated);
if ( expected_token ) {
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
}
else expected_token = parsedBody.layergroupid.split(':')[0];
else {
expected_token = parsedBody.layergroupid.split(':')[0];
}
return null;
},
function do_get0(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png?map_key=1234',
@@ -844,7 +824,7 @@ suite(suiteName, function() {
}, {}, function(res, err) { next(err, res); });
},
function do_check0(err, res) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "image/png");
@@ -856,14 +836,14 @@ suite(suiteName, function() {
return null;
},
function do_restart_server(err/*, res*/) {
if ( err ) throw err;
assert.ifError(err);
// hack simulating restart...
server = new CartodbWindshaft(serverOptions());
server = new CartodbWindshaft(serverOptions);
return null;
},
function do_get1(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png?map_key=1234',
@@ -873,7 +853,7 @@ suite(suiteName, function() {
}, {}, function(res, err) { next(err, res); });
},
function do_check1(err, res) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "image/png");
@@ -885,26 +865,16 @@ suite(suiteName, function() {
return null;
},
function finish(err) {
var errors = [];
if ( err ) {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
if ( errors.length ) done(new Error(errors.join(',')));
else done(null);
});
});
keysToDelete['map_cfg|' + expected_token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done(err);
}
);
});
// https://github.com/cartodb/Windshaft-cartodb/issues/81
test("invalid text-name in CartoCSS", function(done) {
it("invalid text-name in CartoCSS", function(done) {
var layergroup = {
version: '1.0.1',
@@ -932,7 +902,7 @@ suite(suiteName, function() {
});
});
test("quotes CartoCSS", function(done) {
it("quotes CartoCSS", function(done) {
var layergroup = {
version: '1.0.1',
@@ -957,12 +927,14 @@ suite(suiteName, function() {
data: JSON.stringify(layergroup)
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
});
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/87
test("exponential notation in CartoCSS filter values", function(done) {
it("exponential notation in CartoCSS filter values", function(done) {
var layergroup = {
version: '1.0.1',
layers: [
@@ -980,12 +952,14 @@ suite(suiteName, function() {
data: JSON.stringify(layergroup)
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
});
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/93
test("accepts unused directives", function(done) {
it("accepts unused directives", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
@@ -1011,6 +985,7 @@ suite(suiteName, function() {
var parsedBody = JSON.parse(res.body);
if ( expected_token ) {
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
assert.equal(res.headers['x-layergroup-id'], parsedBody.layergroupid);
}
else {
var token_components = parsedBody.layergroupid.split(':');
@@ -1022,7 +997,7 @@ suite(suiteName, function() {
},
function do_get_tile(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png',
@@ -1040,28 +1015,17 @@ suite(suiteName, function() {
});
},
function finish(err) {
var errors = [];
if ( err ) {
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
if ( errors.length ) done(new Error(errors));
else done(null);
});
});
keysToDelete['user:localhost:mapviews:global'] = 5;
keysToDelete['map_cfg|' + expected_token] = 0;
done(err);
}
);
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/91
// and https://github.com/CartoDB/Windshaft-cartodb/issues/38
test("tiles for private tables can be fetched with api_key", function(done) {
var errors = [];
it("tiles for private tables can be fetched with api_key", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
@@ -1085,12 +1049,13 @@ suite(suiteName, function() {
}, {}, function(res) { next(null, res); });
},
function check_result(err, res) {
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var parsedBody = JSON.parse(res.body);
if ( expected_token ) {
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
assert.equal(res.headers['x-layergroup-id'], parsedBody.layergroupid);
}
else {
var token_components = parsedBody.layergroupid.split(':');
@@ -1101,7 +1066,7 @@ suite(suiteName, function() {
},
function do_get_tile(err)
{
if ( err ) throw err;
assert.ifError(err);
var next = this;
assert.response(server, {
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png?api_key=1234',
@@ -1111,43 +1076,29 @@ suite(suiteName, function() {
}, {}, function(res) { next(null, res); });
},
function check_get_tile(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
return null;
},
function cleanup(err) {
if ( err ) errors.push(err.message);
if ( ! expected_token ) return null;
var next = this;
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
next();
});
});
},
function finish(err) {
if ( err ) {
errors.push(err.message);
console.log("Error: " + err);
}
if ( errors.length ) done(new Error(errors));
else done(null);
if (err) {
return done(err);
}
assert.equal(res.statusCode, 200, res.body);
keysToDelete['user:localhost:mapviews:global'] = 5;
keysToDelete['map_cfg|' + expected_token] = 0;
done(err);
}
);
});
// SQL strings can be of arbitrary length, when using POST
// See https://github.com/CartoDB/Windshaft-cartodb/issues/111
test("sql string can be very long", function(done){
it("sql string can be very long", function(done){
var long_val = 'pretty';
for (var i=0; i<1024; ++i) long_val += ' long';
for (var i=0; i<1024; ++i) {
long_val += ' long';
}
long_val += ' string';
var sql = "SELECT ";
for (i=0; i<16; ++i)
for (i=0; i<16; ++i) {
sql += "'" + long_val + "'::text as pretty_long_field_name_" + i + ", ";
}
sql += "cartodb_id, the_geom_webmercator FROM gadm4 g";
var layergroup = {
version: '1.0.0',
@@ -1159,8 +1110,7 @@ suite(suiteName, function() {
} }
]
};
var errors = [];
var expected_token;
var expected_token;
step(
function do_post()
{
@@ -1175,7 +1125,7 @@ suite(suiteName, function() {
}, {}, function(res) { next(null, res); });
},
function check_result(err, res) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var parsedBody = JSON.parse(res.body);
var token_components = parsedBody.layergroupid.split(':');
@@ -1183,28 +1133,19 @@ suite(suiteName, function() {
return null;
},
function cleanup(err) {
if ( err ) errors.push('' + err);
if ( ! expected_token ) return null;
var next = this;
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
if ( err ) errors.push(err.message);
assert.equal(matches.length, 1, "Missing expected token " + expected_token + " from redis: " + matches);
redis_client.del(matches, function(err) {
if ( err ) errors.push(err.message);
next();
});
});
},
function finish(err) {
if ( err ) errors.push('' + err);
if ( errors.length ) done(new Error(errors.join(',')));
else done(null);
if (err) {
return done(err);
}
keysToDelete['user:localhost:mapviews:global'] = 5;
keysToDelete['map_cfg|' + expected_token] = 0;
done(err);
}
);
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/133
test("MapConfig with mapnik layer and no cartocss", function(done) {
it("MapConfig with mapnik layer and no cartocss", function(done) {
var layergroup = {
version: '1.0.0',
@@ -1229,7 +1170,7 @@ suite(suiteName, function() {
}, {}, function(res, err) { next(err, res); });
},
function check_post(err, res) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, 'Missing "errors" in response: ' + JSON.stringify(parsed));
@@ -1246,7 +1187,7 @@ suite(suiteName, function() {
if (!cdbQueryTablesFromPostgresEnabledValue) { // only test if it was using the SQL API
// See https://github.com/CartoDB/Windshaft-cartodb/issues/167
test("lack of response from sql-api will result in a timeout", function(done) {
it("lack of response from sql-api will result in a timeout", function(done) {
var layergroup = {
version: '1.0.0',
@@ -1271,7 +1212,7 @@ suite(suiteName, function() {
}, {}, function(res, err) { next(err, res); });
},
function check_post(err, res) {
if ( err ) throw err;
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, 'Missing "errors" in response: ' + JSON.stringify(parsed));
@@ -1305,25 +1246,29 @@ suite(suiteName, function() {
status: 200
};
test("cache control for layergroup default value", function(done) {
it("cache control for layergroup default value", function(done) {
global.environment.varnish.layergroupTtl = null;
assert.response(server, layergroupTtlRequest, layergroupTtlResponseExpectation,
function(res) {
assert.equal(res.headers['cache-control'], 'public,max-age=86400,must-revalidate');
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
}
);
});
test("cache control for layergroup uses configuration for max-age", function(done) {
it("cache control for layergroup uses configuration for max-age", function(done) {
var layergroupTtl = 300;
global.environment.varnish.layergroupTtl = layergroupTtl;
assert.response(server, layergroupTtlRequest, layergroupTtlResponseExpectation,
function(res) {
assert.equal(res.headers['cache-control'], 'public,max-age=' + layergroupTtl + ',must-revalidate');
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
}
@@ -1331,7 +1276,7 @@ suite(suiteName, function() {
});
test("it's not possible to override authorization with a crafted layergroup", function(done) {
it("it's not possible to override authorization with a crafted layergroup", function(done) {
var layergroup = {
version: '1.0.0',
@@ -1373,25 +1318,6 @@ suite(suiteName, function() {
}
);
});
suiteTeardown(function(done) {
// This test will add map_style records, like
// 'map_style|null|publicuser|my_table',
redis_client.keys("map_style|*", function(err, matches) {
redis_client.del(matches, function() {
redis_client.select(5, function() {
redis_client.keys("user:localhost:mapviews*", function(err, matches) {
redis_client.del(matches, function() {
done();
});
});
});
});
});
});
});

View File

@@ -2,13 +2,13 @@ var testHelper = require('../support/test_helper');
var assert = require('../support/assert');
var redis = require('redis');
var _ = require('underscore');
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
var QueryTablesApi = require('../../lib/cartodb/api/query_tables_api');
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
var serverOptions = require('../../lib/cartodb/server_options')();
var PgQueryRunner = require('../../lib/cartodb/backends/pg_query_runner');
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
@@ -16,15 +16,14 @@ describe('tests from old api translated to multilayer', function() {
var layergroupUrl = '/api/v1/map';
var redisClient = redis.createClient(global.environment.redis.port);
after(function(done) {
// This test will add map_style records, like
// 'map_style|null|publicuser|my_table',
redisClient.keys("map_style|*", function(err, matches) {
redisClient.del(matches, function() {
done();
});
});
var keysToDelete;
beforeEach(function() {
keysToDelete = {};
});
afterEach(function(done) {
testHelper.deleteRedisKeys(keysToDelete, done);
});
var wadusSql = 'select 1 as cartodb_id, null::geometry as the_geom_webmercator';
@@ -108,6 +107,11 @@ describe('tests from old api translated to multilayer', function() {
function(res) {
var parsed = JSON.parse(res.body);
assert.ok(parsed.layergroupid);
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
}
);
@@ -128,6 +132,11 @@ describe('tests from old api translated to multilayer', function() {
function(res) {
var parsed = JSON.parse(res.body);
assert.ok(parsed.layergroupid);
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
keysToDelete['user:cartodb250user:mapviews:global'] = 5;
global.environment.postgres.host = backupDBHost;
done();
@@ -150,6 +159,10 @@ describe('tests from old api translated to multilayer', function() {
function(res) {
var parsed = JSON.parse(res.body);
assert.ok(parsed.layergroupid);
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
keysToDelete['user:cartodb250user:mapviews:global'] = 5;
global.environment.postgres_auth_pass = backupDBPass;
done();
@@ -182,6 +195,9 @@ describe('tests from old api translated to multilayer', function() {
var parsed = JSON.parse(res.body);
assert.ok(parsed.layergroupid);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
}
);
@@ -246,6 +262,46 @@ describe('tests from old api translated to multilayer', function() {
assert.ok(res.headers.hasOwnProperty('x-cache-channel'));
assert.equal(res.headers['x-cache-channel'], expectedCacheChannel);
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done();
}
);
});
// https://github.com/CartoDB/cartodb-postgresql/issues/86
it.skip("should not fail with long table names because table name length limit", function(done) {
var tableName = 'long_table_name_with_enough_chars_to_break_querytables_function';
var expectedCacheChannel = _.template('<%= databaseName %>:public.<%= tableName %>', {
databaseName: _.template(global.environment.postgres_auth_user, {user_id:1}) + '_db',
tableName: tableName
});
var layergroup = singleLayergroupConfig('select * from ' + tableName, '#layer { marker-fill: red; }');
assert.response(server,
{
url: layergroupUrl + '?config=' + encodeURIComponent(JSON.stringify(layergroup)),
method: 'GET',
headers: {
host: 'localhost'
}
},
{
status: 200
},
function(res) {
var parsed = JSON.parse(res.body);
assert.ok(parsed.layergroupid);
assert.ok(res.headers.hasOwnProperty('x-cache-channel'));
assert.equal(res.headers['x-cache-channel'], expectedCacheChannel);
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
done();
}
);
@@ -253,8 +309,8 @@ describe('tests from old api translated to multilayer', function() {
it("creates layergroup fails when postgresql queries fail to figure affected tables in query", function(done) {
var runQueryFn = QueryTablesApi.prototype.runQuery;
QueryTablesApi.prototype.runQuery = function(username, query, queryHandler, callback) {
var runQueryFn = PgQueryRunner.prototype.run;
PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) {
return queryHandler(new Error('fake error message'), [], callback);
};
@@ -272,13 +328,17 @@ describe('tests from old api translated to multilayer', function() {
status: 400
},
function(res) {
QueryTablesApi.prototype.runQuery = runQueryFn;
PgQueryRunner.prototype.run = runQueryFn;
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
// TODO when affected tables query makes the request to fail layergroup should be removed
keysToDelete['map_cfg|4fb7bd7008322ce66f22d20aebba1ab0'] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
var parsed = JSON.parse(res.body);
assert.deepEqual(parsed, {
errors: ["Error: could not fetch affected tables and last updated time: fake error message"]
errors: ["Error: could not fetch affected tables or last updated time: fake error message"]
});
done();
@@ -300,13 +360,17 @@ describe('tests from old api translated to multilayer', function() {
status: 200
},
function(res) {
var runQueryFn = QueryTablesApi.prototype.runQuery;
QueryTablesApi.prototype.runQuery = function(username, query, queryHandler, callback) {
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
var runQueryFn = PgQueryRunner.prototype.run;
PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) {
return queryHandler(new Error('failed to query database for affected tables'), [], callback);
};
// reset internal cacheChannel cache
serverOptions.channelCache = {};
server.layergroupAffectedTablesCache.cache.reset();
assert.response(server,
{
@@ -327,7 +391,7 @@ describe('tests from old api translated to multilayer', function() {
},
function(res) {
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
QueryTablesApi.prototype.runQuery = runQueryFn;
PgQueryRunner.prototype.run = runQueryFn;
done();
}
);

View File

@@ -1,19 +1,20 @@
var test_helper = require('../support/test_helper');
var assert = require('../support/assert');
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
var RedisPool = require('redis-mpool');
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
var Step = require('step');
var _ = require('underscore');
var step = require('step');
suite('named_layers', function() {
describe('named_layers', function() {
// configure redis pool instance to use in tests
var redisPool = RedisPool(global.environment.redis);
var redisPool = new RedisPool(global.environment.redis);
var templateMaps = new TemplateMaps(redisPool, {
max_user_templates: global.environment.maxUserTemplates
@@ -45,6 +46,7 @@ suite('named_layers', function() {
},
layergroup: {
layers: [
wadusLayer,
wadusLayer
]
}
@@ -95,7 +97,17 @@ suite('named_layers', function() {
}
};
suiteSetup(function(done) {
var keysToDelete;
beforeEach(function() {
keysToDelete = {};
});
afterEach(function(done) {
test_helper.deleteRedisKeys(keysToDelete, done);
});
beforeEach(function(done) {
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: true};
templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) {
if (err) {
@@ -112,7 +124,24 @@ suite('named_layers', function() {
});
});
test('should fail for non-existing template name', function(done) {
afterEach(function(done) {
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: false};
templateMaps.delTemplate(username, nestedNamedMapTemplateName, function(err) {
if (err) {
return done(err);
}
templateMaps.delTemplate(username, tokenAuthTemplateName, function(err) {
if (err) {
return done(err);
}
templateMaps.delTemplate(username, templateName, function(err) {
return done(err);
});
});
});
});
it('should fail for non-existing template name', function(done) {
var layergroup = {
version: '1.3.0',
layers: [
@@ -125,7 +154,7 @@ suite('named_layers', function() {
]
};
Step(
step(
function createLayergroup() {
var next = this;
assert.response(server,
@@ -162,7 +191,7 @@ suite('named_layers', function() {
);
});
test('should return 403 if not properly authorized', function(done) {
it('should return 403 if not properly authorized', function(done) {
var layergroup = {
version: '1.3.0',
@@ -178,7 +207,7 @@ suite('named_layers', function() {
]
};
Step(
step(
function createLayergroup() {
var next = this;
assert.response(server,
@@ -219,7 +248,7 @@ suite('named_layers', function() {
});
test('should return 200 and layergroup if properly authorized', function(done) {
it('should return 200 and layergroup if properly authorized', function(done) {
var layergroup = {
version: '1.3.0',
@@ -235,7 +264,7 @@ suite('named_layers', function() {
]
};
Step(
step(
function createLayergroup() {
var next = this;
assert.response(server,
@@ -265,6 +294,9 @@ suite('named_layers', function() {
assert.ok(parsedBody.layergroupid);
assert.ok(parsedBody.last_updated);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
return null;
},
function finish(err) {
@@ -274,7 +306,7 @@ suite('named_layers', function() {
});
test('should return 400 for nested named map layers', function(done) {
it('should return 400 for nested named map layers', function(done) {
var layergroup = {
version: '1.3.0',
@@ -288,7 +320,7 @@ suite('named_layers', function() {
]
};
Step(
step(
function createLayergroup() {
var next = this;
assert.response(server,
@@ -326,7 +358,7 @@ suite('named_layers', function() {
});
test('should return 200 and layergroup with private tables', function(done) {
it('should return 200 and layergroup with private tables', function(done) {
var privateTableTemplateName = 'private_table_template';
var privateTableTemplate = {
@@ -361,7 +393,7 @@ suite('named_layers', function() {
]
};
Step(
step(
function createTemplate() {
templateMaps.addTemplate(username, privateTableTemplate, this);
},
@@ -398,6 +430,9 @@ suite('named_layers', function() {
assert.ok(parsedBody.layergroupid);
assert.ok(parsedBody.last_updated);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
return parsedBody.layergroupid;
},
function requestTile(err, layergroupId) {
@@ -435,7 +470,7 @@ suite('named_layers', function() {
},
function deleteTemplate(err) {
var next = this;
templateMaps.delTemplate(username, privateTableTemplate, function(/*delErr*/) {
templateMaps.delTemplate(username, privateTableTemplateName, function(/*delErr*/) {
// ignore deletion error
next(err);
});
@@ -447,7 +482,7 @@ suite('named_layers', function() {
});
test('should return 200 and layergroup with private tables and interactivity', function(done) {
it('should return 200 and layergroup with private tables and interactivity', function(done) {
var privateTableTemplateNameInteractivity = 'private_table_template_interactivity';
var privateTableTemplate = {
@@ -489,7 +524,7 @@ suite('named_layers', function() {
]
};
Step(
step(
function createTemplate() {
templateMaps.addTemplate(username, privateTableTemplate, this);
},
@@ -526,6 +561,9 @@ suite('named_layers', function() {
assert.ok(parsedBody.layergroupid);
assert.ok(parsedBody.last_updated);
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
return parsedBody.layergroupid;
},
function requestTile(err, layergroupId) {
@@ -563,7 +601,7 @@ suite('named_layers', function() {
},
function deleteTemplate(err) {
var next = this;
templateMaps.delTemplate(username, privateTableTemplate, function(/*delErr*/) {
templateMaps.delTemplate(username, privateTableTemplateNameInteractivity, function(/*delErr*/) {
// ignore deletion error
next(err);
});
@@ -575,7 +613,7 @@ suite('named_layers', function() {
});
test('should return 403 when private table is accessed from non named layer', function(done) {
it('should return 403 when private table is accessed from non named layer', function(done) {
var layergroup = {
version: '1.3.0',
@@ -597,7 +635,7 @@ suite('named_layers', function() {
]
};
Step(
step(
function createLayergroup() {
var next = this;
assert.response(server,
@@ -635,21 +673,202 @@ suite('named_layers', function() {
});
it('should return metadata for named layers', function(done) {
suiteTeardown(function(done) {
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: false};
templateMaps.delTemplate(username, nestedNamedMapTemplateName, function(err) {
if (err) {
return done(err);
}
templateMaps.delTemplate(username, tokenAuthTemplateName, function(err) {
if (err) {
return done(err);
var layergroup = {
version: '1.3.0',
layers: [
{
type: 'plain',
options: {
color: '#fabada'
}
},
{
type: 'cartodb',
options: {
sql: 'select * from test_table',
cartocss: '#layer { marker-fill: #cc3300; }',
cartocss_version: '2.3.0'
}
},
{
type: 'named',
options: {
name: templateName
}
},
{
type: 'torque',
options: {
sql: "select * from test_table LIMIT 0",
cartocss: "Map { -torque-frame-count:1; -torque-resolution:1; " +
"-torque-aggregation-function:'count(*)'; -torque-time-attribute:'updated_at'; }"
}
}
templateMaps.delTemplate(username, templateName, function(err) {
return done(err);
]
};
step(
function createLayergroup() {
var next = this;
assert.response(server,
{
url: '/api/v1/map',
method: 'POST',
headers: {
host: 'localhost',
'Content-Type': 'application/json'
},
data: JSON.stringify(layergroup)
},
{
status: 200
},
function(res, err) {
next(err, res);
}
);
},
function checkLayergroup(err, response) {
if (err) {
throw err;
}
var parsedBody = JSON.parse(response.body);
assert.ok(parsedBody.metadata);
assert.ok(parsedBody.metadata.layers);
assert.equal(parsedBody.metadata.layers.length, 5);
assert.equal(parsedBody.metadata.layers[0].type, 'plain');
assert.equal(parsedBody.metadata.layers[1].type, 'mapnik');
assert.equal(parsedBody.metadata.layers[2].type, 'mapnik');
assert.equal(parsedBody.metadata.layers[3].type, 'mapnik');
assert.equal(parsedBody.metadata.layers[4].type, 'torque');
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
return null;
},
function finish(err) {
done(err);
}
);
});
it('should work with named tiles', function(done) {
var namedTilesTemplateName = 'named_tiles_template';
var namedTilesTemplate = {
version: '0.0.1',
name: namedTilesTemplateName,
auth: {
method: 'open'
},
layergroup: {
layers: [
namedMapLayer,
{
type: 'mapnik',
options: {
sql: 'select * from test_table_private_1',
cartocss: '#layer { marker-fill: #cc3300; }',
cartocss_version: '2.3.0'
}
}
]
}
};
step(
function createTemplate() {
templateMaps.addTemplate(username, namedTilesTemplate, this);
},
function createLayergroup(err) {
if (err) {
throw err;
}
var next = this;
assert.response(server,
{
url: '/api/v1/map/named/' + namedTilesTemplateName + '?api_key=1234',
method: 'POST',
headers: {
host: 'localhost',
'Content-Type': 'application/json'
}
},
{
status: 200
},
function(res, err) {
next(err, res);
}
);
},
function checkLayergroup(err, response) {
if (err) {
throw err;
}
var parsedBody = JSON.parse(response.body);
assert.ok(parsedBody.layergroupid);
assert.ok(parsedBody.last_updated);
assert.equal(parsedBody.metadata.layers[0].type, 'mapnik');
assert.equal(parsedBody.metadata.layers[1].type, 'mapnik');
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
return parsedBody.layergroupid;
},
function requestTile(err, layergroupId) {
if (err) {
throw err;
}
var next = this;
assert.response(server,
{
url: '/api/v1/map/' + layergroupId + '/all/0/0/0.png',
method: 'GET',
headers: {
host: 'localhost'
},
encoding: 'binary'
},
{
status: 200,
headers: {
'content-type': 'image/png'
}
},
function(res, err) {
next(err, res);
}
);
},
function handleTileResponse(err, res) {
if (err) {
throw err;
}
test_helper.checkCache(res);
return true;
},
function deleteTemplate(err) {
var next = this;
templateMaps.delTemplate(username, namedTilesTemplateName, function(/*delErr*/) {
// ignore deletion error
next(err);
});
});
});
},
function finish(err) {
done(err);
}
);
});
});

View File

@@ -0,0 +1,269 @@
var test_helper = require('../support/test_helper');
var RedisPool = require('redis-mpool');
var querystring = require('querystring');
var assert = require('../support/assert');
var mapnik = require('windshaft').mapnik;
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
var NamedMapsCacheEntry = require('../../lib/cartodb/cache/model/named_maps_entry');
describe('named maps authentication', function() {
// configure redis pool instance to use in tests
var redisPool = new RedisPool(global.environment.redis);
var templateMaps = new TemplateMaps(redisPool, {
max_user_templates: global.environment.maxUserTemplates
});
var wadusLayer = {
type: 'cartodb',
options: {
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
cartocss: '#layer { marker-fill: <%= color %>; }',
cartocss_version: '2.3.0'
}
};
var username = 'localhost';
var templateName = 'valid_template';
var template = {
version: '0.0.1',
name: templateName,
auth: {
method: 'open'
},
"placeholders": {
"color": {
"type": "css_color",
"default": "#cc3300"
}
},
layergroup: {
layers: [
wadusLayer
]
}
};
var tokenAuthTemplateName = 'auth_valid_template';
var tokenAuthTemplate = {
version: '0.0.1',
name: tokenAuthTemplateName,
auth: {
method: 'token',
valid_tokens: ['valid1', 'valid2']
},
placeholders: {
color: {
"type": "css_color",
"default": "#cc3300"
}
},
layergroup: {
layers: [
wadusLayer
]
}
};
var namedMapLayer = {
type: 'named',
options: {
name: templateName,
config: {},
auth_tokens: []
}
};
var nestedNamedMapTemplateName = 'nested_template';
var nestedNamedMapTemplate = {
version: '0.0.1',
name: nestedNamedMapTemplateName,
auth: {
method: 'open'
},
layergroup: {
layers: [
namedMapLayer
]
}
};
beforeEach(function (done) {
templateMaps.addTemplate(username, nestedNamedMapTemplate, function (err) {
if (err) {
return done(err);
}
templateMaps.addTemplate(username, tokenAuthTemplate, function (err) {
if (err) {
return done(err);
}
templateMaps.addTemplate(username, template, function (err) {
return done(err);
});
});
});
});
afterEach(function (done) {
templateMaps.delTemplate(username, nestedNamedMapTemplateName, function (err) {
if (err) {
return done(err);
}
templateMaps.delTemplate(username, tokenAuthTemplateName, function (err) {
if (err) {
return done(err);
}
templateMaps.delTemplate(username, templateName, function (err) {
return done(err);
});
});
});
});
function getNamedTile(name, z, x, y, options, callback) {
var url = '/api/v1/map/named/' + name + '/all/' + [z,x,y].join('/') + '.png';
if (options.params) {
url = url + '?' + querystring.stringify(options.params);
}
var requestOptions = {
url: url,
method: 'GET',
headers: {
host: username
},
encoding: 'binary'
};
var statusCode = options.status || 200;
var expectedResponse = {
status: statusCode,
headers: {
'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8'
}
};
assert.response(server,
requestOptions,
expectedResponse,
function (res, err) {
var img;
if (!err && res.headers['content-type'] === 'image/png') {
img = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
}
return callback(err, res, img);
}
);
}
describe('tiles', function() {
it('should return a 404 error for nonexistent template name', function (done) {
var nonexistentName = 'nonexistent';
getNamedTile(nonexistentName, 0, 0, 0, { status: 404 }, function(err, res) {
assert.ok(!err);
assert.deepEqual(
JSON.parse(res.body),
{ errors: ["Template '" + nonexistentName + "' of user '" + username + "' not found"] }
);
done();
});
});
it('should return 403 if not properly authorized', function(done) {
getNamedTile(tokenAuthTemplateName, 0, 0, 0, { status: 403 }, function(err, res) {
assert.ok(!err);
assert.deepEqual(JSON.parse(res.body), { errors: ['Unauthorized template instantiation'] });
done();
});
});
it('should return 200 if properly authorized', function(done) {
getNamedTile(tokenAuthTemplateName, 0, 0, 0, { params: { auth_token: 'valid1' } }, function(err, res, img) {
assert.equal(img.width(), 256);
assert.equal(img.height(), 256);
assert.ok(!err);
test_helper.checkSurrogateKey(res, new NamedMapsCacheEntry(username, tokenAuthTemplateName).key());
done();
});
});
});
function getStaticMap(name, options, callback) {
var url = '/api/v1/map/static/named/' + name + '/640/480.png';
if (options.params) {
url = url + '?' + querystring.stringify(options.params);
}
var requestOptions = {
url: url,
method: 'GET',
headers: {
host: username
},
encoding: 'binary'
};
var statusCode = options.status || 200;
var expectedResponse = {
status: statusCode,
headers: {
'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8'
}
};
assert.response(server,
requestOptions,
expectedResponse,
function (res, err) {
var img;
if (!err && res.headers['content-type'] === 'image/png') {
img = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
}
return callback(err, res, img);
}
);
}
describe('static maps', function() {
it('should return a 404 error for nonexistent template name', function (done) {
var nonexistentName = 'nonexistent';
getStaticMap(nonexistentName, { status: 404 }, function(err, res) {
assert.ok(!err);
assert.deepEqual(
JSON.parse(res.body),
{ errors: ["Template '" + nonexistentName + "' of user '" + username + "' not found"] }
);
done();
});
});
it('should return 403 if not properly authorized', function(done) {
getStaticMap(tokenAuthTemplateName, { status: 403 }, function(err, res) {
assert.ok(!err);
assert.deepEqual(JSON.parse(res.body), { errors: ['Unauthorized template instantiation'] });
done();
});
});
it('should return 200 if properly authorized', function(done) {
getStaticMap(tokenAuthTemplateName, { params: { auth_token: 'valid1' } }, function(err, res, img) {
assert.ok(!err);
assert.equal(img.width(), 640);
assert.equal(img.height(), 480);
test_helper.checkSurrogateKey(res, new NamedMapsCacheEntry(username, tokenAuthTemplateName).key());
test_helper.deleteRedisKeys({'user:localhost:mapviews:global': 5}, done);
});
});
});
});

View File

@@ -0,0 +1,140 @@
require('../support/test_helper');
var RedisPool = require('redis-mpool');
var assert = require('../support/assert');
var mapnik = require('windshaft').mapnik;
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
describe('named maps provider cache', function() {
// configure redis pool instance to use in tests
var redisPool = new RedisPool(global.environment.redis);
var templateMaps = new TemplateMaps(redisPool, {
max_user_templates: global.environment.maxUserTemplates
});
var username = 'localhost';
var templateName = 'template_with_color';
var IMAGE_TOLERANCE = 20;
function createTemplate(color) {
return {
version: '0.0.1',
name: templateName,
auth: {
method: 'open'
},
placeholders: {
color: {
type: "css_color",
default: color
}
},
layergroup: {
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced',
cartocss: '#layer { marker-fill: <%= color %>; marker-line-color: <%= color %>; }',
cartocss_version: '2.3.0'
}
}
]
}
};
}
afterEach(function (done) {
templateMaps.delTemplate(username, templateName, done);
});
function getNamedTile(options, callback) {
if (!callback) {
callback = options;
options = {};
}
var url = '/api/v1/map/named/' + templateName + '/all/' + [0,0,0].join('/') + '.png';
var requestOptions = {
url: url,
method: 'GET',
headers: {
host: username
},
encoding: 'binary'
};
var statusCode = options.statusCode || 200;
var expectedResponse = {
status: statusCode,
headers: {
'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8'
}
};
assert.response(server, requestOptions, expectedResponse, function (res, err) {
var img;
if (statusCode === 200) {
img = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
}
return callback(err, res, img);
});
}
function previewFixture(color) {
return './test/fixtures/provider/populated_places_simple_reduced-' + color + '.png';
}
var colors = ['red', 'red', 'green', 'blue'];
colors.forEach(function(color) {
it('should return an image estimating its bounds based on dataset', function (done) {
templateMaps.addTemplate(username, createTemplate(color), function (err) {
if (err) {
return done(err);
}
getNamedTile(function(err, res, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture(color), IMAGE_TOLERANCE, done);
});
});
});
});
it('should fail to use template from named map provider after template deletion', function (done) {
var color = 'black';
templateMaps.addTemplate(username, createTemplate(color), function (err) {
if (err) {
return done(err);
}
getNamedTile(function(err, res, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture(color), IMAGE_TOLERANCE, function(err) {
assert.ok(!err);
templateMaps.delTemplate(username, templateName, function (err) {
assert.ok(!err);
getNamedTile({ statusCode: 404 }, function(err, res) {
assert.ok(!err);
assert.deepEqual(
JSON.parse(res.body),
{ errors: ["Template 'template_with_color' of user 'localhost' not found"] }
);
// add template again so it's clean in afterEach
templateMaps.addTemplate(username, createTemplate(color), done);
});
});
});
});
});
});
});

View File

@@ -0,0 +1,166 @@
var testHelper = require('../support/test_helper');
var RedisPool = require('redis-mpool');
var assert = require('../support/assert');
var mapnik = require('windshaft').mapnik;
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
describe('named maps static view', function() {
// configure redis pool instance to use in tests
var redisPool = new RedisPool(global.environment.redis);
var templateMaps = new TemplateMaps(redisPool, {
max_user_templates: global.environment.maxUserTemplates
});
var username = 'localhost';
var templateName = 'template_with_view';
var IMAGE_TOLERANCE = 20;
function createTemplate(view) {
return {
version: '0.0.1',
name: templateName,
auth: {
method: 'open'
},
placeholders: {
color: {
type: "css_color",
default: "#cc3300"
}
},
view: view,
layergroup: {
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced',
cartocss: '#layer { marker-fill: <%= color %>; }',
cartocss_version: '2.3.0'
}
}
]
}
};
}
afterEach(function (done) {
templateMaps.delTemplate(username, templateName, done);
});
function getStaticMap(callback) {
var url = '/api/v1/map/static/named/' + templateName + '/640/480.png';
var requestOptions = {
url: url,
method: 'GET',
headers: {
host: username
},
encoding: 'binary'
};
var expectedResponse = {
status: 200,
headers: {
'Content-Type': 'image/png'
}
};
// this could be removed once named maps are invalidated, otherwise you hits the cache
var server = new CartodbWindshaft(serverOptions);
assert.response(server, requestOptions, expectedResponse, function (res, err) {
testHelper.deleteRedisKeys({'user:localhost:mapviews:global': 5}, function() {
return callback(err, mapnik.Image.fromBytes(new Buffer(res.body, 'binary')));
});
});
}
function previewFixture(version) {
return './test/fixtures/previews/populated_places_simple_reduced-' + version + '.png';
}
it('should return an image estimating its bounds based on dataset', function (done) {
templateMaps.addTemplate(username, createTemplate(), function (err) {
if (err) {
return done(err);
}
getStaticMap(function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('estimated'), IMAGE_TOLERANCE, done);
});
});
});
it('should return an image using view zoom + center', function (done) {
var view = {
zoom: 4,
center: {
lng: 40,
lat: 20
}
};
templateMaps.addTemplate(username, createTemplate(view), function (err) {
if (err) {
return done(err);
}
getStaticMap(function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), IMAGE_TOLERANCE, done);
});
});
});
it('should return an image using view bounds', function (done) {
var view = {
bounds: {
west: 0,
south: 0,
east: 45,
north: 45
}
};
templateMaps.addTemplate(username, createTemplate(view), function (err) {
if (err) {
return done(err);
}
getStaticMap(function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('bounds'), IMAGE_TOLERANCE, done);
});
});
});
it('should return an image using view zoom + center when bounds are also present', function (done) {
var view = {
bounds: {
west: 0,
south: 0,
east: 45,
north: 45
},
zoom: 4,
center: {
lng: 40,
lat: 20
}
};
templateMaps.addTemplate(username, createTemplate(view), function (err) {
if (err) {
return done(err);
}
getStaticMap(function(err, img) {
assert.ok(!err);
assert.imageIsSimilarToFile(img, previewFixture('zoom-center'), IMAGE_TOLERANCE, done);
});
});
});
});

View File

@@ -0,0 +1,110 @@
var test_helper = require('../support/test_helper');
var RedisPool = require('redis-mpool');
var querystring = require('querystring');
var assert = require('../support/assert');
var mapnik = require('windshaft').mapnik;
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
var NamedMapsCacheEntry = require('../../lib/cartodb/cache/model/named_maps_entry');
describe('named maps preview stats', function() {
var redisPool = new RedisPool(global.environment.redis);
var templateMaps = new TemplateMaps(redisPool, {
max_user_templates: global.environment.maxUserTemplates
});
var username = 'localhost';
var statTag = 'wadus_viz';
var templateName = 'with_stats';
var template = {
version: '0.0.1',
name: templateName,
auth: {
method: 'open'
},
"placeholders": {
"color": {
"type": "css_color",
"default": "#cc3300"
}
},
layergroup: {
stat_tag: statTag,
layers: [
{
type: 'cartodb',
options: {
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
cartocss: '#layer { marker-fill: <%= color %>; }',
cartocss_version: '2.3.0'
}
}
]
}
};
beforeEach(function (done) {
templateMaps.addTemplate(username, template, done);
});
afterEach(function (done) {
templateMaps.delTemplate(username, templateName, done);
});
function getStaticMap(name, options, callback) {
var url = '/api/v1/map/static/named/' + name + '/640/480.png';
if (options.params) {
url = url + '?' + querystring.stringify(options.params);
}
var requestOptions = {
url: url,
method: 'GET',
headers: {
host: username
},
encoding: 'binary'
};
var statusCode = options.status || 200;
var expectedResponse = {
status: statusCode,
headers: {
'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8'
}
};
assert.response(server,
requestOptions,
expectedResponse,
function (res, err) {
var img;
if (!err && res.headers['content-type'] === 'image/png') {
img = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
}
return callback(err, res, img);
}
);
}
it('should return 200 if properly authorized', function(done) {
getStaticMap(templateName, { params: { auth_token: 'valid1' } }, function(err, res, img) {
assert.ok(!err);
assert.equal(img.width(), 640);
assert.equal(img.height(), 480);
test_helper.checkSurrogateKey(res, new NamedMapsCacheEntry(username, templateName).key());
var redisKeysToDelete = { 'user:localhost:mapviews:global': 5 };
redisKeysToDelete['user:localhost:mapviews:stat_tag:' + statTag] = 5;
test_helper.deleteRedisKeys(redisKeysToDelete, done);
});
});
});

View File

@@ -0,0 +1,303 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var step = require('step');
var cartodbServer = require('../../../lib/cartodb/server');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
describe('attributes', function() {
var server = cartodbServer(PortedServerOptions);
server.setMaxListeners(0);
var test_mapconfig_1 = {
version: '1.1.0',
layers: [
{ type: 'mapnik', options: {
sql: "select 1 as id, 'SRID=4326;POINT(0 0)'::geometry as the_geom",
cartocss: '#style { }',
cartocss_version: '2.0.1'
} },
{ type: 'mapnik', options: {
sql: "select 1 as i, 6 as n, 'SRID=4326;POINT(0 0)'::geometry as the_geom",
attributes: { id:'i', columns: ['n'] },
cartocss: '#style { }',
cartocss_version: '2.0.1'
} }
]
};
function checkCORSHeaders(res) {
assert.equal(
res.headers['access-control-allow-headers'],
'X-Requested-With, X-Prototype-Version, X-CSRF-Token'
);
assert.equal(res.headers['access-control-allow-origin'], '*');
}
var keysToDelete;
beforeEach(function() {
keysToDelete = {};
});
afterEach(function(done) {
testHelper.deleteRedisKeys(keysToDelete, done);
});
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
it("can only be fetched from layer having an attributes spec", function(done) {
var expected_token;
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(test_mapconfig_1)
}, {}, function(res, err) { next(err, res); });
},
function checkPost(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
// CORS headers should be sent with response
// from layergroup creation via POST
checkCORSHeaders(res);
var parsedBody = JSON.parse(res.body);
if ( expected_token ) {
assert.deepEqual(parsedBody, {layergroupid: expected_token, layercount: 2});
} else {
expected_token = parsedBody.layergroupid;
}
return null;
},
function do_get_attr_0(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/attributes/1',
method: 'GET'
}, {}, function(res, err) { next(err, res); });
},
function check_error_0(err, res) {
assert.ifError(err);
assert.equal(
res.statusCode,
400,
res.statusCode + ( res.statusCode !== 200 ? (': ' + res.body) : '' )
);
var parsed = JSON.parse(res.body);
assert.equal(parsed.errors[0], "Layer 0 has no exposed attributes");
return null;
},
function do_get_attr_1(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/1/attributes/1',
method: 'GET'
}, {}, function(res, err) { next(err, res); });
},
function check_attr_1(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.deepEqual(parsed, {"n":6});
return null;
},
function do_get_attr_1_404(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/1/attributes/-666',
method: 'GET'
}, {}, function(res, err) { next(err, res); });
},
function check_attr_1_404(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 404, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors);
var msg = parsed.errors[0];
assert.ok(msg.match(/0 features.*identified by fid -666/), msg);
return null;
},
function finish(err) {
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done(err);
}
);
});
// See https://github.com/CartoDB/Windshaft/issues/131
it("are checked at map creation time", function(done) {
// clone the mapconfig test
var mapconfig = JSON.parse(JSON.stringify(test_mapconfig_1));
// append unexistant attribute name
mapconfig.layers[1].options.sql = 'SELECT * FROM test_table';
mapconfig.layers[1].options.attributes.id = 'unexistant';
mapconfig.layers[1].options.attributes.columns = ['cartodb_id'];
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(mapconfig)
}, {}, function(res, err) { next(err, res); });
},
function checkPost(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 404, res.statusCode + ': ' + (res.statusCode===200?'...':res.body));
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors);
assert.equal(parsed.errors.length, 1);
var msg = parsed.errors[0];
assert.equal(msg, 'column "unexistant" does not exist');
return null;
},
function finish(err) {
done(err);
}
);
});
it("can be used with jsonp", function(done) {
var expected_token;
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(test_mapconfig_1)
}, {}, function(res, err) { next(err, res); });
},
function checkPost(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
// CORS headers should be sent with response
// from layergroup creation via POST
checkCORSHeaders(res);
var parsedBody = JSON.parse(res.body);
if ( expected_token ) {
assert.deepEqual(parsedBody, {layergroupid: expected_token, layercount: 2});
} else {
expected_token = parsedBody.layergroupid;
}
return null;
},
function do_get_attr_0(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token +
'/0/attributes/1?callback=test',
method: 'GET'
}, {}, function(res, err) { next(err, res); });
},
function check_error_0(err, res) {
assert.ifError(err);
// jsonp errors should be returned with HTTP status 200
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
assert.equal(
res.body,
'/**/ typeof test === \'function\' && test({"errors":["Layer 0 has no exposed attributes"]});'
);
return null;
},
function do_get_attr_1(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/1/attributes/1',
method: 'GET'
}, {}, function(res, err) { next(err, res); });
},
function check_attr_1(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.deepEqual(parsed, {"n":6});
return null;
},
function finish(err) {
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done(err);
}
);
});
// Test that you cannot write to the database from an attributes tile request
//
// Test for http://github.com/CartoDB/Windshaft/issues/130
//
it("database access is read-only", function(done) {
// clone the mapconfig test
var mapconfig = JSON.parse(JSON.stringify(test_mapconfig_1));
mapconfig.layers[1].options.sql +=
", test_table_inserter(st_setsrid(st_point(0,0),4326),'write') as w";
mapconfig.layers[1].options.attributes.columns.push('w');
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(mapconfig)
}, {}, function(res, err) { next(err, res); });
},
function checkPost(err, res) {
assert.ifError(err);
// TODO: should be 403 Forbidden
assert.equal(res.statusCode, 400, res.statusCode + ': ' + (res.statusCode===200?'...':res.body));
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors);
assert.equal(parsed.errors.length, 1);
var msg = parsed.errors[0];
assert.equal(msg, "cannot execute INSERT in a read-only transaction");
return null;
},
function finish(err) {
done(err);
}
);
});
});

View File

@@ -0,0 +1,105 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('blend png renderer', function() {
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
var IMAGE_TOLERANCE_PER_MIL = 20;
function plainTorqueMapConfig(plainColor) {
return {
version: '1.2.0',
layers: [
{
type: 'plain',
options: {
color: plainColor
}
},
{
type: 'torque',
options: {
sql: "SELECT * FROM populated_places_simple_reduced " +
"where the_geom && ST_MakeEnvelope(-90, 0, 90, 65)",
cartocss: [
'Map {',
' buffer-size:0;',
' -torque-frame-count:1;',
' -torque-animation-duration:30;',
' -torque-time-attribute:"cartodb_id";',
' -torque-aggregation-function:"count(cartodb_id)";',
' -torque-resolution:1;',
' -torque-data-aggregation:linear;',
'}',
'#populated_places_simple_reduced{',
' comp-op: multiply;',
' marker-fill-opacity: 1;',
' marker-line-color: #FFF;',
' marker-line-width: 0;',
' marker-line-opacity: 1;',
' marker-type: rectangle;',
' marker-width: 3;',
' marker-fill: #FFCC00;',
'}'
].join(' '),
cartocss_version: '2.3.0'
}
}
]
};
}
var testScenarios = [
{
tile: {
z: 2,
x: 2,
y: 1,
layer: 'all',
format: 'png'
},
plainColor: 'white'
},
{
tile: {
z: 2,
x: 1,
y: 1,
layer: 'all',
format: 'png'
},
plainColor: '#339900'
}
];
function blendPngFixture(zxy) {
return './test/fixtures/blend/blend-plain-torque-' + zxy.join('.') + '.png';
}
testScenarios.forEach(function(testScenario) {
var tileRequest = testScenario.tile;
var zxy = [tileRequest.z, tileRequest.x, tileRequest.y];
it('tile all/' + zxy.join('/') + '.png', function (done) {
testClient.getTileLayer(plainTorqueMapConfig(testScenario.plainColor), tileRequest, function(err, res) {
assert.imageEqualsFile(res.body, blendPngFixture(zxy), IMAGE_TOLERANCE_PER_MIL, function(err) {
assert.ok(!err);
done();
});
});
});
});
});

View File

@@ -0,0 +1,166 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var fs = require('fs');
var http = require('http');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('blend layer filtering', function() {
var IMG_TOLERANCE_PER_MIL = 20;
var httpRendererResourcesServer;
var req2paramsFn;
before(function(done) {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
// Start a server to test external resources
httpRendererResourcesServer = http.createServer( function(request, response) {
var filename = __dirname + '/../../fixtures/http/light_nolabels-1-0-0.png';
fs.readFile(filename, {encoding: 'binary'}, function(err, file) {
response.writeHead(200);
response.write(file, "binary");
response.end();
});
});
httpRendererResourcesServer.listen(8033, done);
});
after(function(done) {
BaseController.prototype.req2params = req2paramsFn;
httpRendererResourcesServer.close(done);
});
var mapConfig = {
version: '1.2.0',
layers: [
{
type: 'plain',
options: {
color: '#fabada'
}
},
{
type: 'http',
options: {
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
subdomains: ['abcd']
}
},
{
type: 'mapnik',
options: {
sql: 'SELECT * FROM populated_places_simple_reduced',
cartocss: '#layer { marker-fill:red; } #layer { marker-width: 2; }',
cartocss_version: '2.3.0',
geom_column: 'the_geom'
}
},
{
type: 'torque',
options: {
sql: "SELECT * FROM populated_places_simple_reduced",
cartocss: [
'Map {',
' buffer-size:0;',
' -torque-frame-count:1;',
' -torque-animation-duration:30;',
' -torque-time-attribute:"cartodb_id";',
' -torque-aggregation-function:"count(cartodb_id)";',
' -torque-resolution:1;',
' -torque-data-aggregation:linear;',
'}',
'#populated_places_simple_reduced{',
' comp-op: multiply;',
' marker-fill-opacity: 1;',
' marker-line-color: #FFF;',
' marker-line-width: 0;',
' marker-line-opacity: 1;',
' marker-type: rectangle;',
' marker-width: 3;',
' marker-fill: #FFCC00;',
'}'
].join(' '),
cartocss_version: '2.3.0'
}
},
{
type: 'http',
options: {
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
subdomains: ['abcd']
}
},
{
type: 'torque',
options: {
sql: "SELECT * FROM populated_places_simple_reduced " +
"where the_geom && ST_MakeEnvelope(-90, 0, 90, 65)",
cartocss: [
'Map {',
' buffer-size:0;',
' -torque-frame-count:1;',
' -torque-animation-duration:30;',
' -torque-time-attribute:"cartodb_id";',
' -torque-aggregation-function:"count(cartodb_id)";',
' -torque-resolution:1;',
' -torque-data-aggregation:linear;',
'}',
'#populated_places_simple_reduced{',
' comp-op: multiply;',
' marker-fill-opacity: 1;',
' marker-line-color: #FFF;',
' marker-line-width: 0;',
' marker-line-opacity: 1;',
' marker-type: rectangle;',
' marker-width: 3;',
' marker-fill: #FFCC00;',
'}'
].join(' '),
cartocss_version: '2.3.0'
}
}
]
};
var filteredLayersSuite = [
[2, 2],
[0, 1],
[0, 2],
[1, 2],
[2, 1], // ordering doesn't matter
[0, 3],
[1, 3],
[1, 2, 5],
[1, 2, 3, 4]
];
function blendPngFixture(layers) {
return './test/fixtures/blend/blend-filtering-layers-' + layers.join('.') + '-zxy-1.0.0.png';
}
filteredLayersSuite.forEach(function(filteredLayers) {
var layerFilter = filteredLayers.join(',');
var tileRequest = {
z: 1,
x: 0,
y: 0,
layer: layerFilter,
format: 'png'
};
it('should filter on ' + layerFilter + '/1/0/0.png', function (done) {
testClient.getTileLayer(mapConfig, tileRequest, function(err, res) {
assert.imageEqualsFile(res.body, blendPngFixture(filteredLayers), IMG_TOLERANCE_PER_MIL, function(err) {
assert.ok(!err);
done();
});
});
});
});
});

View File

@@ -0,0 +1,147 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var fs = require('fs');
var http = require('http');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('blend http fallback', function() {
var IMG_TOLERANCE_PER_MIL = 20;
var httpRendererResourcesServer;
var req2paramsFn;
before(function(done) {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
// Start a server to test external resources
httpRendererResourcesServer = http.createServer( function(request, response) {
if (request.url.match(/^\/error404\//)) {
response.writeHead(404);
response.end();
} else {
var filename = __dirname + '/../../fixtures/http/light_nolabels-1-0-0.png';
if (request.url.match(/^\/dark\//)) {
filename = __dirname + '/../../fixtures/http/dark_nolabels-1-0-0.png';
}
fs.readFile(filename, {encoding: 'binary'}, function(err, file) {
response.writeHead(200);
response.write(file, "binary");
response.end();
});
}
});
httpRendererResourcesServer.listen(8033, done);
});
after(function(done) {
BaseController.prototype.req2params = req2paramsFn;
httpRendererResourcesServer.close(done);
});
var mapConfig = {
version: '1.2.0',
layers: [
{
type: 'plain', // <- 0
options: {
color: '#fabada'
}
},
{
type: 'http', // <- 1
options: {
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
subdomains: ['light']
}
},
{
type: 'http', // <- 2
options: {
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
subdomains: ['dark']
}
},
{
type: 'http', // <- 3
options: {
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
subdomains: ['error404']
}
},
{
type: 'mapnik', // <- 4
options: {
sql: 'SELECT * FROM populated_places_simple_reduced',
cartocss: '#layer { marker-fill:red; } #layer { marker-width: 2; }',
cartocss_version: '2.3.0',
geom_column: 'the_geom'
}
}
]
};
var filteredLayersSuite = [
['all'], // layers displayed: 2 + 4, skipping 3 as it fails
[0, 4],
[0, 3], // skips layer 3 as it fails
[1, 2],
[1, 3],
[2, 3],
[3, 4]
];
function blendPngFixture(layers) {
return './test/fixtures/blend/http_fallback/blend-layers-' + layers.join('.') + '-zxy-1.0.0.png';
}
filteredLayersSuite.forEach(function(filteredLayers) {
var layerFilter = filteredLayers.join(',');
var tileRequest = {
z: 1,
x: 0,
y: 0,
layer: layerFilter,
format: 'png'
};
it('should fallback on http error while blending layers ' + layerFilter + '/1/0/0.png', function (done) {
testClient.getTileLayer(mapConfig, tileRequest, function(err, res) {
assert.imageEqualsFile(res.body, blendPngFixture(filteredLayers), IMG_TOLERANCE_PER_MIL, function(err) {
assert.ok(!err, err);
done();
});
});
});
});
it('should keep failing when http layer is requested individually', function(done) {
var tileRequest = {
z: 1,
x: 0,
y: 0,
layer: 3,
format: 'png'
};
var expectedResponse = {
status: 400,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
testClient.getTileLayer(mapConfig, tileRequest, expectedResponse, function(err, res) {
assert.ok(!err);
var parsedBody = JSON.parse(res.body);
assert.deepEqual(parsedBody, {
errors: [
"Unable to fetch http tile: http://127.0.0.1:8033/error404/1/0/0.png [404]"
]
});
done();
});
});
});

View File

@@ -0,0 +1,95 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var serverOptions = require('./support/ported_server_options');
var fs = require('fs');
var http = require('http');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe.skip('blend http client timeout', function() {
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
var mapConfig = {
version: '1.3.0',
layers: [
{
type: 'http',
options: {
urlTemplate: 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png',
subdomains: ['light']
}
},
{
type: 'mapnik',
options: {
sql: 'SELECT * FROM populated_places_simple_reduced',
cartocss: '#layer { marker-fill:red; } #layer { marker-width: 2; }',
cartocss_version: '2.3.0',
geom_column: 'the_geom'
}
}
]
};
var oldHttpRendererTimeout;
var httpRendererTimeout = 100;
var slowHttpRendererResourcesServer;
before(function(done) {
oldHttpRendererTimeout = serverOptions.renderer.http.timeout;
serverOptions.renderer.http.timeout = httpRendererTimeout;
// Start a server to test external resources
slowHttpRendererResourcesServer = http.createServer( function(request, response) {
setTimeout(function() {
var filename = __dirname + '/../fixtures/http/light_nolabels-1-0-0.png';
fs.readFile(filename, {encoding: 'binary'}, function(err, file) {
response.writeHead(200);
response.write(file, "binary");
response.end();
});
}, httpRendererTimeout * 2);
});
slowHttpRendererResourcesServer.listen(8033, done);
});
after(function(done) {
serverOptions.renderer.http.timeout = oldHttpRendererTimeout;
slowHttpRendererResourcesServer.close(done);
});
it('should fail to render when http layer times out', function(done) {
var options = {
statusCode: 400,
contentType: 'application/json; charset=utf-8',
serverOptions: serverOptions
};
testClient.withLayergroup(mapConfig, options, function(err, requestTile, finish) {
var tileUrl = '/all/0/0/0.png';
requestTile(tileUrl, options, function(err, res) {
assert.ok(!err);
var parsedBody = JSON.parse(res.body);
assert.ok(parsedBody.errors);
assert.ok(parsedBody.errors.length);
assert.equal(parsedBody.errors[0], 'Unable to fetch http tile: http://127.0.0.1:8033/light/0/0/0.png');
finish(function(finishErr) {
done(err || finishErr);
});
});
});
});
});

View File

@@ -0,0 +1,129 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var fs = require('fs');
var PortedServerOptions = require('./support/ported_server_options');
var http = require('http');
var testClient = require('./support/test_client');
var nock = require('nock');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('external resources', function() {
var res_serv; // resources server
var res_serv_status = { numrequests:0 }; // status of resources server
var res_serv_port = 8033; // FIXME: make configurable ?
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 25;
var req2paramsFn;
before(function(done) {
nock.enableNetConnect('127.0.0.1');
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
// Start a server to test external resources
res_serv = http.createServer( function(request, response) {
++res_serv_status.numrequests;
var filename = __dirname + '/../../fixtures/markers' + request.url;
fs.readFile(filename, "binary", function(err, file) {
if ( err ) {
response.writeHead(404, {'Content-Type': 'text/plain'});
response.write("404 Not Found\n");
} else {
response.writeHead(200);
response.write(file, "binary");
}
response.end();
});
});
res_serv.listen(res_serv_port, done);
});
after(function(done) {
BaseController.prototype.req2params = req2paramsFn;
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
// Close the resources server
res_serv.close(done);
});
function imageCompareFn(fixture, done) {
return function(err, res) {
if (err) {
return done(err);
}
assert.imageEqualsFile(res.body, './test/fixtures/' + fixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, done);
};
}
it("basic external resource", function(done) {
var circleStyle = "#test_table_3 { marker-file: url('http://127.0.0.1:" + res_serv_port +
"/circle.svg'); marker-transform:'scale(0.2)'; }";
testClient.getTile(testClient.defaultTableMapConfig('test_table_3', circleStyle), 13, 4011, 3088,
imageCompareFn('test_table_13_4011_3088_svg1.png', done));
});
it("different external resource", function(done) {
var squareStyle = "#test_table_3 { marker-file: url('http://127.0.0.1:" + res_serv_port +
"/square.svg'); marker-transform:'scale(0.2)'; }";
testClient.getTile(testClient.defaultTableMapConfig('test_table_3', squareStyle), 13, 4011, 3088,
imageCompareFn('test_table_13_4011_3088_svg2.png', done));
});
// See http://github.com/CartoDB/Windshaft/issues/107
it("external resources get localized on renderer creation if not locally cached", function(done) {
var options = {
serverOptions: PortedServerOptions
};
var externalResourceStyle = "#test_table_3{marker-file: url('http://127.0.0.1:" + res_serv_port +
"/square.svg'); marker-transform:'scale(0.2)'; }";
var externalResourceMapConfig = testClient.defaultTableMapConfig('test_table_3', externalResourceStyle);
testClient.createLayergroup(externalResourceMapConfig, options, function() {
var externalResourceRequestsCount = res_serv_status.numrequests;
testClient.createLayergroup(externalResourceMapConfig, options, function() {
assert.equal(res_serv_status.numrequests, externalResourceRequestsCount);
// reset resources cache
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
externalResourceMapConfig = testClient.defaultTableMapConfig('test_table_3 ', externalResourceStyle);
testClient.createLayergroup(externalResourceMapConfig, options, function() {
assert.equal(res_serv_status.numrequests, externalResourceRequestsCount + 1);
done();
});
});
});
});
it("referencing unexistant external resources returns an error", function(done) {
var url = "http://127.0.0.1:" + res_serv_port + "/notfound.png";
var style = "#test_table_3{marker-file: url('" + url + "'); marker-transform:'scale(0.2)'; }";
var mapConfig = testClient.defaultTableMapConfig('test_table_3', style);
testClient.createLayergroup(mapConfig, { statusCode: 400 }, function(err, res) {
assert.deepEqual(JSON.parse(res.body), {
errors: ["Unable to download '" + url + "' for 'style0' (server returned 404)"]
});
done();
});
});
});

View File

@@ -0,0 +1 @@
{"grid":[" "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," !!! "," !!!!!! "," !!!!!!! "," !!!!!!!! "," !!!!!!!!! "," !!!!!!!! "," !!!!!!! "," !!!!!! "," !!! "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "],"keys":["","2"],"data":{"2":{"cartodb_id":2}}}

View File

@@ -0,0 +1 @@
{"grid":[" "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," !! "," !!! "," !! "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "],"keys":["","2"],"data":{"2":{"cartodb_id":4}}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

View File

@@ -0,0 +1,96 @@
require('../../support/test_helper');
var fs = require('fs');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var serverOptions = require('./support/ported_server_options');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe.skip('render limits', function() {
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 25;
var limitsConfig;
var onTileErrorStrategy;
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
limitsConfig = serverOptions.renderer.mapnik.limits;
serverOptions.renderer.mapnik.limits = {
render: 50,
cacheOnTimeout: false
};
onTileErrorStrategy = serverOptions.renderer.onTileErrorStrategy;
serverOptions.renderer.onTileErrorStrategy = function(err, tile, headers, stats, format, callback) {
callback(err, tile, headers, stats);
};
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
serverOptions.renderer.mapnik.limits = limitsConfig;
serverOptions.renderer.onTileErrorStrategy = onTileErrorStrategy;
});
var slowQuery = 'select pg_sleep(1), * from test_table limit 2';
var slowQueryMapConfig = testClient.singleLayerMapConfig(slowQuery);
it('slow query/render returns with 400 status', function(done) {
var options = {
statusCode: 400,
serverOptions: serverOptions
};
testClient.createLayergroup(slowQueryMapConfig, options, function(err, res) {
assert.deepEqual(JSON.parse(res.body), { errors: ["Render timed out"] });
done();
});
});
it('uses onTileErrorStrategy to handle error and modify response', function(done) {
serverOptions.renderer.onTileErrorStrategy = function(err, tile, headers, stats, format, callback) {
var fixture = __dirname + '/../../fixtures/limits/fallback.png';
fs.readFile(fixture, {encoding: 'binary'}, function(err, img) {
callback(null, img, {'Content-Type': 'image/png'}, {});
});
};
var options = {
statusCode: 200,
contentType: 'image/png',
serverOptions: serverOptions
};
testClient.createLayergroup(slowQueryMapConfig, options, function(err, res) {
var parsed = JSON.parse(res.body);
assert.ok(parsed.layergroupid);
done();
});
});
it('returns a fallback tile that was modified via onTileErrorStrategy', function(done) {
var fixtureImage = './test/fixtures/limits/fallback.png';
serverOptions.renderer.onTileErrorStrategy = function(err, tile, headers, stats, format, callback) {
fs.readFile(fixtureImage, {encoding: null}, function(err, img) {
callback(null, img, {'Content-Type': 'image/png'}, {});
});
};
var options = {
statusCode: 200,
contentType: 'image/png',
serverOptions: serverOptions
};
testClient.withLayergroup(slowQueryMapConfig, options, function(err, requestTile, finish) {
var tileUrl = '/0/0/0.png';
requestTile(tileUrl, options, function(err, res) {
assert.imageEqualsFile(res.body, fixtureImage, IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
finish(function(finishErr) {
done(err || finishErr);
});
});
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,465 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var step = require('step');
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var testClient = require('./support/test_client');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('multilayer error cases', function() {
var server = cartodbServer(ServerOptions);
server.setMaxListeners(0);
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
it("post layergroup with wrong Content-Type", function(done) {
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded' }
}, {}, function(res) {
assert.equal(res.statusCode, 400, res.body);
var parsedBody = JSON.parse(res.body);
assert.deepEqual(parsedBody, {"errors":["layergroup POST data must be of type application/json"]});
done();
});
});
it("post layergroup with no layers", function(done) {
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' }
}, {}, function(res) {
assert.equal(res.statusCode, 400, res.body);
var parsedBody = JSON.parse(res.body);
assert.deepEqual(parsedBody, {"errors":["Missing layers array from layergroup config"]});
done();
});
});
it("post layergroup jsonp errors are returned with 200 status", function(done) {
assert.response(server, {
url: '/database/windshaft_test/layergroup?callback=test',
method: 'POST',
headers: {'Content-Type': 'application/json' }
}, {}, function(res) {
assert.equal(res.statusCode, 200);
assert.equal(
res.body,
'/**/ typeof test === \'function\' && test({"errors":["Missing layers array from layergroup config"]});'
);
done();
});
});
// See https://github.com/CartoDB/Windshaft/issues/154
it("mapnik tokens cannot be used with attributes service", function(done) {
var layergroup = {
version: '1.1.0',
layers: [
{ options: {
sql: 'select cartodb_id, 1 as n, the_geom, !bbox! as b from test_table limit 1',
cartocss: '#layer { marker-fill:red }',
cartocss_version: '2.0.1',
attributes: { id:'cartodb_id', columns:['n'] },
geom_column: 'the_geom'
} }
]
};
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json; charset=utf-8' },
data: JSON.stringify(layergroup)
}, {}, function(res, err) { next(err, res); });
},
function do_check(err, res) {
assert.equal(res.statusCode, 400, res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors);
assert.equal(parsed.errors.length, 1);
var msg = parsed.errors[0];
assert.ok(msg.match(/Attribute service cannot be activated/), msg);
return null;
},
function finish(err) {
done(err);
}
);
});
it("layergroup with no cartocss_version", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
{ options: {
sql: 'select cartodb_id, ST_Translate(the_geom, 50, 0) as the_geom from test_table limit 2',
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
geom_column: 'the_geom'
} }
]
};
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) {
assert.equal(res.statusCode, 400, res.body);
var parsedBody = JSON.parse(res.body);
assert.deepEqual(parsedBody, {errors:["Missing cartocss_version for layer 0 options"]});
done();
});
});
it("sql/cartocss combination errors", function(done) {
var layergroup = {
version: '1.0.1',
layers: [{ options: {
sql: "select 1 as i, 'LINESTRING(0 0, 1 0)'::geometry as the_geom",
cartocss_version: '2.0.2',
cartocss: '#layer [missing=1] { line-width:16; }',
geom_column: 'the_geom'
}}]
};
ServerOptions.afterLayergroupCreateCalls = 0;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) {
try {
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
// See http://github.com/CartoDB/Windshaft/issues/159
assert.equal(ServerOptions.afterLayergroupCreateCalls, 0);
var parsed = JSON.parse(res.body);
assert.ok(parsed);
assert.equal(parsed.errors.length, 1);
var error = parsed.errors[0];
assert.ok(error.match(/column "missing" does not exist/m), error);
// cannot check for error starting with style0 until a new enough mapnik
// is used: https://github.com/mapnik/mapnik/issues/1924
//assert.ok(error.match(/^style0/), "Error doesn't start with style0: " + error);
// TODO: check which layer introduced the problem ?
done();
} catch (err) { done(err); }
});
});
it("sql/interactivity combination error", function(done) {
var layergroup = {
version: '1.0.1',
layers: [
{ options: {
sql: "select 1 as i, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom",
cartocss_version: '2.0.2',
cartocss: '#layer { line-width:16; }',
interactivity: 'i',
geom_column: 'the_geom'
}},
{ options: {
sql: "select 1 as i, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom",
cartocss_version: '2.0.2',
cartocss: '#layer { line-width:16; }',
geom_column: 'the_geom'
}},
{ options: {
sql: "select 1 as i, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom",
cartocss_version: '2.0.2',
cartocss: '#layer { line-width:16; }',
interactivity: 'missing',
geom_column: 'the_geom'
}}
]
};
ServerOptions.afterLayergroupCreateCalls = 0;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) {
try {
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
// See http://github.com/CartoDB/Windshaft/issues/159
assert.equal(ServerOptions.afterLayergroupCreateCalls, 0);
var parsed = JSON.parse(res.body);
assert.ok(parsed);
assert.equal(parsed.errors.length, 1);
var error = parsed.errors[0];
assert.ok(error.match(/column "missing" does not exist/m), error);
// TODO: check which layer introduced the problem ?
done();
} catch (err) { done(err); }
});
});
it("blank CartoCSS error", function(done) {
var layergroup = {
version: '1.0.1',
layers: [
{ options: {
sql: "select 1 as i, 'LINESTRING(0 0, 1 0)'::geometry as the_geom",
cartocss_version: '2.0.2',
cartocss: '#style { line-width:16 }',
interactivity: 'i',
geom_column: 'the_geom'
}},
{ options: {
sql: "select 1 as i, 'LINESTRING(0 0, 1 0)'::geometry as the_geom",
cartocss_version: '2.0.2',
cartocss: '',
interactivity: 'i',
geom_column: 'the_geom'
}}
]
};
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) {
try {
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed);
assert.equal(parsed.errors.length, 1);
var error = parsed.errors[0];
assert.ok(error.match(/^style1: CartoCSS is empty/), error);
done();
} catch (err) { done(err); }
});
});
it("Invalid mapnik-geometry-type CartoCSS error", function(done) {
var layergroup = {
version: '1.0.1',
layers: [
{ options: {
sql: "select 1 as i, 'LINESTRING(0 0, 1 0)'::geometry as the_geom",
cartocss_version: '2.0.2',
cartocss: '#style [mapnik-geometry-type=bogus] { line-width:16 }',
geom_column: 'the_geom'
}},
{ options: {
sql: "select 1 as i, 'LINESTRING(0 0, 1 0)'::geometry as the_geom",
cartocss_version: '2.0.2',
cartocss: '#style [mapnik-geometry-type=bogus] { line-width:16 }',
geom_column: 'the_geom'
}}
]
};
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) {
try {
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed);
assert.equal(parsed.errors.length, 1);
var error = parsed.errors[0];
// carto-0.9.3 used to say "Failed to parse expression",
// carto-0.9.5 says "not a valid keyword"
assert.ok(error.match(/^style0:.*(Failed|not a valid)/), error);
// TODO: check which layer introduced the problem ?
done();
} catch (err) { done(err); }
});
});
it("post'ing style with non existent column in filter returns 400 with error", function(done) {
var layergroup = {
version: '1.0.1',
layers: [
{ options: {
sql: 'select * from test_table limit 1',
cartocss: '#test_table::outline[address="one"], [address="two"] { marker-fill: red; }',
cartocss_version: '2.0.2',
interactivity: [ 'cartodb_id' ],
geom_column: 'the_geom'
} },
{ options: {
sql: 'select * from test_big_poly limit 1',
cartocss: '#test_big_poly { marker-fill:blue }',
cartocss_version: '2.0.2',
interactivity: [ 'cartodb_id' ],
geom_column: 'the_geom'
} }
]
};
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) {
assert.equal(res.statusCode, 400, res.body);
var parsed = JSON.parse(res.body);
assert.equal(parsed.errors.length, 1);
var error = parsed.errors[0];
assert.ok(error.match(/column "address" does not exist/m), error);
done();
});
});
// See https://github.com/Vizzuality/Windshaft/issues/31
it('bogus sql raises 400 status code', function(done) {
var bogusSqlMapConfig = testClient.singleLayerMapConfig('BOGUS FROM test_table');
testClient.createLayergroup(bogusSqlMapConfig, { statusCode: 400 }, function(err, res) {
assert.ok(/syntax error/.test(res.body), "Unexpected error: " + res.body);
done();
});
});
it('bogus sql raises 200 status code for jsonp', function(done) {
var bogusSqlMapConfig = testClient.singleLayerMapConfig('bogus');
var options = {
method: 'GET',
callbackName: 'test',
headers: {
'Content-Type': 'text/javascript; charset=utf-8'
}
};
testClient.createLayergroup(bogusSqlMapConfig, options, function(err, res) {
assert.ok(
/^\/\*\*\/ typeof test === 'function' && test\(/.test(res.body),
"Body start expected callback name: " + res.body
);
assert.ok(/syntax error/.test(res.body), "Unexpected error: " + res.body);
done();
});
});
it('query not selecting the_geom raises 200 status code for jsonp instead of 404', function(done) {
var noGeomMapConfig = testClient.singleLayerMapConfig('select null::geometry the_geom_wadus');
var options = {
method: 'GET',
callbackName: 'test',
headers: {
'Content-Type': 'text/javascript; charset=utf-8'
}
};
testClient.createLayergroup(noGeomMapConfig, options, function(err, res) {
assert.ok(
/^\/\*\*\/ typeof test === 'function' && test\(/.test(res.body),
"Body start expected callback name: " + res.body
);
assert.ok(/column.*does not exist/.test(res.body), "Unexpected error: " + res.body);
done();
});
});
it("query with no geometry field returns 400 status", function(done){
var noGeometrySqlMapConfig = testClient.singleLayerMapConfig('SELECT 1');
testClient.createLayergroup(noGeometrySqlMapConfig, { statusCode: 400 }, function(err, res) {
assert.ok(/column.*does not exist/.test(res.body), "Unexpected error: " + res.body);
done();
});
});
it("bogus style should raise 400 status", function(done){
var bogusStyleMapConfig = testClient.defaultTableMapConfig('test_table', '#test_table{xxxxx;}');
testClient.createLayergroup(bogusStyleMapConfig, { method: 'GET', statusCode: 400 }, done);
});
var defaultErrorExpectedResponse = {
status: 400,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
it('should raise 400 error for out of bounds layer index', function(done){
var mapConfig = testClient.singleLayerMapConfig('select * from test_table', null, null, 'name');
testClient.getGrid(mapConfig, 1, 13, 4011, 3088, defaultErrorExpectedResponse, function(err, res) {
assert.deepEqual(JSON.parse(res.body), { errors: ["Layer '1' not found in layergroup"] });
done();
});
});
////////////////////////////////////////////////////////////////////
//
// OPTIONS LAYERGROUP
//
////////////////////////////////////////////////////////////////////
it("nonexistent layergroup token error", function(done) {
step(
function do_get_tile(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/deadbeef/0/0/0/0.grid.json',
method: 'GET',
encoding: 'binary'
}, {}, function(res, err) { next(err, res); });
},
function checkResponse(err, res) {
assert.ifError(err);
// FIXME: should be 404
assert.equal(res.statusCode, 400, res.statusCode + ':' + res.body);
var parsed = JSON.parse(res.body);
assert.deepEqual(parsed, {"errors": ["Invalid or nonexistent map configuration token 'deadbeef'"]});
return null;
},
function finish(err) {
done(err);
}
);
});
it('error 400 on json syntax error', function(done) {
var layergroup = {
version: '1.0.1',
layers: [
{
options: {
sql: 'select the_geom from test_table limit 1',
cartocss: '#layer { marker-fill:red }'
}
}
]
};
assert.response(server,
{
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json; charset=utf-8' },
data: '{' + JSON.stringify(layergroup)
},
{
status: 400
},
function(res) {
var parsedBody = JSON.parse(res.body);
assert.deepEqual(parsedBody, { errors: ['SyntaxError: Unexpected token {'] });
done();
}
);
});
});

View File

@@ -0,0 +1,377 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var _ = require('underscore');
var cartodbServer = require('../../../lib/cartodb/server');
var getLayerTypeFn = require('windshaft').model.MapConfig.prototype.getType;
var PortedServerOptions = require('./support/ported_server_options');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('multilayer interactivity and layers order', function() {
var server = cartodbServer(PortedServerOptions);
server.setMaxListeners(0);
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
function layerType(layer) {
return layer.type || 'undefined';
}
function testInteractivityLayersOrderScenario(testScenario) {
it(testScenario.desc, function(done) {
var layergroup = {
version: '1.3.0',
layers: testScenario.layers
};
PortedServerOptions.afterLayergroupCreateCalls = 0;
assert.response(server,
{
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(layergroup)
},
{
//status: 200, don't use status here to have a more meaningful error message
headers: {
'content-type': 'application/json; charset=utf-8'
}
},
function(response) {
assert.equal(
response.statusCode,
200,
'Expected status code 200, got ' + response.statusCode +
'\n\tResponse body: ' + response.body +
'\n\tLayer types: ' + layergroup.layers.map(layerType).join(', ')
);
// assert.equal(PortedServerOptions.afterLayergroupCreateCalls, 1);
var layergroupResponse = JSON.parse(response.body);
assert.ok(layergroupResponse);
var layergroupId = layergroupResponse.layergroupid;
assert.ok(layergroupId);
assert.equal(layergroupResponse.metadata.layers.length, layergroup.layers.length);
// check layers metadata at least match in number
var layersMetadata = layergroupResponse.metadata.layers;
assert.equal(layersMetadata.length, layergroup.layers.length);
for (var i = 0, len = layersMetadata.length; i < len; i++) {
assert.equal(
getLayerTypeFn(layersMetadata[i].type),
getLayerTypeFn(layergroup.layers[i].type)
);
}
// check torque metadata at least match in number
var torqueLayers = layergroup.layers.filter(function(layer) { return layer.type === 'torque'; });
if (torqueLayers.length) {
assert.equal(Object.keys(layergroupResponse.metadata.torque).length, torqueLayers.length);
}
var keysToDelete = {
'user:localhost:mapviews:global': 5
};
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
testHelper.deleteRedisKeys(keysToDelete, done);
}
);
});
}
var cartocssVersion = '2.3.0';
var cartocss = '#layer { line-width:16; }';
var sql = "select 1 as i, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom";
var sqlWadus = "select 1 as wadus, st_setsrid('LINESTRING(0 0, 1 0)'::geometry, 4326) as the_geom";
var httpLayer = {
type: 'http',
options: {
urlTemplate: 'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png',
subdomains: ['a','b','c']
}
};
var torqueLayer = {
type: 'torque',
options: {
sql: "select 1 id, '1970-01-02'::date d, 'POINT(0 0)'::geometry the_geom_webmercator",
cartocss: [
"Map {",
"-torque-frame-count:2;",
"-torque-resolution:3;",
"-torque-time-attribute:d;",
"-torque-aggregation-function:'count(id)';",
"}"
].join(' '),
cartocss_version: '2.0.1'
}
};
var mapnikLayer = {
type: 'mapnik',
options: {
sql: sql,
cartocss_version: cartocssVersion,
cartocss: cartocss,
geom_column: 'the_geom'
}
};
var mapnikInteractivityLayer = {
type: 'mapnik',
options: {
sql: sql,
cartocss_version: cartocssVersion,
cartocss: cartocss,
interactivity: 'i',
geom_column: 'the_geom'
}
};
var cartodbLayer = {
type: 'cartodb',
options: {
sql: sql,
cartocss_version: cartocssVersion,
cartocss: cartocss,
geom_column: 'the_geom'
}
};
var cartodbInteractivityLayer = {
type: 'cartodb',
options: {
sql: sql,
cartocss_version: cartocssVersion,
cartocss: cartocss,
interactivity: 'i',
geom_column: 'the_geom'
}
};
var cartodbWadusInteractivityLayer = {
type: 'cartodb',
options: {
sql: sqlWadus,
cartocss_version: cartocssVersion,
cartocss: cartocss,
interactivity: 'wadus',
geom_column: 'the_geom'
}
};
var noTypeLayer = {
options: {
sql: sql,
cartocss_version: cartocssVersion,
cartocss: cartocss,
geom_column: 'the_geom'
}
};
var noTypeInteractivityLayer = {
options: {
sql: sql,
cartocss_version: cartocssVersion,
cartocss: cartocss,
interactivity: 'i',
geom_column: 'the_geom'
}
};
var allLayers = [
httpLayer,
torqueLayer,
mapnikLayer,
mapnikInteractivityLayer,
cartodbLayer,
cartodbInteractivityLayer,
cartodbWadusInteractivityLayer,
noTypeLayer,
noTypeInteractivityLayer
];
var testScenarios = [
{
desc: 'one layer, no interactivity',
layers: [cartodbLayer]
},
{
desc: 'one layer with interactivity',
layers: [cartodbInteractivityLayer]
},
{
desc: 'two layers, interactivity mix',
layers: [
cartodbLayer,
cartodbInteractivityLayer
]
},
{
desc: 'two layers, different interactivity columns',
layers: [
cartodbWadusInteractivityLayer,
cartodbInteractivityLayer
]
},
{
desc: 'mix of no interactivity with interactivity',
layers: [
cartodbInteractivityLayer,
cartodbLayer,
cartodbWadusInteractivityLayer
]
},
{
desc: 'interactivity layers + torque',
layers: [
cartodbWadusInteractivityLayer,
cartodbInteractivityLayer,
torqueLayer
]
},
{
desc: 'torque + interactivity layers',
layers: [
torqueLayer,
cartodbWadusInteractivityLayer,
cartodbInteractivityLayer
]
},
{
desc: 'interactivity + torque + interactivity',
layers: [
cartodbInteractivityLayer,
torqueLayer,
cartodbInteractivityLayer
]
},
{
desc: 'http + mix of [no] interactivity layers',
layers: [
httpLayer,
cartodbInteractivityLayer,
cartodbLayer,
cartodbInteractivityLayer
]
},
{
desc: 'http + interactivity + torque + interactivity',
layers: [
httpLayer,
cartodbInteractivityLayer,
torqueLayer,
cartodbInteractivityLayer
]
},
{
desc: 'http + interactivity + torque + no interactivity + torque + interactivity',
layers: [
httpLayer,
cartodbInteractivityLayer,
torqueLayer,
cartodbLayer,
torqueLayer,
cartodbWadusInteractivityLayer
]
},
{
desc: 'mapnik type two layers, interactivity mix',
layers: [
mapnikLayer,
mapnikInteractivityLayer
]
},
{
desc: 'mapnik type mix of no interactivity with interactivity',
layers: [
cartodbInteractivityLayer,
cartodbLayer,
mapnikInteractivityLayer
]
},
{
desc: 'mapnik type interactivity layers + torque',
layers: [
mapnikInteractivityLayer,
cartodbInteractivityLayer,
torqueLayer
]
},
{
desc: 'mapnik type http + interactivity + torque + interactivity',
layers: [
httpLayer,
mapnikInteractivityLayer,
torqueLayer,
cartodbInteractivityLayer
]
},
{
desc: 'no type two layers, interactivity mix',
layers: [
noTypeLayer,
noTypeInteractivityLayer
]
},
{
desc: 'no type mix of no interactivity with interactivity',
layers: [
noTypeInteractivityLayer,
noTypeLayer,
mapnikInteractivityLayer
]
},
{
desc: 'no type interactivity layers + torque',
layers: [
noTypeLayer,
noTypeInteractivityLayer,
torqueLayer
]
},
{
desc: 'no type http + interactivity + torque + interactivity',
layers: [
httpLayer,
noTypeInteractivityLayer,
torqueLayer,
noTypeInteractivityLayer
]
}
];
var chaosScenariosSize = 25;
var chaosScenarios = [];
for (var i = 0; i < chaosScenariosSize; i++) {
// Underscore.js' sample method uses Fisher-Yates shuffle internally, see http://bost.ocks.org/mike/shuffle/
var randomLayers = _.sample(allLayers, _.random(1, allLayers.length));
chaosScenarios.push({
desc: 'chaos scenario layer types: ' + randomLayers.map(layerType).join(', '),
layers: randomLayers
});
}
testScenarios.forEach(testInteractivityLayersOrderScenario);
chaosScenarios.forEach(testInteractivityLayersOrderScenario);
});

View File

@@ -0,0 +1,158 @@
var testHelper =require('../../support/test_helper');
var assert = require('../../support/assert');
var step = require('step');
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
describe('raster', function() {
var server = cartodbServer(ServerOptions);
server.setMaxListeners(0);
function checkCORSHeaders(res) {
assert.equal(res.headers['access-control-allow-headers'], 'X-Requested-With, X-Prototype-Version, X-CSRF-Token');
assert.equal(res.headers['access-control-allow-origin'], '*');
}
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 2;
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
it("can render raster for valid mapconfig", function(done) {
var mapconfig = {
version: '1.2.0',
layers: [
{ type: 'mapnik', options: {
sql: "select ST_AsRaster(" +
" ST_MakeEnvelope(-100,-40, 100, 40, 4326), " +
" 1.0, -1.0, '8BUI', 127) as rst",
geom_column: 'rst',
geom_type: 'raster',
cartocss: '#layer { raster-opacity:1.0 }',
cartocss_version: '2.0.1'
} }
]
};
var expected_token;
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(mapconfig)
}, {}, function(res, err) { next(err, res); });
},
function checkPost(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
// CORS headers should be sent with response
// from layergroup creation via POST
checkCORSHeaders(res);
var parsedBody = JSON.parse(res.body);
if ( expected_token ) {
assert.deepEqual(parsedBody, {layergroupid: expected_token, layercount: 2});
} else {
expected_token = parsedBody.layergroupid;
}
return null;
},
function do_get_tile(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/0/0.png',
method: 'GET',
encoding: 'binary'
}, {}, function(res, err) { next(err, res); });
},
function check_response(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.body);
assert.deepEqual(res.headers['content-type'], "image/png");
var next = this;
assert.imageEqualsFile(res.body,
'./test/fixtures/raster_gray_rect.png',
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
try {
assert.ifError(err);
next();
} catch (err) { next(err); }
});
},
function finish(err) {
if (err) {
return done(err);
}
var keysToDelete = {
'user:localhost:mapviews:global': 5
};
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
testHelper.deleteRedisKeys(keysToDelete, done);
}
);
});
it("raster geom type does not allow interactivity", function(done) {
var mapconfig = {
version: '1.2.0',
layers: [
{
type: 'cartodb',
options: {
sql: [
"select 1 id,",
"ST_AsRaster(ST_MakeEnvelope(-100, -40, 100, 40, 4326), 1.0, -1.0, '8BUI', 127) as rst"
].join(' '),
geom_column: 'rst',
geom_type: 'raster',
cartocss: '#layer { raster-opacity: 1.0 }',
cartocss_version: '2.0.1',
interactivity: 'id'
}
}
]
};
assert.response(server,
{
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(mapconfig)
},
{
status: 400
},
function(res, err) {
assert.ok(!err);
checkCORSHeaders(res);
var parsedBody = JSON.parse(res.body);
assert.deepEqual(parsedBody, { errors: [ 'Mapnik raster layers do not support interactivity' ] });
done();
}
);
});
});

View File

@@ -0,0 +1,64 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var ServerOptions = require('./support/ported_server_options');
var testClient = require('./support/test_client');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('regressions', function() {
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
});
// See https://github.com/Vizzuality/Windshaft/issues/65
it("#65 catching non-Error exception doesn't kill the backend", function(done) {
var mapConfig = testClient.defaultTableMapConfig('test_table');
testClient.withLayergroup(mapConfig, function(err, requestTile, finish) {
var options = {
statusCode: 400,
contentType: 'application/json; charset=utf-8'
};
requestTile('/0/0/0.png?testUnexpectedError=1', options, function(err, res) {
assert.deepEqual(JSON.parse(res.body), { "errors": ["test unexpected error"] });
finish(done);
});
});
});
// Test that you cannot write to the database from a tile request
//
// See http://github.com/CartoDB/Windshaft/issues/130
// [x] Needs a fix on the mapnik side: https://github.com/mapnik/mapnik/pull/2143
//
it("#130 database access is read-only", function(done) {
var writeSqlMapConfig = testClient.singleLayerMapConfig(
'select st_point(0,0) as the_geom, * from test_table_inserter(st_setsrid(st_point(0,0),4326),\'write\')'
);
var expectedResponse = {
status: 400,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
testClient.getTile(writeSqlMapConfig, 0, 0, 0, expectedResponse, function(err, res) {
var parsedBody = JSON.parse(res.body);
assert.ok(parsedBody.errors);
assert.equal(parsedBody.errors.length, 1);
assert.ok(parsedBody.errors[0].match(/read-only transaction/), 'read-only error message expected');
done();
});
});
});

View File

@@ -0,0 +1,138 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var mapnik = require('windshaft').mapnik;
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
describe('retina support', function() {
var layergroupId = null;
var server = cartodbServer(ServerOptions);
server.setMaxListeners(0);
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
var keysToDelete;
beforeEach(function(done) {
keysToDelete = {'user:localhost:mapviews:global': 5};
var retinaSampleMapConfig = {
version: '1.2.0',
layers: [
{
options: {
sql: 'SELECT * FROM populated_places_simple_reduced',
cartocss: '#layer { marker-fill:red; } #layer { marker-width: 2; }',
cartocss_version: '2.3.0',
geom_column: 'the_geom'
}
}
]
};
assert.response(server,
{
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(retinaSampleMapConfig)
},
{
},
function (res, err) {
assert.ok(!err, 'Failed to create layergroup');
layergroupId = JSON.parse(res.body).layergroupid;
done();
}
);
});
afterEach(function(done) {
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
testHelper.deleteRedisKeys(keysToDelete, done);
});
function testRetinaImage(scaleFactor, responseHead, assertFn) {
assert.response(server,
{
url: '/database/windshaft_test/layergroup/' + layergroupId + '/0/0/0' + scaleFactor + '.png',
method: 'GET',
encoding: 'binary'
},
responseHead,
assertFn
);
}
function testValidImageDimmensions(scaleFactor, imageSize, done) {
testRetinaImage(scaleFactor,
{
status: 200,
headers: {
'Content-Type': 'image/png'
}
},
function(res, err) {
assert.ok(!err, 'Failed to request 0/0/0' + scaleFactor + '.png tile');
var image = new mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
assert.equal(image.width(), imageSize);
assert.equal(image.height(), imageSize);
done();
}
);
}
it('image dimensions when scale factor is not defined', function(done) {
testValidImageDimmensions('', 256, done);
});
it('image dimensions when scale factor = @1x', function(done) {
testValidImageDimmensions('@1x', 256, done);
});
it('image dimensions when scale factor = @2x', function(done) {
testValidImageDimmensions('@2x', 512, done);
});
it('error when scale factor is not enabled', function(done) {
var scaleFactor = '@4x';
testRetinaImage(scaleFactor,
{
status: 404,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
},
function(res, err) {
assert.ok(!err, 'Failed to request 0/0/0' + scaleFactor + '.png tile');
assert.deepEqual(JSON.parse(res.body), { errors: ["Tile with specified resolution not found"] } );
done();
}
);
});
});

View File

@@ -0,0 +1,170 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var testClient = require('./support/test_client');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('server', function() {
var server = cartodbServer(ServerOptions);
server.setMaxListeners(0);
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
});
////////////////////////////////////////////////////////////////////
//
// GET INVALID
//
////////////////////////////////////////////////////////////////////
it("get call to server returns 200", function(done){
assert.response(server, {
url: '/',
method: 'GET'
},{
// FIXME: shouldn't this be a 404 ?
status: 200
}, function() { done(); } );
});
////////////////////////////////////////////////////////////////////
//
// GET VERSION
//
////////////////////////////////////////////////////////////////////
it("get /version returns versions", function(done){
assert.response(server, {
url: '/version',
method: 'GET'
},{
status: 200
}, function(res) {
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('windshaft'), "No 'windshaft' version in " + parsed);
assert.ok(parsed.hasOwnProperty('grainstore'), "No 'grainstore' version in " + parsed);
assert.ok(parsed.hasOwnProperty('node_mapnik'), "No 'node_mapnik' version in " + parsed);
assert.ok(parsed.hasOwnProperty('mapnik'), "No 'mapnik' version in " + parsed);
// TODO: check actual versions ?
done();
});
});
////////////////////////////////////////////////////////////////////
//
// GET GRID
//
////////////////////////////////////////////////////////////////////
it("grid jsonp", function(done){
var mapConfig = testClient.singleLayerMapConfig('select * from test_table', null, null, 'name');
testClient.getGridJsonp(mapConfig, 0, 13, 4011, 3088, 'jsonp_test', function(err, res) {
assert.equal(res.statusCode, 200, res.body);
assert.deepEqual(res.headers['content-type'], 'text/javascript; charset=utf-8');
var didRunJsonCallback = false;
var response = {};
// jshint ignore:start
function jsonp_test(body) {
response = body;
didRunJsonCallback = true;
}
eval(res.body);
// jshint ignore:end
assert.ok(didRunJsonCallback);
assert.utfgridEqualsFile(response, './test/fixtures/test_table_13_4011_3088.grid.json', 2, done);
});
});
it("get'ing a json with default style and single interactivity should return a grid", function(done){
var mapConfig = testClient.singleLayerMapConfig('select * from test_table', null, null, 'name');
testClient.getGrid(mapConfig, 0, 13, 4011, 3088, function(err, res) {
var expected_json = {
"1":{"name":"Hawai"},
"2":{"name":"El Estocolmo"},
"3":{"name":"El Rey del Tallarín"},
"4":{"name":"El Lacón"},
"5":{"name":"El Pico"}
};
assert.deepEqual(JSON.parse(res.body).data, expected_json);
done();
});
});
it("get'ing a json with default style and no interactivity should return an error", function(done){
var mapConfig = testClient.singleLayerMapConfig('select * from test_table');
var expectedResponse = {
status: 400,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
testClient.getGrid(mapConfig, 0, 13, 4011, 3088, expectedResponse, function(err, res) {
assert.deepEqual(JSON.parse(res.body), {"errors":["Tileset has no interactivity"]});
done();
});
});
it("get grid jsonp error is returned with 200 status", function(done){
var mapConfig = testClient.singleLayerMapConfig('select * from test_table');
var expectedResponse = {
status: 200,
headers: {
'Content-Type': 'text/javascript; charset=utf-8'
}
};
testClient.getGridJsonp(mapConfig, 0, 13, 4011, 3088, 'test', expectedResponse, function(err, res) {
assert.ok(res.body.match(/"errors":/), 'missing error in response: ' + res.body);
done();
});
});
// See http://github.com/Vizzuality/Windshaft/issues/50
it("get'ing a json with no data should return an empty grid", function(done){
var query = 'select * from test_table limit 0';
var mapConfig = testClient.singleLayerMapConfig(query, null, null, 'name');
testClient.getGrid(mapConfig, 0, 13, 4011, 3088, function(err, res) {
assert.utfgridEqualsFile(res.body, './test/fixtures/test_table_13_4011_3088_empty.grid.json', 2, done);
});
});
// Another test for http://github.com/Vizzuality/Windshaft/issues/50
it("get'ing a json with no data but interactivity should return an empty grid", function(done){
var query = 'SELECT * FROM test_table limit 0';
var mapConfig = testClient.singleLayerMapConfig(query, null, null, 'cartodb_id');
testClient.getGrid(mapConfig, 0, 13, 4011, 3088, function(err, res) {
assert.utfgridEqualsFile(res.body, './test/fixtures/test_table_13_4011_3088_empty.grid.json', 2, done);
});
});
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/67
it("get'ing a solid grid while changing interactivity fields", function(done){
var query = 'SELECT * FROM test_big_poly';
var style211 = "#test_big_poly{polygon-fill:blue;}"; // for solid
var mapConfigName = testClient.singleLayerMapConfig(query, style211, null, 'name');
testClient.getGrid(mapConfigName, 0, 3, 2, 2, function(err, res) {
var expected_data = { "1":{"name":"west"} };
assert.deepEqual(JSON.parse(res.body).data, expected_data);
var mapConfigCartodbId = testClient.singleLayerMapConfig(query, style211, null, 'cartodb_id');
testClient.getGrid(mapConfigCartodbId, 0, 3, 2, 2, function(err, res) {
var expected_data = { "1":{"cartodb_id":"1"} };
assert.deepEqual(JSON.parse(res.body).data, expected_data);
done();
});
});
});
});

View File

@@ -0,0 +1,261 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var mapnik = require('windshaft').mapnik;
var semver = require('semver');
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var testClient = require('./support/test_client');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('server_gettile', function() {
var server = cartodbServer(ServerOptions);
server.setMaxListeners(0);
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 25;
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
});
function imageCompareFn(fixture, done) {
return function(err, res) {
if (err) {
return done(err);
}
assert.imageEqualsFile(res.body, './test/fixtures/' + fixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, done);
};
}
////////////////////////////////////////////////////////////////////
//
// GET TILE
// --{
////////////////////////////////////////////////////////////////////
it("get'ing a tile with default style should return an expected tile", function(done){
testClient.getTile(testClient.defaultTableMapConfig('test_table'), 13, 4011, 3088,
imageCompareFn('test_table_13_4011_3088.png', done)
);
});
it("response of get tile can be served by renderer cache", function(done) {
var tileUrl = '/13/4011/3088.png';
var lastXwc;
var mapConfig = testClient.defaultTableMapConfig('test_table');
testClient.withLayergroup(mapConfig, function (err, requestTile, finish) {
requestTile(tileUrl, function (err, res) {
var xwc = res.headers['x-windshaft-cache'];
assert.ok(xwc);
assert.ok(xwc > 0);
lastXwc = xwc;
requestTile(tileUrl, function (err, res) {
var xwc = res.headers['x-windshaft-cache'];
assert.ok(xwc);
assert.ok(xwc > 0);
assert.ok(xwc >= lastXwc);
requestTile(tileUrl + '?cache_buster=wadus', function (err, res) {
var xwc = res.headers['x-windshaft-cache'];
assert.ok(!xwc);
finish(done);
});
});
});
});
});
it("should not choke when queries end with a semicolon", function(done){
testClient.getTile(testClient.singleLayerMapConfig('SELECT * FROM test_table limit 2;'), 0, 0, 0, done);
});
it("should not choke when sql ends with a semicolon and some blanks", function(done){
testClient.getTile(testClient.singleLayerMapConfig('SELECT * FROM test_table limit 2; \t\n'), 0, 0, 0, done);
});
it("should not strip quoted semicolons within an sql query", function(done){
testClient.getTile(
testClient.singleLayerMapConfig("SELECT * FROM test_table where name != ';\n'"), 0, 0, 0, done
);
});
it("getting two tiles with same configuration uses renderer cache", function(done) {
var imageFixture = './test/fixtures/test_table_13_4011_3088_styled.png';
var tileUrl = '/13/4011/3088.png';
var mapConfig = testClient.defaultTableMapConfig(
'test_table',
'#test_table{marker-fill: blue;marker-line-color: black;}'
);
function validateLayergroup(res) {
// cache is hit because we create a renderer to validate the map config
assert.ok(!res.headers.hasOwnProperty('x-windshaft-cache'), "Did hit renderer cache on first time");
}
testClient.withLayergroup(mapConfig, validateLayergroup, function(err, requestTile, finish) {
requestTile(tileUrl, function(err, res) {
assert.ok(res.headers.hasOwnProperty('x-windshaft-cache'), "Did not hit renderer cache on second time");
assert.ok(res.headers['x-windshaft-cache'] >= 0);
assert.imageEqualsFile(res.body, imageFixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
finish(function(finishErr) {
done(err || finishErr);
});
});
});
});
});
var test_style_black_200 = "#test_table{marker-fill:black;marker-line-color:black;marker-width:5}";
var test_style_black_210 = "#test_table{marker-fill:black;marker-line-color:black;marker-width:10}";
it("get'ing a tile with url specified 2.0.0 style should return an expected tile", function(done){
testClient.getTile(testClient.defaultTableMapConfig('test_table', test_style_black_200, '2.0.0'),
13, 4011, 3088, imageCompareFn('test_table_13_4011_3088_styled_black.png', done));
});
it("get'ing a tile with url specified 2.1.0 style should return an expected tile", function(done){
testClient.getTile(testClient.defaultTableMapConfig('test_table', test_style_black_210, '2.1.0'),
13, 4011, 3088, imageCompareFn('test_table_13_4011_3088_styled_black.png', done));
});
// See http://github.com/CartoDB/Windshaft/issues/99
it("unused directives are tolerated", function(done){
var style = "#test_table{point-transform: 'scale(100)';}";
var sql = "SELECT 1 as cartodb_id, 'SRID=4326;POINT(0 0)'::geometry as the_geom";
testClient.getTile(testClient.singleLayerMapConfig(sql, style), 0, 0, 0,
imageCompareFn('test_default_mapnik_point.png', done));
});
// See http://github.com/CartoDB/Windshaft/issues/100
var test_strictness = function(done) {
var nonStrictMapConfig = testClient.singleLayerMapConfig(
"SELECT 1 as cartodb_id, 'SRID=3857;POINT(666 666)'::geometry as the_geom",
"#test_table{point-transform: 'scale(100)';}"
);
testClient.withLayergroup(nonStrictMapConfig, function(err, requestTile, finish) {
var options = {
statusCode: 400,
contentType: 'application/json; charset=utf-8'
};
requestTile('/0/0/0.png?strict=1', options, function() {
finish(done);
});
});
};
var test_strict_lbl = "unused directives are not tolerated if strict";
if ( semver.satisfies(mapnik.versions.mapnik, '2.3.x') ) {
// Strictness handling changed in 2.3.x, possibly a bug: see http://github.com/mapnik/mapnik/issues/2301
it.skip('[skipped due to http://github.com/mapnik/mapnik/issues/2301]' + test_strict_lbl, test_strictness);
}
else {
it(test_strict_lbl, test_strictness);
}
it('high cpu regression with mapnik <2.3.x', function(done) {
var sql = [
"SELECT 'my polygon name here' AS name,",
"st_envelope(st_buffer(st_transform(",
"st_setsrid(st_makepoint(-26.6592894004,49.7990296995),4326),3857),10000000)) AS the_geom",
"FROM generate_series(-6,6) x",
"UNION ALL",
"SELECT 'my marker name here' AS name,",
" st_transform(st_setsrid(st_makepoint(49.6042060319,-49.0522997372),4326),3857) AS the_geom",
"FROM generate_series(-6,6) x"
].join(' ');
var style = [
'#test_table {marker-fill:#ff7;',
' marker-max-error:0.447492761618;',
' marker-line-opacity:0.659371340628;',
' marker-allow-overlap:true;',
' polygon-fill:green;',
' marker-spacing:0.0;',
' marker-width:4.0;',
' marker-height:18.0;',
' marker-opacity:0.942312062822;',
' line-color:green;',
' line-gamma:0.945973211092;',
' line-cap:square;',
' polygon-opacity:0.12576055992;',
' marker-type:arrow;',
' polygon-gamma:0.46354913107;',
' line-dasharray:33,23;',
' line-join:bevel;',
' marker-placement:line;',
' line-width:1.0;',
' marker-line-color:#ff7;',
' line-opacity:0.39403752154;',
' marker-line-width:3.0;',
'}'
].join('');
testClient.getTile(testClient.singleLayerMapConfig(sql, style), 13, 4011, 3088, done);
});
// https://github.com/CartoDB/Windshaft-cartodb/issues/316
it('should return errors with better formatting', function(done) {
var mapConfig = {
"version": "1.0.1",
"minzoom": 0,
"maxzoom": 20,
"layers": [
{
"type": 'mapnik',
"options": {
"cartocss_version": '2.1.1',
"sql": "SELECT null::geometry AS the_geom",
"cartocss": [
'@water: #cdd2d4;',
'Map {',
'\tbackground-color: @water;',
'\tbufferz-size: 256;',
'}',
'@landmass_fill: lighten(#e3e3dc, 8%);'
].join('\n')
}
},
{
"type": 'mapnik',
"options": {
"cartocss_version": '2.1.1',
"sql": "SELECT the_geom FROM false_background_zoomed('!scale_denominator!', !bbox!) AS _",
"cartocss": [
'#false_background {',
'\tpolygon-fill: @landmass_fill;',
'}'
].join('\n')
}
}
]
};
var options = {
statusCode: 400
};
testClient.createLayergroup(mapConfig, options, function(err, res, parsedBody) {
assert.ok(parsedBody.errors);
// more assertions when errors is populated with better format
done();
});
});
});

View File

@@ -0,0 +1,192 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var _ = require('underscore');
var fs = require('fs');
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 85;
describe('server_png8_format', function() {
var serverOptionsPng32 = ServerOptions;
serverOptionsPng32.grainstore = _.clone(ServerOptions.grainstore);
serverOptionsPng32.grainstore.mapnik_tile_format = 'png32';
var serverPng32 = new cartodbServer(serverOptionsPng32);
serverPng32.setMaxListeners(0);
var serverOptionsPng8 = ServerOptions;
serverOptionsPng8.grainstore = _.clone(ServerOptions.grainstore);
serverOptionsPng8.grainstore.mapnik_tile_format = 'png8:m=h';
var serverPng8 = new cartodbServer(serverOptionsPng8);
serverPng8.setMaxListeners(0);
var layergroupId;
var req2paramsFn;
before(function(done) {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
var testPngFilesDir = __dirname + '/../../results/png';
fs.readdirSync(testPngFilesDir)
.filter(function(fileName) {
return /.*\.png$/.test(fileName);
})
.map(function(fileName) {
return testPngFilesDir + '/' + fileName;
})
.forEach(fs.unlinkSync);
done();
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
var keysToDelete;
beforeEach(function() {
keysToDelete = {
'user:localhost:mapviews:global': 5
};
});
afterEach(function(done) {
testHelper.deleteRedisKeys(keysToDelete, done);
});
function testOutputForPng32AndPng8(desc, tile, callback) {
var bufferPng32,
bufferPng8;
it(desc + '; tile: ' + JSON.stringify(tile), function(done){
assert.response(
serverPng32,
{
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(layergroup)
},
{
status: 200
},
function(res, err) {
if (err) {
return done(err);
}
layergroupId = JSON.parse(res.body).layergroupid;
var tilePartialUrl = _.template('/<%= z %>/<%= x %>/<%= y %>.png', tile);
var requestPayload = {
url: '/database/windshaft_test/layergroup/' + layergroupId + tilePartialUrl,
method: 'GET',
encoding: 'binary'
};
var requestHeaders = {
status: 200,
headers: {
'Content-Type': 'image/png'
}
};
assert.response(serverPng32, requestPayload, requestHeaders, function(responsePng32) {
assert.equal(responsePng32.headers['content-type'], "image/png");
bufferPng32 = responsePng32.body;
assert.response(serverPng8, requestPayload, requestHeaders, function(responsePng8) {
assert.equal(responsePng8.headers['content-type'], "image/png");
bufferPng8 = responsePng8.body;
assert.ok(bufferPng8.length < bufferPng32.length);
assert.imageBuffersAreEqual(bufferPng32, bufferPng8, IMAGE_EQUALS_TOLERANCE_PER_MIL,
function(err, imagePaths, similarity) {
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
callback(err, imagePaths, similarity, done);
}
);
});
});
}
);
});
}
var currentLevel = 3,
allLevelTiles = [],
maxLevelTile = Math.pow(2, currentLevel);
for (var i = 0; i < maxLevelTile; i++) {
for (var j = 0; j < maxLevelTile; j++) {
allLevelTiles.push({
z: currentLevel,
x: i,
y: j
});
}
}
var layergroup = {
version: '1.3.0',
layers: [
{
options: {
sql: 'SELECT * FROM populated_places_simple_reduced',
cartocss: [
'#populated_places_simple_reduced {',
'marker-fill: #FFCC00;',
'marker-width: 10;',
'marker-line-color: #FFF;',
'marker-line-width: 1.5;',
'marker-line-opacity: 1;',
'marker-fill-opacity: 0.9;',
'marker-comp-op: multiply;',
'marker-type: ellipse;',
'marker-placement: point;',
'marker-allow-overlap: true;',
'marker-clip: false;',
'}'
].join(' '),
cartocss_version: '2.0.1'
}
}
]
};
var allImagePaths = [],
similarities = [];
allLevelTiles.forEach(function(tile) {
testOutputForPng32AndPng8('intensity visualization', tile, function(err, imagePaths, similarity, done) {
allImagePaths.push(imagePaths);
similarities.push(similarity);
var transformPaths = [];
for (var i = 0, len = allImagePaths.length; i < len; i++) {
if (similarities[i] > 0.075) {
transformPaths.push({
passive: allImagePaths[i][0],
active: allImagePaths[i][1],
similarity: similarities[i]
});
}
}
var output = 'handleResults(' + JSON.stringify(transformPaths) + ');';
fs.writeFileSync('test/results/png/results.js', output);
assert.ifError(err);
done();
});
});
});

View File

@@ -0,0 +1,154 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var http = require('http');
var fs = require('fs');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('static_maps', function() {
var validUrlTemplate = 'http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png';
var invalidUrlTemplate = 'http://127.0.0.1:8033/INVALID/{z}/{x}/{y}.png';
var httpRendererResourcesServer;
var req2paramsFn;
before(function(done) {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
// Start a server to test external resources
httpRendererResourcesServer = http.createServer( function(request, response) {
var filename = __dirname + '/../../fixtures/http/basemap.png';
fs.readFile(filename, {encoding: 'binary'}, function(err, file) {
response.writeHead(200);
response.write(file, "binary");
response.end();
});
});
httpRendererResourcesServer.listen(8033, done);
});
after(function(done) {
BaseController.prototype.req2params = req2paramsFn;
httpRendererResourcesServer.close(done);
});
function staticMapConfig(urlTemplate, cartocss) {
return {
version: '1.2.0',
layers: [
{
type: 'http',
options: {
urlTemplate: urlTemplate,
subdomains: ['abcd']
}
},
{
type: 'mapnik',
options: {
sql: 'SELECT * FROM populated_places_simple_reduced',
cartocss: cartocss || '#layer { marker-fill:red; } #layer { marker-width: 2; }',
cartocss_version: '2.3.0'
}
}
]
};
}
var zoom = 3,
lat = 0,
lon = 0,
width = 400,
height = 300;
it('center image', function (done) {
var mapConfig = staticMapConfig(validUrlTemplate);
testClient.getStaticCenter(mapConfig, zoom, lat, lon, width, height, function(err, res, image) {
if (err) {
return done(err);
}
assert.equal(image.width(), width);
assert.equal(image.height(), height);
done();
});
});
it('center image with invalid basemap', function (done) {
var mapConfig = staticMapConfig(invalidUrlTemplate);
testClient.getStaticCenter(mapConfig, zoom, lat, lon, width, height, function(err, res, image) {
if (err) {
return done(err);
}
assert.equal(image.width(), width);
assert.equal(image.height(), height);
done();
});
});
var west = -90,
south = -45,
east = 90,
north = 45,
bbWidth = 640,
bbHeight = 480;
it('bbox', function (done) {
var mapConfig = staticMapConfig(validUrlTemplate);
testClient.getStaticBbox(mapConfig, west, south, east, north, bbWidth, bbHeight, function(err, res, image) {
if (err) {
return done(err);
}
assert.equal(image.width(), bbWidth);
assert.equal(image.height(), bbHeight);
done();
});
});
it('should not fail for coordinates out of range', function (done) {
var outOfRangeHeight = 3000;
var mapConfig = staticMapConfig(validUrlTemplate);
testClient.getStaticCenter(mapConfig, 1, lat, lon, width, outOfRangeHeight, function(err, res, image) {
if (err) {
return done(err);
}
assert.equal(image.width(), width);
assert.equal(image.height(), outOfRangeHeight);
done();
});
});
it('should keep failing for other errors', function (done) {
var invalidStyleForZoom = '#layer { marker-fill:red; } #layer[zoom='+zoom+'] { marker-width: [wadus] * 2; }';
var mapConfig = staticMapConfig(validUrlTemplate, invalidStyleForZoom);
var expectedResponse = {
statusCode: 400,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
testClient.getStaticCenter(mapConfig, zoom, lat, lon, width, height, expectedResponse, function(err, res) {
assert.ok(!err);
var parsedBody = JSON.parse(res.body);
assert.ok(parsedBody.errors);
assert.ok(parsedBody.errors.length);
assert.ok(parsedBody.errors[0].match(/column \"wadus\" does not exist/));
done();
});
});
});

View File

@@ -0,0 +1,84 @@
var _ = require('underscore');
var serverOptions = require('../../../../lib/cartodb/server_options');
var LayergroupToken = require('../../../../lib/cartodb/models/layergroup_token');
var mapnik = require('windshaft').mapnik;
module.exports = _.extend({}, serverOptions, {
base_url: '/database/:dbname/table/:table',
base_url_mapconfig: '/database/:dbname/layergroup',
grainstore: {
datasource: {
geometry_field: 'the_geom',
srid: 4326
},
cachedir: global.environment.millstone.cache_basedir,
mapnik_version: global.environment.mapnik_version || mapnik.versions.mapnik,
gc_prob: 0 // run the garbage collector at each invocation
},
renderer: {
mapnik: {
poolSize: 4,//require('os').cpus().length,
metatile: 1,
bufferSize: 64,
snapToGrid: false,
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
scale_factors: [1, 2],
limits: {
render: 0,
cacheOnTimeout: true
}
},
http: {
timeout: 5000,
whitelist: ['http://127.0.0.1:8033/{s}/{z}/{x}/{y}.png'],
fallbackImage: {
type: 'fs',
src: __dirname + '/../../test/fixtures/http/basemap.png'
}
}
},
redis: global.environment.redis,
enable_cors: global.environment.enable_cors,
unbuffered_logging: true, // for smoother teardown from tests
log_format: null, // do not log anything
afterLayergroupCreateCalls: 0,
useProfiler: true,
req2params: function(req, callback){
if ( req.query.testUnexpectedError ) {
return callback('test unexpected error');
}
// this is in case you want to test sql parameters eg ...png?sql=select * from my_table limit 10
req.params = _.extend({}, req.params);
if (req.params.token) {
req.params.token = LayergroupToken.parse(req.params.token).token;
}
_.extend(req.params, req.query);
req.params.user = 'localhost';
req.context = {user: 'localhost'};
req.params.dbhost = global.environment.postgres.host;
req.params.dbport = req.params.dbport || global.environment.postgres.port;
req.params.dbuser = 'test_windshaft_publicuser';
if (req.params.dbname !== 'windshaft_test2') {
req.params.dbuser = 'test_windshaft_cartodb_user_1';
}
req.params.dbname = 'test_windshaft_cartodb_user_1_db';
// increment number of calls counter
global.req2params_calls = global.req2params_calls ? global.req2params_calls + 1 : 1;
// send the finished req object on
callback(null,req);
},
afterLayergroupCreate: function(req, cfg, res, callback) {
res.layercount = cfg.layers.length;
// config.afterLayergroupCreateCalls++;
callback(null);
}
});

View File

@@ -0,0 +1,488 @@
var testHelper = require('../../../support/test_helper');
var LayergroupToken = require('../../../../lib/cartodb/models/layergroup_token');
var step = require('step');
var assert = require('../../../support/assert');
var _ = require('underscore');
var querystring = require('querystring');
var mapnik = require('windshaft').mapnik;
var CartodbServer = require('../../../../lib/cartodb/server');
var PortedServerOptions = require('./ported_server_options');
var DEFAULT_POINT_STYLE = [
'#layer {',
' marker-fill: #FF6600;',
' marker-opacity: 1;',
' marker-width: 16;',
' marker-line-color: white;',
' marker-line-width: 3;',
' marker-line-opacity: 0.9;',
' marker-placement: point;',
' marker-type: ellipse;',
' marker-allow-overlap: true;',
'}'
].join('');
module.exports = {
createLayergroup: createLayergroup,
withLayergroup: withLayergroup,
singleLayerMapConfig: singleLayerMapConfig,
defaultTableMapConfig: defaultTableMapConfig,
getStaticBbox: getStaticBbox,
getStaticCenter: getStaticCenter,
getGrid: getGrid,
getGridJsonp: getGridJsonp,
getTorque: getTorque,
getTile: getTile,
getTileLayer: getTileLayer
};
var server = new CartodbServer(PortedServerOptions);
server.setMaxListeners(0);
var jsonContentType = 'application/json; charset=utf-8';
var jsContentType = 'text/javascript; charset=utf-8';
var pngContentType = 'image/png';
function createLayergroup(layergroupConfig, options, callback) {
if (!callback) {
callback = options;
options = {
method: 'POST',
statusCode: 200
};
}
var expectedResponse = {
status: options.statusCode || 200,
headers: options.headers || {
'Content-Type': 'application/json; charset=utf-8'
}
};
step(
function requestLayergroup() {
var next = this;
var request = layergroupRequest(layergroupConfig, options.method, options.callbackName, options.params);
assert.response(serverInstance(options), request, expectedResponse, function (res, err) {
next(err, res);
});
},
function validateLayergroup(err, res) {
assert.ok(!err, 'Failed to request layergroup');
var parsedBody;
var layergroupid;
if (options.callbackName) {
global[options.callbackName] = function(layergroup) {
layergroupid = layergroup.layergroupid;
};
// jshint ignore:start
eval(res.body);
// jshint ignore:end
delete global[options.callbackName];
} else {
parsedBody = JSON.parse(res.body);
layergroupid = parsedBody.layergroupid;
if (layergroupid) {
layergroupid = LayergroupToken.parse(layergroupid).token;
}
}
if (layergroupid) {
var keysToDelete = {
'user:localhost:mapviews:global': 5
};
var redisKey = 'map_cfg|' + layergroupid;
keysToDelete[redisKey] = 0;
testHelper.deleteRedisKeys(keysToDelete, function() {
return callback(err, res, parsedBody);
});
} else {
return callback(err, res, parsedBody);
}
}
);
}
function serverInstance(options) {
if (options.server) {
return options.server;
}
if (options.serverOptions) {
var otherServer = new CartodbServer(options.serverOptions);
otherServer.req2params = options.serverOptions.req2params;
otherServer.setMaxListeners(0);
return otherServer;
}
return server;
}
function layergroupRequest(layergroupConfig, method, callbackName, extraParams) {
method = method || 'POST';
var request = {
url: '/database/windshaft_test/layergroup',
headers: {
'Content-Type': 'application/json'
}
};
var urlParams = _.extend({}, extraParams);
if (callbackName) {
urlParams.callback = callbackName;
}
if (method.toUpperCase() === 'GET') {
request.method = 'GET';
urlParams.config = JSON.stringify(layergroupConfig);
} else {
request.method = 'POST';
request.data = JSON.stringify(layergroupConfig);
}
if (Object.keys(urlParams).length) {
request.url += '?' + querystring.stringify(urlParams);
}
return request;
}
function singleLayerMapConfig(sql, cartocss, cartocssVersion, interactivity) {
return {
version: '1.3.0',
layers: [
{
type: 'mapnik',
options: {
sql: sql,
cartocss: cartocss || DEFAULT_POINT_STYLE,
cartocss_version: cartocssVersion || '2.3.0',
interactivity: interactivity,
geom_column: 'the_geom'
}
}
]
};
}
function defaultTableMapConfig(tableName, cartocss, cartocssVersion, interactivity) {
return singleLayerMapConfig(defaultTableQuery(tableName), cartocss, cartocssVersion, interactivity);
}
function defaultTableQuery(tableName) {
return _.template('SELECT * FROM <%= tableName %>', {tableName: tableName});
}
function getStaticBbox(layergroupConfig, west, south, east, north, width, height, expectedResponse, callback) {
if (!callback) {
callback = expectedResponse;
expectedResponse = pngContentType;
}
var url = [
'static',
'bbox',
'<%= layergroupid %>',
[west, south, east, north].join(','),
width,
height
].join('/') + '.png';
return getGeneric(layergroupConfig, url, expectedResponse, callback);
}
function getStaticCenter(layergroupConfig, zoom, lat, lon, width, height, expectedResponse, callback) {
if (!callback) {
callback = expectedResponse;
expectedResponse = pngContentType;
}
var url = [
'static',
'center',
'<%= layergroupid %>',
zoom,
lat,
lon,
width,
height
].join('/') + '.png';
return getGeneric(layergroupConfig, url, expectedResponse, callback);
}
function getGrid(layergroupConfig, layer, z, x, y, expectedResponse, callback) {
if (!callback) {
callback = expectedResponse;
expectedResponse = jsonContentType;
}
var options = {
layer: layer,
z: z,
x: x,
y: y,
format: 'grid.json'
};
return getLayer(layergroupConfig, options, expectedResponse, callback);
}
function getGridJsonp(layergroupConfig, layer, z, x, y, jsonpCallbackName, expectedResponse, callback) {
if (!callback) {
callback = expectedResponse;
expectedResponse = jsContentType;
}
var options = {
layer: layer,
z: z,
x: x,
y: y,
format: 'grid.json',
jsonpCallbackName: jsonpCallbackName
};
return getLayer(layergroupConfig, options, expectedResponse, callback);
}
function getTorque(layergroupConfig, layer, z, x, y, expectedResponse, callback) {
if (!callback) {
callback = expectedResponse;
expectedResponse = jsonContentType;
}
var options = {
layer: layer,
z: z,
x: x,
y: y,
format: 'torque.json'
};
return getLayer(layergroupConfig, options, expectedResponse, callback);
}
function getTile(layergroupConfig, z, x, y, expectedResponse, callback) {
if (!callback) {
callback = expectedResponse;
expectedResponse = pngContentType;
}
var options = {
z: z,
x: x,
y: y,
format: 'png'
};
return getLayer(layergroupConfig, options, expectedResponse, callback);
}
function getTileLayer(layergroupConfig, options, expectedResponse, callback) {
if (!callback) {
callback = expectedResponse;
expectedResponse = pngContentType;
}
return getLayer(layergroupConfig, options, expectedResponse, callback);
}
function getLayer(layergroupConfig, options, expectedResponse, callback) {
return getGeneric(layergroupConfig, tileUrlStrategy(options), expectedResponse, callback);
}
function tileUrlStrategy(options) {
var urlLayerPattern = [
'<%= layer %>',
'<%= z %>',
'<%= x %>',
'<%= y %>'
].join('/') + '.<%= format %>';
if (options.jsonpCallbackName) {
urlLayerPattern += '?callback=<%= jsonpCallbackName %>';
}
var urlNoLayerPattern = [
'<%= z %>',
'<%= x %>',
'<%= y %>'
].join('/') + '.<%= format %>';
var urlTemplate = _.template((options.layer === undefined) ? urlNoLayerPattern : urlLayerPattern);
options.format = options.format || 'png';
return '<%= layergroupid %>/' + urlTemplate(_.defaults(options, { z: 0, x: 0, y: 0, layer: 0 }));
}
function getGeneric(layergroupConfig, url, expectedResponse, callback) {
if (_.isString(expectedResponse)) {
expectedResponse = {
status: 200,
headers: {
'Content-Type': expectedResponse
}
};
}
var contentType = expectedResponse.headers['Content-Type'];
var layergroupid = null;
step(
function requestLayergroup() {
var next = this;
var request = {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(layergroupConfig)
};
var expectedResponse = {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
assert.response(server, request, expectedResponse, function (res, err) {
next(err, res);
});
},
function validateLayergroup(err, res) {
assert.ok(!err, 'Failed to create layergroup');
var parsedBody = JSON.parse(res.body);
layergroupid = parsedBody.layergroupid;
assert.ok(layergroupid);
return res;
},
function requestTile(err, res) {
assert.ok(!err, 'Invalid layergroup response: ' + res.body);
var next = this;
var finalUrl = '/database/windshaft_test/layergroup/' + _.template(url, {
layergroupid: layergroupid
});
var request = {
url: finalUrl,
method: 'GET'
};
if (contentType === pngContentType) {
request.encoding = 'binary';
}
assert.response(server, request, expectedResponse, function (res, err) {
next(err, res);
});
},
function validateTile(err, res) {
assert.ok(!err, 'Failed to get tile');
var img;
if (contentType === pngContentType) {
img = new mapnik.Image.fromBytesSync(new Buffer(res.body, 'binary'));
}
var keysToDelete = {
'user:localhost:mapviews:global': 5
};
var redisKey = 'map_cfg|' + LayergroupToken.parse(layergroupid).token;
keysToDelete[redisKey] = 0;
testHelper.deleteRedisKeys(keysToDelete, function() {
return callback(err, res, img);
});
}
);
}
function withLayergroup(layergroupConfig, options, callback) {
var validationLayergroupFn = function() {};
if (!callback) {
callback = options;
options = {};
}
if (_.isFunction(options)) {
validationLayergroupFn = options;
options = {};
}
var layergroupExpectedResponse = {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
step(
function requestLayergroup() {
var next = this;
var request = layergroupRequest(layergroupConfig, 'POST');
assert.response(serverInstance(options), request, layergroupExpectedResponse, function (res, err) {
next(err, res);
});
},
function validateLayergroup(err, res) {
assert.ok(!err, 'Failed to request layergroup');
var parsedBody = JSON.parse(res.body);
var layergroupid = parsedBody.layergroupid;
assert.ok(layergroupid, 'No layergroup was created');
validationLayergroupFn(res);
function requestTile(layergroupUrl, options, callback) {
if (!callback) {
callback = options;
options = {
statusCode: 200,
contentType: pngContentType
};
}
var baseUrlTpl = '/database/windshaft_test/layergroup/<%= layergroupid %>';
var finalUrl = _.template(baseUrlTpl, { layergroupid: layergroupid }) + layergroupUrl;
var request = {
url: finalUrl,
method: 'GET'
};
if (options.contentType === pngContentType) {
request.encoding = 'binary';
}
var tileExpectedResponse = {
status: options.statusCode || 200,
headers: {
'Content-Type': options.contentType || pngContentType
}
};
assert.response(serverInstance(options), request, tileExpectedResponse, function (res, err) {
callback(err, res);
});
}
function finish(done) {
var keysToDelete = {
'user:localhost:mapviews:global': 5
};
var redisKey = 'map_cfg|' + LayergroupToken.parse(layergroupid).token;
keysToDelete[redisKey] = 0;
testHelper.deleteRedisKeys(keysToDelete, done);
}
return callback(err, requestTile, finish);
}
);
}

View File

@@ -0,0 +1,436 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var _ = require('underscore');
var step = require('step');
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
describe('torque', function() {
var server = cartodbServer(ServerOptions);
server.setMaxListeners(0);
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
var keysToDelete;
beforeEach(function() {
keysToDelete = {};
});
afterEach(function(done) {
testHelper.deleteRedisKeys(keysToDelete, done);
});
function checkCORSHeaders(res) {
assert.equal(res.headers['access-control-allow-headers'], 'X-Requested-With, X-Prototype-Version, X-CSRF-Token');
assert.equal(res.headers['access-control-allow-origin'], '*');
}
it("missing required property from torque layer", function(done) {
var layergroup = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: 'select cartodb_id, the_geom from test_table',
geom_column: 'the_geom',
srid: 4326,
cartocss: 'Map { marker-fill:blue; }'
} }
]
};
step(
function do_post1()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) { next(null, res); });
},
function checkResponse(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, parsed);
var error = parsed.errors[0];
assert.equal(error,
"Missing required property '-torque-frame-count' in torque layer CartoCSS");
return null;
},
function do_post2(err)
{
assert.ifError(err);
var next = this;
var css = 'Map { -torque-frame-count: 2; }';
layergroup.layers[0].options.cartocss = css;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) { next(null, res); });
},
function checkResponse2(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, parsed);
var error = parsed.errors[0];
assert.equal(error,
"Missing required property '-torque-resolution' in torque layer CartoCSS");
return null;
},
function do_post3(err)
{
assert.ifError(err);
var next = this;
var css = 'Map { -torque-frame-count: 2; -torque-resolution: 3; }';
layergroup.layers[0].options.cartocss = css;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) { next(null, res); });
},
function checkResponse3(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, parsed);
var error = parsed.errors[0];
assert.equal(error,
"Missing required property '-torque-aggregation-function' in torque layer CartoCSS");
return null;
},
function finish(err) {
done(err);
}
);
});
// See http://github.com/CartoDB/Windshaft/issues/150
it.skip("unquoted property in torque layer", function(done) {
var layergroup = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: 'select updated_at as d, cartodb_id as id, the_geom from test_table',
geom_column: 'the_geom',
srid: 4326,
cartocss: 'Map { -torque-frame-count:2; -torque-resolution:3; -torque-time-attribute:"d"; ' +
'-torque-aggregation-function:count(id); }'
} }
]
};
step(
function do_post1()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) { next(null, res); });
},
function checkResponse(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, parsed);
var error = parsed.errors[0];
assert.equal(error, "Something meaningful here");
return null;
},
function finish(err) {
done(err);
}
);
});
it("can render tile for valid mapconfig", function(done) {
var mapconfig = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: "select 1 as id, '1970-01-02'::date as d, 'POINT(0 0)'::geometry as the_geom UNION select 2, " +
"'1970-01-01'::date, 'POINT(1 1)'::geometry",
geom_column: 'the_geom',
cartocss: 'Map { -torque-frame-count:2; -torque-resolution:3; -torque-time-attribute:d; ' +
'-torque-aggregation-function:\'count(id)\'; }',
cartocss_version: '2.0.1'
} }
]
};
var expected_token;
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(mapconfig)
}, {}, function(res, err) { next(err, res); });
},
function checkPost(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
// CORS headers should be sent with response
// from layergroup creation via POST
checkCORSHeaders(res);
var parsedBody = JSON.parse(res.body);
if ( expected_token ) {
assert.deepEqual(parsedBody, {layergroupid: expected_token, layercount: 2});
} else {
expected_token = parsedBody.layergroupid;
}
var meta = parsedBody.metadata;
assert.ok(!_.isUndefined(meta),
'No metadata in torque MapConfig creation response: ' + res.body);
var tm = meta.torque;
assert.ok(tm,
'No "torque" in metadata:' + JSON.stringify(meta));
var tm0 = tm[0];
assert.ok(tm0,
'No layer 0 in "torque" in metadata:' + JSON.stringify(tm));
var expectedTorqueMetadata = {"start":0,"end":86400000,"data_steps":2,"column_type":"date"};
assert.deepEqual(tm0, expectedTorqueMetadata);
assert.deepEqual(meta.layers[0].meta, expectedTorqueMetadata);
return null;
},
function do_get_tile(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/0/0.png',
method: 'GET',
encoding: 'binary'
}, {}, function(res, err) { next(err, res); });
},
function check_mapnik_error_1(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ( res.statusCode !== 200 ? (': ' + res.body) : '' ));
var parsed = JSON.parse(res.body);
assert.equal(parsed.errors.length, 1);
assert.equal(parsed.errors[0], "No 'mapnik' layers in MapConfig");
return null;
},
function do_get_grid0(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/0/0/0.grid.json',
method: 'GET'
}, {}, function(res, err) { next(err, res); });
},
function check_mapnik_error_2(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ( res.statusCode !== 200 ? (': ' + res.body) : '' ));
var parsed = JSON.parse(res.body);
assert.equal(parsed.errors.length, 1);
assert.equal(parsed.errors[0], "Unsupported format grid.json");
return null;
},
function do_get_torque0(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/0/0/0.json.torque',
method: 'GET'
}, {}, function(res, err) { next(err, res); });
},
function check_torque0_response(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
var tile_content = [{"x__uint8":43,"y__uint8":43,"vals__uint8":[1,1],"dates__uint16":[0,1]}];
var parsed = JSON.parse(res.body);
assert.deepEqual(tile_content, parsed);
return null;
},
function do_get_torque0_1(err)
{
assert.ifError(err);
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/0/0/0.torque.json',
method: 'GET'
}, {}, function(res, err) { next(err, res); });
},
function check_torque0_response_1(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
var tile_content = [{"x__uint8":43,"y__uint8":43,"vals__uint8":[1,1],"dates__uint16":[0,1]}];
var parsed = JSON.parse(res.body);
assert.deepEqual(tile_content, parsed);
return null;
},
function finish(err) {
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done(err);
}
);
});
// Test that you cannot write to the database from a torque tile request
//
// Test for http://github.com/CartoDB/Windshaft/issues/130
//
it("database access is read-only", function(done) {
var mapconfig = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: "select 'SRID=3857;POINT(0 0)'::geometry as g, now() as d,* from " +
"test_table_inserter(st_setsrid(st_point(0,0),4326),'write')",
geom_column: 'g',
cartocss: 'Map { -torque-frame-count:2; -torque-resolution:3; -torque-time-attribute:d; ' +
'-torque-aggregation-function:\'count(*)\'; }',
cartocss_version: '2.0.1'
} }
]
};
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(mapconfig)
}, {}, function(res, err) { next(err, res); });
},
function checkPost(err, res) {
assert.ifError(err);
// TODO: should be 403 Forbidden
assert.equal(res.statusCode, 400, res.statusCode + ': ' + (res.statusCode===200?'...':res.body));
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors);
assert.equal(parsed.errors.length, 1);
var msg = parsed.errors[0];
assert.equal(msg, "TorqueRenderer: cannot execute INSERT in a read-only transaction");
return null;
},
function finish(err) {
done(err);
}
);
});
// See http://github.com/CartoDB/Windshaft/issues/164
it("gives a 500 on database connection refused", function(done) {
var mapconfig = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: "select 1 as id, '1970-01-03'::date as d, 'POINT(0 0)'::geometry as the_geom UNION select 2, " +
"'1970-01-01'::date, 'POINT(1 1)'::geometry",
geom_column: 'the_geom',
cartocss: 'Map { -torque-frame-count:2; -torque-resolution:3; -torque-time-attribute:d; ' +
'-torque-aggregation-function:\'count(id)\'; }',
cartocss_version: '2.0.1'
} }
]
};
step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup?dbport=1234567',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(mapconfig)
}, {}, function(res, err) { next(err, res); });
},
function checkPost(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 500, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, parsed);
var error = parsed.errors[0];
assert.equal(error, "TorqueRenderer: cannot connect to the database");
return null;
},
function finish(err) {
done(err);
}
);
});
it("checks types for torque-specific styles", function(done) {
var wrongStyle = ["Map {",
"-torque-frame-count:512;",
"-torque-animation-duration:30;",
"-torque-time-attribute:'cartodb_id';",
"-torque-aggregation-function:count(cartodb_id);", //unquoted aggregation function
"-torque-resolution:4;",
"-torque-data-aggregation:linear;",
"}"].join(" ");
var layergroup = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: 'select cartodb_id, the_geom from test_table',
geom_column: 'the_geom',
srid: 4326,
cartocss: wrongStyle
} }
]
};
step(
function request(){
var next = this;
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) { next(null, res); });
},
function checkResponse(err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
var parsed = JSON.parse(res.body);
assert.ok(parsed.errors, parsed);
var error = parsed.errors[0];
assert.equal(error,
"Unexpected type for property '-torque-aggregation-function', expected string");
done();
return null;
}
);
});
});

View File

@@ -0,0 +1,459 @@
var testHelper = require('../../support/test_helper');
var assert = require('../../support/assert');
var cartodbServer = require('../../../lib/cartodb/server');
var ServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
describe('torque boundary points', function() {
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = ServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
var layergroupIdToDelete = null;
beforeEach(function() {
layergroupIdToDelete = null;
});
afterEach(function(done) {
var keysToDelete = {
'user:localhost:mapviews:global': 5
};
var mapKey = 'map_cfg|' + LayergroupToken.parse(layergroupIdToDelete).token;
keysToDelete[mapKey] = 0;
testHelper.deleteRedisKeys(keysToDelete, done);
});
var server = cartodbServer(ServerOptions);
server.setMaxListeners(0);
var boundaryPointsMapConfig = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: "WITH p AS ( " +
" SELECT '1970-01-02'::date as d, " +
// r1 is pixel resolution at zoom level 1
// s1 is tile size at zoom level 1
" 1e-40 as o, 78271.517578125 as r1, 20037508.5 as s1 )" +
" SELECT 1 as i, d, ST_MakePoint(0,0) as ug FROM p" +
" UNION ALL " +
"SELECT 2, d, ST_MakePoint(-o,-o) FROM p" +
" UNION ALL " +
"SELECT 3, d, ST_MakePoint(-o,o) FROM p" +
" UNION ALL " +
"SELECT 4, d, ST_MakePoint(o,-o) FROM p" +
" UNION ALL " +
"SELECT 5, d, ST_MakePoint(o,o) FROM p" +
" UNION ALL " +
"SELECT 6, d, ST_MakePoint(-r1,r1) FROM p" +
" UNION ALL " +
"SELECT 7, d, ST_MakePoint(-r1/2,r1) FROM p" +
" UNION ALL " +
"SELECT 8, d, ST_MakePoint(-r1,-r1) FROM p" +
" UNION ALL " +
// this is discarded, being on the right boundary
"SELECT 9, d, ST_MakePoint(s1,0) FROM p" +
" UNION ALL " +
// this is discarded, being on the top boundary
"SELECT 10, d, ST_MakePoint(0,s1) FROM p" +
" UNION ALL " +
// this is discarded, rounding to the right boundary
"SELECT 11, d, ST_MakePoint(s1-r1/2,0) FROM p" +
" UNION ALL " +
// this is discarded, rounding to the top boundary
"SELECT 12, d, ST_MakePoint(0,s1-r1/2) FROM p",
geom_column: 'ug',
cartocss: 'Map { ' +
'-torque-frame-count:2; ' +
'-torque-resolution:1; ' +
'-torque-time-attribute:"d"; ' +
'-torque-data-aggregation:linear; ' +
'-torque-aggregation-function:"count(i)"; }',
cartocss_version: '1.0.1'
} }
]
};
var tileRequests = [
{
desc: '0/0/0',
repr: [
' ------',
'| |',
'| |',
' ------'
],
z: 0,
x: 0,
y: 0,
expects: [
{
x__uint8: 255,
y__uint8: 128,
vals__uint8: [{
v: 2,
d: 'i=9 and i=11'
}],
dates__uint16: [0]
},
{
x__uint8: 128,
y__uint8: 128,
vals__uint8: [{
v: 8,
d: 'i=[1..8]'
}],
dates__uint16: [0]
},
{
x__uint8: 128,
y__uint8: 255,
vals__uint8: [{
v: 2,
d: 'i=10 and i=12'
}],
dates__uint16: [0]
}
]
},
{
desc: '1/0/0',
repr: [
'*00 | 10',
'----+----',
'01 | 11'
],
z: 1,
x: 0,
y: 0,
expects: [
{
x__uint8: 255,
y__uint8: 0,
vals__uint8: [{v: 2, d: 'i=6 (-r1,r1) and i=7(-r1/2,r1)'}],
dates__uint16: [0]
},
{
x__uint8: 255,
y__uint8: 1,
vals__uint8: [{v: 2, d: 'i=1 and i=3'}],
dates__uint16: [0]
},
{
x__uint8: 255,
y__uint8: 255,
vals__uint8: [{v: 2, d: 'i=10 and i=12'}],
dates__uint16: [0]
}
]
},
{
desc: '1/1/0',
repr: [
'00 | 10*',
'----+----',
'01 | 11'
],
z: 1,
x: 1,
y: 0,
expects: [
{
x__uint8: 255,
y__uint8: 0,
vals__uint8: [{v: 2, d: 'i=9 and i=11'}],
dates__uint16: [0]
},
{
x__uint8: 0,
y__uint8: 0,
vals__uint8: [{v: 2, d: 'i=1 and i=5'}],
dates__uint16: [0]
},
{
x__uint8: 0,
y__uint8: 255,
vals__uint8: [{v: 2, d: 'i=10 and i=12'}],
dates__uint16: [0]
}
]
},
{
desc: '1/0/1',
repr: [
'00 | 10',
'----+----',
'*01 | 11'
],
z: 1,
x: 0,
y: 1,
expects: [
{
x__uint8: 255,
y__uint8: 255,
vals__uint8: [{v: 3, d: 'i=1, i=2 and i=8'}],
dates__uint16: [0]
}
]
},
{
desc: '1/1/1',
repr: [
'00 | 10',
'----+----',
'01 | 11*'
],
z: 1,
x: 1,
y: 1,
expects: [
{
x__uint8: 255,
y__uint8: 255,
vals__uint8: [{
v: 2,
d: 'i=9 and i=11'
}],
dates__uint16: [0]
},
{
x__uint8: 0,
y__uint8: 255,
vals__uint8: [{
v: 2,
d: 'i=1 and i=4'
}],
dates__uint16: [0]
}
]
}
];
tileRequests.forEach(function(tileRequest) {
// See https://github.com/CartoDB/Windshaft/issues/186
var desc = 'handles ' + tileRequest.desc + '.json.torque\n\n\t' + tileRequest.repr.join('\n\t') + '\n\n';
it(desc, function (done) {
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(boundaryPointsMapConfig)
}, {}, function (res, err) {
assert.ok(!err, 'Failed to create layergroup');
var parsedBody = JSON.parse(res.body);
var expected_token = parsedBody.layergroupid;
layergroupIdToDelete = expected_token;
var partialUrl = tileRequest.z + '/' + tileRequest.x + '/' + tileRequest.y;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + expected_token + '/0/' + partialUrl + '.json.torque',
method: 'GET'
}, {}, function (res, err) {
assert.ok(!err, 'Failed to get json');
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
var parsed = JSON.parse(res.body);
var i = 0;
tileRequest.expects.forEach(function(expected) {
assert.equal(
parsed[i].x__uint8,
expected.x__uint8,
parsed[i].x__uint8 + ' == ' + expected.x__uint8 +
'\nRESULT\n------' +
'\n' + JSON.stringify(parsed, null, 4) +
'\nEXPECTED\n--------' +
'\n' + JSON.stringify(tileRequest.expects, null, 4)
);
assert.equal(
parsed[i].y__uint8,
expected.y__uint8,
parsed[i].y__uint8 + ' == ' + expected.y__uint8 +
'\nRESULT\n------' +
'\n' + JSON.stringify(parsed, null, 4) +
'\nEXPECTED\n--------' +
'\n' + JSON.stringify(tileRequest.expects, null, 4)
);
var j = 0;
expected.vals__uint8.forEach(function(val) {
assert.equal(
parsed[i].vals__uint8[j],
val.v,
'desc: ' + val.d +
'Number of points got=' + parsed.length + '; ' +
'expected=' + tileRequest.expects.length +
'\n\tindex=' + i +
'\n\tvals__uint8 index=' + j +
'\n\tgot=' + parsed[i].vals__uint8[j] +
'\n\texpected=' + val.v +
'\nRESULT\n------' +
'\n' + JSON.stringify(parsed, null, 4) +
'\nEXPECTED\n--------' +
'\n' + JSON.stringify(tileRequest.expects, null, 4));
j++;
});
i++;
});
assert.equal(
parsed.length,
tileRequest.expects.length,
'Number of points did not match ' +
'got=' + parsed.length + '; ' +
'expected=' + tileRequest.expects.length +
'\nRESULT\n------' +
'\n' + JSON.stringify(parsed, null, 4) +
'\nEXPECTED\n--------' +
'\n' + JSON.stringify(tileRequest.expects, null, 4));
done();
});
});
});
});
it('regression london point', function(done) {
var londonPointMapConfig = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: "SELECT " +
"1 as i, " +
"'1970-01-02'::date as d, " +
"ST_MakePoint(-11309.9155492599,6715342.44989312) g",
geom_column: 'g',
cartocss: 'Map { ' +
'-torque-frame-count:2; ' +
'-torque-resolution:1; ' +
'-torque-time-attribute:"d"; ' +
'-torque-data-aggregation:linear; ' +
'-torque-aggregation-function:"count(i)"; }',
cartocss_version: '1.0.1'
} }
]
};
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(londonPointMapConfig)
}, {}, function (res, err) {
assert.ok(!err, 'Failed to create layergroup');
var parsedBody = JSON.parse(res.body);
var layergroupId = parsedBody.layergroupid;
layergroupIdToDelete = layergroupId;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + layergroupId + '/0/2/1/1.json.torque',
method: 'GET'
}, {}, function (res, err) {
assert.ok(!err, 'Failed to request torque.json');
var parsed = JSON.parse(res.body);
assert.deepEqual(parsed, [{
x__uint8: 255,
y__uint8: 172,
vals__uint8: [1],
dates__uint16: [0]
}]);
done();
});
});
});
it('should consider resolution for least value in query', function(done) {
var londonPointMapConfig = {
version: '1.1.0',
layers: [
{ type: 'torque', options: {
sql: "" +
"SELECT " +
"0 as i, " +
"st_transform('0101000020E6100000FABD3AB4B5031C400581A80ECC2F4940'::geometry, 3857) as g " +
"UNION ALL " +
"SELECT " +
"2 as i, " +
"st_transform('0101000020E61000006739E30EAE031C406625C0C3C72F4940'::geometry, 3857) as g " +
"UNION ALL " +
"SELECT " +
"3 as i, " +
"st_transform('0101000020E6100000E26DB8A2A7031C40C8BAA5C2C52F4940'::geometry, 3857) as g",
geom_column: 'g',
cartocss: 'Map { ' +
'-torque-frame-count:1; ' +
'-torque-animation-duration:30;' +
'-torque-time-attribute:"i"; ' +
'-torque-aggregation-function:"count(i)"; ' +
'-torque-resolution:2; ' +
'-torque-data-aggregation: cumulative; }',
cartocss_version: '1.0.1'
} }
]
};
assert.response(server, {
url: '/database/windshaft_test/layergroup',
method: 'POST',
headers: {'Content-Type': 'application/json' },
data: JSON.stringify(londonPointMapConfig)
}, {}, function (res, err) {
assert.ok(!err, 'Failed to create layergroup');
var parsedBody = JSON.parse(res.body);
var layergroupId = parsedBody.layergroupid;
layergroupIdToDelete = layergroupId;
assert.response(server, {
url: '/database/windshaft_test/layergroup/' + layergroupId + '/0/13/4255/2765.json.torque',
method: 'GET'
}, {}, function (res, err) {
assert.ok(!err, 'Failed to request torque.json');
var parsed = JSON.parse(res.body);
assert.deepEqual(parsed, [
{
x__uint8: 47,
y__uint8: 127,
vals__uint8: [2],
dates__uint16: [0]
},
{
x__uint8: 48,
y__uint8: 127,
vals__uint8: [1],
dates__uint16: [0]
}
]);
done();
});
});
});
});

View File

@@ -0,0 +1,155 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('torque png renderer', function() {
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
var IMAGE_TOLERANCE_PER_MIL = 20;
var torquePngPointsMapConfig = {
version: '1.2.0',
layers: [
{
type: 'torque',
options: {
sql: "SELECT * FROM populated_places_simple_reduced where the_geom" +
" && ST_MakeEnvelope(-90, 0, 90, 65)",
cartocss: [
'Map {',
' buffer-size:0;',
' -torque-frame-count:1;',
' -torque-animation-duration:30;',
' -torque-time-attribute:"cartodb_id";',
' -torque-aggregation-function:"count(cartodb_id)";',
' -torque-resolution:1;',
' -torque-data-aggregation:linear;',
'}',
'#populated_places_simple_reduced{',
' comp-op: multiply;',
' marker-fill-opacity: 1;',
' marker-line-color: #FFF;',
' marker-line-width: 0;',
' marker-line-opacity: 1;',
' marker-type: rectangle;',
' marker-width: 3;',
' marker-fill: #FFCC00;',
'}'
].join(' '),
cartocss_version: '2.3.0'
}
}
]
};
var tileRequests = [
{
z: 2,
x: 2,
y: 1,
layer: 0,
format: 'torque.png'
},
{
z: 2,
x: 1,
y: 1,
layer: 0,
format: 'torque.png'
}
];
function torquePngFixture(zxy) {
return './test/fixtures/torque/populated_places_simple_reduced-' + zxy.join('.') + '.png';
}
tileRequests.forEach(function(tileRequest) {
var z = tileRequest.z,
x = tileRequest.x,
y = tileRequest.y;
var zxy = [z, x, y];
it('tile ' + zxy.join('/') + '.torque.png', function (done) {
testClient.getTileLayer(torquePngPointsMapConfig, tileRequest, function(err, res) {
assert.imageEqualsFile(res.body, torquePngFixture(zxy), IMAGE_TOLERANCE_PER_MIL, function(err) {
assert.ok(!err);
done();
});
});
});
});
var mapConfigTorqueOffset = {
version: '1.3.0',
layers: [
{
type: 'torque',
options: {
sql: "SELECT * FROM populated_places_simple_reduced",
cartocss: [
'Map {',
'buffer-size:0;',
'-torque-frame-count:1;',
'-torque-animation-duration:30;',
'-torque-time-attribute:"cartodb_id";',
'-torque-aggregation-function:"count(cartodb_id)";',
'-torque-resolution:2;',
'-torque-data-aggregation:linear;',
'}',
'',
'#populated_places_simple_reduced{',
' comp-op: lighter;',
' marker-fill-opacity: 0.9;',
' marker-line-color: #2167AB;',
' marker-line-width: 5;',
' marker-line-opacity: 1;',
' marker-type: ellipse;',
' marker-width: 6;',
' marker-fill: #FF9900;',
'}',
'#populated_places_simple_reduced[frame-offset=1] {',
' marker-width:8;',
' marker-fill-opacity:0.45; ',
'}',
'#populated_places_simple_reduced[frame-offset=2] {',
' marker-width:10;',
' marker-fill-opacity:0.225; ',
'}'
].join(' '),
cartocss_version: '2.3.0'
}
}
]
};
it('torque static map with offset', function(done) {
var w = 600,
h = 400;
testClient.getStaticBbox(mapConfigTorqueOffset, -170, -87, 170, 87, w, h, function(err, res, img) {
if (err) {
return done(err);
}
assert.equal(img.width(), w);
assert.equal(img.height(), h);
done();
});
});
});

View File

@@ -0,0 +1,97 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('torque tiles at 0,0 point', function() {
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
/*
Tiles are represented as in:
---------
| TL | TR |
|--(0,0)--|
| BL | BR |
---------
*/
var tiles = [
{
what: 'tl',
x: 3,
y: 3,
expects: []
},
{
what: 'tr',
x: 4,
y: 3,
expects: []
},
{
what: 'bl',
x: 3,
y: 4,
expects: [{"x__uint8":1,"y__uint8":1,"vals__uint8":[1],"dates__uint16":[0]}]
},
{
what: 'br',
x: 4,
y: 4,
expects: [{"x__uint8":0,"y__uint8":1,"vals__uint8":[1],"dates__uint16":[0]}]
}
];
tiles.forEach(function(tile) {
it(tile.what, function(done) {
var query = 'select 1 cartodb_id,' +
' ST_Transform(ST_SetSRID(ST_MakePoint(0, 0), 4326), 3857) the_geom_webmercator';
var mapConfig = {
version: '1.3.0',
layers: [
{
type: 'torque',
options: {
sql: query,
cartocss: [
'Map {',
' -torque-time-attribute: "cartodb_id";',
' -torque-aggregation-function: "count(cartodb_id)";',
' -torque-frame-count: 1;',
' -torque-animation-duration: 15;',
' -torque-resolution: 128',
'}',
'#layer {',
' marker-fill: #fff;',
' marker-fill-opacity: 0.4;',
' marker-width: 1;',
'}'
].join(' '),
cartocss_version: '2.3.0'
}
}
]
};
testClient.getTorque(mapConfig, 0, 3, tile.x, tile.y, function(err, res) {
assert.deepEqual(JSON.parse(res.body), tile.expects);
done();
});
});
});
});

View File

@@ -0,0 +1,131 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var testClient = require('./support/test_client');
var PortedServerOptions = require('./support/ported_server_options');
var BaseController = require('../../../lib/cartodb/controllers/base');
describe('wrap x coordinate', function() {
var req2paramsFn;
before(function() {
req2paramsFn = BaseController.prototype.req2params;
BaseController.prototype.req2params = PortedServerOptions.req2params;
});
after(function() {
BaseController.prototype.req2params = req2paramsFn;
});
describe('renders correct tile', function() {
var IMG_TOLERANCE_PER_MIL = 20;
function plainTorqueMapConfig(plainColor) {
return {
version: '1.2.0',
layers: [
{
type: 'plain',
options: {
color: plainColor
}
},
{
type: 'torque',
options: {
sql: "SELECT * FROM populated_places_simple_reduced " +
"where the_geom && ST_MakeEnvelope(-90, 0, 90, 65)",
cartocss: [
'Map {',
' buffer-size:0;',
' -torque-frame-count:1;',
' -torque-animation-duration:30;',
' -torque-time-attribute:"cartodb_id";',
' -torque-aggregation-function:"count(cartodb_id)";',
' -torque-resolution:1;',
' -torque-data-aggregation:linear;',
'}',
'#populated_places_simple_reduced{',
' comp-op: multiply;',
' marker-fill-opacity: 1;',
' marker-line-color: #FFF;',
' marker-line-width: 0;',
' marker-line-opacity: 1;',
' marker-type: rectangle;',
' marker-width: 3;',
' marker-fill: #FFCC00;',
'}'
].join(' '),
cartocss_version: '2.3.0'
}
}
]
};
}
var testScenarios = [
{
tile: {
z: 2,
x: -2,
y: 1,
layer: 'all',
format: 'png'
},
fixture: {
z: 2,
x: 2,
y: 1
},
plainColor: 'white'
},
{
tile: {
z: 2,
x: -3,
y: 1,
layer: 'all',
format: 'png'
},
fixture: {
z: 2,
x: 1,
y: 1
},
plainColor: '#339900'
}
];
function blendPngFixture(zxy) {
return './test/fixtures/blend/blend-plain-torque-' + zxy.join('.') + '.png';
}
testScenarios.forEach(function(testScenario) {
var tileRequest = testScenario.tile;
var zxy = [tileRequest.z, tileRequest.x, tileRequest.y];
var fixtureZxy = [testScenario.fixture.z, testScenario.fixture.x, testScenario.fixture.y];
it('tile all/' + zxy.join('/') + '.png', function (done) {
testClient.getTileLayer(plainTorqueMapConfig(testScenario.plainColor), tileRequest, function(err, res) {
assert.imageEqualsFile(res.body, blendPngFixture(fixtureZxy), IMG_TOLERANCE_PER_MIL, function(err) {
assert.ok(!err);
done();
});
});
});
});
});
describe('mapnik', function() {
it("can get a tile with negative x coordinate", function(done){
testClient.getTile(testClient.defaultTableMapConfig('test_table'), 2, -2, 1, function(err, res, img) {
assert.ok(!err);
assert.ok(img);
assert.equal(img.width(), 256);
assert.equal(img.height(), 256);
done();
});
});
});
});

View File

@@ -4,16 +4,16 @@ var assert = require('../support/assert');
var querystring = require('querystring');
var step = require('step');
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
var serverOptions = require('../../lib/cartodb/server_options')();
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
suite('server', function() {
describe('server', function() {
// TODO: I guess this should be a 404 instead...
test("get call to server returns 200", function(done){
it("get call to server returns 200", function(done){
step(
function doGet() {
var next = this;
@@ -23,7 +23,7 @@ suite('server', function() {
},{}, function(res, err) { next(err,res); });
},
function doCheck(err, res) {
if ( err ) throw err;
assert.ifError(err);
assert.ok(res.statusCode, 200);
var cc = res.headers['x-cache-channel'];
assert.ok(!cc);
@@ -35,7 +35,7 @@ suite('server', function() {
);
});
test("get call to server returns 200", function(done){
it("get call to server returns 200", function(done){
assert.response(server, {
url: '/version',
method: 'GET'
@@ -44,25 +44,19 @@ suite('server', function() {
}, function(res) {
var parsed = JSON.parse(res.body);
assert.ok(parsed.hasOwnProperty('windshaft_cartodb'), "No 'windshaft_cartodb' version in " + parsed);
console.log("Windshaft-cartodb: " + parsed.windshaft_cartodb);
assert.ok(parsed.hasOwnProperty('windshaft'), "No 'windshaft' version in " + parsed);
console.log("Windshaft: " + parsed.windshaft);
assert.ok(parsed.hasOwnProperty('grainstore'), "No 'grainstore' version in " + parsed);
console.log("Grainstore: " + parsed.grainstore);
assert.ok(parsed.hasOwnProperty('node_mapnik'), "No 'node_mapnik' version in " + parsed);
console.log("Node-mapnik: " + parsed.node_mapnik);
assert.ok(parsed.hasOwnProperty('mapnik'), "No 'mapnik' version in " + parsed);
console.log("Mapnik: " + parsed.mapnik);
// TODO: check actual versions ?
done();
});
});
});
suite('server old_api', function() {
describe('server old_api', function() {
// See https://github.com/CartoDB/Windshaft-cartodb/issues/115
test.skip("get'ing tile with not-strictly-valid style", function(done) {
it.skip("get'ing tile with not-strictly-valid style", function(done) {
var style = querystring.stringify({style: '#test_table{line-color:black}}', style_version: '2.0.0'});
assert.response(server, {
headers: {host: 'localhost'},

File diff suppressed because it is too large Load Diff

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