Compare commits

..

69 Commits

Author SHA1 Message Date
Sandro Santilli
a660fdee12 Release 1.5.2 2013-12-05 14:05:41 +01:00
Sandro Santilli
e3b0813230 Fix use of old layergroups on mapnik upgrade (#97) 2013-12-05 14:02:16 +01:00
Sandro Santilli
8347dd4257 Set grainstore's GC run probability, for documentation purpose
It sets it to the current grainstore default, so nothing changes.
2013-12-05 11:11:13 +01:00
Sandro Santilli
dac89b30cb Allow requesting run_test.sh to prepare redis but not postgresql
Adds --nocreate-pg, --nocreate-redis, --nodrop-pg, --nodrop-redis
NOTE that dropping pg is still unimplemented
2013-12-05 11:07:12 +01:00
Sandro Santilli
5951608d2e Add backward-compatibility fix item in NEWS (#96) 2013-11-29 13:33:53 +01:00
Sandro Santilli
13c39f598b Prepare for 1.5.2 2013-11-29 13:32:28 +01:00
Sandro Santilli
6ca3135a3c Revert "fixed #91" -- the fix was for an unconfirmed bug
This reverts commit 9155724082.
See #38 for further action
2013-11-29 13:25:10 +01:00
Sandro Santilli
f582b04b23 Enable test for fetcing tiles of private tables using api_key
See #39 and #91
2013-11-29 13:20:21 +01:00
Sandro Santilli
8a1539af7f Add test for fetching tile of private table showing api_key
See #38 and #91
2013-11-29 13:14:43 +01:00
javi
9155724082 fixed #91 2013-11-29 12:46:45 +01:00
javi
75b0ab5c87 fixed #96 2013-11-29 12:46:25 +01:00
Sandro Santilli
046c570f5a Release 1.5.1 2013-11-28 18:52:11 +01:00
Sandro Santilli
f10d7cd93f Accept unused CartoCSS directives
Closes #93

An example unused CartoCSS directive is
"point-transform" without "point-file"
or "point-url". Unused means it has no effect.

It used to be accepted but regressed in release 1.5.0
2013-11-28 18:47:45 +01:00
Sandro Santilli
364c6e4910 Fix test for invalid font usage after Windshaft update (#90)
NOTE: the error is less friendly now, see
      http://github.com/mapbox/carto/issues/242
2013-11-28 18:41:45 +01:00
Sandro Santilli
c67199b3eb Survive presence of malformed CartoCSS in redis
Closes #94, enable relative testcase
2013-11-28 18:31:37 +01:00
Sandro Santilli
87a9e0ca79 Add package keywords 2013-11-28 11:17:00 +01:00
Sandro Santilli
87e18da45b Set test redis port to 6335 2013-11-27 12:16:43 +01:00
Sandro Santilli
1712829808 Add (pending) test for getting unrenderable stored styles (#94)
Required upgrading mocha tester to ~0.14.0
2013-11-27 09:47:33 +01:00
Sandro Santilli
f1f9f5f233 Notify travis builds on #cartodb @ freenode.irc 2013-11-26 18:11:49 +01:00
Sandro Santilli
19512b776f Use a variable to hold the name of test database 2013-11-26 11:27:38 +01:00
Sandro Santilli
82956e0b7c Reduce ppa and explicit package usage
Should fix travis builds despite package compatibilit bugs
(https://travis-ci.org/CartoDB/Windshaft-cartodb/builds/14314805)
2013-11-21 18:17:58 +01:00
Sandro Santilli
0a30a6cd56 Add note about new directives in the 1.5.0 section 2013-11-21 16:00:47 +01:00
Sandro Santilli
a9151b856c Improve documentation for postgres_auth_* configuration directives 2013-11-21 15:55:20 +01:00
Sandro Santilli
d864441f74 Prepare for 1.5.1 2013-11-19 15:45:33 +01:00
Sandro Santilli
6b39864a1b Release 1.5.0 2013-11-19 15:43:11 +01:00
Sandro Santilli
dcc75dbf5f Drop unneeded include from outermost app 2013-11-18 10:07:28 +01:00
Sandro Santilli
4c16087ea8 Update to cartodb-redis 0.1.0 2013-11-15 19:14:00 +01:00
Sandro Santilli
d61c373fad CartoDB redis interaction delegated to "cartodb-redis" module 2013-11-15 15:49:04 +01:00
Sandro Santilli
2e4526ce8e Require windshaft-0.14.3 to get 3 new bugfixes:
- Return CORS headers when creating layergroups via GET
 - Fix http status on database authentication error
 - Ensure bogus text-face-name error raises at layergroup creation
2013-11-14 16:26:05 +01:00
Sandro Santilli
f1045078e1 Remember per-environment ./configure parameters
This is to avoid breaking test.js configuration while switching
between branches.
2013-11-14 16:21:40 +01:00
Sandro Santilli
ab3e76ce36 Fix ticket reference 2013-11-13 11:24:07 +01:00
Sandro Santilli
88daefe353 Add support for reading user-specific database_password from redis
This commits adds support for CartoDB-2.5.0 model.
Closes #89.
Change is backward compatible.
2013-11-12 23:20:50 +01:00
Sandro Santilli
bcb6807306 Avoid caches during test for user-specific database_host 2013-11-12 22:38:42 +01:00
Sandro Santilli
2897c0c8bf Do not force ending dot in SQL-API hostname, for easier testing 2013-11-12 17:41:10 +01:00
Sandro Santilli
eb9dfed731 Improve tests robustness on failure 2013-11-12 17:29:57 +01:00
Sandro Santilli
6567ea5090 Add NEWS item about CartoDB-2.5.0+ user-specific database_host (#88) 2013-11-11 17:49:57 +01:00
Sandro Santilli
b428a7be9a Add test for redis-specifid database_host. Closes #88 2013-11-11 17:49:05 +01:00
Sandro Santilli
759ebadfc8 Read user's database_host from redis, when available (#88)
Still lacks a testcase
2013-11-11 17:23:10 +01:00
Sandro Santilli
69f15a5086 Indent fixes 2013-11-11 16:55:51 +01:00
Sandro Santilli
4b12295862 Tweak error messages on missing redis variables, update tests 2013-11-11 15:56:58 +01:00
Luis Bosque
0508203ba3 Add function to read database host from redis 2013-11-11 15:47:13 +01:00
Sandro Santilli
4eb3368c99 Style only change 2013-11-11 15:45:34 +01:00
Sandro Santilli
357702d61b Add support for specifying database connection passwords 2013-11-11 00:50:03 +01:00
Sandro Santilli
4baff6e018 Release 1.14.1 2013-11-08 12:43:47 +01:00
Sandro Santilli
5b4184c2db Fix support for exponential notation in CartoCSS filter values
Closes #87.
Includes testcase
2013-11-08 12:34:34 +01:00
Sandro Santilli
895aa3b977 Prepare for 1.4.1 2013-10-31 16:02:40 +01:00
Sandro Santilli
d9d2adf5d8 Add Support for Mapnik-2.2.0. Closes #78. 2013-10-31 15:54:15 +01:00
Sandro Santilli
9b18ad2637 Prepare for mapnik-2.2.0 support (#78)
- Tolerate change in CartoCSS error message between 0.9.3 and 0.9.5
- Expect default style to be different for mapnik-2.2.0+ target
2013-10-29 20:45:15 +01:00
Sandro Santilli
78ba279eb8 Prepare for 1.3.7 2013-10-11 10:06:38 +02:00
Sandro Santilli
cef82aedd8 Release 1.3.6, fixing support for node-0.8.9 2013-10-11 10:05:31 +02:00
Sandro Santilli
5ae1513eea Prepare for 1.3.6 2013-10-03 17:15:11 +02:00
Sandro Santilli
dc52bb8751 Release 1.3.5 2013-10-03 17:10:37 +02:00
Sandro Santilli
47f42e4031 Fix support for apostrophes in CartoCSS
Requires windshaft 0.13.7
Jira ref CDB-414
2013-10-03 17:03:13 +02:00
Sandro Santilli
66a77cd255 Do not let anonymous requests use authorized renderer caches
Puts dbuser in params, for correct use by Windshaft renderer cache.
Before this fix, and after commit 1c9f63c9, the renderer cache key
did not contain the db user.
2013-09-23 12:02:43 +02:00
Sandro Santilli
0301cef1bb tweak test description 2013-09-23 10:57:22 +02:00
Sandro Santilli
72b3f23f72 Add more profile slots 2013-09-19 14:34:03 +01:00
Sandro Santilli
8685ef640e Remove spaces from configuration input, to make editing easier :) 2013-09-18 14:37:22 +02:00
Sandro Santilli
2d36521f92 Make testsuite accept an installed mapnik version 2.1.0
See https://travis-ci.org/CartoDB/Windshaft-cartodb/builds/11286823
2013-09-12 18:37:25 +02:00
Sandro Santilli
ca4644f4ce Add travis widget, fix documented node dependency 2013-09-12 18:09:18 +02:00
Sandro Santilli
58a462ab95 Add travis configuration 2013-09-12 18:04:38 +02:00
Sandro Santilli
c2d4aace56 Read test redis port configuration from test.js env 2013-09-12 17:55:16 +02:00
Sandro Santilli
4d524e5969 Clean handling of redis connection failures in testcase 2013-09-12 17:48:35 +02:00
Sandro Santilli
8c74a39262 Fix error for invalid text-name in CartoCSS. Closes #81. 2013-09-12 17:32:10 +02:00
Sandro Santilli
743b5388a3 Add backward compatibility sqlapi configuration item in NEWS 2013-09-12 16:20:28 +02:00
Sandro Santilli
4144ad2c7a Only use sqlapi configuration "host" if "domain" is undefined
We'll consider an empty string domain as valid (it's actually used
for testsuite).
2013-09-12 16:19:01 +02:00
Javier Arce
fb4ef5f768 Sets the sqlapi domain. Fixes #82 2013-09-12 15:36:50 +02:00
Sandro Santilli
cbb85e5dd8 Read redis port from test.js environment when running tests 2013-09-12 10:17:02 +02:00
Sandro Santilli
56bfed5a0e Fix use of blank-prefixed "zoom" variable in CartoCSS 2013-09-09 11:58:51 +02:00
Luis Bosque
ff9af5f923 Target v1.3.5 2013-09-06 12:13:09 +02:00
65 changed files with 805 additions and 11165 deletions

2
.gitignore vendored
View File

@@ -5,5 +5,3 @@ config/environments/*.js
tools/munin/windshaft.conf
logs/
pids/
redis.pid
test.log

View File

@@ -1,18 +1,16 @@
addons:
postgresql: "9.3"
before_install:
- sudo apt-get install -y pkg-config libcairo2-dev libjpeg8-dev libgif-dev
- sudo apt-add-repository --yes ppa:mapnik/v2.1.0
- sudo apt-get update -q
- sudo apt-get install -q libmapnik-dev
- createdb template_postgis
- psql -c "CREATE EXTENSION postgis" template_postgis
env:
- NPROCS=1 JOBS=1 PGUSER=postgres
- NPROCS=1 JOBS=1
language: node_js
node_js:
- "0.8"
- "0.10"
notifications:
irc:

View File

@@ -1,20 +1,11 @@
1. Test (make clean all check), fix if broken before proceeding
2. Ensure proper version in package.json
3. Ensure NEWS section exists for the new version, review it, add release date
4. Drop npm-shrinkwrap.json
5. Run npm shrinkwrap to recreate npm-shrinkwrap.json
6. Commit package.json, npm-shrinwrap.json, NEWS
7. git tag -a Major.Minor.Patch # use NEWS section as content
8. Announce on cartodb@googlegroups.com
9. Stub NEWS/package for next version
1. Ensure proper version in package.json
2. Ensure NEWS section exists for the new version, review it, add release date
3. Drop npm-shrinkwrap.json
4. Run npm install
5. Test (make check or npm test), fix if broken before proceeding
6. Run npm shrinkwrap
7. Commit package.json, npm-shrinwrap.json, NEWS
8. Tag Major.Minor.Patch
9. Announce
10. Stub NEWS/package for next version
Versions:
Bugfix releases increment Patch component of version.
Feature releases increment Minor and set Patch to zero.
If backward compatibility is broken, increment Major and
set to zero Minor and Patch.
Branches named 'b<Major>.<Minor>' are kept for any critical
fix that might need to be shipped before next feature release
is ready.

27
LICENCE Normal file
View File

@@ -0,0 +1,27 @@
Copyright (c) 2011, Vizzuality
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software
must display the following acknowledgement:
This product includes software developed by Vizzuality.
4. Neither the name of Vizzuality nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

27
LICENSE
View File

@@ -1,27 +0,0 @@
Copyright (c) 2014, Vizzuality
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -17,8 +17,10 @@ config/environments/test.js: config.status--test
check-local: config/environments/test.js
./run_tests.sh ${RUNTESTFLAGS} \
test/unit/cartodb/*.js \
test/acceptance/*.js
test/unit/cartodb/req2params.test.js \
test/acceptance/cache_validator.js \
test/acceptance/server.js \
test/acceptance/multilayer.js
check-submodules:
PATH="$$PATH:$(srcdir)/node_modules/.bin/"; \

515
NEWS.md
View File

@@ -1,518 +1,3 @@
1.26.2 -- 2015-01-28
--------------------
Bugfixes:
- Accept 'open' string in templates' `auth` as authorized.
1.26.1 -- 2015-01-28
--------------------
Announcements:
- Upgrades windshaft to 0.35.1, see https://github.com/CartoDB/Windshaft/pull/254
1.26.0 -- 2015-01-27
--------------------
Announcements:
- Upgrades windshaft to 0.35.0, supports mapconfig version `1.3.0`
1.25.0 -- 2015-01-26
--------------------
Announcements:
- No more signed maps (#227 and #238)
- Splits template maps endpoint into its own controller
- Removes TemplateMaps dependency on SignedMaps
- Token validation is done against the template
- Template is always extended with default values for auth and placeholders
- MapConfig is extended, in order to validate auth_tokens, with template info:
- template name
- template auth
- No more locks to create, update or delete templates
- Trusting in redis' hash semantics
- Some tradeoffs:
* A client having more templates than allowed by a race condition between limit (HLEN) check and creation (HSET)
* Updating a template could happen while deleting it, resulting in a new template
* Templates already instantiated will be accessible through their layergroup so it is possible to continue requesting tiles/grids/etc.
- Authorisation is now handled by template maps
- Template instantiation returns new instances with default values if they are missing
New features:
- Basic layergroup validation on named map creation/update (#196)
- Add named maps surrogate keys and call invalidation on template modification/deletion (#247)
- Extends TemplateMaps backend with EventEmitter
- Emits for create, update and delete templates
- VarnishHttpCacheBackend will invalidate a varnish instance via HTTP PURGE method
- In the future there could be more backends, for instance to invalidate a CDN.
- NamedMapsEntry has the responsibility to generate a cache key for a named map
- It probably should receive a template/named map instead of owner and template name
- SurrogateKeysCache is responsible to tag responses with a header
- It also is responsible for invalidations given an Invalidation Backend
- In the future it could have several backends so it can invalidates different caches
- SurrogateKeysCache is subscribed to TemplateMaps events to do the invalidations
1.24.0 -- 2015-01-15
--------------------
Announcements:
- Upgrades windshaft to 0.34.0 for retina support
1.23.1 -- 2015-01-14
--------------------
Announcements:
- Regenerate npm-shrinkwrap.json
1.23.0 -- 2015-01-14
--------------------
Announcements:
- Upgrades windshaft to 0.33.0
New features:
- Sets HTTP renderer configuration in server_options
1.22.0 -- 2015-01-13
--------------------
New features:
- Health check endpoint
1.21.2 -- 2014-12-15
--------------------
Announcements:
- Upgrades windshaft to 0.32.4
1.21.1 -- 2014-12-11
--------------------
Announcements:
- Upgrades windshaft to 0.32.2
Bugfixes:
- Closes fd for log files on `kill -HUP` (#230)
1.21.0 -- 2014-10-24
--------------------
New features:
- Allow a different cache-control max-age for layergroup responses
1.20.2 -- 2014-10-20
--------------------
Announcements:
- Upgrades windshaft to 0.31.0
1.20.1 -- 2014-10-17
--------------------
Announcements:
- Upgrades redis-mpool to 0.3.0
1.20.0 -- 2014-10-15
--------------------
New features:
- Report to statsd the status of redis pools
- Upgrades Windshaft to start reporting redis/renderers/mapnik pool metrics
Enhancements:
- Share one redis-mpool across the application
1.19.0 -- 2014-10-14
--------------------
Announcements:
- Dropping support for npm <1.2.1
npm-shrinkwrap.json is incompatible when generated with npm >=1.2.1 and consumed by npm <1.2.1
- Upgrades windshaft to 0.28.2
- Generates npm-shrinkwrap.json with npm >1.2.0
1.18.2 -- 2014-10-13
--------------------
Bug fixes:
- Defaults resultSet to object if undefined in QueryTablesApi
Announcements:
- Upgrades windshaft to 0.28.1
1.18.1 -- 2014-10-13
--------------------
New features:
- Allow to add more node.js' threadpool workers via process.env.UV_THREADPOOL_SIZE
1.18.0 -- 2014-10-03
--------------------
Announcements:
- Comes back to use mapnik 2.3.x based on cartodb/node-mapnik@1.4.15-cdb from windshaft@0.28.0
1.17.2 -- 2014-10-01
--------------------
Announcements:
- Upgrades windshaft to 0.27.2 which downgrades node-mapnik to 0.7.26-cdb1
1.17.1 -- 2014-09-30
--------------------
Announcements:
- Upgrades windshaft to 0.27.1 which downgrades node-mapnik to 1.4.10
Enhancements:
- TTL for template locks so they are not kept forever
- Upgrades mocha
1.17.0 -- 2014-09-25
--------------------
New features:
- Starts using mapnik 2.3.x
Enhancements:
- Upgrades windshaft and cartodb-redis
- Supports `!scale_denominator!` dynamic param in SQL queries
- Metrics revamp: removes and adds some metrics
- Adds poolSize configuration for mapnik
1.16.1 -- 2014-08-19
--------------------
Enhancements:
- Upgrades cartodb-redis
1.16.0 -- 2014-08-18
--------------------
New features:
- Configurable QueryTablesAPI to call directly postgresql using cartodb-psql
or to keep using a request to the SQL API
Enhancements:
- Removes mapnik dependency as it now relies on Windshaft to check mapnik version
- Upgrades dependencies:
- underscore
- lzma
- log4js
- rollbar
- windshaft
- request
1.15.0 -- 2014-08-13
--------------------
Enhancements:
- Upgrades dependencies:
- redis-mpool
- cartodb-redis
- windshaft
- Specifies name in the redis pool
- Slow pool configuration in example configurations
1.14.0 -- 2014-08-07
--------------------
Enhancements:
- SQL API requests moved to its own entity
New features:
- Affected tables and last updated time for a query are performed in a single
request to the SQL API
- Allow specifying the tile format, upgrades windshaft and grainstore
dependencies for this matter
1.13.1 -- 2014-08-04
--------------------
Enhancements:
- Profiler header sent as JSON string
1.13.0 -- 2014-07-30
--------------------
New features:
- Support for postgresql schemas
- Use public user from redis
- Support for several auth tokens
1.12.1 -- 2014-06-24
--------------------
Enhancements:
- Caches layergroup and sets X-Cache-Channel in GET requests also in named maps
1.12.0 -- 2014-06-24
--------------------
New features:
- Caches layergroup and sets X-Cache-Channel in GET requests
1.11.1 -- 2014-05-07
--------------------
Enhancements:
- Upgrade Windshaft to 0.21.0, see
http://github.com/CartoDB/Windshaft/blob/0.21.0/NEWS
1.11.0 -- 2014-04-28
--------------------
New features:
- Add support for log_filename directive
- Reopen log file on SIGHUP, for better logrotate integration
Enhancements:
- Set default PostgreSQL application name to "cartodb_tiler"
1.10.2 -- 2014-04-08
--------------------
Bug fixes:
- Fix show_style tool broken since 1.8.1
- Fix X-Cache-Channel of tiles accessed via signed token (#188)
1.10.1 -- 2014-03-21
--------------------
Bug fixes:
- Do not cache non-success jsonp responses (#186)
1.10.0 -- 2014-03-20
--------------------
New features:
- Add optional support for rollbar (#150)
Enhancements:
- Do not send connection details to client (#183)
- Upgrade node-varnish to 0.3.0
- Upgrade Windshaft to 0.20.0, see
http://github.com/CartoDB/Windshaft/blob/0.20.0/NEWS
- Include tiler version in startup log
- Install an uncaught exception handler
- Require own fork of node-mapnik, with temptative fix
for libxml usage (glibc detected corruptions)
Other changes:
- Switch to 3-clause BSD license (#184)
1.9.0 -- 2014-03-10
-------------------
New features:
- Allow to set server related configuration in serverMetadata (#182)
1.8.5 -- 2014-03-10
-------------------
Enhancements:
- Set statsd prefix for all endpoints
- Respond with a permission denied on attempt to access map tiles waiving
signature of someone who had not left any (#170)
- Do not log an error on GET / (#177)
- Do not UNWATCH on every redis client release (#161)
- Include API docs (#164)
- Add "cacheDns" statsd setting in the example configs
- Do not send duplicated stats on template instanciation
- Do not die on dns resolution errors (#178, #180)
Bug fixes:
- Do not cache map creation responses (#176)
1.8.4 -- 2014-03-03
-------------------
Enhancements:
- Really skip CDB_TableMetadata lookup for sql affected by no tables (#169)
- Upgrade windshaft to 0.19.2, see node_modules/windshaft/NEWS
- Clarify obscure "ECONNREFUSED" error message (#171)
- Change some http status responses to be more appropriate to the case
- Forbid using map signatures of foreign users (#172)
- Forbid instanciating templates of foreign users (#173)
- Allow passing environment configuration name via NODE_ENV to app.js
- Print environment configuration name on app start
Bug fixes:
- Fix database connection settings on template instanciation (#174)
1.8.3 -- 2014-02-27
-------------------
Enhancements:
- Upgrades windshaft to 0.19.1 with many performance improvements,
See node_modules/windshaft/NEWS
- Improve speed of instanciating a map (#147, #159, #165)
- Give meaningful error on attempts to use map tokens
with attribute service (#156)
- Reduce sql-api communication timeout, and allow overriding (#167)
[ new sqlapi.timeout directive, defaults to 100 ms ]
- Do not query CDB_TableMetadata for queries affected by no tables (#168)
1.8.2 -- 2014-02-25
-------------------
Enhancements:
* Allow using ":host" as part of statsd.prefix (#153)
* Expand "addCacheChannel" stats
* Allow using GET with sql-api for queries shorter than configured len (#155)
[ new sqlapi.max_get_sql_length directive, defaults to 2048 ]
* Do not log an error for a legit request requiring no X-Cache-Channel
Bug fixes:
* Fix munin plugin after log format changes (#154)
1.8.1 -- 2014-02-19
-------------------
Enhancements:
* Use log4js logger (#138)
Bug fixes:
* Always generate X-Cache-Channel for token-based tile responses (#152)
1.8.0 -- 2014-02-18
-------------------
Enhancements:
* Add script to flush caches (#140)
* Add script to list templates
* Add statsd support (#139)
* Add support for specifying a varnish password
* Avoid sending multiple varnish invalidation at once (#135)
* Tested with node-0.10 (#141)
* Use single redis pooler for torque and grainstore
* Reduce cost of garbage collection for localized resources
* Allow limiting number of templates for each user (#136)
* Allow configuring TTL of mapConfigs via "mapConfigTTL"
1.7.1 -- 2014-02-11
-------------------
Enhancements:
* Disable debug logging unless "debug" config param evaluates to true (#137)
* Require windshaft 0.17.2 for further reducing log noise (#137)
1.7.0 -- 2014-02-11
-------------------
New features:
* Add support for torque tiles (#112)
* Add attributes service (#118)
* Implement Unified Map API (#126)
* Make endpoints configurable (#127)
Enhancements:
* Allow specifying fixed sqlapi host address (#117)
* Include template hash in template instance response, to keep caches
of different instances separated (#105)
Bug fixes:
* Allow space padding in template variables usage (#129)
* Allow passing numbers as values for numeric template variables (#130)
1.6.3 -- 2014-01-30
-------------------
Bug fixes:
* layergroup accept both map_key and api_key (#91)
* Fix public instanciation of signed template accessing private data (#114)
* Fix show_style in presence of complex styles
* Fix use of maxzoom in layergroup config (via windshaft-0.15.1)
Enhancements:
* Add support for instanciating a template map with JSONP (#116)
* Stop processing XML on renderer creation, not needed anymore since 1.6.1
introduced on-demand XML generation.
1.6.2 -- 2014-01-23
-------------------
Bug fixes:
* Fix support for long (>64k chars) queries in layergroup creation (#111)
Enhancements:
* Enhance tools/show_style to accept an environment parameter and
print XML style now it is not in redis anymore (#110)
* Support CORS in template instanciation endpoint (#113)
1.6.1 -- 2014-01-15
-------------------
Bug fixes:
* Drop cache headers from error responses (#107)
* Localize external CartoCSS resources at renderer creation time (#108)
1.6.0 -- 2014-01-10
-------------------
New features:
* Add 'user_from_host' directive to generalize username extraction (#100)
* Implement signed template maps (#98)
Other changes:
* Update cartodb-redis dependency to "~0.3.0"
* Update redis-server dependency to "2.4.0+"
1.5.2 -- 2013-12-05
-------------------

View File

@@ -1,8 +1,7 @@
Windshaft-CartoDB
==================
[![Build Status](http://travis-ci.org/CartoDB/Windshaft-cartodb.png)]
(http://travis-ci.org/CartoDB/Windshaft-cartodb)
[![Build Status](https://travis-ci.org/CartoDB/Windshaft-cartodb.png)](http://travis-ci.org/CartoDB/Windshaft-cartodb)
This is the CartoDB map tiler. It extends Windshaft with some extra
functionality and custom filters for authentication
@@ -12,10 +11,8 @@ functionality and custom filters for authentication
* 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 signed template maps API
(http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps)
* provides a infowindow endpoint for windshaft
* provides a ``map_metadata`` endpoint for windshaft
Requirements
------------
@@ -24,16 +21,13 @@ Requirements
- node-0.8.x+
- PostgreSQL-8.3+
- PostGIS-1.5.0+
- Redis 2.4.0+ (http://www.redis.io)
- Redis 2.2.0+ (http://www.redis.io)
- Mapnik 2.0 or 2.1
[for cache control]
- CartoDB-SQL-API 1.0.0+
- CartoDB 0.9.5+ (for ``CDB_QueryTables``)
- Varnish (http://www.varnish-cache.org)
[for running the testsuite]
- Imagemagick (http://www.imagemagick.org)
- Varnish (https://www.varnish-cache.org)
Configure
---------

95
app.js
View File

@@ -7,88 +7,29 @@
* environments: [development, production]
*/
var path = require('path'),
fs = require('fs'),
RedisPool = require('redis-mpool')
;
if ( process.argv[2] ) ENV = process.argv[2];
else if ( process.env['NODE_ENV'] ) ENV = process.env['NODE_ENV'];
else ENV = 'development';
process.env['NODE_ENV'] = ENV;
// sanity check
var ENV = process.argv[2]
if (ENV != 'development' && ENV != 'production' && ENV != 'staging' ){
console.error("\nnode app.js [environment]");
console.error("environments: development, production, staging\n");
console.error("environments: [development, production, staging]\n");
process.exit(1);
}
var _ = require('underscore');
var _ = require('underscore')
, Step = require('step')
, CartodbWindshaft = require('./lib/cartodb/cartodb_windshaft');
// set environment specific variables
global.settings = require(__dirname + '/config/settings');
global.environment = require(__dirname + '/config/environments/' + ENV);
global.environment.api_hostname = require('os').hostname().split('.')[0];
_.extend(global.settings, global.environment);
global.log4js = require('log4js');
log4js_config = {
appenders: [],
replaceConsole:true
};
if (global.environment.uv_threadpool_size) {
process.env.UV_THREADPOOL_SIZE = global.environment.uv_threadpool_size;
}
if ( global.environment.log_filename ) {
var logdir = path.dirname(global.environment.log_filename);
// See cwd inlog4js.configure call below
logdir = path.resolve(__dirname, logdir);
if ( ! fs.existsSync(logdir) ) {
console.error("Log filename directory does not exist: " + logdir);
process.exit(1);
}
console.log("Logs will be written to " + global.environment.log_filename);
log4js_config.appenders.push(
{ type: "file", filename: global.environment.log_filename }
);
} else {
log4js_config.appenders.push(
{ type: "console", layout: { type:'basic' } }
);
}
if ( global.environment.rollbar ) {
log4js_config.appenders.push({
type: __dirname + "/lib/cartodb/log4js_rollbar.js",
options: global.environment.rollbar
});
}
log4js.configure(log4js_config, { cwd: __dirname });
global.logger = log4js.getLogger();
var redisOpts = _.extend(global.environment.redis, { name: 'windshaft' }),
redisPool = new RedisPool(redisOpts);
// 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 Windshaft = require('windshaft');
var serverOptions = require('./lib/cartodb/server_options');
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);
});
}
// 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
@@ -97,12 +38,8 @@ ws.maxConnections = global.environment.maxConnections || 128;
ws.listen(global.environment.port, global.environment.host);
var version = require("./package").version;
ws.on('listening', function() {
console.log("Windshaft tileserver " + version + " started on "
+ global.environment.host + ':' + global.environment.port
+ " (" + ENV + ")");
console.log("Windshaft tileserver started on " + global.environment.host + ':' + global.environment.port);
});
// DEPRECATED, use SIGUSR2
@@ -114,15 +51,3 @@ process.on('SIGUSR1', function() {
process.on('SIGUSR2', function() {
ws.dumpCacheStats();
});
process.on('SIGHUP', function() {
global.log4js.clearAndShutdownAppenders(function() {
global.log4js.configure(log4js_config);
global.logger = log4js.getLogger();
console.log('Log files reloaded');
});
});
process.on('uncaughtException', function(err) {
logger.error('Uncaught exception: ' + err.stack);
});

View File

@@ -2,45 +2,14 @@ var config = {
environment: 'development'
,port: 8181
,host: '127.0.0.1'
,uv_threadpool_size: undefined
// Regular expression pattern to extract username
// from hostname. Must have a single grabbing block.
,user_from_host: '^(.*)\\.localhost'
// Base URLs for the APIs
//
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
//
// Base url for the Templated Maps API
// "/api/v1/map/named" is the new API,
// "/tiles/template" is for compatibility with versions up to 1.6.x
,base_url_templated: '(?:/api/v1/map/named|/tiles/template)'
// Base url for the Detached Maps API
// "maps" is the the new API,
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
,base_url_detached: '(?:/api/v1/map|/tiles/layergroup)'
// Base url for the Inline Maps and Table Maps API
,base_url_legacy: '/tiles/:table'
// Maximum number of connections for one process
// 128 is a good value with a limit of 1024 open file descriptors
,maxConnections:128
// Maximum number of templates per user. Unlimited by default.
,maxUserTemplates:1024
// Seconds since "last creation" before a detached
// or template instance map expires. Or: how long do you want
// to be able to navigate the map without a reload ?
// Defaults to 7200 (2 hours)
,mapConfigTTL: 7200
// idle socket timeout, in milliseconds
// idle socket timeout, in miliseconds
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: false
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
// If log_filename is given logs will be written
// there, in append mode. Otherwise stdout is used (default).
// Log file will be re-opened on receiving the HUP signal
,log_filename: undefined
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
// Templated database username for authorized user
// Supported labels: 'user_id' (read from redis)
,postgres_auth_user: 'development_cartodb_user_<%= user_id %>'
@@ -63,39 +32,14 @@ var config = {
*/
row_limit: 65535,
simplify_geometries: true,
use_overviews: true, // use overviews to retrieve raster
/*
* Set persist_connection to false if you want
* database connections to be closed on renderer
* expiration (1 minute after last use).
* Setting to true (the default) would never
* close any connection for the server's lifetime
*/
persist_connection: false,
max_size: 500
}
,mapnik_version: undefined
,mapnik_tile_format: 'png8:m=h'
,statsd: {
host: 'localhost',
port: 8125,
prefix: 'dev.',
cacheDns: true
// support all allowed node-statsd options
}
,renderer: {
// Milliseconds since last access before renderer cache item expires
cache_ttl: 60000,
metatile: 4,
bufferSize: 64,
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
http: {
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
'http://{s}.example.com/{z}/{x}/{y}.png'
]
}
bufferSize: 64
}
,millstone: {
// Needs to be writable by server user
@@ -107,67 +51,29 @@ var config = {
// Max number of connections in each pool.
// Users will be put on a queue when the limit is hit.
// Set to maxConnection to have no possible queues.
// There are currently 2 pools involved in serving
// There are currently 3 pools involved in serving
// windshaft-cartodb requests so multiply this number
// by 2 to know how many possible connections will be
// by 3 to know how many possible connections will be
// kept open by the server. The default is 50.
max: 50,
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
idleTimeoutMillis: 1, // idle time before dropping connection
reapIntervalMillis: 1, // time between cleanups
slowQueries: {
log: true,
elapsedThreshold: 200
},
slowPool: {
log: true, // whether a slow acquire must be logged or not
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
},
emitter: {
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
}
reapIntervalMillis: 1 // time between cleanups
}
,sqlapi: {
protocol: 'http',
// If "host" is given, it will be used
// to connect to the SQL-API without a
// DNS lookup
host: '127.0.0.1',
port: 8080,
// The "domain" part will be appended to
// the cartodb username and passed to
// SQL-API requests in the Host HTTP header
domain: 'localhost.lan',
version: 'v1',
// Maximum lenght of SQL query for GET
// requests. Longer queries will be sent
// using POST. Defaults to 2048
max_get_sql_length: 2048,
// Maximum time to wait for a response,
// in milliseconds. Defaults to 100.
timeout: 100
port: 8080,
version: 'v1'
}
,varnish: {
host: 'localhost',
port: 6082, // the por for the telnet interface where varnish is listening to
http_port: 6081, // the port for the HTTP interface where varnish is listening to
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
secret: 'xxx',
ttl: 86400,
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
port: 6082,
ttl: 86400
}
// 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.
,useProfiler:true
// Settings for the health check available at /health
,health: {
enabled: false,
username: 'localhost',
z: 0,
x: 0,
y: 0
}
};
module.exports = config;

View File

@@ -2,45 +2,14 @@ var config = {
environment: 'production'
,port: 8181
,host: '127.0.0.1'
,uv_threadpool_size: undefined
// Regular expression pattern to extract username
// from hostname. Must have a single grabbing block.
,user_from_host: '^(.*)\\.cartodb\\.com$'
// Base URLs for the APIs
//
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
//
// Base url for the Templated Maps API
// "/api/v1/map/named" is the new API,
// "/tiles/template" is for compatibility with versions up to 1.6.x
,base_url_templated: '(?:/api/v1/map/named|/tiles/template)'
// Base url for the Detached Maps API
// "maps" is the the new API,
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
,base_url_detached: '(?:/api/v1/map|/tiles/layergroup)'
// Base url for the Inline Maps and Table Maps API
,base_url_legacy: '/tiles/:table'
// Maximum number of connections for one process
// 128 is a good value with a limit of 1024 open file descriptors
,maxConnections:128
// Maximum number of templates per user. Unlimited by default.
,maxUserTemplates:1024
// Seconds since "last creation" before a detached
// or template instance map expires. Or: how long do you want
// to be able to navigate the map without a reload ?
// Defaults to 7200 (2 hours)
,mapConfigTTL: 7200
// idle socket timeout, in milliseconds
// idle socket timeout, in miliseconds
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: true
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
// If log_filename is given logs will be written
// there, in append mode. Otherwise stdout is used (default).
// Log file will be re-opened on receiving the HUP signal
,log_filename: 'logs/node-windshaft.log'
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
// Templated database username for authorized user
// Supported labels: 'user_id' (read from redis)
,postgres_auth_user: 'cartodb_user_<%= user_id %>'
@@ -56,40 +25,15 @@ var config = {
port: 6432,
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
row_limit: 65535,
/*
* Set persist_connection to false if you want
* database connections to be closed on renderer
* expiration (1 minute after last use).
* Setting to true (the default) would never
* close any connection for the server's lifetime
*/
persist_connection: false,
simplify_geometries: true,
use_overviews: true, // use overviews to retrieve raster
max_size: 500
}
,mapnik_version: undefined
,mapnik_tile_format: 'png8:m=h'
,statsd: {
host: 'localhost',
port: 8125,
prefix: ':host.', // could be hostname, better not containing dots
cacheDns: true
// support all allowed node-statsd options
}
,renderer: {
// Milliseconds since last access before renderer cache item expires
cache_ttl: 60000,
metatile: 4,
bufferSize: 64,
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
http: {
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
'http://{s}.example.com/{z}/{x}/{y}.png'
]
}
bufferSize: 64
}
,millstone: {
// Needs to be writable by server user
@@ -101,82 +45,29 @@ var config = {
// Max number of connections in each pool.
// Users will be put on a queue when the limit is hit.
// Set to maxConnection to have no possible queues.
// There are currently 2 pools involved in serving
// There are currently 3 pools involved in serving
// windshaft-cartodb requests so multiply this number
// by 2 to know how many possible connections will be
// by 3 to know how many possible connections will be
// kept open by the server. The default is 50.
max: 50,
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
idleTimeoutMillis: 30000, // idle time before dropping connection
reapIntervalMillis: 1000, // time between cleanups
slowQueries: {
log: true,
elapsedThreshold: 200
},
slowPool: {
log: true, // whether a slow acquire must be logged or not
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
},
emitter: {
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
}
reapIntervalMillis: 1000 // time between cleanups
}
,sqlapi: {
protocol: 'https',
// If "host" is given, it will be used
// to connect to the SQL-API without a
// DNS lookup
//host: '127.0.0.1',
port: 8080,
// The "domain" part will be appended to
// the cartodb username and passed to
// SQL-API requests in the Host HTTP header
domain: 'cartodb.com',
version: 'v2',
// Maximum lenght of SQL query for GET
// requests. Longer queries will be sent
// using POST. Defaults to 2048
max_get_sql_length: 2048,
// Maximum time to wait for a response,
// in milliseconds. Defaults to 100.
timeout: 100
port: 8080,
version: 'v2'
}
,varnish: {
host: 'localhost',
port: 6082, // the por for the telnet interface where varnish is listening to
http_port: 6081, // the port for the HTTP interface where varnish is listening to
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
secret: 'xxx',
ttl: 86400,
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
port: 6082,
ttl: 86400
}
// 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.
,useProfiler:false
,serverMetadata: {
cdn_url: {
http: 'api.cartocdn.com',
https: 'cartocdn.global.ssl.fastly.net'
}
}
// Optional rollbar support
,rollbar: {
token: 'secret',
// See http://github.com/rollbar/node_rollbar#configuration-reference
options: {
endpoint: 'https://api.rollbar.com/api/1/',
handler: 'inline'
}
}
// Settings for the health check available at /health
,health: {
enabled: true,
username: 'localhost',
z: 0,
x: 0,
y: 0
}
};
module.exports = config;

View File

@@ -2,45 +2,14 @@ var config = {
environment: 'production'
,port: 8181
,host: '127.0.0.1'
,uv_threadpool_size: undefined
// Regular expression pattern to extract username
// from hostname. Must have a single grabbing block.
,user_from_host: '^(.*)\\.cartodb\\.com$'
// Base URLs for the APIs
//
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
//
// Base url for the Templated Maps API
// "/api/v1/maps/named" is the new API,
// "/tiles/template" is for compatibility with versions up to 1.6.x
,base_url_templated: '(?:/api/v1/maps/named|/tiles/template)'
// Base url for the Detached Maps API
// "/api/v1/maps" is the the new API,
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
,base_url_detached: '(?:/api/v1/maps|/tiles/layergroup)'
// Base url for the Inline Maps and Table Maps API
,base_url_legacy: '/tiles/:table'
// Maximum number of connections for one process
// 128 is a good value with a limit of 1024 open file descriptors
,maxConnections:128
// Maximum number of templates per user. Unlimited by default.
,maxUserTemplates:1024
// Seconds since "last creation" before a detached
// or template instance map expires. Or: how long do you want
// to be able to navigate the map without a reload ?
// Defaults to 7200 (2 hours)
,mapConfigTTL: 7200
// idle socket timeout, in milliseconds
// idle socket timeout, in miliseconds
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: true
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type]'
// If log_filename is given logs will be written
// there, in append mode. Otherwise stdout is used (default).
// Log file will be re-opened on receiving the HUP signal
,log_filename: 'logs/node-windshaft.log'
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type]'
// Templated database username for authorized user
// Supported labels: 'user_id' (read from redis)
,postgres_auth_user: 'cartodb_staging_user_<%= user_id %>'
@@ -57,39 +26,14 @@ var config = {
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
row_limit: 65535,
simplify_geometries: true,
use_overviews: true, // use overviews to retrieve raster
/*
* Set persist_connection to false if you want
* database connections to be closed on renderer
* expiration (1 minute after last use).
* Setting to true (the default) would never
* close any connection for the server's lifetime
*/
persist_connection: false,
max_size: 500
}
,mapnik_version: undefined
,mapnik_tile_format: 'png8:m=h'
,statsd: {
host: 'localhost',
port: 8125,
prefix: 'stage.:host.',
cacheDns: true
// support all allowed node-statsd options
}
,renderer: {
// Milliseconds since last access before renderer cache item expires
cache_ttl: 60000,
metatile: 4,
bufferSize: 64,
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
http: {
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
'http://{s}.example.com/{z}/{x}/{y}.png'
]
}
bufferSize: 64
}
,millstone: {
// Needs to be writable by server user
@@ -101,82 +45,29 @@ var config = {
// Max number of connections in each pool.
// Users will be put on a queue when the limit is hit.
// Set to maxConnection to have no possible queues.
// There are currently 2 pools involved in serving
// There are currently 3 pools involved in serving
// windshaft-cartodb requests so multiply this number
// by 2 to know how many possible connections will be
// by 3 to know how many possible connections will be
// kept open by the server. The default is 50.
max: 50,
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
idleTimeoutMillis: 30000, // idle time before dropping connection
reapIntervalMillis: 1000, // time between cleanups
slowQueries: {
log: true,
elapsedThreshold: 200
},
slowPool: {
log: true, // whether a slow acquire must be logged or not
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
},
emitter: {
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
}
reapIntervalMillis: 1000 // time between cleanups
}
,sqlapi: {
protocol: 'https',
// If "host" is given, it will be used
// to connect to the SQL-API without a
// DNS lookup
//host: '127.0.0.1',
port: 8080,
// The "domain" part will be appended to
// the cartodb username and passed to
// SQL-API requests in the Host HTTP header
domain: 'cartodb.com',
version: 'v2',
// Maximum lenght of SQL query for GET
// requests. Longer queries will be sent
// using POST. Defaults to 2048
max_get_sql_length: 2048,
// Maximum time to wait for a response,
// in milliseconds. Defaults to 100.
timeout: 100
port: 8080,
version: 'v2'
}
,varnish: {
host: 'localhost',
port: 6082, // the por for the telnet interface where varnish is listening to
http_port: 6081, // the port for the HTTP interface where varnish is listening to
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
secret: 'xxx',
ttl: 86400,
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
port: 6082,
ttl: 86400
}
// 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.
,useProfiler:true
,serverMetadata: {
cdn_url: {
http: 'api.cartocdn.com',
https: 'cartocdn.global.ssl.fastly.net'
}
}
// Optional rollbar support
,rollbar: {
token: 'secret',
// See http://github.com/rollbar/node_rollbar#configuration-reference
options: {
endpoint: 'https://api.rollbar.com/api/1/',
handler: 'inline'
}
}
// Settings for the health check available at /health
,health: {
enabled: false,
username: 'localhost',
z: 0,
x: 0,
y: 0
}
};
module.exports = config;

View File

@@ -2,51 +2,20 @@ var config = {
environment: 'test'
,port: 8888
,host: '127.0.0.1'
,uv_threadpool_size: undefined
// Regular expression pattern to extract username
// from hostname. Must have a single grabbing block.
,user_from_host: '(.*)'
// Base URLs for the APIs
//
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
//
// Base url for the Templated Maps API
// "/api/v1/map/named" is the new API,
// "/tiles/template" is for compatibility with versions up to 1.6.x
,base_url_templated: '(?:/api/v1/map/named|/tiles/template)'
// Base url for the Detached Maps API
// "maps" is the the new API,
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
,base_url_detached: '(?:/api/v1/map|/tiles/layergroup)'
// Base url for the Inline Maps and Table Maps API
,base_url_legacy: '/tiles/:table'
// Maximum number of connections for one process
// 128 is a good value with a limit of 1024 open file descriptors
,maxConnections:128
// Maximum number of templates per user. Unlimited by default.
,maxUserTemplates:1024
// Seconds since "last creation" before a detached
// or template instance map expires. Or: how long do you want
// to be able to navigate the map without a reload ?
// Defaults to 7200 (2 hours)
,mapConfigTTL: 7200
// idle socket timeout, in milliseconds
// idle socket timeout, in miliseconds
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: false
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
// If log_filename is given logs will be written
// there, in append mode. Otherwise stdout is used (default).
// Log file will be re-opened on receiving the HUP signal
//,log_filename: 'logs/node-windshaft.log'
// Templated database username for authorized user
// Supported labels: 'user_id' (read from redis)
,postgres_auth_user: 'test_windshaft_cartodb_user_<%= user_id %>'
,postgres_auth_user: 'test_cartodb_user_<%= user_id %>'
// Templated database password for authorized user
// Supported labels: 'user_id', 'user_password' (both read from redis)
,postgres_auth_pass: 'test_windshaft_cartodb_user_<%= user_id %>_pass'
,postgres_auth_pass: 'test_cartodb_user_<%= user_id %>_pass'
,postgres: {
// Parameters to pass to datasource plugin of mapnik
// See http://github.com/mapnik/mapnik/wiki/PostGIS
@@ -57,39 +26,14 @@ var config = {
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
row_limit: 65535,
simplify_geometries: true,
use_overviews: true, // use overviews to retrieve raster
/*
* Set persist_connection to false if you want
* database connections to be closed on renderer
* expiration (1 minute after last use).
* Setting to true (the default) would never
* close any connection for the server's lifetime
*/
persist_connection: false,
max_size: 500
}
,mapnik_version: ''
,mapnik_tile_format: 'png8:m=h'
,statsd: {
host: 'localhost',
port: 8125,
prefix: 'test.:host.',
cacheDns: true
// support all allowed node-statsd options
}
,mapnik_version: '2.0.2'
,renderer: {
// Milliseconds since last access before renderer cache item expires
cache_ttl: 60000,
metatile: 4,
bufferSize: 64,
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
http: {
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
'http://{s}.example.com/{z}/{x}/{y}.png'
]
}
bufferSize: 64
}
,millstone: {
// Needs to be writable by server user
@@ -101,69 +45,31 @@ var config = {
// Max number of connections in each pool.
// Users will be put on a queue when the limit is hit.
// Set to maxConnection to have no possible queues.
// There are currently 2 pools involved in serving
// There are currently 3 pools involved in serving
// windshaft-cartodb requests so multiply this number
// by 2 to know how many possible connections will be
// by 3 to know how many possible connections will be
// kept open by the server. The default is 50.
max: 50,
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
idleTimeoutMillis: 1, // idle time before dropping connection
reapIntervalMillis: 1, // time between cleanups
slowQueries: {
log: true,
elapsedThreshold: 200
},
slowPool: {
log: true, // whether a slow acquire must be logged or not
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
},
emitter: {
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
}
reapIntervalMillis: 1 // time between cleanups
}
,sqlapi: {
protocol: 'http',
// If "host" is given, it will be used
// to connect to the SQL-API without a
// DNS lookup
host: '127.0.0.1',
port: 1080,
// The "domain" part will be appended to
// the cartodb username and passed to
// SQL-API requests in the Host HTTP header
domain: 'donot_look_this_up',
domain: '',
// This port will be used by "make check" for testing purposes
// It must be available
version: 'v1',
// Maximum lenght of SQL query for GET
// requests. Longer queries will be sent
// using POST. Defaults to 2048
max_get_sql_length: 2048,
// Maximum time to wait for a response,
// in milliseconds. Defaults to 100.
timeout: 100
port: 1080,
version: 'v1'
}
,varnish: {
host: '',
port: null, // the por for the telnet interface where varnish is listening to
http_port: 6081, // the port for the HTTP interface where varnish is listening to
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
secret: 'xxx',
ttl: 86400,
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
port: null,
ttl: 86400
}
// 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.
,useProfiler:true
// Settings for the health check available at /health
,health: {
enabled: false,
username: 'localhost',
z: 0,
x: 0,
y: 0
}
,useProfiler:false
};
module.exports = config;

1
config/settings.js Normal file
View File

@@ -0,0 +1 @@
module.exports.oneDay = 86400000;

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

@@ -1,674 +0,0 @@
## Maps API
The CartoDB Maps API allows you to generate maps based on data hosted in your CartoDB account, and you can apply custom SQL and CartoCSS to the data. The API generates a XYZ-based URL to fetch Web Mercator projected tiles using web clients such as [Leaflet](http://leafletjs.com), [Google Maps](https://developers.google.com/maps/), or [OpenLayers](http://openlayers.org/).
You can create two types of maps with the Maps API:
- **Anonymous maps**
Maps that can be created using your CartoDB public data. Any client can change the read-only SQL and CartoCSS parameters that generate the map tiles. These maps can be created from a JavaScript application alone and no authenticated calls are needed. See [this CartoDB.js example]({{ '/cartodb-platform/cartodb-js.html' | prepend: site.baseurl }}).
- **Named maps**
Maps that have access to your private data. These maps require an owner to setup and modify any SQL and CartoCSS parameters and are not modifiable without new setup calls.
## Quickstart
### Anonymous maps
Here is an example of how to create an anonymous map with JavaScript:
```javascript
var mapconfig = {
"version": "1.0.1",
"layers": [{
"type": "cartodb",
"options": {
"cartocss_version": "2.1.1",
"cartocss": "#layer { polygon-fill: #FFF; }",
"sql": "select * from european_countries_e"
}
}]
}
$.ajax({
crossOrigin: true,
type: 'POST',
dataType: 'json',
contentType: 'application/json',
url: 'http://documentation.cartodb.com/api/v1/map',
data: JSON.stringify(mapconfig),
success: function(data) {
var templateUrl = 'http://documentation.cartodb.com/api/v1/map/' + data.layergroupid + '/{z}/{x}/{y}.png'
console.log(templateUrl);
}
})
```
### Named maps
Let's create a named map using some private tables in a CartoDB account.
The following map config sets up a map of European countries that have a white fill color:
```javascript
{
"version": "0.0.1",
"name": "test",
"auth": {
"method": "open"
},
"layergroup": {
"layers": [{
"type": "cartodb",
"options": {
"cartocss_version": "2.1.1",
"cartocss": "#layer { polygon-fill: #FFF; }",
"sql": "select * from european_countries_e"
}
}]
}
}
```
The map config needs to be sent to CartoDB's Map API using an authenticated call. Here we will use a command line tool called `curl`. For more info about this tool, see [this blog post](http://quickleft.com/blog/command-line-tutorials-curl), or type ``man curl`` in bash. Using `curl`, and storing the config from above in a file `mapconfig.json`, the call would look like:
<div class="code-title notitle code-request"></div>
```bash
curl 'https://{account}.cartodb.com/api/v1/map/named?api_key=APIKEY' -H 'Content-Type: application/json' -d @mapconfig.json
```
To get the `URL` to fetch the tiles you need to instantiate the map, where `template_id` is the template name from the previous response.
<div class="code-title notitle code-request"></div>
```bash
curl -X POST 'http://{account}.cartodb.com/api/v1/map/named/:template_id' -H 'Content-Type: application/json'
```
The response will return JSON with properties for the `layergroupid`, `cdn_url`, and the timestamp (`last_updated`) of the last data modification.
Here is an example response:
```javascript
{
"cdn_url": {
"http": "ashbu.cartocdn.com",
"https": "cartocdn-ashbu.global.ssl.fastly.net"
},
"layergroupid": "c01a54877c62831bb51720263f91fb33:0",
"last_updated": "1970-01-01T00:00:00.000Z"
}
```
You can use the `layergroupid` to instantiate a URL template for accessing tiles on the client. Here we use the `layergroupid` from the example response above in this URL template:
```bash
http://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
```
## General Concepts
The following concepts are the same for every endpoint in the API except when it's noted explicitly.
### Auth
By default, users do not have access to private tables in CartoDB. In order to instantiate a map from private table data, an API Key is required. Additionally, to include some endpoints, an API Key must be included (e.g. creating a named map).
To execute an authorized request, api_key=YOURAPIKEY should be added to the request URL. The param can be also passed as POST param. We **strongly advise** using HTTPS when you are performing requests that include your `api_key`.
### Errors
Errors are reported using standard HTTP codes and extended information encoded in JSON with this format:
```javascript
{
"errors": [
"access forbidden to table TABLE"
]
}
```
If you use JSONP, the 200 HTTP code is always returned so the JavaScript client can receive errors from the JSON object.
### CORS support
All the endpoints which might be accessed using a web browser add CORS headers and allow OPTIONS method.
## Anonymous Maps
Anonymous maps allows you to instantiate a map given SQL and CartoCSS. It also allows you to add interaction capabilities using [UTF Grid.](https://github.com/mapbox/utfgrid-spec)
### Instantiate
#### Definition
<div class="code-title notitle code-request"></div>
```html
POST /api/v1/map
```
#### Params
```javascript
{
"version": "1.0.1",
"layers": [{
"type": "cartodb",
"options": {
"cartocss_version": "2.1.1",
"cartocss": "#layer { polygon-fill: #FFF; }",
"sql": "select * from european_countries_e",
"interactivity": ["cartodb_id", "iso3"]
}
}]
}
```
Should be a [Mapconfig](https://github.com/CartoDB/Windshaft/blob/0.19.1/doc/MapConfig-1.1.0.md).
#### Response
The response includes:
- **layergroupid**
The ID for that map, used to compose the URL for the tiles. The final URL is:
```html
http://{account}.cartodb.com/api/v1/map/:layergroupid/{z}/{x}/{y}.png
```
- **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.
- **cdn_url**
URLs to fetch the data using the best CDN for your zone.
#### Example
<div class="code-title code-request with-result">REQUEST</div>
```bash
curl 'http://documentation.cartodb.com/api/v1/map' -H 'Content-Type: application/json' -d @mapconfig.json
```
<div class="code-title">RESPONSE</div>
```javascript
{
"layergroupid":"c01a54877c62831bb51720263f91fb33:0",
"last_updated":"1970-01-01T00:00:00.000Z"
"cdn_url": {
"http": "http://cdb.com",
"https": "https://cdb.com"
}
}
```
The tiles can be accessed using:
```bash
http://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
```
For UTF grid tiles:
```bash
http://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/{z}/{x}/{y}.grid.json
```
For attributes defined in `attributes` section:
```bash
http://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/attributes/:feature_id
```
Which returns JSON with the attributes defined, like:
```javascript
{ 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. So 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.
### Create JSONP
The JSONP endpoint is provided in order to allow web browsers access which don't support CORS.
#### Definition
<div class="code-title notitle code-request"></div>
```bash
GET /api/v1/map?callback=method
```
#### Params
- **config**
Encoded JSON with the params for creating named maps (the variables defined in the template).
- **lmza**
This attribute contains the same as config but LZMA compressed. It cannot be used at the same time as `config`.
- **callback**
JSON callback name.
#### Example
<div class="code-title code-request with-result">REQUEST</div>
```bash
curl "https://documentation.cartodb.com/api/v1/map?callback=callback&config=%7B%22version%22%3A%221.0.1%22%2C%22layers%22%3A%5B%7B%22type%22%3A%22cartodb%22%2C%22options%22%3A%7B%22sql%22%3A%22select+%2A+from+european_countries_e%22%2C%22cartocss%22%3A%22%23european_countries_e%7B+polygon-fill%3A+%23FF6600%3B+%7D%22%2C%22cartocss_version%22%3A%222.3.0%22%2C%22interactivity%22%3A%5B%22cartodb_id%22%5D%7D%7D%5D%7D"
```
<div class="code-title">RESPONSE</div>
```javascript
callback({
layergroupid: "d9034c133262dfb90285cea26c5c7ad7:0",
cdn_url: {
"http": "http://cdb.com",
"https": "https://cdb.com"
},
last_updated: "1970-01-01T00:00:00.000Z"
})
```
### Remove
Anonymous maps cannot be removed by an API call. They will expire after about five minutes but sometimes longer. If an anonymous map expires and tiles are requested from it, an error will be raised. This could happen if a user leaves a map open and after time returns to the map an attempts to interact with it in a way that requires new tiles (e.g. zoom). The client will need to go through the steps of creating the map again to fix the problem.
## 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 that 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:
- **auth layer**
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
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](http://docs.cartodb.com/cartodb-platform/maps-api.html#auth)).
### Create
#### Definition
<div class="code-title notitle code-request"></div>
```html
POST /api/v1/map/named
```
#### Params
- **api_key** is required
<div class="code-title">template.json</div>
```javascript
{
"version": "0.0.1",
"name": "template_name",
"auth": {
"method": "token",
"valid_tokens": [
"auth_token1",
"auth_token2"
]
},
"placeholders": {
"color": {
"type": "css_color",
"default": "red"
},
"cartodb_id": {
"type": "number",
"default": 1
}
},
"layergroup": {
"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 %>"
}
}
]
}
}
```
##### 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 (_).
- **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. 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 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.
#### Template Format
A templated `layergroup` allows the use of placeholders in the "cartocss" and "sql" elements of the "option" object in any "layer" of a `layergroup` configuration
Valid placeholder names start with a letter and can only contain letters, numbers, or underscores. They have to be written between the `<%=` and `%>` strings in order to be replaced.
##### Example
```javascript
<%= my_color %>
```
The set of supported placeholders for a template will need to be explicitly defined with a specific type and default value for each.
#### Placeholder Types
The 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
Placeholder default values will be used whenever new values are not provided as options at the time of creation on the client. They can also be used to test the template by creating a default version with now options provided.
When using templates, be very careful about your selections as they can give broad access to your data if they are defined losely.
<div class="code-title code-request with-result">REQUEST</div>
```html
curl -X POST \
-H 'Content-Type: application/json' \
-d @template.json \
'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
```
<div class="code-title">RESPONSE</div>
```javascript
{
"template_id":"name",
}
```
### Instantiate
Instantiating a map allows you to get the information needed to fetch tiles. That temporal map is an anonymous map.
#### Definition
<div class="code-title notitle code-request"></div>
```html
POST /api/v1/map/named/:template_name
```
#### Param
- **auth_token** optional, but required when `"method"` is set to `"token"`
```javascript
// params.json
{
"color": "#ff0000",
"cartodb_id": 3
}
```
The fields you pass as `params.json` depend on the variables allowed by the named map. If there are variables missing it will raise an error (HTTP 400).
- **auth_token** *optional* if the named map needs auth
#### Example
You can initialize a template map by passing all of the required parameters in a POST to `/api/v1/map/named/:template_name`.
Valid credentials will be needed if required by the template.
<div class="code-title code-request with-result">REQUEST</div>
```bash
curl -X POST \
-H 'Content-Type: application/json' \
-d @params.json \
'https://documentation.cartodb.com/api/v1/map/named/@template_name?auth_token=AUTH_TOKEN'
```
<div class="code-title">Response</div>
```javascript
{
"layergroupid": "docs@fd2861af@c01a54877c62831bb51720263f91fb33:123456788",
"last_updated": "2013-11-14T11:20:15.000Z"
}
```
<div class="code-title">Error</div>
```javascript
{
"error": "Some error string here"
}
```
You can then use the `layergroupid` for fetching tiles and grids as you would normally (see anonymous map section). However, you'll need to show the `auth_token`, if required by the template.
### Using JSONP
There is also a special endpoint to be able to initialize a map using JSONP (for old browsers).
#### Definition
<div class="code-title notitle code-request"></div>
```bash
GET /api/v1/map/named/:template_name/jsonp
```
#### Params
- **auth_token** optional, but required when `"method"` is set to `"token"`
- **config** Encoded JSON with the params for creating named maps (the variables defined in the template)
- **lmza** This attribute contains the same as config but LZMA compressed. It cannot be used at the same time than `config`.
- **callback:** JSON callback name
<div class="code-title code-request with-result">REQUEST</div>
```bash
curl 'https://documentation.cartodb.com/api/v1/map/named/:template_name/jsonp?auth_token=AUTH_TOKEN&callback=callback&config=template_params_json'
```
<div class="code-title">RESPONSE</div>
```javascript
callback({
"layergroupid":"c01a54877c62831bb51720263f91fb33:0",
"last_updated":"1970-01-01T00:00:00.000Z"
"cdn_url": {
"http": "http://cdb.com",
"https": "https://cdb.com"
}
})
```
This takes the `callback` function (required), `auth_token` if the template needs auth, and `config` which is the variable for the template (in cases where it has variables).
```javascript
url += "config=" + encodeURIComponent(
JSON.stringify({ color: 'red' });
```
The response is in this format:
```javascript
callback({
layergroupid: "dev@744bd0ed9b047f953fae673d56a47b4d:1390844463021.1401",
last_updated: "2014-01-27T17:41:03.021Z"
})
```
### Update
#### Definition
<div class="code-title notitle code-request"></div>
```bash
PUT /api/v1/map/named/:template_name
```
#### Params
- **api_key** is required
#### Response
Same as updating a map.
#### Other Info
Updating a named map removes all the named map instances so they need to be initialized again.
#### Example
<div class="code-title code-request with-result">REQUEST</div>
```bash
curl -X PUT \
-H 'Content-Type: application/json' \
-d @template.json \
'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
```
<div class="code-title">RESPONSE</div>
```javascript
{
"template_id": "@template_name"
}
```
If any template has the same name, it will be updated.
If a template with the same name does NOT exist, a 400 HTTP response is generated with an error in this format:
```javascript
{
"error": "error string here"
}
```
### Delete
Delete the specified template map from the server and disables any previously initialized versions of the map.
#### Definition
<div class="code-title notitle code-request"></div>
```bash
DELETE /api/v1/map/named/:template_name
```
#### Params
- **api_key** is required
#### Example
<div class="code-title code-request">REQUEST</div>
```bash
curl -X DELETE 'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
```
<div class="code-title">RESPONSE</div>
```javascript
{
"error": "Some error string here"
}
```
On success, a 204 (No Content) response would be issued. Otherwise a 4xx response with with an error will be returned.
### Listing Available Templates
This allows you to get a list of all available templates.
#### Definition
<div class="code-title notitle code-request"></div>
```bash
GET /api/v1/map/named/
```
#### Params
- **api_key** is required
#### Example
<div class="code-title code-request with-result">REQUEST</div>
```bash
curl -X GET 'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
```
<div class="code-title with-result">RESPONSE</div>
```javascript
{
"template_ids": ["@template_name1","@template_name2"]
}
```
<div class="code-title">ERROR</div>
```javascript
{
"error": "Some error string here"
}
```
### Getting a Specific Template
This gets the definition of a template.
#### Definition
<div class="code-title notitle code-request"></div>
```bash
GET /api/v1/map/named/:template_name
```
#### Params
- **api_key** is required
#### Example
<div class="code-title code-request with-result">REQUEST</div>
```bash
curl -X GET 'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
```
<div class="code-title with-result">RESPONSE</div>
```javascript
{
"template": {...} // see template.json above
}
```
<div class="code-title">ERROR</div>
```javascript
{
"error": "Some error string here"
}
```
### Use with CartoDB.js
Named maps can be used with CartoDB.js by specifying a named map in a layer source as follows. Named maps are treated almost the same as other layer source types in most other ways.
```js
var layerSource = {
user_name: '{your_user_name}',
type: 'namedmap',
named_map: {
name: '{template_name}',
layers: [{
layer_name: "layer1",
interactivity: "column1, column2, ..."
}]
}
}
cartodb.createLayer('map_dom_id',layerSource)
.addTo(map_object);
```
See [CartoDB.js](http://docs.cartodb.com/cartodb-platform/cartodb-js.html) methods [layer.setParams()](http://docs.cartodb.com/cartodb-platform/cartodb-js.html#layersetparamskey-value) and [layer.setAuthToken()](http://docs.cartodb.com/cartodb-platform/cartodb-js.html#layersetauthtokenauthtoken).

View File

@@ -1,28 +0,0 @@
The Windshaft-CartoDB MultiLayer API extends the [Windshaft MultiLayer API](https://github.com/Vizzuality/Windshaft/wiki/Multilayer-API) in a few ways.
## Last modification timestamp embedded in the token
It encodes a timestamp of 'last modification time' into the map token (token:EPOCH) returned to the client.
It accepts tokens with encoded timestamp from the client considering the token suffix as a cache_buster value.
Clients don't need to be aware of the extension but rather use the API as they would use the base one.
The only difference will be that the _same_ layergroup configuration may result in different tokens if source data was modified between the mapview requests.
## Additional attributes in the response object
Windshaft-CartoDB adds the following attributes in the response object
- ``last_update`` field with ISO format (2013-11-30T12:23:10).
- ``cdn_url`` object containing CDN url client should use (not mandatory) to access the tiles. It's in the form:
```json
{
http: 'http://cdn_url.com/'
https: 'https://secure.cdn_url.com/'
}
```
## Stats tag
Windshaft-CartoDB adds support for a ``stat_tag`` element in the multilayer configuration to help [stats](https://github.com/CartoDB/Windshaft-cartodb/wiki/Redis-stats-format) gathering.

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"
}
```

View File

@@ -1,44 +0,0 @@
Windshaft-cartodb metrics
=========================
See [Windshaft metrics documentation](https://github.com/CartoDB/Windshaft/blob/master/doc/metrics.md) to understand the full picture.
The next list includes the API endpoints, each endpoint may have several inner timers, some of them are displayed within this list as subitems. Find the description for them in the Inner timers section.
## Timers
- **windshaft-cartodb.flush_cache**: time to flush the tile and sql cache
- **windshaft-cartodb.get_template**: time to retrieve an specific template
- **windshaft-cartodb.delete_template**: time to delete an specific template
- **windshaft-cartodb.get_template_list**: time to retrieve the list of owned templates
- **windshaft-cartodb.instance_template_post**: time to create a template via HTTP POST
- **windshaft-cartodb.instance_template_get**: time to create a template via HTTP GET
+ TemplateMaps_instance
+ createLayergroup
There are some endpoints that are not being tracked:
- Adding a template
- Updating a template
### Inner timers
Again, each inner timer may have several inner timers.
- **addCacheChannel**: time to add X-Cache-Channel header based on table last modifications
- **LZMA decompress**: time to decompress request params with LZMA
- **TemplateMaps_instance**: time to retrieve a map template instance, see *getTemplate* and *authorizedByCert*
- **affectedTables**: time to check what are the affected tables for adding the cache channel, see *addCacheChannel*
- **authorize**: time to authorize a request, see *authorizedByAPIKey*, *authorizedByCert*, *authorizedBySigner*
- **authorizedByAPIKey**: time to authorize using an API KEY
- **authorizedByCert**: time to authorize a template instantiation
- **authorizedBySigner**: time to authorize a request with auth_token
- **findLastUpdated**: time to retrieve the last update time for a list of tables, see *affectedTables*
- **generateCacheChannel**: time to generate the headers for the cache channel based on the request, see *addCacheChannel*
- **getSignerMapKey**: time to retrieve from redis the authorized user for a template map
- **getTablePrivacy**: time to retrieve from redis the privacy of a table
- **getTemplate**: time to retrieve from redis the template for a map
- **getUserMapKey**: time to retrieve from redis the user key for a map
- **incMapviewCount**: time to incremenent in redis the map views
- **mapStore_load**: time to retrieve from redis a map configuration
- **req2params.setup**: time to prepare the params from a request, see *req2params* in Windshaft documentation
- **setDBAuth**: time to retrieve from redis and set db user and db password from a user
- **setDBConn**: time to retrieve from redis and set db host and db name from a user
- **setDBParams**: time to prepare all db params to be able to connect/query a database, see *setDBAuth* and *setDBConn*
- **tablePrivacy_getUserDBName**: time to retrieve from redis the database for a user

View File

@@ -1,122 +0,0 @@
var sqlApi = require('../sql/sql_api'),
PSQL = require('cartodb-psql');
function QueryTablesApi() {
}
var affectedTableRegexCache = {
bbox: /!bbox!/g,
scale_denominator: /!scale_denominator!/g,
pixel_width: /!pixel_width!/g,
pixel_height: /!pixel_height!/g
};
module.exports = QueryTablesApi;
QueryTablesApi.prototype.getLastUpdatedTime = function (username, api_key, tableNames, callback) {
var sql = '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(',') +
'])';
// call sql api
sqlApi.query(username, api_key, sql, function(err, rows){
if (err){
var msg = err.message ? err.message : err;
callback(new Error('could not find last updated timestamp: ' + msg));
return;
}
// when the table has not updated_at means it hasn't been changed so a default last_updated is set
var last_updated = 0;
if(rows.length !== 0) {
last_updated = rows[0].max || 0;
}
callback(null, last_updated*1000);
});
};
QueryTablesApi.prototype.getAffectedTablesInQuery = function (username, options, sql, callback) {
var query = 'SELECT CDB_QueryTables($windshaft$' + prepareSql(sql) + '$windshaft$)';
runQuery(username, options, query, handleAffectedTablesInQueryRows, callback);
};
function handleAffectedTablesInQueryRows(err, rows, callback) {
if (err){
var msg = err.message ? err.message : err;
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(',') : [];
callback(null, tableNames);
}
QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (username, options, sql, callback) {
var query = [
'WITH querytables AS (',
'SELECT * FROM CDB_QueryTables($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(' ');
runQuery(username, options, 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));
return;
}
var result = rows[0];
var tableNames = result.tablenames.split(/^\{(.*)\}$/)[1];
tableNames = tableNames ? tableNames.split(',') : [];
var lastUpdatedTime = result.max || 0;
callback(null, {
affectedTables: tableNames,
lastUpdatedTime: lastUpdatedTime * 1000
});
}
function runQuery(username, options, query, queryHandler, callback) {
if (shouldQueryPostgresDirectly()) {
var psql = new PSQL(options);
psql.query(query, function(err, resultSet) {
resultSet = resultSet || {};
var rows = resultSet.rows || [];
queryHandler(err, rows, callback);
});
} else {
sqlApi.query(username, options.api_key, query, function(err, rows) {
queryHandler(err, rows, callback);
});
}
}
function prepareSql(sql) {
return sql
.replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)')
.replace(affectedTableRegexCache.scale_denominator, '0')
.replace(affectedTableRegexCache.pixel_width, '1')
.replace(affectedTableRegexCache.pixel_height, '1')
;
}
function shouldQueryPostgresDirectly() {
return global.environment
&& global.environment.enabledFeatures
&& global.environment.enabledFeatures.cdbQueryTablesFromPostgres;
}

View File

@@ -1,32 +0,0 @@
var request = require('request');
function VarnishHttpCacheBackend(host, port) {
this.host = host;
this.port = port;
}
module.exports = VarnishHttpCacheBackend;
/**
* @param cacheObject should respond to `key() -> String` method
* @param {Function} callback
*/
VarnishHttpCacheBackend.prototype.invalidate = function(cacheObject, callback) {
request(
{
method: 'PURGE',
url: 'http://' + this.host + ':' + this.port + '/key',
headers: {
'Invalidation-Match': '\\b' + cacheObject.key() + '\\b'
}
},
function(err, response) {
if (err || response.statusCode !== 204) {
return callback(new Error('Unable to invalidate Varnish object'));
}
return callback(null);
}
);
};
module.exports = VarnishHttpCacheBackend;

View File

@@ -1,18 +0,0 @@
var crypto = require('crypto');
function NamedMaps(owner, name) {
this.namespace = 'n';
this.owner = owner;
this.name = name;
}
module.exports = NamedMaps;
NamedMaps.prototype.key = function() {
return this.namespace + ':' + shortHashKey(this.owner + ':' + this.name);
};
function shortHashKey(target) {
return crypto.createHash('sha256').update(target).digest('base64').substring(0,6);
}

View File

@@ -1,26 +0,0 @@
/**
* @param cacheBackend should respond to `invalidate(cacheObject, callback)` method
* @constructor
*/
function SurrogateKeysCache(cacheBackend) {
this.cacheBackend = cacheBackend;
}
module.exports = SurrogateKeysCache;
/**
* @param response should respond to `header(key, value)` method
* @param cacheObject should respond to `key() -> String` method
*/
SurrogateKeysCache.prototype.tag = function(response, cacheObject) {
response.header('Surrogate-Key', cacheObject.key());
};
/**
* @param cacheObject should respond to `key() -> String` method
* @param {Function} callback
*/
SurrogateKeysCache.prototype.invalidate = function(cacheObject, callback) {
this.cacheBackend.invalidate(cacheObject, callback);
};

View File

@@ -2,21 +2,16 @@ var _ = require('underscore'),
Varnish = require('node-varnish'),
varnish_queue = null;
function init(host, port, secret) {
varnish_queue = new Varnish.VarnishQueue(host, port, secret);
varnish_queue.on('error', function(e) {
console.log("[CACHE VALIDATOR ERROR] " + e);
});
function init(host, port) {
varnish_queue = new Varnish.VarnishQueue(host, port);
}
function invalidate_db(dbname, table) {
var cmd = 'purge obj.http.X-Cache-Channel ~ "^' + dbname +
':(.*'+ table +'.*)|(table)$"';
try{
varnish_queue.run_cmd(cmd, false);
varnish_queue.run_cmd('purge obj.http.X-Cache-Channel ~ "^' + dbname + ':(.*'+ table +'.*)|(table)$"');
console.log('[SUCCESS FLUSHING CACHE]');
} catch (e) {
console.log("[CACHE VALIDATOR ERROR] could not queue command " +
cmd + " -- " + e);
console.log("[ERROR FLUSHING CACHE] Is enable_cache set to true? Failed for: " + 'purge obj.http.X-Cache-Channel ~ "^' + dbname + ':(.*'+ table +'.*)|(table)$"');
}
}

View File

@@ -1,33 +1,14 @@
var _ = require('underscore')
, Step = require('step')
, Windshaft = require('windshaft')
, TemplateMaps = require('./template_maps.js')
, Cache = require('./cache_validator')
, os = require('os')
, HealthCheck = require('./monitoring/health_check')
;
if ( ! process.env['PGAPPNAME'] )
process.env['PGAPPNAME']='cartodb_tiler';
, Cache = require('./cache_validator');
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});
if(serverOptions.cache_enabled) {
console.log("cache invalidation enabled, varnish on ", serverOptions.varnish_host, ' ', serverOptions.varnish_port);
Cache.init(serverOptions.varnish_host, serverOptions.varnish_port, serverOptions.varnish_secret);
Cache.init(serverOptions.varnish_host, serverOptions.varnish_port);
serverOptions.afterStateChange = function(req, data, callback) {
Cache.invalidate_db(req.params.dbname, req.params.table);
callback(null, data);
@@ -36,42 +17,10 @@ var CartodbWindshaft = function(serverOptions) {
serverOptions.beforeStateChange = function(req, callback) {
var err = null;
if ( ! req.params.hasOwnProperty('_authorizedByApiKey') ) {
if ( ! req.params.hasOwnProperty('dbuser') ) {
err = new Error("map state cannot be changed by unauthenticated request!");
}
callback(err, req);
};
// 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 templateMapsOpts = {
max_user_templates: global.environment.maxUserTemplates
};
var templateMaps = new TemplateMaps(redisPool, templateMapsOpts);
serverOptions.templateMaps = templateMaps;
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);
if (serverOptions.varnish_purge_enabled) {
function invalidateNamedMap(owner, templateName) {
surrogateKeysCache.invalidate(new NamedMapsCacheEntry(owner, templateName), function(err) {
if (err) {
console.warn('Cache: surrogate key invalidation failed');
}
});
}
['update', 'delete'].forEach(function(eventType) {
templateMaps.on(eventType, invalidateNamedMap);
});
}
// boot
@@ -83,88 +32,7 @@ var CartodbWindshaft = function(serverOptions) {
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
);
templateMapsController.register(ws);
/*******************************************************************************************************************
* END Routing
******************************************************************************************************************/
}
/**
* Helper to allow access to the layer to be used in the maps infowindow popup.
@@ -177,10 +45,10 @@ var CartodbWindshaft = function(serverOptions) {
},
function(err, data){
if (err){
ws.sendError(res, {error: err.message}, 500, 'GET INFOWINDOW', err);
//ws.sendResponse(res, [{error: err.message}, 500]);
ws.sendError(res, {error: err.message}, 500, 'GET INFOWINDOW');
//res.send({error: err.message}, 500);
} else {
ws.sendResponse(res, [{infowindow: data}, 200]);
res.send({infowindow: data}, 200);
}
}
);
@@ -198,10 +66,10 @@ var CartodbWindshaft = function(serverOptions) {
},
function(err, data){
if (err){
ws.sendError(res, {error: err.message}, 500, 'GET MAP_METADATA', err);
//ws.sendResponse(res, [err.message, 500]);
ws.sendError(res, {error: err.message}, 500, 'GET MAP_METADATA');
//res.send(err.message, 500);
} else {
ws.sendResponse(res, [{map_metadata: data}, 200]);
res.send({map_metadata: data}, 200);
}
}
);
@@ -212,9 +80,6 @@ var CartodbWindshaft = function(serverOptions) {
* TODO: Move?
*/
ws.del(serverOptions.base_url + '/flush_cache', function(req, res){
if ( req.profiler && req.profiler.statsd_client ) {
req.profiler.start('windshaft-cartodb.flush_cache');
}
ws.doCORS(res);
Step(
function flushCache(){
@@ -222,41 +87,15 @@ var CartodbWindshaft = function(serverOptions) {
},
function sendResponse(err, data){
if (err){
ws.sendError(res, {error: err.message}, 500, 'DELETE CACHE', err);
//ws.sendResponse(res, [500]);
ws.sendError(res, {error: err.message}, 500, 'DELETE CACHE');
//res.send(500);
} else {
ws.sendResponse(res, [{status: 'ok'}, 200]);
res.send({status: 'ok'}, 200);
}
}
);
});
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

@@ -1,478 +0,0 @@
var Step = require('step');
var _ = require('underscore');
function TemplateMapsController(app, serverOptions, templateMaps, metadataBackend, templateBaseUrl, surrogateKeysCache,
NamedMapsCacheEntry) {
this.app = app;
this.serverOptions = serverOptions;
this.templateMaps = templateMaps;
this.metadataBackend = metadataBackend;
this.templateBaseUrl = templateBaseUrl;
this.surrogateKeysCache = surrogateKeysCache;
this.NamedMapsCacheEntry = NamedMapsCacheEntry;
}
module.exports = TemplateMapsController;
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 = self.serverOptions.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 = this.serverOptions.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 = this.serverOptions.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 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 err = new Error("Cannot get template id '"
+ req.params.template_id + "' for user '" + cdbuser + "'");
err.http_status = 404;
throw err;
}
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 = this.serverOptions.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 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 err = new Error("Cannot find template id '"
+ req.params.template_id + "' for user '" + cdbuser + "'");
err.http_status = 404;
throw err;
}
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 = this.serverOptions.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) {
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 = self.serverOptions.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: {}, headers: _.clone(req.headers),
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.serverOptions.setDBAuth(cdbuser, params, this);
},
function setConn(err) {
if ( err ) throw err;
self.serverOptions.setDBConn(cdbuser, params, this);
},
function finish(err) {
callback(err);
}
);
};

View File

@@ -1,49 +0,0 @@
var rollbar = require("rollbar");
/**
* Rollbar Appender. Sends logging events to Rollbar using node-rollbar
*
* @param config object with rollbar configuration data
* {
* token: 'your-secret-token',
* options: node-rollbar options
* }
*/
function rollbarAppender(config) {
var opt = config.options;
rollbar.init(opt.token, opt.options);
return function(loggingEvent) {
/*
For logger.trace('one','two','three'):
{ startTime: Wed Mar 12 2014 16:27:40 GMT+0100 (CET),
categoryName: '[default]',
data: [ 'one', 'two', 'three' ],
level: { level: 5000, levelStr: 'TRACE' },
logger: { category: '[default]', _events: { log: [Object] } } }
*/
// Levels:
// TRACE 5000
// DEBUG 10000
// INFO 20000
// WARN 30000
// ERROR 40000
// FATAL 50000
//
// We only log error and higher errors
//
if ( loggingEvent.level.level < 40000 ) return;
rollbar.reportMessage(loggingEvent.data);
};
}
function configure(config) {
return rollbarAppender(config);
}
exports.name = "rollbar";
exports.appender = rollbarAppender;
exports.configure = configure;

View File

@@ -1,90 +0,0 @@
var _ = require('underscore'),
dot = require('dot'),
fs = require('fs'),
path = require('path'),
Step = require('step');
function HealthCheck(metadataBackend, tilelive) {
this.metadataBackend = metadataBackend;
this.tilelive = tilelive;
}
module.exports = HealthCheck;
var mapnikOptions = {
query: {
metatile: 1,
poolSize: 4,
bufferSize: 64
},
protocol: 'mapnik:',
slashes: true,
xml: null
};
var xmlTemplate = dot.template(fs.readFileSync(path.resolve(__dirname, 'map-config.xml'), 'utf-8'));
HealthCheck.prototype.check = function(config, callback) {
var self = this,
startTime,
result = {
redis: {
ok: false
},
mapnik: {
ok: false
},
tile: {
ok: false
}
};
mapnikXmlParams = config;
Step(
function getDBParams() {
startTime = Date.now();
self.metadataBackend.getAllUserDBParams(config.username, this);
},
function loadMapnik(err, dbParams) {
if (err) {
throw err;
}
result.redis = {
ok: !err,
elapsed: Date.now() - startTime,
size: Object.keys(dbParams).length
};
mapnikOptions.xml = xmlTemplate(mapnikXmlParams);
startTime = Date.now();
self.tilelive.load(mapnikOptions, this);
},
function getTile(err, source) {
if (err) {
throw err;
}
result.mapnik = {
ok: !err,
elapsed: Date.now() - startTime
};
startTime = Date.now();
source.getTile(config.z, config.x, config.y, this);
},
function handleTile(err, tile) {
result.tile = {
ok: !err
};
if (tile) {
result.tile.elapsed = Date.now() - startTime;
result.tile.size = tile.length;
}
callback(err, result);
}
);
};

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,66 +0,0 @@
var _ = require('underscore'),
request = require('request');
module.exports.query = function (username, api_key, sql, callback) {
var api = global.environment.sqlapi;
// build up api string
var sqlapihostname = username;
if ( api.domain ) sqlapihostname += '.' + api.domain;
var sqlapi = api.protocol + '://';
if ( api.host && api.host != api.domain ) sqlapi += api.host;
else sqlapi += sqlapihostname;
sqlapi += ':' + api.port + '/api/' + api.version + '/sql';
var qs = { q: sql };
// add api_key if given
if (_.isString(api_key) && api_key != '') { qs.api_key = api_key; }
// call sql api
//
// NOTE: using POST to avoid size limits:
// See http://github.com/CartoDB/Windshaft-cartodb/issues/111
//
// NOTE: uses "host" header to allow IP based specification
// of sqlapi address (and avoid a DNS lookup)
//
// NOTE: allows for keeping up to "maxConnections" concurrent
// sockets opened per SQL-API host.
// See http://nodejs.org/api/http.html#http_agent_maxsockets
//
var maxSockets = global.environment.maxConnections || 128;
var maxGetLen = api.max_get_sql_length || 2048;
var maxSQLTime = api.timeout || 100; // 1/10 of a second by default
var reqSpec = {
url:sqlapi,
json:true,
headers:{host: sqlapihostname}
// http://nodejs.org/api/http.html#http_agent_maxsockets
,pool:{maxSockets:maxSockets}
// timeout in milliseconds
,timeout:maxSQLTime
};
if ( sql.length > maxGetLen ) {
reqSpec.method = 'POST';
reqSpec.body = qs;
} else {
reqSpec.method = 'GET';
reqSpec.qs = qs;
}
request(reqSpec, function(err, res, body) {
if (err){
console.log('ERROR connecting to SQL API on ' + sqlapi + ': ' + err);
callback(err);
return;
}
if (res.statusCode != 200) {
var msg = res.body.error ? res.body.error : res.body;
callback(new Error(msg));
console.log('unexpected response status (' + res.statusCode + ') for sql query: ' + sql + ': ' + msg);
return;
}
callback(null, body.rows);
});
};

View File

@@ -1,501 +0,0 @@
var crypto = require('crypto'),
Step = require('step'),
_ = require('underscore'),
dot = require('dot');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
// Class handling map templates
//
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps
//
// @param redis_pool an instance of a "redis-mpool"
// See https://github.com/CartoDB/node-redis-mpool
// Needs version 0.x.x of the API.
//
// @param opts TemplateMap options. Supported elements:
// 'max_user_templates' limit on the number of per-user
//
//
function TemplateMaps(redis_pool, opts) {
if (!(this instanceof TemplateMaps)) return new TemplateMaps();
EventEmitter.call(this);
this.redis_pool = redis_pool;
this.opts = opts || {};
// Database containing templates
// TODO: allow configuring ?
// NOTE: currently it is the same as
// the one containing layergroups
this.db_signatures = 0;
//
// Map templates are owned by a user that specifies access permissions
// for their instances.
//
// We have the following datastores:
//
// 1. User templates: set of per-user map templates
// User templates (HASH:tpl_id->tpl_val)
this.key_usr_tpl = dot.template("map_tpl|{{=it.owner}}");
// User template locks (HASH:tpl_id->ctime)
this.key_usr_tpl_lck = dot.template("map_tpl|{{=it.owner}}|locks");
this.lock_ttl = this.opts['lock_ttl'] || 5000;
}
util.inherits(TemplateMaps, EventEmitter);
module.exports = TemplateMaps;
var o = TemplateMaps.prototype;
//--------------- PRIVATE METHODS --------------------------------
o._userTemplateLimit = function() {
return this.opts['max_user_templates'] || 0;
};
/**
* Internal function to communicate with redis
*
* @param redisFunc - the redis function to execute
* @param redisArgs - the arguments for the redis function in an array
* @param callback - function to pass results too.
*/
o._redisCmd = function(redisFunc, redisArgs, callback) {
var redisClient;
var that = this;
var db = that.db_signatures;
Step(
function getRedisClient() {
that.redis_pool.acquire(db, this);
},
function executeQuery(err, data) {
if ( err ) throw 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);
callback(err, data);
}
);
};
var _reValidIdentifier = /^[a-zA-Z][0-9a-zA-Z_]*$/;
o._checkInvalidTemplate = function(template) {
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) ) {
return new Error("Invalid characters in template name '" + tplname + "'");
}
var invalidError = isInvalidLayergroup(template.layergroup);
if (invalidError) {
return invalidError;
}
var placeholders = template.placeholders || {};
var placeholderKeys = Object.keys(placeholders);
for (var i = 0, len = placeholderKeys.length; i < len; i++) {
var placeholderKey = placeholderKeys[i];
if (!placeholderKey.match(_reValidIdentifier)) {
return new Error("Invalid characters in placeholder name '" + placeholderKey + "'");
}
if ( ! placeholders[placeholderKey].hasOwnProperty('default') ) {
return new Error("Missing default for placeholder '" + placeholderKey + "'");
}
if ( ! placeholders[placeholderKey].hasOwnProperty('type') ) {
return new Error("Missing type for placeholder '" + placeholderKey + "'");
}
}
var auth = template.auth || {};
switch ( auth.method ) {
case 'open':
break;
case 'token':
if ( ! _.isArray(auth.valid_tokens) )
return new Error("Invalid 'token' authentication: missing valid_tokens");
if ( ! auth.valid_tokens.length )
return new Error("Invalid 'token' authentication: no valid_tokens");
break;
default:
return new Error("Unsupported authentication method: " + auth.method);
break;
}
return false;
};
function isInvalidLayergroup(layergroup) {
if (!layergroup) {
return new Error('Missing layergroup');
}
var layers = layergroup.layers;
if (!_.isArray(layers) || layers.length === 0) {
return new Error('Missing or empty layers array from layergroup config');
}
var invalidLayers = layers
.map(function(layer, layerIndex) {
return layer.options ? null : layerIndex;
})
.filter(function(layerIndex) {
return layerIndex !== null;
});
if (invalidLayers.length) {
return new Error('Missing `options` in layergroup config for layers: ' + invalidLayers.join(', '));
}
return false;
}
function templateDefaults(template) {
var templateAuth = _.defaults({}, template.auth || {}, {
method: 'open'
});
return _.defaults({ auth: templateAuth }, template, {
placeholders: {}
});
}
//--------------- PUBLIC API -------------------------------------
// Add a template
//
// NOTE: locks user+template_name or fails
//
// @param owner cartodb username of the template owner
//
// @param template layergroup template, see
// http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps#template-format
//
// @param callback function(err, tpl_id)
// Return template identifier (only valid for given user)
//
o.addTemplate = function(owner, template, callback) {
var self = this;
template = templateDefaults(template);
var invalidError = this._checkInvalidTemplate(template);
if ( invalidError ) {
return callback(invalidError);
}
var templateName = template.name;
var userTemplatesKey = this.key_usr_tpl({ owner:owner });
var limit = this._userTemplateLimit();
Step(
function checkLimit() {
if ( ! limit ) {
return 0;
}
self._redisCmd('HLEN', [ userTemplatesKey ], this);
},
function installTemplateIfDoesNotExist(err, numberOfTemplates) {
if ( err ) {
throw err;
}
if ( limit && numberOfTemplates >= limit ) {
throw new Error("User '" + owner + "' reached limit on number of templates " +
"("+ numberOfTemplates + "/" + limit + ")");
}
self._redisCmd('HSETNX', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
},
function validateInstallation(err, wasSet) {
if ( err ) {
throw err;
}
if ( ! wasSet ) {
throw new Error("Template '" + templateName + "' of user '" + owner + "' already exists");
}
return true;
},
function finish(err) {
if (!err) {
self.emit('add', owner, templateName, template);
}
callback(err, templateName, template);
}
);
};
// Delete a template
//
// @param owner cartodb username of the template owner
//
// @param tpl_id template identifier as returned
// by addTemplate or listTemplates
//
// @param callback function(err)
//
o.delTemplate = function(owner, tpl_id, callback) {
var self = this;
Step(
function deleteTemplate() {
self._redisCmd('HDEL', [ self.key_usr_tpl({ owner:owner }), tpl_id ], this);
},
function handleDeletion(err, deleted) {
if (err) {
throw err;
}
if (!deleted) {
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
}
return true;
},
function finish(err) {
if (!err) {
self.emit('delete', owner, tpl_id);
}
callback(err);
}
);
};
// Update a template
//
// NOTE: locks user+template_name or fails
//
// Also deletes and re-creates associated authentication certificate,
// which in turn deletes all instance signatures
//
// @param owner cartodb username of the template owner
//
// @param tpl_id template identifier as returned by addTemplate
//
// @param template layergroup template, see
// http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps#template-format
//
// @param callback function(err)
//
o.updTemplate = function(owner, tpl_id, template, callback) {
var self = this;
template = templateDefaults(template);
var invalidError = this._checkInvalidTemplate(template);
if ( invalidError ) {
return callback(invalidError);
}
var templateName = template.name;
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 });
Step(
function getExistingTemplate() {
self._redisCmd('HGET', [ userTemplatesKey, tpl_id ], this);
},
function updateTemplate(err, currentTemplate) {
if (err) {
throw err;
}
if (!currentTemplate) {
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
}
self._redisCmd('HSET', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
},
function handleTemplateUpdate(err, didSetNewField) {
if (err) {
throw err;
}
if (didSetNewField) {
console.warn('New template created on update operation');
}
return true;
},
function finish(err) {
if (!err) {
self.emit('update', owner, templateName, template);
}
callback(err, template);
}
);
};
// List user templates
//
// @param owner cartodb username of the templates owner
//
// @param callback function(err, tpl_id_list)
// Returns a list of template identifiers
//
o.listTemplates = function(owner, callback) {
this._redisCmd('HKEYS', [ this.key_usr_tpl({owner:owner}) ], callback);
};
// Get a templates
//
// @param owner cartodb username of the template owner
//
// @param tpl_id template identifier as returned
// by addTemplate or listTemplates
//
// @param callback function(err, template)
// Return full template definition
//
o.getTemplate = function(owner, tpl_id, callback) {
var self = this;
Step(
function getTemplate() {
self._redisCmd('HGET', [ self.key_usr_tpl({owner:owner}), tpl_id ], this);
},
function parseTemplate(err, tpl_val) {
if ( err ) throw err;
return JSON.parse(tpl_val);
},
function finish(err, tpl) {
callback(err, tpl);
}
);
};
o.isAuthorized = function(template, authTokens) {
if (!template) {
return false;
}
authTokens = _.isArray(authTokens) ? authTokens : [authTokens];
var templateAuth = template.auth;
if (!templateAuth) {
return false;
}
if (_.isString(templateAuth) && templateAuth === 'open') {
return true;
}
if (templateAuth.method === 'open') {
return true;
}
if (templateAuth.method === 'token') {
return _.intersection(templateAuth.valid_tokens, authTokens).length > 0;
}
return false;
};
// Perform placeholder substitutions on a template
//
// @param template a template object (will not be modified)
//
// @param params an object containing named subsitution parameters
// Only the ones found in the template's placeholders object
// will be used, with missing ones taking default values.
//
// @returns a layergroup configuration
//
// @throws Error on malformed template or parameter
//
var _reNumber = /^([-+]?[\d\.]?\d+([eE][+-]?\d+)?)$/,
_reCSSColorName = /^[a-zA-Z]+$/,
_reCSSColorVal = /^#[0-9a-fA-F]{3,6}$/;
_replaceVars = function(str, params) {
//return _.template(str, params); // lazy way, possibly dangerous
// Construct regular expressions for each param
Object.keys(params).forEach(function(k) {
str = str.replace(new RegExp("<%=\\s*" + k + "\\s*%>", "g"), params[k]);
});
return str;
};
o.instance = function(template, params) {
var all_params = {};
var phold = template.placeholders || {};
Object.keys(phold).forEach(function(k) {
var val = params.hasOwnProperty(k) ? params[k] : phold[k].default;
var type = phold[k].type;
// properly escape
if ( type === 'sql_literal' ) {
// duplicate any single-quote
val = val.replace(/'/g, "''");
}
else if ( type === 'sql_ident' ) {
// duplicate any double-quote
val = val.replace(/"/g, '""');
}
else if ( type === 'number' ) {
// check it's a number
if ( typeof(val) !== 'number' && ! val.match(_reNumber) ) {
throw new Error("Invalid number value for template parameter '"
+ k + "': " + val);
}
}
else if ( type === 'css_color' ) {
// check it only contains letters or
// starts with # and only contains hexdigits
if ( ! val.match(_reCSSColorName) && ! val.match(_reCSSColorVal) ) {
throw new Error("Invalid css_color value for template parameter '"
+ k + "': " + val);
}
}
else {
// NOTE: should be checked at template create/update time
throw new Error("Invalid placeholder type '" + type + "'");
}
all_params[k] = val;
});
// NOTE: we're deep-cloning the layergroup here
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);
// Anything else ?
}
// extra information about the template
layergroup.template = {
name: template.name,
auth: template.auth
};
return layergroup;
};
// Return a fingerPrint of the object
o.fingerPrint = function(template) {
return crypto.createHash('md5')
.update(JSON.stringify(template))
.digest('hex')
;
};

2429
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": "1.26.2",
"version": "1.5.2",
"description": "A map tile server for CartoDB",
"keywords": [
"cartodb"
@@ -22,30 +22,22 @@
"Sandro Santilli <strk@vizzuality.com>"
],
"dependencies": {
"node-varnish": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0",
"underscore" : "~1.6.0",
"dot": "~1.0.2",
"windshaft": "https://github.com/CartoDB/Windshaft/tarball/0.35.1",
"step": "~0.0.5",
"request": "~2.9.203",
"cartodb-redis": "https://github.com/CartoDB/node-cartodb-redis/tarball/0.11.0",
"cartodb-psql": "https://github.com/CartoDB/node-cartodb-psql/tarball/0.4.0",
"redis-mpool": "https://github.com/CartoDB/node-redis-mpool/tarball/0.3.0",
"lzma": "~1.3.7",
"log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb",
"rollbar": "~0.3.13"
"node-varnish": "0.1.1",
"underscore" : "~1.3.3",
"windshaft" : "~0.14.5",
"step": "0.0.x",
"request": "2.9.202",
"cartodb-redis": "~0.1.0",
"mapnik": "~0.7.22",
"lzma": "~1.2.3"
},
"devDependencies": {
"mocha": "~1.21.4",
"redis": "~0.8.6",
"strftime": "~0.8.2",
"semver": "~1.1.4"
"mocha": "1.14.0",
"redis": "~0.8.3",
"strftime": "~0.6.0",
"semver": "~1.1.0"
},
"scripts": {
"test": "make check"
},
"engines": {
"node": ">=0.8 <0.11",
"npm": ">=1.2.1"
}
}

View File

@@ -5,8 +5,6 @@ OPT_CREATE_PGSQL=yes # create the PostgreSQL test environment
OPT_DROP_REDIS=yes # drop the redis test environment
OPT_DROP_PGSQL=yes # drop the PostgreSQL test environment
export PGAPPNAME=cartodb_tiler_tester
cd $(dirname $0)
BASEDIR=$(pwd)
cd -

View File

@@ -1,211 +0,0 @@
var assert = require('../../support/assert');
var redis = require('redis');
var Step = require('step');
var helper = require(__dirname + '/../../support/test_helper');
var SqlApiEmulator = require(__dirname + '/../../support/SQLAPIEmu.js');
var NamedMapsCacheEntry = require(__dirname + '/../../../lib/cartodb/cache/model/named_maps_entry');
var SurrogateKeysCache = require(__dirname + '/../../../lib/cartodb/cache/surrogate_keys_cache');
var CartodbWindshaft = require(__dirname + '/../../../lib/cartodb/cartodb_windshaft');
var ServerOptions = require(__dirname + '/../../../lib/cartodb/server_options');
var serverOptions = ServerOptions();
suite('templates surrogate keys', function() {
var redisClient,
sqlApiServer,
server;
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'
}
}
]
}
},
expectedBody = { template_id: expectedTemplateId };
suiteSetup(function(done) {
// Enable Varnish purge for tests
serverOptions.varnish_purge_enabled = true;
server = new CartodbWindshaft(serverOptions);
sqlApiServer = new SqlApiEmulator(global.environment.sqlapi.port, done);
redisClient = redis.createClient(global.environment.redis.port);
});
var surrogateKeysCacheInvalidateFn = SurrogateKeysCache.prototype.invalidate;
beforeEach(function(done) {
var postTemplateRequest = {
url: '/tiles/template?api_key=1234',
method: 'POST',
headers: {
host: templateOwner,
'Content-Type': 'application/json'
},
data: JSON.stringify(template)
};
Step(
function postTemplate() {
var next = this;
assert.response(server,
postTemplateRequest,
{
status: 200
},
function(res) {
next(null, res);
}
);
},
function rePostTemplate(err, res) {
if (err) {
throw err;
}
var parsedBody = JSON.parse(res.body);
assert.deepEqual(parsedBody, expectedBody);
return true;
},
function finish(err) {
done(err);
}
);
});
test("update template calls surrogate keys invalidation", function(done) {
var cacheEntryKey;
var surrogateKeysCacheInvalidateMethodInvoked = false;
SurrogateKeysCache.prototype.invalidate = function(cacheEntry) {
cacheEntryKey = cacheEntry.key();
surrogateKeysCacheInvalidateMethodInvoked = true;
};
Step(
function putValidTemplate() {
var updateTemplateRequest = {
url: '/tiles/template/' + expectedTemplateId + '/?api_key=1234',
method: 'PUT',
headers: {
host: templateOwner,
'Content-Type': 'application/json'
},
data: JSON.stringify(template)
};
var next = this;
assert.response(server,
updateTemplateRequest,
{
status: 200
},
function(res) {
next(null, res);
}
);
},
function checkValidUpdate(err, res) {
if (err) {
throw err;
}
var parsedBody = JSON.parse(res.body);
assert.deepEqual(parsedBody, expectedBody);
assert.ok(surrogateKeysCacheInvalidateMethodInvoked);
assert.equal(cacheEntryKey, new NamedMapsCacheEntry(templateOwner, templateName).key());
return null;
},
function finish(err) {
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);
});
});
}
);
});
test("delete template calls surrogate keys invalidation", function(done) {
var cacheEntryKey;
var surrogateKeysCacheInvalidateMethodInvoked = false;
SurrogateKeysCache.prototype.invalidate = function(cacheEntry) {
cacheEntryKey = cacheEntry.key();
surrogateKeysCacheInvalidateMethodInvoked = true;
};
Step(
function putValidTemplate() {
var deleteTemplateRequest = {
url: '/tiles/template/' + expectedTemplateId + '/?api_key=1234',
method: 'DELETE',
headers: {
host: templateOwner,
'Content-Type': 'application/json'
}
};
var next = this;
assert.response(server,
deleteTemplateRequest,
{
status: 204
},
function(res) {
next(null, res);
}
);
},
function checkValidUpdate(err) {
if (err) {
throw err;
}
assert.ok(surrogateKeysCacheInvalidateMethodInvoked);
assert.equal(cacheEntryKey, new NamedMapsCacheEntry(templateOwner, templateName).key());
return null;
},
function finish(err) {
done(err);
}
);
});
afterEach(function(done) {
SurrogateKeysCache.prototype.invalidate = surrogateKeysCacheInvalidateFn;
done();
});
suiteTeardown(function(done) {
sqlApiServer.close(done);
});
});

View File

@@ -1,72 +0,0 @@
var 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);
suite('health checks', function () {
beforeEach(function (done) {
global.environment.health = {
enabled: true,
username: 'localhost',
z: 0,
x: 0,
y: 0
};
done();
});
var healthCheckRequest = {
url: '/health',
method: 'GET',
headers: {
host: 'localhost'
}
};
test('returns 200 and ok=true with enabled configuration', function (done) {
assert.response(server,
healthCheckRequest,
{
status: 200
},
function (res, err) {
console.log(res.body);
assert.ok(!err);
var parsed = JSON.parse(res.body);
assert.ok(parsed.enabled);
assert.ok(parsed.ok);
done();
}
);
});
test('fails for invalid user because it is not in redis', function (done) {
global.environment.health.username = 'invalid';
assert.response(server,
healthCheckRequest,
{
status: 503
},
function (res, err) {
assert.ok(!err);
var parsed = JSON.parse(res.body);
assert.equal(parsed.enabled, true);
assert.equal(parsed.ok, false);
assert.equal(parsed.result.redis.ok, false);
done();
}
);
});
});

View File

@@ -4,39 +4,28 @@ var _ = require('underscore');
var redis = require('redis');
var querystring = require('querystring');
var semver = require('semver');
var mapnik = require('mapnik');
var Step = require('step');
var strftime = require('strftime');
var SQLAPIEmu = require(__dirname + '/../support/SQLAPIEmu.js');
var redis_stats_db = 5;
var helper = require(__dirname + '/../support/test_helper');
require(__dirname + '/../support/test_helper');
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');
serverOptions = ServerOptions();
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
[true, false].forEach(function(cdbQueryTablesFromPostgresEnabledValue) {
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: cdbQueryTablesFromPostgresEnabledValue};
suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function() {
suite('multilayer', function() {
var redis_client = redis.createClient(global.environment.redis.port);
var sqlapi_server;
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_pubuser = global.environment.postgres.user;
var test_database = test_user + '_db';
suiteSetup(function(done){
sqlapi_server = new SQLAPIEmu(global.environment.sqlapi.port, done);
});
@@ -61,7 +50,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
]
};
var expected_token; // = "e34dd7e235138a062f8ba7ad051aa3a7";
var expected_token = "e34dd7e235138a062f8ba7ad051aa3a7";
Step(
function do_post()
{
@@ -86,7 +75,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, 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;
next(null, res);
});
},
@@ -110,42 +99,21 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
// Check X-Cache-Channel
cc = res.headers['x-cache-channel'];
assert.ok(cc);
var dbname = test_database;
var dbname = 'test_cartodb_user_1_db'
assert.equal(cc.substring(0, dbname.length), dbname);
var jsonquery = cc.substring(dbname.length+1);
var sentquery = JSON.parse(jsonquery);
var expectedQuery = [layergroup.layers[0].options.sql, ';', layergroup.layers[1].options.sql].join('');
assert.equal(sentquery.q, 'WITH querytables AS ( SELECT * FROM CDB_QueryTables($windshaft$'
+ expectedQuery
+ '$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[])');
assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$'
+ layergroup.layers[0].options.sql + ';'
+ layergroup.layers[1].options.sql
+ '$windshaft$)');
assert.imageEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.png', IMAGE_EQUALS_HIGHER_TOLERANCE_PER_MIL,
assert.imageEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.png', 2,
function(err, similarity) {
next(err);
});
});
},
// See https://github.com/CartoDB/Windshaft-cartodb/issues/170
function do_get_tile_nosignature(err)
{
if ( err ) throw err;
var next = this;
assert.response(server, {
url: '/tiles/layergroup/localhost@' + expected_token + ':cb0/0/0/0.png',
method: 'GET',
headers: {host: 'localhost' },
encoding: 'binary'
}, {}, 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" ?
assert.ok(msg.match(/permission denied/i), msg);
next(err);
});
},
function do_get_grid_layer0(err)
{
if ( err ) throw err;
@@ -157,7 +125,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'GET'
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
assert.equal(res.headers['content-type'], "text/javascript; charset=utf-8; charset=utf-8");
assert.utfgridEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.layer0.grid.json', 2,
function(err, similarity) {
next(err);
@@ -175,7 +143,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'GET'
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
assert.equal(res.headers['content-type'], "text/javascript; charset=utf-8; charset=utf-8");
assert.utfgridEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.layer1.grid.json', 2,
function(err, similarity) {
next(err);
@@ -188,7 +156,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
redis_client.keys("map_style|test_cartodb_user_1_db|~" + 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) {
@@ -202,134 +170,6 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
});
test("should include serverMedata in the response", function(done) {
global.environment.serverMetadata = { cdn_url : { http:'test', https: 'tests' } }
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 expected_token;
Step(
function do_create_get()
{
var next = this;
assert.response(server, {
url: '/tiles/layergroup?config=' + encodeURIComponent(JSON.stringify(layergroup)),
method: 'GET',
headers: {host: 'localhost'}
}, {}, function(res, err) { next(err, res); });
},
function do_check_create(err, res) {
var parsed = JSON.parse(res.body);
assert.ok(_.isEqual(parsed.cdn_url, global.environment.serverMetadata.cdn_url));
done();
}
)
});
test("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 expected_token;
Step(
function do_create_get()
{
var next = this;
assert.response(server, {
url: '/tiles/layergroup?config=' + encodeURIComponent(JSON.stringify(layergroup)),
method: 'GET',
headers: {host: 'localhost'}
}, {}, function(res, err) { next(err, res); });
},
function do_check_create(err, res) {
if ( err ) throw 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);
});
});
}
);
});
test("get creation has no cache if sql is bogus", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
{ options: {
sql: 'select bogus(0,0) as the_geom_webmercator',
cartocss: '#layer { polygon-fill: red; }',
cartocss_version: '2.0.1'
} }
]
};
assert.response(server, {
url: '/tiles/layergroup?config=' + encodeURIComponent(JSON.stringify(layergroup)),
method: 'GET',
headers: {host: 'localhost'}
}, {}, function(res) {
assert.notEqual(res.statusCode, 200);
helper.checkNoCache(res);
done();
});
});
test("get creation has no cache if cartocss is not valid", 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 { invalid-rule:red; }',
cartocss_version: '2.0.1'
} }
]
};
assert.response(server, {
url: '/tiles/layergroup?config=' + encodeURIComponent(JSON.stringify(layergroup)),
method: 'GET',
headers: {host: 'localhost'}
}, {}, function(res) {
assert.notEqual(res.statusCode, 200);
helper.checkNoCache(res);
done();
});
});
test("layergroup can hold substitution tokens", function(done) {
var layergroup = {
@@ -345,7 +185,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
]
};
var expected_token; // = "6d8e4ad5458e2d25cf0eef38e38717a6";
var expected_token = "6d8e4ad5458e2d25cf0eef38e38717a6";
Step(
function do_post()
{
@@ -369,7 +209,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, 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;
next(null, res);
});
},
@@ -389,22 +229,18 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
// Check X-Cache-Channel
var cc = res.headers['x-cache-channel'];
assert.ok(cc);
var dbname = test_database;
var dbname = 'test_cartodb_user_1_db'
assert.equal(cc.substring(0, dbname.length), dbname);
var jsonquery = cc.substring(dbname.length+1);
var sentquery = JSON.parse(jsonquery);
var expectedQuery = layergroup.layers[0].options.sql
.replace(/!bbox!/g, 'ST_MakeEnvelope(0,0,0,0)')
.replace(/!pixel_width!/g, '1')
.replace(/!pixel_height!/g, '1');
assert.equal(sentquery.q, 'WITH querytables AS ( SELECT * FROM CDB_QueryTables($windshaft$'
+ expectedQuery
+ '$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[])');
assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$'
+ layergroup.layers[0].options.sql
.replace(RegExp('!bbox!', 'g'), 'ST_MakeEnvelope(0,0,0,0)')
.replace(RegExp('!pixel_width!', 'g'), '1')
.replace(RegExp('!pixel_height!', 'g'), '1')
+ '$windshaft$)');
assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', 2,
function(err, similarity) {
next(err);
});
@@ -426,22 +262,18 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
// Check X-Cache-Channel
var cc = res.headers['x-cache-channel'];
assert.ok(cc);
var dbname = test_database;
var dbname = 'test_cartodb_user_1_db'
assert.equal(cc.substring(0, dbname.length), dbname);
var jsonquery = cc.substring(dbname.length+1);
var sentquery = JSON.parse(jsonquery);
var expectedQuery = layergroup.layers[0].options.sql
.replace('!bbox!', 'ST_MakeEnvelope(0,0,0,0)')
.replace('!pixel_width!', '1')
.replace('!pixel_height!', '1');
assert.equal(sentquery.q, 'WITH querytables AS ( SELECT * FROM CDB_QueryTables($windshaft$'
+ expectedQuery
+ '$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[])');
assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$'
+ layergroup.layers[0].options.sql
.replace('!bbox!', 'ST_MakeEnvelope(0,0,0,0)')
.replace('!pixel_width!', '1')
.replace('!pixel_height!', '1')
+ '$windshaft$)');
assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', 2,
function(err, similarity) {
next(err);
});
@@ -458,7 +290,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'GET'
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
assert.equal(res.headers['content-type'], "text/javascript; charset=utf-8; charset=utf-8");
assert.utfgridEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.grid.json', 2,
function(err, similarity) {
next(err);
@@ -476,7 +308,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'GET'
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
assert.equal(res.headers['content-type'], "text/javascript; charset=utf-8; charset=utf-8");
assert.utfgridEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.grid.json', 2,
function(err, similarity) {
next(err);
@@ -489,7 +321,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
redis_client.keys("map_style|test_cartodb_user_1_db|~" + 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) {
@@ -585,7 +417,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
var next = this;
// trip epoch
expected_token = expected_token.split(':')[0];
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
redis_client.keys("map_style|test_cartodb_user_1_db|~" + expected_token, function(err, matches) {
redis_client.del(matches, next);
});
},
@@ -628,35 +460,6 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, 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) {
var layergroup = {
stat_tag: 'random_tag',
version: '1.0.0',
layers: [
{ options: {
sql: 'select bogus(0,0) as the_geom_webmercator',
cartocss: '#layer { polygon-fill:red; }',
cartocss_version: '2.0.1'
} }
]
};
assert.response(server, {
url: '/tiles/layergroup',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) {
assert.equal(res.statusCode, 404, res.statusCode + ": " + res.body);
var parsed = JSON.parse(res.body);
var msg = parsed.errors[0];
assert.ok(msg.match(/bogus.*exist/), msg);
helper.checkNoCache(res);
done();
});
});
test("layergroup with 2 private-table layers", function(done) {
var layergroup = {
@@ -677,7 +480,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
]
};
var expected_token; // = "b4ed64d93a411a59f330ab3d798e4009";
var expected_token = "b4ed64d93a411a59f330ab3d798e4009";
Step(
function do_post()
{
@@ -702,7 +505,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, 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;
next(null, res);
});
},
@@ -722,7 +525,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
// Check X-Cache-Channel
var cc = res.headers['x-cache-channel'];
assert.ok(cc);
var dbname = test_database;
var dbname = 'test_cartodb_user_1_db'
assert.equal(cc.substring(0, dbname.length), dbname);
next(err);
});
@@ -752,7 +555,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'GET'
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
assert.equal(res.headers['content-type'], "text/javascript; charset=utf-8; charset=utf-8");
next(err);
});
},
@@ -766,7 +569,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
headers: {host: 'localhost' },
encoding: 'binary'
}, {}, function(res) {
assert.equal(res.statusCode, 403);
assert.equal(res.statusCode, 401);
var re = RegExp('permission denied');
assert.ok(res.body.match(re), 'No "permission denied" error: ' + res.body);
next(err);
@@ -782,7 +585,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
headers: {host: 'localhost' },
method: 'GET'
}, {}, function(res) {
assert.equal(res.statusCode, 403);
assert.equal(res.statusCode, 401);
var re = RegExp('permission denied');
assert.ok(res.body.match(re), 'No "permission denied" error: ' + res.body);
next(err);
@@ -798,7 +601,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
headers: {host: 'localhost' },
method: 'GET'
}, {}, function(res) {
assert.equal(res.statusCode, 403);
assert.equal(res.statusCode, 401);
var re = RegExp('permission denied');
assert.ok(res.body.match(re), 'No "permission denied" error: ' + res.body);
next(err);
@@ -810,7 +613,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
redis_client.keys("map_style|test_cartodb_user_1_db|~" + 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) {
@@ -823,123 +626,6 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
);
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/152
test("x-cache-channel still works for GETs after tiler restart", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
{ options: {
sql: 'select * from test_table where cartodb_id=1',
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
cartocss_version: '2.1.0',
interactivity: 'cartodb_id'
} }
]
};
var expected_token; // = "b4ed64d93a411a59f330ab3d798e4009";
Step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/tiles/layergroup?map_key=1234',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res, err) { next(err, res); });
},
function check_post(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
var parsedBody = JSON.parse(res.body);
var expectedBody = { layergroupid: expected_token };
// check last modified
var qTables = JSON.stringify({
'q': 'SELECT CDB_QueryTables($windshaft$'
+ layergroup.layers[0].options.sql
+ '$windshaft$)'
});
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];
return null;
},
function do_get0(err)
{
if ( err ) throw err;
var next = this;
assert.response(server, {
url: '/tiles/layergroup/' + expected_token + ':cb0/0/0/0.png?map_key=1234',
method: 'GET',
headers: {host: 'localhost' },
encoding: 'binary'
}, {}, function(res, err) { next(err, res); });
},
function do_check0(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "image/png");
// Check X-Cache-Channel
var cc = res.headers['x-cache-channel'];
assert.ok(cc, "Missing X-Cache-Channel");
var dbname = test_database;
assert.equal(cc.substring(0, dbname.length), dbname);
return null;
},
function do_restart_server(err, res) {
if ( err ) throw err;
// hack simulating restart...
serverOptions = ServerOptions();
server = new CartodbWindshaft(serverOptions);
return null;
},
function do_get1(err)
{
if ( err ) throw err;
var next = this;
assert.response(server, {
url: '/tiles/layergroup/' + expected_token + ':cb0/0/0/0.png?map_key=1234',
method: 'GET',
headers: {host: 'localhost' },
encoding: 'binary'
}, {}, function(res, err) { next(err, res); });
},
function do_check1(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "image/png");
// Check X-Cache-Channel
var cc = res.headers['x-cache-channel'];
assert.ok(cc, "Missing X-Cache-Channel on restart");
var dbname = test_database;
assert.equal(cc.substring(0, dbname.length), dbname);
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);
});
});
}
);
});
// https://github.com/cartodb/Windshaft-cartodb/issues/81
test("invalid text-name in CartoCSS", function(done) {
@@ -1070,7 +756,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.body);
assert.equal(res.headers['content-type'], "image/png");
assert.imageEqualsFile(res.body, windshaft_fixtures + '/test_default_mapnik_point.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
assert.imageEqualsFile(res.body, windshaft_fixtures + '/test_default_mapnik_point.png', 2,
function(err, similarity) {
next(err);
});
@@ -1082,7 +768,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
errors.push(err.message);
console.log("Error: " + err);
}
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
redis_client.keys("map_style|test_cartodb_user_1_db|~" + 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) {
@@ -1157,7 +843,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
if ( err ) errors.push(err.message);
if ( ! expected_token ) return null;
var next = this;
redis_client.keys("map_cfg|" + expected_token, function(err, matches) {
redis_client.keys("map_style|test_cartodb_user_1_db|~" + 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) {
@@ -1177,197 +863,6 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
);
});
// 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){
var long_val = 'pretty';
for (var i=0; i<1024; ++i) long_val += ' long'
long_val += ' string';
var sql = "SELECT ";
for (var 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',
layers: [
{ options: {
sql: sql,
cartocss: '#layer { marker-fill:red; }',
cartocss_version: '2.0.1'
} }
]
};
var errors = [];
var expected_token;
Step(
function do_post()
{
var data = JSON.stringify(layergroup);
assert.ok(data.length > 1024*64);
var next = this;
assert.response(server, {
url: '/tiles/layergroup?api_key=1234',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: data
}, {}, function(res) { next(null, res); });
},
function check_result(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var parsedBody = JSON.parse(res.body);
var token_components = parsedBody.layergroupid.split(':');
expected_token = token_components[0];
var last_request = sqlapi_server.getLastRequest();
assert.equal(last_request.method, 'POST');
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);
}
);
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/133
test("MapConfig with mapnik layer and no cartocss", 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',
interactivity: 'cartodb_id'
} }
]
};
Step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/tiles/layergroup',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res, err) { next(err, res); });
},
function check_post(err, res) {
if ( err ) throw 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));
assert.equal(parsed.errors.length, 1);
var msg = parsed.errors[0];
assert.equal(msg, 'Missing cartocss for layer 0 options');
return null;
},
function finish(err) {
done(err);
}
);
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/167
test("lack of response from sql-api will result in a timeout", function(done) {
var layergroup = {
version: '1.0.0',
layers: [
{ options: {
sql: "select *, 'SQLAPINOANSWER' from test_table",
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
cartocss_version: '2.1.0'
} }
]
};
Step(
function do_post()
{
var next = this;
assert.response(server, {
url: '/tiles/layergroup',
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res, err) { next(err, res); });
},
function check_post(err, res) {
if ( err ) throw 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));
assert.equal(parsed.errors.length, 1);
var msg = parsed.errors[0];
assert.ok(msg, /could not fetch source tables/, msg);
return null;
},
function finish(err) {
done(err);
}
);
});
var layergroupTtlRequest = {
url: '/tiles/layergroup?config=' + encodeURIComponent(JSON.stringify({
version: '1.0.0',
layers: [
{ options: {
sql: 'select * from test_table limit 2',
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
cartocss_version: '2.0.1'
} }
]
})),
method: 'GET',
headers: {host: 'localhost'}
};
var layergroupTtlResponseExpectation = {
status: 200
};
test("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');
done();
}
);
});
test("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');
done();
}
);
});
suiteTeardown(function(done) {
// This test will add map_style records, like
@@ -1388,4 +883,3 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
});
});

View File

@@ -4,32 +4,25 @@ var _ = require('underscore');
var redis = require('redis');
var querystring = require('querystring');
var semver = require('semver');
var mapnik = require('mapnik');
var Step = require('step');
var http = require('http');
var SQLAPIEmu = require(__dirname + '/../support/SQLAPIEmu.js');
var helper = require(__dirname + '/../support/test_helper');
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 20,
IMAGE_EQUALS_ZERO_TOLERANCE_PER_MIL = 0;
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
[true, false].forEach(function(cdbQueryTablesFromPostgresEnabledValue) {
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: cdbQueryTablesFromPostgresEnabledValue};
suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function() {
suite('server', function() {
var redis_client = redis.createClient(global.environment.redis.port);
var sqlapi_server;
var mapnik_version = global.environment.mapnik_version || server.getVersion().mapnik;
var test_database = _.template(global.environment.postgres_auth_user, {user_id:1}) + '_db';
var mapnik_version = global.environment.mapnik_version || mapnik.versions.mapnik;
var test_database = 'test_cartodb_user_1_db';
var default_style;
if ( semver.satisfies(mapnik_version, '<2.1.0') ) {
// 2.0.0 default
@@ -60,25 +53,12 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
// TODO: I guess this should be a 404 instead...
test("get call to server returns 200", function(done){
Step(
function doGet() {
var next = this;
assert.response(server, {
url: '/',
method: 'GET'
},{}, function(res, err) { next(err,res); });
},
function doCheck(err, res) {
if ( err ) throw err;
assert.ok(res.statusCode, 200);
var cc = res.headers['x-cache-channel'];
assert.ok(!cc);
return null;
},
function finish(err) {
done(err);
}
);
assert.response(server, {
url: '/',
method: 'GET'
},{
status: 200
}, function() { done(); });
});
/////////////////////////////////////////////////////////////////////////////////
@@ -127,7 +107,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
}, function(res) {
var parsed = JSON.parse(res.body);
assert.equal(parsed.style, _.template(default_style, {table: 'my_table'}));
assert.equal(parsed.style_version, mapnik_version);
assert.equal(parsed.style_version, mapnik.versions.mapnik);
done();
});
});
@@ -141,10 +121,10 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'GET'
},{
}, function(res) {
assert.equal(res.statusCode, 403, res.statusCode + ':' + res.body);
// FIXME: should be 401 Unauthorized
assert.equal(res.statusCode, 400, res.body);
assert.deepEqual(JSON.parse(res.body),
{error: 'Sorry, you are unauthorized (permission denied)'});
assert.ok(!res.headers.hasOwnProperty('cache-control'));
done();
});
});
@@ -158,11 +138,10 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'GET'
},{
}, function(res) {
// FIXME: should be 403 Forbidden or 404 User Not Found
// FIXME: should be 401 Unauthorized
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
assert.deepEqual(JSON.parse(res.body),
{error:"missing unknown_user's database_name in redis (try CARTODB/script/restore_redis)"});
assert.ok(!res.headers.hasOwnProperty('cache-control'));
done();
});
});
@@ -179,7 +158,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
var parsed = JSON.parse(res.body);
var style = _.template(default_style, {table: 'test_table_private_1'});
assert.equal(parsed.style, style);
assert.equal(parsed.style_version, mapnik_version);
assert.equal(parsed.style_version, mapnik.versions.mapnik);
done();
});
});
@@ -194,8 +173,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
},
function setupRedisBase(err, matches) {
if ( err ) throw err;
assert.equal(matches.length, 0,
'Unexpected redis keys at test start: ' + matches.join("\n"));
assert.equal(matches.length, 0);
redis_client.set(base_key,
JSON.stringify({ style: style }),
this);
@@ -234,12 +212,9 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
url: '/tiles/my_table/style',
method: 'POST'
},{
status: 400,
body: '{"error":"must send style information"}'
}, function(res) {
assert.equal(res.statusCode, 400);
assert.ok(!res.headers.hasOwnProperty('cache-control'));
done();
});
}, function() { done(); });
});
test("post'ing bad style returns 400 with error", function(done){
@@ -326,7 +301,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
headers: {host: 'localhost', 'Content-Type': 'application/x-www-form-urlencoded' },
data: querystring.stringify({style: 'Map { background-color:#aaa; }'})
},{}, function(res) {
// FIXME: should be 403 Forbidden
// FIXME: should be 401 Unauthorized
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
assert.ok(res.body.indexOf('map state cannot be changed by unauthenticated request') != -1, res.body);
@@ -376,7 +351,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
assert.equal(res.statusCode, 200, res.body);
var parsed = JSON.parse(res.body);
assert.equal(parsed.style, style);
assert.equal(parsed.style_version, mapnik_version);
assert.equal(parsed.style_version, mapnik.versions.mapnik);
done();
});
});
@@ -404,7 +379,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
var parsed = JSON.parse(res.body);
// NOTE: no transform expected for the specific style
assert.equal(parsed.style, style);
assert.equal(parsed.style_version, mapnik_version);
assert.equal(parsed.style_version, mapnik.versions.mapnik);
done();
});
});
@@ -424,8 +399,8 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'DELETE',
headers: {host: 'localhost'},
},{}, function(res) {
// FIXME: should be 403 Forbidden
assert.equal(res.statusCode, 400, res.body);
// FIXME: should be 401 Unauthorized
assert.equal(res.statusCode, 500, res.body);
assert.ok(res.body.indexOf('map state cannot be changed by unauthenticated request') != -1, res.body);
// check that the style wasn't really deleted !
assert.response(server, {
@@ -538,7 +513,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
url: '/tiles/test_table_private_1/infowindow',
method: 'GET'
},{}, function(res) {
// FIXME: should be 403 Forbidden
// FIXME: should be 401 Unauthorized
assert.equal(res.statusCode, 500, res.statusCode + ': ' + res.body);
done();
});
@@ -553,7 +528,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'GET'
},{
}, function(res) {
// FIXME: should be 403 Forbidden
// FIXME: should be 401 Unauthorized
assert.equal(res.statusCode, 500, res.statusCode + ': ' + res.body);
assert.deepEqual(JSON.parse(res.body),
{error:"missing unknown_user's database_name in redis (try CARTODB/script/restore_redis)"});
@@ -586,7 +561,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'GET'
},{
status: 200,
headers: { 'Content-Type': 'application/json; charset=utf-8',
headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8',
'X-Cache-Channel': test_database+':gadm4' }
}, function() { done(); });
});
@@ -598,7 +573,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'GET'
},{
status: 200,
headers: { 'Content-Type': 'application/json; charset=utf-8' }
headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8' }
}, function() { done(); });
});
@@ -610,7 +585,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'GET'
},{
status: 200,
headers: { 'Content-Type': 'application/json; charset=utf-8' }
headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8' }
}, function() { done(); });
});
@@ -621,26 +596,8 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
url: '/tiles/test_table_private_1/6/31/24.grid.json',
method: 'GET'
},{}, function(res) {
// 403 Forbidden
assert.equal(res.statusCode, 403, res.statusCode + ': ' + res.body);
done();
});
});
// See http://github.com/CartoDB/Windshaft-cartodb/issues/186
test("get'ing the grid of a private table should fail when unauthenticated (jsonp)",
function(done) {
assert.response(server, {
headers: {host: 'localhost'},
url: '/tiles/test_table_private_1/6/31/24.grid.json?callback=x',
method: 'GET'
},{}, function(res) {
// It's forbidden, but jsonp calls for status = 200
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
// Still, we do NOT want to add caching headers here
// See https://github.com/CartoDB/Windshaft-cartodb/issues/186
assert.ok(!res.headers.hasOwnProperty('cache-control'),
"Unexpected Cache-Control: " + res.headers['cache-control']);
// 401 Unauthorized
assert.equal(res.statusCode, 401, res.statusCode + ': ' + res.body);
done();
});
});
@@ -654,7 +611,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'GET'
},{
}, function(res) {
// FIXME: should be 403 Forbidden
// FIXME: should be 401 Unauthorized
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
assert.deepEqual(JSON.parse(res.body),
{error:"missing unknown_user's database_name in redis (try CARTODB/script/restore_redis)"});
@@ -788,8 +745,8 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'GET'
},{
}, function(res) {
// 403 Forbidden
assert.equal(res.statusCode, 403, res.statusCode + ': ' + res.body);
// 401 Unauthorized
assert.equal(res.statusCode, 401, res.statusCode + ': ' + res.body);
done();
});
});
@@ -805,12 +762,10 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'GET'
},{
}, function(res) {
// FIXME: should be 403 Forbidden
// FIXME: should be 401 Unauthorized
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
assert.deepEqual(JSON.parse(res.body),
{error:"missing unknown_user's database_name in redis (try CARTODB/script/restore_redis)"});
assert.ok(!res.headers.hasOwnProperty('cache-control'),
"Unexpected Cache-Control: " + res.headers['cache-control']);
done();
});
});
@@ -829,11 +784,8 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'GET'
},{
}, function(res) {
// 403 Forbidden
assert.equal(res.statusCode, 403, res.statusCode + ': ' + res.body);
// Failed in 1.6.0 of https://github.com/CartoDB/Windshaft-cartodb/issues/107
assert.ok(!res.headers.hasOwnProperty('cache-control'),
"Unexpected Cache-Control: " + res.headers['cache-control']);
// 401 Unauthorized
assert.equal(res.statusCode, 401, res.statusCode + ': ' + res.body);
done();
});
});
@@ -849,7 +801,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var ct = res.headers['content-type'];
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2,
function(err, similarity) {
if (err) throw err;
done();
@@ -880,7 +832,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body,
'./test/fixtures/test_table_15_16046_12354_styled_black.png',
IMAGE_EQUALS_TOLERANCE_PER_MIL, this);
2, this);
},
function checkImage(err, similarity) {
if (err) throw err;
@@ -896,8 +848,8 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
// See https://github.com/CartoDB/Windshaft-cartodb/issues/89
test("get'ing a tile with a user-specific database password", function(done){
var style = querystring.stringify({style: test_style_black_200, style_version: '2.0.0'});
var backupDBPass = global.environment.postgres_auth_pass;
global.environment.postgres_auth_pass = '<%= user_password %>';
var backupDBPass = global.settings.postgres_auth_pass;
global.settings.postgres_auth_pass = '<%= user_password %>';
Step (
function() {
var next = this;
@@ -917,14 +869,14 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body,
'./test/fixtures/test_table_15_16046_12354_styled_black.png',
IMAGE_EQUALS_TOLERANCE_PER_MIL, this);
2, this);
},
function checkImage(err, similarity) {
if (err) throw err;
return null
},
function finish(err) {
global.environment.postgres_auth_pass = backupDBPass;
global.settings.postgres_auth_pass = backupDBPass;
done(err);
}
);
@@ -941,7 +893,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var ct = res.headers['content-type'];
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2,
function(err, similarity) {
if (err) throw err;
done();
@@ -978,7 +930,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var ct = res.headers['content-type'];
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2,
function(err, similarity) {
next(err);
});
@@ -1018,7 +970,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var ct = res.headers['content-type'];
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body, './test/fixtures/blank.png', IMAGE_EQUALS_ZERO_TOLERANCE_PER_MIL,
assert.imageEqualsFile(res.body, './test/fixtures/blank.png', 0,
function(err, similarity) {
if (err) next(err);
else next();
@@ -1038,7 +990,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var ct = res.headers['content-type'];
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body, './test/fixtures/blank.png', IMAGE_EQUALS_ZERO_TOLERANCE_PER_MIL,
assert.imageEqualsFile(res.body, './test/fixtures/blank.png', 0,
function(err, similarity) {
if (err) next(err);
else next();
@@ -1075,7 +1027,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var ct = res.headers['content-type'];
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2,
function(err, similarity) {
// NOTE: we expect them to be EQUAL here
if (err) { next(err); return; }
@@ -1112,7 +1064,7 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var ct = res.headers['content-type'];
assert.equal(ct, 'image/png');
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2,
function(err, similarity) {
// NOTE: we expect them to be different here
if (err) next();
@@ -1149,8 +1101,8 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
var ct = res.headers['content-type'];
assert.equal(ct, 'image/png');
var cc = res.headers['x-cache-channel'];
assert(cc, 'Missing X-Cache-Channel');
var dbname = test_database;
assert(cc);
var dbname = 'test_cartodb_user_1_db'
assert.equal(cc.substring(0, dbname.length), dbname);
var jsonquery = cc.substring(dbname.length+1);
var sentquery = JSON.parse(jsonquery);
@@ -1164,38 +1116,6 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
);
});
test("passes hostname header to sqlapi", function(done){
var qo = {
sql: "SELECT * from gadm4",
map_key: 1234
};
var sqlapi;
Step(
function sendRequest(err) {
var next = this;
assert.response(server, {
headers: {host: 'localhost'},
url: '/tiles/gadm4/6/31/24.png?' + querystring.stringify(qo),
method: 'GET'
},{}, function(res) { next(null, res); });
},
function checkResponse(err, res) {
if ( err ) throw err;
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
var last_request = sqlapi_server.getLastRequest();
assert.ok(last_request);
var host = last_request.headers['host'];
assert.ok(host);
assert.equal(last_request.method, 'GET');
assert.equal(host, 'localhost.donot_look_this_up');
return null;
},
function finish(err) {
done(err);
}
);
});
test("requests to skip cache on sqlapi error", function(done){
var qo = {
sql: "SELECT g.cartodb_id, g.codineprov, t.the_geom_webmercator "
@@ -1246,20 +1166,6 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
});
});
// See https://github.com/CartoDB/Windshaft-cartodb/issues/115
test.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'},
url: '/tiles/test_table/0/0/0.png?' + style, // madrid
method: 'GET',
encoding: 'binary'
},{}, function(res){
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
done();
});
});
/////////////////////////////////////////////////////////////////////////////////
//
// DELETE CACHE
@@ -1273,7 +1179,6 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'DELETE'
},{}, function(res) {
assert.equal(res.statusCode, 404, res.statusCode + ': ' + res.body);
assert.ok(!res.headers.hasOwnProperty('cache-control'));
done();
});
});
@@ -1303,9 +1208,8 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
url: '/tiles/test_table_private_1/map_metadata',
method: 'GET'
},{}, function(res) {
// FIXME: should be 403 instead
// FIXME: should be 401 instead
assert.equal(res.statusCode, 500, res.statusCode + ': ' + res.body);
assert.ok(!res.headers.hasOwnProperty('cache-control'));
done();
});
});
@@ -1358,7 +1262,6 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
method: 'DELETE'
},{}, function(res) {
assert.equal(res.statusCode, 404, res.statusCode + ': ' + res.body);
assert.ok(!res.headers.hasOwnProperty('cache-control'));
done();
});
});
@@ -1393,4 +1296,3 @@ suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function(
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,81 +1,38 @@
var http = require('http');
var url = require('url');
var _ = require('underscore');
var SQLAPIEmulator = function(port, cb) {
var o = function(port, cb) {
this.queries = [];
var that = this;
this.requests = [];
this.sqlapi_server = http.createServer(function(req,res) {
//console.log("server got request with method " + req.method);
var query;
that.requests.push(req);
if ( req.method == 'GET' ) {
query = url.parse(req.url, true).query;
that.handleQuery(query, res);
}
else if ( req.method == 'POST') {
var data = '';
req.on('data', function(chunk) {
//console.log("GOT Chunk " + chunk);
data += chunk;
});
req.on('end', function() {
//console.log("Data is: "); console.dir(data);
query = JSON.parse(data);
//console.log("handleQuery is " + that.handleQuery);
that.handleQuery(query, res);
});
}
else {
that.handleQuery('SQLAPIEmu does not support method' + req.method, res);
}
var query = url.parse(req.url, true).query;
that.queries.push(query);
if ( query.q.match('SQLAPIERROR') ) {
res.statusCode = 400;
res.write(JSON.stringify({'error':'Some error occurred'}));
} else if ( query.q.match('EPOCH.* as max') ) {
// This is the structure of the known query sent by tiler
var row = {
'max': 1234567890.123
};
res.write(JSON.stringify({rows: [ row ]}));
} else {
var qs = JSON.stringify(query);
var row = {
// This is the structure of the known query sent by tiler
'cdb_querytables': '{' + qs + '}',
'max': qs
};
res.write(JSON.stringify({rows: [ row ]}));
}
res.end();
}).listen(port, cb);
};
SQLAPIEmulator.prototype.handleQuery = function(query, res) {
this.queries.push(query);
if ( query.q.match('SQLAPIERROR') ) {
res.statusCode = 400;
res.write(JSON.stringify({'error':'Some error occurred'}));
} else if ( query.q.match('SQLAPINOANSWER') ) {
console.log("SQLAPIEmulator will never respond, on request");
return;
} else if (query.q.match('tablenames')) {
var tableNames = JSON.stringify(query);
res.write(queryResult({tablenames: '{' + tableNames + '}', max: 1234567890.123}));
} else if ( query.q.match('EPOCH.* as max') ) {
// This is the structure of the known query sent by tiler
res.write(queryResult({max: 1234567890.123}));
} else {
if ( query.q.match('_private_') && query.api_key === undefined) {
res.statusCode = 403;
res.write(JSON.stringify({'error':'forbidden: ' + JSON.stringify(query)}));
} else {
var qs = JSON.stringify(query);
res.write(queryResult({cdb_querytables: '{' + qs + '}', max: 1234567890.123}));
}
}
res.end();
};
SQLAPIEmulator.prototype.close = function(cb) {
o.prototype.close = function(cb) {
this.sqlapi_server.close(cb);
};
SQLAPIEmulator.prototype.getLastRequest = function() {
return this.requests.pop();
};
function queryResult(row) {
return JSON.stringify({
rows: [row]
});
}
module.exports = SQLAPIEmulator;
module.exports = o;

View File

@@ -1,11 +1,10 @@
// Cribbed from the ever prolific Konstantin Kaefer
// https://github.com/mapbox/tilelive-mapnik/blob/master/test/support/assert.js
var exec = require('child_process').exec,
fs = require('fs'),
http = require('http'),
path = require('path'),
util = require('util');
var fs = require('fs');
var http = require('http');
var path = require('path');
var exec = require('child_process').exec;
var assert = module.exports = exports = require('assert');
@@ -67,51 +66,35 @@ assert.utfgridEqualsFile = function(buffer, file_b, tolerance, callback) {
callback(err);
};
/**
* Takes an image data as an input and an image path and compare them using ImageMagick fuzz algorithm, if case the
* similarity is not within the tolerance limit it will callback with an error.
*
* @param buffer The image data to compare from
* @param {string} referenceImageRelativeFilePath The relative file to compare against
* @param {number} tolerance tolerated mean color distance, as a per mil (‰)
* @param {function} callback Will call to home with null in case there is no error, otherwise with the error itself
* @see FUZZY in http://www.imagemagick.org/script/command-line-options.php#metric
*/
assert.imageEqualsFile = function(buffer, referenceImageRelativeFilePath, tolerance, callback) {
//
// @param tol tolerated color distance as a percent over max channel value
// by default this is zero. For meaningful values, see
// http://www.imagemagick.org/script/command-line-options.php#metric
//
assert.imageEqualsFile = function(buffer, file_b, tol, callback) {
if (!callback) callback = function(err) { if (err) throw err; };
var referenceImageFilePath = path.resolve(referenceImageRelativeFilePath),
testImageFilePath = '/tmp/windshaft-test-image-' + (Math.random() * 1e16); // TODO: make predictable
var err = fs.writeFileSync(testImageFilePath, buffer, 'binary');
file_b = path.resolve(file_b);
var file_a = '/tmp/windshaft-test-image-test.png'; // + (Math.random() * 1e16); // TODO: make predictable
var err = fs.writeFileSync(file_a, buffer, 'binary');
if (err) throw err;
var imageMagickCmd = util.format(
'compare -metric fuzz "%s" "%s" /dev/null',
testImageFilePath, referenceImageFilePath
);
exec(imageMagickCmd, function(err, stdout, stderr) {
var fuzz = tol + '%';
exec('compare -fuzz ' + fuzz + ' -metric AE "' + file_a + '" "' +
file_b + '" /dev/null', function(err, stdout, stderr) {
if (err) {
fs.unlinkSync(testImageFilePath);
fs.unlinkSync(file_a);
callback(err);
} else {
stderr = stderr.trim();
var metrics = stderr.match(/([0-9]*) \((.*)\)/);
if ( ! metrics ) {
callback(new Error("No match for " + stderr));
return;
}
var similarity = parseFloat(metrics[2]),
tolerancePerMil = (tolerance / 1000);
if (similarity > tolerancePerMil) {
err = new Error(util.format(
'Images %s and %s are not equal (got %d similarity, expected %d)',
testImageFilePath, referenceImageFilePath, similarity, tolerancePerMil)
);
err.similarity = similarity;
callback(err);
var similarity = parseFloat(stderr);
if ( similarity > 0 ) {
var err = new Error('Images not equal(' + similarity + '): ' +
file_a + ' ' + file_b);
err.similarity = similarity;
callback(err);
} else {
fs.unlinkSync(testImageFilePath);
callback(null);
fs.unlinkSync(file_a);
callback(null);
}
}
});
@@ -226,8 +209,6 @@ assert.response = function(server, req, res, msg){
response.on('end', function(){
if (timer) clearTimeout(timer);
check();
// Assert response body
if (res.body !== undefined) {
var eql = res.body instanceof RegExp
@@ -273,6 +254,7 @@ assert.response = function(server, req, res, msg){
// Callback
callback(response);
check();
});
});

View File

@@ -76,35 +76,27 @@ if test x"$PREPARE_PGSQL" = xyes; then
sed "s/:PUBLICPASS/${PUBLICPASS}/" |
sed "s/:TESTUSER/${TESTUSER}/" |
sed "s/:TESTPASS/${TESTPASS}/" |
psql -v ON_ERROR_STOP=1 ${TEST_DB} || exit 1
psql ${TEST_DB}
fi
if test x"$PREPARE_REDIS" = xyes; then
echo "preparing redis..."
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 5
HMSET rails:users:localhost id ${TESTUSERID} \
database_name "${TEST_DB}" \
database_host localhost \
map_key 1234
SADD rails:users:localhost:map_key 1235
EOF
echo "HSET rails:users:localhost id ${TESTUSERID}" | redis-cli -p ${REDIS_PORT} -n 5
echo 'HSET rails:users:localhost database_name "'"${TEST_DB}"'"' | redis-cli -p ${REDIS_PORT} -n 5
echo "HSET rails:users:localhost map_key 1234" | redis-cli -p ${REDIS_PORT} -n 5
echo "SADD rails:users:localhost:map_key 1235" | redis-cli -p ${REDIS_PORT} -n 5
# A user configured as with cartodb-2.5.0+
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 5
HMSET rails:users:cartodb250user id ${TESTUSERID} \
database_name "${TEST_DB}" \
database_host "localhost" \
database_password "${TESTPASS}" \
map_key 4321
EOF
echo "HSET rails:users:cartodb250user id ${TESTUSERID}" | redis-cli -p ${REDIS_PORT} -n 5
echo 'HSET rails:users:cartodb250user database_name "'${TEST_DB}'"' | redis-cli -p ${REDIS_PORT} -n 5
echo 'HSET rails:users:cartodb250user database_host "localhost"' | redis-cli -p ${REDIS_PORT} -n 5
echo 'HSET rails:users:cartodb250user database_password "'${TESTPASS}'"' | redis-cli -p ${REDIS_PORT} -n 5
echo "HSET rails:users:cartodb250user map_key 4321" | redis-cli -p ${REDIS_PORT} -n 5
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 0
HSET rails:${TEST_DB}:my_table infowindow "this, that, the other"
HSET rails:${TEST_DB}:test_table_private_1 privacy "0"
EOF
echo 'HSET rails:'"${TEST_DB}"':my_table infowindow "this, that, the other"' | redis-cli -p ${REDIS_PORT} -n 0
echo 'HSET rails:'"${TEST_DB}"':test_table_private_1 privacy "0"' | redis-cli -p ${REDIS_PORT} -n 0
fi

View File

@@ -174,6 +174,5 @@ CREATE TABLE test_table_private_1 (
CONSTRAINT enforce_srid_the_geom CHECK ((st_srid(the_geom) = 4326)),
CONSTRAINT enforce_srid_the_geom_webmercator CHECK ((st_srid(the_geom_webmercator) = 3857))
);
INSERT INTO test_table_private_1 SELECT * from test_table;
GRANT ALL ON TABLE test_table_private_1 TO :TESTUSER;

View File

@@ -6,19 +6,17 @@
*/
var _ = require('underscore');
var assert = require('assert');
var LZMA = require('lzma').LZMA;
var lzmaWorker = new LZMA();
var LZMA = require('lzma/lzma_worker.js').LZMA;
// set environment specific variables
global.settings = require(__dirname + '/../../config/settings');
global.environment = require(__dirname + '/../../config/environments/test');
process.env.NODE_ENV = 'test';
_.extend(global.settings, global.environment);
// Utility function to compress & encode LZMA
function lzma_compress_to_base64(payload, mode, callback) {
lzmaWorker.compress(payload, mode,
LZMA.compress(payload, mode,
function(ints) {
ints = ints.map(function(c) { return String.fromCharCode(c + 128) }).join('')
var base64 = new Buffer(ints, 'binary').toString('base64');
@@ -30,36 +28,7 @@ function lzma_compress_to_base64(payload, mode, callback) {
);
}
// Check that the response headers do not request caching
// Throws on failure
function checkNoCache(res) {
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
assert.ok(!res.headers.hasOwnProperty('cache-control')); // is this correct ?
assert.ok(!res.headers.hasOwnProperty('last-modified')); // is this correct ?
}
/**
* Check that the response headers do not request caching
* @see checkNoCache
* @param res
*/
function checkCache(res) {
assert.ok(res.headers.hasOwnProperty('x-cache-channel'));
assert.ok(res.headers.hasOwnProperty('cache-control'));
assert.ok(res.headers.hasOwnProperty('last-modified'));
}
function checkSurrogateKey(res, expectedKey) {
assert.ok(res.headers.hasOwnProperty('surrogate-key'));
assert.equal(res.headers['surrogate-key'], expectedKey);
}
module.exports = {
lzma_compress_to_base64: lzma_compress_to_base64,
checkNoCache: checkNoCache,
checkSurrogateKey: checkSurrogateKey,
checkCache: checkCache
};
lzma_compress_to_base64: lzma_compress_to_base64
}

View File

@@ -1,29 +0,0 @@
var assert = require('assert');
var _ = require('underscore');
var NamedMapsCacheEntry = require('../../../../../lib/cartodb/cache/model/named_maps_entry');
suite('cache named_maps_entry', function() {
var namedMapOwner = 'foo',
namedMapName = 'wadus_name',
namedMapsCacheEntry = new NamedMapsCacheEntry(namedMapOwner, namedMapName),
entryKey = namedMapsCacheEntry.key();
test('key is a string', function() {
assert.ok(_.isString(entryKey));
});
test('key is 8 chars length', function() {
assert.equal(entryKey.length, 8);
var entryKeyParts = entryKey.split(':');
assert.equal(entryKeyParts.length, 2);
assert.equal(entryKeyParts[0], 'n');
});
test('key is name spaced for named maps', function() {
var entryKeyParts = entryKey.split(':');
assert.equal(entryKeyParts.length, 2);
assert.equal(entryKeyParts[0], 'n');
});
});

View File

@@ -7,12 +7,7 @@ var assert = require('assert')
suite('req2params', function() {
// configure redis pool instance to use in tests
var opts = require('../../../lib/cartodb/server_options')();
var test_user = _.template(global.environment.postgres_auth_user, {user_id:1});
var test_pubuser = global.environment.postgres.user;
var test_database = test_user + '_db';
var opts = require('../../../lib/cartodb/server_options');
test('can be found in server_options', function(){
assert.ok(_.isFunction(opts.req2params));
@@ -20,45 +15,49 @@ suite('req2params', function() {
test('cleans up request', function(done){
opts.req2params({headers: { host:'localhost' }, query: {dbuser:'hacker',dbname:'secret'}}, function(err, req) {
if ( err ) { done(err); return; }
if ( err ) { console.log(err); throw new Error(err); }
assert.ok(_.isObject(req.query), 'request has query');
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
assert.ok(req.hasOwnProperty('params'), 'request has params');
assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity');
assert.equal(req.params.dbname, test_database, 'could forge dbname: '+ req.params.dbname);
assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')');
assert.equal(req.params.dbname, 'test_cartodb_user_1_db', 'could forge dbname: '+ req.params.dbname);
assert.ok(!req.params.hasOwnProperty('dbuser'), 'could inject dbuser ('+req.params.dbuser+')');
done();
});
});
test('sets dbname from redis metadata', function(done){
opts.req2params({headers: { host:'localhost' }, query: {} }, function(err, req) {
if ( err ) { done(err); return; }
if ( err ) { console.log(err); throw new Error(err); }
//console.dir(req);
assert.ok(_.isObject(req.query), 'request has query');
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
assert.ok(req.hasOwnProperty('params'), 'request has params');
assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity');
assert.equal(req.params.dbname, test_database);
assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')');
// database_name for user "localhost" (see test/support/prepare_db.sh)
assert.equal(req.params.dbname, 'test_cartodb_user_1_db');
// unauthenticated request gets no dbuser
assert.ok(!req.params.hasOwnProperty('dbuser'), 'could inject dbuser ('+req.params.dbuser+')');
done();
});
});
test('sets also dbuser for authenticated requests', function(done){
opts.req2params({headers: { host:'localhost' }, query: {map_key: '1234'} }, function(err, req) {
if ( err ) { done(err); return; }
if ( err ) { console.log(err); throw new Error(err); }
//console.dir(req);
assert.ok(_.isObject(req.query), 'request has query');
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
assert.ok(req.hasOwnProperty('params'), 'request has params');
assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity');
assert.equal(req.params.dbname, test_database);
assert.equal(req.params.dbuser, test_user);
// database_name for user "localhost" (see test/support/prepare_db.sh)
assert.equal(req.params.dbname, 'test_cartodb_user_1_db');
// id for user "localhost" (see test/support/prepare_db.sh)
assert.equal(req.params.dbuser, 'test_cartodb_user_1');
opts.req2params({headers: { host:'localhost' }, query: {map_key: '1235'} }, function(err, req) {
// wrong key resets params to no user
assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')');
assert.ok(!req.params.hasOwnProperty('dbuser'), 'could inject dbuser ('+req.params.dbuser+')');
done();
});
});
@@ -71,8 +70,7 @@ suite('req2params', function() {
cache_buster: 5
};
test_helper.lzma_compress_to_base64(JSON.stringify(qo), 1, function(err, data) {
opts.req2params({ headers: { host:'localhost' }, query: { non_included: 'toberemoved', api_key: 'test', style: 'override', lzma: data }}, function(err, req) {
if ( err ) { done(err); return; }
opts.req2params({ query: { non_included: 'toberemoved', api_key: 'test', style: 'override', lzma: data }}, function(err, req) {
var query = req.params
assert.equal(qo.style, query.style)
assert.equal(qo.style_version, query.style_version)

View File

@@ -1,525 +0,0 @@
var assert = require('assert')
//, _ = require('underscore')
, RedisPool = require('redis-mpool')
, TemplateMaps = require('../../../lib/cartodb/template_maps.js')
, test_helper = require('../../support/test_helper')
, Step = require('step')
, _ = require('underscore')
, tests = module.exports = {};
suite('template_maps', function() {
// configure redis pool instance to use in tests
var redis_pool = RedisPool(global.environment.redis);
var wadusLayer = {
options: {
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
cartocss: '#layer { marker-fill:blue; }',
cartocss_version: '2.3.0'
}
};
var validTemplate = {
version:'0.0.1',
name: 'first',
auth: {},
layergroup: {
layers: [
wadusLayer
]
}
};
var owner = 'me';
test('does not accept template with unsupported version', function(done) {
var tmap = new TemplateMaps(redis_pool);
assert.ok(tmap);
var tpl = { version:'6.6.6',
name:'k', auth: {}, layergroup: {layers:[wadusLayer]} };
Step(
function() {
tmap.addTemplate('me', tpl, this);
},
function checkFailed(err) {
assert.ok(err);
assert.ok(err.message.match(/unsupported.*version/i), err);
return null;
},
function finish(err) {
done(err);
}
);
});
test('does not accept template with missing name', function(done) {
var tmap = new TemplateMaps(redis_pool);
assert.ok(tmap);
var tpl = { version:'0.0.1',
auth: {}, layergroup: {layers:[wadusLayer]} };
Step(
function() {
tmap.addTemplate('me', tpl, this);
},
function checkFailed(err) {
assert.ok(err);
assert.ok(err.message.match(/missing.*name/i), err);
return null;
},
function finish(err) {
done(err);
}
);
});
test('does not accept template with invalid name', function(done) {
var tmap = new TemplateMaps(redis_pool);
assert.ok(tmap);
var tpl = { version:'0.0.1',
auth: {}, layergroup: {layers:[wadusLayer]} };
var invalidnames = [ "ab|", "a b", "a@b", "1ab", "_x", "", " x", "x " ];
var testNext = function() {
if ( ! invalidnames.length ) { done(); return; }
var n = invalidnames.pop();
tpl.name = n;
tmap.addTemplate('me', tpl, function(err) {
if ( ! err ) {
done(new Error("Unexpected success with invalid name '" + n + "'"));
}
else if ( ! err.message.match(/template.*name/i) ) {
done(new Error("Unexpected error message with invalid name '" + n
+ "': " + err));
}
else {
testNext();
}
});
};
testNext();
});
test('does not accept template with invalid placeholder name', function(done) {
var tmap = new TemplateMaps(redis_pool);
assert.ok(tmap);
var tpl = { version:'0.0.1',
name: "valid", placeholders: {},
auth: {}, layergroup: {layers:[wadusLayer]} };
var invalidnames = [ "ab|", "a b", "a@b", "1ab", "_x", "", " x", "x " ];
var testNext = function() {
if ( ! invalidnames.length ) { done(); return; }
var n = invalidnames.pop();
tpl.placeholders = {};
tpl.placeholders[n] = { type:'number', default:1 };
tmap.addTemplate('me', tpl, function(err) {
if ( ! err ) {
done(new Error("Unexpected success with invalid name '" + n + "'"));
}
else if ( ! err.message.match(/invalid.*name/i) ) {
done(new Error("Unexpected error message with invalid name '" + n
+ "': " + err));
}
else {
testNext();
}
});
};
testNext();
});
test('does not accept template with missing placeholder default', function(done) {
var tmap = new TemplateMaps(redis_pool);
assert.ok(tmap);
var tpl = { version:'0.0.1',
name: "valid", placeholders: { v: {} },
auth: {}, layergroup: {layers:[wadusLayer]} };
tmap.addTemplate('me', tpl, function(err) {
if ( ! err ) {
done(new Error("Unexpected success with missing placeholder default"));
}
else if ( ! err.message.match(/missing default/i) ) {
done(new Error("Unexpected error message with missing placeholder default: "
+ err));
}
else {
done();
}
});
});
test('does not accept template with missing placeholder type', function(done) {
var tmap = new TemplateMaps(redis_pool);
assert.ok(tmap);
var tpl = { version:'0.0.1',
name: "valid", placeholders: { v: { default:1 } },
auth: {}, layergroup: {layers:[wadusLayer]} };
tmap.addTemplate('me', tpl, function(err) {
if ( ! err ) {
done(new Error("Unexpected success with missing placeholder type"));
}
else if ( ! err.message.match(/missing type/i) ) {
done(new Error("Unexpected error message with missing placeholder default: "
+ err));
}
else {
done();
}
});
});
// See http://github.com/CartoDB/Windshaft-cartodb/issues/128
test('does not accept template with invalid token auth (undefined tokens)',
function(done) {
var tmap = new TemplateMaps(redis_pool);
assert.ok(tmap);
var tpl = { version:'0.0.1',
name: "invalid_auth1", placeholders: { },
auth: { method: 'token' }, layergroup: {layers:[wadusLayer]} };
tmap.addTemplate('me', tpl, function(err) {
if ( ! err ) {
done(new Error("Unexpected success with invalid token auth (undefined tokens)"));
}
else if ( ! err.message.match(/invalid 'token' authentication/i) ) {
done(new Error("Unexpected error message with invalid token auth (undefined tokens): "
+ err));
}
else {
done();
}
});
});
test('add, get and delete a valid template', function(done) {
var tmap = new TemplateMaps(redis_pool);
assert.ok(tmap);
var expected_failure = false;
var tpl_id;
var tpl = { version:'0.0.1',
name: 'first', auth: {}, layergroup: {layers:[wadusLayer]} };
Step(
function() {
tmap.addTemplate('me', tpl, this);
},
function addOmonimousTemplate(err, id) {
if ( err ) throw err;
tpl_id = id;
assert.equal(tpl_id, 'first');
expected_failure = true;
// should fail, as it already exists
tmap.addTemplate('me', tpl, this);
},
function getTemplate(err) {
if ( ! expected_failure && err ) throw err;
assert.ok(err);
assert.ok(err.message.match(/already exists/i), err);
tmap.getTemplate('me', tpl_id, this);
},
function delTemplate(err, got_tpl) {
if ( err ) throw err;
assert.deepEqual(got_tpl, _.extend({}, tpl, {auth: {method: 'open'}, placeholders: {}}));
tmap.delTemplate('me', tpl_id, this);
},
function finish(err) {
done(err);
}
);
});
test('add multiple templates, list them', function(done) {
var tmap = new TemplateMaps(redis_pool);
assert.ok(tmap);
var expected_failure = false;
var tpl1 = { version:'0.0.1', name: 'first', auth: {}, layergroup: {layers:[wadusLayer]} };
var tpl1_id;
var tpl2 = { version:'0.0.1', name: 'second', auth: {}, layergroup: {layers:[wadusLayer]} };
var tpl2_id;
Step(
function addTemplate1() {
tmap.addTemplate('me', tpl1, this);
},
function addTemplate2(err, id) {
if ( err ) throw err;
tpl1_id = id;
tmap.addTemplate('me', tpl2, this);
},
function listTemplates(err, id) {
if ( err ) throw err;
tpl2_id = id;
tmap.listTemplates('me', this);
},
function checkTemplates(err, ids) {
if ( err ) throw err;
assert.equal(ids.length, 2);
assert.ok(ids.indexOf(tpl1_id) != -1, ids.join(','));
assert.ok(ids.indexOf(tpl2_id) != -1, ids.join(','));
return null;
},
function delTemplate1(err) {
if ( tpl1_id ) {
var next = this;
tmap.delTemplate('me', tpl1_id, function(e) {
if ( err || e ) next(new Error(err + '; ' + e));
else next();
});
} else {
if ( err ) throw err;
return null;
}
},
function delTemplate2(err) {
if ( tpl2_id ) {
var next = this;
tmap.delTemplate('me', tpl2_id, function(e) {
if ( err || e ) next(new Error(err + '; ' + e));
else next();
});
} else {
if ( err ) throw err;
return null;
}
},
function finish(err) {
done(err);
}
);
});
test('update templates', function(done) {
var tmap = new TemplateMaps(redis_pool);
assert.ok(tmap);
var expected_failure = false;
var owner = 'me';
var tpl = { version:'0.0.1',
name: 'first',
auth: { method: 'open' },
layergroup: {layers:[wadusLayer]}
};
var tpl_id;
Step(
function addTemplate() {
tmap.addTemplate(owner, tpl, this);
},
// Updating template name should fail
function updateTemplateName(err, id) {
if ( err ) throw err;
tpl_id = id;
expected_failure = true;
tpl.name = 'second';
tmap.updTemplate(owner, tpl_id, tpl, this);
},
function updateTemplateAuth(err) {
if ( err && ! expected_failure) throw err;
expected_failure = false;
assert.ok(err);
tpl.name = 'first';
tpl.auth.method = 'token';
tpl.auth.valid_tokens = [ 'tok1' ];
tmap.updTemplate(owner, tpl_id, tpl, this);
},
function updateTemplateWithInvalid(err) {
if ( err ) throw err;
tpl.version = '999.999.999';
expected_failure = true;
tmap.updTemplate(owner, tpl_id, tpl, this);
},
function updateUnexistentTemplate(err) {
if ( err && ! expected_failure) throw err;
expected_failure = false;
assert.ok(err);
assert.ok(err.message.match(/unsupported.*version/i), err);
tpl.version = '0.0.1';
expected_failure = true;
tmap.updTemplate(owner, 'unexistent', tpl, this);
},
function delTemplate(err) {
if ( err && ! expected_failure) throw err;
expected_failure = false;
assert.ok(err);
assert.ok(err.message.match(/cannot update name/i), err);
tmap.delTemplate(owner, tpl_id, this);
},
function finish(err) {
done(err);
}
);
});
test('instanciate templates', function() {
var tmap = new TemplateMaps(redis_pool);
assert.ok(tmap);
var tpl1 = {
version: '0.0.1',
name: 'acceptance1',
auth: { method: 'open' },
placeholders: {
fill: { type: "css_color", default: "red" },
color: { type: "css_color", default: "#a0fF9A" },
name: { type: "sql_literal", default: "test" },
zoom: { type: "number", default: "0" },
test_number: { type: "number", default: 23 },
},
layergroup: {
version: '1.0.0',
global_cartocss_version: '2.0.2',
layers: [
{ options: {
sql: "select '<%=name %>' || id, g from t",
cartocss: '#layer { marker-fill:<%= fill %>; marker-width: <%=test_number %>; }'
} },
{ options: {
sql: "select fun('<%= name%>') g from x",
cartocss: '#layer { line-color:<%= color %>; marker-fill:<%= color %>; }'
} },
{ options: {
sql: "select g from x",
cartocss: '#layer[zoom=<%=zoom%>] { }'
} }
]
}
};
var inst = tmap.instance(tpl1, {});
var lyr = inst.layers[0].options;
assert.equal(lyr.sql, "select 'test' || id, g from t");
assert.equal(lyr.cartocss, '#layer { marker-fill:red; marker-width: 23; }');
lyr = inst.layers[1].options;
assert.equal(lyr.sql, "select fun('test') g from x");
assert.equal(lyr.cartocss, '#layer { line-color:#a0fF9A; marker-fill:#a0fF9A; }');
inst = tmap.instance(tpl1, {color:'yellow', name:"it's dangerous"});
lyr = inst.layers[0].options;
assert.equal(lyr.sql, "select 'it''s dangerous' || id, g from t");
assert.equal(lyr.cartocss, '#layer { marker-fill:red; marker-width: 23; }');
lyr = inst.layers[1].options;
assert.equal(lyr.sql, "select fun('it''s dangerous') g from x");
assert.equal(lyr.cartocss, '#layer { line-color:yellow; marker-fill:yellow; }');
// Invalid css_color
var err = null;
try { inst = tmap.instance(tpl1, {color:'##ff00ff'}); }
catch (e) { err = e; }
assert.ok(err);
assert.ok(err.message.match(/invalid css_color/i), err);
// Invalid css_color 2 (too few digits)
var err = null;
try { inst = tmap.instance(tpl1, {color:'#ff'}); }
catch (e) { err = e; }
assert.ok(err);
assert.ok(err.message.match(/invalid css_color/i), err);
// Invalid css_color 3 (too many digits)
var err = null;
try { inst = tmap.instance(tpl1, {color:'#1234567'}); }
catch (e) { err = e; }
assert.ok(err);
assert.ok(err.message.match(/invalid css_color/i), err);
// Invalid number
var err = null;
try { inst = tmap.instance(tpl1, {zoom:'#'}); }
catch (e) { err = e; }
assert.ok(err);
assert.ok(err.message.match(/invalid number/i), err);
// Invalid number 2
var err = null;
try { inst = tmap.instance(tpl1, {zoom:'23e'}); }
catch (e) { err = e; }
assert.ok(err);
assert.ok(err.message.match(/invalid number/i), err);
// Valid number
var err = null;
try { inst = tmap.instance(tpl1, {zoom:'-.23e10'}); }
catch (e) { err = e; }
assert.ok(!err);
});
// Can set a limit on the number of user templates
test('can limit number of user templates', function(done) {
var tmap = new TemplateMaps(redis_pool, {
max_user_templates: 2
});
assert.ok(tmap);
var tpl = { version:'0.0.1', auth: {}, layergroup: {layers:[wadusLayer]} };
var expectErr = false;
var idMe = [];
var idYou = [];
Step(
function oneForMe() {
tpl.name = 'oneForMe';
tmap.addTemplate('me', tpl, this);
},
function twoForMe(err, id) {
if ( err ) throw err;
assert.ok(id);
idMe.push(id);
tpl.name = 'twoForMe';
tmap.addTemplate('me', tpl, this);
},
function threeForMe(err, id) {
if ( err ) throw err;
assert.ok(id);
idMe.push(id);
tpl.name = 'threeForMe';
expectErr = true;
tmap.addTemplate('me', tpl, this);
},
function errForMe(err, id) {
if ( err && ! expectErr ) throw err;
expectErr = false;
assert.ok(err);
assert.ok(err.message.match(/limit.*template/), err);
return null;
},
function delOneMe(err) {
if ( err ) throw err;
tmap.delTemplate('me', idMe.shift(), this);
},
function threeForMeRetry(err) {
if ( err ) throw err;
tpl.name = 'threeForMe';
tmap.addTemplate('me', tpl, this);
},
function oneForYou(err, id) {
if ( err ) throw err;
assert.ok(id);
idMe.push(id);
tpl.name = 'oneForYou';
tmap.addTemplate('you', tpl, this);
},
function twoForYou(err, id) {
if ( err ) throw err;
assert.ok(id);
idYou.push(id);
tpl.name = 'twoForYou';
tmap.addTemplate('you', tpl, this);
},
function threeForYou(err, id) {
if ( err ) throw err;
assert.ok(id);
idYou.push(id);
tpl.name = 'threeForYou';
expectErr = true;
tmap.addTemplate('you', tpl, this);
},
function errForYou(err, id) {
if ( err && ! expectErr ) throw err;
expectErr = false;
assert.ok(err);
assert.ok(err.message.match(/limit.*template/), err);
return null;
},
function finish(err) {
// TODO: delete all templates
done(err);
}
);
});
});

View File

@@ -1,105 +0,0 @@
var assert = require('assert');
var RedisPool = require('redis-mpool');
var TemplateMaps = require('../../../lib/cartodb/template_maps');
var test_helper = require('../../support/test_helper');
var Step = require('step');
var tests = module.exports = {};
suite('template_maps_auth', function() {
// configure redis pool instance to use in tests
var redisPool = new RedisPool(global.environment.redis),
templateMaps = new TemplateMaps(redisPool, {max_user_templates: 1000});
function makeTemplate(method, validTokens) {
var template = {
name: 'wadus_template',
auth: {
method: method
}
};
if (method === 'token') {
template.auth.valid_tokens = validTokens || [];
}
return template;
}
var methodToken = 'token',
methodOpen = 'open';
var tokenFoo = 'foo',
tokenBar = 'bar';
var authorizationTestScenarios = [
{
desc: 'open method is always authorized',
template: makeTemplate(methodOpen),
token: undefined,
expected: true
},
{
desc: 'token method is authorized for valid token',
template: makeTemplate(methodToken, [tokenFoo]),
token: tokenFoo,
expected: true
},
{
desc: 'token method not authorized for invalid token',
template: makeTemplate(methodToken, [tokenFoo]),
token: tokenBar,
expected: false
},
{
desc: 'token method is authorized for valid token array',
template: makeTemplate(methodToken, [tokenFoo]),
token: [tokenFoo],
expected: true
},
{
desc: 'token method not authorized for invalid token array',
template: makeTemplate(methodToken, [tokenFoo]),
token: [tokenBar],
expected: false
},
{
desc: 'wadus method not authorized',
template: makeTemplate('wadus', [tokenFoo]),
token: tokenFoo,
expected: false
},
{
desc: 'undefined template result in not authorized',
template: undefined,
token: tokenFoo,
expected: false
},
{
desc: 'undefined template auth result in not authorized',
template: {},
token: tokenFoo,
expected: false
}
];
authorizationTestScenarios.forEach(function(testScenario) {
test(testScenario.desc, function(done) {
var debugMessage = testScenario.expected ? 'should be authorized' : 'unexpectedly authorized';
var result = templateMaps.isAuthorized(testScenario.template, testScenario.token);
assert.equal(result, testScenario.expected, debugMessage);
done();
})
});
test("auth as 'open' string is authorized", function(done) {
var template = {
name: 'wadus_template',
auth: 'open'
};
assert.ok(templateMaps.isAuthorized(template));
done();
});
});

View File

@@ -1,106 +0,0 @@
var assert = require('assert');
var RedisPool = require('redis-mpool');
var TemplateMaps = require('../../../lib/cartodb/template_maps.js');
var test_helper = require('../../support/test_helper');
var _ = require('underscore');
suite('template_maps', function() {
var redisPool = new RedisPool(global.environment.redis),
templateMaps = new TemplateMaps(redisPool);
var owner = 'me';
var templateName = 'wadus';
var defaultTemplate = {
version:'0.0.1',
name: templateName,
layergroup: {
layers: [
{
options: {
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
cartocss: '#layer { marker-fill:blue; }',
cartocss_version: '2.3.0'
}
}
]
}
};
function makeTemplate(auth, placeholders) {
return _.extend({}, defaultTemplate, {
auth: auth,
placeholders: placeholders
});
}
var defaultAuth = {
method: 'open'
};
var authTokenSample = {
method: 'token',
valid_tokens: ['wadus_token']
};
var placeholdersSample = {
wadus: {
type: 'number',
default: 1
}
};
var testScenarios = [
{
desc: 'default auth and placeholders values',
template: defaultTemplate,
expected: {
auth: defaultAuth,
placeholders: {}
}
},
{
desc: 'default placeholders but specified auth',
template: makeTemplate(authTokenSample),
expected: {
auth: authTokenSample,
placeholders: {}
}
},
{
desc: 'default auth but specified placeholders',
template: makeTemplate(undefined, placeholdersSample),
expected: {
auth: defaultAuth,
placeholders: placeholdersSample
}
},
{
desc: 'specified auth and placeholders',
template: makeTemplate(authTokenSample, placeholdersSample),
expected: {
auth: authTokenSample,
placeholders: placeholdersSample
}
}
];
testScenarios.forEach(function(testScenario) {
test('adding template returns a new instance with ' + testScenario.desc, function(done) {
templateMaps.addTemplate(owner, testScenario.template, function(err, templateId, template) {
assert.ok(!err, 'Unexpected error adding template: ' + (err && err.message));
assert.ok(testScenario.template !== template, 'template instances should be different');
assert.equal(template.name, templateName);
assert.deepEqual(template.auth, testScenario.expected.auth);
assert.deepEqual(template.placeholders, testScenario.expected.placeholders);
templateMaps.delTemplate(owner, templateName, done);
});
});
});
});

View File

@@ -1,112 +0,0 @@
var assert = require('assert');
var RedisPool = require('redis-mpool');
var TemplateMaps = require('../../../lib/cartodb/template_maps.js');
var test_helper = require('../../support/test_helper');
var _ = require('underscore');
suite('template_maps', function() {
var redisPool = new RedisPool(global.environment.redis),
templateMaps = new TemplateMaps(redisPool);
var owner = 'me';
var templateName = 'wadus';
var defaultTemplate = {
version:'0.0.1',
name: templateName
};
function makeTemplate(layers) {
var layergroup = {
layers: layers
};
return _.extend({}, defaultTemplate, {
layergroup: layergroup
});
}
var layerWithMissingOptions = {},
minimumValidLayer = {
options: {
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
cartocss: '#layer { marker-fill:blue; }',
cartocss_version: '2.3.0'
}
};
var testScenarios = [
{
desc: 'Missing layers array does not validate',
template: makeTemplate(),
expected: {
isValid: false,
message: 'Missing or empty layers array from layergroup config'
}
},
{
desc: 'Empty layers array does not validate',
template: makeTemplate([]),
expected: {
isValid: false,
message: 'Missing or empty layers array from layergroup config'
}
},
{
desc: 'Layer with missing options does not validate',
template: makeTemplate([
layerWithMissingOptions
]),
expected: {
isValid: false,
message: 'Missing `options` in layergroup config for layers: 0'
}
},
{
desc: 'Multiple layers report invalid layer',
template: makeTemplate([
minimumValidLayer,
layerWithMissingOptions
]),
expected: {
isValid: false,
message: 'Missing `options` in layergroup config for layers: 1'
}
},
{
desc: 'default auth but specified placeholders',
template: makeTemplate([
minimumValidLayer
]),
expected: {
isValid: true,
message: ''
}
}
];
testScenarios.forEach(function(testScenario) {
test(testScenario.desc, function(done) {
templateMaps.addTemplate(owner, testScenario.template, function(err) {
if (testScenario.expected.isValid) {
assert.ok(!err);
templateMaps.delTemplate(owner, templateName, done);
} else {
assert.ok(err);
assert.equal(err.message, testScenario.expected.message);
done();
}
});
});
});
});

View File

@@ -1,49 +0,0 @@
#!/bin/sh
verbose=no
tiler_url=http://dev.localhost.lan:8181/tiles/template
apikey=${CDB_APIKEY}
while test -n "$1"; do
if test "$1" = "-v"; then
verbose=yes
elif test "$1" = "-k"; then
shift
apikey="$1"
elif test "$1" = "-u"; then
shift
tiler_url="$1"
elif test -z "$cfg"; then
cfg="$1"
else
echo "Unused parameter $1" >&2
fi
shift
done
if test -z "$cfg"; then
echo "Usage: $0 [-v] [-k <api_key>] [-u <tiler_url>] <template_config>" >&2
echo "Default <tiler_url> is ${tiler_url}" >&2
echo "Default <api_key> is read from CDB_APIKEY env variable" >&2
exit 1
fi
cmd="curl -skH Content-Type:application/json --data-binary @- ${tiler_url}?api_key=${apikey}"
if test x${verbose} = xyes; then
cmd="${cmd} -v"
fi
res=`cat ${cfg} | tr '\n' ' ' | ${cmd}`
if test $? -gt 0; then
echo "curl command failed: ${cmd}"
fi
if test x${verbose} = xyes; then
echo "${res}"
fi
# Successful response contains no space
echo "$res" | grep " " && { echo $res && exit 1; }
tok=`echo "$res" | sed 's/.*"template_id":"\([^"]*\)".*/\1/'`
echo $tok

View File

@@ -1,45 +0,0 @@
#!/bin/sh
verbose=no
tiler_url=http://dev.localhost.lan:8181/tiles/template
apikey=${CDB_APIKEY}
while test -n "$1"; do
if test "$1" = "-v"; then
verbose=yes
elif test "$1" = "-k"; then
shift
apikey="$1"
elif test "$1" = "-u"; then
shift
tiler_url="$1"
elif test -z "$tpl"; then
tpl="$1"
else
echo "Unused parameter $1" >&2
fi
shift
done
if test -z "$tpl"; then
echo "Usage: $0 [-v] [-k <api_key>] [-u <tiler_url>] <template_id>" >&2
echo "Default <tiler_url> is ${tiler_url}" >&2
echo "Default <api_key> is read from CDB_APIKEY env variable" >&2
exit 1
fi
cmd="curl -X DELETE -skH Content-Type:application/json ${tiler_url}/${tpl}?api_key=${apikey}"
if test x${verbose} = xyes; then
cmd="${cmd} -v"
fi
res=`${cmd}`
if test $? -gt 0; then
echo "curl command failed: ${cmd}"
fi
if test x${verbose} = xyes; then
echo "${res}"
fi
tok=`echo "$res" | sed 's/.*"template_id":"\([^"]*\)".*/\1/'`
echo $tok

View File

@@ -1,11 +0,0 @@
{"version":"1.0.1",
"layers":[{
"type":"cartodb",
"options":{
"sql":"select 1 as id, ST_SetSRID(ST_MakePoint(0,0),3857) as the_geom_webmercator",
"cartocss":"#style{ marker-width: 12;}",
"cartocss_version":"2.1.1",
"Interactivity":"id"
}
}]
}

View File

@@ -1,11 +0,0 @@
{"version":"1.0.1",
"layers":[{
"type":"cartodb",
"options":{
"sql":"select 1 as id, ST_Transform(ST_SetSRID(ST_MakePoint(x/1000,x/2000),4326),3857) as the_geom_webmercator FROM generate_series(-170000,170000) x",
"cartocss":"#style{ marker-width: 12;}",
"cartocss_version":"2.1.1",
"Interactivity":"id"
}
}]
}

View File

@@ -1,10 +0,0 @@
{"version":"1.0.1",
"layers":[{
"type":"torque",
"options":{
"sql":"select 1 as id, ST_SetSRID(ST_MakePoint(0,0),3857) as the_geom_webmercator",
"cartocss":"Map{ -torque-time-attribute:'id'; -torque-aggregation-function:'count(id)'; -torque-frame-count:2; -torque-resolution:2}",
"cartocss_version": "2.1.1"
}
}]
}

View File

@@ -1,17 +0,0 @@
{
"version":"0.0.1",
"name":"simple",
"placeholders":{},
"auth":{ "method":"open" },
"layergroup":{
"version":"1.0.1",
"layers":[{
"type":"cartodb",
"options":{
"sql":"select ST_SetSRID(ST_MakePoint(0,0),3857) as the_geom_webmercator",
"cartocss":"#s{ marker-width: 12;}",
"cartocss_version":"2.1.1"
}
}]
}
}

View File

@@ -1,59 +0,0 @@
#!/usr/bin/env node
var path = require('path');
var request = require('request');
function usage(me, exitcode) {
console.log("Usage: " + me + " [--env <environment>] <username> <tablename>");
process.exit(exitcode);
}
var node_path = process.argv.shift();
var script_path = process.argv.shift();
var basedir = path.dirname(script_path);
var me = path.basename(script_path);
var ENV = 'development.js';
var username, table;
var arg;
while ( arg = process.argv.shift() ) {
if ( arg == '--env' ) {
ENV = process.argv.shift();
}
else if ( ! username ) {
username = arg;
}
else if ( ! table ) {
table = arg;
}
else {
console.warn("Unused parameter " + arg);
}
}
if ( ! table ) {
usage(me, 1);
}
global.environment = require('../config/environments/' + ENV);
// _after_ setting global.environment
var serverOptions = require('../lib/cartodb/server_options');
var host = global.environment.host;
var port = global.environment.port;
var re = ''+serverOptions.re_userFromHost;
var hostname = re.replace(/^\/\^/, '')
.replace(/\/$$/, '')
.replace(/\\/g,'')
.replace(/\([^)]*\)/,username)
;
//console.log("re: " + re);
//console.log("hostname: " + hostname);
var url = 'http://' + host + ':' + port + '/tiles/' + table + '/flush_cache';
request.del({ url: url, headers: { host: hostname } },
function(err, res, body) {
if ( err ) throw err;
console.log(res.body);
});

View File

@@ -1,53 +0,0 @@
#!/bin/sh
verbose=no
tiler_url=http://dev.localhost.lan:8181/tiles/template
apikey=${CDB_APIKEY}
while test -n "$1"; do
if test "$1" = "-v"; then
verbose=yes
elif test "$1" = "-k"; then
shift
apikey="$1"
elif test "$1" = "-u"; then
shift
tiler_url="$1"
elif test -z "$tpl"; then
tpl="$1"
elif test -z "$cfg"; then
cfg="$1"
else
echo "Unused parameter $1" >&2
fi
shift
done
if test -z "$tpl"; then
echo "Usage: $0 [-v] [-k <api_key>] [-u <tiler_url>] <template_id> [<template_params>]" >&2
echo "Default <tiler_url> is ${tiler_url}" >&2
echo "Default <api_key> is read from CDB_APIKEY env variable" >&2
exit 1
fi
if test -z "$cfg"; then
cfg="/dev/null"
fi
tiler_url="${tiler_url}/${tpl}"
cmd="curl -X POST -skH Content-Type:application/json --data-binary @- ${tiler_url}?api_key=${apikey}"
if test x${verbose} = xyes; then
cmd="${cmd} -v"
fi
res=`cat ${cfg} | tr '\n' ' ' | ${cmd}`
if test $? -gt 0; then
echo "curl command failed: ${cmd}"
fi
if test x${verbose} = xyes; then
echo "${res}"
fi
tok=`echo "$res" | sed 's/.*"layergroupid":"\([^"]*\)".*/\1/'`
echo $tok

View File

@@ -1,45 +0,0 @@
#!/bin/sh
verbose=no
tiler_url=http://dev.localhost.lan:8181/tiles/template
apikey=${CDB_APIKEY}
while test -n "$1"; do
if test "$1" = "-v"; then
verbose=yes
elif test "$1" = "-k"; then
shift
apikey="$1"
elif test "$1" = "-u"; then
shift
tiler_url="$1"
elif test "$1" = "-h" -o "$1" = "-?"; then
echo "Usage: $0 [-v] [-k <api_key>] [-u <tiler_url>]" >&2
echo "Default <tiler_url> is ${tiler_url}" >&2
echo "Default <api_key> is read from CDB_APIKEY env variable" >&2
exit 0
else
echo "Unused parameter $1" >&2
fi
shift
done
cmd="curl -X GET -sk ${tiler_url}?api_key=${apikey}"
if test x${verbose} = xyes; then
cmd="${cmd} -v"
fi
res=`${cmd}`
if test $? -gt 0; then
echo "curl command failed: ${cmd}"
fi
if test x${verbose} = xyes; then
echo "${res}"
fi
node <<EOF
var parsed = JSON.parse('$res');
console.dir(parsed);
EOF

View File

@@ -68,7 +68,7 @@ for pid in ${pids}; do
log=$(grep "${pid}" "${tmpreport}" | grep -w 1w | awk '{print $9}')
if test -e "${log}"; then
kill -USR2 "${pid}"
cnt=$(tac ${log} | sed -n -e '/ItemKey/p;/ RenderCache /q' | wc -l)
cnt=$(tac ${log} | sed -n -e '/ItemKey/p;/^RenderCache/q' | wc -l)
if test $cnt -gt $maxcache; then maxcache=$cnt; fi
else
# report the error...

View File

@@ -1,54 +0,0 @@
#!/bin/sh
verbose=no
tiler_url=http://dev.localhost.lan:8181/tiles/template
apikey=${CDB_APIKEY}
while test -n "$1"; do
if test "$1" = "-k"; then
shift
apikey="$1"
elif test "$1" = "-u"; then
shift
tiler_url="$1"
elif test -z "$tpl"; then
tpl="$1"
else
echo "Unused parameter $1" >&2
fi
shift
done
if test -z "$tpl"; then
echo "Usage: $0 [-v] [-k <api_key>] [-u <tiler_url>] <template_config>" >&2
echo "Default <tiler_url> is ${tiler_url}" >&2
echo "Default <api_key> is read from CDB_APIKEY env variable" >&2
exit 1
fi
basedir=$(cd $(dirname $0); cd ..; pwd)
export CDB_APIKEY=${apikey}
max=3000000
i=0
while test "$i" -le "$max"; do
tpln=`cat ${tpl} | sed "s/\"name\":\"\(.*\)\"/\"name\":\"\1${i}\"/"`
tpl_id=`echo ${tpln} | ${basedir}/create_template -u ${tiler_url} /dev/stdin`
if test $? -ne 0; then
echo $tpl_id >&2
break
fi
tpl_id=`echo ${tpln} | ${basedir}/update_template -u ${tiler_url} ${tpl_id} /dev/stdin`
if test $? -ne 0; then
echo $tpl_id >&2
break
fi
out=`${basedir}/delete_template -u ${tiler_url} ${tpl_id}`
if test $? -ne 0; then
echo $out >&2
break
fi
i=$((i+1))
if test `expr $i % 100` -eq 0; then
echo -n "."
fi
done

View File

@@ -1,148 +1,29 @@
#!/usr/bin/env node
#!/bin/sh
var path = require('path');
var redis = require('redis');
var Step = require('step');
# TODO: port to node, if you really need it
REDIS_PORT=6379 # default port
function usage(me, exitcode) {
console.log("Usage: " + me + " [--env <environment>] <username> [<tablename>|~<token>]");
process.exit(exitcode);
}
if test -z "$1"; then
echo "Usage: $0 <username> [<tablename>|~<token>]" >&2
exit 1
fi
var node_path = process.argv.shift();
var script_path = process.argv.shift();
var basedir = path.dirname(script_path);
var me = path.basename(script_path);
var ENV = 'development.js';
var username, token;
var arg;
while ( arg = process.argv.shift() ) {
if ( arg == '--env' ) {
ENV = process.argv.shift();
}
else if ( ! username ) {
username = arg;
}
else if ( ! token ) {
token = arg;
}
else {
console.warn("Unused parameter " + arg);
}
}
if ( ! username ) usage(me, 1);
console.log("Using environment " + ENV);
global.environment = require('../config/environments/' + ENV);
// _after_ setting global.environment
var serverOptions = require('../lib/cartodb/server_options')();
var client;
var dbname;
Step(
function getClient() {
client = redis.createClient(serverOptions.redis.port, serverOptions.redis.host);
client.on('connect', this);
},
function getUserMeta(err) {
if ( err ) throw err;
client.select(5);
client.hgetall('rails:users:' + username, this);
},
function readDB(err, data) {
if ( err ) throw err;
if ( ! data )
throw new Error('Username ' + username + ' unknown by redis on port '
+ serverOptions.redis.port + ' (try CARTODB/script/restore_redis?)');
//console.log("Data:"); console.dir(data);
dbname = data['database_name'];
console.log("Database name for user " + username + ": " + dbname);
client.select(0);
return null;
},
function showTokens(err) {
if ( err ) throw err;
if ( token ) return null;
var next = this;
Step(
function getTokens() {
client.keys('map_style|' + dbname + '|*', this);
},
function showTokens(err, data) {
if (err) throw err;
if ( data ) console.log(data.join('\n'));
return null;
},
function showTokensFinish(err) {
next(err);
}
);
},
function showStyle(err) {
if ( err ) throw err;
if ( ! token ) return null;
var next = this;
Step(
function getStyle() {
client.get('map_style|' + dbname + '|' + token, this);
},
function showStyle(err, data) {
if ( err ) throw err;
if ( ! data ) {
throw new Error(token + ': no such map style known by redis on port '
+ serverOptions.redis.port);
}
//console.log("data: " + data);
var x=JSON.parse(data);
printMapnikStyle(x, this);
},
function showStyleFinish(err) {
next(err);
}
);
},
function finish(err) {
if ( err ) {
console.error(err.message)
process.exit(1);
}
process.exit(0);
}
);
function printMapnikStyle(x, callback) {
console.log('style: ' + x.style);
console.log('version: ' + x.version);
var grainstore = require(basedir + '/../node_modules/windshaft/node_modules/grainstore/lib/grainstore');
var mml_store = new grainstore.MMLStore(serverOptions.redis, serverOptions.grainstore);
var builderconfig = {dbname:dbname};
if ( token.match(/^~/) ) {
builderconfig.token = token.substring(1);
} else {
builderconfig.table = token;
}
var mml_builder;
Step(
function getBuilder() {
mml_builder = mml_store.mml_builder(builderconfig, this);
},
function getXML(err, builder) {
if ( err ) throw err;
mml_builder.toXML(this);
},
function showXML(err, xml) {
if ( err ) throw err;
console.log('- XML - ');
console.log(xml);
return null;
},
function finish(err) {
callback(err);
}
);
}
username="$1"
token="$2"
dbname=`redis-cli -p ${REDIS_PORT} -n 5 hget "rails:users:${username}" "database_name"`
if test $? -ne 0; then
exit 1
fi
if test -z "${dbname}"; then
echo "Username ${username} unknown by redis (try CARTODB/script/restore_redis?)" >&2
exit 1
fi
echo "Database name for user ${username}: ${dbname}" # only if verbose?
if test -n "$token"; then
redis-cli get "map_style|${dbname}|${token}" | sed -e 's/\\n/\n/g' -e 's/\\//g'
else
redis-cli keys "map_style|${dbname}|*"
fi

View File

@@ -1,47 +0,0 @@
#!/bin/sh
verbose=no
tiler_url=http://dev.localhost.lan:8181/tiles/template
apikey=${CDB_APIKEY}
while test -n "$1"; do
if test "$1" = "-v"; then
verbose=yes
elif test "$1" = "-k"; then
shift
apikey="$1"
elif test "$1" = "-u"; then
shift
tiler_url="$1"
elif test -z "$tpl"; then
tpl="$1"
elif test -z "$cfg"; then
cfg="$1"
else
echo "Unused parameter $1" >&2
fi
shift
done
if test -z "$cfg"; then
echo "Usage: $0 [-v] [-k <api_key>] [-u <tiler_url>] <template_id> <template_config>" >&2
echo "Default <tiler_url> is ${tiler_url}" >&2
echo "Default <api_key> is read from CDB_APIKEY env variable" >&2
exit 1
fi
cmd="curl -X PUT -skH Content-Type:application/json --data-binary @- ${tiler_url}/${tpl}?api_key=${apikey}"
if test x${verbose} = xyes; then
cmd="${cmd} -v"
fi
res=`cat ${cfg} | tr '\n' ' ' | ${cmd}`
if test $? -gt 0; then
echo "curl command failed: ${cmd}"
fi
if test x${verbose} = xyes; then
echo "${res}"
fi
tok=`echo "$res" | sed 's/.*"template_id":"\([^"]*\)".*/\1/'`
echo $tok